external-dns/provider/aws_test.go
Martin Linkhorst 03d76204f9 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
2017-04-13 17:57:18 +02:00

774 lines
29 KiB
Go

/*
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")
}
}
}