diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index cd4c180bc..ca77f36e2 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -151,8 +151,21 @@ func NewEndpointWithTTL(dnsName, recordType string, ttl TTL, targets ...string) } } +// WithProviderSpecific attaches a key/value pair to the Endpoint and returns the Endpoint. +// This can be used to pass additional data through the stages of ExternalDNS's Endpoint processing. +// The assumption is that most of the time this will be provider specific metadata that doesn't +// warrant its own field on the Endpoint object itself. It differs from Labels in the fact that it's +// not persisted in the Registry but only kept in memory during a single record synchronization. +func (e *Endpoint) WithProviderSpecific(key, value string) *Endpoint { + if e.ProviderSpecific == nil { + e.ProviderSpecific = ProviderSpecific{} + } + e.ProviderSpecific[key] = value + return e +} + func (e *Endpoint) String() string { - return fmt.Sprintf("%s %d IN %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Targets) + return fmt.Sprintf("%s %d IN %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Targets, e.ProviderSpecific) } // DNSEndpointSpec defines the desired state of DNSEndpoint diff --git a/internal/testutils/endpoint.go b/internal/testutils/endpoint.go index 9e74db431..f13a6b4c7 100644 --- a/internal/testutils/endpoint.go +++ b/internal/testutils/endpoint.go @@ -48,7 +48,8 @@ func (b byAllFields) Less(i, j int) bool { func SameEndpoint(a, b *endpoint.Endpoint) bool { return a.DNSName == b.DNSName && a.Targets.Same(b.Targets) && a.RecordType == b.RecordType && a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] && a.RecordTTL == b.RecordTTL && - a.Labels[endpoint.ResourceLabelKey] == b.Labels[endpoint.ResourceLabelKey] + a.Labels[endpoint.ResourceLabelKey] == b.Labels[endpoint.ResourceLabelKey] && + SameMap(a.ProviderSpecific, b.ProviderSpecific) } // SameEndpoints compares two slices of endpoints regardless of order @@ -79,3 +80,18 @@ func SamePlanChanges(a, b map[string][]*endpoint.Endpoint) bool { return SameEndpoints(a["Create"], b["Create"]) && SameEndpoints(a["Delete"], b["Delete"]) && SameEndpoints(a["UpdateOld"], b["UpdateOld"]) && SameEndpoints(a["UpdateNew"], b["UpdateNew"]) } + +// SameMap verifies that two maps contain the same string/string key/value pairs +func SameMap(a, b map[string]string) bool { + if len(a) != len(b) { + return false + } + + for k, v := range a { + if v != b[k] { + return false + } + } + + return true +} diff --git a/internal/testutils/endpoint_test.go b/internal/testutils/endpoint_test.go index bdecd6a06..f14ae655c 100644 --- a/internal/testutils/endpoint_test.go +++ b/internal/testutils/endpoint_test.go @@ -55,16 +55,22 @@ func ExampleSameEndpoints() { RecordType: "CNAME", RecordTTL: endpoint.TTL(60), }, + { + DNSName: "example.org", + Targets: endpoint.Targets{"load-balancer.org"}, + ProviderSpecific: endpoint.ProviderSpecific{"foo": "bar"}, + }, } sort.Sort(byAllFields(eps)) for _, ep := range eps { fmt.Println(ep) } // Output: - // abc.com 0 IN A 1.2.3.4 - // abc.com 0 IN TXT something - // bbc.com 0 IN CNAME foo.com - // cbc.com 60 IN CNAME foo.com - // example.org 0 IN load-balancer.org - // example.org 0 IN TXT load-balancer.org + // abc.com 0 IN A 1.2.3.4 map[] + // abc.com 0 IN TXT something map[] + // bbc.com 0 IN CNAME foo.com map[] + // cbc.com 60 IN CNAME foo.com map[] + // example.org 0 IN load-balancer.org map[] + // example.org 0 IN load-balancer.org map[foo:bar] + // example.org 0 IN TXT load-balancer.org map[] } diff --git a/provider/aws.go b/provider/aws.go index 951dab0a3..6aa0f0412 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -34,6 +34,9 @@ import ( const ( recordTTL = 300 + // provider specific key that designates whether an AWS ALIAS record has the EvaluateTargetHealth + // field set to true. + providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health" ) var ( @@ -227,7 +230,10 @@ func (p *AWSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) { } if r.AliasTarget != nil { - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), endpoint.RecordTypeCNAME, ttl, aws.StringValue(r.AliasTarget.DNSName))) + ep := endpoint. + NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), endpoint.RecordTypeCNAME, ttl, aws.StringValue(r.AliasTarget.DNSName)). + WithProviderSpecific(providerSpecificEvaluateTargetHealth, fmt.Sprintf("%t", aws.BoolValue(r.AliasTarget.EvaluateTargetHealth))) + endpoints = append(endpoints, ep) } } @@ -359,11 +365,16 @@ func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *rou } if isAWSLoadBalancer(endpoint) { + evalTargetHealth := p.evaluateTargetHealth + if _, ok := endpoint.ProviderSpecific[providerSpecificEvaluateTargetHealth]; ok { + evalTargetHealth = endpoint.ProviderSpecific[providerSpecificEvaluateTargetHealth] == "true" + } + change.ResourceRecordSet.Type = aws.String(route53.RRTypeA) change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{ DNSName: aws.String(endpoint.Targets[0]), HostedZoneId: aws.String(canonicalHostedZone(endpoint.Targets[0])), - EvaluateTargetHealth: aws.Bool(p.evaluateTargetHealth), + EvaluateTargetHealth: aws.Bool(evalTargetHealth), } } else { change.ResourceRecordSet.Type = aws.String(endpoint.RecordType) diff --git a/provider/aws_test.go b/provider/aws_test.go index aac6a3e6a..60c451e99 100644 --- a/provider/aws_test.go +++ b/provider/aws_test.go @@ -22,6 +22,7 @@ import ( "sort" "strings" "testing" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/route53" @@ -31,7 +32,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) const ( @@ -249,12 +249,13 @@ func TestAWSZones(t *testing.T) { } func TestAWSRecords(t *testing.T) { - provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{ + provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, false, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), - endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), + endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), + endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), + endpoint.NewEndpoint("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), }) @@ -265,8 +266,9 @@ func TestAWSRecords(t *testing.T) { endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), - endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), + endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), + endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), + endpoint.NewEndpoint("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), }) } @@ -336,12 +338,12 @@ func TestAWSDeleteRecords(t *testing.T) { endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"), - endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), - endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), + endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), + endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpointWithTTL("delete-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), } - provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, originalEndpoints) + provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, false, originalEndpoints) require.NoError(t, provider.DeleteRecords(originalEndpoints)) @@ -764,14 +766,22 @@ func TestAWSCreateRecordsWithCNAME(t *testing.T) { } func TestAWSCreateRecordsWithALIAS(t *testing.T) { - for _, evaluateTargetHealth := range []bool{ - true, - false, + for key, evaluateTargetHealth := range map[string]bool{ + "true": true, + "false": false, + "": false, } { - provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), evaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ - {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME}, + { + DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", + Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, + RecordType: endpoint.RecordTypeCNAME, + ProviderSpecific: endpoint.ProviderSpecific{ + providerSpecificEvaluateTargetHealth: key, + }, + }, } require.NoError(t, provider.CreateRecords(records))