diff --git a/controller/controller.go b/controller/controller.go index cc4feefb72..2c9f15402f 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -134,6 +134,12 @@ func (c *Controller) RunOnce(ctx context.Context) error { } sourceEndpointsTotal.Set(float64(len(endpoints))) + if prov := c.Registry.Provider(); prov != nil { + if endpointModifyingProvider, ok := (*prov).(provider.EndpointModifyingProvider); ok { + endpointModifyingProvider.ModifyEndpoints(endpoints) + } + } + plan := &plan.Plan{ Policies: []plan.Policy{c.Policy}, Current: records, diff --git a/plan/plan.go b/plan/plan.go index 675eecf5fc..b01620fbb4 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -183,12 +183,6 @@ func shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool { return false } for _, c := range current.ProviderSpecific { - // don't consider target health when detecting changes - // see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954 - if c.Name == "aws/evaluate-target-health" { - continue - } - found := false for _, d := range desired.ProviderSpecific { if d.Name == c.Name { diff --git a/provider/aws/aws.go b/provider/aws/aws.go index 91961c9e1f..09fc47d8cb 100644 --- a/provider/aws/aws.go +++ b/provider/aws/aws.go @@ -41,6 +41,8 @@ const ( recordTTL = 300 // provider specific key that designates whether an AWS ALIAS record has the EvaluateTargetHealth // field set to true. + providerSpecificAlias = "alias" + providerSpecificTargetHostedZone = "aws/target-hosted-zone" providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health" providerSpecificWeight = "aws/weight" providerSpecificRegion = "aws/region" @@ -292,7 +294,8 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos } 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))) + WithProviderSpecific(providerSpecificEvaluateTargetHealth, fmt.Sprintf("%t", aws.BoolValue(r.AliasTarget.EvaluateTargetHealth))). + WithProviderSpecific(providerSpecificAlias, "true") newEndpoints = append(newEndpoints, ep) } @@ -360,6 +363,8 @@ func (p *AWSProvider) DeleteRecords(ctx context.Context, endpoints []*endpoint.E func (p *AWSProvider) doRecords(ctx context.Context, action string, endpoints []*endpoint.Endpoint) error { zones, err := p.Zones(ctx) + p.ModifyEndpoints(endpoints) + if err != nil { return err } @@ -475,6 +480,30 @@ func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint, return changes } +func (p *AWSProvider) ModifyEndpoints(endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + alias := false + if _, ok := ep.GetProviderSpecificProperty(providerSpecificAlias); ok { + alias = true + } else if useAlias(ep, p.preferCNAME) { + alias = true + log.Debugf("Modifying endpoint: %v, setting %s=true", ep, providerSpecificAlias) + ep.ProviderSpecific = append(ep.ProviderSpecific, endpoint.ProviderSpecificProperty{ + Name: providerSpecificAlias, + Value: "true", + }) + } + + if _, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); alias && !ok { + log.Debugf("Modifying endpoint: %v, setting %s=%t", ep, providerSpecificEvaluateTargetHealth, p.evaluateTargetHealth) + ep.ProviderSpecific = append(ep.ProviderSpecific, endpoint.ProviderSpecificProperty{ + Name: providerSpecificEvaluateTargetHealth, + Value: fmt.Sprintf("%t", p.evaluateTargetHealth), + }) + } + } +} + // newChange returns a route53 Change and a boolean indicating if there should also be a change to a AAAA record // returned Change is based on the given record by the given action, e.g. // action=ChangeActionCreate returns a change for creation of the record and @@ -487,8 +516,7 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint, recordsCac }, } dualstack := false - - if useAlias(ep, p.preferCNAME) { + if targetHostedZone := isAWSAlias(ep); targetHostedZone != "" { evalTargetHealth := p.evaluateTargetHealth if prop, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok { evalTargetHealth = prop.Value == "true" @@ -497,22 +525,12 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint, recordsCac if val, ok := ep.Labels[endpoint.DualstackLabelKey]; ok { dualstack = val == "true" } - change.ResourceRecordSet.Type = aws.String(route53.RRTypeA) change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{ DNSName: aws.String(ep.Targets[0]), - HostedZoneId: aws.String(canonicalHostedZone(ep.Targets[0])), + HostedZoneId: aws.String(cleanZoneID(targetHostedZone)), EvaluateTargetHealth: aws.Bool(evalTargetHealth), } - } else if hostedZone := isAWSAlias(ep, recordsCache); hostedZone != "" { - for _, zone := range zones { - change.ResourceRecordSet.Type = aws.String(route53.RRTypeA) - change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{ - DNSName: aws.String(ep.Targets[0]), - HostedZoneId: aws.String(cleanZoneID(*zone.Id)), - EvaluateTargetHealth: aws.Bool(p.evaluateTargetHealth), - } - } } else { change.ResourceRecordSet.Type = aws.String(ep.RecordType) if !ep.RecordTTL.IsConfigured() { @@ -671,6 +689,18 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet []*route53.Ch continue } for _, z := range zones { + if c.ResourceRecordSet.AliasTarget != nil && aws.StringValue(c.ResourceRecordSet.AliasTarget.HostedZoneId) == "same-zone" { + // alias record is to be created; target needs to be in the same zone as endpoint + // if it's not, this will fail + rrset := *c.ResourceRecordSet + aliasTarget := *rrset.AliasTarget + aliasTarget.HostedZoneId = aws.String(cleanZoneID(aws.StringValue(z.Id))) + rrset.AliasTarget = &aliasTarget + c = &route53.Change{ + Action: c.Action, + ResourceRecordSet: &rrset, + } + } changes[aws.StringValue(z.Id)] = append(changes[aws.StringValue(z.Id)], c) log.Debugf("Adding %s to zone %s [Id: %s]", hostname, aws.StringValue(z.Name), aws.StringValue(z.Id)) } @@ -726,17 +756,24 @@ func useAlias(ep *endpoint.Endpoint, preferCNAME bool) bool { return false } -// isAWSAlias determines if a given hostname belongs to an AWS Alias record by doing an reverse lookup. -func isAWSAlias(ep *endpoint.Endpoint, addrs []*endpoint.Endpoint) string { - if prop, exists := ep.GetProviderSpecificProperty("alias"); ep.RecordType == endpoint.RecordTypeCNAME && exists && prop.Value == "true" { - for _, addr := range addrs { - if len(ep.Targets) > 0 && addr.DNSName == ep.Targets[0] { - if hostedZone := canonicalHostedZone(addr.Targets[0]); hostedZone != "" { - return hostedZone - } +// isAWSAlias determines if a given hostname is an AWS Alias record +func isAWSAlias(ep *endpoint.Endpoint) string { + prop, exists := ep.GetProviderSpecificProperty(providerSpecificAlias) + if exists && prop.Value == "true" && ep.RecordType == endpoint.RecordTypeCNAME && len(ep.Targets) > 0 { + // alias records can only point to canonical hosted zones (e.g. to ELBs) or other records in the same zone - } + if hostedZoneID, ok := ep.GetProviderSpecificProperty(providerSpecificTargetHostedZone); ok { + // existing Endpoint where we got the target hosted zone from the Route53 data + return hostedZoneID.Value + } + + // check if the target is in a canonical hosted zone + if canonicalHostedZone := canonicalHostedZone(ep.Targets[0]); canonicalHostedZone != "" { + return canonicalHostedZone } + + // if not, target needs to be in the same zone + return "same-zone" } return "" } diff --git a/provider/aws/aws_test.go b/provider/aws/aws_test.go index 69da266f35..879c97f657 100644 --- a/provider/aws/aws_test.go +++ b/provider/aws/aws_test.go @@ -335,9 +335,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.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), - endpoint.NewEndpointWithTTL("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), - endpoint.NewEndpointWithTTL("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), + endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), + endpoint.NewEndpointWithTTL("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), + endpoint.NewEndpointWithTTL("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "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"), endpoint.NewEndpointWithTTL("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "random"), endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10"), @@ -357,8 +357,10 @@ func TestAWSCreateRecords(t *testing.T) { records := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), - endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"), - endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("create-test-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"), + endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"), + endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), + endpoint.NewEndpoint("create-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), } @@ -370,8 +372,10 @@ func TestAWSCreateRecords(t *testing.T) { validateEndpoints(t, records, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"), - endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("create-test-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"), + endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.example.com"), + endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"), + endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), }) } @@ -417,6 +421,7 @@ func TestAWSDeleteRecords(t *testing.T) { 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").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.NewEndpoint("delete-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "delete-test.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificTargetHostedZone, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), 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"), } @@ -971,33 +976,23 @@ func TestAWSisAWSAlias(t *testing.T) { for _, tc := range []struct { target string recordType string - alias string - expected string + alias bool + hz string }{ - {"bar.example.org", endpoint.RecordTypeCNAME, "true", "Z215JYRZR1TBD5"}, - {"foo.example.org", endpoint.RecordTypeCNAME, "true", ""}, + {"foo.example.org", endpoint.RecordTypeCNAME, false, ""}, // normal CNAME + {"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME, true, "Z215JYRZR1TBD5"}, // pointing to ELB DNS name + {"foobar.example.org", endpoint.RecordTypeCNAME, true, "Z1234567890ABC"}, // HZID retrieved by Route53 + {"baz.example.org", endpoint.RecordTypeCNAME, true, "same-zone"}, // record to be created } { ep := &endpoint.Endpoint{ Targets: endpoint.Targets{tc.target}, RecordType: tc.recordType, - ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "alias", - Value: tc.alias, - }, - }, } - addrs := []*endpoint.Endpoint{ - { - DNSName: "foo.example.org", - Targets: endpoint.Targets{"foobar.example.org"}, - }, - { - DNSName: "bar.example.org", - Targets: endpoint.Targets{"bar.eu-central-1.elb.amazonaws.com"}, - }, + if tc.alias { + ep = ep.WithProviderSpecific(providerSpecificAlias, "true") + ep = ep.WithProviderSpecific(providerSpecificTargetHostedZone, tc.hz) } - assert.Equal(t, tc.expected, isAWSAlias(ep, addrs)) + assert.Equal(t, tc.hz, isAWSAlias(ep), "%v", tc) } } diff --git a/provider/provider.go b/provider/provider.go index c16bab748b..2e622f8ba0 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -31,6 +31,11 @@ type Provider interface { ApplyChanges(ctx context.Context, changes *plan.Changes) error } +type EndpointModifyingProvider interface { + Provider + ModifyEndpoints([]*endpoint.Endpoint) +} + type contextKey struct { name string } diff --git a/registry/aws_sd_registry.go b/registry/aws_sd_registry.go index f9a0f0d65f..9d9532e22e 100644 --- a/registry/aws_sd_registry.go +++ b/registry/aws_sd_registry.go @@ -42,6 +42,10 @@ func NewAWSSDRegistry(provider provider.Provider, ownerID string) (*AWSSDRegistr }, nil } +func (sdr *AWSSDRegistry) Provider() *provider.Provider { + return &sdr.provider +} + // Records calls AWS SD API and expects AWS SD provider to provider Owner/Resource information as a serialized // value in the AWSSDDescriptionLabel value in the Labels map func (sdr *AWSSDRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { diff --git a/registry/noop.go b/registry/noop.go index 4b91fbaf56..c10a432da0 100644 --- a/registry/noop.go +++ b/registry/noop.go @@ -36,6 +36,10 @@ func NewNoopRegistry(provider provider.Provider) (*NoopRegistry, error) { }, nil } +func (im *NoopRegistry) Provider() *provider.Provider { + return &im.provider +} + // Records returns the current records from the dns provider func (im *NoopRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { return im.provider.Records(ctx) diff --git a/registry/registry.go b/registry/registry.go index 746e7fdd39..a29d2c9653 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -20,9 +20,9 @@ import ( "context" log "github.com/sirupsen/logrus" - "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) // Registry is an interface which should enables ownership concept in external-dns @@ -30,6 +30,7 @@ import ( // each entry includes owner information // ApplyChanges(changes *plan.Changes) propagates the changes to the DNS Provider API and correspondingly updates ownership depending on type of registry being used type Registry interface { + Provider() *provider.Provider Records(ctx context.Context) ([]*endpoint.Endpoint, error) ApplyChanges(ctx context.Context, changes *plan.Changes) error } diff --git a/registry/txt.go b/registry/txt.go index 2d99b0660a..697d54ff46 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -58,6 +58,10 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, ownerID string, cache }, nil } +func (im *TXTRegistry) Provider() *provider.Provider { + return &im.provider +} + // Records returns the current records from the registry excluding TXT Records // If TXT records was created previously to indicate ownership its corresponding value // will be added to the endpoints Labels map