From 50f196c0db280de7a9b55a8f7fd6b0a607ac5472 Mon Sep 17 00:00:00 2001 From: Andrey Lebedev Date: Tue, 14 Jun 2022 12:23:25 +0200 Subject: [PATCH] Handle the migration to the new TXT format: create missing records --- controller/controller.go | 4 + docs/registry.md | 3 +- main.go | 2 +- plan/plan.go | 11 +- plan/plan_test.go | 34 +++++ registry/aws_sd_registry.go | 5 + registry/noop.go | 5 + registry/registry.go | 1 + registry/txt.go | 44 ++++++- registry/txt_test.go | 243 +++++++++++++++++++++++++++++++++--- 10 files changed, 327 insertions(+), 25 deletions(-) diff --git a/controller/controller.go b/controller/controller.go index d609c089d..689fc9ef9 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -167,6 +167,9 @@ func (c *Controller) RunOnce(ctx context.Context) error { deprecatedRegistryErrors.Inc() return err } + + missingRecords := c.Registry.MissingRecords() + registryEndpointsTotal.Set(float64(len(records))) regARecords := filterARecords(records) registryARecords.Set(float64(len(regARecords))) @@ -189,6 +192,7 @@ func (c *Controller) RunOnce(ctx context.Context) error { Policies: []plan.Policy{c.Policy}, Current: records, Desired: endpoints, + Missing: missingRecords, DomainFilter: endpoint.MatchAllDomainFilters{c.DomainFilter, c.Registry.GetDomainFilter()}, PropertyComparator: c.Registry.PropertyValuesEqual, ManagedRecords: c.ManagedRecordTypes, diff --git a/docs/registry.md b/docs/registry.md index b9bb93904..615ba305a 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -7,7 +7,8 @@ It contains record type it manages, e.g.: 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. +In order to maintain compatibility, both records will be maintained for some time, in order to have downgrade possibility. +The controller will try to create the "new format" TXT records if they are not present to ease the migration from the versions < 0.12.0. Later on, the old format will be dropped and only the new format will be kept (-). diff --git a/main.go b/main.go index 14fd99b1d..2995e408f 100644 --- a/main.go +++ b/main.go @@ -341,7 +341,7 @@ func main() { case "noop": r, err = registry.NewNoopRegistry(p) case "txt": - r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement) + r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes) case "aws-sd": r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID) default: diff --git a/plan/plan.go b/plan/plan.go index 30ee39931..d16676452 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -37,6 +37,8 @@ type Plan struct { Current []*endpoint.Endpoint // List of desired records Desired []*endpoint.Endpoint + // List of missing records to be created, use for the migrations (e.g. old-new TXT format) + Missing []*endpoint.Endpoint // Policies under which the desired changes are calculated Policies []Policy // List of changes necessary to move towards desired state @@ -170,6 +172,11 @@ func (p *Plan) Calculate() *Plan { changes = pol.Apply(changes) } + // Handle the migration of the TXT records created before the new format (introduced in v0.12.0) + if len(p.Missing) > 0 { + changes.Create = append(changes.Create, filterRecordsForPlan(p.Missing, p.DomainFilter, append(p.ManagedRecords, endpoint.RecordTypeTXT))...) + } + plan := &Plan{ Current: p.Current, Desired: p.Desired, @@ -250,7 +257,7 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Do log.Debugf("ignoring record %s that does not match domain filter", record.DNSName) continue } - if isManagedRecord(record.RecordType, managedRecords) { + if IsManagedRecord(record.RecordType, managedRecords) { filtered = append(filtered, record) } } @@ -293,7 +300,7 @@ func CompareBoolean(defaultValue bool, name, current, previous string) bool { return v1 == v2 } -func isManagedRecord(record string, managedRecords []string) bool { +func IsManagedRecord(record string, managedRecords []string) bool { for _, r := range managedRecords { if record == r { return true diff --git a/plan/plan_test.go b/plan/plan_test.go index d402f0eb2..42b0fb295 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -48,6 +48,9 @@ type PlanTestSuite struct { domainFilterFiltered2 *endpoint.Endpoint domainFilterFiltered3 *endpoint.Endpoint domainFilterExcluded *endpoint.Endpoint + domainFilterFilteredTXT1 *endpoint.Endpoint + domainFilterFilteredTXT2 *endpoint.Endpoint + domainFilterExcludedTXT *endpoint.Endpoint } func (suite *PlanTestSuite) SetupTest() { @@ -203,6 +206,21 @@ func (suite *PlanTestSuite) SetupTest() { Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A", } + suite.domainFilterFilteredTXT1 = &endpoint.Endpoint{ + DNSName: "a-foo.domain.tld", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: "TXT", + } + suite.domainFilterFilteredTXT2 = &endpoint.Endpoint{ + DNSName: "cname-bar.domain.tld", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: "TXT", + } + suite.domainFilterExcludedTXT = &endpoint.Endpoint{ + DNSName: "cname-bar.otherdomain.tld", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: "TXT", + } } func (suite *PlanTestSuite) TestSyncFirstRound() { @@ -667,6 +685,22 @@ func (suite *PlanTestSuite) TestDomainFiltersUpdate() { validateEntries(suite.T(), changes.Delete, expectedDelete) } +func (suite *PlanTestSuite) TestMissing() { + + missing := []*endpoint.Endpoint{suite.domainFilterFilteredTXT1, suite.domainFilterFilteredTXT2, suite.domainFilterExcludedTXT} + expectedCreate := []*endpoint.Endpoint{suite.domainFilterFilteredTXT1, suite.domainFilterFilteredTXT2} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Missing: missing, + DomainFilter: endpoint.NewDomainFilter([]string{"domain.tld"}), + ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) +} + func TestPlan(t *testing.T) { suite.Run(t, new(PlanTestSuite)) } diff --git a/registry/aws_sd_registry.go b/registry/aws_sd_registry.go index b084fdbdf..a2ff3350d 100644 --- a/registry/aws_sd_registry.go +++ b/registry/aws_sd_registry.go @@ -67,6 +67,11 @@ func (sdr *AWSSDRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, er return records, nil } +// MissingRecords returns nil because there is no missing records for AWSSD registry +func (sdr *AWSSDRegistry) MissingRecords() []*endpoint.Endpoint { + return nil +} + // ApplyChanges filters out records not owned the External-DNS, additionally it adds the required label // inserted in the AWS SD instance as a CreateID field func (sdr *AWSSDRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { diff --git a/registry/noop.go b/registry/noop.go index 73257730c..d48cd82fe 100644 --- a/registry/noop.go +++ b/registry/noop.go @@ -45,6 +45,11 @@ func (im *NoopRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, erro return im.provider.Records(ctx) } +// MissingRecords returns nil because there is no missing records for Noop registry +func (im *NoopRegistry) MissingRecords() []*endpoint.Endpoint { + return nil +} + // ApplyChanges propagates changes to the dns provider func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { return im.provider.ApplyChanges(ctx, changes) diff --git a/registry/registry.go b/registry/registry.go index d80890b0c..59b299140 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -35,6 +35,7 @@ type Registry interface { PropertyValuesEqual(attribute string, previous string, current string) bool AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint GetDomainFilter() endpoint.DomainFilterInterface + MissingRecords() []*endpoint.Endpoint } //TODO(ideahitme): consider moving this to Plan diff --git a/registry/txt.go b/registry/txt.go index 6ca721f7b..2e0c33a3c 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -47,10 +47,15 @@ type TXTRegistry struct { // registry TXT records corresponding to wildcard records will be invalid (and rejected by most providers), due to // having a '*' appear (not as the first character) - see https://tools.ietf.org/html/rfc1034#section-4.3.3 wildcardReplacement string + + managedRecordTypes []string + + // missingTXTRecords stores TXT records which are missing after the migration to the new format + missingTXTRecords []*endpoint.Endpoint } // NewTXTRegistry returns new TXTRegistry object -func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string) (*TXTRegistry, error) { +func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string, managedRecordTypes []string) (*TXTRegistry, error) { if ownerID == "" { return nil, errors.New("owner id cannot be empty") } @@ -67,6 +72,7 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st mapper: mapper, cacheInterval: cacheInterval, wildcardReplacement: txtWildcardReplacement, + managedRecordTypes: managedRecordTypes, }, nil } @@ -95,8 +101,10 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error } endpoints := []*endpoint.Endpoint{} + missingEndpoints := []*endpoint.Endpoint{} labelMap := map[string]endpoint.Labels{} + txtRecordsMap := map[string]struct{}{} for _, record := range records { if record.RecordType != endpoint.RecordTypeTXT { @@ -117,6 +125,7 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error } key := fmt.Sprintf("%s::%s", im.mapper.toEndpointName(record.DNSName), record.SetIdentifier) labelMap[key] = labels + txtRecordsMap[record.DNSName] = struct{}{} } for _, ep := range endpoints { @@ -135,6 +144,26 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error ep.Labels[k] = v } } + + // Handle the migration of TXT records created before the new format (introduced in v0.12.0). + // The migration is done for the TXT records owned by this instance only. + if len(txtRecordsMap) > 0 && ep.Labels[endpoint.OwnerLabelKey] == im.ownerID { + if plan.IsManagedRecord(ep.RecordType, im.managedRecordTypes) { + // Get desired TXT records and detect the missing ones + desiredTXTs := im.generateTXTRecord(ep) + missingDesiredTXTs := []*endpoint.Endpoint{} + for _, desiredTXT := range desiredTXTs { + if _, exists := txtRecordsMap[desiredTXT.DNSName]; !exists { + missingDesiredTXTs = append(missingDesiredTXTs, desiredTXT) + } + } + if len(desiredTXTs) > len(missingDesiredTXTs) { + // Add missing TXT records only if those are managed (by externaldns) ones. + // The unmanaged record has both of the desired TXT records missing. + missingEndpoints = append(missingEndpoints, missingDesiredTXTs...) + } + } + } } // Update the cache. @@ -143,12 +172,25 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error im.recordsCacheRefreshTime = time.Now() } + im.missingTXTRecords = missingEndpoints + return endpoints, nil } +// MissingRecords returns the TXT record to be created. +// The missing records are collected during the run of Records method. +func (im *TXTRegistry) MissingRecords() []*endpoint.Endpoint { + return im.missingTXTRecords +} + // 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 { + // Missing TXT records are added to the set of changes. + // Obviously, we don't need any other TXT record for them. + if r.RecordType == endpoint.RecordTypeTXT { + return nil + } // 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 diff --git a/registry/txt_test.go b/registry/txt_test.go index b1b2e0672..27d7d66a5 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -41,24 +41,25 @@ func TestTXTRegistry(t *testing.T) { t.Run("TestNewTXTRegistry", testTXTRegistryNew) t.Run("TestRecords", testTXTRegistryRecords) t.Run("TestApplyChanges", testTXTRegistryApplyChanges) + t.Run("TestMissingRecords", testTXTRegistryMissingRecords) } func testTXTRegistryNew(t *testing.T) { p := inmemory.NewInMemoryProvider() - _, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "") + _, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}) require.Error(t, err) - _, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "") + _, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "", []string{}) require.Error(t, err) - r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "") + r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}) require.NoError(t, err) assert.Equal(t, p, r.provider) - r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "") + r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "", []string{}) require.NoError(t, err) - _, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "") + _, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "", []string{}) require.Error(t, err) _, ok := r.mapper.(affixNameMapper) @@ -66,7 +67,7 @@ func testTXTRegistryNew(t *testing.T) { assert.Equal(t, "owner", r.ownerID) assert.Equal(t, p, r.provider) - r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "") + r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) require.NoError(t, err) _, ok = r.mapper.(affixNameMapper) @@ -182,13 +183,13 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc") + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}) records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) // Ensure prefix is case-insensitive - r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "") + r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "", []string{}) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) @@ -287,13 +288,13 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "") + r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}) records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) // Ensure prefix is case-insensitive - r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "") + r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "", []string{}) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) @@ -368,7 +369,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "") + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) @@ -411,7 +412,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { 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, "") + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -500,7 +501,7 @@ func testTXTRegistryApplyChangesWithTemplatedPrefix(t *testing.T) { p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{}, }) - r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "") + r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "", []string{}) 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"), @@ -543,7 +544,7 @@ func testTXTRegistryApplyChangesWithTemplatedSuffix(t *testing.T) { 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, "") + r, _ := NewTXTRegistry(p, "", "-%{record_type}suffix", "owner", time.Hour, "", []string{}) 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"), @@ -608,7 +609,7 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) { 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") + r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard", []string{}) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -712,7 +713,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "") + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -766,6 +767,208 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { require.NoError(t, err) } +func testTXTRegistryMissingRecords(t *testing.T) { + t.Run("No prefix", testTXTRegistryMissingRecordsNoPrefix) + t.Run("With Prefix", testTXTRegistryMissingRecordsWithPrefix) +} + +func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) { + ctx := context.Background() + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + p.ApplyChanges(ctx, &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("oldformat.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("oldformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("oldformat2.test-zone.example.org", "bar.loadbalancer.com", endpoint.RecordTypeA, ""), + newEndpointWithOwner("oldformat2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("newformat.test-zone.example.org", "foobar.nameserver.com", endpoint.RecordTypeNS, ""), + newEndpointWithOwner("ns-newformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("newformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("noheritage.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("oldformat-otherowner.test-zone.example.org", "bar.loadbalancer.com", endpoint.RecordTypeA, ""), + newEndpointWithOwner("oldformat-otherowner.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=otherowner\"", endpoint.RecordTypeTXT, ""), + endpoint.NewEndpoint("unmanaged1.test-zone.example.org", endpoint.RecordTypeA, "unmanaged1.loadbalancer.com"), + endpoint.NewEndpoint("unmanaged2.test-zone.example.org", endpoint.RecordTypeCNAME, "unmanaged2.loadbalancer.com"), + }, + }) + expectedRecords := []*endpoint.Endpoint{ + { + DNSName: "oldformat.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + // owner was added from the TXT record's target + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "oldformat2.test-zone.example.org", + Targets: endpoint.Targets{"bar.loadbalancer.com"}, + RecordType: endpoint.RecordTypeA, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "newformat.test-zone.example.org", + Targets: endpoint.Targets{"foobar.nameserver.com"}, + RecordType: endpoint.RecordTypeNS, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + // Only TXT records with the wrong heritage are returned by Records() + { + DNSName: "noheritage.test-zone.example.org", + Targets: endpoint.Targets{"random"}, + RecordType: endpoint.RecordTypeTXT, + Labels: map[string]string{ + // No owner because it's not external-dns heritage + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "oldformat-otherowner.test-zone.example.org", + Targets: endpoint.Targets{"bar.loadbalancer.com"}, + RecordType: endpoint.RecordTypeA, + Labels: map[string]string{ + // Records() retrieves all the records of the zone, no matter the owner + endpoint.OwnerLabelKey: "otherowner", + }, + }, + { + DNSName: "unmanaged1.test-zone.example.org", + Targets: endpoint.Targets{"unmanaged1.loadbalancer.com"}, + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "unmanaged2.test-zone.example.org", + Targets: endpoint.Targets{"unmanaged2.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + } + + expectedMissingRecords := []*endpoint.Endpoint{ + { + DNSName: "cname-oldformat.test-zone.example.org", + // owner is taken from the source record (A, CNAME, etc.) + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: endpoint.RecordTypeTXT, + }, + { + DNSName: "a-oldformat2.test-zone.example.org", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: endpoint.RecordTypeTXT, + }, + } + + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}) + records, _ := r.Records(ctx) + missingRecords := r.MissingRecords() + + assert.True(t, testutils.SameEndpoints(records, expectedRecords)) + assert.True(t, testutils.SameEndpoints(missingRecords, expectedMissingRecords)) +} + +func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { + ctx := context.Background() + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + p.ApplyChanges(ctx, &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("oldformat.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("txt.oldformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("oldformat2.test-zone.example.org", "bar.loadbalancer.com", endpoint.RecordTypeA, ""), + newEndpointWithOwner("txt.oldformat2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("newformat.test-zone.example.org", "foobar.nameserver.com", endpoint.RecordTypeNS, ""), + newEndpointWithOwner("txt.ns-newformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("txt.newformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("noheritage.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("oldformat-otherowner.test-zone.example.org", "bar.loadbalancer.com", endpoint.RecordTypeA, ""), + newEndpointWithOwner("txt.oldformat-otherowner.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=otherowner\"", endpoint.RecordTypeTXT, ""), + endpoint.NewEndpoint("unmanaged1.test-zone.example.org", endpoint.RecordTypeA, "unmanaged1.loadbalancer.com"), + endpoint.NewEndpoint("unmanaged2.test-zone.example.org", endpoint.RecordTypeCNAME, "unmanaged2.loadbalancer.com"), + }, + }) + expectedRecords := []*endpoint.Endpoint{ + { + DNSName: "oldformat.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + // owner was added from the TXT record's target + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "oldformat2.test-zone.example.org", + Targets: endpoint.Targets{"bar.loadbalancer.com"}, + RecordType: endpoint.RecordTypeA, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "newformat.test-zone.example.org", + Targets: endpoint.Targets{"foobar.nameserver.com"}, + RecordType: endpoint.RecordTypeNS, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "noheritage.test-zone.example.org", + Targets: endpoint.Targets{"random"}, + RecordType: endpoint.RecordTypeTXT, + Labels: map[string]string{ + // No owner because it's not external-dns heritage + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "oldformat-otherowner.test-zone.example.org", + Targets: endpoint.Targets{"bar.loadbalancer.com"}, + RecordType: endpoint.RecordTypeA, + Labels: map[string]string{ + // All the records of the zone are retrieved, no matter the owner + endpoint.OwnerLabelKey: "otherowner", + }, + }, + { + DNSName: "unmanaged1.test-zone.example.org", + Targets: endpoint.Targets{"unmanaged1.loadbalancer.com"}, + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "unmanaged2.test-zone.example.org", + Targets: endpoint.Targets{"unmanaged2.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + } + + expectedMissingRecords := []*endpoint.Endpoint{ + { + DNSName: "txt.cname-oldformat.test-zone.example.org", + // owner is taken from the source record (A, CNAME, etc.) + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: endpoint.RecordTypeTXT, + }, + { + DNSName: "txt.a-oldformat2.test-zone.example.org", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: endpoint.RecordTypeTXT, + }, + } + + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}) + records, _ := r.Records(ctx) + missingRecords := r.MissingRecords() + + assert.True(t, testutils.SameEndpoints(records, expectedRecords)) + assert.True(t, testutils.SameEndpoints(missingRecords, expectedMissingRecords)) +} + func TestCacheMethods(t *testing.T) { cache := []*endpoint.Endpoint{ newEndpointWithOwner("thing.com", "1.2.3.4", "A", "owner"), @@ -877,7 +1080,7 @@ func TestNewTXTScheme(t *testing.T) { newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "") + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -938,18 +1141,18 @@ func TestGenerateTXT(t *testing.T) { DNSName: "foo.test-zone.example.org", Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, RecordType: endpoint.RecordTypeTXT, - Labels: map[string]string{}, + 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{}, + Labels: map[string]string{}, }, } p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "") + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) gotTXT := r.generateTXTRecord(record) assert.Equal(t, expectedTXT, gotTXT) }