From 03d76204f9ba4fbe9026194d621fd8be2e60d18a Mon Sep 17 00:00:00 2001 From: Martin Linkhorst Date: Thu, 13 Apr 2017 17:57:18 +0200 Subject: [PATCH] support multiple hosted zones and automatic lookup (#152) * feat(aws): support multiple hosted zones and automatic lookup * chore: run gofmt with the simplified command * fix(aws): add missing method from google provider * fix: remove superflous parameter from google provider * feat: make domain configurable via flag * fix(aws): remove unused constant * fix(aws): don't log actions that were filtered out * feat(aws): detect best possible zone to put dns entries in * fix(aws): log error instead of failing if a change batch fails * chore: update changelog with support for multiple zones --- CHANGELOG.md | 1 + main.go | 2 +- pkg/apis/externaldns/types.go | 2 + pkg/apis/externaldns/types_test.go | 6 + provider/aws.go | 151 ++++++-- provider/aws_test.go | 588 +++++++++++++++-------------- provider/provider.go | 6 +- provider/provider_test.go | 45 +++ 8 files changed, 489 insertions(+), 312 deletions(-) create mode 100644 provider/provider_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a34c29b79d..4ad6d96b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ Features: + - Route 53: Support creation of records in multiple hosted zones. - Route 53: Support creation of ALIAS records when endpoint target is a ELB/ALB. - Ownership via TXT records 1. Create TXT records to mark the records managed by External DNS diff --git a/main.go b/main.go index 659bb695f6..bcb79d2887 100644 --- a/main.go +++ b/main.go @@ -96,7 +96,7 @@ func main() { case "google": p, err = provider.NewGoogleProvider(cfg.GoogleProject, cfg.DryRun) case "aws": - p, err = provider.NewAWSProvider(cfg.DryRun) + p, err = provider.NewAWSProvider(cfg.Domain, cfg.DryRun) default: log.Fatalf("unknown dns provider: %s", cfg.Provider) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 091070d828..a5fad15958 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -35,6 +35,7 @@ type Config struct { KubeConfig string Namespace string Zone string + Domain string Sources []string Provider string GoogleProject string @@ -64,6 +65,7 @@ func (cfg *Config) ParseFlags(args []string) error { flags.StringVar(&cfg.KubeConfig, "kubeconfig", "", "path to a local kubeconfig file") flags.StringVar(&cfg.Namespace, "namespace", v1.NamespaceAll, "the namespace to look for endpoints; all namespaces by default") flags.StringVar(&cfg.Zone, "zone", "", "the ID of the hosted zone to target") + flags.StringVar(&cfg.Domain, "domain", "example.org.", "the name of the top-level domain to manage") flags.StringArrayVar(&cfg.Sources, "source", nil, "the sources to gather endpoints: [service, ingress], e.g. --source service --source ingress") flags.StringVar(&cfg.Provider, "provider", "", "the DNS provider to materialize the records in: ") flags.StringVar(&cfg.GoogleProject, "google-project", "", "gcloud project to target") diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index e12bdb3cda..76f0708eb8 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -37,6 +37,7 @@ func TestParseFlags(t *testing.T) { KubeConfig: "", Namespace: "", Zone: "", + Domain: "example.org.", Sources: nil, Provider: "", GoogleProject: "", @@ -62,6 +63,7 @@ func TestParseFlags(t *testing.T) { KubeConfig: "", Namespace: "", Zone: "", + Domain: "example.org.", Sources: nil, Provider: "", GoogleProject: "", @@ -87,6 +89,7 @@ func TestParseFlags(t *testing.T) { KubeConfig: "myhome", Namespace: "", Zone: "", + Domain: "example.org.", Sources: nil, Provider: "", GoogleProject: "", @@ -117,6 +120,7 @@ func TestParseFlags(t *testing.T) { KubeConfig: "", Namespace: "", Zone: "", + Domain: "example.org.", Sources: nil, Provider: "", GoogleProject: "", @@ -141,6 +145,7 @@ func TestParseFlags(t *testing.T) { "--kubeconfig", "/some/path", "--namespace", "namespace", "--zone", "zone", + "--domain", "kubernetes.io.", "--source", "source", "--provider", "provider", "--google-project", "project", @@ -160,6 +165,7 @@ func TestParseFlags(t *testing.T) { KubeConfig: "/some/path", Namespace: "namespace", Zone: "zone", + Domain: "kubernetes.io.", Sources: []string{"source"}, Provider: "provider", GoogleProject: "project", diff --git a/provider/aws.go b/provider/aws.go index 359f724493..c907aad9af 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "net" "strings" log "github.com/Sirupsen/logrus" @@ -30,7 +31,6 @@ import ( ) const ( - hostedZonePrefix = "/hostedzone/" elbHostnameSuffix = ".elb.amazonaws.com" evaluateTargetHealth = true recordTTL = 300 @@ -62,16 +62,19 @@ type Route53API interface { ListResourceRecordSetsPages(input *route53.ListResourceRecordSetsInput, fn func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool)) error ChangeResourceRecordSets(*route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) CreateHostedZone(*route53.CreateHostedZoneInput) (*route53.CreateHostedZoneOutput, error) + ListHostedZonesPages(input *route53.ListHostedZonesInput, fn func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool)) error } // AWSProvider is an implementation of Provider for AWS Route53. type AWSProvider struct { Client Route53API DryRun bool + // only consider hosted zones managing domains ending in this suffix + Domain string } // NewAWSProvider initializes a new AWS Route53 based Provider. -func NewAWSProvider(dryRun bool) (Provider, error) { +func NewAWSProvider(domain string, dryRun bool) (Provider, error) { config := aws.NewConfig() session, err := session.NewSessionWithOptions(session.Options{ @@ -84,15 +87,41 @@ func NewAWSProvider(dryRun bool) (Provider, error) { provider := &AWSProvider{ Client: route53.New(session), + Domain: domain, DryRun: dryRun, } return provider, nil } +// Zones returns the list of hosted zones. +func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) { + zones := make(map[string]*route53.HostedZone) + + f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) { + for _, zone := range resp.HostedZones { + if strings.HasSuffix(aws.StringValue(zone.Name), p.Domain) { + zones[aws.StringValue(zone.Id)] = zone + } + } + + return true + } + + err := p.Client.ListHostedZonesPages(&route53.ListHostedZonesInput{}, f) + if err != nil { + return nil, err + } + + return zones, nil +} + // Records returns the list of records in a given hosted zone. -func (p *AWSProvider) Records(zone string) ([]*endpoint.Endpoint, error) { - endpoints := []*endpoint.Endpoint{} +func (p *AWSProvider) Records(_ string) (endpoints []*endpoint.Endpoint, _ error) { + zones, err := p.Zones() + if err != nil { + return nil, err + } f := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) { for _, r := range resp.ResourceRecordSets { @@ -117,70 +146,106 @@ func (p *AWSProvider) Records(zone string) ([]*endpoint.Endpoint, error) { return true } - params := &route53.ListResourceRecordSetsInput{ - HostedZoneId: aws.String(expandedHostedZoneID(zone)), - } + for _, z := range zones { + params := &route53.ListResourceRecordSetsInput{ + HostedZoneId: z.Id, + } - if err := p.Client.ListResourceRecordSetsPages(params, f); err != nil { - return nil, err + if err := p.Client.ListResourceRecordSetsPages(params, f); err != nil { + return nil, err + } } return endpoints, nil } // CreateRecords creates a given set of DNS records in the given hosted zone. -func (p *AWSProvider) CreateRecords(zone string, endpoints []*endpoint.Endpoint) error { - return p.submitChanges(zone, newChanges(route53.ChangeActionCreate, endpoints)) +func (p *AWSProvider) CreateRecords(endpoints []*endpoint.Endpoint) error { + return p.submitChanges(newChanges(route53.ChangeActionCreate, endpoints)) } // UpdateRecords updates a given set of old records to a new set of records in a given hosted zone. -func (p *AWSProvider) UpdateRecords(zone string, endpoints, _ []*endpoint.Endpoint) error { - return p.submitChanges(zone, newChanges(route53.ChangeActionUpsert, endpoints)) +func (p *AWSProvider) UpdateRecords(endpoints, _ []*endpoint.Endpoint) error { + return p.submitChanges(newChanges(route53.ChangeActionUpsert, endpoints)) } // DeleteRecords deletes a given set of DNS records in a given zone. -func (p *AWSProvider) DeleteRecords(zone string, endpoints []*endpoint.Endpoint) error { - return p.submitChanges(zone, newChanges(route53.ChangeActionDelete, endpoints)) +func (p *AWSProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error { + return p.submitChanges(newChanges(route53.ChangeActionDelete, endpoints)) } // ApplyChanges applies a given set of changes in a given zone. -func (p *AWSProvider) ApplyChanges(zone string, changes *plan.Changes) error { +func (p *AWSProvider) ApplyChanges(_ string, changes *plan.Changes) error { combinedChanges := make([]*route53.Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) combinedChanges = append(combinedChanges, newChanges(route53.ChangeActionCreate, changes.Create)...) combinedChanges = append(combinedChanges, newChanges(route53.ChangeActionUpsert, changes.UpdateNew)...) combinedChanges = append(combinedChanges, newChanges(route53.ChangeActionDelete, changes.Delete)...) - return p.submitChanges(zone, combinedChanges) + return p.submitChanges(combinedChanges) } // submitChanges takes a zone and a collection of Changes and sends them as a single transaction. -func (p *AWSProvider) submitChanges(zone string, changes []*route53.Change) error { +func (p *AWSProvider) submitChanges(changes []*route53.Change) error { // return early if there is nothing to change if len(changes) == 0 { return nil } - if p.DryRun { - for _, change := range changes { - log.Infof("Changing records: %s %s", aws.StringValue(change.Action), change.String()) + zones, err := p.Zones() + if err != nil { + return err + } + + // separate into per-zone change sets to be passed to the API. + changesByZone := changesByZone(zones, changes) + + for z, cs := range changesByZone { + if p.DryRun { + for _, c := range cs { + log.Infof("Changing records: %s %s", aws.StringValue(c.Action), c.String()) + } + } else { + params := &route53.ChangeResourceRecordSetsInput{ + HostedZoneId: aws.String(z), + ChangeBatch: &route53.ChangeBatch{ + Changes: cs, + }, + } + + if _, err := p.Client.ChangeResourceRecordSets(params); err != nil { + log.Error(err) + } } + } - return nil + return nil +} + +// changesByZone separates a multi-zone change into a single change per zone. +func changesByZone(zones map[string]*route53.HostedZone, changeSet []*route53.Change) map[string][]*route53.Change { + changes := make(map[string][]*route53.Change) + + for _, z := range zones { + changes[aws.StringValue(z.Id)] = []*route53.Change{} } - params := &route53.ChangeResourceRecordSetsInput{ - HostedZoneId: aws.String(expandedHostedZoneID(zone)), - ChangeBatch: &route53.ChangeBatch{ - Changes: changes, - }, + for _, c := range changeSet { + hostname := ensureTrailingDot(aws.StringValue(c.ResourceRecordSet.Name)) + + if zone := suitableZone(hostname, zones); zone != nil { + changes[aws.StringValue(zone.Id)] = append(changes[aws.StringValue(zone.Id)], c) + } } - if _, err := p.Client.ChangeResourceRecordSets(params); err != nil { - return err + // separating a change could lead to empty sub changes, remove them here. + for zone, change := range changes { + if len(change) == 0 { + delete(changes, zone) + } } - return nil + return changes } // newChanges returns a collection of Changes based on the given records and action. @@ -225,6 +290,22 @@ func newChange(action string, endpoint *endpoint.Endpoint) *route53.Change { return change } +// suitableZone returns the most suitable zone for a given hostname and a set of zones. +func suitableZone(hostname string, zones map[string]*route53.HostedZone) *route53.HostedZone { + var zone *route53.HostedZone + + for _, z := range zones { + if strings.HasSuffix(hostname, aws.StringValue(z.Name)) { + if zone == nil || len(aws.StringValue(z.Name)) > len(aws.StringValue(zone.Name)) { + zone = z + } + } + } + + return zone +} + +// isAWSLoadBalancer determines if a given hostname belongs to an AWS load balancer. func isAWSLoadBalancer(ep *endpoint.Endpoint) bool { if ep.RecordType == "" { return canonicalHostedZone(ep.Target) != "" @@ -233,6 +314,7 @@ func isAWSLoadBalancer(ep *endpoint.Endpoint) bool { return ep.RecordType == "ALIAS" } +// canonicalHostedZone returns the matching canonical zone for a given hostname. func canonicalHostedZone(hostname string) string { for suffix, zone := range canonicalHostedZones { if strings.HasSuffix(hostname, suffix) { @@ -243,6 +325,11 @@ func canonicalHostedZone(hostname string) string { return "" } -func expandedHostedZoneID(zone string) string { - return hostedZonePrefix + strings.TrimPrefix(zone, hostedZonePrefix) +// ensureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already. +func ensureTrailingDot(hostname string) string { + if net.ParseIP(hostname) != nil { + return hostname + } + + return strings.TrimSuffix(hostname, ".") + "." } diff --git a/provider/aws_test.go b/provider/aws_test.go index d8fad62953..b43b8d6ce3 100644 --- a/provider/aws_test.go +++ b/provider/aws_test.go @@ -20,7 +20,6 @@ import ( "fmt" "net" "reflect" - "strings" "testing" "github.com/aws/aws-sdk-go/aws" @@ -32,11 +31,6 @@ import ( "github.com/kubernetes-incubator/external-dns/plan" ) -const ( - // ID of the hosted zone where the tests are running. - testZone = "ext-dns-test.teapot.zalan.do." -) - // Compile time check for interface conformance var _ Route53API = &Route53APIStub{} @@ -126,6 +120,16 @@ func (r *Route53APIStub) ChangeResourceRecordSets(input *route53.ChangeResourceR 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 @@ -139,84 +143,124 @@ func (r *Route53APIStub) CreateHostedZone(input *route53.CreateHostedZoneInput) 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, false, []*endpoint.Endpoint{ - endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - endpoint.NewEndpoint("list-test-alias.ext-dns-test.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "ALIAS"), + 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(testZone) + records, err := provider.Records("_") if err != nil { t.Fatal(err) } + validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - endpoint.NewEndpoint("list-test-alias.ext-dns-test.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "ALIAS"), + 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, false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), + 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(testZone, records); err != nil { + if err := provider.CreateRecords(records); err != nil { t.Fatal(err) } - records, err := provider.Records(testZone) + records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), + 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, false, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), + 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.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), + 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.ext-dns-test.teapot.zalan.do", "1.2.3.4", "A"), + 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(testZone, updatedRecords, currentRecords); err != nil { + if err := provider.UpdateRecords(updatedRecords, currentRecords); err != nil { t.Fatal(err) } - records, err := provider.Records(testZone) + records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "1.2.3.4", "A"), + 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.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - endpoint.NewEndpoint("delete-test-cname.ext-dns-test.teapot.zalan.do", "foo.example.org", "CNAME"), - endpoint.NewEndpoint("delete-test-cname.ext-dns-test.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "ALIAS"), - endpoint.NewEndpoint("delete-test-cname-alias.ext-dns-test.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "CNAME"), + 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, false, originalEndpoints) + provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, originalEndpoints) - if err := provider.DeleteRecords(testZone, originalEndpoints); err != nil { + if err := provider.DeleteRecords(originalEndpoints); err != nil { t.Fatal(err) } - records, err := provider.Records(testZone) + records, err := provider.Records("_") if err != nil { t.Fatal(err) } @@ -225,24 +269,42 @@ func TestAWSDeleteRecords(t *testing.T) { } func TestAWSApplyChanges(t *testing.T) { - provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), + 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.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), + 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.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), + 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.ext-dns-test.teapot.zalan.do", "1.2.3.4", ""), + 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.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), + 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{ @@ -252,115 +314,66 @@ func TestAWSApplyChanges(t *testing.T) { Delete: deleteRecords, } - if err := provider.ApplyChanges(testZone, changes); err != nil { - t.Fatal(err) - } - - records, err := provider.Records(testZone) - if err != nil { - t.Fatal(err) - } - - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "1.2.3.4", "A"), - }) -} - -func TestAWSApplyNoChanges(t *testing.T) { - provider := newAWSProvider(t, false, []*endpoint.Endpoint{}) - - if err := provider.ApplyChanges(testZone, &plan.Changes{}); err != nil { - t.Error(err) - } -} - -func TestAWSCreateRecordsDryRun(t *testing.T) { - provider := newAWSProvider(t, true, []*endpoint.Endpoint{}) - - records := []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), - } - - if err := provider.CreateRecords(testZone, records); err != nil { - t.Fatal(err) - } - - records, err := provider.Records(testZone) - if err != nil { - t.Fatal(err) - } - - validateEndpoints(t, records, []*endpoint.Endpoint{}) -} - -func TestAWSUpdateRecordsDryRun(t *testing.T) { - provider := newAWSProvider(t, true, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - }) - - currentRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), - } - updatedRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "1.2.3.4", ""), - } - - if err := provider.UpdateRecords(testZone, updatedRecords, currentRecords); err != nil { + if err := provider.ApplyChanges("_", changes); err != nil { t.Fatal(err) } - records, err := provider.Records(testZone) + records, err := provider.Records("_") if err != nil { t.Fatal(err) } validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), + 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 TestAWSDeleteRecordsDryRun(t *testing.T) { +func TestAWSApplyChangesDryRun(t *testing.T) { originalEndpoints := []*endpoint.Endpoint{ - endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - endpoint.NewEndpoint("delete-test-cname.ext-dns-test.teapot.zalan.do", "foo.example.org", "CNAME"), - endpoint.NewEndpoint("delete-test-cname.ext-dns-test.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "ALIAS"), - endpoint.NewEndpoint("delete-test-cname-alias.ext-dns-test.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", "CNAME"), - } - - provider := newAWSProvider(t, true, originalEndpoints) - - if err := provider.DeleteRecords(testZone, originalEndpoints); err != nil { - t.Fatal(err) - } - - records, err := provider.Records(testZone) - if err != nil { - t.Fatal(err) + 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"), } - validateEndpoints(t, records, originalEndpoints) -} - -func TestAWSApplyChangesDryRun(t *testing.T) { - provider := newAWSProvider(t, true, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - }) + provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", true, originalEndpoints) createRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), + 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.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), + 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.ext-dns-test.teapot.zalan.do", "1.2.3.4", ""), + 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.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""), + 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{ @@ -370,148 +383,160 @@ func TestAWSApplyChangesDryRun(t *testing.T) { Delete: deleteRecords, } - if err := provider.ApplyChanges(testZone, changes); err != nil { + if err := provider.ApplyChanges("_", changes); err != nil { t.Fatal(err) } - records, err := provider.Records(testZone) + records, err := provider.Records("_") if err != nil { t.Fatal(err) } - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - }) + validateEndpoints(t, records, originalEndpoints) } -func TestAWSCreateRecordsCNAME(t *testing.T) { - provider := newAWSProvider(t, false, []*endpoint.Endpoint{}) - - records := []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "foo.example.org", ""), - } - - if err := provider.CreateRecords(testZone, records); err != nil { - t.Fatal(err) +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), + }, + }, } - records, err := provider.Records(testZone) - if err != nil { - t.Fatal(err) + 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."), + }, } - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "foo.example.org", "CNAME"), - }) -} - -func TestAWSUpdateRecordsCNAME(t *testing.T) { - provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "foo.example.org", "CNAME"), - }) + changesByZone := changesByZone(zones, changes) - currentRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "foo.example.org", ""), - } - updatedRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "bar.example.org", ""), + if len(changesByZone) != 2 { + t.Fatalf("expected %d change(s), got %d", 2, len(changesByZone)) } - if err := provider.UpdateRecords(testZone, updatedRecords, currentRecords); err != nil { - t.Fatal(err) - } - - records, err := provider.Records(testZone) - if err != nil { - t.Fatal(err) - } - - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "bar.example.org", "CNAME"), + 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), + }, + }, }) -} -func TestAWSDeleteRecordsCNAME(t *testing.T) { - provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "baz.example.org", "CNAME"), + 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), + }, + }, }) +} - currentRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "baz.example.org", ""), +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") } +} - if err := provider.DeleteRecords(testZone, currentRecords); err != nil { - t.Fatal(err) +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)) } - records, err := provider.Records(testZone) - if err != nil { - t.Fatal(err) + for i, zone := range zones { + validateAWSZone(t, zone, expected[i]) } - - validateEndpoints(t, records, []*endpoint.Endpoint{}) } -func TestAWSApplyChangesCNAME(t *testing.T) { - provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "foo.example.org", "CNAME"), - endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "qux.example.org", "CNAME"), - }) - - createRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "foo.example.org", ""), +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)) } - currentRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "bar.example.org", ""), - } - updatedRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "baz.example.org", ""), + if aws.StringValue(zone.Name) != aws.StringValue(expected.Name) { + t.Errorf("expected %s, got %s", aws.StringValue(expected.Name), aws.StringValue(zone.Name)) } +} - deleteRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "qux.example.org", ""), +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)) } - changes := &plan.Changes{ - Create: createRecords, - UpdateNew: updatedRecords, - UpdateOld: currentRecords, - Delete: deleteRecords, + for i := range records { + validateAWSChangeRecord(t, records[i], expected[i]) } +} - if err := provider.ApplyChanges(testZone, changes); err != nil { - t.Fatal(err) +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)) } - records, err := provider.Records(testZone) - if err != nil { - t.Fatal(err) + 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)) } - - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "foo.example.org", "CNAME"), - endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "baz.example.org", "CNAME"), - }) } func TestAWSCreateRecordsWithCNAME(t *testing.T) { - provider := newAWSProvider(t, false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "foo.example.org"}, + {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Target: "foo.example.org"}, } - if err := provider.CreateRecords(testZone, records); err != nil { + if err := provider.CreateRecords(records); err != nil { t.Fatal(err) } - recordSets := listRecords(t, provider.Client) + 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.ext-dns-test.teapot.zalan.do."), + 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{ @@ -524,17 +549,17 @@ func TestAWSCreateRecordsWithCNAME(t *testing.T) { } func TestAWSCreateRecordsWithALIAS(t *testing.T) { - provider := newAWSProvider(t, false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ - {DNSName: "create-test.ext-dns-test.teapot.zalan.do", Target: "foo.eu-central-1.elb.amazonaws.com"}, + {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Target: "foo.eu-central-1.elb.amazonaws.com"}, } - if err := provider.CreateRecords(testZone, records); err != nil { + if err := provider.CreateRecords(records); err != nil { t.Fatal(err) } - recordSets := listRecords(t, provider.Client) + recordSets := listAWSRecords(t, provider.Client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") validateRecords(t, recordSets, []*route53.ResourceRecordSet{ { @@ -543,7 +568,7 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { EvaluateTargetHealth: aws.Bool(true), HostedZoneId: aws.String("Z215JYRZR1TBD5"), }, - Name: aws.String("create-test.ext-dns-test.teapot.zalan.do."), + Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: aws.String("A"), }, }) @@ -604,72 +629,69 @@ func TestAWSCanonicalHostedZone(t *testing.T) { } } -func TestAWSSanitizeZone(t *testing.T) { - provider := newAWSProvider(t, false, []*endpoint.Endpoint{ - endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - }) - - records, err := provider.Records(testZone) - if err != nil { - t.Fatal(err) +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.")}, } - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - }) + 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) - records, err = provider.Records("/hostedzone/" + testZone) - if err != nil { - t.Fatal(err) + if suitableZone != tc.expected { + t.Errorf("expected %s, got %s", tc.expected, suitableZone) + } } - - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpoint("list-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", "A"), - }) } -func newAWSProvider(t *testing.T, dryRun bool, records []*endpoint.Endpoint) *AWSProvider { - client := NewRoute53APIStub() - - if _, err := client.CreateHostedZone(&route53.CreateHostedZoneInput{ +func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) { + params := &route53.CreateHostedZoneInput{ CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"), - Name: aws.String("ext-dns-test.teapot.zalan.do."), - }); err != nil { + 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) } } +} - provider := &AWSProvider{ - Client: client, - DryRun: false, - } - - setupRecords(t, provider, records) +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.") - provider.DryRun = dryRun - - return provider -} + records, err := provider.Records("_") + if err != nil { + t.Fatal(err) + } -func setupRecords(t *testing.T, provider *AWSProvider, endpoints []*endpoint.Endpoint) { - clearRecords(t, provider) + validateEndpoints(t, records, []*endpoint.Endpoint{}) - if err := provider.CreateRecords(testZone, endpoints); err != nil { + if err = provider.CreateRecords(endpoints); err != nil { t.Fatal(err) } - records, err := provider.Records(testZone) + records, err = provider.Records("_") if err != nil { t.Fatal(err) } + validateEndpoints(t, records, endpoints) } -func listRecords(t *testing.T, client Route53API) []*route53.ResourceRecordSet { +func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.ResourceRecordSet { recordSets := []*route53.ResourceRecordSet{} if err := client.ListResourceRecordSetsPages(&route53.ListResourceRecordSetsInput{ - HostedZoneId: aws.String(expandedHostedZoneID(testZone)), + HostedZoneId: aws.String(zone), }, func(resp *route53.ListResourceRecordSetsOutput, _ bool) bool { for _, recordSet := range resp.ResourceRecordSets { switch aws.StringValue(recordSet.Type) { @@ -684,8 +706,8 @@ func listRecords(t *testing.T, client Route53API) []*route53.ResourceRecordSet { return recordSets } -func clearRecords(t *testing.T, provider *AWSProvider) { - recordSets := listRecords(t, provider.Client) +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 { @@ -697,7 +719,7 @@ func clearRecords(t *testing.T, provider *AWSProvider) { if len(changes) != 0 { if _, err := provider.Client.ChangeResourceRecordSets(&route53.ChangeResourceRecordSetsInput{ - HostedZoneId: aws.String(testZone), + HostedZoneId: aws.String(zone), ChangeBatch: &route53.ChangeBatch{ Changes: changes, }, @@ -705,24 +727,42 @@ func clearRecords(t *testing.T, provider *AWSProvider) { t.Fatal(err) } } +} - records, err := provider.Records(testZone) - if 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, } - validateEndpoints(t, records, []*endpoint.Endpoint{}) -} + 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."), + }) -func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { - if !testutils.SameEndpoints(endpoints, expected) { - t.Errorf("expected %v, got %v", expected, endpoints) - } + 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(records), len(expected)) + t.Errorf("expected %d records, got %d", len(expected), len(records)) } for i := range records { @@ -731,7 +771,3 @@ func validateRecords(t *testing.T, records []*route53.ResourceRecordSet, expecte } } } - -func ensureTrailingDot(hostname string) string { - return strings.TrimSuffix(hostname, ".") + "." -} diff --git a/provider/provider.go b/provider/provider.go index a52f61ac1f..4d6e39a28f 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -35,8 +35,8 @@ func suitableType(ep *endpoint.Endpoint) string { if ep.RecordType != "" { return ep.RecordType } - if net.ParseIP(ep.Target) == nil { - return "CNAME" + if net.ParseIP(ep.Target) != nil { + return "A" } - return "A" + return "CNAME" } diff --git a/provider/provider_test.go b/provider/provider_test.go new file mode 100644 index 0000000000..da654e99db --- /dev/null +++ b/provider/provider_test.go @@ -0,0 +1,45 @@ +/* +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 ( + "testing" + + "github.com/kubernetes-incubator/external-dns/endpoint" +) + +func TestSuitableType(t *testing.T) { + for _, tc := range []struct { + target, recordType, expected string + }{ + {"8.8.8.8", "", "A"}, + {"foo.example.org", "", "CNAME"}, + {"foo.example.org", "ALIAS", "ALIAS"}, + {"bar.eu-central-1.elb.amazonaws.com", "CNAME", "CNAME"}, + } { + ep := &endpoint.Endpoint{ + Target: tc.target, + RecordType: tc.recordType, + } + + recordType := suitableType(ep) + + if recordType != tc.expected { + t.Errorf("expected %s, got %s", tc.expected, recordType) + } + } +}