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
This commit is contained in:
Martin Linkhorst 2017-04-13 17:57:18 +02:00 committed by GitHub
parent d7513580f7
commit 03d76204f9
8 changed files with 517 additions and 340 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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: <aws|google>")
flags.StringVar(&cfg.GoogleProject, "google-project", "", "gcloud project to target")

View File

@ -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",

View File

@ -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,72 +146,108 @@ 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())
}
return nil
}
params := &route53.ChangeResourceRecordSetsInput{
HostedZoneId: aws.String(expandedHostedZoneID(zone)),
ChangeBatch: &route53.ChangeBatch{
Changes: changes,
},
}
if _, err := p.Client.ChangeResourceRecordSets(params); err != nil {
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
}
// 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{}
}
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)
}
}
// 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 changes
}
// newChanges returns a collection of Changes based on the given records and action.
func newChanges(action string, endpoints []*endpoint.Endpoint) []*route53.Change {
changes := make([]*route53.Change, 0, len(endpoints))
@ -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, ".") + "."
}

View File

@ -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 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"),
})
func TestAWSZones(t *testing.T) {
provider := newAWSProvider(t, "ext-dns-test-2.teapot.zalan.do.", false, []*endpoint.Endpoint{})
records, err := provider.Records(testZone)
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.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,89 +314,80 @@ func TestAWSApplyChanges(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("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"),
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 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", ""),
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"),
}
if err := provider.CreateRecords(testZone, records); err != nil {
t.Fatal(err)
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"),
}
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", ""),
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"),
}
if err := provider.UpdateRecords(testZone, updatedRecords, currentRecords); err != nil {
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(testZone)
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"),
})
}
func TestAWSDeleteRecordsDryRun(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)
records, err := provider.Records("_")
if err != nil {
t.Fatal(err)
}
@ -342,176 +395,148 @@ func TestAWSDeleteRecordsDryRun(t *testing.T) {
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"),
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),
},
},
})
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""),
}
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", ""),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "8.8.8.8", ""),
}
changes := &plan.Changes{
Create: createRecords,
UpdateNew: updatedRecords,
UpdateOld: currentRecords,
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("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"),
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 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", ""),
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.CreateRecords(testZone, records); 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", "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"),
})
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", ""),
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))
}
if err := provider.UpdateRecords(testZone, updatedRecords, currentRecords); err != nil {
t.Fatal(err)
for i, zone := range zones {
validateAWSZone(t, zone, expected[i])
}
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"),
})
}
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"),
})
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "baz.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))
}
if err := provider.DeleteRecords(testZone, currentRecords); err != nil {
t.Fatal(err)
if aws.StringValue(zone.Name) != aws.StringValue(expected.Name) {
t.Errorf("expected %s, got %s", aws.StringValue(expected.Name), aws.StringValue(zone.Name))
}
records, err := provider.Records(testZone)
if err != nil {
t.Fatal(err)
}
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 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))
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "bar.example.org", ""),
for i := range records {
validateAWSChangeRecord(t, records[i], expected[i])
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.ext-dns-test.teapot.zalan.do", "baz.example.org", ""),
}
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))
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.ext-dns-test.teapot.zalan.do", "qux.example.org", ""),
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))
}
changes := &plan.Changes{
Create: createRecords,
UpdateNew: updatedRecords,
UpdateOld: currentRecords,
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", "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)
provider.DryRun = dryRun
return provider
}
func setupRecords(t *testing.T, provider *AWSProvider, endpoints []*endpoint.Endpoint) {
clearRecords(t, provider)
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.")
if err := provider.CreateRecords(testZone, endpoints); 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{})
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 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)
}
validateEndpoints(t, records, []*endpoint.Endpoint{})
}
func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
if !testutils.SameEndpoints(endpoints, expected) {
t.Errorf("expected %v, got %v", expected, endpoints)
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(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, ".") + "."
}

View File

@ -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"
}

45
provider/provider_test.go Normal file
View File

@ -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)
}
}
}