/* 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 provider import ( "fmt" "net" "reflect" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/route53" "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/internal/testutils" "github.com/kubernetes-incubator/external-dns/plan" ) // 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]*route53.HostedZone recordSets map[string]map[string][]*route53.ResourceRecordSet } // NewRoute53APIStub returns an initialized Route53APIStub func NewRoute53APIStub() *Route53APIStub { return &Route53APIStub{ zones: make(map[string]*route53.HostedZone), recordSets: make(map[string]map[string][]*route53.ResourceRecordSet), } } func (r *Route53APIStub) ListResourceRecordSetsPages(input *route53.ListResourceRecordSetsInput, fn func(p *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool)) error { output := route53.ListResourceRecordSetsOutput{} // TODO: Support optional input args. if len(r.recordSets) <= 0 { output.ResourceRecordSets = []*route53.ResourceRecordSet{} } else if _, ok := r.recordSets[aws.StringValue(input.HostedZoneId)]; !ok { output.ResourceRecordSets = []*route53.ResourceRecordSet{} } else { for _, rrsets := range r.recordSets[aws.StringValue(input.HostedZoneId)] { for _, rrset := range rrsets { output.ResourceRecordSets = append(output.ResourceRecordSets, rrset) } } } lastPage := true fn(&output, lastPage) return nil } func (r *Route53APIStub) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) { _, ok := r.zones[aws.StringValue(input.HostedZoneId)] if !ok { return nil, fmt.Errorf("Hosted zone doesn't exist: %s", aws.StringValue(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[aws.StringValue(input.HostedZoneId)] if !ok { recordSets = make(map[string][]*route53.ResourceRecordSet) } for _, change := range input.ChangeBatch.Changes { if aws.StringValue(change.ResourceRecordSet.Type) == route53.RRTypeA { for _, rrs := range change.ResourceRecordSet.ResourceRecords { if net.ParseIP(aws.StringValue(rrs.Value)) == nil { return nil, fmt.Errorf("A records must point to IPs") } } } change.ResourceRecordSet.Name = aws.String(ensureTrailingDot(aws.StringValue(change.ResourceRecordSet.Name))) if change.ResourceRecordSet.AliasTarget != nil { change.ResourceRecordSet.AliasTarget.DNSName = aws.String(ensureTrailingDot(aws.StringValue(change.ResourceRecordSet.AliasTarget.DNSName))) } key := aws.StringValue(change.ResourceRecordSet.Name) + "::" + aws.StringValue(change.ResourceRecordSet.Type) switch aws.StringValue(change.Action) { case route53.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 route53.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 route53.ChangeActionUpsert: recordSets[key] = []*route53.ResourceRecordSet{change.ResourceRecordSet} } } r.recordSets[aws.StringValue(input.HostedZoneId)] = recordSets return output, nil // TODO: We should ideally return status etc, but we don't' use that yet. } func (r *Route53APIStub) ListHostedZonesPages(input *route53.ListHostedZonesInput, fn func(p *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool)) error { output := &route53.ListHostedZonesOutput{} for _, zone := range r.zones { output.HostedZones = append(output.HostedZones, zone) } lastPage := true fn(output, lastPage) return nil } func (r *Route53APIStub) CreateHostedZone(input *route53.CreateHostedZoneInput) (*route53.CreateHostedZoneOutput, error) { name := aws.StringValue(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] = &route53.HostedZone{ Id: aws.String(id), Name: aws.String(name), } return &route53.CreateHostedZoneOutput{HostedZone: r.zones[id]}, nil } func TestAWSZones(t *testing.T) { provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{}) zones, err := provider.Zones() if err != nil { t.Fatal(err) } validateAWSZones(t, zones, map[string]*route53.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."), }, "/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."), }, }) } func TestAWSRecords(t *testing.T) { provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{ endpoint.NewEndpoint("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", "A"), endpoint.NewEndpoint("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "ALIAS"), }) records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, []*endpoint.Endpoint{ endpoint.NewEndpoint("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", "A"), endpoint.NewEndpoint("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "ALIAS"), }) } func TestAWSCreateRecords(t *testing.T) { provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", ""), endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", ""), endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", ""), } if err := provider.CreateRecords(records); err != nil { t.Fatal(err) } records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", "A"), endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", "CNAME"), }) } func TestAWSUpdateRecords(t *testing.T) { provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", "A"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", "CNAME"), }) currentRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", "A"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", "CNAME"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", "A"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", "A"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", "CNAME"), } if err := provider.UpdateRecords(updatedRecords, currentRecords); err != nil { t.Fatal(err) } records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", "A"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", "A"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", "CNAME"), }) } func TestAWSDeleteRecords(t *testing.T) { originalEndpoints := []*endpoint.Endpoint{ endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", "A"), endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", "CNAME"), endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "ALIAS"), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "CNAME"), } provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, originalEndpoints) if err := provider.DeleteRecords(originalEndpoints); err != nil { t.Fatal(err) } records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, []*endpoint.Endpoint{}) } func TestAWSApplyChanges(t *testing.T) { provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", "A"), endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", "A"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", "CNAME"), endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", "CNAME"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", "ALIAS"), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", "ALIAS"), }) createRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", ""), endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", ""), endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", ""), endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", "ALIAS"), } currentRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", ""), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", ""), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", ""), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", "ALIAS"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", ""), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", ""), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", ""), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", "ALIAS"), } deleteRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", ""), endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", ""), endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", ""), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", "ALIAS"), } changes := &plan.Changes{ Create: createRecords, UpdateNew: updatedRecords, UpdateOld: currentRecords, Delete: deleteRecords, } if err := provider.ApplyChanges("_", changes); err != nil { t.Fatal(err) } records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", "A"), endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", "A"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", "A"), endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", "CNAME"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", "CNAME"), endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", "ALIAS"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", "ALIAS"), }) } func TestAWSApplyChangesDryRun(t *testing.T) { originalEndpoints := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", "A"), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", "A"), endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", "A"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", "CNAME"), endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", "CNAME"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", "ALIAS"), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", "ALIAS"), } provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", true, originalEndpoints) createRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", ""), endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", ""), endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", ""), endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", "ALIAS"), } currentRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", ""), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", ""), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", ""), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", "ALIAS"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", ""), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", ""), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", ""), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", "ALIAS"), } deleteRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", ""), endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", ""), endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", ""), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", "ALIAS"), } changes := &plan.Changes{ Create: createRecords, UpdateNew: updatedRecords, UpdateOld: currentRecords, Delete: deleteRecords, } if err := provider.ApplyChanges("_", changes); err != nil { t.Fatal(err) } records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, originalEndpoints) } func TestAWSChangesByZones(t *testing.T) { changes := []*route53.Change{ { Action: aws.String(route53.ChangeActionCreate), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String("qux.foo.example.org"), TTL: aws.Int64(1), }, }, { Action: aws.String(route53.ChangeActionCreate), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2), }, }, { Action: aws.String(route53.ChangeActionDelete), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String("wambo.foo.example.org"), TTL: aws.Int64(10), }, }, { Action: aws.String(route53.ChangeActionDelete), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20), }, }, } zones := map[string]*route53.HostedZone{ "foo-example-org": { Id: aws.String("foo-example-org"), Name: aws.String("foo.example.org."), }, "bar-example-org": { Id: aws.String("bar-example-org"), Name: aws.String("bar.example.org."), }, "baz-example-org": { Id: aws.String("baz-example-org"), Name: aws.String("baz.example.org."), }, } changesByZone := changesByZone(zones, changes) if len(changesByZone) != 2 { t.Fatalf("expected %d change(s), got %d", 2, len(changesByZone)) } validateAWSChangeRecords(t, changesByZone["foo-example-org"], []*route53.Change{ { Action: aws.String(route53.ChangeActionCreate), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String("qux.foo.example.org"), TTL: aws.Int64(1), }, }, { Action: aws.String(route53.ChangeActionDelete), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String("wambo.foo.example.org"), TTL: aws.Int64(10), }, }, }) validateAWSChangeRecords(t, changesByZone["bar-example-org"], []*route53.Change{ { Action: aws.String(route53.ChangeActionCreate), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String("qux.bar.example.org"), TTL: aws.Int64(2), }, }, { Action: aws.String(route53.ChangeActionDelete), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String("wambo.bar.example.org"), TTL: aws.Int64(20), }, }, }) } func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { if !testutils.SameEndpoints(endpoints, expected) { t.Errorf("expected and actual endpoints don't match") } } func validateAWSZones(t *testing.T, zones map[string]*route53.HostedZone, expected map[string]*route53.HostedZone) { if len(zones) != len(expected) { t.Fatalf("expected %d zone(s), got %d", len(expected), len(zones)) } for i, zone := range zones { validateAWSZone(t, zone, expected[i]) } } func validateAWSZone(t *testing.T, zone *route53.HostedZone, expected *route53.HostedZone) { if aws.StringValue(zone.Id) != aws.StringValue(expected.Id) { t.Errorf("expected %s, got %s", aws.StringValue(expected.Id), aws.StringValue(zone.Id)) } if aws.StringValue(zone.Name) != aws.StringValue(expected.Name) { t.Errorf("expected %s, got %s", aws.StringValue(expected.Name), aws.StringValue(zone.Name)) } } func validateAWSChangeRecords(t *testing.T, records []*route53.Change, expected []*route53.Change) { if len(records) != len(expected) { t.Fatalf("expected %d change(s), got %d", len(expected), len(records)) } for i := range records { validateAWSChangeRecord(t, records[i], expected[i]) } } func validateAWSChangeRecord(t *testing.T, record *route53.Change, expected *route53.Change) { if aws.StringValue(record.Action) != aws.StringValue(expected.Action) { t.Errorf("expected %s, got %s", aws.StringValue(expected.Action), aws.StringValue(record.Action)) } if aws.StringValue(record.ResourceRecordSet.Name) != aws.StringValue(expected.ResourceRecordSet.Name) { t.Errorf("expected %s, got %s", aws.StringValue(expected.ResourceRecordSet.Name), aws.StringValue(record.ResourceRecordSet.Name)) } } func TestAWSCreateRecordsWithCNAME(t *testing.T) { provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Target: "foo.example.org"}, } if err := provider.CreateRecords(records); err != nil { t.Fatal(err) } recordSets := listAWSRecords(t, provider.Client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") validateRecords(t, recordSets, []*route53.ResourceRecordSet{ { Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: aws.String("CNAME"), TTL: aws.Int64(300), ResourceRecords: []*route53.ResourceRecord{ { Value: aws.String("foo.example.org"), }, }, }, }) } func TestAWSCreateRecordsWithALIAS(t *testing.T) { provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Target: "foo.eu-central-1.elb.amazonaws.com"}, } if err := provider.CreateRecords(records); err != nil { t.Fatal(err) } recordSets := listAWSRecords(t, provider.Client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") validateRecords(t, recordSets, []*route53.ResourceRecordSet{ { AliasTarget: &route53.AliasTarget{ DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), EvaluateTargetHealth: aws.Bool(true), HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: aws.String("A"), }, }) } func TestAWSisLoadBalancer(t *testing.T) { for _, tc := range []struct { target string recordType string expected bool }{ {"bar.eu-central-1.elb.amazonaws.com", "", true}, {"bar.eu-central-1.elb.amazonaws.com", "ALIAS", true}, {"bar.eu-central-1.elb.amazonaws.com", "CNAME", false}, {"foo.example.org", "", false}, {"foo.example.org", "ALIAS", true}, {"foo.example.org", "CNAME", false}, } { ep := &endpoint.Endpoint{ Target: tc.target, RecordType: tc.recordType, } isLB := isAWSLoadBalancer(ep) if isLB != tc.expected { t.Errorf("expected %t, got %t", tc.expected, isLB) } } } func TestAWSCanonicalHostedZone(t *testing.T) { for _, tc := range []struct { hostname string expected string }{ {"foo.us-east-1.elb.amazonaws.com", "Z35SXDOTRQ7X7K"}, {"foo.us-east-2.elb.amazonaws.com", "Z3AADJGX6KTTL2"}, {"foo.us-west-1.elb.amazonaws.com", "Z368ELLRRE2KJ0"}, {"foo.us-west-2.elb.amazonaws.com", "Z1H1FL5HABSF5"}, {"foo.ca-central-1.elb.amazonaws.com", "ZQSVJUPU6J1EY"}, {"foo.ap-south-1.elb.amazonaws.com", "ZP97RAFLXTNZK"}, {"foo.ap-northeast-2.elb.amazonaws.com", "ZWKZPGTI48KDX"}, {"foo.ap-southeast-1.elb.amazonaws.com", "Z1LMS91P8CMLE5"}, {"foo.ap-southeast-2.elb.amazonaws.com", "Z1GM3OXH4ZPM65"}, {"foo.ap-northeast-1.elb.amazonaws.com", "Z14GRHDCWA56QT"}, {"foo.eu-central-1.elb.amazonaws.com", "Z215JYRZR1TBD5"}, {"foo.eu-west-1.elb.amazonaws.com", "Z32O12XQLNTSW2"}, {"foo.eu-west-2.elb.amazonaws.com", "ZHURV8PSTC4K8"}, {"foo.sa-east-1.elb.amazonaws.com", "Z2P70J7HTTTPLU"}, {"foo.example.org", ""}, } { zone := canonicalHostedZone(tc.hostname) if zone != tc.expected { t.Errorf("expected %v, got %v", tc.expected, zone) } } } func TestAWSSuitableZone(t *testing.T) { zones := map[string]*route53.HostedZone{ "example-org": {Id: aws.String("example-org"), Name: aws.String("example.org.")}, "bar-example-org": {Id: aws.String("bar-example-org"), Name: aws.String("bar.example.org.")}, } for _, tc := range []struct { hostname string expected *route53.HostedZone }{ {"foo.bar.example.org.", zones["bar-example-org"]}, {"foo.example.org.", zones["example-org"]}, {"foo.kubernetes.io.", nil}, } { suitableZone := suitableZone(tc.hostname, zones) if suitableZone != tc.expected { t.Errorf("expected %s, got %s", tc.expected, suitableZone) } } } func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) { params := &route53.CreateHostedZoneInput{ CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"), Name: zone.Name, } if _, err := provider.Client.CreateHostedZone(params); err != nil { if err, ok := err.(awserr.Error); !ok || err.Code() != route53.ErrCodeHostedZoneAlreadyExists { t.Fatal(err) } } } func setupAWSRecords(t *testing.T, provider *AWSProvider, endpoints []*endpoint.Endpoint) { clearAWSRecords(t, provider, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") clearAWSRecords(t, provider, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.") clearAWSRecords(t, provider, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.") records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, []*endpoint.Endpoint{}) if err = provider.CreateRecords(endpoints); err != nil { t.Fatal(err) } records, err = provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, endpoints) } func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.ResourceRecordSet { recordSets := []*route53.ResourceRecordSet{} if err := client.ListResourceRecordSetsPages(&route53.ListResourceRecordSetsInput{ HostedZoneId: aws.String(zone), }, func(resp *route53.ListResourceRecordSetsOutput, _ bool) bool { for _, recordSet := range resp.ResourceRecordSets { switch aws.StringValue(recordSet.Type) { case "A", "CNAME": recordSets = append(recordSets, recordSet) } } return true }); err != nil { t.Fatal(err) } return recordSets } func clearAWSRecords(t *testing.T, provider *AWSProvider, zone string) { recordSets := listAWSRecords(t, provider.Client, zone) changes := make([]*route53.Change, 0, len(recordSets)) for _, recordSet := range recordSets { changes = append(changes, &route53.Change{ Action: aws.String(route53.ChangeActionDelete), ResourceRecordSet: recordSet, }) } if len(changes) != 0 { if _, err := provider.Client.ChangeResourceRecordSets(&route53.ChangeResourceRecordSetsInput{ HostedZoneId: aws.String(zone), ChangeBatch: &route53.ChangeBatch{ Changes: changes, }, }); err != nil { t.Fatal(err) } } } func newAWSProvider(t *testing.T, domain string, dryRun bool, records []*endpoint.Endpoint) *AWSProvider { client := NewRoute53APIStub() provider := &AWSProvider{ Client: client, Domain: domain, DryRun: false, } createAWSZone(t, provider, &route53.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."), }) createAWSZone(t, provider, &route53.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."), }) createAWSZone(t, provider, &route53.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."), }) setupAWSRecords(t, provider, records) provider.DryRun = dryRun return provider } func validateRecords(t *testing.T, records []*route53.ResourceRecordSet, expected []*route53.ResourceRecordSet) { if len(records) != len(expected) { t.Errorf("expected %d records, got %d", len(expected), len(records)) } for i := range records { if !reflect.DeepEqual(records[i], expected[i]) { t.Errorf("record is wrong") } } }