Merge pull request #1008 from devkid/feature/aws-routing-policies

[RFC] Add support for all AWS Route53 routing policies; add additional Setldentifier abstraction layer
This commit is contained in:
Nick Jüttner 2019-11-19 11:21:12 +01:00 committed by GitHub
commit 9418e3acd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 430 additions and 137 deletions

View File

@ -327,6 +327,23 @@ spec:
This will set the DNS record's TTL to 60 seconds. This will set the DNS record's TTL to 60 seconds.
## Routing policies
Route53 offers [different routing policies](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html). The routing policy for a record can be controlled with the following annotations:
* `external-dns.alpha.kubernetes.io/set-identifier`: this **needs** to be set to use any of the following routing policies
For any given DNS name, only **one** of the following routing policies can be used:
* Weighted records: `external-dns.alpha.kubernetes.io/aws-weight`
* Latency-based routing: `external-dns.alpha.kubernetes.io/aws-region`
* Failover:`external-dns.alpha.kubernetes.io/aws-failover`
* Geolocation-based routing:
* `external-dns.alpha.kubernetes.io/aws-geolocation-continent-code`
* `external-dns.alpha.kubernetes.io/aws-geolocation-country-code`
* `external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code`
* Multi-value answer:`external-dns.alpha.kubernetes.io/aws-multi-value-answer`
## Clean up ## Clean up
Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly. Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly.

View File

@ -126,6 +126,8 @@ type Endpoint struct {
Targets Targets `json:"targets,omitempty"` Targets Targets `json:"targets,omitempty"`
// RecordType type of record, e.g. CNAME, A, SRV, TXT etc // RecordType type of record, e.g. CNAME, A, SRV, TXT etc
RecordType string `json:"recordType,omitempty"` RecordType string `json:"recordType,omitempty"`
// Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple')
SetIdentifier string `json:"setIdentifier,omitempty"`
// TTL for the record // TTL for the record
RecordTTL TTL `json:"recordTTL,omitempty"` RecordTTL TTL `json:"recordTTL,omitempty"`
// Labels stores labels defined for the Endpoint // Labels stores labels defined for the Endpoint
@ -157,6 +159,11 @@ func NewEndpointWithTTL(dnsName, recordType string, ttl TTL, targets ...string)
} }
} }
func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint {
e.SetIdentifier = setIdentifier
return e
}
// WithProviderSpecific attaches a key/value pair to the Endpoint and returns the Endpoint. // 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. // 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 // The assumption is that most of the time this will be provider specific metadata that doesn't
@ -182,7 +189,7 @@ func (e *Endpoint) GetProviderSpecificProperty(key string) (ProviderSpecificProp
} }
func (e *Endpoint) String() string { func (e *Endpoint) String() string {
return fmt.Sprintf("%s %d IN %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Targets, e.ProviderSpecific) return fmt.Sprintf("%s %d IN %s %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.SetIdentifier, e.Targets, e.ProviderSpecific)
} }
// DNSEndpointSpec defines the desired state of DNSEndpoint // DNSEndpointSpec defines the desired state of DNSEndpoint

View File

@ -47,10 +47,10 @@ func (b byAllFields) Less(i, j int) bool {
// SameEndpoint returns true if two endpoints are same // SameEndpoint returns true if two endpoints are same
// considers example.org. and example.org DNSName/Target as different endpoints // considers example.org. and example.org DNSName/Target as different endpoints
func SameEndpoint(a, b *endpoint.Endpoint) bool { func SameEndpoint(a, b *endpoint.Endpoint) bool {
return a.DNSName == b.DNSName && a.Targets.Same(b.Targets) && a.RecordType == b.RecordType && return a.DNSName == b.DNSName && a.Targets.Same(b.Targets) && a.RecordType == b.RecordType && a.SetIdentifier == b.SetIdentifier &&
a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] && a.RecordTTL == b.RecordTTL && 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] &&
SameProverSpecific(a.ProviderSpecific, b.ProviderSpecific) SameProviderSpecific(a.ProviderSpecific, b.ProviderSpecific)
} }
// SameEndpoints compares two slices of endpoints regardless of order // SameEndpoints compares two slices of endpoints regardless of order
@ -101,7 +101,7 @@ func SamePlanChanges(a, b map[string][]*endpoint.Endpoint) bool {
SameEndpoints(a["UpdateOld"], b["UpdateOld"]) && SameEndpoints(a["UpdateNew"], b["UpdateNew"]) SameEndpoints(a["UpdateOld"], b["UpdateOld"]) && SameEndpoints(a["UpdateNew"], b["UpdateNew"])
} }
// SameProverSpecific verifies that two maps contain the same string/string key/value pairs // SameProviderSpecific verifies that two maps contain the same string/string key/value pairs
func SameProverSpecific(a, b endpoint.ProviderSpecific) bool { func SameProviderSpecific(a, b endpoint.ProviderSpecific) bool {
return reflect.DeepEqual(a, b) return reflect.DeepEqual(a, b)
} }

View File

@ -40,9 +40,10 @@ func ExampleSameEndpoints() {
RecordType: endpoint.RecordTypeTXT, RecordType: endpoint.RecordTypeTXT,
}, },
{ {
DNSName: "abc.com", DNSName: "abc.com",
Targets: endpoint.Targets{"1.2.3.4"}, Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA, RecordType: endpoint.RecordTypeA,
SetIdentifier: "test-set-1",
}, },
{ {
DNSName: "bbc.com", DNSName: "bbc.com",
@ -68,11 +69,11 @@ func ExampleSameEndpoints() {
fmt.Println(ep) fmt.Println(ep)
} }
// Output: // Output:
// abc.com 0 IN A 1.2.3.4 [] // abc.com 0 IN A test-set-1 1.2.3.4 []
// abc.com 0 IN TXT something [] // abc.com 0 IN TXT something []
// bbc.com 0 IN CNAME foo.com [] // bbc.com 0 IN CNAME foo.com []
// cbc.com 60 IN CNAME foo.com [] // cbc.com 60 IN CNAME foo.com []
// example.org 0 IN load-balancer.org [] // example.org 0 IN load-balancer.org []
// example.org 0 IN load-balancer.org [{foo bar}] // example.org 0 IN load-balancer.org [{foo bar}]
// example.org 0 IN TXT load-balancer.org [] // example.org 0 IN TXT load-balancer.org []
} }

View File

@ -63,12 +63,12 @@ bar.com | | [->191.1.1.1, ->190.1.1.1] | = create (bar.com -> 1
"=", i.e. result of calculation relies on supplied ConflictResolver "=", i.e. result of calculation relies on supplied ConflictResolver
*/ */
type planTable struct { type planTable struct {
rows map[string]*planTableRow rows map[string]map[string]*planTableRow
resolver ConflictResolver resolver ConflictResolver
} }
func newPlanTable() planTable { //TODO: make resolver configurable func newPlanTable() planTable { //TODO: make resolver configurable
return planTable{map[string]*planTableRow{}, PerResource{}} return planTable{map[string]map[string]*planTableRow{}, PerResource{}}
} }
// planTableRow // planTableRow
@ -86,52 +86,23 @@ func (t planTableRow) String() string {
func (t planTable) addCurrent(e *endpoint.Endpoint) { func (t planTable) addCurrent(e *endpoint.Endpoint) {
dnsName := normalizeDNSName(e.DNSName) dnsName := normalizeDNSName(e.DNSName)
if _, ok := t.rows[dnsName]; !ok { if _, ok := t.rows[dnsName]; !ok {
t.rows[dnsName] = &planTableRow{} t.rows[dnsName] = make(map[string]*planTableRow)
} }
t.rows[dnsName].current = e if _, ok := t.rows[dnsName][e.SetIdentifier]; !ok {
t.rows[dnsName][e.SetIdentifier] = &planTableRow{}
}
t.rows[dnsName][e.SetIdentifier].current = e
} }
func (t planTable) addCandidate(e *endpoint.Endpoint) { func (t planTable) addCandidate(e *endpoint.Endpoint) {
dnsName := normalizeDNSName(e.DNSName) dnsName := normalizeDNSName(e.DNSName)
if _, ok := t.rows[dnsName]; !ok { if _, ok := t.rows[dnsName]; !ok {
t.rows[dnsName] = &planTableRow{} t.rows[dnsName] = make(map[string]*planTableRow)
} }
t.rows[dnsName].candidates = append(t.rows[dnsName].candidates, e) if _, ok := t.rows[dnsName][e.SetIdentifier]; !ok {
} t.rows[dnsName][e.SetIdentifier] = &planTableRow{}
// TODO: allows record type change, which might not be supported by all dns providers
func (t planTable) getUpdates() (updateNew []*endpoint.Endpoint, updateOld []*endpoint.Endpoint) {
for _, row := range t.rows {
if row.current != nil && len(row.candidates) > 0 { //dns name is taken
update := t.resolver.ResolveUpdate(row.current, row.candidates)
// compare "update" to "current" to figure out if actual update is required
if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || shouldUpdateProviderSpecific(update, row.current) {
inheritOwner(row.current, update)
updateNew = append(updateNew, update)
updateOld = append(updateOld, row.current)
}
continue
}
} }
return t.rows[dnsName][e.SetIdentifier].candidates = append(t.rows[dnsName][e.SetIdentifier].candidates, e)
}
func (t planTable) getCreates() (createList []*endpoint.Endpoint) {
for _, row := range t.rows {
if row.current == nil { //dns name not taken
createList = append(createList, t.resolver.ResolveCreate(row.candidates))
}
}
return
}
func (t planTable) getDeletes() (deleteList []*endpoint.Endpoint) {
for _, row := range t.rows {
if row.current != nil && len(row.candidates) == 0 {
deleteList = append(deleteList, row.current)
}
}
return
} }
// Calculate computes the actions needed to move current state towards desired // Calculate computes the actions needed to move current state towards desired
@ -148,9 +119,29 @@ func (p *Plan) Calculate() *Plan {
} }
changes := &Changes{} changes := &Changes{}
changes.Create = t.getCreates()
changes.Delete = t.getDeletes() for _, topRow := range t.rows {
changes.UpdateNew, changes.UpdateOld = t.getUpdates() for _, row := range topRow {
if row.current == nil { //dns name not taken
changes.Create = append(changes.Create, t.resolver.ResolveCreate(row.candidates))
}
if row.current != nil && len(row.candidates) == 0 {
changes.Delete = append(changes.Delete, row.current)
}
// TODO: allows record type change, which might not be supported by all dns providers
if row.current != nil && len(row.candidates) > 0 { //dns name is taken
update := t.resolver.ResolveUpdate(row.current, row.candidates)
// compare "update" to "current" to figure out if actual update is required
if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || shouldUpdateProviderSpecific(update, row.current) {
inheritOwner(row.current, update)
changes.UpdateNew = append(changes.UpdateNew, update)
changes.UpdateOld = append(changes.UpdateOld, row.current)
}
continue
}
}
}
for _, pol := range p.Policies { for _, pol := range p.Policies {
changes = pol.Apply(changes) changes = pol.Apply(changes)
} }
@ -196,11 +187,34 @@ func shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool {
continue continue
} }
found := false
for _, d := range desired.ProviderSpecific { for _, d := range desired.ProviderSpecific {
if d.Name == c.Name && d.Value != c.Value { if d.Name == c.Name {
return true if d.Value != c.Value {
// provider-specific attribute updated
return true
}
found = true
break
} }
} }
if !found {
// provider-specific attribute deleted
return true
}
}
for _, d := range desired.ProviderSpecific {
found := false
for _, c := range current.ProviderSpecific {
if d.Name == c.Name {
found = true
break
}
}
if !found {
// provider-specific attribute added
return true
}
} }
return false return false

View File

@ -38,6 +38,9 @@ type PlanTestSuite struct {
bar127AWithProviderSpecificTrue *endpoint.Endpoint bar127AWithProviderSpecificTrue *endpoint.Endpoint
bar127AWithProviderSpecificFalse *endpoint.Endpoint bar127AWithProviderSpecificFalse *endpoint.Endpoint
bar192A *endpoint.Endpoint bar192A *endpoint.Endpoint
multiple1 *endpoint.Endpoint
multiple2 *endpoint.Endpoint
multiple3 *endpoint.Endpoint
} }
func (suite *PlanTestSuite) SetupTest() { func (suite *PlanTestSuite) SetupTest() {
@ -138,7 +141,24 @@ func (suite *PlanTestSuite) SetupTest() {
endpoint.ResourceLabelKey: "ingress/default/bar-192", endpoint.ResourceLabelKey: "ingress/default/bar-192",
}, },
} }
suite.multiple1 = &endpoint.Endpoint{
DNSName: "multiple",
Targets: endpoint.Targets{"192.168.0.1"},
RecordType: "A",
SetIdentifier: "test-set-1",
}
suite.multiple2 = &endpoint.Endpoint{
DNSName: "multiple",
Targets: endpoint.Targets{"192.168.0.2"},
RecordType: "A",
SetIdentifier: "test-set-1",
}
suite.multiple3 = &endpoint.Endpoint{
DNSName: "multiple",
Targets: endpoint.Targets{"192.168.0.2"},
RecordType: "A",
SetIdentifier: "test-set-2",
}
} }
func (suite *PlanTestSuite) TestSyncFirstRound() { func (suite *PlanTestSuite) TestSyncFirstRound() {
@ -427,6 +447,50 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() {
validateEntries(suite.T(), changes.Delete, expectedDelete) validateEntries(suite.T(), changes.Delete, expectedDelete)
} }
func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier() {
current := []*endpoint.Endpoint{suite.multiple1}
desired := []*endpoint.Endpoint{suite.multiple2, suite.multiple3}
expectedCreate := []*endpoint.Endpoint{suite.multiple3}
expectedUpdateOld := []*endpoint.Endpoint{suite.multiple1}
expectedUpdateNew := []*endpoint.Endpoint{suite.multiple2}
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
}
changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Create, expectedCreate)
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
validateEntries(suite.T(), changes.Delete, expectedDelete)
}
func (suite *PlanTestSuite) TestSetIdentifierUpdateCreatesAndDeletes() {
current := []*endpoint.Endpoint{suite.multiple2}
desired := []*endpoint.Endpoint{suite.multiple3}
expectedCreate := []*endpoint.Endpoint{suite.multiple3}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{suite.multiple2}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
}
changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Create, expectedCreate)
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
validateEntries(suite.T(), changes.Delete, expectedDelete)
}
func TestPlan(t *testing.T) { func TestPlan(t *testing.T) {
suite.Run(t, new(PlanTestSuite)) suite.Run(t, new(PlanTestSuite))
} }

View File

@ -20,6 +20,7 @@ import (
"context" "context"
"fmt" "fmt"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
@ -37,7 +38,14 @@ const (
recordTTL = 300 recordTTL = 300
// provider specific key that designates whether an AWS ALIAS record has the EvaluateTargetHealth // provider specific key that designates whether an AWS ALIAS record has the EvaluateTargetHealth
// field set to true. // field set to true.
providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health" providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health"
providerSpecificWeight = "aws/weight"
providerSpecificRegion = "aws/region"
providerSpecificFailover = "aws/failover"
providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code"
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
) )
var ( var (
@ -249,6 +257,8 @@ func (p *AWSProvider) records(zones map[string]*route53.HostedZone) ([]*endpoint
endpoints := make([]*endpoint.Endpoint, 0) endpoints := make([]*endpoint.Endpoint, 0)
f := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) { f := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) {
for _, r := range resp.ResourceRecordSets { for _, r := range resp.ResourceRecordSets {
newEndpoints := make([]*endpoint.Endpoint, 0)
// TODO(linki, ownership): Remove once ownership system is in place. // TODO(linki, ownership): Remove once ownership system is in place.
// See: https://github.com/kubernetes-sigs/external-dns/pull/122/files/74e2c3d3e237411e619aefc5aab694742001cdec#r109863370 // See: https://github.com/kubernetes-sigs/external-dns/pull/122/files/74e2c3d3e237411e619aefc5aab694742001cdec#r109863370
@ -267,7 +277,7 @@ func (p *AWSProvider) records(zones map[string]*route53.HostedZone) ([]*endpoint
targets[idx] = aws.StringValue(rr.Value) targets[idx] = aws.StringValue(rr.Value)
} }
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.Type), ttl, targets...)) newEndpoints = append(newEndpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.Type), ttl, targets...))
} }
if r.AliasTarget != nil { if r.AliasTarget != nil {
@ -278,6 +288,36 @@ func (p *AWSProvider) records(zones map[string]*route53.HostedZone) ([]*endpoint
ep := endpoint. ep := endpoint.
NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), endpoint.RecordTypeCNAME, ttl, aws.StringValue(r.AliasTarget.DNSName)). 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)))
newEndpoints = append(newEndpoints, ep)
}
for _, ep := range newEndpoints {
if r.SetIdentifier != nil {
ep.SetIdentifier = aws.StringValue(r.SetIdentifier)
switch {
case r.Weight != nil:
ep.WithProviderSpecific(providerSpecificWeight, fmt.Sprintf("%d", aws.Int64Value(r.Weight)))
case r.Region != nil:
ep.WithProviderSpecific(providerSpecificRegion, aws.StringValue(r.Region))
case r.Failover != nil:
ep.WithProviderSpecific(providerSpecificFailover, aws.StringValue(r.Failover))
case r.MultiValueAnswer != nil && aws.BoolValue(r.MultiValueAnswer):
ep.WithProviderSpecific(providerSpecificMultiValueAnswer, "")
case r.GeoLocation != nil:
if r.GeoLocation.ContinentCode != nil {
ep.WithProviderSpecific(providerSpecificGeolocationContinentCode, aws.StringValue(r.GeoLocation.ContinentCode))
} else {
if r.GeoLocation.CountryCode != nil {
ep.WithProviderSpecific(providerSpecificGeolocationCountryCode, aws.StringValue(r.GeoLocation.CountryCode))
}
if r.GeoLocation.SubdivisionCode != nil {
ep.WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, aws.StringValue(r.GeoLocation.SubdivisionCode))
}
}
default:
// one of the above needs to be set, otherwise SetIdentifier doesn't make sense
}
}
endpoints = append(endpoints, ep) endpoints = append(endpoints, ep)
} }
} }
@ -483,6 +523,47 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint, recordsCac
} }
} }
setIdentifier := ep.SetIdentifier
if setIdentifier != "" {
change.ResourceRecordSet.SetIdentifier = aws.String(setIdentifier)
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificWeight); ok {
weight, err := strconv.ParseInt(prop.Value, 10, 64)
if err != nil {
log.Errorf("Failed parsing value of %s: %s: %v; using weight of 0", providerSpecificWeight, prop.Value, err)
weight = 0
}
change.ResourceRecordSet.Weight = aws.Int64(weight)
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificRegion); ok {
change.ResourceRecordSet.Region = aws.String(prop.Value)
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificFailover); ok {
change.ResourceRecordSet.Failover = aws.String(prop.Value)
}
if _, ok := ep.GetProviderSpecificProperty(providerSpecificMultiValueAnswer); ok {
change.ResourceRecordSet.MultiValueAnswer = aws.Bool(true)
}
var geolocation = &route53.GeoLocation{}
useGeolocation := false
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationContinentCode); ok {
geolocation.ContinentCode = aws.String(prop.Value)
useGeolocation = true
} else {
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationCountryCode); ok {
geolocation.CountryCode = aws.String(prop.Value)
useGeolocation = true
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationSubdivisionCode); ok {
geolocation.SubdivisionCode = aws.String(prop.Value)
useGeolocation = true
}
}
if useGeolocation {
change.ResourceRecordSet.GeoLocation = geolocation
}
}
return change, dualstack return change, dualstack
} }

View File

@ -184,7 +184,11 @@ func (r *Route53APIStub) ChangeResourceRecordSets(input *route53.ChangeResourceR
change.ResourceRecordSet.AliasTarget.DNSName = aws.String(wildcardEscape(ensureTrailingDot(aws.StringValue(change.ResourceRecordSet.AliasTarget.DNSName)))) change.ResourceRecordSet.AliasTarget.DNSName = aws.String(wildcardEscape(ensureTrailingDot(aws.StringValue(change.ResourceRecordSet.AliasTarget.DNSName))))
} }
key := aws.StringValue(change.ResourceRecordSet.Name) + "::" + aws.StringValue(change.ResourceRecordSet.Type) setId := ""
if change.ResourceRecordSet.SetIdentifier != nil {
setId = aws.StringValue(change.ResourceRecordSet.SetIdentifier)
}
key := aws.StringValue(change.ResourceRecordSet.Name) + "::" + aws.StringValue(change.ResourceRecordSet.Type) + "::" + setId
switch aws.StringValue(change.Action) { switch aws.StringValue(change.Action) {
case route53.ChangeActionCreate: case route53.ChangeActionCreate:
if _, found := recordSets[key]; found { if _, found := recordSets[key]; found {
@ -314,6 +318,13 @@ func TestAWSRecords(t *testing.T) {
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.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"), 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("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"),
endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20"),
endpoint.NewEndpointWithTTL("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificRegion, "us-east-1"),
endpoint.NewEndpointWithTTL("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificFailover, "PRIMARY"),
endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"),
}) })
records, err := provider.Records() records, err := provider.Records()
@ -328,6 +339,13 @@ func TestAWSRecords(t *testing.T) {
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-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-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("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("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"),
endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20"),
endpoint.NewEndpointWithTTL("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificRegion, "us-east-1"),
endpoint.NewEndpointWithTTL("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificFailover, "PRIMARY"),
endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"),
}) })
} }
@ -797,7 +815,7 @@ func TestAWSBatchChangeSetExceedingNameChange(t *testing.T) {
} }
func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
assert.True(t, testutils.SameEndpoints(endpoints, expected), "expected and actual endpoints don't match. %s:%s", endpoints, expected) assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %s:%s", endpoints, expected)
} }
func validateAWSZones(t *testing.T, zones map[string]*route53.HostedZone, expected map[string]*route53.HostedZone) { func validateAWSZones(t *testing.T, zones map[string]*route53.HostedZone, expected map[string]*route53.HostedZone) {

View File

@ -131,7 +131,7 @@ func (im *InMemoryProvider) Records() ([]*endpoint.Endpoint, error) {
} }
for _, record := range records { for _, record := range records {
ep := endpoint.NewEndpoint(record.Name, record.Type, record.Target) ep := endpoint.NewEndpoint(record.Name, record.Type, record.Target).WithSetIdentifier(record.SetIdentifier)
ep.Labels = record.Labels ep.Labels = record.Labels
endpoints = append(endpoints, ep) endpoints = append(endpoints, ep)
} }
@ -204,10 +204,11 @@ func convertToInMemoryRecord(endpoints []*endpoint.Endpoint) []*inMemoryRecord {
records := []*inMemoryRecord{} records := []*inMemoryRecord{}
for _, ep := range endpoints { for _, ep := range endpoints {
records = append(records, &inMemoryRecord{ records = append(records, &inMemoryRecord{
Type: ep.RecordType, Type: ep.RecordType,
Name: ep.DNSName, Name: ep.DNSName,
Target: ep.Targets[0], Target: ep.Targets[0],
Labels: ep.Labels, SetIdentifier: ep.SetIdentifier,
Labels: ep.Labels,
}) })
} }
return records return records
@ -246,10 +247,11 @@ func (f *filter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]st
// Name - DNS name assigned to the record // Name - DNS name assigned to the record
// Target - target of the record // Target - target of the record
type inMemoryRecord struct { type inMemoryRecord struct {
Type string Type string
Name string SetIdentifier string
Target string Name string
Labels endpoint.Labels Target string
Labels endpoint.Labels
} }
type zone map[string][]*inMemoryRecord type zone map[string][]*inMemoryRecord
@ -328,15 +330,19 @@ func (c *inMemoryClient) ApplyChanges(ctx context.Context, zoneID string, change
return nil return nil
} }
func (c *inMemoryClient) updateMesh(mesh map[string]map[string]bool, record *inMemoryRecord) error { func (c *inMemoryClient) updateMesh(mesh map[string]map[string]map[string]bool, record *inMemoryRecord) error {
if _, exists := mesh[record.Name]; exists { if _, exists := mesh[record.Name]; exists {
if mesh[record.Name][record.Type] { if _, exists := mesh[record.Name][record.Type]; exists {
return ErrDuplicateRecordFound if mesh[record.Name][record.Type][record.SetIdentifier] {
return ErrDuplicateRecordFound
}
mesh[record.Name][record.Type][record.SetIdentifier] = true
return nil
} }
mesh[record.Name][record.Type] = true mesh[record.Name][record.Type] = map[string]bool{record.SetIdentifier: true}
return nil return nil
} }
mesh[record.Name] = map[string]bool{record.Type: true} mesh[record.Name] = map[string]map[string]bool{record.Type: {record.SetIdentifier: true}}
return nil return nil
} }
@ -346,9 +352,9 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang
if !ok { if !ok {
return ErrZoneNotFound return ErrZoneNotFound
} }
mesh := map[string]map[string]bool{} mesh := map[string]map[string]map[string]bool{}
for _, newEndpoint := range changes.Create { for _, newEndpoint := range changes.Create {
if c.findByType(newEndpoint.Type, curZone[newEndpoint.Name]) != nil { if c.findByTypeAndSetIdentifier(newEndpoint.Type, newEndpoint.SetIdentifier, curZone[newEndpoint.Name]) != nil {
return ErrRecordAlreadyExists return ErrRecordAlreadyExists
} }
if err := c.updateMesh(mesh, newEndpoint); err != nil { if err := c.updateMesh(mesh, newEndpoint); err != nil {
@ -356,7 +362,7 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang
} }
} }
for _, updateEndpoint := range changes.UpdateNew { for _, updateEndpoint := range changes.UpdateNew {
if c.findByType(updateEndpoint.Type, curZone[updateEndpoint.Name]) == nil { if c.findByTypeAndSetIdentifier(updateEndpoint.Type, updateEndpoint.SetIdentifier, curZone[updateEndpoint.Name]) == nil {
return ErrRecordNotFound return ErrRecordNotFound
} }
if err := c.updateMesh(mesh, updateEndpoint); err != nil { if err := c.updateMesh(mesh, updateEndpoint); err != nil {
@ -364,12 +370,12 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang
} }
} }
for _, updateOldEndpoint := range changes.UpdateOld { for _, updateOldEndpoint := range changes.UpdateOld {
if rec := c.findByType(updateOldEndpoint.Type, curZone[updateOldEndpoint.Name]); rec == nil || rec.Target != updateOldEndpoint.Target { if rec := c.findByTypeAndSetIdentifier(updateOldEndpoint.Type, updateOldEndpoint.SetIdentifier, curZone[updateOldEndpoint.Name]); rec == nil || rec.Target != updateOldEndpoint.Target {
return ErrRecordNotFound return ErrRecordNotFound
} }
} }
for _, deleteEndpoint := range changes.Delete { for _, deleteEndpoint := range changes.Delete {
if rec := c.findByType(deleteEndpoint.Type, curZone[deleteEndpoint.Name]); rec == nil || rec.Target != deleteEndpoint.Target { if rec := c.findByTypeAndSetIdentifier(deleteEndpoint.Type, deleteEndpoint.SetIdentifier, curZone[deleteEndpoint.Name]); rec == nil || rec.Target != deleteEndpoint.Target {
return ErrRecordNotFound return ErrRecordNotFound
} }
if err := c.updateMesh(mesh, deleteEndpoint); err != nil { if err := c.updateMesh(mesh, deleteEndpoint); err != nil {
@ -379,9 +385,9 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang
return nil return nil
} }
func (c *inMemoryClient) findByType(recordType string, records []*inMemoryRecord) *inMemoryRecord { func (c *inMemoryClient) findByTypeAndSetIdentifier(recordType, setIdentifier string, records []*inMemoryRecord) *inMemoryRecord {
for _, record := range records { for _, record := range records {
if record.Type == recordType { if record.Type == recordType && record.SetIdentifier == setIdentifier {
return record return record
} }
} }

View File

@ -43,11 +43,12 @@ func TestInMemoryProvider(t *testing.T) {
func testInMemoryFindByType(t *testing.T) { func testInMemoryFindByType(t *testing.T) {
for _, ti := range []struct { for _, ti := range []struct {
title string title string
findType string findType string
records []*inMemoryRecord findSetIdentifier string
expected *inMemoryRecord records []*inMemoryRecord
expectedEmpty bool expected *inMemoryRecord
expectedEmpty bool
}{ }{
{ {
title: "no records, empty type", title: "no records, empty type",
@ -112,10 +113,32 @@ func testInMemoryFindByType(t *testing.T) {
Type: endpoint.RecordTypeA, Type: endpoint.RecordTypeA,
}, },
}, },
{
title: "multiple records, right type and set identifier",
findType: endpoint.RecordTypeA,
findSetIdentifier: "test-set-1",
records: []*inMemoryRecord{
{
Type: endpoint.RecordTypeA,
SetIdentifier: "test-set-1",
},
{
Type: endpoint.RecordTypeA,
SetIdentifier: "test-set-2",
},
{
Type: endpoint.RecordTypeTXT,
},
},
expected: &inMemoryRecord{
Type: endpoint.RecordTypeA,
SetIdentifier: "test-set-1",
},
},
} { } {
t.Run(ti.title, func(t *testing.T) { t.Run(ti.title, func(t *testing.T) {
c := newInMemoryClient() c := newInMemoryClient()
record := c.findByType(ti.findType, ti.records) record := c.findByTypeAndSetIdentifier(ti.findType, ti.findSetIdentifier, ti.records)
if ti.expectedEmpty { if ti.expectedEmpty {
assert.Nil(t, record) assert.Nil(t, record)
} else { } else {

View File

@ -21,6 +21,7 @@ import (
"errors" "errors"
"time" "time"
"fmt"
"strings" "strings"
"github.com/kubernetes-sigs/external-dns/endpoint" "github.com/kubernetes-sigs/external-dns/endpoint"
@ -94,15 +95,16 @@ func (im *TXTRegistry) Records() ([]*endpoint.Endpoint, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
endpointDNSName := im.mapper.toEndpointName(record.DNSName) key := fmt.Sprintf("%s::%s", im.mapper.toEndpointName(record.DNSName), record.SetIdentifier)
labelMap[endpointDNSName] = labels labelMap[key] = labels
} }
for _, ep := range endpoints { for _, ep := range endpoints {
if ep.Labels == nil { if ep.Labels == nil {
ep.Labels = endpoint.NewLabels() ep.Labels = endpoint.NewLabels()
} }
if labels, ok := labelMap[ep.DNSName]; ok { key := fmt.Sprintf("%s::%s", ep.DNSName, ep.SetIdentifier)
if labels, ok := labelMap[key]; ok {
for k, v := range labels { for k, v := range labels {
ep.Labels[k] = v ep.Labels[k] = v
} }
@ -132,7 +134,8 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
r.Labels = make(map[string]string) r.Labels = make(map[string]string)
} }
r.Labels[endpoint.OwnerLabelKey] = im.ownerID r.Labels[endpoint.OwnerLabelKey] = im.ownerID
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)) txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
txt.ProviderSpecific = r.ProviderSpecific
filteredChanges.Create = append(filteredChanges.Create, txt) filteredChanges.Create = append(filteredChanges.Create, txt)
if im.cacheInterval > 0 { if im.cacheInterval > 0 {
@ -141,7 +144,8 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
} }
for _, r := range filteredChanges.Delete { for _, r := range filteredChanges.Delete {
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)) txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
txt.ProviderSpecific = r.ProviderSpecific
// when we delete TXT records for which value has changed (due to new label) this would still work because // when we delete TXT records for which value has changed (due to new label) this would still work because
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed // !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
@ -154,7 +158,8 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
// make sure TXT records are consistently updated as well // make sure TXT records are consistently updated as well
for _, r := range filteredChanges.UpdateOld { for _, r := range filteredChanges.UpdateOld {
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)) txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
txt.ProviderSpecific = r.ProviderSpecific
// when we updateOld TXT records for which value has changed (due to new label) this would still work because // when we updateOld TXT records for which value has changed (due to new label) this would still work because
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed // !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
filteredChanges.UpdateOld = append(filteredChanges.UpdateOld, txt) filteredChanges.UpdateOld = append(filteredChanges.UpdateOld, txt)
@ -166,7 +171,8 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
// make sure TXT records are consistently updated as well // make sure TXT records are consistently updated as well
for _, r := range filteredChanges.UpdateNew { for _, r := range filteredChanges.UpdateNew {
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)) txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
txt.ProviderSpecific = r.ProviderSpecific
filteredChanges.UpdateNew = append(filteredChanges.UpdateNew, txt) filteredChanges.UpdateNew = append(filteredChanges.UpdateNew, txt)
// add new version of record to cache // add new version of record to cache
if im.cacheInterval > 0 { if im.cacheInterval > 0 {
@ -230,7 +236,7 @@ func (im *TXTRegistry) removeFromCache(ep *endpoint.Endpoint) {
} }
for i, e := range im.recordsCache { for i, e := range im.recordsCache {
if e.DNSName == ep.DNSName && e.RecordType == ep.RecordType && e.Targets.Same(ep.Targets) { if e.DNSName == ep.DNSName && e.RecordType == ep.RecordType && e.SetIdentifier == ep.SetIdentifier && e.Targets.Same(ep.Targets) {
// We found a match delete the endpoint from the cache. // We found a match delete the endpoint from the cache.
im.recordsCache = append(im.recordsCache[:i], im.recordsCache[i+1:]...) im.recordsCache = append(im.recordsCache[:i], im.recordsCache[i+1:]...)
return return

View File

@ -80,6 +80,10 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
newEndpointWithOwner("TxT.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), // case-insensitive TXT prefix newEndpointWithOwner("TxT.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), // case-insensitive TXT prefix
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
}, },
}) })
expectedRecords := []*endpoint.Endpoint{ expectedRecords := []*endpoint.Endpoint{
@ -134,6 +138,24 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
endpoint.OwnerLabelKey: "", endpoint.OwnerLabelKey: "",
}, },
}, },
{
DNSName: "multiple.test-zone.example.org",
Targets: endpoint.Targets{"lb1.loadbalancer.com"},
SetIdentifier: "test-set-1",
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "multiple.test-zone.example.org",
Targets: endpoint.Targets{"lb2.loadbalancer.com"},
SetIdentifier: "test-set-2",
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
} }
r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour) r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour)
@ -246,6 +268,10 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
}, },
}) })
r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour) r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour)
@ -253,33 +279,45 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
changes := &plan.Changes{ changes := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"), newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"),
}, },
Delete: []*endpoint.Endpoint{ Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"),
}, },
UpdateNew: []*endpoint.Endpoint{ UpdateNew: []*endpoint.Endpoint{
newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"), newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"),
}, },
UpdateOld: []*endpoint.Endpoint{ UpdateOld: []*endpoint.Endpoint{
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"),
}, },
} }
expected := &plan.Changes{ expected := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"), newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"),
newEndpointWithOwner("txt.new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "owner", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"),
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-3"),
}, },
Delete: []*endpoint.Endpoint{ Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"),
}, },
UpdateNew: []*endpoint.Endpoint{ UpdateNew: []*endpoint.Endpoint{
newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"), newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"),
newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
}, },
UpdateOld: []*endpoint.Endpoint{ UpdateOld: []*endpoint.Endpoint{
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
}, },
} }
p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) {

View File

@ -43,7 +43,7 @@ func (ms *dedupSource) Endpoints() ([]*endpoint.Endpoint, error) {
} }
for _, ep := range endpoints { for _, ep := range endpoints {
identifier := ep.DNSName + " / " + ep.Targets.String() identifier := ep.DNSName + " / " + ep.SetIdentifier + " / " + ep.Targets.String()
if _, ok := collected[identifier]; ok { if _, ok := collected[identifier]; ok {
log.Debugf("Removing duplicate endpoint %s", ep) log.Debugf("Removing duplicate endpoint %s", ep)

View File

@ -174,14 +174,14 @@ func (sc *gatewaySource) endpointsFromTemplate(config *istiomodel.Config) ([]*en
} }
} }
providerSpecific := getProviderSpecificAnnotations(config.Annotations) providerSpecific, setIdentifier := getProviderSpecificAnnotations(config.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// splits the FQDN template and removes the trailing periods // splits the FQDN template and removes the trailing periods
hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",") hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",")
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
hostname = strings.TrimSuffix(hostname, ".") hostname = strings.TrimSuffix(hostname, ".")
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
} }
return endpoints, nil return endpoints, nil
} }
@ -266,7 +266,7 @@ func (sc *gatewaySource) endpointsFromGatewayConfig(config istiomodel.Config) ([
gateway := config.Spec.(*istionetworking.Gateway) gateway := config.Spec.(*istionetworking.Gateway)
providerSpecific := getProviderSpecificAnnotations(config.Annotations) providerSpecific, setIdentifier := getProviderSpecificAnnotations(config.Annotations)
for _, server := range gateway.Servers { for _, server := range gateway.Servers {
for _, host := range server.Hosts { for _, host := range server.Hosts {
@ -282,7 +282,7 @@ func (sc *gatewaySource) endpointsFromGatewayConfig(config istiomodel.Config) ([
host = parts[1] host = parts[1]
} }
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...)
} }
} }
@ -290,7 +290,7 @@ func (sc *gatewaySource) endpointsFromGatewayConfig(config istiomodel.Config) ([
if !sc.ignoreHostnameAnnotation { if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(config.Annotations) hostnameList := getHostnamesFromAnnotations(config.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
} }
} }

View File

@ -187,14 +187,14 @@ func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoin
targets = targetsFromIngressStatus(ing.Status) targets = targetsFromIngressStatus(ing.Status)
} }
providerSpecific := getProviderSpecificAnnotations(ing.Annotations) providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// splits the FQDN template and removes the trailing periods // splits the FQDN template and removes the trailing periods
hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",") hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",")
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
hostname = strings.TrimSuffix(hostname, ".") hostname = strings.TrimSuffix(hostname, ".")
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
} }
return endpoints, nil return endpoints, nil
} }
@ -261,13 +261,13 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [
targets = targetsFromIngressStatus(ing.Status) targets = targetsFromIngressStatus(ing.Status)
} }
providerSpecific := getProviderSpecificAnnotations(ing.Annotations) providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
for _, rule := range ing.Spec.Rules { for _, rule := range ing.Spec.Rules {
if rule.Host == "" { if rule.Host == "" {
continue continue
} }
endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...)
} }
for _, tls := range ing.Spec.TLS { for _, tls := range ing.Spec.TLS {
@ -275,7 +275,7 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [
if host == "" { if host == "" {
continue continue
} }
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...)
} }
} }
@ -283,7 +283,7 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [
if !ignoreHostnameAnnotation { if !ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(ing.Annotations) hostnameList := getHostnamesFromAnnotations(ing.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
} }
} }
return endpoints return endpoints

View File

@ -209,14 +209,14 @@ func (sc *ingressRouteSource) endpointsFromTemplate(ingressRoute *contourapi.Ing
} }
} }
providerSpecific := getProviderSpecificAnnotations(ingressRoute.Annotations) providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// splits the FQDN template and removes the trailing periods // splits the FQDN template and removes the trailing periods
hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",") hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",")
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
hostname = strings.TrimSuffix(hostname, ".") hostname = strings.TrimSuffix(hostname, ".")
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
} }
return endpoints, nil return endpoints, nil
} }
@ -302,11 +302,11 @@ func (sc *ingressRouteSource) endpointsFromIngressRoute(ingressRoute *contourapi
} }
} }
providerSpecific := getProviderSpecificAnnotations(ingressRoute.Annotations) providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations)
if virtualHost := ingressRoute.Spec.VirtualHost; virtualHost != nil { if virtualHost := ingressRoute.Spec.VirtualHost; virtualHost != nil {
if fqdn := virtualHost.Fqdn; fqdn != "" { if fqdn := virtualHost.Fqdn; fqdn != "" {
endpoints = append(endpoints, endpointsForHostname(fqdn, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(fqdn, targets, ttl, providerSpecific, setIdentifier)...)
} }
} }
@ -314,7 +314,7 @@ func (sc *ingressRouteSource) endpointsFromIngressRoute(ingressRoute *contourapi
if !sc.ignoreHostnameAnnotation { if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
} }
} }

View File

@ -285,10 +285,10 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.End
return nil, fmt.Errorf("failed to apply template on service %s: %v", svc.String(), err) return nil, fmt.Errorf("failed to apply template on service %s: %v", svc.String(), err)
} }
providerSpecific := getProviderSpecificAnnotations(svc.Annotations) providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",") hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",")
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific)...) endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier)...)
} }
return endpoints, nil return endpoints, nil
@ -299,10 +299,10 @@ func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// Skip endpoints if we do not want entries from annotations // Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation { if !sc.ignoreHostnameAnnotation {
providerSpecific := getProviderSpecificAnnotations(svc.Annotations) providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
hostnameList := getHostnamesFromAnnotations(svc.Annotations) hostnameList := getHostnamesFromAnnotations(svc.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific)...) endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier)...)
} }
} }
return endpoints return endpoints
@ -358,7 +358,7 @@ func (sc *serviceSource) setResourceLabel(service *v1.Service, endpoints []*endp
} }
} }
func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, providerSpecific endpoint.ProviderSpecific) []*endpoint.Endpoint { func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, providerSpecific endpoint.ProviderSpecific, setIdentifier string) []*endpoint.Endpoint {
hostname = strings.TrimSuffix(hostname, ".") hostname = strings.TrimSuffix(hostname, ".")
ttl, err := getTTLFromAnnotations(svc.Annotations) ttl, err := getTTLFromAnnotations(svc.Annotations)
if err != nil { if err != nil {
@ -366,21 +366,19 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
} }
epA := &endpoint.Endpoint{ epA := &endpoint.Endpoint{
RecordTTL: ttl, RecordTTL: ttl,
RecordType: endpoint.RecordTypeA, RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(), Labels: endpoint.NewLabels(),
Targets: make(endpoint.Targets, 0, defaultTargetsCapacity), Targets: make(endpoint.Targets, 0, defaultTargetsCapacity),
DNSName: hostname, DNSName: hostname,
ProviderSpecific: providerSpecific,
} }
epCNAME := &endpoint.Endpoint{ epCNAME := &endpoint.Endpoint{
RecordTTL: ttl, RecordTTL: ttl,
RecordType: endpoint.RecordTypeCNAME, RecordType: endpoint.RecordTypeCNAME,
Labels: endpoint.NewLabels(), Labels: endpoint.NewLabels(),
Targets: make(endpoint.Targets, 0, defaultTargetsCapacity), Targets: make(endpoint.Targets, 0, defaultTargetsCapacity),
DNSName: hostname, DNSName: hostname,
ProviderSpecific: providerSpecific,
} }
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
@ -423,6 +421,10 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
if len(epCNAME.Targets) > 0 { if len(epCNAME.Targets) > 0 {
endpoints = append(endpoints, epCNAME) endpoints = append(endpoints, epCNAME)
} }
for _, endpoint := range endpoints {
endpoint.ProviderSpecific = providerSpecific
endpoint.SetIdentifier = setIdentifier
}
return endpoints return endpoints
} }

View File

@ -45,6 +45,8 @@ const (
const ( const (
// The annotation used for determining if traffic will go through Cloudflare // The annotation used for determining if traffic will go through Cloudflare
CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied" CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier"
) )
const ( const (
@ -86,7 +88,7 @@ func getAliasFromAnnotations(annotations map[string]string) bool {
return exists && aliasAnnotation == "true" return exists && aliasAnnotation == "true"
} }
func getProviderSpecificAnnotations(annotations map[string]string) endpoint.ProviderSpecific { func getProviderSpecificAnnotations(annotations map[string]string) (endpoint.ProviderSpecific, string) {
providerSpecificAnnotations := endpoint.ProviderSpecific{} providerSpecificAnnotations := endpoint.ProviderSpecific{}
v, exists := annotations[CloudflareProxiedKey] v, exists := annotations[CloudflareProxiedKey]
@ -102,7 +104,19 @@ func getProviderSpecificAnnotations(annotations map[string]string) endpoint.Prov
Value: "true", Value: "true",
}) })
} }
return providerSpecificAnnotations setIdentifier := ""
for k, v := range annotations {
if k == SetIdentifierKey {
setIdentifier = v
} else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/aws-") {
attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/aws-")
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("aws/%s", attr),
Value: v,
})
}
}
return providerSpecificAnnotations, setIdentifier
} }
// getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation. // getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
@ -133,7 +147,7 @@ func suitableType(target string) string {
} }
// endpointsForHostname returns the endpoint objects for each host-target combination. // endpointsForHostname returns the endpoint objects for each host-target combination.
func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific) []*endpoint.Endpoint { func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
var aTargets endpoint.Targets var aTargets endpoint.Targets
@ -156,6 +170,7 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin
RecordType: endpoint.RecordTypeA, RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(), Labels: endpoint.NewLabels(),
ProviderSpecific: providerSpecific, ProviderSpecific: providerSpecific,
SetIdentifier: setIdentifier,
} }
endpoints = append(endpoints, epA) endpoints = append(endpoints, epA)
} }
@ -168,6 +183,7 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin
RecordType: endpoint.RecordTypeCNAME, RecordType: endpoint.RecordTypeCNAME,
Labels: endpoint.NewLabels(), Labels: endpoint.NewLabels(),
ProviderSpecific: providerSpecific, ProviderSpecific: providerSpecific,
SetIdentifier: setIdentifier,
} }
endpoints = append(endpoints, epCNAME) endpoints = append(endpoints, epCNAME)
} }