From 25c7cb28ea9a0fa96a581e8770f753a35aa25f77 Mon Sep 17 00:00:00 2001 From: Dinar Valeev Date: Tue, 6 Jul 2021 16:50:29 +0200 Subject: [PATCH] Add additional txt record with record type marker In order to track multiple record types with the same name, lets migrate to new format, were record name contains record type in it. Signed-off-by: Dinar Valeev --- docs/proposal/registry.md | 3 +- docs/registry.md | 14 ++ pkg/apis/externaldns/types.go | 4 +- registry/txt.go | 151 +++++++++++++++--- registry/txt_test.go | 281 ++++++++++++++++++++++++++++++++-- 5 files changed, 411 insertions(+), 42 deletions(-) create mode 100644 docs/registry.md diff --git a/docs/proposal/registry.md b/docs/proposal/registry.md index e05d81a250..84f355dc7f 100644 --- a/docs/proposal/registry.md +++ b/docs/proposal/registry.md @@ -32,8 +32,7 @@ The following presents two ways to implement the registry, and we are planning t This implementation idea is borrowed from [Mate](https://github.com/linki/mate/) -Each record created by external-dns is accompanied by the TXT record, which internally stores the external-dns identifier. For example, if external dns with `owner-id="external-dns-1"` record to be created with dns name `foo.zone.org`, external-dns will create a TXT record with the same dns name `foo.zone.org` and injected value of `"external-dns-1"`. The transfer of ownership can be done by modifying the value of the TXT record. If no TXT record exists for the record or the value does not match its own `owner-id`, then external-dns will simply ignore it. - +Each record created by external-dns is accompanied by the TXT record, which internally stores the external-dns identifier. For example, if external dns with `owner-id="external-dns-1"` record to be created with dns name `foo.zone.org`, external-dns will create a TXT record with the same dns name `-foo.zone.org` and injected value of `"external-dns-1"`. The transfer of ownership can be done by modifying the value of the TXT record. If no TXT record exists for the record or the value does not match its own `owner-id`, then external-dns will simply ignore it. #### Goods 1. Easy to guarantee cross-cluster ownership safety diff --git a/docs/registry.md b/docs/registry.md new file mode 100644 index 0000000000..b9bb93904d --- /dev/null +++ b/docs/registry.md @@ -0,0 +1,14 @@ +### TXT Registry migration to a new format ### + +In order to support more record types and be able to track ownership without TXT record name clash, a new TXT record is introduced. +It contains record type it manages, e.g.: +* A record foo.example.com will be tracked with classic foo.example.com TXT record +* At the same time a new TXT record will be created a-foo.example.com + +Prefix and suffix are extended with %{record_type} template where the user can control how prefixed/suffixed records should look like. + +In order to maintain compatibility, both records will be maintained for some time, in order to have downgrade possibility. + +Later on, the old format will be dropped and only the new format will be kept (-). + +Cleanup will be done by controller itself. diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index f559c84d52..154879f60c 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -512,8 +512,8 @@ func (cfg *Config) ParseFlags(args []string) error { // Flags related to the registry app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "aws-sd") app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID) - app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix) - app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix) + app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Could contain record type template like '%{record_type}-prefix-'. Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix) + app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Could contain record type template like '-%{record_type}-suffix'. Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix) app.Flag("txt-wildcard-replacement", "When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional)").Default(defaultConfig.TXTWildcardReplacement).StringVar(&cfg.TXTWildcardReplacement) // Flags related to the main control loop diff --git a/registry/txt.go b/registry/txt.go index 42944615c4..6ca721f7bd 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -30,6 +30,8 @@ import ( "sigs.k8s.io/external-dns/provider" ) +const recordTemplate = "%{record_type}" + // TXTRegistry implements registry interface with ownership implemented via associated TXT records type TXTRegistry struct { provider provider.Provider @@ -68,6 +70,10 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st }, nil } +func getSupportedTypes() []string { + return []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS} +} + func (im *TXTRegistry) GetDomainFilter() endpoint.DomainFilterInterface { return im.provider.GetDomainFilter() } @@ -140,6 +146,19 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error return endpoints, nil } +// generateTXTRecord generates both "old" and "new" TXT records. +// Once we decide to drop old format we need to drop toTXTName() and rename toNewTXTName +func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpoint { + // old TXT record format + txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier) + txt.ProviderSpecific = r.ProviderSpecific + // new TXT record format (containing record type) + txtNew := endpoint.NewEndpoint(im.mapper.toNewTXTName(r.DNSName, r.RecordType), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier) + txtNew.ProviderSpecific = r.ProviderSpecific + + return []*endpoint.Endpoint{txt, txtNew} +} + // ApplyChanges updates dns provider with the changes // for each created/deleted record it will also take into account TXT records for creation/deletion func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { @@ -154,9 +173,8 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) r.Labels = make(map[string]string) } r.Labels[endpoint.OwnerLabelKey] = im.ownerID - 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, im.generateTXTRecord(r)...) if im.cacheInterval > 0 { im.addToCache(r) @@ -164,12 +182,10 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) } for _, r := range filteredChanges.Delete { - 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 // !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed - filteredChanges.Delete = append(filteredChanges.Delete, txt) + // !!! After migration to the new TXT registry format we can drop records in old format here!!! + filteredChanges.Delete = append(filteredChanges.Delete, im.generateTXTRecord(r)...) if im.cacheInterval > 0 { im.removeFromCache(r) @@ -178,11 +194,9 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) // make sure TXT records are consistently updated as well for _, r := range filteredChanges.UpdateOld { - 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 // !!! 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, im.generateTXTRecord(r)...) // remove old version of record from cache if im.cacheInterval > 0 { im.removeFromCache(r) @@ -191,9 +205,7 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) // make sure TXT records are consistently updated as well for _, r := range filteredChanges.UpdateNew { - 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, im.generateTXTRecord(r)...) // add new version of record to cache if im.cacheInterval > 0 { im.addToCache(r) @@ -229,6 +241,7 @@ func (im *TXTRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoi type nameMapper interface { toEndpointName(string) string toTXTName(string) string + toNewTXTName(string, string) string } type affixNameMapper struct { @@ -239,21 +252,71 @@ type affixNameMapper struct { var _ nameMapper = affixNameMapper{} -func newaffixNameMapper(prefix string, suffix string, wildcardReplacement string) affixNameMapper { +func newaffixNameMapper(prefix, suffix, wildcardReplacement string) affixNameMapper { return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix), wildcardReplacement: strings.ToLower(wildcardReplacement)} } +func dropRecordType(name string) string { + nameS := strings.Split(name, "-") + for _, t := range getSupportedTypes() { + if nameS[0] == strings.ToLower(t) { + return strings.TrimPrefix(name, nameS[0]+"-") + } + } + return name +} + +// dropAffix strips TXT record to find an endpoint name it manages +// It takes into consideration a fact that it could contain record type +// So it gets stripped first +func (pr affixNameMapper) dropAffix(name string) string { + if pr.recordTypeInAffix() { + for _, t := range getSupportedTypes() { + t = strings.ToLower(t) + iPrefix := strings.ReplaceAll(pr.prefix, recordTemplate, t) + iSuffix := strings.ReplaceAll(pr.suffix, recordTemplate, t) + if pr.isPrefix() && strings.HasPrefix(name, iPrefix) { + return strings.TrimPrefix(name, iPrefix) + } + + if pr.isSuffix() && strings.HasSuffix(name, iSuffix) { + return strings.TrimSuffix(name, iSuffix) + } + } + } + if strings.HasPrefix(name, pr.prefix) && pr.isPrefix() { + return strings.TrimPrefix(name, pr.prefix) + } + + if strings.HasSuffix(name, pr.suffix) && pr.isSuffix() { + return strings.TrimSuffix(name, pr.suffix) + } + return "" +} + +func (pr affixNameMapper) dropAffixTemplate(name string) string { + return strings.ReplaceAll(name, recordTemplate, "") +} + +func (pr affixNameMapper) isPrefix() bool { + return len(pr.suffix) == 0 +} +func (pr affixNameMapper) isSuffix() bool { + return len(pr.prefix) == 0 && len(pr.suffix) > 0 +} + func (pr affixNameMapper) toEndpointName(txtDNSName string) string { - lowerDNSName := strings.ToLower(txtDNSName) - if strings.HasPrefix(lowerDNSName, pr.prefix) && len(pr.suffix) == 0 { - return strings.TrimPrefix(lowerDNSName, pr.prefix) + lowerDNSName := dropRecordType(strings.ToLower(txtDNSName)) + + // drop prefix + if strings.HasPrefix(lowerDNSName, pr.prefix) && pr.isPrefix() { + return pr.dropAffix(lowerDNSName) } - if len(pr.suffix) > 0 { + // drop suffix + if pr.isSuffix() { DNSName := strings.SplitN(lowerDNSName, ".", 2) - if strings.HasSuffix(DNSName[0], pr.suffix) { - return strings.TrimSuffix(DNSName[0], pr.suffix) + "." + DNSName[1] - } + return pr.dropAffix(DNSName[0]) + "." + DNSName[1] } return "" } @@ -261,15 +324,57 @@ func (pr affixNameMapper) toEndpointName(txtDNSName string) string { func (pr affixNameMapper) toTXTName(endpointDNSName string) string { DNSName := strings.SplitN(endpointDNSName, ".", 2) + prefix := pr.dropAffixTemplate(pr.prefix) + suffix := pr.dropAffixTemplate(pr.suffix) // If specified, replace a leading asterisk in the generated txt record name with some other string if pr.wildcardReplacement != "" && DNSName[0] == "*" { DNSName[0] = pr.wildcardReplacement } if len(DNSName) < 2 { - return pr.prefix + DNSName[0] + pr.suffix + return prefix + DNSName[0] + suffix + } + return prefix + DNSName[0] + suffix + "." + DNSName[1] +} + +func (pr affixNameMapper) recordTypeInAffix() bool { + if strings.Contains(pr.prefix, recordTemplate) { + return true } - return pr.prefix + DNSName[0] + pr.suffix + "." + DNSName[1] + if strings.Contains(pr.suffix, recordTemplate) { + return true + } + return false +} + +func (pr affixNameMapper) normalizeAffixTemplate(afix, recordType string) string { + if strings.Contains(afix, recordTemplate) { + return strings.ReplaceAll(afix, recordTemplate, recordType) + } + return afix +} +func (pr affixNameMapper) toNewTXTName(endpointDNSName, recordType string) string { + DNSName := strings.SplitN(endpointDNSName, ".", 2) + recordType = strings.ToLower(recordType) + recordT := recordType + "-" + + prefix := pr.normalizeAffixTemplate(pr.prefix, recordType) + suffix := pr.normalizeAffixTemplate(pr.suffix, recordType) + + // If specified, replace a leading asterisk in the generated txt record name with some other string + if pr.wildcardReplacement != "" && DNSName[0] == "*" { + DNSName[0] = pr.wildcardReplacement + } + + if !pr.recordTypeInAffix() { + DNSName[0] = recordT + DNSName[0] + } + + if len(DNSName) < 2 { + return prefix + DNSName[0] + suffix + } + + return prefix + DNSName[0] + suffix + "." + DNSName[1] } func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) { diff --git a/registry/txt_test.go b/registry/txt_test.go index b3e20dd6ef..b1b2e0672e 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -19,6 +19,7 @@ package registry import ( "context" "reflect" + "strings" "testing" "time" @@ -375,6 +376,8 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { func testTXTRegistryApplyChanges(t *testing.T) { t.Run("With Prefix", testTXTRegistryApplyChangesWithPrefix) + t.Run("With Templated Prefix", testTXTRegistryApplyChangesWithTemplatedPrefix) + t.Run("With Templated Suffix", testTXTRegistryApplyChangesWithTemplatedSuffix) t.Run("With Suffix", testTXTRegistryApplyChangesWithSuffix) t.Run("No prefix", testTXTRegistryApplyChangesNoPrefix) } @@ -396,21 +399,25 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("txt.cname-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("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("txt.cname-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("txt.cname-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"), + newEndpointWithOwner("txt.cname-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, "") changes := &plan.Changes{ Create: []*endpoint.Endpoint{ - 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"), - newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"), + newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), + newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), + newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), @@ -427,30 +434,39 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { } expected := &plan.Changes{ 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", endpoint.RecordTypeCNAME, "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, ""), - newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "owner", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), + newEndpointWithOwner("txt.cname-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", endpoint.RecordTypeCNAME, "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"), - newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"), + newEndpointWithOwner("txt.cname-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"), + newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwner("txt.example", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("txt.cname-example", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), }, Delete: []*endpoint.Endpoint{ 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.cname-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"), + newEndpointWithOwner("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), }, UpdateNew: []*endpoint.Endpoint{ 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.cname-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"), + newEndpointWithOwner("txt.cname-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{ 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.cname-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"), + newEndpointWithOwner("txt.cname-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) { @@ -473,6 +489,95 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { require.NoError(t, err) } +func testTXTRegistryApplyChangesWithTemplatedPrefix(t *testing.T) { + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + ctxEndpoints := []*endpoint.Endpoint{} + ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) + } + p.ApplyChanges(ctx, &plan.Changes{ + Create: []*endpoint.Endpoint{}, + }) + r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "") + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), + }, + Delete: []*endpoint.Endpoint{}, + UpdateOld: []*endpoint.Endpoint{}, + UpdateNew: []*endpoint.Endpoint{}, + } + expected := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), + newEndpointWithOwner("prefix.new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("prefixcname.new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), + }, + } + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + mExpected := map[string][]*endpoint.Endpoint{ + "Create": expected.Create, + "UpdateNew": expected.UpdateNew, + "UpdateOld": expected.UpdateOld, + "Delete": expected.Delete, + } + mGot := map[string][]*endpoint.Endpoint{ + "Create": got.Create, + "UpdateNew": got.UpdateNew, + "UpdateOld": got.UpdateOld, + "Delete": got.Delete, + } + assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) + assert.Equal(t, nil, ctx.Value(provider.RecordsContextKey)) + } + err := r.ApplyChanges(ctx, changes) + require.NoError(t, err) +} +func testTXTRegistryApplyChangesWithTemplatedSuffix(t *testing.T) { + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + ctxEndpoints := []*endpoint.Endpoint{} + ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) + } + r, _ := NewTXTRegistry(p, "", "-%{record_type}suffix", "owner", time.Hour, "") + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), + }, + Delete: []*endpoint.Endpoint{}, + UpdateOld: []*endpoint.Endpoint{}, + UpdateNew: []*endpoint.Endpoint{}, + } + expected := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), + newEndpointWithOwner("new-record-1-cnamesuffix.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("new-record-1-suffix.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), + }, + } + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + mExpected := map[string][]*endpoint.Endpoint{ + "Create": expected.Create, + "UpdateNew": expected.UpdateNew, + "UpdateOld": expected.UpdateOld, + "Delete": expected.Delete, + } + mGot := map[string][]*endpoint.Endpoint{ + "Create": got.Create, + "UpdateNew": got.UpdateNew, + "UpdateOld": got.UpdateOld, + "Delete": got.Delete, + } + assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) + assert.Equal(t, nil, ctx.Value(provider.RecordsContextKey)) + } + err := r.ApplyChanges(ctx, changes) + require.NoError(t, err) +} func testTXTRegistryApplyChangesWithSuffix(t *testing.T) { p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) @@ -485,27 +590,32 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) { Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), - newEndpointWithOwner("bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("bar-txt.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-tar-txt.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-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-foobar-txt.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-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), + newEndpointWithOwner("cname-multiple-txt.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-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), + newEndpointWithOwner("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), }, }) r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard") changes := &plan.Changes{ Create: []*endpoint.Endpoint{ - 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"), - newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"), - newEndpointWithOwnerResource("*.wildcard.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", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), + newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), + newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), + newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), @@ -522,32 +632,42 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) { } expected := &plan.Changes{ 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", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwner("new-record-1-txt.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("cname-new-record-1-txt.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", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-3"), - newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"), + newEndpointWithOwner("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-3"), + newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwner("example-txt", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), - newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"), + newEndpointWithOwner("cname-example-txt", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwner("wildcard-txt.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-wildcard-txt.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-foobar-txt.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("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), + newEndpointWithOwner("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), }, UpdateNew: []*endpoint.Endpoint{ newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"), newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-tar-txt.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("multiple-txt.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"), + newEndpointWithOwner("cname-multiple-txt.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{ newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-tar-txt.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("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), + newEndpointWithOwner("cname-multiple-txt.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) { @@ -589,6 +709,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { 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", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "") @@ -612,12 +733,15 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { Create: []*endpoint.Endpoint{ newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, UpdateNew: []*endpoint.Endpoint{}, UpdateOld: []*endpoint.Endpoint{}, @@ -703,6 +827,133 @@ func TestCacheMethods(t *testing.T) { } } +func TestDropPrefix(t *testing.T) { + mapper := newaffixNameMapper("foo-%{record_type}-", "", "") + cnameRecord := "foo-cname-test.example.com" + aRecord := "foo-a-test.example.com" + expectedCnameRecord := "test.example.com" + expectedARecord := "test.example.com" + actualCnameRecord := mapper.dropAffix(cnameRecord) + actualARecord := mapper.dropAffix(aRecord) + assert.Equal(t, expectedCnameRecord, actualCnameRecord) + assert.Equal(t, expectedARecord, actualARecord) +} + +func TestDropSuffix(t *testing.T) { + mapper := newaffixNameMapper("", "-%{record_type}-foo", "") + aRecord := "test-a-foo.example.com" + expectedARecord := "test.example.com" + r := strings.SplitN(aRecord, ".", 2) + actualARecord := mapper.dropAffix(r[0]) + "." + r[1] + assert.Equal(t, expectedARecord, actualARecord) +} + +func TestDropRecordType(t *testing.T) { + r := "ns-zone.example.com" + expectedRecord := "zone.example.com" + actualRecord := dropRecordType(r) + assert.Equal(t, expectedRecord, actualRecord) +} + +func TestNewTXTScheme(t *testing.T) { + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + ctxEndpoints := []*endpoint.Endpoint{} + ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) + } + p.ApplyChanges(ctx, &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), + 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", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + }, + }) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "") + + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""), + }, + Delete: []*endpoint.Endpoint{ + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), + }, + UpdateNew: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2"), + }, + UpdateOld: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2"), + }, + } + expected := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"), + newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"), + newEndpointWithOwner("example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + }, + Delete: []*endpoint.Endpoint{ + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), + newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + }, + UpdateNew: []*endpoint.Endpoint{}, + UpdateOld: []*endpoint.Endpoint{}, + } + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + mExpected := map[string][]*endpoint.Endpoint{ + "Create": expected.Create, + "UpdateNew": expected.UpdateNew, + "UpdateOld": expected.UpdateOld, + "Delete": expected.Delete, + } + mGot := map[string][]*endpoint.Endpoint{ + "Create": got.Create, + "UpdateNew": got.UpdateNew, + "UpdateOld": got.UpdateOld, + "Delete": got.Delete, + } + assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) + assert.Equal(t, nil, ctx.Value(provider.RecordsContextKey)) + } + err := r.ApplyChanges(ctx, changes) + require.NoError(t, err) +} + +func TestGenerateTXT(t *testing.T) { + record := newEndpointWithOwner("foo.test-zone.example.org", "new-foo.loadbalancer.com", endpoint.RecordTypeCNAME, "owner") + expectedTXT := []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: endpoint.RecordTypeTXT, + Labels: map[string]string{}, + }, + { + DNSName: "cname-foo.test-zone.example.org", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: endpoint.RecordTypeTXT, + Labels: map[string]string{}, + }, + } + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "") + gotTXT := r.generateTXTRecord(record) + assert.Equal(t, expectedTXT, gotTXT) +} + /** helper methods