google.go 11.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

17
package provider
18 19

import (
20
	"fmt"
21
	"sort"
22 23
	"strings"

24
	"cloud.google.com/go/compute/metadata"
25
	"github.com/linki/instrumented_http"
26
	log "github.com/sirupsen/logrus"
27

28 29
	dns "google.golang.org/api/dns/v1"

30
	"golang.org/x/net/context"
31
	"golang.org/x/oauth2/google"
32

33
	googleapi "google.golang.org/api/googleapi"
34

35
	"github.com/kubernetes-incubator/external-dns/endpoint"
36 37 38
	"github.com/kubernetes-incubator/external-dns/plan"
)

39 40 41 42
const (
	googleRecordTTL = 300
)

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
type managedZonesCreateCallInterface interface {
	Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error)
}

type managedZonesListCallInterface interface {
	Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error
}

type managedZonesServiceInterface interface {
	Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface
	List(project string) managedZonesListCallInterface
}

type resourceRecordSetsListCallInterface interface {
	Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error
}

type resourceRecordSetsClientInterface interface {
	List(project string, managedZone string) resourceRecordSetsListCallInterface
}

type changesCreateCallInterface interface {
	Do(opts ...googleapi.CallOption) (*dns.Change, error)
}

type changesServiceInterface interface {
	Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface
}

type resourceRecordSetsService struct {
73
	service *dns.ResourceRecordSetsService
74 75 76
}

func (r resourceRecordSetsService) List(project string, managedZone string) resourceRecordSetsListCallInterface {
77
	return r.service.List(project, managedZone)
78 79 80
}

type managedZonesService struct {
81
	service *dns.ManagedZonesService
82 83 84
}

func (m managedZonesService) Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface {
85
	return m.service.Create(project, managedzone)
86 87 88
}

func (m managedZonesService) List(project string) managedZonesListCallInterface {
89
	return m.service.List(project)
90 91 92
}

type changesService struct {
93
	service *dns.ChangesService
94 95 96
}

func (c changesService) Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface {
97
	return c.service.Create(project, managedZone, change)
98 99
}

100 101
// GoogleProvider is an implementation of Provider for Google CloudDNS.
type GoogleProvider struct {
102
	// The Google project to work in
103
	project string
104
	// Enabled dry-run will print any modifying actions rather than execute them.
105
	dryRun bool
106
	// only consider hosted zones managing domains ending in this suffix
107
	domainFilter DomainFilter
108 109
	// only consider hosted zones ending with this zone id
	zoneIDFilter ZoneIDFilter
110
	// A client for managing resource record sets
111
	resourceRecordSetsClient resourceRecordSetsClientInterface
112
	// A client for managing hosted zones
113
	managedZonesClient managedZonesServiceInterface
114
	// A client for managing change sets
115 116 117
	changesClient changesServiceInterface
}

118
// NewGoogleProvider initializes a new Google CloudDNS based Provider.
119
func NewGoogleProvider(project string, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) (*GoogleProvider, error) {
120 121 122 123 124
	gcloud, err := google.DefaultClient(context.TODO(), dns.NdevClouddnsReadwriteScope)
	if err != nil {
		return nil, err
	}

125 126 127 128 129 130 131
	gcloud = instrumented_http.NewClient(gcloud, &instrumented_http.Callbacks{
		PathProcessor: func(path string) string {
			parts := strings.Split(path, "/")
			return parts[len(parts)-1]
		},
	})

132 133 134 135 136
	dnsClient, err := dns.New(gcloud)
	if err != nil {
		return nil, err
	}

137 138 139 140 141 142 143 144
	if project == "" {
		mProject, mErr := metadata.ProjectID()
		if mErr == nil {
			log.Infof("Google project auto-detected: %s", mProject)
			project = mProject
		}
	}

145
	provider := &GoogleProvider{
146 147
		project:      project,
		domainFilter: domainFilter,
148
		zoneIDFilter: zoneIDFilter,
149
		dryRun:       dryRun,
150 151 152
		resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets},
		managedZonesClient:       managedZonesService{dnsClient.ManagedZones},
		changesClient:            changesService{dnsClient.Changes},
153 154 155
	}

	return provider, nil
156 157 158
}

// Zones returns the list of hosted zones.
159
func (p *GoogleProvider) Zones() (map[string]*dns.ManagedZone, error) {
160 161
	zones := make(map[string]*dns.ManagedZone)

162
	f := func(resp *dns.ManagedZonesListResponse) error {
163
		for _, zone := range resp.ManagedZones {
164 165 166 167 168
			if p.domainFilter.Match(zone.DnsName) || p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Id)) {
				zones[zone.Name] = zone
				log.Debugf("Matched %s (zone: %s)", zone.DnsName, zone.Name)
			} else {
				log.Debugf("Filtered %s (zone: %s)", zone.DnsName, zone.Name)
169 170 171
			}
		}

172 173 174
		return nil
	}

175
	log.Debugf("Matching zones against domain filters: %v", p.domainFilter.filters)
176
	if err := p.managedZonesClient.List(p.project).Pages(context.TODO(), f); err != nil {
177 178 179
		return nil, err
	}

180 181 182 183 184 185 186 187
	if len(zones) == 0 {
		if p.domainFilter.IsConfigured() {
			log.Warnf("No zones in the project, %s, match domain filters: %v", p.project, p.domainFilter.filters)
		} else {
			log.Warnf("No zones found in the project, %s", p.project)
		}
	}

188 189 190 191
	for _, zone := range zones {
		log.Debugf("Considering zone: %s (domain: %s)", zone.Name, zone.DnsName)
	}

192
	return zones, nil
193 194
}

195
// Records returns the list of records in all relevant zones.
196
func (p *GoogleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
197
	zones, err := p.Zones()
198
	if err != nil {
199
		return nil, err
200 201
	}

202 203
	f := func(resp *dns.ResourceRecordSetsListResponse) error {
		for _, r := range resp.Rrsets {
204 205 206 207 208 209 210 211
			if !supportedRecordType(r.Type) {
				continue
			}
			ep := &endpoint.Endpoint{
				DNSName:    strings.TrimSuffix(r.Name, "."),
				RecordType: r.Type,
				Targets:    make(endpoint.Targets, 0, len(r.Rrdatas)),
			}
212 213
			for _, rr := range r.Rrdatas {
				// each page is processed sequentially, no need for a mutex here.
214
				ep.Targets = append(ep.Targets, strings.TrimSuffix(rr, "."))
215
			}
216 217
			sort.Sort(ep.Targets)
			endpoints = append(endpoints, ep)
218 219
		}

220 221 222
		return nil
	}

223 224 225 226
	for _, z := range zones {
		if err := p.resourceRecordSetsClient.List(p.project, z.Name).Pages(context.TODO(), f); err != nil {
			return nil, err
		}
227 228 229
	}

	return endpoints, nil
230 231
}

232
// CreateRecords creates a given set of DNS records in the given hosted zone.
233
func (p *GoogleProvider) CreateRecords(endpoints []*endpoint.Endpoint) error {
234
	change := &dns.Change{}
235

236
	change.Additions = append(change.Additions, p.newFilteredRecords(endpoints)...)
237

238
	return p.submitChange(change)
239
}
240

241
// UpdateRecords updates a given set of old records to a new set of records in a given hosted zone.
242
func (p *GoogleProvider) UpdateRecords(records, oldRecords []*endpoint.Endpoint) error {
243
	change := &dns.Change{}
244

245 246
	change.Additions = append(change.Additions, p.newFilteredRecords(records)...)
	change.Deletions = append(change.Deletions, p.newFilteredRecords(oldRecords)...)
247

248
	return p.submitChange(change)
249 250
}

251
// DeleteRecords deletes a given set of DNS records in a given zone.
252
func (p *GoogleProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error {
253
	change := &dns.Change{}
254

255
	change.Deletions = append(change.Deletions, p.newFilteredRecords(endpoints)...)
256

257
	return p.submitChange(change)
258
}
259

260
// ApplyChanges applies a given set of changes in a given zone.
261
func (p *GoogleProvider) ApplyChanges(changes *plan.Changes) error {
262
	change := &dns.Change{}
263

264
	change.Additions = append(change.Additions, p.newFilteredRecords(changes.Create)...)
265

266 267
	change.Additions = append(change.Additions, p.newFilteredRecords(changes.UpdateNew)...)
	change.Deletions = append(change.Deletions, p.newFilteredRecords(changes.UpdateOld)...)
268

269
	change.Deletions = append(change.Deletions, p.newFilteredRecords(changes.Delete)...)
270

271
	return p.submitChange(change)
272 273
}

274 275 276 277 278 279 280 281 282 283 284 285 286
// newFilteredRecords returns a collection of RecordSets based on the given endpoints and domainFilter.
func (p *GoogleProvider) newFilteredRecords(endpoints []*endpoint.Endpoint) []*dns.ResourceRecordSet {
	records := []*dns.ResourceRecordSet{}

	for _, endpoint := range endpoints {
		if p.domainFilter.Match(endpoint.DNSName) {
			records = append(records, newRecord(endpoint))
		}
	}

	return records
}

287
// submitChange takes a zone and a Change and sends it to Google.
288
func (p *GoogleProvider) submitChange(change *dns.Change) error {
289
	if len(change.Additions) == 0 && len(change.Deletions) == 0 {
Yerken's avatar
Yerken committed
290
		log.Info("All records are already up to date")
291
		return nil
292 293
	}

294 295 296 297 298 299 300 301
	zones, err := p.Zones()
	if err != nil {
		return err
	}

	// separate into per-zone change sets to be passed to the API.
	changes := separateChange(zones, change)

302 303 304 305 306 307 308 309 310 311 312 313 314 315
	for z, c := range changes {
		log.Infof("Change zone: %v", z)
		for _, del := range c.Deletions {
			log.Infof("Del records: %s %s %s %d", del.Name, del.Type, del.Rrdatas, del.Ttl)
		}
		for _, add := range c.Additions {
			log.Infof("Add records: %s %s %s %d", add.Name, add.Type, add.Rrdatas, add.Ttl)
		}
	}

	if p.dryRun {
		return nil
	}

316 317
	for z, c := range changes {
		if _, err := p.changesClient.Create(p.project, z, c).Do(); err != nil {
318 319
			return err
		}
320 321 322 323 324
	}

	return nil
}

325 326 327
// separateChange separates a multi-zone change into a single change per zone.
func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[string]*dns.Change {
	changes := make(map[string]*dns.Change)
328
	zoneNameIDMapper := zoneIDName{}
329
	for _, z := range zones {
330
		zoneNameIDMapper[z.Name] = z.DnsName
331 332 333 334 335 336
		changes[z.Name] = &dns.Change{
			Additions: []*dns.ResourceRecordSet{},
			Deletions: []*dns.ResourceRecordSet{},
		}
	}
	for _, a := range change.Additions {
337 338
		if zoneName, _ := zoneNameIDMapper.FindZone(ensureTrailingDot(a.Name)); zoneName != "" {
			changes[zoneName].Additions = append(changes[zoneName].Additions, a)
339 340
		} else {
			log.Warnf("No matching zone for record addition: %s %s %s %d", a.Name, a.Type, a.Rrdatas, a.Ttl)
341 342 343 344
		}
	}

	for _, d := range change.Deletions {
345 346
		if zoneName, _ := zoneNameIDMapper.FindZone(ensureTrailingDot(d.Name)); zoneName != "" {
			changes[zoneName].Deletions = append(changes[zoneName].Deletions, d)
347 348
		} else {
			log.Warnf("No matching zone for record deletion: %s %s %s %d", d.Name, d.Type, d.Rrdatas, d.Ttl)
349 350 351 352 353 354 355 356 357 358 359 360 361
		}
	}

	// separating a change could lead to empty sub changes, remove them here.
	for zone, change := range changes {
		if len(change.Additions) == 0 && len(change.Deletions) == 0 {
			delete(changes, zone)
		}
	}

	return changes
}

362
// newRecord returns a RecordSet based on the given endpoint.
363
func newRecord(ep *endpoint.Endpoint) *dns.ResourceRecordSet {
364 365 366
	// TODO(linki): works around appending a trailing dot to TXT records. I think
	// we should go back to storing DNS names with a trailing dot internally. This
	// way we can use it has is here and trim it off if it exists when necessary.
367 368
	targets := make([]string, len(ep.Targets))
	copy(targets, []string(ep.Targets))
369
	if ep.RecordType == endpoint.RecordTypeCNAME {
370
		targets[0] = ensureTrailingDot(targets[0])
371 372
	}

373 374 375 376 377 378
	// no annotation results in a Ttl of 0, default to 300 for backwards-compatability
	var ttl int64 = googleRecordTTL
	if ep.RecordTTL.IsConfigured() {
		ttl = int64(ep.RecordTTL)
	}

379
	return &dns.ResourceRecordSet{
380
		Name:    ensureTrailingDot(ep.DNSName),
381
		Rrdatas: targets,
382
		Ttl:     ttl,
383
		Type:    ep.RecordType,
384
	}
385
}