sigs.k8s.io/external-dns@v0.14.1/provider/aws/aws.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 aws 18 19 import ( 20 "context" 21 "fmt" 22 "sort" 23 "strconv" 24 "strings" 25 "time" 26 27 "github.com/aws/aws-sdk-go/aws" 28 "github.com/aws/aws-sdk-go/aws/request" 29 "github.com/aws/aws-sdk-go/service/route53" 30 log "github.com/sirupsen/logrus" 31 32 "sigs.k8s.io/external-dns/endpoint" 33 "sigs.k8s.io/external-dns/plan" 34 "sigs.k8s.io/external-dns/provider" 35 ) 36 37 const ( 38 recordTTL = 300 39 // From the experiments, it seems that the default MaxItems applied is 100, 40 // and that, on the server side, there is a hard limit of 300 elements per page. 41 // After a discussion with AWS representants, clients should accept 42 // when less items are returned, and still paginate accordingly. 43 // As we are using the standard AWS client, this should already be compliant. 44 // Hence, ifever AWS decides to raise this limit, we will automatically reduce the pressure on rate limits 45 route53PageSize = "300" 46 // providerSpecificAlias specifies whether a CNAME endpoint maps to an AWS ALIAS record. 47 providerSpecificAlias = "alias" 48 providerSpecificTargetHostedZone = "aws/target-hosted-zone" 49 // providerSpecificEvaluateTargetHealth specifies whether an AWS ALIAS record 50 // has the EvaluateTargetHealth field set to true. Present iff the endpoint 51 // has a `providerSpecificAlias` value of `true`. 52 providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health" 53 providerSpecificWeight = "aws/weight" 54 providerSpecificRegion = "aws/region" 55 providerSpecificFailover = "aws/failover" 56 providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code" 57 providerSpecificGeolocationCountryCode = "aws/geolocation-country-code" 58 providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code" 59 providerSpecificMultiValueAnswer = "aws/multi-value-answer" 60 providerSpecificHealthCheckID = "aws/health-check-id" 61 sameZoneAlias = "same-zone" 62 ) 63 64 // see: https://docs.aws.amazon.com/general/latest/gr/elb.html 65 var canonicalHostedZones = map[string]string{ 66 // Application Load Balancers and Classic Load Balancers 67 "us-east-2.elb.amazonaws.com": "Z3AADJGX6KTTL2", 68 "us-east-1.elb.amazonaws.com": "Z35SXDOTRQ7X7K", 69 "us-west-1.elb.amazonaws.com": "Z368ELLRRE2KJ0", 70 "us-west-2.elb.amazonaws.com": "Z1H1FL5HABSF5", 71 "ca-central-1.elb.amazonaws.com": "ZQSVJUPU6J1EY", 72 "ap-east-1.elb.amazonaws.com": "Z3DQVH9N71FHZ0", 73 "ap-south-1.elb.amazonaws.com": "ZP97RAFLXTNZK", 74 "ap-south-2.elb.amazonaws.com": "Z0173938T07WNTVAEPZN", 75 "ap-northeast-2.elb.amazonaws.com": "ZWKZPGTI48KDX", 76 "ap-northeast-3.elb.amazonaws.com": "Z5LXEXXYW11ES", 77 "ap-southeast-1.elb.amazonaws.com": "Z1LMS91P8CMLE5", 78 "ap-southeast-2.elb.amazonaws.com": "Z1GM3OXH4ZPM65", 79 "ap-southeast-3.elb.amazonaws.com": "Z08888821HLRG5A9ZRTER", 80 "ap-southeast-4.elb.amazonaws.com": "Z09517862IB2WZLPXG76F", 81 "ap-northeast-1.elb.amazonaws.com": "Z14GRHDCWA56QT", 82 "eu-central-1.elb.amazonaws.com": "Z215JYRZR1TBD5", 83 "eu-central-2.elb.amazonaws.com": "Z06391101F2ZOEP8P5EB3", 84 "eu-west-1.elb.amazonaws.com": "Z32O12XQLNTSW2", 85 "eu-west-2.elb.amazonaws.com": "ZHURV8PSTC4K8", 86 "eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4", 87 "eu-north-1.elb.amazonaws.com": "Z23TAZ6LKFMNIO", 88 "eu-south-1.elb.amazonaws.com": "Z3ULH7SSC9OV64", 89 "eu-south-2.elb.amazonaws.com": "Z0956581394HF5D5LXGAP", 90 "sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU", 91 "cn-north-1.elb.amazonaws.com.cn": "Z1GDH35T77C1KE", 92 "cn-northwest-1.elb.amazonaws.com.cn": "ZM7IZAIOVVDZF", 93 "us-gov-west-1.elb.amazonaws.com": "Z33AYJ8TM3BH4J", 94 "us-gov-east-1.elb.amazonaws.com": "Z166TLBEWOO7G0", 95 "me-central-1.elb.amazonaws.com": "Z08230872XQRWHG2XF6I", 96 "me-south-1.elb.amazonaws.com": "ZS929ML54UICD", 97 "af-south-1.elb.amazonaws.com": "Z268VQBMOI5EKX", 98 "il-central-1.elb.amazonaws.com": "Z09170902867EHPV2DABU", 99 // Network Load Balancers 100 "elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP", 101 "elb.us-east-1.amazonaws.com": "Z26RNL4JYFTOTI", 102 "elb.us-west-1.amazonaws.com": "Z24FKFUX50B4VW", 103 "elb.us-west-2.amazonaws.com": "Z18D5FSROUN65G", 104 "elb.ca-central-1.amazonaws.com": "Z2EPGBW3API2WT", 105 "elb.ap-east-1.amazonaws.com": "Z12Y7K3UBGUAD1", 106 "elb.ap-south-1.amazonaws.com": "ZVDDRBQ08TROA", 107 "elb.ap-south-2.amazonaws.com": "Z0711778386UTO08407HT", 108 "elb.ap-northeast-3.amazonaws.com": "Z1GWIQ4HH19I5X", 109 "elb.ap-northeast-2.amazonaws.com": "ZIBE1TIR4HY56", 110 "elb.ap-southeast-1.amazonaws.com": "ZKVM4W9LS7TM", 111 "elb.ap-southeast-2.amazonaws.com": "ZCT6FZBF4DROD", 112 "elb.ap-southeast-3.amazonaws.com": "Z01971771FYVNCOVWJU1G", 113 "elb.ap-southeast-4.amazonaws.com": "Z01156963G8MIIL7X90IV", 114 "elb.ap-northeast-1.amazonaws.com": "Z31USIVHYNEOWT", 115 "elb.eu-central-1.amazonaws.com": "Z3F0SRJ5LGBH90", 116 "elb.eu-central-2.amazonaws.com": "Z02239872DOALSIDCX66S", 117 "elb.eu-west-1.amazonaws.com": "Z2IFOLAFXWLO4F", 118 "elb.eu-west-2.amazonaws.com": "ZD4D7Y8KGAS4G", 119 "elb.eu-west-3.amazonaws.com": "Z1CMS0P5QUZ6D5", 120 "elb.eu-north-1.amazonaws.com": "Z1UDT6IFJ4EJM", 121 "elb.eu-south-1.amazonaws.com": "Z23146JA1KNAFP", 122 "elb.eu-south-2.amazonaws.com": "Z1011216NVTVYADP1SSV", 123 "elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU", 124 "elb.cn-north-1.amazonaws.com.cn": "Z3QFB96KMJ7ED6", 125 "elb.cn-northwest-1.amazonaws.com.cn": "ZQEIKTCZ8352D", 126 "elb.us-gov-west-1.amazonaws.com": "ZMG1MZ2THAWF1", 127 "elb.us-gov-east-1.amazonaws.com": "Z1ZSMQQ6Q24QQ8", 128 "elb.me-central-1.amazonaws.com": "Z00282643NTTLPANJJG2P", 129 "elb.me-south-1.amazonaws.com": "Z3QSRYVP46NYYV", 130 "elb.af-south-1.amazonaws.com": "Z203XCE67M25HM", 131 "elb.il-central-1.amazonaws.com": "Z0313266YDI6ZRHTGQY4", 132 // Global Accelerator 133 "awsglobalaccelerator.com": "Z2BJ6XQ5FK7U4H", 134 // Cloudfront and AWS API Gateway edge-optimized endpoints 135 "cloudfront.net": "Z2FDTNDATAQYW2", 136 // VPC Endpoint (PrivateLink) 137 "eu-west-2.vpce.amazonaws.com": "Z7K1066E3PUKB", 138 "us-east-2.vpce.amazonaws.com": "ZC8PG0KIFKBRI", 139 "af-south-1.vpce.amazonaws.com": "Z09302161J80N9A7UTP7U", 140 "ap-east-1.vpce.amazonaws.com": "Z2LIHJ7PKBEMWN", 141 "ap-northeast-1.vpce.amazonaws.com": "Z2E726K9Y6RL4W", 142 "ap-northeast-2.vpce.amazonaws.com": "Z27UANNT0PRK1T", 143 "ap-northeast-3.vpce.amazonaws.com": "Z376B5OMM2JZL2", 144 "ap-south-1.vpce.amazonaws.com": "Z2KVTB3ZLFM7JR", 145 "ap-south-2.vpce.amazonaws.com": "Z0952991RWSF5AHIQDIY", 146 "ap-southeast-1.vpce.amazonaws.com": "Z18LLCSTV4NVNL", 147 "ap-southeast-2.vpce.amazonaws.com": "ZDK2GCRPAFKGO", 148 "ap-southeast-3.vpce.amazonaws.com": "Z03881013RZ9BYYZO8N5W", 149 "ap-southeast-4.vpce.amazonaws.com": "Z07508191CO1RNBX3X3AU", 150 "ca-central-1.vpce.amazonaws.com": "ZRCXCF510Y6P9", 151 "eu-central-1.vpce.amazonaws.com": "Z273ZU8SZ5RJPC", 152 "eu-central-2.vpce.amazonaws.com": "Z045369019J4FUQ4S272E", 153 "eu-north-1.vpce.amazonaws.com": "Z3OWWK6JFDEDGC", 154 "eu-south-1.vpce.amazonaws.com": "Z2A5FDNRLY7KZG", 155 "eu-south-2.vpce.amazonaws.com": "Z014396544HENR57XQCJ", 156 "eu-west-1.vpce.amazonaws.com": "Z38GZ743OKFT7T", 157 "eu-west-3.vpce.amazonaws.com": "Z1DWHTMFP0WECP", 158 "me-central-1.vpce.amazonaws.com": "Z07122992YCEUCB9A9570", 159 "me-south-1.vpce.amazonaws.com": "Z3B95P3VBGEQGY", 160 "sa-east-1.vpce.amazonaws.com": "Z2LXUWEVLCVZIB", 161 "us-east-1.vpce.amazonaws.com": "Z7HUB22UULQXV", 162 "us-gov-east-1.vpce.amazonaws.com": "Z2MU5TEIGO9WXB", 163 "us-gov-west-1.vpce.amazonaws.com": "Z12529ZODG2B6H", 164 "us-west-1.vpce.amazonaws.com": "Z12I86A8N7VCZO", 165 "us-west-2.vpce.amazonaws.com": "Z1YSA3EXCYUU9Z", 166 // AWS API Gateway (Regional endpoints) 167 // See: https://docs.aws.amazon.com/general/latest/gr/apigateway.html 168 "execute-api.us-east-2.amazonaws.com": "ZOJJZC49E0EPZ", 169 "execute-api.us-east-1.amazonaws.com": "Z1UJRXOUMOOFQ8", 170 "execute-api.us-west-1.amazonaws.com": "Z2MUQ32089INYE", 171 "execute-api.us-west-2.amazonaws.com": "Z2OJLYMUO9EFXC", 172 "execute-api.af-south-1.amazonaws.com": "Z2DHW2332DAMTN", 173 "execute-api.ap-east-1.amazonaws.com": "Z3FD1VL90ND7K5", 174 "execute-api.ap-south-1.amazonaws.com": "Z3VO1THU9YC4UR", 175 "execute-api.ap-northeast-2.amazonaws.com": "Z20JF4UZKIW1U8", 176 "execute-api.ap-southeast-1.amazonaws.com": "ZL327KTPIQFUL", 177 "execute-api.ap-southeast-2.amazonaws.com": "Z2RPCDW04V8134", 178 "execute-api.ap-northeast-1.amazonaws.com": "Z1YSHQZHG15GKL", 179 "execute-api.ca-central-1.amazonaws.com": "Z19DQILCV0OWEC", 180 "execute-api.eu-central-1.amazonaws.com": "Z1U9ULNL0V5AJ3", 181 "execute-api.eu-west-1.amazonaws.com": "ZLY8HYME6SFDD", 182 "execute-api.eu-west-2.amazonaws.com": "ZJ5UAJN8Y3Z2Q", 183 "execute-api.eu-south-1.amazonaws.com": "Z3BT4WSQ9TDYZV", 184 "execute-api.eu-west-3.amazonaws.com": "Z3KY65QIEKYHQQ", 185 "execute-api.eu-south-2.amazonaws.com": "Z02499852UI5HEQ5JVWX3", 186 "execute-api.eu-north-1.amazonaws.com": "Z3UWIKFBOOGXPP", 187 "execute-api.me-south-1.amazonaws.com": "Z20ZBPC0SS8806", 188 "execute-api.me-central-1.amazonaws.com": "Z08780021BKYYY8U0YHTV", 189 "execute-api.sa-east-1.amazonaws.com": "ZCMLWB8V5SYIT", 190 "execute-api.us-gov-east-1.amazonaws.com": "Z3SE9ATJYCRCZJ", 191 "execute-api.us-gov-west-1.amazonaws.com": "Z1K6XKP9SAGWDV", 192 } 193 194 // Route53API is the subset of the AWS Route53 API that we actually use. Add methods as required. Signatures must match exactly. 195 // mostly taken from: https://github.com/kubernetes/kubernetes/blob/853167624edb6bc0cfdcdfb88e746e178f5db36c/federation/pkg/dnsprovider/providers/aws/route53/stubs/route53api.go 196 type Route53API interface { 197 ListResourceRecordSetsPagesWithContext(ctx context.Context, input *route53.ListResourceRecordSetsInput, fn func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error 198 ChangeResourceRecordSetsWithContext(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, opts ...request.Option) (*route53.ChangeResourceRecordSetsOutput, error) 199 CreateHostedZoneWithContext(ctx context.Context, input *route53.CreateHostedZoneInput, opts ...request.Option) (*route53.CreateHostedZoneOutput, error) 200 ListHostedZonesPagesWithContext(ctx context.Context, input *route53.ListHostedZonesInput, fn func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error 201 ListTagsForResourceWithContext(ctx context.Context, input *route53.ListTagsForResourceInput, opts ...request.Option) (*route53.ListTagsForResourceOutput, error) 202 } 203 204 // wrapper to handle ownership relation throughout the provider implementation 205 type Route53Change struct { 206 route53.Change 207 OwnedRecord string 208 sizeBytes int 209 sizeValues int 210 } 211 212 type Route53Changes []*Route53Change 213 214 func (cs Route53Changes) Route53Changes() []*route53.Change { 215 ret := []*route53.Change{} 216 for _, c := range cs { 217 ret = append(ret, &c.Change) 218 } 219 return ret 220 } 221 222 type zonesListCache struct { 223 age time.Time 224 duration time.Duration 225 zones map[string]*route53.HostedZone 226 } 227 228 // AWSProvider is an implementation of Provider for AWS Route53. 229 type AWSProvider struct { 230 provider.BaseProvider 231 client Route53API 232 dryRun bool 233 batchChangeSize int 234 batchChangeSizeBytes int 235 batchChangeSizeValues int 236 batchChangeInterval time.Duration 237 evaluateTargetHealth bool 238 // only consider hosted zones managing domains ending in this suffix 239 domainFilter endpoint.DomainFilter 240 // filter hosted zones by id 241 zoneIDFilter provider.ZoneIDFilter 242 // filter hosted zones by type (e.g. private or public) 243 zoneTypeFilter provider.ZoneTypeFilter 244 // filter hosted zones by tags 245 zoneTagFilter provider.ZoneTagFilter 246 // extend filter for sub-domains in the zone (e.g. first.us-east-1.example.com) 247 zoneMatchParent bool 248 preferCNAME bool 249 zonesCache *zonesListCache 250 // queue for collecting changes to submit them in the next iteration, but after all other changes 251 failedChangesQueue map[string]Route53Changes 252 } 253 254 // AWSConfig contains configuration to create a new AWS provider. 255 type AWSConfig struct { 256 DomainFilter endpoint.DomainFilter 257 ZoneIDFilter provider.ZoneIDFilter 258 ZoneTypeFilter provider.ZoneTypeFilter 259 ZoneTagFilter provider.ZoneTagFilter 260 ZoneMatchParent bool 261 BatchChangeSize int 262 BatchChangeSizeBytes int 263 BatchChangeSizeValues int 264 BatchChangeInterval time.Duration 265 EvaluateTargetHealth bool 266 PreferCNAME bool 267 DryRun bool 268 ZoneCacheDuration time.Duration 269 } 270 271 // NewAWSProvider initializes a new AWS Route53 based Provider. 272 func NewAWSProvider(awsConfig AWSConfig, client Route53API) (*AWSProvider, error) { 273 provider := &AWSProvider{ 274 client: client, 275 domainFilter: awsConfig.DomainFilter, 276 zoneIDFilter: awsConfig.ZoneIDFilter, 277 zoneTypeFilter: awsConfig.ZoneTypeFilter, 278 zoneTagFilter: awsConfig.ZoneTagFilter, 279 zoneMatchParent: awsConfig.ZoneMatchParent, 280 batchChangeSize: awsConfig.BatchChangeSize, 281 batchChangeSizeBytes: awsConfig.BatchChangeSizeBytes, 282 batchChangeSizeValues: awsConfig.BatchChangeSizeValues, 283 batchChangeInterval: awsConfig.BatchChangeInterval, 284 evaluateTargetHealth: awsConfig.EvaluateTargetHealth, 285 preferCNAME: awsConfig.PreferCNAME, 286 dryRun: awsConfig.DryRun, 287 zonesCache: &zonesListCache{duration: awsConfig.ZoneCacheDuration}, 288 failedChangesQueue: make(map[string]Route53Changes), 289 } 290 291 return provider, nil 292 } 293 294 // Zones returns the list of hosted zones. 295 func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone, error) { 296 if p.zonesCache.zones != nil && time.Since(p.zonesCache.age) < p.zonesCache.duration { 297 log.Debug("Using cached zones list") 298 return p.zonesCache.zones, nil 299 } 300 log.Debug("Refreshing zones list cache") 301 302 zones := make(map[string]*route53.HostedZone) 303 304 var tagErr error 305 f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) { 306 for _, zone := range resp.HostedZones { 307 if !p.zoneIDFilter.Match(aws.StringValue(zone.Id)) { 308 continue 309 } 310 311 if !p.zoneTypeFilter.Match(zone) { 312 continue 313 } 314 315 if !p.domainFilter.Match(aws.StringValue(zone.Name)) { 316 if !p.zoneMatchParent { 317 continue 318 } 319 if !p.domainFilter.MatchParent(aws.StringValue(zone.Name)) { 320 continue 321 } 322 } 323 324 // Only fetch tags if a tag filter was specified 325 if !p.zoneTagFilter.IsEmpty() { 326 tags, err := p.tagsForZone(ctx, *zone.Id) 327 if err != nil { 328 tagErr = err 329 return false 330 } 331 if !p.zoneTagFilter.Match(tags) { 332 continue 333 } 334 } 335 336 zones[aws.StringValue(zone.Id)] = zone 337 } 338 339 return true 340 } 341 342 err := p.client.ListHostedZonesPagesWithContext(ctx, &route53.ListHostedZonesInput{}, f) 343 if err != nil { 344 return nil, provider.NewSoftError(fmt.Errorf("failed to list hosted zones: %w", err)) 345 } 346 if tagErr != nil { 347 return nil, provider.NewSoftError(fmt.Errorf("failed to list zones tags: %w", tagErr)) 348 } 349 350 for _, zone := range zones { 351 log.Debugf("Considering zone: %s (domain: %s)", aws.StringValue(zone.Id), aws.StringValue(zone.Name)) 352 } 353 354 if p.zonesCache.duration > time.Duration(0) { 355 p.zonesCache.zones = zones 356 p.zonesCache.age = time.Now() 357 } 358 359 return zones, nil 360 } 361 362 // wildcardUnescape converts \\052.abc back to *.abc 363 // Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk 364 func wildcardUnescape(s string) string { 365 return strings.Replace(s, "\\052", "*", 1) 366 } 367 368 // Records returns the list of records in a given hosted zone. 369 func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { 370 zones, err := p.Zones(ctx) 371 if err != nil { 372 return nil, provider.NewSoftError(fmt.Errorf("records retrieval failed: %w", err)) 373 } 374 375 return p.records(ctx, zones) 376 } 377 378 func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.HostedZone) ([]*endpoint.Endpoint, error) { 379 endpoints := make([]*endpoint.Endpoint, 0) 380 f := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) { 381 for _, r := range resp.ResourceRecordSets { 382 newEndpoints := make([]*endpoint.Endpoint, 0) 383 384 if !p.SupportedRecordType(aws.StringValue(r.Type)) { 385 continue 386 } 387 388 var ttl endpoint.TTL 389 if r.TTL != nil { 390 ttl = endpoint.TTL(*r.TTL) 391 } 392 393 if len(r.ResourceRecords) > 0 { 394 targets := make([]string, len(r.ResourceRecords)) 395 for idx, rr := range r.ResourceRecords { 396 targets[idx] = aws.StringValue(rr.Value) 397 } 398 399 ep := endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.Type), ttl, targets...) 400 if aws.StringValue(r.Type) == endpoint.RecordTypeCNAME { 401 ep = ep.WithProviderSpecific(providerSpecificAlias, "false") 402 } 403 newEndpoints = append(newEndpoints, ep) 404 } 405 406 if r.AliasTarget != nil { 407 // Alias records don't have TTLs so provide the default to match the TXT generation 408 if ttl == 0 { 409 ttl = recordTTL 410 } 411 ep := endpoint. 412 NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), endpoint.RecordTypeA, ttl, aws.StringValue(r.AliasTarget.DNSName)). 413 WithProviderSpecific(providerSpecificEvaluateTargetHealth, fmt.Sprintf("%t", aws.BoolValue(r.AliasTarget.EvaluateTargetHealth))). 414 WithProviderSpecific(providerSpecificAlias, "true") 415 newEndpoints = append(newEndpoints, ep) 416 } 417 418 for _, ep := range newEndpoints { 419 if r.SetIdentifier != nil { 420 ep.SetIdentifier = aws.StringValue(r.SetIdentifier) 421 switch { 422 case r.Weight != nil: 423 ep.WithProviderSpecific(providerSpecificWeight, fmt.Sprintf("%d", aws.Int64Value(r.Weight))) 424 case r.Region != nil: 425 ep.WithProviderSpecific(providerSpecificRegion, aws.StringValue(r.Region)) 426 case r.Failover != nil: 427 ep.WithProviderSpecific(providerSpecificFailover, aws.StringValue(r.Failover)) 428 case r.MultiValueAnswer != nil && aws.BoolValue(r.MultiValueAnswer): 429 ep.WithProviderSpecific(providerSpecificMultiValueAnswer, "") 430 case r.GeoLocation != nil: 431 if r.GeoLocation.ContinentCode != nil { 432 ep.WithProviderSpecific(providerSpecificGeolocationContinentCode, aws.StringValue(r.GeoLocation.ContinentCode)) 433 } else { 434 if r.GeoLocation.CountryCode != nil { 435 ep.WithProviderSpecific(providerSpecificGeolocationCountryCode, aws.StringValue(r.GeoLocation.CountryCode)) 436 } 437 if r.GeoLocation.SubdivisionCode != nil { 438 ep.WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, aws.StringValue(r.GeoLocation.SubdivisionCode)) 439 } 440 } 441 default: 442 // one of the above needs to be set, otherwise SetIdentifier doesn't make sense 443 } 444 } 445 446 if r.HealthCheckId != nil { 447 ep.WithProviderSpecific(providerSpecificHealthCheckID, aws.StringValue(r.HealthCheckId)) 448 } 449 450 endpoints = append(endpoints, ep) 451 } 452 } 453 454 return true 455 } 456 457 for _, z := range zones { 458 params := &route53.ListResourceRecordSetsInput{ 459 HostedZoneId: z.Id, 460 MaxItems: aws.String(route53PageSize), 461 } 462 463 if err := p.client.ListResourceRecordSetsPagesWithContext(ctx, params, f); err != nil { 464 return nil, provider.NewSoftError(fmt.Errorf("failed to list resource records sets for zone %s: %w", *z.Id, err)) 465 } 466 } 467 468 return endpoints, nil 469 } 470 471 // Identify if old and new endpoints require DELETE/CREATE instead of UPDATE. 472 func (p *AWSProvider) requiresDeleteCreate(old *endpoint.Endpoint, new *endpoint.Endpoint) bool { 473 // a change of record type 474 if old.RecordType != new.RecordType { 475 return true 476 } 477 478 // an ALIAS record change to/from an A 479 if old.RecordType == endpoint.RecordTypeA { 480 oldAlias, _ := old.GetProviderSpecificProperty(providerSpecificAlias) 481 newAlias, _ := new.GetProviderSpecificProperty(providerSpecificAlias) 482 if oldAlias != newAlias { 483 return true 484 } 485 } 486 487 // a set identifier change 488 if old.SetIdentifier != new.SetIdentifier { 489 return true 490 } 491 492 // a change of routing policy 493 // default to true for geolocation properties if any geolocation property exists in old/new but not the other 494 for _, propType := range [7]string{providerSpecificWeight, providerSpecificRegion, providerSpecificFailover, 495 providerSpecificFailover, providerSpecificGeolocationContinentCode, providerSpecificGeolocationCountryCode, 496 providerSpecificGeolocationSubdivisionCode} { 497 _, oldPolicy := old.GetProviderSpecificProperty(propType) 498 _, newPolicy := new.GetProviderSpecificProperty(propType) 499 if oldPolicy != newPolicy { 500 return true 501 } 502 } 503 504 return false 505 } 506 507 func (p *AWSProvider) createUpdateChanges(newEndpoints, oldEndpoints []*endpoint.Endpoint) Route53Changes { 508 var deletes []*endpoint.Endpoint 509 var creates []*endpoint.Endpoint 510 var updates []*endpoint.Endpoint 511 512 for i, new := range newEndpoints { 513 old := oldEndpoints[i] 514 if p.requiresDeleteCreate(old, new) { 515 deletes = append(deletes, old) 516 creates = append(creates, new) 517 } else { 518 // Safe to perform an UPSERT. 519 updates = append(updates, new) 520 } 521 } 522 523 combined := make(Route53Changes, 0, len(deletes)+len(creates)+len(updates)) 524 combined = append(combined, p.newChanges(route53.ChangeActionCreate, creates)...) 525 combined = append(combined, p.newChanges(route53.ChangeActionUpsert, updates)...) 526 combined = append(combined, p.newChanges(route53.ChangeActionDelete, deletes)...) 527 return combined 528 } 529 530 // GetDomainFilter generates a filter to exclude any domain that is not controlled by the provider 531 func (p *AWSProvider) GetDomainFilter() endpoint.DomainFilter { 532 zones, err := p.Zones(context.Background()) 533 if err != nil { 534 log.Errorf("failed to list zones: %v", err) 535 return endpoint.DomainFilter{} 536 } 537 zoneNames := []string(nil) 538 for _, z := range zones { 539 zoneNames = append(zoneNames, aws.StringValue(z.Name), "."+aws.StringValue(z.Name)) 540 } 541 log.Infof("Applying provider record filter for domains: %v", zoneNames) 542 return endpoint.NewDomainFilter(zoneNames) 543 } 544 545 // ApplyChanges applies a given set of changes in a given zone. 546 func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 547 zones, err := p.Zones(ctx) 548 if err != nil { 549 return provider.NewSoftError(fmt.Errorf("failed to list zones, not applying changes: %w", err)) 550 } 551 552 updateChanges := p.createUpdateChanges(changes.UpdateNew, changes.UpdateOld) 553 554 combinedChanges := make(Route53Changes, 0, len(changes.Delete)+len(changes.Create)+len(updateChanges)) 555 combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionCreate, changes.Create)...) 556 combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionDelete, changes.Delete)...) 557 combinedChanges = append(combinedChanges, updateChanges...) 558 559 return p.submitChanges(ctx, combinedChanges, zones) 560 } 561 562 // submitChanges takes a zone and a collection of Changes and sends them as a single transaction. 563 func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes, zones map[string]*route53.HostedZone) error { 564 // return early if there is nothing to change 565 if len(changes) == 0 { 566 log.Info("All records are already up to date") 567 return nil 568 } 569 570 // separate into per-zone change sets to be passed to the API. 571 changesByZone := changesByZone(zones, changes) 572 if len(changesByZone) == 0 { 573 log.Info("All records are already up to date, there are no changes for the matching hosted zones") 574 } 575 576 var failedZones []string 577 for z, cs := range changesByZone { 578 var failedUpdate bool 579 580 // group changes into new changes and into changes that failed in a previous iteration and are retried 581 retriedChanges, newChanges := findChangesInQueue(cs, p.failedChangesQueue[z]) 582 p.failedChangesQueue[z] = nil 583 584 batchCs := append(batchChangeSet(newChanges, p.batchChangeSize, p.batchChangeSizeBytes, p.batchChangeSizeValues), 585 batchChangeSet(retriedChanges, p.batchChangeSize, p.batchChangeSizeBytes, p.batchChangeSizeValues)...) 586 for i, b := range batchCs { 587 if len(b) == 0 { 588 continue 589 } 590 591 for _, c := range b { 592 log.Infof("Desired change: %s %s %s [Id: %s]", *c.Action, *c.ResourceRecordSet.Name, *c.ResourceRecordSet.Type, z) 593 } 594 595 if !p.dryRun { 596 params := &route53.ChangeResourceRecordSetsInput{ 597 HostedZoneId: aws.String(z), 598 ChangeBatch: &route53.ChangeBatch{ 599 Changes: b.Route53Changes(), 600 }, 601 } 602 603 successfulChanges := 0 604 605 if _, err := p.client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil { 606 log.Errorf("Failure in zone %s [Id: %s] when submitting change batch: %v", aws.StringValue(zones[z].Name), z, err) 607 608 changesByOwnership := groupChangesByNameAndOwnershipRelation(b) 609 610 if len(changesByOwnership) > 1 { 611 log.Debug("Trying to submit change sets one-by-one instead") 612 613 for _, changes := range changesByOwnership { 614 for _, c := range changes { 615 log.Debugf("Desired change: %s %s %s [Id: %s]", *c.Action, *c.ResourceRecordSet.Name, *c.ResourceRecordSet.Type, z) 616 } 617 params.ChangeBatch = &route53.ChangeBatch{ 618 Changes: changes.Route53Changes(), 619 } 620 if _, err := p.client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil { 621 failedUpdate = true 622 log.Errorf("Failed submitting change (error: %v), it will be retried in a separate change batch in the next iteration", err) 623 p.failedChangesQueue[z] = append(p.failedChangesQueue[z], changes...) 624 } else { 625 successfulChanges = successfulChanges + len(changes) 626 } 627 } 628 } else { 629 failedUpdate = true 630 } 631 } else { 632 successfulChanges = len(b) 633 } 634 635 if successfulChanges > 0 { 636 // z is the R53 Hosted Zone ID already as aws.StringValue 637 log.Infof("%d record(s) in zone %s [Id: %s] were successfully updated", successfulChanges, aws.StringValue(zones[z].Name), z) 638 } 639 640 if i != len(batchCs)-1 { 641 time.Sleep(p.batchChangeInterval) 642 } 643 } 644 } 645 646 if failedUpdate { 647 failedZones = append(failedZones, z) 648 } 649 } 650 651 if len(failedZones) > 0 { 652 return provider.NewSoftError(fmt.Errorf("failed to submit all changes for the following zones: %v", failedZones)) 653 } 654 655 return nil 656 } 657 658 // newChanges returns a collection of Changes based on the given records and action. 659 func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint) Route53Changes { 660 changes := make(Route53Changes, 0, len(endpoints)) 661 662 for _, endpoint := range endpoints { 663 change, dualstack := p.newChange(action, endpoint) 664 changes = append(changes, change) 665 if dualstack { 666 // make a copy of change, modify RRS type to AAAA, then add new change 667 rrs := *change.ResourceRecordSet 668 change2 := &Route53Change{Change: route53.Change{Action: change.Action, ResourceRecordSet: &rrs}} 669 change2.ResourceRecordSet.Type = aws.String(route53.RRTypeAaaa) 670 changes = append(changes, change2) 671 } 672 } 673 674 return changes 675 } 676 677 // AdjustEndpoints modifies the provided endpoints (coming from various sources) to match 678 // the endpoints that the provider returns in `Records` so that the change plan will not have 679 // unneeded (potentially failing) changes. 680 // Example: CNAME endpoints pointing to ELBs will have a `alias` provider-specific property 681 // added to match the endpoints generated from existing alias records in Route53. 682 func (p *AWSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { 683 for _, ep := range endpoints { 684 alias := false 685 686 if aliasString, ok := ep.GetProviderSpecificProperty(providerSpecificAlias); ok { 687 alias = aliasString == "true" 688 if alias { 689 if ep.RecordType != endpoint.RecordTypeA && ep.RecordType != endpoint.RecordTypeCNAME { 690 ep.DeleteProviderSpecificProperty(providerSpecificAlias) 691 } 692 } else { 693 if ep.RecordType == endpoint.RecordTypeCNAME { 694 if aliasString != "false" { 695 ep.SetProviderSpecificProperty(providerSpecificAlias, "false") 696 } 697 } else { 698 ep.DeleteProviderSpecificProperty(providerSpecificAlias) 699 } 700 } 701 } else if ep.RecordType == endpoint.RecordTypeCNAME { 702 alias = useAlias(ep, p.preferCNAME) 703 log.Debugf("Modifying endpoint: %v, setting %s=%v", ep, providerSpecificAlias, alias) 704 ep.SetProviderSpecificProperty(providerSpecificAlias, strconv.FormatBool(alias)) 705 } 706 707 if alias { 708 ep.RecordType = endpoint.RecordTypeA 709 if ep.RecordTTL.IsConfigured() { 710 log.Debugf("Modifying endpoint: %v, setting ttl=%v", ep, recordTTL) 711 ep.RecordTTL = recordTTL 712 } 713 if prop, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok { 714 if prop != "true" && prop != "false" { 715 ep.SetProviderSpecificProperty(providerSpecificEvaluateTargetHealth, "false") 716 } 717 } else { 718 ep.SetProviderSpecificProperty(providerSpecificEvaluateTargetHealth, strconv.FormatBool(p.evaluateTargetHealth)) 719 } 720 } else { 721 ep.DeleteProviderSpecificProperty(providerSpecificEvaluateTargetHealth) 722 } 723 } 724 return endpoints, nil 725 } 726 727 // newChange returns a route53 Change and a boolean indicating if there should also be a change to a AAAA record 728 // returned Change is based on the given record by the given action, e.g. 729 // action=ChangeActionCreate returns a change for creation of the record and 730 // action=ChangeActionDelete returns a change for deletion of the record. 731 func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53Change, bool) { 732 change := &Route53Change{ 733 Change: route53.Change{ 734 Action: aws.String(action), 735 ResourceRecordSet: &route53.ResourceRecordSet{ 736 Name: aws.String(ep.DNSName), 737 }, 738 }, 739 } 740 dualstack := false 741 if targetHostedZone := isAWSAlias(ep); targetHostedZone != "" { 742 evalTargetHealth := p.evaluateTargetHealth 743 if prop, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok { 744 evalTargetHealth = prop == "true" 745 } 746 // If the endpoint has a Dualstack label, append a change for AAAA record as well. 747 if val, ok := ep.Labels[endpoint.DualstackLabelKey]; ok { 748 dualstack = val == "true" 749 } 750 change.ResourceRecordSet.Type = aws.String(route53.RRTypeA) 751 change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{ 752 DNSName: aws.String(ep.Targets[0]), 753 HostedZoneId: aws.String(cleanZoneID(targetHostedZone)), 754 EvaluateTargetHealth: aws.Bool(evalTargetHealth), 755 } 756 change.sizeBytes += len([]byte(ep.Targets[0])) 757 change.sizeValues += 1 758 } else { 759 change.ResourceRecordSet.Type = aws.String(ep.RecordType) 760 if !ep.RecordTTL.IsConfigured() { 761 change.ResourceRecordSet.TTL = aws.Int64(recordTTL) 762 } else { 763 change.ResourceRecordSet.TTL = aws.Int64(int64(ep.RecordTTL)) 764 } 765 change.ResourceRecordSet.ResourceRecords = make([]*route53.ResourceRecord, len(ep.Targets)) 766 for idx, val := range ep.Targets { 767 change.ResourceRecordSet.ResourceRecords[idx] = &route53.ResourceRecord{ 768 Value: aws.String(val), 769 } 770 change.sizeBytes += len([]byte(val)) 771 change.sizeValues += 1 772 } 773 } 774 775 if action == route53.ChangeActionUpsert { 776 // If the value of the Action element is UPSERT, each ResourceRecord element and each character in a Value 777 // element is counted twice 778 change.sizeBytes *= 2 779 change.sizeValues *= 2 780 } 781 782 setIdentifier := ep.SetIdentifier 783 if setIdentifier != "" { 784 change.ResourceRecordSet.SetIdentifier = aws.String(setIdentifier) 785 if prop, ok := ep.GetProviderSpecificProperty(providerSpecificWeight); ok { 786 weight, err := strconv.ParseInt(prop, 10, 64) 787 if err != nil { 788 log.Errorf("Failed parsing value of %s: %s: %v; using weight of 0", providerSpecificWeight, prop, err) 789 weight = 0 790 } 791 change.ResourceRecordSet.Weight = aws.Int64(weight) 792 } 793 if prop, ok := ep.GetProviderSpecificProperty(providerSpecificRegion); ok { 794 change.ResourceRecordSet.Region = aws.String(prop) 795 } 796 if prop, ok := ep.GetProviderSpecificProperty(providerSpecificFailover); ok { 797 change.ResourceRecordSet.Failover = aws.String(prop) 798 } 799 if _, ok := ep.GetProviderSpecificProperty(providerSpecificMultiValueAnswer); ok { 800 change.ResourceRecordSet.MultiValueAnswer = aws.Bool(true) 801 } 802 803 geolocation := &route53.GeoLocation{} 804 useGeolocation := false 805 if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationContinentCode); ok { 806 geolocation.ContinentCode = aws.String(prop) 807 useGeolocation = true 808 } else { 809 if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationCountryCode); ok { 810 geolocation.CountryCode = aws.String(prop) 811 useGeolocation = true 812 } 813 if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationSubdivisionCode); ok { 814 geolocation.SubdivisionCode = aws.String(prop) 815 useGeolocation = true 816 } 817 } 818 if useGeolocation { 819 change.ResourceRecordSet.GeoLocation = geolocation 820 } 821 } 822 823 if prop, ok := ep.GetProviderSpecificProperty(providerSpecificHealthCheckID); ok { 824 change.ResourceRecordSet.HealthCheckId = aws.String(prop) 825 } 826 827 if ownedRecord, ok := ep.Labels[endpoint.OwnedRecordLabelKey]; ok { 828 change.OwnedRecord = ownedRecord 829 } 830 831 return change, dualstack 832 } 833 834 // searches for `changes` that are contained in `queue` and returns the `changes` separated by whether they were found in the queue (`foundChanges`) or not (`notFoundChanges`) 835 func findChangesInQueue(changes Route53Changes, queue Route53Changes) (foundChanges, notFoundChanges Route53Changes) { 836 if queue == nil { 837 return Route53Changes{}, changes 838 } 839 840 for _, c := range changes { 841 found := false 842 for _, qc := range queue { 843 if c == qc { 844 foundChanges = append(foundChanges, c) 845 found = true 846 break 847 } 848 } 849 if !found { 850 notFoundChanges = append(notFoundChanges, c) 851 } 852 } 853 854 return 855 } 856 857 // group the given changes by name and ownership relation to ensure these are always submitted in the same transaction to Route53; 858 // grouping by name is done to always submit changes with the same name but different set identifier together, 859 // grouping by ownership relation is done to always submit changes of records and e.g. their corresponding TXT registry records together 860 func groupChangesByNameAndOwnershipRelation(cs Route53Changes) map[string]Route53Changes { 861 changesByOwnership := make(map[string]Route53Changes) 862 for _, v := range cs { 863 key := v.OwnedRecord 864 if key == "" { 865 key = aws.StringValue(v.ResourceRecordSet.Name) 866 } 867 changesByOwnership[key] = append(changesByOwnership[key], v) 868 } 869 return changesByOwnership 870 } 871 872 func (p *AWSProvider) tagsForZone(ctx context.Context, zoneID string) (map[string]string, error) { 873 response, err := p.client.ListTagsForResourceWithContext(ctx, &route53.ListTagsForResourceInput{ 874 ResourceType: aws.String("hostedzone"), 875 ResourceId: aws.String(zoneID), 876 }) 877 if err != nil { 878 return nil, provider.NewSoftError(fmt.Errorf("failed to list tags for zone %s: %w", zoneID, err)) 879 } 880 tagMap := map[string]string{} 881 for _, tag := range response.ResourceTagSet.Tags { 882 tagMap[*tag.Key] = *tag.Value 883 } 884 return tagMap, nil 885 } 886 887 // count bytes for all changes values 888 func countChangeBytes(cs Route53Changes) int { 889 count := 0 890 for _, c := range cs { 891 count += c.sizeBytes 892 } 893 return count 894 } 895 896 // count total value count for all changes 897 func countChangeValues(cs Route53Changes) int { 898 count := 0 899 for _, c := range cs { 900 count += c.sizeValues 901 } 902 return count 903 } 904 905 func batchChangeSet(cs Route53Changes, batchSize int, batchSizeBytes int, batchSizeValues int) []Route53Changes { 906 if len(cs) <= batchSize && countChangeBytes(cs) <= batchSizeBytes && countChangeValues(cs) <= batchSizeValues { 907 res := sortChangesByActionNameType(cs) 908 return []Route53Changes{res} 909 } 910 911 batchChanges := make([]Route53Changes, 0) 912 913 changesByOwnership := groupChangesByNameAndOwnershipRelation(cs) 914 915 names := make([]string, 0) 916 for v := range changesByOwnership { 917 names = append(names, v) 918 } 919 sort.Strings(names) 920 921 currentBatch := Route53Changes{} 922 for k, name := range names { 923 v := changesByOwnership[name] 924 vBytes := countChangeBytes(v) 925 vValues := countChangeValues(v) 926 if len(v) > batchSize { 927 log.Warnf("Total changes for %v exceeds max batch size of %d, total changes: %d; changes will not be performed", k, batchSize, len(v)) 928 continue 929 } 930 if vBytes > batchSizeBytes { 931 log.Warnf("Total changes for %v exceeds max batch size bytes of %d, total changes bytes: %d; changes will not be performed", k, batchSizeBytes, vBytes) 932 continue 933 } 934 if vValues > batchSizeValues { 935 log.Warnf("Total changes for %v exceeds max batch size values of %d, total changes values: %d; changes will not be performed", k, batchSizeValues, vValues) 936 continue 937 } 938 939 bytes := countChangeBytes(currentBatch) + vBytes 940 values := countChangeValues(currentBatch) + vValues 941 942 if len(currentBatch)+len(v) > batchSize || bytes > batchSizeBytes || values > batchSizeValues { 943 // currentBatch would be too large if we add this changeset; 944 // add currentBatch to batchChanges and start a new currentBatch 945 batchChanges = append(batchChanges, sortChangesByActionNameType(currentBatch)) 946 currentBatch = append(Route53Changes{}, v...) 947 } else { 948 currentBatch = append(currentBatch, v...) 949 } 950 } 951 if len(currentBatch) > 0 { 952 // add final currentBatch 953 batchChanges = append(batchChanges, sortChangesByActionNameType(currentBatch)) 954 } 955 956 return batchChanges 957 } 958 959 func sortChangesByActionNameType(cs Route53Changes) Route53Changes { 960 sort.SliceStable(cs, func(i, j int) bool { 961 if *cs[i].Action > *cs[j].Action { 962 return true 963 } 964 if *cs[i].Action < *cs[j].Action { 965 return false 966 } 967 if *cs[i].ResourceRecordSet.Name < *cs[j].ResourceRecordSet.Name { 968 return true 969 } 970 if *cs[i].ResourceRecordSet.Name > *cs[j].ResourceRecordSet.Name { 971 return false 972 } 973 return *cs[i].ResourceRecordSet.Type < *cs[j].ResourceRecordSet.Type 974 }) 975 976 return cs 977 } 978 979 // changesByZone separates a multi-zone change into a single change per zone. 980 func changesByZone(zones map[string]*route53.HostedZone, changeSet Route53Changes) map[string]Route53Changes { 981 changes := make(map[string]Route53Changes) 982 983 for _, z := range zones { 984 changes[aws.StringValue(z.Id)] = Route53Changes{} 985 } 986 987 for _, c := range changeSet { 988 hostname := provider.EnsureTrailingDot(aws.StringValue(c.ResourceRecordSet.Name)) 989 990 zones := suitableZones(hostname, zones) 991 if len(zones) == 0 { 992 log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.String()) 993 continue 994 } 995 for _, z := range zones { 996 if c.ResourceRecordSet.AliasTarget != nil && aws.StringValue(c.ResourceRecordSet.AliasTarget.HostedZoneId) == sameZoneAlias { 997 // alias record is to be created; target needs to be in the same zone as endpoint 998 // if it's not, this will fail 999 rrset := *c.ResourceRecordSet 1000 aliasTarget := *rrset.AliasTarget 1001 aliasTarget.HostedZoneId = aws.String(cleanZoneID(aws.StringValue(z.Id))) 1002 rrset.AliasTarget = &aliasTarget 1003 c = &Route53Change{ 1004 Change: route53.Change{ 1005 Action: c.Action, 1006 ResourceRecordSet: &rrset, 1007 }, 1008 } 1009 } 1010 changes[aws.StringValue(z.Id)] = append(changes[aws.StringValue(z.Id)], c) 1011 log.Debugf("Adding %s to zone %s [Id: %s]", hostname, aws.StringValue(z.Name), aws.StringValue(z.Id)) 1012 } 1013 } 1014 1015 // separating a change could lead to empty sub changes, remove them here. 1016 for zone, change := range changes { 1017 if len(change) == 0 { 1018 delete(changes, zone) 1019 } 1020 } 1021 1022 return changes 1023 } 1024 1025 // suitableZones returns all suitable private zones and the most suitable public zone 1026 // 1027 // for a given hostname and a set of zones. 1028 func suitableZones(hostname string, zones map[string]*route53.HostedZone) []*route53.HostedZone { 1029 var matchingZones []*route53.HostedZone 1030 var publicZone *route53.HostedZone 1031 1032 for _, z := range zones { 1033 if aws.StringValue(z.Name) == hostname || strings.HasSuffix(hostname, "."+aws.StringValue(z.Name)) { 1034 if z.Config == nil || !aws.BoolValue(z.Config.PrivateZone) { 1035 // Only select the best matching public zone 1036 if publicZone == nil || len(aws.StringValue(z.Name)) > len(aws.StringValue(publicZone.Name)) { 1037 publicZone = z 1038 } 1039 } else { 1040 // Include all private zones 1041 matchingZones = append(matchingZones, z) 1042 } 1043 } 1044 } 1045 1046 if publicZone != nil { 1047 matchingZones = append(matchingZones, publicZone) 1048 } 1049 1050 return matchingZones 1051 } 1052 1053 // useAlias determines if AWS ALIAS should be used. 1054 func useAlias(ep *endpoint.Endpoint, preferCNAME bool) bool { 1055 if preferCNAME { 1056 return false 1057 } 1058 1059 if ep.RecordType == endpoint.RecordTypeCNAME && len(ep.Targets) > 0 { 1060 return canonicalHostedZone(ep.Targets[0]) != "" 1061 } 1062 1063 return false 1064 } 1065 1066 // isAWSAlias determines if a given endpoint is supposed to create an AWS Alias record 1067 // and (if so) returns the target hosted zone ID 1068 func isAWSAlias(ep *endpoint.Endpoint) string { 1069 isAlias, exists := ep.GetProviderSpecificProperty(providerSpecificAlias) 1070 if exists && isAlias == "true" && ep.RecordType == endpoint.RecordTypeA && len(ep.Targets) > 0 { 1071 // alias records can only point to canonical hosted zones (e.g. to ELBs) or other records in the same zone 1072 1073 if hostedZoneID, ok := ep.GetProviderSpecificProperty(providerSpecificTargetHostedZone); ok { 1074 // existing Endpoint where we got the target hosted zone from the Route53 data 1075 return hostedZoneID 1076 } 1077 1078 // check if the target is in a canonical hosted zone 1079 if canonicalHostedZone := canonicalHostedZone(ep.Targets[0]); canonicalHostedZone != "" { 1080 return canonicalHostedZone 1081 } 1082 1083 // if not, target needs to be in the same zone 1084 return sameZoneAlias 1085 } 1086 return "" 1087 } 1088 1089 // canonicalHostedZone returns the matching canonical zone for a given hostname. 1090 func canonicalHostedZone(hostname string) string { 1091 for suffix, zone := range canonicalHostedZones { 1092 if strings.HasSuffix(hostname, suffix) { 1093 return zone 1094 } 1095 } 1096 1097 if strings.HasSuffix(hostname, ".amazonaws.com") { 1098 // hostname is an AWS hostname, but could not find canonical hosted zone. 1099 // This could mean that a new region has been added but is not supported yet. 1100 log.Warnf("Could not find canonical hosted zone for domain %s. This may be because your region is not supported yet.", hostname) 1101 } 1102 1103 return "" 1104 } 1105 1106 // cleanZoneID removes the "/hostedzone/" prefix 1107 func cleanZoneID(id string) string { 1108 return strings.TrimPrefix(id, "/hostedzone/") 1109 } 1110 1111 func (p *AWSProvider) SupportedRecordType(recordType string) bool { 1112 switch recordType { 1113 case "MX": 1114 return true 1115 default: 1116 return provider.SupportedRecordType(recordType) 1117 } 1118 }