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  }