sigs.k8s.io/external-dns@v0.14.1/provider/aws/aws_test.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  	"math"
    23  	"net"
    24  	"sort"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/aws/aws-sdk-go/aws"
    30  	"github.com/aws/aws-sdk-go/aws/request"
    31  	"github.com/aws/aws-sdk-go/service/route53"
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/mock"
    34  	"github.com/stretchr/testify/require"
    35  
    36  	"sigs.k8s.io/external-dns/endpoint"
    37  	"sigs.k8s.io/external-dns/internal/testutils"
    38  	"sigs.k8s.io/external-dns/plan"
    39  	"sigs.k8s.io/external-dns/provider"
    40  )
    41  
    42  const (
    43  	defaultBatchChangeSize       = 4000
    44  	defaultBatchChangeSizeBytes  = 32000
    45  	defaultBatchChangeSizeValues = 1000
    46  	defaultBatchChangeInterval   = time.Second
    47  	defaultEvaluateTargetHealth  = true
    48  )
    49  
    50  // Compile time check for interface conformance
    51  var _ Route53API = &Route53APIStub{}
    52  
    53  // Route53APIStub is a minimal implementation of Route53API, used primarily for unit testing.
    54  // See http://http://docs.aws.amazon.com/sdk-for-go/api/service/route53.html for descriptions
    55  // of all of its methods.
    56  // mostly taken from: https://github.com/kubernetes/kubernetes/blob/853167624edb6bc0cfdcdfb88e746e178f5db36c/federation/pkg/dnsprovider/providers/aws/route53/stubs/route53api.go
    57  type Route53APIStub struct {
    58  	zones      map[string]*route53.HostedZone
    59  	recordSets map[string]map[string][]*route53.ResourceRecordSet
    60  	zoneTags   map[string][]*route53.Tag
    61  	m          dynamicMock
    62  	t          *testing.T
    63  }
    64  
    65  // MockMethod starts a description of an expectation of the specified method
    66  // being called.
    67  //
    68  //	Route53APIStub.MockMethod("MyMethod", arg1, arg2)
    69  func (r *Route53APIStub) MockMethod(method string, args ...interface{}) *mock.Call {
    70  	return r.m.On(method, args...)
    71  }
    72  
    73  // NewRoute53APIStub returns an initialized Route53APIStub
    74  func NewRoute53APIStub(t *testing.T) *Route53APIStub {
    75  	return &Route53APIStub{
    76  		zones:      make(map[string]*route53.HostedZone),
    77  		recordSets: make(map[string]map[string][]*route53.ResourceRecordSet),
    78  		zoneTags:   make(map[string][]*route53.Tag),
    79  		t:          t,
    80  	}
    81  }
    82  
    83  func (r *Route53APIStub) ListResourceRecordSetsPagesWithContext(ctx context.Context, input *route53.ListResourceRecordSetsInput, fn func(p *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error {
    84  	output := route53.ListResourceRecordSetsOutput{} // TODO: Support optional input args.
    85  	require.NotNil(r.t, input.MaxItems)
    86  	assert.EqualValues(r.t, route53PageSize, *input.MaxItems)
    87  	if len(r.recordSets) == 0 {
    88  		output.ResourceRecordSets = []*route53.ResourceRecordSet{}
    89  	} else if _, ok := r.recordSets[aws.StringValue(input.HostedZoneId)]; !ok {
    90  		output.ResourceRecordSets = []*route53.ResourceRecordSet{}
    91  	} else {
    92  		for _, rrsets := range r.recordSets[aws.StringValue(input.HostedZoneId)] {
    93  			output.ResourceRecordSets = append(output.ResourceRecordSets, rrsets...)
    94  		}
    95  	}
    96  	lastPage := true
    97  	fn(&output, lastPage)
    98  	return nil
    99  }
   100  
   101  type Route53APICounter struct {
   102  	wrapped Route53API
   103  	calls   map[string]int
   104  }
   105  
   106  func NewRoute53APICounter(w Route53API) *Route53APICounter {
   107  	return &Route53APICounter{
   108  		wrapped: w,
   109  		calls:   map[string]int{},
   110  	}
   111  }
   112  
   113  func (c *Route53APICounter) ListResourceRecordSetsPagesWithContext(ctx context.Context, input *route53.ListResourceRecordSetsInput, fn func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error {
   114  	c.calls["ListResourceRecordSetsPages"]++
   115  	return c.wrapped.ListResourceRecordSetsPagesWithContext(ctx, input, fn)
   116  }
   117  
   118  func (c *Route53APICounter) ChangeResourceRecordSetsWithContext(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, opts ...request.Option) (*route53.ChangeResourceRecordSetsOutput, error) {
   119  	c.calls["ChangeResourceRecordSets"]++
   120  	return c.wrapped.ChangeResourceRecordSetsWithContext(ctx, input)
   121  }
   122  
   123  func (c *Route53APICounter) CreateHostedZoneWithContext(ctx context.Context, input *route53.CreateHostedZoneInput, opts ...request.Option) (*route53.CreateHostedZoneOutput, error) {
   124  	c.calls["CreateHostedZone"]++
   125  	return c.wrapped.CreateHostedZoneWithContext(ctx, input)
   126  }
   127  
   128  func (c *Route53APICounter) ListHostedZonesPagesWithContext(ctx context.Context, input *route53.ListHostedZonesInput, fn func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error {
   129  	c.calls["ListHostedZonesPages"]++
   130  	return c.wrapped.ListHostedZonesPagesWithContext(ctx, input, fn)
   131  }
   132  
   133  func (c *Route53APICounter) ListTagsForResourceWithContext(ctx context.Context, input *route53.ListTagsForResourceInput, opts ...request.Option) (*route53.ListTagsForResourceOutput, error) {
   134  	c.calls["ListTagsForResource"]++
   135  	return c.wrapped.ListTagsForResourceWithContext(ctx, input)
   136  }
   137  
   138  // Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk
   139  func wildcardEscape(s string) string {
   140  	if strings.Contains(s, "*") {
   141  		s = strings.Replace(s, "*", "\\052", 1)
   142  	}
   143  	return s
   144  }
   145  
   146  func (r *Route53APIStub) ListTagsForResourceWithContext(ctx context.Context, input *route53.ListTagsForResourceInput, opts ...request.Option) (*route53.ListTagsForResourceOutput, error) {
   147  	if aws.StringValue(input.ResourceType) == "hostedzone" {
   148  		tags := r.zoneTags[aws.StringValue(input.ResourceId)]
   149  		return &route53.ListTagsForResourceOutput{
   150  			ResourceTagSet: &route53.ResourceTagSet{
   151  				ResourceId:   input.ResourceId,
   152  				ResourceType: input.ResourceType,
   153  				Tags:         tags,
   154  			},
   155  		}, nil
   156  	}
   157  	return &route53.ListTagsForResourceOutput{}, nil
   158  }
   159  
   160  func (r *Route53APIStub) ChangeResourceRecordSetsWithContext(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, opts ...request.Option) (*route53.ChangeResourceRecordSetsOutput, error) {
   161  	if r.m.isMocked("ChangeResourceRecordSets", input) {
   162  		return r.m.ChangeResourceRecordSets(input)
   163  	}
   164  
   165  	_, ok := r.zones[aws.StringValue(input.HostedZoneId)]
   166  	if !ok {
   167  		return nil, fmt.Errorf("Hosted zone doesn't exist: %s", aws.StringValue(input.HostedZoneId))
   168  	}
   169  
   170  	if len(input.ChangeBatch.Changes) == 0 {
   171  		return nil, fmt.Errorf("ChangeBatch doesn't contain any changes")
   172  	}
   173  
   174  	output := &route53.ChangeResourceRecordSetsOutput{}
   175  	recordSets, ok := r.recordSets[aws.StringValue(input.HostedZoneId)]
   176  	if !ok {
   177  		recordSets = make(map[string][]*route53.ResourceRecordSet)
   178  	}
   179  
   180  	for _, change := range input.ChangeBatch.Changes {
   181  		if aws.StringValue(change.ResourceRecordSet.Type) == route53.RRTypeA {
   182  			for _, rrs := range change.ResourceRecordSet.ResourceRecords {
   183  				if net.ParseIP(aws.StringValue(rrs.Value)) == nil {
   184  					return nil, fmt.Errorf("A records must point to IPs")
   185  				}
   186  			}
   187  		}
   188  
   189  		change.ResourceRecordSet.Name = aws.String(wildcardEscape(provider.EnsureTrailingDot(aws.StringValue(change.ResourceRecordSet.Name))))
   190  
   191  		if change.ResourceRecordSet.AliasTarget != nil {
   192  			change.ResourceRecordSet.AliasTarget.DNSName = aws.String(wildcardEscape(provider.EnsureTrailingDot(aws.StringValue(change.ResourceRecordSet.AliasTarget.DNSName))))
   193  		}
   194  
   195  		setID := ""
   196  		if change.ResourceRecordSet.SetIdentifier != nil {
   197  			setID = aws.StringValue(change.ResourceRecordSet.SetIdentifier)
   198  		}
   199  		key := aws.StringValue(change.ResourceRecordSet.Name) + "::" + aws.StringValue(change.ResourceRecordSet.Type) + "::" + setID
   200  		switch aws.StringValue(change.Action) {
   201  		case route53.ChangeActionCreate:
   202  			if _, found := recordSets[key]; found {
   203  				return nil, fmt.Errorf("Attempt to create duplicate rrset %s", key) // TODO: Return AWS errors with codes etc
   204  			}
   205  			recordSets[key] = append(recordSets[key], change.ResourceRecordSet)
   206  		case route53.ChangeActionDelete:
   207  			if _, found := recordSets[key]; !found {
   208  				return nil, fmt.Errorf("Attempt to delete non-existent rrset %s", key) // TODO: Check other fields too
   209  			}
   210  			delete(recordSets, key)
   211  		case route53.ChangeActionUpsert:
   212  			recordSets[key] = []*route53.ResourceRecordSet{change.ResourceRecordSet}
   213  		}
   214  	}
   215  	r.recordSets[aws.StringValue(input.HostedZoneId)] = recordSets
   216  	return output, nil // TODO: We should ideally return status etc, but we don't' use that yet.
   217  }
   218  
   219  func (r *Route53APIStub) ListHostedZonesPagesWithContext(ctx context.Context, input *route53.ListHostedZonesInput, fn func(p *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error {
   220  	output := &route53.ListHostedZonesOutput{}
   221  	for _, zone := range r.zones {
   222  		output.HostedZones = append(output.HostedZones, zone)
   223  	}
   224  	lastPage := true
   225  	fn(output, lastPage)
   226  	return nil
   227  }
   228  
   229  func (r *Route53APIStub) CreateHostedZoneWithContext(ctx context.Context, input *route53.CreateHostedZoneInput, opts ...request.Option) (*route53.CreateHostedZoneOutput, error) {
   230  	name := aws.StringValue(input.Name)
   231  	id := "/hostedzone/" + name
   232  	if _, ok := r.zones[id]; ok {
   233  		return nil, fmt.Errorf("Error creating hosted DNS zone: %s already exists", id)
   234  	}
   235  	r.zones[id] = &route53.HostedZone{
   236  		Id:     aws.String(id),
   237  		Name:   aws.String(name),
   238  		Config: input.HostedZoneConfig,
   239  	}
   240  	return &route53.CreateHostedZoneOutput{HostedZone: r.zones[id]}, nil
   241  }
   242  
   243  type dynamicMock struct {
   244  	mock.Mock
   245  }
   246  
   247  func (m *dynamicMock) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) {
   248  	args := m.Called(input)
   249  	if args.Get(0) != nil {
   250  		return args.Get(0).(*route53.ChangeResourceRecordSetsOutput), args.Error(1)
   251  	}
   252  	return nil, args.Error(1)
   253  }
   254  
   255  func (m *dynamicMock) isMocked(method string, arguments ...interface{}) bool {
   256  	for _, call := range m.ExpectedCalls {
   257  		if call.Method == method && call.Repeatability > -1 {
   258  			_, diffCount := call.Arguments.Diff(arguments)
   259  			if diffCount == 0 {
   260  				return true
   261  			}
   262  		}
   263  	}
   264  	return false
   265  }
   266  
   267  func TestAWSZones(t *testing.T) {
   268  	publicZones := map[string]*route53.HostedZone{
   269  		"/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.": {
   270  			Id:   aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."),
   271  			Name: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."),
   272  		},
   273  		"/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.": {
   274  			Id:   aws.String("/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."),
   275  			Name: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."),
   276  		},
   277  	}
   278  
   279  	privateZones := map[string]*route53.HostedZone{
   280  		"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.": {
   281  			Id:   aws.String("/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."),
   282  			Name: aws.String("zone-3.ext-dns-test-2.teapot.zalan.do."),
   283  		},
   284  	}
   285  
   286  	allZones := map[string]*route53.HostedZone{}
   287  	for k, v := range publicZones {
   288  		allZones[k] = v
   289  	}
   290  	for k, v := range privateZones {
   291  		allZones[k] = v
   292  	}
   293  
   294  	noZones := map[string]*route53.HostedZone{}
   295  
   296  	for _, ti := range []struct {
   297  		msg            string
   298  		zoneIDFilter   provider.ZoneIDFilter
   299  		zoneTypeFilter provider.ZoneTypeFilter
   300  		zoneTagFilter  provider.ZoneTagFilter
   301  		expectedZones  map[string]*route53.HostedZone
   302  	}{
   303  		{"no filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), allZones},
   304  		{"public filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("public"), provider.NewZoneTagFilter([]string{}), publicZones},
   305  		{"private filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("private"), provider.NewZoneTagFilter([]string{}), privateZones},
   306  		{"unknown filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("unknown"), provider.NewZoneTagFilter([]string{}), noZones},
   307  		{"zone id filter", provider.NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), privateZones},
   308  		{"tag filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{"zone=3"}), privateZones},
   309  	} {
   310  		provider, _ := newAWSProviderWithTagFilter(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, nil)
   311  
   312  		zones, err := provider.Zones(context.Background())
   313  		require.NoError(t, err)
   314  
   315  		validateAWSZones(t, zones, ti.expectedZones)
   316  	}
   317  }
   318  
   319  func TestAWSRecordsFilter(t *testing.T) {
   320  	provider, _ := newAWSProvider(t, endpoint.DomainFilter{}, provider.ZoneIDFilter{}, provider.ZoneTypeFilter{}, false, false, nil)
   321  	domainFilter := provider.GetDomainFilter()
   322  	assert.NotNil(t, domainFilter)
   323  	require.IsType(t, endpoint.DomainFilter{}, domainFilter)
   324  	count := 0
   325  	filters := domainFilter.Filters
   326  	for _, tld := range []string{
   327  		"zone-4.ext-dns-test-3.teapot.zalan.do",
   328  		".zone-4.ext-dns-test-3.teapot.zalan.do",
   329  		"zone-2.ext-dns-test-2.teapot.zalan.do",
   330  		".zone-2.ext-dns-test-2.teapot.zalan.do",
   331  		"zone-3.ext-dns-test-2.teapot.zalan.do",
   332  		".zone-3.ext-dns-test-2.teapot.zalan.do",
   333  		"zone-4.ext-dns-test-3.teapot.zalan.do",
   334  		".zone-4.ext-dns-test-3.teapot.zalan.do",
   335  	} {
   336  		assert.Contains(t, filters, tld)
   337  		count++
   338  	}
   339  	assert.Len(t, filters, count)
   340  }
   341  
   342  func TestAWSRecords(t *testing.T) {
   343  	provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, []*route53.ResourceRecordSet{
   344  		{
   345  			Name:            aws.String("list-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   346  			Type:            aws.String(route53.RRTypeA),
   347  			TTL:             aws.Int64(recordTTL),
   348  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   349  		},
   350  		{
   351  			Name:            aws.String("list-test.zone-2.ext-dns-test-2.teapot.zalan.do."),
   352  			Type:            aws.String(route53.RRTypeA),
   353  			TTL:             aws.Int64(recordTTL),
   354  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}},
   355  		},
   356  		{
   357  			Name:            aws.String("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do."),
   358  			Type:            aws.String(route53.RRTypeA),
   359  			TTL:             aws.Int64(recordTTL),
   360  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}},
   361  		},
   362  		{
   363  			Name: aws.String("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."),
   364  			Type: aws.String(route53.RRTypeA),
   365  			AliasTarget: &route53.AliasTarget{
   366  				DNSName:              aws.String("foo.eu-central-1.elb.amazonaws.com."),
   367  				EvaluateTargetHealth: aws.Bool(false),
   368  				HostedZoneId:         aws.String("Z215JYRZR1TBD5"),
   369  			},
   370  		},
   371  		{
   372  			Name: aws.String("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."),
   373  			Type: aws.String(route53.RRTypeA),
   374  			AliasTarget: &route53.AliasTarget{
   375  				DNSName:              aws.String("foo.eu-central-1.elb.amazonaws.com."),
   376  				EvaluateTargetHealth: aws.Bool(false),
   377  				HostedZoneId:         aws.String("Z215JYRZR1TBD5"),
   378  			},
   379  		},
   380  		{
   381  			Name: aws.String("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do."),
   382  			Type: aws.String(route53.RRTypeA),
   383  			AliasTarget: &route53.AliasTarget{
   384  				DNSName:              aws.String("foo.eu-central-1.elb.amazonaws.com."),
   385  				EvaluateTargetHealth: aws.Bool(true),
   386  				HostedZoneId:         aws.String("Z215JYRZR1TBD5"),
   387  			},
   388  		},
   389  		{
   390  			Name:            aws.String("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do."),
   391  			Type:            aws.String(route53.RRTypeA),
   392  			TTL:             aws.Int64(recordTTL),
   393  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}},
   394  		},
   395  		{
   396  			Name:            aws.String("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do."),
   397  			Type:            aws.String(route53.RRTypeTxt),
   398  			TTL:             aws.Int64(recordTTL),
   399  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("random")}},
   400  		},
   401  		{
   402  			Name:            aws.String("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   403  			Type:            aws.String(route53.RRTypeA),
   404  			TTL:             aws.Int64(recordTTL),
   405  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   406  			SetIdentifier:   aws.String("test-set-1"),
   407  			Weight:          aws.Int64(10),
   408  		},
   409  		{
   410  			Name:            aws.String("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   411  			Type:            aws.String(route53.RRTypeA),
   412  			TTL:             aws.Int64(recordTTL),
   413  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}},
   414  			SetIdentifier:   aws.String("test-set-2"),
   415  			Weight:          aws.Int64(20),
   416  		},
   417  		{
   418  			Name:            aws.String("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   419  			Type:            aws.String(route53.RRTypeA),
   420  			TTL:             aws.Int64(recordTTL),
   421  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   422  			SetIdentifier:   aws.String("test-set"),
   423  			Region:          aws.String("us-east-1"),
   424  		},
   425  		{
   426  			Name:            aws.String("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   427  			Type:            aws.String(route53.RRTypeA),
   428  			TTL:             aws.Int64(recordTTL),
   429  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   430  			SetIdentifier:   aws.String("test-set"),
   431  			Failover:        aws.String("PRIMARY"),
   432  		},
   433  		{
   434  			Name:             aws.String("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   435  			Type:             aws.String(route53.RRTypeA),
   436  			TTL:              aws.Int64(recordTTL),
   437  			ResourceRecords:  []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   438  			SetIdentifier:    aws.String("test-set"),
   439  			MultiValueAnswer: aws.Bool(true),
   440  		},
   441  		{
   442  			Name:            aws.String("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   443  			Type:            aws.String(route53.RRTypeA),
   444  			TTL:             aws.Int64(recordTTL),
   445  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   446  			SetIdentifier:   aws.String("test-set-1"),
   447  			GeoLocation: &route53.GeoLocation{
   448  				ContinentCode: aws.String("EU"),
   449  			},
   450  		},
   451  		{
   452  			Name:            aws.String("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   453  			Type:            aws.String(route53.RRTypeA),
   454  			TTL:             aws.Int64(recordTTL),
   455  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}},
   456  			SetIdentifier:   aws.String("test-set-2"),
   457  			GeoLocation: &route53.GeoLocation{
   458  				CountryCode: aws.String("DE"),
   459  			},
   460  		},
   461  		{
   462  			Name:            aws.String("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   463  			Type:            aws.String(route53.RRTypeA),
   464  			TTL:             aws.Int64(recordTTL),
   465  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   466  			SetIdentifier:   aws.String("test-set-1"),
   467  			GeoLocation: &route53.GeoLocation{
   468  				SubdivisionCode: aws.String("NY"),
   469  			},
   470  		},
   471  		{
   472  			Name:            aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   473  			Type:            aws.String(route53.RRTypeCname),
   474  			TTL:             aws.Int64(recordTTL),
   475  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.example.com")}},
   476  			SetIdentifier:   aws.String("test-set-1"),
   477  			HealthCheckId:   aws.String("foo-bar-healthcheck-id"),
   478  			Weight:          aws.Int64(10),
   479  		},
   480  		{
   481  			Name:            aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   482  			Type:            aws.String(route53.RRTypeA),
   483  			TTL:             aws.Int64(recordTTL),
   484  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}},
   485  			SetIdentifier:   aws.String("test-set-2"),
   486  			HealthCheckId:   aws.String("abc-def-healthcheck-id"),
   487  			Weight:          aws.Int64(20),
   488  		},
   489  		{
   490  			Name:            aws.String("mail.zone-1.ext-dns-test-2.teapot.zalan.do."),
   491  			Type:            aws.String(route53.RRTypeMx),
   492  			TTL:             aws.Int64(recordTTL),
   493  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.example.com")}, {Value: aws.String("20 mailhost2.example.com")}},
   494  		},
   495  	})
   496  
   497  	records, err := provider.Records(context.Background())
   498  	require.NoError(t, err)
   499  
   500  	validateEndpoints(t, provider, records, []*endpoint.Endpoint{
   501  		endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
   502  		endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
   503  		endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
   504  		endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"),
   505  		endpoint.NewEndpointWithTTL("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"),
   506  		endpoint.NewEndpointWithTTL("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"),
   507  		endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
   508  		endpoint.NewEndpointWithTTL("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "random"),
   509  		endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10"),
   510  		endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20"),
   511  		endpoint.NewEndpointWithTTL("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificRegion, "us-east-1"),
   512  		endpoint.NewEndpointWithTTL("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificFailover, "PRIMARY"),
   513  		endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""),
   514  		endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"),
   515  		endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"),
   516  		endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"),
   517  		endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id").WithProviderSpecific(providerSpecificAlias, "false"),
   518  		endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"),
   519  		endpoint.NewEndpointWithTTL("mail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 mailhost1.example.com", "20 mailhost2.example.com"),
   520  	})
   521  }
   522  
   523  func TestAWSAdjustEndpoints(t *testing.T) {
   524  	provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil)
   525  
   526  	records := []*endpoint.Endpoint{
   527  		endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
   528  		endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"),
   529  		endpoint.NewEndpointWithTTL("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, 60, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true"),
   530  		endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
   531  		endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"),
   532  		endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health
   533  		endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
   534  	}
   535  
   536  	records, err := provider.AdjustEndpoints(records)
   537  	assert.NoError(t, err)
   538  
   539  	validateEndpoints(t, provider, records, []*endpoint.Endpoint{
   540  		endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
   541  		endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com").WithProviderSpecific(providerSpecificAlias, "false"),
   542  		endpoint.NewEndpointWithTTL("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, 300, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
   543  		endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
   544  		endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"),
   545  		endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health
   546  		endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
   547  	})
   548  }
   549  
   550  func TestAWSApplyChanges(t *testing.T) {
   551  	tests := []struct {
   552  		name       string
   553  		setup      func(p *AWSProvider) context.Context
   554  		listRRSets int
   555  	}{
   556  		{"no cache", func(p *AWSProvider) context.Context { return context.Background() }, 0},
   557  		{"cached", func(p *AWSProvider) context.Context {
   558  			ctx := context.Background()
   559  			records, err := p.Records(ctx)
   560  			require.NoError(t, err)
   561  			return context.WithValue(ctx, provider.RecordsContextKey, records)
   562  		}, 0},
   563  	}
   564  
   565  	for _, tt := range tests {
   566  		provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*route53.ResourceRecordSet{
   567  			{
   568  				Name:            aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   569  				Type:            aws.String(route53.RRTypeA),
   570  				TTL:             aws.Int64(recordTTL),
   571  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}},
   572  			},
   573  			{
   574  				Name:            aws.String("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   575  				Type:            aws.String(route53.RRTypeA),
   576  				TTL:             aws.Int64(recordTTL),
   577  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}},
   578  			},
   579  			{
   580  				Name:            aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."),
   581  				Type:            aws.String(route53.RRTypeA),
   582  				TTL:             aws.Int64(recordTTL),
   583  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}},
   584  			},
   585  			{
   586  				Name:            aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."),
   587  				Type:            aws.String(route53.RRTypeA),
   588  				TTL:             aws.Int64(recordTTL),
   589  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}},
   590  			},
   591  			{
   592  				Name:            aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   593  				Type:            aws.String(route53.RRTypeA),
   594  				TTL:             aws.Int64(recordTTL),
   595  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.1.1.1")}},
   596  			},
   597  			{
   598  				Name: aws.String("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   599  				Type: aws.String(route53.RRTypeA),
   600  				AliasTarget: &route53.AliasTarget{
   601  					DNSName:              aws.String("foo.eu-central-1.elb.amazonaws.com."),
   602  					EvaluateTargetHealth: aws.Bool(true),
   603  					HostedZoneId:         aws.String("Z215JYRZR1TBD5"),
   604  				},
   605  			},
   606  			{
   607  				Name:            aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   608  				Type:            aws.String(route53.RRTypeCname),
   609  				TTL:             aws.Int64(recordTTL),
   610  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}},
   611  			},
   612  			{
   613  				Name:            aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   614  				Type:            aws.String(route53.RRTypeCname),
   615  				TTL:             aws.Int64(recordTTL),
   616  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}},
   617  			},
   618  			{
   619  				Name:            aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."),
   620  				Type:            aws.String(route53.RRTypeCname),
   621  				TTL:             aws.Int64(recordTTL),
   622  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}},
   623  			},
   624  			{
   625  				Name:            aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."),
   626  				Type:            aws.String(route53.RRTypeCname),
   627  				TTL:             aws.Int64(recordTTL),
   628  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}},
   629  			},
   630  			{
   631  				Name:            aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."),
   632  				Type:            aws.String(route53.RRTypeA),
   633  				TTL:             aws.Int64(recordTTL),
   634  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}},
   635  			},
   636  			{
   637  				Name:            aws.String("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."),
   638  				Type:            aws.String(route53.RRTypeA),
   639  				TTL:             aws.Int64(recordTTL),
   640  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}},
   641  			},
   642  			{
   643  				Name:            aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."),
   644  				Type:            aws.String(route53.RRTypeA),
   645  				TTL:             aws.Int64(recordTTL),
   646  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   647  				SetIdentifier:   aws.String("weighted-to-simple"),
   648  				Weight:          aws.Int64(10),
   649  			},
   650  			{
   651  				Name:            aws.String("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do."),
   652  				Type:            aws.String(route53.RRTypeA),
   653  				TTL:             aws.Int64(recordTTL),
   654  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   655  			},
   656  			{
   657  				Name:            aws.String("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do."),
   658  				Type:            aws.String(route53.RRTypeA),
   659  				TTL:             aws.Int64(recordTTL),
   660  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   661  				SetIdentifier:   aws.String("policy-change"),
   662  				Weight:          aws.Int64(10),
   663  			},
   664  			{
   665  				Name:            aws.String("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do."),
   666  				Type:            aws.String(route53.RRTypeA),
   667  				TTL:             aws.Int64(recordTTL),
   668  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   669  				SetIdentifier:   aws.String("before"),
   670  				Weight:          aws.Int64(10),
   671  			},
   672  			{
   673  				Name:            aws.String("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do."),
   674  				Type:            aws.String(route53.RRTypeA),
   675  				TTL:             aws.Int64(recordTTL),
   676  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   677  				SetIdentifier:   aws.String("no-change"),
   678  				Weight:          aws.Int64(10),
   679  			},
   680  			{
   681  				Name:            aws.String("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."),
   682  				Type:            aws.String(route53.RRTypeMx),
   683  				TTL:             aws.Int64(recordTTL),
   684  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost2.bar.elb.amazonaws.com")}},
   685  			},
   686  			{
   687  				Name:            aws.String("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."),
   688  				Type:            aws.String(route53.RRTypeMx),
   689  				TTL:             aws.Int64(recordTTL),
   690  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("30 mailhost1.foo.elb.amazonaws.com")}},
   691  			},
   692  		})
   693  
   694  		createRecords := []*endpoint.Endpoint{
   695  			endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
   696  			endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
   697  			endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
   698  			endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
   699  			endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
   700  			endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com"),
   701  		}
   702  
   703  		currentRecords := []*endpoint.Endpoint{
   704  			endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
   705  			endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
   706  			endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"),
   707  			endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
   708  			endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
   709  			endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
   710  			endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
   711  			endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("weighted-to-simple").WithProviderSpecific(providerSpecificWeight, "10"),
   712  			endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
   713  			endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificWeight, "10"),
   714  			endpoint.NewEndpoint("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("before").WithProviderSpecific(providerSpecificWeight, "10"),
   715  			endpoint.NewEndpoint("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("no-change").WithProviderSpecific(providerSpecificWeight, "10"),
   716  			endpoint.NewEndpoint("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost2.bar.elb.amazonaws.com"),
   717  		}
   718  		updatedRecords := []*endpoint.Endpoint{
   719  			endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
   720  			endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
   721  			endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
   722  			endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "my-internal-host.example.com"),
   723  			endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
   724  			endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
   725  			endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
   726  			endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
   727  			endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("simple-to-weighted").WithProviderSpecific(providerSpecificWeight, "10"),
   728  			endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificRegion, "us-east-1"),
   729  			endpoint.NewEndpoint("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("after").WithProviderSpecific(providerSpecificWeight, "10"),
   730  			endpoint.NewEndpoint("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("no-change").WithProviderSpecific(providerSpecificWeight, "20"),
   731  			endpoint.NewEndpoint("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "20 mailhost3.foo.elb.amazonaws.com"),
   732  		}
   733  
   734  		deleteRecords := []*endpoint.Endpoint{
   735  			endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
   736  			endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
   737  			endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
   738  			endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
   739  			endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
   740  			endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mailhost1.foo.elb.amazonaws.com"),
   741  		}
   742  
   743  		changes := &plan.Changes{
   744  			Create:    createRecords,
   745  			UpdateNew: updatedRecords,
   746  			UpdateOld: currentRecords,
   747  			Delete:    deleteRecords,
   748  		}
   749  
   750  		ctx := tt.setup(provider)
   751  
   752  		provider.zonesCache = &zonesListCache{duration: 0 * time.Minute}
   753  		counter := NewRoute53APICounter(provider.client)
   754  		provider.client = counter
   755  		require.NoError(t, provider.ApplyChanges(ctx, changes))
   756  
   757  		assert.Equal(t, 1, counter.calls["ListHostedZonesPages"], tt.name)
   758  		assert.Equal(t, tt.listRRSets, counter.calls["ListResourceRecordSetsPages"], tt.name)
   759  
   760  		validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{
   761  			{
   762  				Name:            aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   763  				Type:            aws.String(route53.RRTypeA),
   764  				TTL:             aws.Int64(recordTTL),
   765  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}},
   766  			},
   767  			{
   768  				Name:            aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   769  				Type:            aws.String(route53.RRTypeA),
   770  				TTL:             aws.Int64(recordTTL),
   771  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   772  			},
   773  			{
   774  				Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   775  				Type: aws.String(route53.RRTypeA),
   776  				AliasTarget: &route53.AliasTarget{
   777  					DNSName:              aws.String("foo.elb.amazonaws.com."),
   778  					EvaluateTargetHealth: aws.Bool(true),
   779  					HostedZoneId:         aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."),
   780  				},
   781  			},
   782  			{
   783  				Name:            aws.String("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   784  				Type:            aws.String(route53.RRTypeCname),
   785  				TTL:             aws.Int64(recordTTL),
   786  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("my-internal-host.example.com")}},
   787  			},
   788  			{
   789  				Name:            aws.String("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   790  				Type:            aws.String(route53.RRTypeCname),
   791  				TTL:             aws.Int64(recordTTL),
   792  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}},
   793  			},
   794  			{
   795  				Name:            aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   796  				Type:            aws.String(route53.RRTypeCname),
   797  				TTL:             aws.Int64(recordTTL),
   798  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("baz.elb.amazonaws.com")}},
   799  			},
   800  			{
   801  				Name:            aws.String("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."),
   802  				Type:            aws.String(route53.RRTypeCname),
   803  				TTL:             aws.Int64(recordTTL),
   804  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}},
   805  			},
   806  			{
   807  				Name:            aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."),
   808  				Type:            aws.String(route53.RRTypeCname),
   809  				TTL:             aws.Int64(recordTTL),
   810  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("baz.elb.amazonaws.com")}},
   811  			},
   812  			{
   813  				Name:            aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."),
   814  				Type:            aws.String(route53.RRTypeA),
   815  				TTL:             aws.Int64(recordTTL),
   816  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   817  			},
   818  			{
   819  				Name:            aws.String("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do."),
   820  				Type:            aws.String(route53.RRTypeA),
   821  				TTL:             aws.Int64(recordTTL),
   822  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   823  				SetIdentifier:   aws.String("simple-to-weighted"),
   824  				Weight:          aws.Int64(10),
   825  			},
   826  			{
   827  				Name:            aws.String("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do."),
   828  				Type:            aws.String(route53.RRTypeA),
   829  				TTL:             aws.Int64(recordTTL),
   830  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   831  				SetIdentifier:   aws.String("policy-change"),
   832  				Region:          aws.String("us-east-1"),
   833  			},
   834  			{
   835  				Name:            aws.String("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do."),
   836  				Type:            aws.String(route53.RRTypeA),
   837  				TTL:             aws.Int64(recordTTL),
   838  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   839  				SetIdentifier:   aws.String("after"),
   840  				Weight:          aws.Int64(10),
   841  			},
   842  			{
   843  				Name:            aws.String("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do."),
   844  				Type:            aws.String(route53.RRTypeA),
   845  				TTL:             aws.Int64(recordTTL),
   846  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}},
   847  				SetIdentifier:   aws.String("no-change"),
   848  				Weight:          aws.Int64(20),
   849  			},
   850  			{
   851  				Name:            aws.String("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."),
   852  				Type:            aws.String(route53.RRTypeMx),
   853  				TTL:             aws.Int64(recordTTL),
   854  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}},
   855  			},
   856  		})
   857  		validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{
   858  			{
   859  				Name:            aws.String("create-test.zone-2.ext-dns-test-2.teapot.zalan.do."),
   860  				Type:            aws.String(route53.RRTypeA),
   861  				TTL:             aws.Int64(recordTTL),
   862  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}},
   863  			},
   864  			{
   865  				Name:            aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."),
   866  				Type:            aws.String(route53.RRTypeA),
   867  				TTL:             aws.Int64(recordTTL),
   868  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}},
   869  			},
   870  			{
   871  				Name:            aws.String("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."),
   872  				Type:            aws.String(route53.RRTypeA),
   873  				TTL:             aws.Int64(recordTTL),
   874  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}},
   875  			},
   876  			{
   877  				Name:            aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."),
   878  				Type:            aws.String(route53.RRTypeA),
   879  				TTL:             aws.Int64(recordTTL),
   880  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}},
   881  			},
   882  			{
   883  				Name:            aws.String("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."),
   884  				Type:            aws.String(route53.RRTypeMx),
   885  				TTL:             aws.Int64(recordTTL),
   886  				ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("20 mailhost3.foo.elb.amazonaws.com")}},
   887  			},
   888  		})
   889  	}
   890  }
   891  
   892  func TestAWSApplyChangesDryRun(t *testing.T) {
   893  	originalRecords := []*route53.ResourceRecordSet{
   894  		{
   895  			Name:            aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   896  			Type:            aws.String(route53.RRTypeA),
   897  			TTL:             aws.Int64(recordTTL),
   898  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}},
   899  		},
   900  		{
   901  			Name:            aws.String("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
   902  			Type:            aws.String(route53.RRTypeA),
   903  			TTL:             aws.Int64(recordTTL),
   904  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}},
   905  		},
   906  		{
   907  			Name:            aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."),
   908  			Type:            aws.String(route53.RRTypeA),
   909  			TTL:             aws.Int64(recordTTL),
   910  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}},
   911  		},
   912  		{
   913  			Name:            aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."),
   914  			Type:            aws.String(route53.RRTypeA),
   915  			TTL:             aws.Int64(recordTTL),
   916  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}},
   917  		},
   918  		{
   919  			Name:            aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   920  			Type:            aws.String(route53.RRTypeA),
   921  			TTL:             aws.Int64(recordTTL),
   922  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.1.1.1")}},
   923  		},
   924  		{
   925  			Name:            aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   926  			Type:            aws.String(route53.RRTypeCname),
   927  			TTL:             aws.Int64(recordTTL),
   928  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}},
   929  		},
   930  		{
   931  			Name:            aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."),
   932  			Type:            aws.String(route53.RRTypeCname),
   933  			TTL:             aws.Int64(recordTTL),
   934  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}},
   935  		},
   936  		{
   937  			Name:            aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."),
   938  			Type:            aws.String(route53.RRTypeCname),
   939  			TTL:             aws.Int64(recordTTL),
   940  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}},
   941  		},
   942  		{
   943  			Name:            aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."),
   944  			Type:            aws.String(route53.RRTypeCname),
   945  			TTL:             aws.Int64(recordTTL),
   946  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}},
   947  		},
   948  		{
   949  			Name:            aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."),
   950  			Type:            aws.String(route53.RRTypeA),
   951  			TTL:             aws.Int64(recordTTL),
   952  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}},
   953  		},
   954  		{
   955  			Name:            aws.String("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."),
   956  			Type:            aws.String(route53.RRTypeA),
   957  			TTL:             aws.Int64(recordTTL),
   958  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}},
   959  		},
   960  		{
   961  			Name:            aws.String("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."),
   962  			Type:            aws.String(route53.RRTypeMx),
   963  			TTL:             aws.Int64(recordTTL),
   964  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("20 mail.foo.elb.amazonaws.com")}},
   965  		},
   966  		{
   967  			Name:            aws.String("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."),
   968  			Type:            aws.String(route53.RRTypeMx),
   969  			TTL:             aws.Int64(recordTTL),
   970  			ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mail.bar.elb.amazonaws.com")}},
   971  		},
   972  	}
   973  
   974  	provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalRecords)
   975  
   976  	createRecords := []*endpoint.Endpoint{
   977  		endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
   978  		endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
   979  		endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
   980  		endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
   981  		endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
   982  		endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mail.foo.elb.amazonaws.com"),
   983  	}
   984  
   985  	currentRecords := []*endpoint.Endpoint{
   986  		endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
   987  		endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
   988  		endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"),
   989  		endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
   990  		endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
   991  		endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
   992  		endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "20 mail.foo.elb.amazonaws.com"),
   993  	}
   994  	updatedRecords := []*endpoint.Endpoint{
   995  		endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
   996  		endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
   997  		endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
   998  		endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
   999  		endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
  1000  		endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
  1001  		endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mail.bar.elb.amazonaws.com"),
  1002  	}
  1003  
  1004  	deleteRecords := []*endpoint.Endpoint{
  1005  		endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
  1006  		endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
  1007  		endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
  1008  		endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
  1009  		endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
  1010  		endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mail.bar.elb.amazonaws.com"),
  1011  	}
  1012  
  1013  	changes := &plan.Changes{
  1014  		Create:    createRecords,
  1015  		UpdateNew: updatedRecords,
  1016  		UpdateOld: currentRecords,
  1017  		Delete:    deleteRecords,
  1018  	}
  1019  
  1020  	ctx := context.Background()
  1021  
  1022  	require.NoError(t, provider.ApplyChanges(ctx, changes))
  1023  
  1024  	validateRecords(t,
  1025  		append(
  1026  			listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."),
  1027  			listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.")...),
  1028  		originalRecords)
  1029  }
  1030  
  1031  func TestAWSChangesByZones(t *testing.T) {
  1032  	changes := Route53Changes{
  1033  		{
  1034  			Change: route53.Change{
  1035  				Action: aws.String(route53.ChangeActionCreate),
  1036  				ResourceRecordSet: &route53.ResourceRecordSet{
  1037  					Name: aws.String("qux.foo.example.org"), TTL: aws.Int64(1),
  1038  				},
  1039  			},
  1040  		},
  1041  		{
  1042  			Change: route53.Change{
  1043  				Action: aws.String(route53.ChangeActionCreate),
  1044  				ResourceRecordSet: &route53.ResourceRecordSet{
  1045  					Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2),
  1046  				},
  1047  			},
  1048  		},
  1049  		{
  1050  			Change: route53.Change{
  1051  				Action: aws.String(route53.ChangeActionDelete),
  1052  				ResourceRecordSet: &route53.ResourceRecordSet{
  1053  					Name: aws.String("wambo.foo.example.org"), TTL: aws.Int64(10),
  1054  				},
  1055  			},
  1056  		},
  1057  		{
  1058  			Change: route53.Change{
  1059  				Action: aws.String(route53.ChangeActionDelete),
  1060  				ResourceRecordSet: &route53.ResourceRecordSet{
  1061  					Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20),
  1062  				},
  1063  			},
  1064  		},
  1065  	}
  1066  
  1067  	zones := map[string]*route53.HostedZone{
  1068  		"foo-example-org": {
  1069  			Id:   aws.String("foo-example-org"),
  1070  			Name: aws.String("foo.example.org."),
  1071  		},
  1072  		"bar-example-org": {
  1073  			Id:   aws.String("bar-example-org"),
  1074  			Name: aws.String("bar.example.org."),
  1075  		},
  1076  		"bar-example-org-private": {
  1077  			Id:     aws.String("bar-example-org-private"),
  1078  			Name:   aws.String("bar.example.org."),
  1079  			Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)},
  1080  		},
  1081  		"baz-example-org": {
  1082  			Id:   aws.String("baz-example-org"),
  1083  			Name: aws.String("baz.example.org."),
  1084  		},
  1085  	}
  1086  
  1087  	changesByZone := changesByZone(zones, changes)
  1088  	require.Len(t, changesByZone, 3)
  1089  
  1090  	validateAWSChangeRecords(t, changesByZone["foo-example-org"], Route53Changes{
  1091  		{
  1092  			Change: route53.Change{
  1093  				Action: aws.String(route53.ChangeActionCreate),
  1094  				ResourceRecordSet: &route53.ResourceRecordSet{
  1095  					Name: aws.String("qux.foo.example.org"), TTL: aws.Int64(1),
  1096  				},
  1097  			},
  1098  		},
  1099  		{
  1100  			Change: route53.Change{
  1101  				Action: aws.String(route53.ChangeActionDelete),
  1102  				ResourceRecordSet: &route53.ResourceRecordSet{
  1103  					Name: aws.String("wambo.foo.example.org"), TTL: aws.Int64(10),
  1104  				},
  1105  			},
  1106  		},
  1107  	})
  1108  
  1109  	validateAWSChangeRecords(t, changesByZone["bar-example-org"], Route53Changes{
  1110  		{
  1111  			Change: route53.Change{
  1112  				Action: aws.String(route53.ChangeActionCreate),
  1113  				ResourceRecordSet: &route53.ResourceRecordSet{
  1114  					Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2),
  1115  				},
  1116  			},
  1117  		},
  1118  		{
  1119  			Change: route53.Change{
  1120  				Action: aws.String(route53.ChangeActionDelete),
  1121  				ResourceRecordSet: &route53.ResourceRecordSet{
  1122  					Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20),
  1123  				},
  1124  			},
  1125  		},
  1126  	})
  1127  
  1128  	validateAWSChangeRecords(t, changesByZone["bar-example-org-private"], Route53Changes{
  1129  		{
  1130  			Change: route53.Change{
  1131  				Action: aws.String(route53.ChangeActionCreate),
  1132  				ResourceRecordSet: &route53.ResourceRecordSet{
  1133  					Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2),
  1134  				},
  1135  			},
  1136  		},
  1137  		{
  1138  			Change: route53.Change{
  1139  				Action: aws.String(route53.ChangeActionDelete),
  1140  				ResourceRecordSet: &route53.ResourceRecordSet{
  1141  					Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20),
  1142  				},
  1143  			},
  1144  		},
  1145  	})
  1146  }
  1147  
  1148  func TestAWSsubmitChanges(t *testing.T) {
  1149  	provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil)
  1150  	const subnets = 16
  1151  	const hosts = defaultBatchChangeSize / subnets
  1152  
  1153  	endpoints := make([]*endpoint.Endpoint, 0)
  1154  	for i := 0; i < subnets; i++ {
  1155  		for j := 1; j < (hosts + 1); j++ {
  1156  			hostname := fmt.Sprintf("subnet%dhost%d.zone-1.ext-dns-test-2.teapot.zalan.do", i, j)
  1157  			ip := fmt.Sprintf("1.1.%d.%d", i, j)
  1158  			ep := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, endpoint.TTL(recordTTL), ip)
  1159  			endpoints = append(endpoints, ep)
  1160  		}
  1161  	}
  1162  
  1163  	ctx := context.Background()
  1164  	zones, _ := provider.Zones(ctx)
  1165  	records, _ := provider.Records(ctx)
  1166  	cs := make(Route53Changes, 0, len(endpoints))
  1167  	cs = append(cs, provider.newChanges(route53.ChangeActionCreate, endpoints)...)
  1168  
  1169  	require.NoError(t, provider.submitChanges(ctx, cs, zones))
  1170  
  1171  	records, err := provider.Records(ctx)
  1172  	require.NoError(t, err)
  1173  
  1174  	validateEndpoints(t, provider, records, endpoints)
  1175  }
  1176  
  1177  func TestAWSsubmitChangesError(t *testing.T) {
  1178  	provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil)
  1179  	clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure"))
  1180  
  1181  	ctx := context.Background()
  1182  	zones, err := provider.Zones(ctx)
  1183  	require.NoError(t, err)
  1184  
  1185  	ep := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1")
  1186  	cs := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep})
  1187  
  1188  	require.Error(t, provider.submitChanges(ctx, cs, zones))
  1189  }
  1190  
  1191  func TestAWSsubmitChangesRetryOnError(t *testing.T) {
  1192  	provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil)
  1193  
  1194  	ctx := context.Background()
  1195  	zones, err := provider.Zones(ctx)
  1196  	require.NoError(t, err)
  1197  
  1198  	ep1 := endpoint.NewEndpointWithTTL("success.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1")
  1199  	ep2 := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.2")
  1200  	ep3 := endpoint.NewEndpointWithTTL("success2.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.3")
  1201  
  1202  	ep2txt := endpoint.NewEndpointWithTTL("fail__edns_housekeeping.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "something") // "__edns_housekeeping" is the TXT suffix
  1203  	ep2txt.Labels = map[string]string{
  1204  		endpoint.OwnedRecordLabelKey: "fail.zone-1.ext-dns-test-2.teapot.zalan.do",
  1205  	}
  1206  
  1207  	// "success" and "fail" are created in the first step, both are submitted in the same batch; this should fail
  1208  	cs1 := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep2, ep2txt, ep1})
  1209  	input1 := &route53.ChangeResourceRecordSetsInput{
  1210  		HostedZoneId: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."),
  1211  		ChangeBatch: &route53.ChangeBatch{
  1212  			Changes: cs1.Route53Changes(),
  1213  		},
  1214  	}
  1215  	clientStub.MockMethod("ChangeResourceRecordSets", input1).Return(nil, fmt.Errorf("Mock route53 failure"))
  1216  
  1217  	// because of the failure, changes will be retried one by one; make "fail" submitted in its own batch fail as well
  1218  	cs2 := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep2, ep2txt})
  1219  	input2 := &route53.ChangeResourceRecordSetsInput{
  1220  		HostedZoneId: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."),
  1221  		ChangeBatch: &route53.ChangeBatch{
  1222  			Changes: cs2.Route53Changes(),
  1223  		},
  1224  	}
  1225  	clientStub.MockMethod("ChangeResourceRecordSets", input2).Return(nil, fmt.Errorf("Mock route53 failure"))
  1226  
  1227  	// "success" should have been created, verify that we still get an error because "fail" failed
  1228  	require.Error(t, provider.submitChanges(ctx, cs1, zones))
  1229  
  1230  	// assert that "success" was successfully created and "fail" and its TXT record were not
  1231  	records, err := provider.Records(ctx)
  1232  	require.NoError(t, err)
  1233  	require.True(t, containsRecordWithDNSName(records, "success.zone-1.ext-dns-test-2.teapot.zalan.do"))
  1234  	require.False(t, containsRecordWithDNSName(records, "fail.zone-1.ext-dns-test-2.teapot.zalan.do"))
  1235  	require.False(t, containsRecordWithDNSName(records, "fail__edns_housekeeping.zone-1.ext-dns-test-2.teapot.zalan.do"))
  1236  
  1237  	// next batch should contain "fail" and "success2", should succeed this time
  1238  	cs3 := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep2, ep2txt, ep3})
  1239  	require.NoError(t, provider.submitChanges(ctx, cs3, zones))
  1240  
  1241  	// verify all records are there
  1242  	records, err = provider.Records(ctx)
  1243  	require.NoError(t, err)
  1244  	require.True(t, containsRecordWithDNSName(records, "success.zone-1.ext-dns-test-2.teapot.zalan.do"))
  1245  	require.True(t, containsRecordWithDNSName(records, "fail.zone-1.ext-dns-test-2.teapot.zalan.do"))
  1246  	require.True(t, containsRecordWithDNSName(records, "success2.zone-1.ext-dns-test-2.teapot.zalan.do"))
  1247  	require.True(t, containsRecordWithDNSName(records, "fail__edns_housekeeping.zone-1.ext-dns-test-2.teapot.zalan.do"))
  1248  }
  1249  
  1250  func TestAWSBatchChangeSet(t *testing.T) {
  1251  	var cs Route53Changes
  1252  
  1253  	for i := 1; i <= defaultBatchChangeSize; i += 2 {
  1254  		cs = append(cs, &Route53Change{
  1255  			Change: route53.Change{
  1256  				Action: aws.String(route53.ChangeActionCreate),
  1257  				ResourceRecordSet: &route53.ResourceRecordSet{
  1258  					Name: aws.String(fmt.Sprintf("host-%d", i)),
  1259  					Type: aws.String("A"),
  1260  				},
  1261  			},
  1262  		})
  1263  		cs = append(cs, &Route53Change{
  1264  			Change: route53.Change{
  1265  				Action: aws.String(route53.ChangeActionCreate),
  1266  				ResourceRecordSet: &route53.ResourceRecordSet{
  1267  					Name: aws.String(fmt.Sprintf("host-%d", i)),
  1268  					Type: aws.String("TXT"),
  1269  				},
  1270  			},
  1271  		})
  1272  	}
  1273  
  1274  	batchCs := batchChangeSet(cs, defaultBatchChangeSize, defaultBatchChangeSizeBytes, defaultBatchChangeSizeValues)
  1275  
  1276  	require.Equal(t, 1, len(batchCs))
  1277  
  1278  	// sorting cs not needed as it should be returned as is
  1279  	validateAWSChangeRecords(t, batchCs[0], cs)
  1280  }
  1281  
  1282  func TestAWSBatchChangeSetExceeding(t *testing.T) {
  1283  	var cs Route53Changes
  1284  	const testCount = 50
  1285  	const testLimit = 11
  1286  	const expectedBatchCount = 5
  1287  	const expectedChangesCount = 10
  1288  
  1289  	for i := 1; i <= testCount; i += 2 {
  1290  		cs = append(cs,
  1291  			&Route53Change{
  1292  				Change: route53.Change{
  1293  					Action: aws.String(route53.ChangeActionCreate),
  1294  					ResourceRecordSet: &route53.ResourceRecordSet{
  1295  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1296  						Type: aws.String("A"),
  1297  					},
  1298  				},
  1299  			},
  1300  			&Route53Change{
  1301  				Change: route53.Change{
  1302  					Action: aws.String(route53.ChangeActionCreate),
  1303  					ResourceRecordSet: &route53.ResourceRecordSet{
  1304  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1305  						Type: aws.String("TXT"),
  1306  					},
  1307  				},
  1308  			},
  1309  		)
  1310  	}
  1311  
  1312  	batchCs := batchChangeSet(cs, testLimit, defaultBatchChangeSizeBytes, defaultBatchChangeSizeValues)
  1313  
  1314  	require.Equal(t, expectedBatchCount, len(batchCs))
  1315  
  1316  	// sorting cs needed to match batchCs
  1317  	for i, batch := range batchCs {
  1318  		validateAWSChangeRecords(t, batch, sortChangesByActionNameType(cs)[i*expectedChangesCount:expectedChangesCount*(i+1)])
  1319  	}
  1320  }
  1321  
  1322  func TestAWSBatchChangeSetExceedingNameChange(t *testing.T) {
  1323  	var cs Route53Changes
  1324  	const testCount = 10
  1325  	const testLimit = 1
  1326  
  1327  	for i := 1; i <= testCount; i += 2 {
  1328  		cs = append(cs,
  1329  			&Route53Change{
  1330  				Change: route53.Change{
  1331  					Action: aws.String(route53.ChangeActionCreate),
  1332  					ResourceRecordSet: &route53.ResourceRecordSet{
  1333  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1334  						Type: aws.String("A"),
  1335  					},
  1336  				},
  1337  			},
  1338  			&Route53Change{
  1339  				Change: route53.Change{
  1340  					Action: aws.String(route53.ChangeActionCreate),
  1341  					ResourceRecordSet: &route53.ResourceRecordSet{
  1342  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1343  						Type: aws.String("TXT"),
  1344  					},
  1345  				},
  1346  			},
  1347  		)
  1348  	}
  1349  
  1350  	batchCs := batchChangeSet(cs, testLimit, defaultBatchChangeSizeBytes, defaultBatchChangeSizeValues)
  1351  
  1352  	require.Equal(t, 0, len(batchCs))
  1353  }
  1354  
  1355  func TestAWSBatchChangeSetExceedingBytesLimit(t *testing.T) {
  1356  	const (
  1357  		testCount = 50
  1358  		testLimit = 100
  1359  		groupSize = 2
  1360  	)
  1361  
  1362  	var (
  1363  		cs Route53Changes
  1364  		// Bytes for each name
  1365  		testBytes = len([]byte("1.2.3.4")) + len([]byte("test-record"))
  1366  		// testCount / groupSize / (testLimit // bytes)
  1367  		expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testBytes)
  1368  		// Round up
  1369  		expectedBatchCount = int(math.Ceil(expectedBatchCountFloat))
  1370  	)
  1371  
  1372  	for i := 1; i <= testCount; i += groupSize {
  1373  		cs = append(cs,
  1374  			&Route53Change{
  1375  				Change: route53.Change{
  1376  					Action: aws.String(route53.ChangeActionCreate),
  1377  					ResourceRecordSet: &route53.ResourceRecordSet{
  1378  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1379  						Type: aws.String("A"),
  1380  						ResourceRecords: []*route53.ResourceRecord{
  1381  							{
  1382  								Value: aws.String("1.2.3.4"),
  1383  							},
  1384  						},
  1385  					},
  1386  				},
  1387  				sizeBytes:  len([]byte("1.2.3.4")),
  1388  				sizeValues: 1,
  1389  			},
  1390  			&Route53Change{
  1391  				Change: route53.Change{
  1392  					Action: aws.String(route53.ChangeActionCreate),
  1393  					ResourceRecordSet: &route53.ResourceRecordSet{
  1394  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1395  						Type: aws.String("TXT"),
  1396  						ResourceRecords: []*route53.ResourceRecord{
  1397  							{
  1398  								Value: aws.String("txt-record"),
  1399  							},
  1400  						},
  1401  					},
  1402  				},
  1403  				sizeBytes:  len([]byte("txt-record")),
  1404  				sizeValues: 1,
  1405  			},
  1406  		)
  1407  	}
  1408  
  1409  	batchCs := batchChangeSet(cs, defaultBatchChangeSize, testLimit, defaultBatchChangeSizeValues)
  1410  
  1411  	require.Equal(t, expectedBatchCount, len(batchCs))
  1412  }
  1413  
  1414  func TestAWSBatchChangeSetExceedingBytesLimitUpsert(t *testing.T) {
  1415  	const (
  1416  		testCount = 50
  1417  		testLimit = 100
  1418  		groupSize = 2
  1419  	)
  1420  
  1421  	var (
  1422  		cs Route53Changes
  1423  		// Bytes for each name multiplied by 2 for Upsert records
  1424  		testBytes = (len([]byte("1.2.3.4")) + len([]byte("test-record"))) * 2
  1425  		// testCount / groupSize / (testLimit // bytes)
  1426  		expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testBytes)
  1427  		// Round up
  1428  		expectedBatchCount = int(math.Ceil(expectedBatchCountFloat))
  1429  	)
  1430  
  1431  	for i := 1; i <= testCount; i += groupSize {
  1432  		cs = append(cs,
  1433  			&Route53Change{
  1434  				Change: route53.Change{
  1435  					Action: aws.String(route53.ChangeActionUpsert),
  1436  					ResourceRecordSet: &route53.ResourceRecordSet{
  1437  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1438  						Type: aws.String("A"),
  1439  						ResourceRecords: []*route53.ResourceRecord{
  1440  							{
  1441  								Value: aws.String("1.2.3.4"),
  1442  							},
  1443  						},
  1444  					},
  1445  				},
  1446  				sizeBytes:  len([]byte("1.2.3.4")) * 2,
  1447  				sizeValues: 1,
  1448  			},
  1449  			&Route53Change{
  1450  				Change: route53.Change{
  1451  					Action: aws.String(route53.ChangeActionUpsert),
  1452  					ResourceRecordSet: &route53.ResourceRecordSet{
  1453  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1454  						Type: aws.String("TXT"),
  1455  						ResourceRecords: []*route53.ResourceRecord{
  1456  							{
  1457  								Value: aws.String("txt-record"),
  1458  							},
  1459  						},
  1460  					},
  1461  				},
  1462  				sizeBytes:  len([]byte("txt-record")) * 2,
  1463  				sizeValues: 1,
  1464  			},
  1465  		)
  1466  	}
  1467  
  1468  	batchCs := batchChangeSet(cs, defaultBatchChangeSize, testLimit, defaultBatchChangeSizeValues)
  1469  
  1470  	require.Equal(t, expectedBatchCount, len(batchCs))
  1471  }
  1472  
  1473  func TestAWSBatchChangeSetExceedingValuesLimit(t *testing.T) {
  1474  	const (
  1475  		testCount = 50
  1476  		testLimit = 100
  1477  		groupSize = 2
  1478  		// Values for each group
  1479  		testValues = 2
  1480  	)
  1481  
  1482  	var (
  1483  		cs Route53Changes
  1484  		// testCount / groupSize / (testLimit // bytes)
  1485  		expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testValues)
  1486  		// Round up
  1487  		expectedBatchCount = int(math.Ceil(expectedBatchCountFloat))
  1488  	)
  1489  
  1490  	for i := 1; i <= testCount; i += groupSize {
  1491  		cs = append(cs,
  1492  			&Route53Change{
  1493  				Change: route53.Change{
  1494  					Action: aws.String(route53.ChangeActionCreate),
  1495  					ResourceRecordSet: &route53.ResourceRecordSet{
  1496  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1497  						Type: aws.String("A"),
  1498  						ResourceRecords: []*route53.ResourceRecord{
  1499  							{
  1500  								Value: aws.String("1.2.3.4"),
  1501  							},
  1502  						},
  1503  					},
  1504  				},
  1505  				sizeBytes:  len([]byte("1.2.3.4")),
  1506  				sizeValues: 1,
  1507  			},
  1508  			&Route53Change{
  1509  				Change: route53.Change{
  1510  					Action: aws.String(route53.ChangeActionCreate),
  1511  					ResourceRecordSet: &route53.ResourceRecordSet{
  1512  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1513  						Type: aws.String("TXT"),
  1514  						ResourceRecords: []*route53.ResourceRecord{
  1515  							{
  1516  								Value: aws.String("txt-record"),
  1517  							},
  1518  						},
  1519  					},
  1520  				},
  1521  				sizeBytes:  len([]byte("txt-record")),
  1522  				sizeValues: 1,
  1523  			},
  1524  		)
  1525  	}
  1526  
  1527  	batchCs := batchChangeSet(cs, defaultBatchChangeSize, defaultBatchChangeSizeBytes, testLimit)
  1528  
  1529  	require.Equal(t, expectedBatchCount, len(batchCs))
  1530  }
  1531  
  1532  func TestAWSBatchChangeSetExceedingValuesLimitUpsert(t *testing.T) {
  1533  	const (
  1534  		testCount = 50
  1535  		testLimit = 100
  1536  		groupSize = 2
  1537  		// Values for each group multiplied by 2 for Upsert records
  1538  		testValues = 2 * 2
  1539  	)
  1540  
  1541  	var (
  1542  		cs Route53Changes
  1543  		// testCount / groupSize / (testLimit // bytes)
  1544  		expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testValues)
  1545  		// Round up
  1546  		expectedBatchCount = int(math.Ceil(expectedBatchCountFloat))
  1547  	)
  1548  
  1549  	for i := 1; i <= testCount; i += groupSize {
  1550  		cs = append(cs,
  1551  			&Route53Change{
  1552  				Change: route53.Change{
  1553  					Action: aws.String(route53.ChangeActionUpsert),
  1554  					ResourceRecordSet: &route53.ResourceRecordSet{
  1555  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1556  						Type: aws.String("A"),
  1557  						ResourceRecords: []*route53.ResourceRecord{
  1558  							{
  1559  								Value: aws.String("1.2.3.4"),
  1560  							},
  1561  						},
  1562  					},
  1563  				},
  1564  				sizeBytes:  len([]byte("1.2.3.4")) * 2,
  1565  				sizeValues: 1,
  1566  			},
  1567  			&Route53Change{
  1568  				Change: route53.Change{
  1569  					Action: aws.String(route53.ChangeActionUpsert),
  1570  					ResourceRecordSet: &route53.ResourceRecordSet{
  1571  						Name: aws.String(fmt.Sprintf("host-%d", i)),
  1572  						Type: aws.String("TXT"),
  1573  						ResourceRecords: []*route53.ResourceRecord{
  1574  							{
  1575  								Value: aws.String("txt-record"),
  1576  							},
  1577  						},
  1578  					},
  1579  				},
  1580  				sizeBytes:  len([]byte("txt-record")) * 2,
  1581  				sizeValues: 1,
  1582  			},
  1583  		)
  1584  	}
  1585  
  1586  	batchCs := batchChangeSet(cs, defaultBatchChangeSize, defaultBatchChangeSizeBytes, testLimit)
  1587  
  1588  	require.Equal(t, expectedBatchCount, len(batchCs))
  1589  }
  1590  
  1591  func validateEndpoints(t *testing.T, provider *AWSProvider, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
  1592  	assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %+v:%+v", endpoints, expected)
  1593  
  1594  	normalized, err := provider.AdjustEndpoints(endpoints)
  1595  	assert.NoError(t, err)
  1596  	assert.True(t, testutils.SameEndpoints(normalized, expected), "actual and normalized endpoints don't match. %+v:%+v", endpoints, normalized)
  1597  }
  1598  
  1599  func validateAWSZones(t *testing.T, zones map[string]*route53.HostedZone, expected map[string]*route53.HostedZone) {
  1600  	require.Len(t, zones, len(expected))
  1601  
  1602  	for i, zone := range zones {
  1603  		validateAWSZone(t, zone, expected[i])
  1604  	}
  1605  }
  1606  
  1607  func validateAWSZone(t *testing.T, zone *route53.HostedZone, expected *route53.HostedZone) {
  1608  	assert.Equal(t, aws.StringValue(expected.Id), aws.StringValue(zone.Id))
  1609  	assert.Equal(t, aws.StringValue(expected.Name), aws.StringValue(zone.Name))
  1610  }
  1611  
  1612  func validateAWSChangeRecords(t *testing.T, records Route53Changes, expected Route53Changes) {
  1613  	require.Len(t, records, len(expected))
  1614  
  1615  	for i := range records {
  1616  		validateAWSChangeRecord(t, records[i], expected[i])
  1617  	}
  1618  }
  1619  
  1620  func validateAWSChangeRecord(t *testing.T, record *Route53Change, expected *Route53Change) {
  1621  	assert.Equal(t, aws.StringValue(expected.Action), aws.StringValue(record.Action))
  1622  	assert.Equal(t, aws.StringValue(expected.ResourceRecordSet.Name), aws.StringValue(record.ResourceRecordSet.Name))
  1623  	assert.Equal(t, aws.StringValue(expected.ResourceRecordSet.Type), aws.StringValue(record.ResourceRecordSet.Type))
  1624  }
  1625  
  1626  func TestAWSCreateRecordsWithCNAME(t *testing.T) {
  1627  	provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil)
  1628  
  1629  	records := []*endpoint.Endpoint{
  1630  		{DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.example.org"}, RecordType: endpoint.RecordTypeCNAME},
  1631  	}
  1632  
  1633  	adjusted, err := provider.AdjustEndpoints(records)
  1634  	require.NoError(t, err)
  1635  	require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{
  1636  		Create: adjusted,
  1637  	}))
  1638  
  1639  	recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.")
  1640  
  1641  	validateRecords(t, recordSets, []*route53.ResourceRecordSet{
  1642  		{
  1643  			Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
  1644  			Type: aws.String(endpoint.RecordTypeCNAME),
  1645  			TTL:  aws.Int64(300),
  1646  			ResourceRecords: []*route53.ResourceRecord{
  1647  				{
  1648  					Value: aws.String("foo.example.org"),
  1649  				},
  1650  			},
  1651  		},
  1652  	})
  1653  }
  1654  
  1655  func TestAWSCreateRecordsWithALIAS(t *testing.T) {
  1656  	for key, evaluateTargetHealth := range map[string]bool{
  1657  		"true":  true,
  1658  		"false": false,
  1659  		"":      false,
  1660  	} {
  1661  		provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil)
  1662  
  1663  		// Test dualstack and ipv4 load balancer targets
  1664  		records := []*endpoint.Endpoint{
  1665  			{
  1666  				DNSName:    "create-test.zone-1.ext-dns-test-2.teapot.zalan.do",
  1667  				Targets:    endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"},
  1668  				RecordType: endpoint.RecordTypeA,
  1669  				ProviderSpecific: endpoint.ProviderSpecific{
  1670  					endpoint.ProviderSpecificProperty{
  1671  						Name:  providerSpecificAlias,
  1672  						Value: "true",
  1673  					},
  1674  					endpoint.ProviderSpecificProperty{
  1675  						Name:  providerSpecificEvaluateTargetHealth,
  1676  						Value: key,
  1677  					},
  1678  				},
  1679  			},
  1680  			{
  1681  				DNSName:    "create-test-dualstack.zone-1.ext-dns-test-2.teapot.zalan.do",
  1682  				Targets:    endpoint.Targets{"bar.eu-central-1.elb.amazonaws.com"},
  1683  				RecordType: endpoint.RecordTypeA,
  1684  				ProviderSpecific: endpoint.ProviderSpecific{
  1685  					endpoint.ProviderSpecificProperty{
  1686  						Name:  providerSpecificAlias,
  1687  						Value: "true",
  1688  					},
  1689  					endpoint.ProviderSpecificProperty{
  1690  						Name:  providerSpecificEvaluateTargetHealth,
  1691  						Value: key,
  1692  					},
  1693  				},
  1694  				Labels: map[string]string{endpoint.DualstackLabelKey: "true"},
  1695  			},
  1696  		}
  1697  		adjusted, err := provider.AdjustEndpoints(records)
  1698  		require.NoError(t, err)
  1699  		require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{
  1700  			Create: adjusted,
  1701  		}))
  1702  
  1703  		recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.")
  1704  
  1705  		validateRecords(t, recordSets, []*route53.ResourceRecordSet{
  1706  			{
  1707  				AliasTarget: &route53.AliasTarget{
  1708  					DNSName:              aws.String("foo.eu-central-1.elb.amazonaws.com."),
  1709  					EvaluateTargetHealth: aws.Bool(evaluateTargetHealth),
  1710  					HostedZoneId:         aws.String("Z215JYRZR1TBD5"),
  1711  				},
  1712  				Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
  1713  				Type: aws.String(route53.RRTypeA),
  1714  			},
  1715  			{
  1716  				AliasTarget: &route53.AliasTarget{
  1717  					DNSName:              aws.String("bar.eu-central-1.elb.amazonaws.com."),
  1718  					EvaluateTargetHealth: aws.Bool(evaluateTargetHealth),
  1719  					HostedZoneId:         aws.String("Z215JYRZR1TBD5"),
  1720  				},
  1721  				Name: aws.String("create-test-dualstack.zone-1.ext-dns-test-2.teapot.zalan.do."),
  1722  				Type: aws.String(route53.RRTypeA),
  1723  			},
  1724  			{
  1725  				AliasTarget: &route53.AliasTarget{
  1726  					DNSName:              aws.String("bar.eu-central-1.elb.amazonaws.com."),
  1727  					EvaluateTargetHealth: aws.Bool(evaluateTargetHealth),
  1728  					HostedZoneId:         aws.String("Z215JYRZR1TBD5"),
  1729  				},
  1730  				Name: aws.String("create-test-dualstack.zone-1.ext-dns-test-2.teapot.zalan.do."),
  1731  				Type: aws.String(route53.RRTypeAaaa),
  1732  			},
  1733  		})
  1734  	}
  1735  }
  1736  
  1737  func TestAWSisLoadBalancer(t *testing.T) {
  1738  	for _, tc := range []struct {
  1739  		target      string
  1740  		recordType  string
  1741  		preferCNAME bool
  1742  		expected    bool
  1743  	}{
  1744  		{"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME, false, true},
  1745  		{"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME, true, false},
  1746  		{"foo.example.org", endpoint.RecordTypeCNAME, false, false},
  1747  		{"foo.example.org", endpoint.RecordTypeCNAME, true, false},
  1748  	} {
  1749  		ep := &endpoint.Endpoint{
  1750  			Targets:    endpoint.Targets{tc.target},
  1751  			RecordType: tc.recordType,
  1752  		}
  1753  		assert.Equal(t, tc.expected, useAlias(ep, tc.preferCNAME))
  1754  	}
  1755  }
  1756  
  1757  func TestAWSisAWSAlias(t *testing.T) {
  1758  	for _, tc := range []struct {
  1759  		target     string
  1760  		recordType string
  1761  		alias      bool
  1762  		hz         string
  1763  	}{
  1764  		{"foo.example.org", endpoint.RecordTypeA, false, ""},                                 // normal A record
  1765  		{"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeA, true, "Z215JYRZR1TBD5"}, // pointing to ELB DNS name
  1766  		{"foobar.example.org", endpoint.RecordTypeA, true, "Z1234567890ABC"},                 // HZID retrieved by Route53
  1767  		{"baz.example.org", endpoint.RecordTypeA, true, sameZoneAlias},                       // record to be created
  1768  	} {
  1769  		ep := &endpoint.Endpoint{
  1770  			Targets:    endpoint.Targets{tc.target},
  1771  			RecordType: tc.recordType,
  1772  		}
  1773  		if tc.alias {
  1774  			ep = ep.WithProviderSpecific(providerSpecificAlias, "true")
  1775  			ep = ep.WithProviderSpecific(providerSpecificTargetHostedZone, tc.hz)
  1776  		}
  1777  		assert.Equal(t, tc.hz, isAWSAlias(ep), "%v", tc)
  1778  	}
  1779  }
  1780  
  1781  func TestAWSCanonicalHostedZone(t *testing.T) {
  1782  	for suffix, id := range canonicalHostedZones {
  1783  		zone := canonicalHostedZone(fmt.Sprintf("foo.%s", suffix))
  1784  		assert.Equal(t, id, zone)
  1785  	}
  1786  
  1787  	zone := canonicalHostedZone("foo.example.org")
  1788  	assert.Equal(t, "", zone, "no canonical zone should be returned for a non-aws hostname")
  1789  }
  1790  
  1791  func TestAWSSuitableZones(t *testing.T) {
  1792  	zones := map[string]*route53.HostedZone{
  1793  		// Public domain
  1794  		"example-org": {Id: aws.String("example-org"), Name: aws.String("example.org.")},
  1795  		// Public subdomain
  1796  		"bar-example-org": {Id: aws.String("bar-example-org"), Name: aws.String("bar.example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}},
  1797  		// Public subdomain
  1798  		"longfoo-bar-example-org": {Id: aws.String("longfoo-bar-example-org"), Name: aws.String("longfoo.bar.example.org.")},
  1799  		// Private domain
  1800  		"example-org-private": {Id: aws.String("example-org-private"), Name: aws.String("example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}},
  1801  		// Private subdomain
  1802  		"bar-example-org-private": {Id: aws.String("bar-example-org-private"), Name: aws.String("bar.example.org."), Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}},
  1803  	}
  1804  
  1805  	for _, tc := range []struct {
  1806  		hostname string
  1807  		expected []*route53.HostedZone
  1808  	}{
  1809  		// bar.example.org is NOT suitable
  1810  		{"foobar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["example-org"]}},
  1811  
  1812  		// all matching private zones are suitable
  1813  		// https://github.com/kubernetes-sigs/external-dns/pull/356
  1814  		{"bar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}},
  1815  
  1816  		{"foo.bar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}},
  1817  		{"foo.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["example-org"]}},
  1818  		{"foo.kubernetes.io.", nil},
  1819  	} {
  1820  		suitableZones := suitableZones(tc.hostname, zones)
  1821  		sort.Slice(suitableZones, func(i, j int) bool {
  1822  			return *suitableZones[i].Id < *suitableZones[j].Id
  1823  		})
  1824  		sort.Slice(tc.expected, func(i, j int) bool {
  1825  			return *tc.expected[i].Id < *tc.expected[j].Id
  1826  		})
  1827  		assert.Equal(t, tc.expected, suitableZones)
  1828  	}
  1829  }
  1830  
  1831  func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) {
  1832  	params := &route53.CreateHostedZoneInput{
  1833  		CallerReference:  aws.String("external-dns.alpha.kubernetes.io/test-zone"),
  1834  		Name:             zone.Name,
  1835  		HostedZoneConfig: zone.Config,
  1836  	}
  1837  
  1838  	if _, err := provider.client.CreateHostedZoneWithContext(context.Background(), params); err != nil {
  1839  		require.EqualError(t, err, route53.ErrCodeHostedZoneAlreadyExists)
  1840  	}
  1841  }
  1842  
  1843  func setAWSRecords(t *testing.T, provider *AWSProvider, records []*route53.ResourceRecordSet) {
  1844  	dryRun := provider.dryRun
  1845  	provider.dryRun = false
  1846  	defer func() {
  1847  		provider.dryRun = dryRun
  1848  	}()
  1849  
  1850  	ctx := context.Background()
  1851  	endpoints, err := provider.Records(ctx)
  1852  	require.NoError(t, err)
  1853  
  1854  	validateEndpoints(t, provider, endpoints, []*endpoint.Endpoint{})
  1855  
  1856  	var changes Route53Changes
  1857  	for _, record := range records {
  1858  		changes = append(changes, &Route53Change{
  1859  			Change: route53.Change{
  1860  				Action:            aws.String(route53.ChangeActionCreate),
  1861  				ResourceRecordSet: record,
  1862  			},
  1863  		})
  1864  	}
  1865  
  1866  	zones, err := provider.Zones(ctx)
  1867  	require.NoError(t, err)
  1868  	err = provider.submitChanges(ctx, changes, zones)
  1869  	require.NoError(t, err)
  1870  
  1871  	_, err = provider.Records(ctx)
  1872  	require.NoError(t, err)
  1873  }
  1874  
  1875  func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.ResourceRecordSet {
  1876  	recordSets := []*route53.ResourceRecordSet{}
  1877  	require.NoError(t, client.ListResourceRecordSetsPagesWithContext(context.Background(), &route53.ListResourceRecordSetsInput{
  1878  		HostedZoneId: aws.String(zone),
  1879  		MaxItems:     aws.String(route53PageSize),
  1880  	}, func(resp *route53.ListResourceRecordSetsOutput, _ bool) bool {
  1881  		recordSets = append(recordSets, resp.ResourceRecordSets...)
  1882  		return true
  1883  	}))
  1884  
  1885  	return recordSets
  1886  }
  1887  
  1888  func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*route53.ResourceRecordSet) (*AWSProvider, *Route53APIStub) {
  1889  	return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, provider.NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records)
  1890  }
  1891  
  1892  func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, zoneTagFilter provider.ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*route53.ResourceRecordSet) (*AWSProvider, *Route53APIStub) {
  1893  	client := NewRoute53APIStub(t)
  1894  
  1895  	provider := &AWSProvider{
  1896  		client:                client,
  1897  		batchChangeSize:       defaultBatchChangeSize,
  1898  		batchChangeSizeBytes:  defaultBatchChangeSizeBytes,
  1899  		batchChangeSizeValues: defaultBatchChangeSizeValues,
  1900  		batchChangeInterval:   defaultBatchChangeInterval,
  1901  		evaluateTargetHealth:  evaluateTargetHealth,
  1902  		domainFilter:          domainFilter,
  1903  		zoneIDFilter:          zoneIDFilter,
  1904  		zoneTypeFilter:        zoneTypeFilter,
  1905  		zoneTagFilter:         zoneTagFilter,
  1906  		dryRun:                false,
  1907  		zonesCache:            &zonesListCache{duration: 1 * time.Minute},
  1908  		failedChangesQueue:    make(map[string]Route53Changes),
  1909  	}
  1910  
  1911  	createAWSZone(t, provider, &route53.HostedZone{
  1912  		Id:     aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."),
  1913  		Name:   aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."),
  1914  		Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)},
  1915  	})
  1916  
  1917  	createAWSZone(t, provider, &route53.HostedZone{
  1918  		Id:     aws.String("/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."),
  1919  		Name:   aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."),
  1920  		Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)},
  1921  	})
  1922  
  1923  	createAWSZone(t, provider, &route53.HostedZone{
  1924  		Id:     aws.String("/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."),
  1925  		Name:   aws.String("zone-3.ext-dns-test-2.teapot.zalan.do."),
  1926  		Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)},
  1927  	})
  1928  
  1929  	// filtered out by domain filter
  1930  	createAWSZone(t, provider, &route53.HostedZone{
  1931  		Id:     aws.String("/hostedzone/zone-4.ext-dns-test-3.teapot.zalan.do."),
  1932  		Name:   aws.String("zone-4.ext-dns-test-3.teapot.zalan.do."),
  1933  		Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)},
  1934  	})
  1935  
  1936  	setupZoneTags(provider.client.(*Route53APIStub))
  1937  
  1938  	setAWSRecords(t, provider, records)
  1939  
  1940  	provider.dryRun = dryRun
  1941  
  1942  	return provider, client
  1943  }
  1944  
  1945  func setupZoneTags(client *Route53APIStub) {
  1946  	addZoneTags(client.zoneTags, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.", map[string]string{
  1947  		"zone-1-tag-1": "tag-1-value",
  1948  		"domain":       "test-2",
  1949  		"zone":         "1",
  1950  	})
  1951  	addZoneTags(client.zoneTags, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.", map[string]string{
  1952  		"zone-2-tag-1": "tag-1-value",
  1953  		"domain":       "test-2",
  1954  		"zone":         "2",
  1955  	})
  1956  	addZoneTags(client.zoneTags, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.", map[string]string{
  1957  		"zone-3-tag-1": "tag-1-value",
  1958  		"domain":       "test-2",
  1959  		"zone":         "3",
  1960  	})
  1961  	addZoneTags(client.zoneTags, "/hostedzone/zone-4.ext-dns-test-2.teapot.zalan.do.", map[string]string{
  1962  		"zone-4-tag-1": "tag-1-value",
  1963  		"domain":       "test-3",
  1964  		"zone":         "4",
  1965  	})
  1966  }
  1967  
  1968  func addZoneTags(tagMap map[string][]*route53.Tag, zoneID string, tags map[string]string) {
  1969  	tagList := make([]*route53.Tag, 0, len(tags))
  1970  	for k, v := range tags {
  1971  		tagList = append(tagList, &route53.Tag{
  1972  			Key:   aws.String(k),
  1973  			Value: aws.String(v),
  1974  		})
  1975  	}
  1976  	tagMap[zoneID] = tagList
  1977  }
  1978  
  1979  func validateRecords(t *testing.T, records []*route53.ResourceRecordSet, expected []*route53.ResourceRecordSet) {
  1980  	assert.ElementsMatch(t, expected, records)
  1981  }
  1982  
  1983  func containsRecordWithDNSName(records []*endpoint.Endpoint, dnsName string) bool {
  1984  	for _, record := range records {
  1985  		if record.DNSName == dnsName {
  1986  			return true
  1987  		}
  1988  	}
  1989  	return false
  1990  }
  1991  
  1992  func TestRequiresDeleteCreate(t *testing.T) {
  1993  	provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"foo.bar."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil)
  1994  
  1995  	oldRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8")
  1996  	newRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar").WithProviderSpecific(providerSpecificAlias, "false")
  1997  
  1998  	assert.False(t, provider.requiresDeleteCreate(oldRecordType, oldRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, oldRecordType)
  1999  	assert.True(t, provider.requiresDeleteCreate(oldRecordType, newRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, newRecordType)
  2000  
  2001  	oldAtoAlias := endpoint.NewEndpointWithTTL("AtoAlias", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.1.1.1")
  2002  	newAtoAlias := endpoint.NewEndpointWithTTL("AtoAlias", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "bar.us-east-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true")
  2003  
  2004  	assert.False(t, provider.requiresDeleteCreate(oldAtoAlias, oldAtoAlias), "actual and expected endpoints don't match. %+v:%+v", oldAtoAlias, oldAtoAlias.DNSName)
  2005  	assert.True(t, provider.requiresDeleteCreate(oldAtoAlias, newAtoAlias), "actual and expected endpoints don't match. %+v:%+v", oldAtoAlias, newAtoAlias)
  2006  
  2007  	oldPolicy := endpoint.NewEndpointWithTTL("policy", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8").WithSetIdentifier("nochange").WithProviderSpecific(providerSpecificRegion, "us-east-1")
  2008  	newPolicy := endpoint.NewEndpointWithTTL("policy", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8").WithSetIdentifier("nochange").WithProviderSpecific(providerSpecificWeight, "10")
  2009  
  2010  	assert.False(t, provider.requiresDeleteCreate(oldPolicy, oldPolicy), "actual and expected endpoints don't match. %+v:%+v", oldPolicy, oldPolicy)
  2011  	assert.True(t, provider.requiresDeleteCreate(oldPolicy, newPolicy), "actual and expected endpoints don't match. %+v:%+v", oldPolicy, newPolicy)
  2012  
  2013  	oldSetIdentifier := endpoint.NewEndpointWithTTL("setIdentifier", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8").WithSetIdentifier("old")
  2014  	newSetIdentifier := endpoint.NewEndpointWithTTL("setIdentifier", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8").WithSetIdentifier("new")
  2015  
  2016  	assert.False(t, provider.requiresDeleteCreate(oldSetIdentifier, oldSetIdentifier), "actual and expected endpoints don't match. %+v:%+v", oldSetIdentifier, oldSetIdentifier)
  2017  	assert.True(t, provider.requiresDeleteCreate(oldSetIdentifier, newSetIdentifier), "actual and expected endpoints don't match. %+v:%+v", oldSetIdentifier, newSetIdentifier)
  2018  }