/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package aws import ( "bytes" "context" "fmt" "math" "net" "sort" "strings" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/route53" route53types "github.com/aws/aws-sdk-go-v2/service/route53/types" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" ) const ( defaultBatchChangeSize = 4000 defaultBatchChangeSizeBytes = 32000 defaultBatchChangeSizeValues = 1000 defaultBatchChangeInterval = time.Second defaultEvaluateTargetHealth = true ) // Compile time check for interface conformance var _ Route53API = &Route53APIStub{} // Route53APIStub is a minimal implementation of Route53API, used primarily for unit testing. // See http://http://docs.aws.amazon.com/sdk-for-go/api/service/route53.html for descriptions // of all of its methods. // mostly taken from: https://github.com/kubernetes/kubernetes/blob/853167624edb6bc0cfdcdfb88e746e178f5db36c/federation/pkg/dnsprovider/providers/aws/route53/stubs/route53api.go type Route53APIStub struct { zones map[string]*route53types.HostedZone recordSets map[string]map[string][]route53types.ResourceRecordSet zoneTags map[string][]route53types.Tag m dynamicMock t *testing.T } // MockMethod starts a description of an expectation of the specified method // being called. // // Route53APIStub.MockMethod("MyMethod", arg1, arg2) func (r *Route53APIStub) MockMethod(method string, args ...interface{}) *mock.Call { return r.m.On(method, args...) } // NewRoute53APIStub returns an initialized Route53APIStub func NewRoute53APIStub(t *testing.T) *Route53APIStub { return &Route53APIStub{ zones: make(map[string]*route53types.HostedZone), recordSets: make(map[string]map[string][]route53types.ResourceRecordSet), zoneTags: make(map[string][]route53types.Tag), t: t, } } func (r *Route53APIStub) ListResourceRecordSets(ctx context.Context, input *route53.ListResourceRecordSetsInput, optFns ...func(options *route53.Options)) (*route53.ListResourceRecordSetsOutput, error) { if r.m.isMocked("ListResourceRecordSets", input) { return r.m.ListResourceRecordSets(ctx, input, optFns...) } output := &route53.ListResourceRecordSetsOutput{} // TODO: Support optional input args. require.NotNil(r.t, input.MaxItems) assert.Equal(r.t, route53PageSize, *input.MaxItems) if len(r.recordSets) == 0 { output.ResourceRecordSets = []route53types.ResourceRecordSet{} } else if _, ok := r.recordSets[*input.HostedZoneId]; !ok { output.ResourceRecordSets = []route53types.ResourceRecordSet{} } else { for _, rrsets := range r.recordSets[*input.HostedZoneId] { output.ResourceRecordSets = append(output.ResourceRecordSets, rrsets...) } } return output, nil } type Route53APICounter struct { wrapped Route53API calls map[string]int } func NewRoute53APICounter(w Route53API) *Route53APICounter { return &Route53APICounter{ wrapped: w, calls: map[string]int{}, } } func (c *Route53APICounter) ListResourceRecordSets(ctx context.Context, input *route53.ListResourceRecordSetsInput, optFns ...func(options *route53.Options)) (*route53.ListResourceRecordSetsOutput, error) { c.calls["ListResourceRecordSetsPages"]++ return c.wrapped.ListResourceRecordSets(ctx, input, optFns...) } func (c *Route53APICounter) ChangeResourceRecordSets(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) { c.calls["ChangeResourceRecordSets"]++ return c.wrapped.ChangeResourceRecordSets(ctx, input, optFns...) } func (c *Route53APICounter) CreateHostedZone(ctx context.Context, input *route53.CreateHostedZoneInput, optFns ...func(*route53.Options)) (*route53.CreateHostedZoneOutput, error) { c.calls["CreateHostedZone"]++ return c.wrapped.CreateHostedZone(ctx, input, optFns...) } func (c *Route53APICounter) ListHostedZones(ctx context.Context, input *route53.ListHostedZonesInput, optFns ...func(options *route53.Options)) (*route53.ListHostedZonesOutput, error) { c.calls["ListHostedZonesPages"]++ return c.wrapped.ListHostedZones(ctx, input, optFns...) } func (c *Route53APICounter) ListTagsForResources(ctx context.Context, input *route53.ListTagsForResourcesInput, optFns ...func(options *route53.Options)) (*route53.ListTagsForResourcesOutput, error) { c.calls["ListTagsForResource"]++ return c.wrapped.ListTagsForResources(ctx, input, optFns...) } // Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk func wildcardEscape(s string) string { if strings.Contains(s, "*") { s = strings.Replace(s, "*", "\\052", 1) } return s } // Route53 octal escapes https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html func specialCharactersEscape(s string) string { var result strings.Builder for _, char := range s { if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '-' || char == '.' { result.WriteRune(char) } else { octalCode := fmt.Sprintf("\\%03o", char) result.WriteString(octalCode) } } return result.String() } func (r *Route53APIStub) ListTagsForResources(ctx context.Context, input *route53.ListTagsForResourcesInput, optFns ...func(options *route53.Options)) (*route53.ListTagsForResourcesOutput, error) { if input.ResourceType == route53types.TagResourceTypeHostedzone { var sets []route53types.ResourceTagSet for _, el := range input.ResourceIds { zoneId := fmt.Sprintf("/%s/%s", input.ResourceType, el) if strings.Contains(zoneId, "ext-dns-test-error-on-list-tags") { return nil, fmt.Errorf("operation error Route53APIStub: ListTagsForResource") } if r.zoneTags[zoneId] != nil { sets = append(sets, route53types.ResourceTagSet{ ResourceId: &el, ResourceType: route53types.TagResourceTypeHostedzone, Tags: r.zoneTags[zoneId], }) } } return &route53.ListTagsForResourcesOutput{ResourceTagSets: sets}, nil } return &route53.ListTagsForResourcesOutput{}, nil } func (r *Route53APIStub) ChangeResourceRecordSets(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, optFns ...func(options *route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) { if r.m.isMocked("ChangeResourceRecordSets", input) { return r.m.ChangeResourceRecordSets(input) } _, ok := r.zones[*input.HostedZoneId] if !ok { return nil, fmt.Errorf("hosted zone doesn't exist: %s", *input.HostedZoneId) } if len(input.ChangeBatch.Changes) == 0 { return nil, fmt.Errorf("ChangeBatch doesn't contain any changes") } output := &route53.ChangeResourceRecordSetsOutput{} recordSets, ok := r.recordSets[*input.HostedZoneId] if !ok { recordSets = make(map[string][]route53types.ResourceRecordSet) } for _, change := range input.ChangeBatch.Changes { if change.ResourceRecordSet.Type == route53types.RRTypeA { for _, rrs := range change.ResourceRecordSet.ResourceRecords { if net.ParseIP(*rrs.Value) == nil { return nil, fmt.Errorf("A records must point to IPs") } } } change.ResourceRecordSet.Name = aws.String(wildcardEscape(provider.EnsureTrailingDot(*change.ResourceRecordSet.Name))) if change.ResourceRecordSet.AliasTarget != nil { change.ResourceRecordSet.AliasTarget.DNSName = aws.String(wildcardEscape(provider.EnsureTrailingDot(*change.ResourceRecordSet.AliasTarget.DNSName))) } setID := "" if change.ResourceRecordSet.SetIdentifier != nil { setID = *change.ResourceRecordSet.SetIdentifier } key := *change.ResourceRecordSet.Name + "::" + string(change.ResourceRecordSet.Type) + "::" + setID switch change.Action { case route53types.ChangeActionCreate: if _, found := recordSets[key]; found { return nil, fmt.Errorf("attempt to create duplicate rrset %s", key) // TODO: Return AWS errors with codes etc } recordSets[key] = append(recordSets[key], *change.ResourceRecordSet) case route53types.ChangeActionDelete: if _, found := recordSets[key]; !found { return nil, fmt.Errorf("attempt to delete non-existent rrset %s", key) // TODO: Check other fields too } delete(recordSets, key) case route53types.ChangeActionUpsert: recordSets[key] = []route53types.ResourceRecordSet{*change.ResourceRecordSet} } } r.recordSets[*input.HostedZoneId] = recordSets return output, nil // TODO: We should ideally return status etc, but we don't' use that yet. } func (r *Route53APIStub) ListHostedZones(ctx context.Context, input *route53.ListHostedZonesInput, optFns ...func(options *route53.Options)) (*route53.ListHostedZonesOutput, error) { output := &route53.ListHostedZonesOutput{} for _, zone := range r.zones { output.HostedZones = append(output.HostedZones, *zone) } return output, nil } func (r *Route53APIStub) CreateHostedZone(ctx context.Context, input *route53.CreateHostedZoneInput, optFns ...func(options *route53.Options)) (*route53.CreateHostedZoneOutput, error) { name := *input.Name id := "/hostedzone/" + name if _, ok := r.zones[id]; ok { return nil, fmt.Errorf("Error creating hosted DNS zone: %s already exists", id) } r.zones[id] = &route53types.HostedZone{ Id: aws.String(id), Name: aws.String(name), Config: input.HostedZoneConfig, } return &route53.CreateHostedZoneOutput{HostedZone: r.zones[id]}, nil } type dynamicMock struct { mock.Mock } func (m *dynamicMock) ListResourceRecordSets(ctx context.Context, input *route53.ListResourceRecordSetsInput, optFns ...func(options *route53.Options)) (*route53.ListResourceRecordSetsOutput, error) { args := m.Called(input) if args.Get(0) != nil { return args.Get(0).(*route53.ListResourceRecordSetsOutput), args.Error(1) } return nil, args.Error(1) } func (m *dynamicMock) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) { args := m.Called(input) if args.Get(0) != nil { return args.Get(0).(*route53.ChangeResourceRecordSetsOutput), args.Error(1) } return nil, args.Error(1) } func (m *dynamicMock) isMocked(method string, arguments ...interface{}) bool { for _, call := range m.ExpectedCalls { if call.Method == method && call.Repeatability > -1 { _, diffCount := call.Arguments.Diff(arguments) if diffCount == 0 { return true } } } return false } func TestAWSZones(t *testing.T) { publicZones := map[string]*route53types.HostedZone{ "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.": { Id: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), }, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.": { Id: aws.String("/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."), }, } privateZones := map[string]*route53types.HostedZone{ "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.": { Id: aws.String("/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("zone-3.ext-dns-test-2.teapot.zalan.do."), }, } allZones := map[string]*route53types.HostedZone{} for k, v := range publicZones { allZones[k] = v } for k, v := range privateZones { allZones[k] = v } noZones := map[string]*route53types.HostedZone{} for _, ti := range []struct { msg string zoneIDFilter provider.ZoneIDFilter zoneTypeFilter provider.ZoneTypeFilter zoneTagFilter provider.ZoneTagFilter expectedZones map[string]*route53types.HostedZone }{ {"no filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), allZones}, {"public filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("public"), provider.NewZoneTagFilter([]string{}), publicZones}, {"private filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("private"), provider.NewZoneTagFilter([]string{}), privateZones}, {"unknown filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("unknown"), provider.NewZoneTagFilter([]string{}), noZones}, {"zone id filter", provider.NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), privateZones}, {"tag filter zero zone match", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{"zone=not-exists"}), noZones}, {"tag filter single zone match", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{"zone=3"}), privateZones}, } { t.Run(ti.msg, func(t *testing.T) { provider, _ := newAWSProviderWithTagFilter(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, nil) zones, err := provider.Zones(context.Background()) require.NoError(t, err) validateAWSZones(t, zones, ti.expectedZones) }) } } func TestAWSZonesWithTagFilterError(t *testing.T) { client := NewRoute53APIStub(t) provider := &AWSProvider{ clients: map[string]Route53API{defaultAWSProfile: client}, zoneTagFilter: provider.NewZoneTagFilter([]string{"zone=2"}), dryRun: false, zonesCache: &zonesListCache{duration: 1 * time.Minute}, } createAWSZone(t, provider, &route53types.HostedZone{ Id: aws.String("/hostedzone/zone-1.ext-dns-test-ok.example.com."), Name: aws.String("zone-1.ext-dns-test-ok.example.com."), Config: &route53types.HostedZoneConfig{PrivateZone: false}, }) createAWSZone(t, provider, &route53types.HostedZone{ Id: aws.String("/hostedzone/zone-2.ext-dns-test-error-on-list-tags.example.com."), Name: aws.String("zone-2.ext-dns-test-error-on-list-tags.example.com."), Config: &route53types.HostedZoneConfig{PrivateZone: false}, }) _, err := provider.Zones(context.Background()) require.Error(t, err) require.ErrorContains(t, err, "failed to list tags for zones") } func TestAWSRecordsFilter(t *testing.T) { provider, _ := newAWSProvider(t, &endpoint.DomainFilter{}, provider.ZoneIDFilter{}, provider.ZoneTypeFilter{}, false, false, nil) domainFilter := provider.GetDomainFilter() require.NotNil(t, domainFilter) require.IsType(t, &endpoint.DomainFilter{}, domainFilter) count := 0 filters := domainFilter.(*endpoint.DomainFilter).Filters for _, tld := range []string{ "zone-4.ext-dns-test-3.teapot.zalan.do", ".zone-4.ext-dns-test-3.teapot.zalan.do", "zone-2.ext-dns-test-2.teapot.zalan.do", ".zone-2.ext-dns-test-2.teapot.zalan.do", "zone-3.ext-dns-test-2.teapot.zalan.do", ".zone-3.ext-dns-test-2.teapot.zalan.do", "zone-4.ext-dns-test-3.teapot.zalan.do", ".zone-4.ext-dns-test-3.teapot.zalan.do", } { assert.Contains(t, filters, tld) count++ } assert.Len(t, filters, count) } func TestAWSRecords(t *testing.T) { provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, []route53types.ResourceRecordSet{ { Name: aws.String("list-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, }, { Name: aws.String("list-test.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, }, { Name: aws.String(wildcardEscape("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do.")), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, }, { Name: aws.String(specialCharactersEscape("escape-%!s()-codes.zone-2.ext-dns-test-2.teapot.zalan.do.")), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("example")}}, }, { Name: aws.String(specialCharactersEscape("escape-%!s()-codes-a.zone-2.ext-dns-test-2.teapot.zalan.do.")), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, }, { Name: aws.String(specialCharactersEscape("escape-%!s()-codes-alias.zone-2.ext-dns-test-2.teapot.zalan.do.")), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("escape-codes.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: false, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String(specialCharactersEscape("escape-%!s()-codes-alias.zone-2.ext-dns-test-2.teapot.zalan.do.")), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("escape-codes.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: false, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: false, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: false, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: false, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: false, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, }, { Name: aws.String("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeTxt, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("random")}}, }, { Name: aws.String("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("test-set-1"), Weight: aws.Int64(10), }, { Name: aws.String("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("4.3.2.1")}}, SetIdentifier: aws.String("test-set-2"), Weight: aws.Int64(20), }, { Name: aws.String("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("test-set"), Region: route53types.ResourceRecordSetRegionUsEast1, }, { Name: aws.String("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("test-set"), Failover: route53types.ResourceRecordSetFailoverPrimary, }, { Name: aws.String("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("test-set"), MultiValueAnswer: aws.Bool(true), }, { Name: aws.String("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("test-set-1"), GeoLocation: &route53types.GeoLocation{ ContinentCode: aws.String("EU"), }, }, { Name: aws.String("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("4.3.2.1")}}, SetIdentifier: aws.String("test-set-2"), GeoLocation: &route53types.GeoLocation{ CountryCode: aws.String("DE"), }, }, { Name: aws.String("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("test-set-1"), GeoLocation: &route53types.GeoLocation{ SubdivisionCode: aws.String("NY"), }, }, { Name: aws.String("geoproximitylocation-region.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("test-set-1"), GeoProximityLocation: &route53types.GeoProximityLocation{ AWSRegion: aws.String("us-west-2"), Bias: aws.Int32(10), }, }, { Name: aws.String("geoproximitylocation-localzone.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("test-set-1"), GeoProximityLocation: &route53types.GeoProximityLocation{ LocalZoneGroup: aws.String("usw2-pdx1-az1"), Bias: aws.Int32(10), }, }, { Name: aws.String("geoproximitylocation-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("test-set-1"), GeoProximityLocation: &route53types.GeoProximityLocation{ Coordinates: &route53types.Coordinates{ Latitude: aws.String("90"), Longitude: aws.String("90"), }, Bias: aws.Int32(0), }, }, { Name: aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("foo.example.com")}}, SetIdentifier: aws.String("test-set-1"), HealthCheckId: aws.String("foo-bar-healthcheck-id"), Weight: aws.Int64(10), }, { Name: aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("4.3.2.1")}}, SetIdentifier: aws.String("test-set-2"), HealthCheckId: aws.String("abc-def-healthcheck-id"), Weight: aws.Int64(20), }, { Name: aws.String("mail.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeMx, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("10 mailhost1.example.com")}, {Value: aws.String("20 mailhost2.example.com")}}, }, }) records, err := provider.Records(context.Background()) require.NoError(t, err) validateEndpoints(t, provider, records, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4"), endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("escape-%!s()-codes.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(defaultTTL), "example").WithProviderSpecific(providerSpecificAlias, "false"), endpoint.NewEndpointWithTTL("escape-%!s()-codes-a.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4"), endpoint.NewEndpointWithTTL("escape-%!s()-codes-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "escape-codes.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpointWithTTL("escape-%!s()-codes-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, endpoint.TTL(defaultTTL), "escape-codes.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, endpoint.TTL(defaultTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpointWithTTL("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpointWithTTL("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, endpoint.TTL(defaultTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpointWithTTL("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpointWithTTL("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, endpoint.TTL(defaultTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "8.8.8.8", "8.8.4.4"), endpoint.NewEndpointWithTTL("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(defaultTTL), "random"), endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10"), endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20"), endpoint.NewEndpointWithTTL("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificRegion, "us-east-1"), endpoint.NewEndpointWithTTL("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificFailover, "PRIMARY"), endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""), endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"), endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"), endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"), endpoint.NewEndpointWithTTL("geoproximitylocation-region.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"), endpoint.NewEndpointWithTTL("geoproximitylocation-localzone.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-pdx1-az1").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"), endpoint.NewEndpointWithTTL("geoproximitylocation-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, "90,90").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "0"), endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(defaultTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id").WithProviderSpecific(providerSpecificAlias, "false"), endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"), endpoint.NewEndpointWithTTL("mail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, endpoint.TTL(defaultTTL), "10 mailhost1.example.com", "20 mailhost2.example.com"), }) } func TestAWSRecordsSoftError(t *testing.T) { pvd, subClient := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, []route53types.ResourceRecordSet{ { Name: aws.String("list-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, }, }) subClient.MockMethod("ListResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure")) _, err := pvd.Records(context.Background()) require.Error(t, err) require.ErrorIs(t, err, provider.SoftError) } func TestAWSAdjustEndpoints(t *testing.T) { provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) records := []*endpoint.Endpoint{ endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"), 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"), endpoint.NewEndpointWithTTL("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, 60, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), 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"), 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 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"), endpoint.NewEndpoint("a-test-geoproximity-no-bias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2"), } records, err := provider.AdjustEndpoints(records) require.NoError(t, err) validateEndpoints(t, provider, records, []*endpoint.Endpoint{ endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com").WithProviderSpecific(providerSpecificAlias, "false"), 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"), endpoint.NewEndpointWithTTL("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, 300, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), 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"), endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), 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"), 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 endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health 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"), endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpoint("a-test-geoproximity-no-bias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "0"), }) } func TestAWSApplyChanges(t *testing.T) { tests := []struct { name string setup func(p *AWSProvider) context.Context listRRSets int }{ {"no cache", func(p *AWSProvider) context.Context { return context.Background() }, 0}, {"cached", func(p *AWSProvider) context.Context { ctx := context.Background() records, err := p.Records(ctx) require.NoError(t, err) return context.WithValue(ctx, provider.RecordsContextKey, records) }, 0}, } for _, tt := range tests { provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []route53types.ResourceRecordSet{ { Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, }, { Name: aws.String("update-test-aaaa.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1111")}}, }, { Name: aws.String("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, }, { Name: aws.String("delete-test-aaaa.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1111")}}, }, { Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.4.4")}}, }, { Name: aws.String("update-test-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1001")}}, }, { Name: aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.4.4")}}, }, { Name: aws.String("delete-test-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1001")}}, }, { Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.1.1.1")}}, }, { Name: aws.String("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, }, { Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("bar.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("bar.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, }, { Name: aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("qux.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("qux.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, }, { Name: aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, }, { Name: aws.String("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, }, { Name: aws.String("delete-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1111")}, {Value: aws.String("2606:4700:4700::1001")}}, }, { Name: aws.String("delete-test-geoproximity.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("geoproximity-delete"), GeoProximityLocation: &route53types.GeoProximityLocation{ AWSRegion: aws.String("us-west-2"), Bias: aws.Int32(10), }, }, { Name: aws.String("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("geoproximity-update"), GeoProximityLocation: &route53types.GeoProximityLocation{ LocalZoneGroup: aws.String("usw2-lax1-az2"), }, }, { Name: aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("weighted-to-simple"), Weight: aws.Int64(10), }, { Name: aws.String("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, }, { Name: aws.String("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("policy-change"), Weight: aws.Int64(10), }, { Name: aws.String("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("before"), Weight: aws.Int64(10), }, { Name: aws.String("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("no-change"), Weight: aws.Int64(10), }, { Name: aws.String("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeMx, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("10 mailhost2.bar.elb.amazonaws.com")}}, }, { Name: aws.String("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeMx, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("30 mailhost1.foo.elb.amazonaws.com")}}, }, { Name: aws.String(specialCharactersEscape("escape-%!s()-codes.zone-2.ext-dns-test-2.teapot.zalan.do.")), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("no-change"), Weight: aws.Int64(10), }, }) createRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), endpoint.NewEndpoint("create-test-aaaa.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111"), endpoint.NewEndpoint("create-test-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1001"), endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), endpoint.NewEndpoint("create-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"), endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-geoproximity-region.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"). WithSetIdentifier("geoproximity-region"). WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2"). WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"), endpoint.NewEndpoint("create-test-geoproximity-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"). WithSetIdentifier("geoproximity-coordinates"). WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, "60,60"), } currentRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), endpoint.NewEndpoint("update-test-aaaa.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111"), endpoint.NewEndpoint("update-test-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1001"), endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"), 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"), endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "bar.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "bar.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), endpoint.NewEndpoint("update-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"), endpoint.NewEndpoint("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"). WithSetIdentifier("geoproximity-update"). WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-lax1-az2"), 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"), endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 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"), 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"), 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"), endpoint.NewEndpoint("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost2.bar.elb.amazonaws.com"), endpoint.NewEndpoint("escape-%!s()-codes.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithSetIdentifier("no-change").WithProviderSpecific(providerSpecificWeight, "10"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"), endpoint.NewEndpoint("update-test-aaaa.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1001"), endpoint.NewEndpoint("update-test-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111"), 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"), endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "my-internal-host.example.com"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "baz.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "baz.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), endpoint.NewEndpoint("update-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1001", "2606:4700:4700::1111"), endpoint.NewEndpoint("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"). WithSetIdentifier("geoproximity-update"). WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-phx2-az1"), endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 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"), 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"), 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"), 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"), endpoint.NewEndpoint("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "20 mailhost3.foo.elb.amazonaws.com"), } deleteRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), endpoint.NewEndpoint("delete-test-aaaa.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111"), endpoint.NewEndpoint("delete-test-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1001"), endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "qux.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "qux.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), endpoint.NewEndpoint("delete-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"), endpoint.NewEndpoint("delete-test-geoproximity.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("geoproximity-delete").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"), endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mailhost1.foo.elb.amazonaws.com"), } changes := &plan.Changes{ Create: createRecords, UpdateNew: updatedRecords, UpdateOld: currentRecords, Delete: deleteRecords, } ctx := tt.setup(provider) provider.zonesCache = &zonesListCache{duration: 0 * time.Minute} counter := NewRoute53APICounter(provider.clients[defaultAWSProfile]) provider.clients[defaultAWSProfile] = counter require.NoError(t, provider.ApplyChanges(ctx, changes)) assert.Equal(t, 1, counter.calls["ListHostedZonesPages"], tt.name) assert.Equal(t, tt.listRRSets, counter.calls["ListResourceRecordSetsPages"], tt.name) validateRecords(t, listAWSRecords(t, provider.clients[defaultAWSProfile], "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []route53types.ResourceRecordSet{ { Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, }, { Name: aws.String("create-test-aaaa.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1111")}}, }, { Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, }, { Name: aws.String("update-test-aaaa.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1001")}}, }, { Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), }, }, { Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), }, }, { Name: aws.String("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("my-internal-host.example.com")}}, }, { Name: aws.String("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}}, }, { Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("baz.elb.amazonaws.com")}}, }, { Name: aws.String("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}}, }, { Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("baz.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), }, }, { Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("baz.elb.amazonaws.com."), EvaluateTargetHealth: true, HostedZoneId: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), }, }, { Name: aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, }, { Name: aws.String("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("simple-to-weighted"), Weight: aws.Int64(10), }, { Name: aws.String("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("policy-change"), Region: route53types.ResourceRecordSetRegionUsEast1, }, { Name: aws.String("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("after"), Weight: aws.Int64(10), }, { Name: aws.String("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("no-change"), Weight: aws.Int64(20), }, { Name: aws.String("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeMx, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}}, }, { Name: aws.String("create-test-geoproximity-region.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, SetIdentifier: aws.String("geoproximity-region"), GeoProximityLocation: &route53types.GeoProximityLocation{ AWSRegion: aws.String("us-west-2"), Bias: aws.Int32(10), }, }, { Name: aws.String("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("geoproximity-update"), GeoProximityLocation: &route53types.GeoProximityLocation{ LocalZoneGroup: aws.String("usw2-phx2-az1"), }, }, { Name: aws.String("create-test-geoproximity-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, SetIdentifier: aws.String("geoproximity-coordinates"), GeoProximityLocation: &route53types.GeoProximityLocation{ Coordinates: &route53types.Coordinates{ Latitude: aws.String("60"), Longitude: aws.String("60"), }, }, }, }) validateRecords(t, listAWSRecords(t, provider.clients[defaultAWSProfile], "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []route53types.ResourceRecordSet{ { Name: aws.String("escape-\\045\\041s\\050\\074nil\\076\\051-codes.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, SetIdentifier: aws.String("no-change"), Weight: aws.Int64(10), }, { Name: aws.String("create-test.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.4.4")}}, }, { Name: aws.String("create-test-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1001")}}, }, { Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("4.3.2.1")}}, }, { Name: aws.String("update-test-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1111")}}, }, { Name: aws.String("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, }, { Name: aws.String("create-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1111")}, {Value: aws.String("2606:4700:4700::1001")}}, }, { Name: aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, }, { Name: aws.String("update-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1001")}, {Value: aws.String("2606:4700:4700::1111")}}, }, { Name: aws.String("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeMx, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("20 mailhost3.foo.elb.amazonaws.com")}}, }, }) } } func TestAWSApplyChangesDryRun(t *testing.T) { originalRecords := []route53types.ResourceRecordSet{ { Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, }, { Name: aws.String("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, }, { Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.4.4")}}, }, { Name: aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.4.4")}}, }, { Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.1.1.1")}}, }, { Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, }, { Name: aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, }, { Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, }, { Name: aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, }, { Name: aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, }, { Name: aws.String("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, }, { Name: aws.String("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeMx, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("20 mail.foo.elb.amazonaws.com")}}, }, { Name: aws.String("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeMx, TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("10 mail.bar.elb.amazonaws.com")}}, }, } provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalRecords) createRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mail.foo.elb.amazonaws.com"), } currentRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "20 mail.foo.elb.amazonaws.com"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"), endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mail.bar.elb.amazonaws.com"), } deleteRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mail.bar.elb.amazonaws.com"), } changes := &plan.Changes{ Create: createRecords, UpdateNew: updatedRecords, UpdateOld: currentRecords, Delete: deleteRecords, } ctx := context.Background() require.NoError(t, provider.ApplyChanges(ctx, changes)) validateRecords(t, append( listAWSRecords(t, provider.clients[defaultAWSProfile], "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), listAWSRecords(t, provider.clients[defaultAWSProfile], "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.")...), originalRecords) } func TestAWSChangesByZones(t *testing.T) { changes := Route53Changes{ { Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("qux.foo.example.org"), TTL: aws.Int64(1), }, }, }, { Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2), }, }, }, { Change: route53types.Change{ Action: route53types.ChangeActionDelete, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("wambo.foo.example.org"), TTL: aws.Int64(10), }, }, }, { Change: route53types.Change{ Action: route53types.ChangeActionDelete, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20), }, }, }, } zones := map[string]*profiledZone{ "foo-example-org": { profile: defaultAWSProfile, zone: &route53types.HostedZone{ Id: aws.String("foo-example-org"), Name: aws.String("foo.example.org."), }, }, "bar-example-org": { profile: defaultAWSProfile, zone: &route53types.HostedZone{ Id: aws.String("bar-example-org"), Name: aws.String("bar.example.org."), }, }, "bar-example-org-private": { profile: defaultAWSProfile, zone: &route53types.HostedZone{ Id: aws.String("bar-example-org-private"), Name: aws.String("bar.example.org."), Config: &route53types.HostedZoneConfig{PrivateZone: true}, }, }, "baz-example-org": { profile: defaultAWSProfile, zone: &route53types.HostedZone{ Id: aws.String("baz-example-org"), Name: aws.String("baz.example.org."), }, }, } changesByZone := changesByZone(zones, changes) require.Len(t, changesByZone, 3) validateAWSChangeRecords(t, changesByZone["foo-example-org"], Route53Changes{ { Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("qux.foo.example.org"), TTL: aws.Int64(1), }, }, }, { Change: route53types.Change{ Action: route53types.ChangeActionDelete, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("wambo.foo.example.org"), TTL: aws.Int64(10), }, }, }, }) validateAWSChangeRecords(t, changesByZone["bar-example-org"], Route53Changes{ { Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2), }, }, }, { Change: route53types.Change{ Action: route53types.ChangeActionDelete, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20), }, }, }, }) validateAWSChangeRecords(t, changesByZone["bar-example-org-private"], Route53Changes{ { Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2), }, }, }, { Change: route53types.Change{ Action: route53types.ChangeActionDelete, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20), }, }, }, }) } func TestAWSsubmitChanges(t *testing.T) { provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) const subnets = 16 const hosts = defaultBatchChangeSize / subnets endpoints := make([]*endpoint.Endpoint, 0) for i := 0; i < subnets; i++ { for j := 1; j < (hosts + 1); j++ { hostname := fmt.Sprintf("subnet%dhost%d.zone-1.ext-dns-test-2.teapot.zalan.do", i, j) ip := fmt.Sprintf("1.1.%d.%d", i, j) ep := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, endpoint.TTL(defaultTTL), ip) endpoints = append(endpoints, ep) } } ctx := context.Background() zones, _ := provider.zones(ctx) records, _ := provider.Records(ctx) cs := make(Route53Changes, 0, len(endpoints)) cs = append(cs, provider.newChanges(route53types.ChangeActionCreate, endpoints)...) require.NoError(t, provider.submitChanges(ctx, cs, zones)) records, err := provider.Records(ctx) require.NoError(t, err) validateEndpoints(t, provider, records, endpoints) } func TestAWSsubmitChangesError(t *testing.T) { provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure")) ctx := context.Background() zones, err := provider.zones(ctx) require.NoError(t, err) ep := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.0.0.1") cs := provider.newChanges(route53types.ChangeActionCreate, []*endpoint.Endpoint{ep}) require.Error(t, provider.submitChanges(ctx, cs, zones)) } func TestAWSsubmitChangesRetryOnError(t *testing.T) { provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) ctx := context.Background() zones, err := provider.zones(ctx) require.NoError(t, err) ep1 := endpoint.NewEndpointWithTTL("success.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.0.0.1") ep2 := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.0.0.2") ep3 := endpoint.NewEndpointWithTTL("success2.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.0.0.3") ep2txt := endpoint.NewEndpointWithTTL("fail__edns_housekeeping.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(defaultTTL), "something") // "__edns_housekeeping" is the TXT suffix ep2txt.Labels = map[string]string{ endpoint.OwnedRecordLabelKey: "fail.zone-1.ext-dns-test-2.teapot.zalan.do", } // "success" and "fail" are created in the first step, both are submitted in the same batch; this should fail cs1 := provider.newChanges(route53types.ChangeActionCreate, []*endpoint.Endpoint{ep2, ep2txt, ep1}) input1 := &route53.ChangeResourceRecordSetsInput{ HostedZoneId: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), ChangeBatch: &route53types.ChangeBatch{ Changes: cs1.Route53Changes(), }, } clientStub.MockMethod("ChangeResourceRecordSets", input1).Return(nil, fmt.Errorf("Mock route53 failure")) // because of the failure, changes will be retried one by one; make "fail" submitted in its own batch fail as well cs2 := provider.newChanges(route53types.ChangeActionCreate, []*endpoint.Endpoint{ep2, ep2txt}) input2 := &route53.ChangeResourceRecordSetsInput{ HostedZoneId: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), ChangeBatch: &route53types.ChangeBatch{ Changes: cs2.Route53Changes(), }, } clientStub.MockMethod("ChangeResourceRecordSets", input2).Return(nil, fmt.Errorf("Mock route53 failure")) // "success" should have been created, verify that we still get an error because "fail" failed require.Error(t, provider.submitChanges(ctx, cs1, zones)) // assert that "success" was successfully created and "fail" and its TXT record were not records, err := provider.Records(ctx) require.NoError(t, err) require.True(t, containsRecordWithDNSName(records, "success.zone-1.ext-dns-test-2.teapot.zalan.do")) require.False(t, containsRecordWithDNSName(records, "fail.zone-1.ext-dns-test-2.teapot.zalan.do")) require.False(t, containsRecordWithDNSName(records, "fail__edns_housekeeping.zone-1.ext-dns-test-2.teapot.zalan.do")) // next batch should contain "fail" and "success2", should succeed this time cs3 := provider.newChanges(route53types.ChangeActionCreate, []*endpoint.Endpoint{ep2, ep2txt, ep3}) require.NoError(t, provider.submitChanges(ctx, cs3, zones)) // verify all records are there records, err = provider.Records(ctx) require.NoError(t, err) require.True(t, containsRecordWithDNSName(records, "success.zone-1.ext-dns-test-2.teapot.zalan.do")) require.True(t, containsRecordWithDNSName(records, "fail.zone-1.ext-dns-test-2.teapot.zalan.do")) require.True(t, containsRecordWithDNSName(records, "success2.zone-1.ext-dns-test-2.teapot.zalan.do")) require.True(t, containsRecordWithDNSName(records, "fail__edns_housekeeping.zone-1.ext-dns-test-2.teapot.zalan.do")) } func TestAWSBatchChangeSet(t *testing.T) { var cs Route53Changes for i := 1; i <= defaultBatchChangeSize; i += 2 { cs = append(cs, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeA, }, }, }) cs = append(cs, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeTxt, }, }, }) } batchCs := batchChangeSet(cs, defaultBatchChangeSize, defaultBatchChangeSizeBytes, defaultBatchChangeSizeValues) require.Len(t, batchCs, 1) // sorting cs not needed as it should be returned as is validateAWSChangeRecords(t, batchCs[0], cs) } func TestAWSBatchChangeSetExceeding(t *testing.T) { var cs Route53Changes const testCount = 50 const testLimit = 11 const expectedBatchCount = 5 const expectedChangesCount = 10 for i := 1; i <= testCount; i += 2 { cs = append(cs, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeA, }, }, }, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeTxt, }, }, }, ) } batchCs := batchChangeSet(cs, testLimit, defaultBatchChangeSizeBytes, defaultBatchChangeSizeValues) require.Len(t, batchCs, expectedBatchCount) // sorting cs needed to match batchCs for i, batch := range batchCs { validateAWSChangeRecords(t, batch, sortChangesByActionNameType(cs)[i*expectedChangesCount:expectedChangesCount*(i+1)]) } } func TestAWSBatchChangeSetExceedingNameChange(t *testing.T) { var cs Route53Changes const testCount = 10 const testLimit = 1 for i := 1; i <= testCount; i += 2 { cs = append(cs, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeA, }, }, }, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeTxt, }, }, }, ) } batchCs := batchChangeSet(cs, testLimit, defaultBatchChangeSizeBytes, defaultBatchChangeSizeValues) require.Empty(t, batchCs) } func TestAWSBatchChangeSetExceedingBytesLimit(t *testing.T) { const ( testCount = 50 testLimit = 100 groupSize = 2 ) var ( cs Route53Changes // Bytes for each name testBytes = len([]byte("1.2.3.4")) + len([]byte("test-record")) // testCount / groupSize / (testLimit // bytes) expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testBytes) // Round up expectedBatchCount = int(math.Ceil(expectedBatchCountFloat)) ) for i := 1; i <= testCount; i += groupSize { cs = append(cs, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeA, ResourceRecords: []route53types.ResourceRecord{ { Value: aws.String("1.2.3.4"), }, }, }, }, sizeBytes: len([]byte("1.2.3.4")), sizeValues: 1, }, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeTxt, ResourceRecords: []route53types.ResourceRecord{ { Value: aws.String("txt-record"), }, }, }, }, sizeBytes: len([]byte("txt-record")), sizeValues: 1, }, ) } batchCs := batchChangeSet(cs, defaultBatchChangeSize, testLimit, defaultBatchChangeSizeValues) require.Len(t, batchCs, expectedBatchCount) } func TestAWSBatchChangeSetExceedingBytesLimitUpsert(t *testing.T) { const ( testCount = 50 testLimit = 100 groupSize = 2 ) var ( cs Route53Changes // Bytes for each name multiplied by 2 for Upsert records testBytes = (len([]byte("1.2.3.4")) + len([]byte("test-record"))) * 2 // testCount / groupSize / (testLimit // bytes) expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testBytes) // Round up expectedBatchCount = int(math.Ceil(expectedBatchCountFloat)) ) for i := 1; i <= testCount; i += groupSize { cs = append(cs, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionUpsert, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeA, ResourceRecords: []route53types.ResourceRecord{ { Value: aws.String("1.2.3.4"), }, }, }, }, sizeBytes: len([]byte("1.2.3.4")) * 2, sizeValues: 1, }, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionUpsert, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeTxt, ResourceRecords: []route53types.ResourceRecord{ { Value: aws.String("txt-record"), }, }, }, }, sizeBytes: len([]byte("txt-record")) * 2, sizeValues: 1, }, ) } batchCs := batchChangeSet(cs, defaultBatchChangeSize, testLimit, defaultBatchChangeSizeValues) require.Len(t, batchCs, expectedBatchCount) } func TestAWSBatchChangeSetExceedingValuesLimit(t *testing.T) { const ( testCount = 50 testLimit = 100 groupSize = 2 // Values for each group testValues = 2 ) var ( cs Route53Changes // testCount / groupSize / (testLimit // bytes) expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testValues) // Round up expectedBatchCount = int(math.Ceil(expectedBatchCountFloat)) ) for i := 1; i <= testCount; i += groupSize { cs = append(cs, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeA, ResourceRecords: []route53types.ResourceRecord{ { Value: aws.String("1.2.3.4"), }, }, }, }, sizeBytes: len([]byte("1.2.3.4")), sizeValues: 1, }, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeTxt, ResourceRecords: []route53types.ResourceRecord{ { Value: aws.String("txt-record"), }, }, }, }, sizeBytes: len([]byte("txt-record")), sizeValues: 1, }, ) } batchCs := batchChangeSet(cs, defaultBatchChangeSize, defaultBatchChangeSizeBytes, testLimit) require.Len(t, batchCs, expectedBatchCount) } func TestAWSBatchChangeSetExceedingValuesLimitUpsert(t *testing.T) { const ( testCount = 50 testLimit = 100 groupSize = 2 // Values for each group multiplied by 2 for Upsert records testValues = 2 * 2 ) var ( cs Route53Changes // testCount / groupSize / (testLimit // bytes) expectedBatchCountFloat = float64(testCount) / float64(groupSize) / float64(testLimit/testValues) // Round up expectedBatchCount = int(math.Ceil(expectedBatchCountFloat)) ) for i := 1; i <= testCount; i += groupSize { cs = append(cs, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionUpsert, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeA, ResourceRecords: []route53types.ResourceRecord{ { Value: aws.String("1.2.3.4"), }, }, }, }, sizeBytes: len([]byte("1.2.3.4")) * 2, sizeValues: 1, }, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionUpsert, ResourceRecordSet: &route53types.ResourceRecordSet{ Name: aws.String(fmt.Sprintf("host-%d", i)), Type: route53types.RRTypeTxt, ResourceRecords: []route53types.ResourceRecord{ { Value: aws.String("txt-record"), }, }, }, }, sizeBytes: len([]byte("txt-record")) * 2, sizeValues: 1, }, ) } batchCs := batchChangeSet(cs, defaultBatchChangeSize, defaultBatchChangeSizeBytes, testLimit) require.Len(t, batchCs, expectedBatchCount) } func validateEndpoints(t *testing.T, provider *AWSProvider, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %+v:%+v", endpoints, expected) normalized, err := provider.AdjustEndpoints(endpoints) assert.NoError(t, err) assert.True(t, testutils.SameEndpoints(normalized, expected), "normalized and expected endpoints don't match. %+v:%+v", normalized, expected) } func validateAWSZones(t *testing.T, zones map[string]*route53types.HostedZone, expected map[string]*route53types.HostedZone) { require.Len(t, zones, len(expected)) for i, zone := range zones { validateAWSZone(t, zone, expected[i]) } } func validateAWSZone(t *testing.T, zone *route53types.HostedZone, expected *route53types.HostedZone) { assert.Equal(t, *expected.Id, *zone.Id) assert.Equal(t, *expected.Name, *zone.Name) } func validateAWSChangeRecords(t *testing.T, records Route53Changes, expected Route53Changes) { require.Len(t, records, len(expected)) for i := range records { validateAWSChangeRecord(t, records[i], expected[i]) } } func validateAWSChangeRecord(t *testing.T, record *Route53Change, expected *Route53Change) { assert.Equal(t, expected.Action, record.Action) assert.Equal(t, *expected.ResourceRecordSet.Name, *record.ResourceRecordSet.Name) assert.Equal(t, expected.ResourceRecordSet.Type, record.ResourceRecordSet.Type) } func TestAWSCreateRecordsWithCNAME(t *testing.T) { provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) records := []*endpoint.Endpoint{ {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.example.org"}, RecordType: endpoint.RecordTypeCNAME}, } adjusted, err := provider.AdjustEndpoints(records) require.NoError(t, err) require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ Create: adjusted, })) recordSets := listAWSRecords(t, provider.clients[defaultAWSProfile], "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") validateRecords(t, recordSets, []route53types.ResourceRecordSet{ { Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, TTL: aws.Int64(300), ResourceRecords: []route53types.ResourceRecord{ { Value: aws.String("foo.example.org"), }, }, }, }) } func TestAWSCreateRecordsWithALIAS(t *testing.T) { for key, evaluateTargetHealth := range map[string]bool{ "true": true, "false": false, "": false, } { provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) records := []*endpoint.Endpoint{ { DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeA, ProviderSpecific: endpoint.ProviderSpecific{ endpoint.ProviderSpecificProperty{ Name: providerSpecificAlias, Value: "true", }, endpoint.ProviderSpecificProperty{ Name: providerSpecificEvaluateTargetHealth, Value: key, }, }, }, { DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeAAAA, ProviderSpecific: endpoint.ProviderSpecific{ endpoint.ProviderSpecificProperty{ Name: providerSpecificAlias, Value: "true", }, endpoint.ProviderSpecificProperty{ Name: providerSpecificEvaluateTargetHealth, Value: key, }, }, }, } adjusted, err := provider.AdjustEndpoints(records) require.NoError(t, err) require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ Create: adjusted, })) recordSets := listAWSRecords(t, provider.clients[defaultAWSProfile], "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") validateRecords(t, recordSets, []route53types.ResourceRecordSet{ { AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: evaluateTargetHealth, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, }, { AliasTarget: &route53types.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: evaluateTargetHealth, HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeAaaa, }, }) } } func TestAWSisLoadBalancer(t *testing.T) { for _, tc := range []struct { target string recordType string preferCNAME bool expected bool }{ {"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME, false, true}, {"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME, true, false}, {"foo.example.org", endpoint.RecordTypeCNAME, false, false}, {"foo.example.org", endpoint.RecordTypeCNAME, true, false}, } { ep := &endpoint.Endpoint{ Targets: endpoint.Targets{tc.target}, RecordType: tc.recordType, } assert.Equal(t, tc.expected, useAlias(ep, tc.preferCNAME)) } } func TestAWSisAWSAlias(t *testing.T) { for _, tc := range []struct { target string recordType string alias bool hz string }{ {"foo.example.org", endpoint.RecordTypeA, false, ""}, // normal A record {"foo.example.org", endpoint.RecordTypeAAAA, false, ""}, // normal AAAA record {"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeA, true, "Z215JYRZR1TBD5"}, // pointing to ELB DNS name (alias A) {"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeAAAA, true, "Z215JYRZR1TBD5"}, // pointing to ELB DNS name (alias AAAA) {"foobar.example.org", endpoint.RecordTypeA, true, "Z1234567890ABC"}, // HZID retrieved by Route53 (alias A) {"foobar.example.org", endpoint.RecordTypeAAAA, true, "Z1234567890ABC"}, // HZID retrieved by Route53 (alias AAAA) {"baz.example.org", endpoint.RecordTypeA, true, sameZoneAlias}, // record to be created (alias A) {"baz.example.org", endpoint.RecordTypeAAAA, true, sameZoneAlias}, // record to be created (alias AAAA) } { ep := &endpoint.Endpoint{ Targets: endpoint.Targets{tc.target}, RecordType: tc.recordType, } if tc.alias { ep = ep.WithProviderSpecific(providerSpecificAlias, "true") ep = ep.WithProviderSpecific(providerSpecificTargetHostedZone, tc.hz) } assert.Equal(t, tc.hz, isAWSAlias(ep), "%v", tc) } } func TestAWSCanonicalHostedZone(t *testing.T) { for suffix, id := range canonicalHostedZones { zone := canonicalHostedZone(fmt.Sprintf("foo.%s", suffix)) assert.Equal(t, id, zone, "zone suffix: %s", suffix) } zone := canonicalHostedZone("foo.example.org") assert.Empty(t, zone, "no canonical zone should be returned for a non-aws hostname") } func TestAWSCanonicalHostedZoneNotExist(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) host := "foo.elb.eastwest-1.amazonaws.com" _ = canonicalHostedZone(host) assert.Containsf(t, buf.String(), "Could not find canonical hosted zone for domain", host) } func BenchmarkTestAWSCanonicalHostedZone(b *testing.B) { for i := 0; i < b.N; i++ { for suffix := range canonicalHostedZones { _ = canonicalHostedZone(fmt.Sprintf("foo.%s", suffix)) } } } func BenchmarkTestAWSNonCanonicalHostedZone(b *testing.B) { for i := 0; i < b.N; i++ { for range canonicalHostedZones { _ = canonicalHostedZone("extremely.long.zone-2.ext.dns.test.zone.non.canonical.example.com") } } } func TestAWSSuitableZones(t *testing.T) { zones := map[string]*profiledZone{ // Public domain "example-org": {profile: defaultAWSProfile, zone: &route53types.HostedZone{Id: aws.String("example-org"), Name: aws.String("example.org.")}}, // Public subdomain "bar-example-org": {profile: defaultAWSProfile, zone: &route53types.HostedZone{Id: aws.String("bar-example-org"), Name: aws.String("bar.example.org."), Config: &route53types.HostedZoneConfig{PrivateZone: false}}}, // Public subdomain "longfoo-bar-example-org": {profile: defaultAWSProfile, zone: &route53types.HostedZone{Id: aws.String("longfoo-bar-example-org"), Name: aws.String("longfoo.bar.example.org.")}}, // Private domain "example-org-private": {profile: defaultAWSProfile, zone: &route53types.HostedZone{Id: aws.String("example-org-private"), Name: aws.String("example.org."), Config: &route53types.HostedZoneConfig{PrivateZone: true}}}, // Private subdomain "bar-example-org-private": {profile: defaultAWSProfile, zone: &route53types.HostedZone{Id: aws.String("bar-example-org-private"), Name: aws.String("bar.example.org."), Config: &route53types.HostedZoneConfig{PrivateZone: true}}}, } for _, tc := range []struct { hostname string expected []*profiledZone }{ // bar.example.org is NOT suitable {"foobar.example.org.", []*profiledZone{zones["example-org-private"], zones["example-org"]}}, // all matching private zones are suitable // https://github.com/kubernetes-sigs/external-dns/pull/356 {"bar.example.org.", []*profiledZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}}, {"foo.bar.example.org.", []*profiledZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}}, {"foo.example.org.", []*profiledZone{zones["example-org-private"], zones["example-org"]}}, {"foo.kubernetes.io.", nil}, } { suitableZones := suitableZones(tc.hostname, zones) sort.Slice(suitableZones, func(i, j int) bool { return *suitableZones[i].zone.Id < *suitableZones[j].zone.Id }) sort.Slice(tc.expected, func(i, j int) bool { return *tc.expected[i].zone.Id < *tc.expected[j].zone.Id }) assert.Equal(t, tc.expected, suitableZones) } } func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53types.HostedZone) { params := &route53.CreateHostedZoneInput{ CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"), Name: zone.Name, HostedZoneConfig: zone.Config, } if _, err := provider.clients[defaultAWSProfile].CreateHostedZone(context.Background(), params); err != nil { var hzExists *route53types.HostedZoneAlreadyExists require.ErrorAs(t, err, &hzExists) } } func setAWSRecords(t *testing.T, provider *AWSProvider, records []route53types.ResourceRecordSet) { dryRun := provider.dryRun provider.dryRun = false defer func() { provider.dryRun = dryRun }() ctx := context.Background() endpoints, err := provider.Records(ctx) require.NoError(t, err) validateEndpoints(t, provider, endpoints, []*endpoint.Endpoint{}) var changes Route53Changes for _, record := range records { changes = append(changes, &Route53Change{ Change: route53types.Change{ Action: route53types.ChangeActionCreate, ResourceRecordSet: &record, }, }) } zones, err := provider.zones(ctx) require.NoError(t, err) err = provider.submitChanges(ctx, changes, zones) require.NoError(t, err) _, err = provider.Records(ctx) require.NoError(t, err) } func listAWSRecords(t *testing.T, client Route53API, zone string) []route53types.ResourceRecordSet { resp, err := client.ListResourceRecordSets(context.Background(), &route53.ListResourceRecordSetsInput{ HostedZoneId: aws.String(zone), MaxItems: aws.Int32(route53PageSize), }) require.NoError(t, err) return resp.ResourceRecordSets } func newAWSProvider(t *testing.T, domainFilter *endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []route53types.ResourceRecordSet) (*AWSProvider, *Route53APIStub) { return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, provider.NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records) } func newAWSProviderWithTagFilter(t *testing.T, domainFilter *endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, zoneTagFilter provider.ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []route53types.ResourceRecordSet) (*AWSProvider, *Route53APIStub) { client := NewRoute53APIStub(t) provider := &AWSProvider{ clients: map[string]Route53API{defaultAWSProfile: client}, batchChangeSize: defaultBatchChangeSize, batchChangeSizeBytes: defaultBatchChangeSizeBytes, batchChangeSizeValues: defaultBatchChangeSizeValues, batchChangeInterval: defaultBatchChangeInterval, evaluateTargetHealth: evaluateTargetHealth, domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, zoneTypeFilter: zoneTypeFilter, zoneTagFilter: zoneTagFilter, dryRun: false, zonesCache: &zonesListCache{duration: 1 * time.Minute}, failedChangesQueue: make(map[string]Route53Changes), } createAWSZone(t, provider, &route53types.HostedZone{ Id: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), Config: &route53types.HostedZoneConfig{PrivateZone: false}, }) createAWSZone(t, provider, &route53types.HostedZone{ Id: aws.String("/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."), Config: &route53types.HostedZoneConfig{PrivateZone: false}, }) createAWSZone(t, provider, &route53types.HostedZone{ Id: aws.String("/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("zone-3.ext-dns-test-2.teapot.zalan.do."), Config: &route53types.HostedZoneConfig{PrivateZone: true}, }) // filtered out by domain filter createAWSZone(t, provider, &route53types.HostedZone{ Id: aws.String("/hostedzone/zone-4.ext-dns-test-3.teapot.zalan.do."), Name: aws.String("zone-4.ext-dns-test-3.teapot.zalan.do."), Config: &route53types.HostedZoneConfig{PrivateZone: false}, }) setupZoneTags(provider.clients[defaultAWSProfile].(*Route53APIStub)) setAWSRecords(t, provider, records) provider.dryRun = dryRun return provider, client } func setupZoneTags(client *Route53APIStub) { addZoneTags(client.zoneTags, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.", map[string]string{ "zone-1-tag-1": "tag-1-value", "domain": "test-2", "zone": "1", }) addZoneTags(client.zoneTags, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.", map[string]string{ "zone-2-tag-1": "tag-1-value", "domain": "test-2", "zone": "2", }) addZoneTags(client.zoneTags, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.", map[string]string{ "zone-3-tag-1": "tag-1-value", "domain": "test-2", "zone": "3", }) addZoneTags(client.zoneTags, "/hostedzone/zone-4.ext-dns-test-2.teapot.zalan.do.", map[string]string{ "zone-4-tag-1": "tag-1-value", "domain": "test-3", "zone": "4", }) } func addZoneTags(tagMap map[string][]route53types.Tag, zoneID string, tags map[string]string) { tagList := make([]route53types.Tag, 0, len(tags)) for k, v := range tags { tagList = append(tagList, route53types.Tag{ Key: aws.String(k), Value: aws.String(v), }) } tagMap[zoneID] = tagList } func validateRecords(t *testing.T, records []route53types.ResourceRecordSet, expected []route53types.ResourceRecordSet) { assert.ElementsMatch(t, expected, records) } func containsRecordWithDNSName(records []*endpoint.Endpoint, dnsName string) bool { for _, record := range records { if record.DNSName == dnsName { return true } } return false } func TestRequiresDeleteCreate(t *testing.T) { provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"foo.bar."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) oldRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "8.8.8.8") newRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeCNAME, endpoint.TTL(defaultTTL), "bar").WithProviderSpecific(providerSpecificAlias, "false") assert.False(t, provider.requiresDeleteCreate(oldRecordType, oldRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, oldRecordType) assert.True(t, provider.requiresDeleteCreate(oldRecordType, newRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, newRecordType) oldAtoAlias := endpoint.NewEndpointWithTTL("AtoAlias", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.1.1.1") newAtoAlias := endpoint.NewEndpointWithTTL("AtoAlias", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "bar.us-east-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true") assert.False(t, provider.requiresDeleteCreate(oldAtoAlias, oldAtoAlias), "actual and expected endpoints don't match. %+v:%+v", oldAtoAlias, oldAtoAlias.DNSName) assert.True(t, provider.requiresDeleteCreate(oldAtoAlias, newAtoAlias), "actual and expected endpoints don't match. %+v:%+v", oldAtoAlias, newAtoAlias) oldPolicy := endpoint.NewEndpointWithTTL("policy", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "8.8.8.8").WithSetIdentifier("nochange").WithProviderSpecific(providerSpecificRegion, "us-east-1") newPolicy := endpoint.NewEndpointWithTTL("policy", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "8.8.8.8").WithSetIdentifier("nochange").WithProviderSpecific(providerSpecificWeight, "10") assert.False(t, provider.requiresDeleteCreate(oldPolicy, oldPolicy), "actual and expected endpoints don't match. %+v:%+v", oldPolicy, oldPolicy) assert.True(t, provider.requiresDeleteCreate(oldPolicy, newPolicy), "actual and expected endpoints don't match. %+v:%+v", oldPolicy, newPolicy) oldSetIdentifier := endpoint.NewEndpointWithTTL("setIdentifier", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "8.8.8.8").WithSetIdentifier("old") newSetIdentifier := endpoint.NewEndpointWithTTL("setIdentifier", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "8.8.8.8").WithSetIdentifier("new") assert.False(t, provider.requiresDeleteCreate(oldSetIdentifier, oldSetIdentifier), "actual and expected endpoints don't match. %+v:%+v", oldSetIdentifier, oldSetIdentifier) assert.True(t, provider.requiresDeleteCreate(oldSetIdentifier, newSetIdentifier), "actual and expected endpoints don't match. %+v:%+v", oldSetIdentifier, newSetIdentifier) } func TestConvertOctalToAscii(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "Characters escaped !\"#$%&'()*+,-/:;", input: "txt-\\041\\042\\043\\044\\045\\046\\047\\050\\051\\052\\053\\054-\\057\\072\\073-test.example.com", expected: "txt-!\"#$%&'()*+,-/:;-test.example.com", }, { name: "Characters escaped <=>?@[\\]^_`{|}~", input: "txt-\\074\\075\\076\\077\\100\\133\\134\\135\\136_\\140\\173\\174\\175\\176-test2.example.com", expected: "txt-<=>?@[\\]^_`{|}~-test2.example.com", }, { name: "No escaped characters in domain", input: "txt-awesome-test3.example.com", expected: "txt-awesome-test3.example.com", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := convertOctalToAscii(tt.input) assert.Equal(t, tt.expected, actual) }) } } func TestGeoProximityWithAWSRegion(t *testing.T) { tests := []struct { name string region string hasRegion bool expectedSet bool expectedRegion string }{ { name: "valid AWS region", region: "us-west-2", hasRegion: true, expectedSet: true, expectedRegion: "us-west-2", }, { name: "another valid AWS region", region: "eu-central-1", hasRegion: true, expectedSet: true, expectedRegion: "eu-central-1", }, { name: "empty region string", region: "", hasRegion: true, expectedSet: true, expectedRegion: "", }, { name: "no region property set", region: "", hasRegion: false, expectedSet: false, expectedRegion: "", }, { name: "region with special characters", region: "us-gov-west-1", hasRegion: true, expectedSet: true, expectedRegion: "us-gov-west-1", }, { name: "region with numbers", region: "ap-southeast-3", hasRegion: true, expectedSet: true, expectedRegion: "ap-southeast-3", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ep := &endpoint.Endpoint{ DNSName: "test.example.com", SetIdentifier: "test-set", } if tt.hasRegion { ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion, tt.region) } gp := newGeoProximity(ep) result := gp.withAWSRegion() assert.Equal(t, tt.expectedSet, result.isSet) if tt.expectedSet { assert.NotNil(t, result.location.AWSRegion) assert.Equal(t, tt.expectedRegion, *result.location.AWSRegion) } else { assert.Nil(t, result.location.AWSRegion) } // Verify the method returns the same instance for chaining assert.Equal(t, gp, result) }) } } func TestGeoProximityWithLocalZoneGroup(t *testing.T) { tests := []struct { name string localZoneGroup string hasLocalZoneGroup bool expectedSet bool expectedLocalZoneGroup string }{ { name: "valid local zone group", localZoneGroup: "usw2-lax1-az1", hasLocalZoneGroup: true, expectedSet: true, expectedLocalZoneGroup: "usw2-lax1-az1", }, { name: "empty local zone group", localZoneGroup: "", hasLocalZoneGroup: true, expectedSet: true, expectedLocalZoneGroup: "", }, { name: "no local zone group property", localZoneGroup: "", hasLocalZoneGroup: false, expectedSet: false, expectedLocalZoneGroup: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ep := &endpoint.Endpoint{ DNSName: "test.example.com", SetIdentifier: "test-set", } if tt.hasLocalZoneGroup { ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup, tt.localZoneGroup) } gp := newGeoProximity(ep) result := gp.withLocalZoneGroup() assert.Equal(t, tt.expectedSet, result.isSet) if tt.expectedSet { assert.NotNil(t, result.location.LocalZoneGroup) assert.Equal(t, tt.expectedLocalZoneGroup, *result.location.LocalZoneGroup) } else { assert.Nil(t, result.location.LocalZoneGroup) } // Verify method returns same instance for chaining assert.Equal(t, gp, result) }) } } func TestGeoProximityWithCoordinates(t *testing.T) { tests := []struct { name string coordinates string expectedSet bool expectedLat string expectedLong string shouldHaveCoords bool }{ { name: "valid coordinates", coordinates: "45.0,90.0", expectedSet: true, expectedLat: "45.0", expectedLong: "90.0", shouldHaveCoords: true, }, { name: "edge case min coordinates", coordinates: "-90.0,-180.0", expectedSet: true, expectedLat: "-90.0", expectedLong: "-180.0", shouldHaveCoords: true, }, { name: "edge case max coordinates", coordinates: "90.0,180.0", expectedSet: true, expectedLat: "90.0", expectedLong: "180.0", shouldHaveCoords: true, }, { name: "invalid latitude too high", coordinates: "91.0,90.0", expectedSet: false, shouldHaveCoords: false, }, { name: "invalid longitude too low", coordinates: "45.0,-181.0", expectedSet: false, shouldHaveCoords: false, }, { name: "invalid format - single value", coordinates: "45.0", expectedSet: false, shouldHaveCoords: false, }, { name: "invalid format - three values", coordinates: "45.0,90.0,10.0", expectedSet: false, shouldHaveCoords: false, }, { name: "invalid format - non-numeric", coordinates: "abc,def", expectedSet: false, shouldHaveCoords: false, }, { name: "no coordinates property", coordinates: "", expectedSet: false, shouldHaveCoords: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ep := &endpoint.Endpoint{} if tt.coordinates != "" { ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates, tt.coordinates) } gp := newGeoProximity(ep) result := gp.withCoordinates() assert.Equal(t, tt.expectedSet, result.isSet) if tt.shouldHaveCoords { assert.NotNil(t, result.location.Coordinates) assert.Equal(t, tt.expectedLat, *result.location.Coordinates.Latitude) assert.Equal(t, tt.expectedLong, *result.location.Coordinates.Longitude) } else { assert.Nil(t, result.location.Coordinates) } }) } } func TestGeoProximityWithBias(t *testing.T) { tests := []struct { name string bias string hasBias bool expectedSet bool expectedBias int32 }{ { name: "valid positive bias", bias: "10", hasBias: true, expectedSet: true, expectedBias: 10, }, { name: "valid negative bias", bias: "-5", hasBias: true, expectedSet: true, expectedBias: -5, }, { name: "zero bias", bias: "0", hasBias: true, expectedSet: true, expectedBias: 0, }, { name: "large positive bias", bias: "99", hasBias: true, expectedSet: true, expectedBias: 99, }, { name: "large negative bias", bias: "-99", hasBias: true, expectedSet: true, expectedBias: -99, }, { name: "invalid bias - non-numeric", bias: "abc", hasBias: true, expectedSet: true, expectedBias: 0, // defaults to 0 on error }, { name: "invalid bias - float", bias: "10.5", hasBias: true, expectedSet: true, expectedBias: 0, // defaults to 0 on error }, { name: "empty bias string", bias: "", hasBias: true, expectedSet: true, expectedBias: 0, // defaults to 0 on error }, { name: "no bias property", bias: "", hasBias: false, expectedSet: false, expectedBias: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ep := &endpoint.Endpoint{ DNSName: "test.example.com", SetIdentifier: "test-set", } if tt.hasBias { ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationBias, tt.bias) } gp := newGeoProximity(ep) result := gp.withBias() assert.Equal(t, tt.expectedSet, result.isSet) if tt.expectedSet { assert.NotNil(t, result.location.Bias) assert.Equal(t, tt.expectedBias, *result.location.Bias) } else { assert.Nil(t, result.location.Bias) } // Verify method returns same instance for chaining assert.Equal(t, gp, result) }) } } func TestAWSProvider_createUpdateChanges_NewMoreThanOld(t *testing.T) { provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"foo.bar."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), true, false, nil) oldEndpoints := []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("record1.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "1.1.1.1"), nil, } newEndpoints := []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("record1.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "1.1.1.1"), endpoint.NewEndpointWithTTL("record2.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "2.2.2.2"), endpoint.NewEndpointWithTTL("record3.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "3.3.3.3"), } changes := provider.createUpdateChanges(newEndpoints, oldEndpoints) // record2 should be created, record1 should be upserted var creates, upserts, deletes int for _, c := range changes { switch c.Action { case route53types.ChangeActionCreate: creates++ case route53types.ChangeActionUpsert: upserts++ case route53types.ChangeActionDelete: deletes++ } } require.Equal(t, 0, creates, "should create the extra new endpoint") require.Equal(t, 1, upserts, "should upsert the matching endpoint") require.Equal(t, 0, deletes, "should not delete anything") }