sigs.k8s.io/external-dns@v0.14.1/provider/google/google.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package google 18 19 import ( 20 "context" 21 "fmt" 22 "sort" 23 "strings" 24 "time" 25 26 "cloud.google.com/go/compute/metadata" 27 "github.com/linki/instrumented_http" 28 log "github.com/sirupsen/logrus" 29 "golang.org/x/oauth2/google" 30 dns "google.golang.org/api/dns/v1" 31 googleapi "google.golang.org/api/googleapi" 32 "google.golang.org/api/option" 33 34 "sigs.k8s.io/external-dns/endpoint" 35 "sigs.k8s.io/external-dns/plan" 36 "sigs.k8s.io/external-dns/provider" 37 ) 38 39 const ( 40 googleRecordTTL = 300 41 ) 42 43 type managedZonesCreateCallInterface interface { 44 Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error) 45 } 46 47 type managedZonesListCallInterface interface { 48 Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error 49 } 50 51 type managedZonesServiceInterface interface { 52 Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface 53 List(project string) managedZonesListCallInterface 54 } 55 56 type resourceRecordSetsListCallInterface interface { 57 Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error 58 } 59 60 type resourceRecordSetsClientInterface interface { 61 List(project string, managedZone string) resourceRecordSetsListCallInterface 62 } 63 64 type changesCreateCallInterface interface { 65 Do(opts ...googleapi.CallOption) (*dns.Change, error) 66 } 67 68 type changesServiceInterface interface { 69 Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface 70 } 71 72 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 // GoogleProvider is an implementation of Provider for Google CloudDNS. 101 type GoogleProvider struct { 102 provider.BaseProvider 103 // The Google project to work in 104 project string 105 // Enabled dry-run will print any modifying actions rather than execute them. 106 dryRun bool 107 // Max batch size to submit to Google Cloud DNS per transaction. 108 batchChangeSize int 109 // Interval between batch updates. 110 batchChangeInterval time.Duration 111 // only consider hosted zones managing domains ending in this suffix 112 domainFilter endpoint.DomainFilter 113 // filter for zones based on visibility 114 zoneTypeFilter provider.ZoneTypeFilter 115 // only consider hosted zones ending with this zone id 116 zoneIDFilter provider.ZoneIDFilter 117 // A client for managing resource record sets 118 resourceRecordSetsClient resourceRecordSetsClientInterface 119 // A client for managing hosted zones 120 managedZonesClient managedZonesServiceInterface 121 // A client for managing change sets 122 changesClient changesServiceInterface 123 // The context parameter to be passed for gcloud API calls. 124 ctx context.Context 125 } 126 127 // NewGoogleProvider initializes a new Google CloudDNS based Provider. 128 func NewGoogleProvider(ctx context.Context, project string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, batchChangeSize int, batchChangeInterval time.Duration, zoneVisibility string, dryRun bool) (*GoogleProvider, error) { 129 gcloud, err := google.DefaultClient(ctx, dns.NdevClouddnsReadwriteScope) 130 if err != nil { 131 return nil, err 132 } 133 134 gcloud = instrumented_http.NewClient(gcloud, &instrumented_http.Callbacks{ 135 PathProcessor: func(path string) string { 136 parts := strings.Split(path, "/") 137 return parts[len(parts)-1] 138 }, 139 }) 140 141 dnsClient, err := dns.NewService(ctx, option.WithHTTPClient(gcloud)) 142 if err != nil { 143 return nil, err 144 } 145 146 if project == "" { 147 mProject, mErr := metadata.ProjectID() 148 if mErr != nil { 149 return nil, fmt.Errorf("failed to auto-detect the project id: %w", mErr) 150 } 151 log.Infof("Google project auto-detected: %s", mProject) 152 project = mProject 153 } 154 155 zoneTypeFilter := provider.NewZoneTypeFilter(zoneVisibility) 156 157 provider := &GoogleProvider{ 158 project: project, 159 dryRun: dryRun, 160 batchChangeSize: batchChangeSize, 161 batchChangeInterval: batchChangeInterval, 162 domainFilter: domainFilter, 163 zoneTypeFilter: zoneTypeFilter, 164 zoneIDFilter: zoneIDFilter, 165 resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets}, 166 managedZonesClient: managedZonesService{dnsClient.ManagedZones}, 167 changesClient: changesService{dnsClient.Changes}, 168 ctx: ctx, 169 } 170 171 return provider, nil 172 } 173 174 // Zones returns the list of hosted zones. 175 func (p *GoogleProvider) Zones(ctx context.Context) (map[string]*dns.ManagedZone, error) { 176 zones := make(map[string]*dns.ManagedZone) 177 178 f := func(resp *dns.ManagedZonesListResponse) error { 179 for _, zone := range resp.ManagedZones { 180 if zone.PeeringConfig == nil { 181 if p.domainFilter.Match(zone.DnsName) && p.zoneTypeFilter.Match(zone.Visibility) && (p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Id)) || p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Name))) { 182 zones[zone.Name] = zone 183 log.Debugf("Matched %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility) 184 } else { 185 log.Debugf("Filtered %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility) 186 } 187 } else { 188 log.Debugf("Filtered peering zone %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility) 189 } 190 } 191 192 return nil 193 } 194 195 log.Debugf("Matching zones against domain filters: %v", p.domainFilter) 196 if err := p.managedZonesClient.List(p.project).Pages(ctx, f); err != nil { 197 return nil, err 198 } 199 200 if len(zones) == 0 { 201 log.Warnf("No zones in the project, %s, match domain filters: %v", p.project, p.domainFilter) 202 } 203 204 for _, zone := range zones { 205 log.Debugf("Considering zone: %s (domain: %s)", zone.Name, zone.DnsName) 206 } 207 208 return zones, nil 209 } 210 211 // Records returns the list of records in all relevant zones. 212 func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { 213 zones, err := p.Zones(ctx) 214 if err != nil { 215 return nil, err 216 } 217 218 f := func(resp *dns.ResourceRecordSetsListResponse) error { 219 for _, r := range resp.Rrsets { 220 if !p.SupportedRecordType(r.Type) { 221 continue 222 } 223 endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), r.Rrdatas...)) 224 } 225 226 return nil 227 } 228 229 for _, z := range zones { 230 if err := p.resourceRecordSetsClient.List(p.project, z.Name).Pages(ctx, f); err != nil { 231 return nil, err 232 } 233 } 234 235 return endpoints, nil 236 } 237 238 // ApplyChanges applies a given set of changes in a given zone. 239 func (p *GoogleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 240 change := &dns.Change{} 241 242 change.Additions = append(change.Additions, p.newFilteredRecords(changes.Create)...) 243 244 change.Additions = append(change.Additions, p.newFilteredRecords(changes.UpdateNew)...) 245 change.Deletions = append(change.Deletions, p.newFilteredRecords(changes.UpdateOld)...) 246 247 change.Deletions = append(change.Deletions, p.newFilteredRecords(changes.Delete)...) 248 249 return p.submitChange(ctx, change) 250 } 251 252 // SupportedRecordType returns true if the record type is supported by the provider 253 func (p *GoogleProvider) SupportedRecordType(recordType string) bool { 254 switch recordType { 255 case "MX": 256 return true 257 default: 258 return provider.SupportedRecordType(recordType) 259 } 260 } 261 262 // newFilteredRecords returns a collection of RecordSets based on the given endpoints and domainFilter. 263 func (p *GoogleProvider) newFilteredRecords(endpoints []*endpoint.Endpoint) []*dns.ResourceRecordSet { 264 records := []*dns.ResourceRecordSet{} 265 266 for _, endpoint := range endpoints { 267 if p.domainFilter.Match(endpoint.DNSName) { 268 records = append(records, newRecord(endpoint)) 269 } 270 } 271 272 return records 273 } 274 275 // submitChange takes a zone and a Change and sends it to Google. 276 func (p *GoogleProvider) submitChange(ctx context.Context, change *dns.Change) error { 277 if len(change.Additions) == 0 && len(change.Deletions) == 0 { 278 log.Info("All records are already up to date") 279 return nil 280 } 281 282 zones, err := p.Zones(ctx) 283 if err != nil { 284 return err 285 } 286 287 // separate into per-zone change sets to be passed to the API. 288 changes := separateChange(zones, change) 289 290 for zone, change := range changes { 291 for batch, c := range batchChange(change, p.batchChangeSize) { 292 log.Infof("Change zone: %v batch #%d", zone, batch) 293 for _, del := range c.Deletions { 294 log.Infof("Del records: %s %s %s %d", del.Name, del.Type, del.Rrdatas, del.Ttl) 295 } 296 for _, add := range c.Additions { 297 log.Infof("Add records: %s %s %s %d", add.Name, add.Type, add.Rrdatas, add.Ttl) 298 } 299 300 if p.dryRun { 301 continue 302 } 303 304 if _, err := p.changesClient.Create(p.project, zone, c).Do(); err != nil { 305 return err 306 } 307 308 time.Sleep(p.batchChangeInterval) 309 } 310 } 311 312 return nil 313 } 314 315 // batchChange separates a zone in multiple transaction. 316 func batchChange(change *dns.Change, batchSize int) []*dns.Change { 317 changes := []*dns.Change{} 318 319 if batchSize == 0 { 320 return append(changes, change) 321 } 322 323 type dnsChange struct { 324 additions []*dns.ResourceRecordSet 325 deletions []*dns.ResourceRecordSet 326 } 327 328 changesByName := map[string]*dnsChange{} 329 330 for _, a := range change.Additions { 331 change, ok := changesByName[a.Name] 332 if !ok { 333 change = &dnsChange{} 334 changesByName[a.Name] = change 335 } 336 337 change.additions = append(change.additions, a) 338 } 339 340 for _, a := range change.Deletions { 341 change, ok := changesByName[a.Name] 342 if !ok { 343 change = &dnsChange{} 344 changesByName[a.Name] = change 345 } 346 347 change.deletions = append(change.deletions, a) 348 } 349 350 names := make([]string, 0) 351 for v := range changesByName { 352 names = append(names, v) 353 } 354 sort.Strings(names) 355 356 currentChange := &dns.Change{} 357 var totalChanges int 358 for _, name := range names { 359 c := changesByName[name] 360 361 totalChangesByName := len(c.additions) + len(c.deletions) 362 363 if totalChangesByName > batchSize { 364 log.Warnf("Total changes for %s exceeds max batch size of %d, total changes: %d", name, 365 batchSize, totalChangesByName) 366 continue 367 } 368 369 if totalChanges+totalChangesByName > batchSize { 370 totalChanges = 0 371 changes = append(changes, currentChange) 372 currentChange = &dns.Change{} 373 } 374 375 currentChange.Additions = append(currentChange.Additions, c.additions...) 376 currentChange.Deletions = append(currentChange.Deletions, c.deletions...) 377 378 totalChanges += totalChangesByName 379 } 380 381 if totalChanges > 0 { 382 changes = append(changes, currentChange) 383 } 384 385 return changes 386 } 387 388 // separateChange separates a multi-zone change into a single change per zone. 389 func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[string]*dns.Change { 390 changes := make(map[string]*dns.Change) 391 zoneNameIDMapper := provider.ZoneIDName{} 392 for _, z := range zones { 393 zoneNameIDMapper[z.Name] = z.DnsName 394 changes[z.Name] = &dns.Change{ 395 Additions: []*dns.ResourceRecordSet{}, 396 Deletions: []*dns.ResourceRecordSet{}, 397 } 398 } 399 for _, a := range change.Additions { 400 if zoneName, _ := zoneNameIDMapper.FindZone(provider.EnsureTrailingDot(a.Name)); zoneName != "" { 401 changes[zoneName].Additions = append(changes[zoneName].Additions, a) 402 } else { 403 log.Warnf("No matching zone for record addition: %s %s %s %d", a.Name, a.Type, a.Rrdatas, a.Ttl) 404 } 405 } 406 407 for _, d := range change.Deletions { 408 if zoneName, _ := zoneNameIDMapper.FindZone(provider.EnsureTrailingDot(d.Name)); zoneName != "" { 409 changes[zoneName].Deletions = append(changes[zoneName].Deletions, d) 410 } else { 411 log.Warnf("No matching zone for record deletion: %s %s %s %d", d.Name, d.Type, d.Rrdatas, d.Ttl) 412 } 413 } 414 415 // separating a change could lead to empty sub changes, remove them here. 416 for zone, change := range changes { 417 if len(change.Additions) == 0 && len(change.Deletions) == 0 { 418 delete(changes, zone) 419 } 420 } 421 422 return changes 423 } 424 425 // newRecord returns a RecordSet based on the given endpoint. 426 func newRecord(ep *endpoint.Endpoint) *dns.ResourceRecordSet { 427 // TODO(linki): works around appending a trailing dot to TXT records. I think 428 // we should go back to storing DNS names with a trailing dot internally. This 429 // way we can use it has is here and trim it off if it exists when necessary. 430 targets := make([]string, len(ep.Targets)) 431 copy(targets, []string(ep.Targets)) 432 if ep.RecordType == endpoint.RecordTypeCNAME { 433 targets[0] = provider.EnsureTrailingDot(targets[0]) 434 } 435 436 if ep.RecordType == endpoint.RecordTypeMX { 437 for i, mxRecord := range ep.Targets { 438 targets[i] = provider.EnsureTrailingDot(mxRecord) 439 } 440 } 441 442 if ep.RecordType == endpoint.RecordTypeSRV { 443 for i, srvRecord := range ep.Targets { 444 targets[i] = provider.EnsureTrailingDot(srvRecord) 445 } 446 } 447 448 // no annotation results in a Ttl of 0, default to 300 for backwards-compatibility 449 var ttl int64 = googleRecordTTL 450 if ep.RecordTTL.IsConfigured() { 451 ttl = int64(ep.RecordTTL) 452 } 453 454 return &dns.ResourceRecordSet{ 455 Name: provider.EnsureTrailingDot(ep.DNSName), 456 Rrdatas: targets, 457 Ttl: ttl, 458 Type: ep.RecordType, 459 } 460 }