From 41b1154cdda731d2271894436573dfdaf3d13c99 Mon Sep 17 00:00:00 2001 From: troll-os <39793416+troll-os@users.noreply.github.com> Date: Mon, 29 Sep 2025 08:20:19 +0000 Subject: [PATCH] feat: add new flags to allow migration of OwnerID (#4823) * Reintroduce base config for txt owner migration # Conflicts: # controller/execute.go # registry/txt.go # Conflicts: # pkg/apis/externaldns/types.go * Added label update logic and fixed existing tests * Fixed existing declaration in tests, re introduced tests for new flag, regened flags.md from make * Fixed tests logic and target expression evaluation, fixed update of label in the TXT registry process * Set Old owner id var down the plan to calculate changes correctly * Lint fixes * (wip) Code cleaning and test coverage * Simplified label overwriting on migration and implem tests for coverage * Fix tests * Update txt registry doc * Fix rebase issues in txt test * Update docs/registry/txt.md Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> * Update docs/registry/txt.md Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> * Update docs/registry/txt.md Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> * Fix label overriding in TXT record generation when migration is enabled * Make linter happy * Regen flags, fix types tests after types updates * Removed boolean flag that enabled migration, evaluate only against old owner flag instead --------- Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- controller/controller.go | 3 + controller/execute.go | 3 +- docs/flags.md | 1 + docs/registry/txt.md | 88 +++++++++++++++++ pkg/apis/externaldns/types.go | 3 + pkg/apis/externaldns/types_test.go | 4 + plan/plan.go | 9 +- plan/plan_test.go | 26 +++++ registry/txt.go | 17 +++- registry/txt_encryption_test.go | 10 +- registry/txt_test.go | 151 ++++++++++++++++++++++------- 11 files changed, 272 insertions(+), 43 deletions(-) diff --git a/controller/controller.go b/controller/controller.go index bdbddfb64..f0a3fbde8 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -182,6 +182,8 @@ type Controller struct { ExcludeRecordTypes []string // MinEventSyncInterval is used as a window for batching events MinEventSyncInterval time.Duration + // Old txt-owner value we need to migrate from + TXTOwnerOld string } // RunOnce runs a single iteration of a reconciliation loop. @@ -236,6 +238,7 @@ func (c *Controller) RunOnce(ctx context.Context) error { ManagedRecords: c.ManagedRecordTypes, ExcludeRecords: c.ExcludeRecordTypes, OwnerID: c.Registry.OwnerID(), + OldOwnerId: c.TXTOwnerOld, } plan = plan.Calculate() diff --git a/controller/execute.go b/controller/execute.go index 5782db8da..d2140c8d4 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -382,6 +382,7 @@ func buildController( ManagedRecordTypes: cfg.ManagedDNSRecordTypes, ExcludeRecordTypes: cfg.ExcludeDNSRecordTypes, MinEventSyncInterval: cfg.MinEventSyncInterval, + TXTOwnerOld: cfg.TXTOwnerOld, EventEmitter: eventEmitter, }, nil } @@ -418,7 +419,7 @@ func selectRegistry(cfg *externaldns.Config, p provider.Provider) (registry.Regi case "noop": r, err = registry.NewNoopRegistry(p) case "txt": - r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey)) + r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey), cfg.TXTOwnerOld) case "aws-sd": r, err = registry.NewAWSSDRegistry(p, cfg.TXTOwnerID) default: diff --git a/docs/flags.md b/docs/flags.md index 61ce89340..f03026c99 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -164,6 +164,7 @@ | `--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) | | `--[no-]txt-encrypt-enabled` | When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled) | | `--txt-encrypt-aes-key=""` | When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true) | +| `--migrate-from-txt-owner=""` | Old txt-owner-id that needs to be overwritten (default: default) | | `--dynamodb-region=""` | When using the DynamoDB registry, the AWS region of the DynamoDB table (optional) | | `--dynamodb-table="external-dns"` | When using the DynamoDB registry, the name of the DynamoDB table (default: "external-dns") | | `--txt-cache-interval=0s` | The interval between cache synchronizations in duration format (default: disabled) | diff --git a/docs/registry/txt.md b/docs/registry/txt.md index 5c2b9857a..46ca34cbc 100644 --- a/docs/registry/txt.md +++ b/docs/registry/txt.md @@ -205,3 +205,91 @@ The TXT registry can optionally cache DNS records read from the provider. This c rate limits imposed by the provider. Caching is enabled by specifying a cache duration with the `--txt-cache-interval` flag. + +## OwnerID migration + +The owner ID of the TXT records managed by external-dns instance can be updated. + +When `--migrate-from-txt-owner` is set, it will enable the migration checks +in the run loop using `--txt-owner-id=new-owner-id` and the value you defined for this flag. + +If you want to test the outputs of a migration beforehand, you can use the `--dry-run` flag +along with `--migrate-from-txt-owner`. + +Example, if you had a standard deployment like so: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + replicas: 1 + selector: + matchLabels: + app: external-dns + strategy: + type: Recreate + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + image: registry.k8s.io/external-dns/external-dns:v0.19.0 + imagePullPolicy: Always + args: + - "--txt-prefix=%{record_type}-" + - "--txt-cache-interval=2m" + - "--log-level=debug" + - "--log-format=text" + - "--txt-owner-id=old-owner" + - "--policy=sync" + - "--provider=some-provider" + - "--registry=txt" + - "--interval=1m" + - "--source=ingress" +``` + +You can update your deployment to migrate like so : + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + replicas: 1 + selector: + matchLabels: + app: external-dns + strategy: + type: Recreate + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + imagePullPolicy: Always + image: registry.k8s.io/external-dns/external-dns:v0.19.0 + args: + - "--txt-prefix=%{record_type}-" + - "--txt-cache-interval=2m" + - "--log-level=debug" + - "--log-format=text" + - "--txt-owner-id=new-owner" + - "--migrate-from-txt-owner=old-owner" + - "--policy=sync" + - "--provider=some-provider" + - "--registry=txt" + - "--interval=1m" + - "--source=ingress" +``` + +If you didn't set the owner ID, the value set by external-dns is `default`. You can set the +`--migrate-from-txt-owner` flag to `default` to migrate the associated records. diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index d9f8e6efb..c2fff68b3 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -142,6 +142,7 @@ type Config struct { Policy string Registry string TXTOwnerID string + TXTOwnerOld string TXTPrefix string TXTSuffix string TXTEncryptEnabled bool @@ -371,6 +372,7 @@ var defaultConfig = &Config{ TXTEncryptAESKey: "", TXTEncryptEnabled: false, TXTOwnerID: "default", + TXTOwnerOld: "", TXTPrefix: "", TXTSuffix: "", TXTWildcardReplacement: "", @@ -782,6 +784,7 @@ func bindFlags(b FlagBinder, cfg *Config) { b.StringVar("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)", defaultConfig.TXTWildcardReplacement, &cfg.TXTWildcardReplacement) b.BoolVar("txt-encrypt-enabled", "When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled)", defaultConfig.TXTEncryptEnabled, &cfg.TXTEncryptEnabled) b.StringVar("txt-encrypt-aes-key", "When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true)", defaultConfig.TXTEncryptAESKey, &cfg.TXTEncryptAESKey) + b.StringVar("migrate-from-txt-owner", "Old txt-owner-id that needs to be overwritten (default: default)", defaultConfig.TXTOwnerOld, &cfg.TXTOwnerOld) b.StringVar("dynamodb-region", "When using the DynamoDB registry, the AWS region of the DynamoDB table (optional)", cfg.AWSDynamoDBRegion, &cfg.AWSDynamoDBRegion) b.StringVar("dynamodb-table", "When using the DynamoDB registry, the name of the DynamoDB table (default: \"external-dns\")", defaultConfig.AWSDynamoDBTable, &cfg.AWSDynamoDBTable) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 9a35cd3eb..5a6d7f96c 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -104,6 +104,7 @@ var ( Policy: "sync", Registry: "txt", TXTOwnerID: "default", + TXTOwnerOld: "", TXTPrefix: "", TXTCacheInterval: 0, Interval: time.Minute, @@ -217,6 +218,7 @@ var ( Registry: "noop", TXTOwnerID: "owner-1", TXTPrefix: "associated-txt-record", + TXTOwnerOld: "old-owner", TXTCacheInterval: 12 * time.Hour, Interval: 10 * time.Minute, MinEventSyncInterval: 50 * time.Second, @@ -360,6 +362,7 @@ func TestParseFlags(t *testing.T) { "--policy=upsert-only", "--registry=noop", "--txt-owner-id=owner-1", + "--migrate-from-txt-owner=old-owner", "--txt-prefix=associated-txt-record", "--txt-cache-interval=12h", "--dynamodb-table=custom-table", @@ -482,6 +485,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_REGISTRY": "noop", "EXTERNAL_DNS_TXT_OWNER_ID": "owner-1", "EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record", + "EXTERNAL_DNS_MIGRATE_FROM_TXT_OWNER": "old-owner", "EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h", "EXTERNAL_DNS_TXT_NEW_FORMAT_ONLY": "1", "EXTERNAL_DNS_INTERVAL": "10m", diff --git a/plan/plan.go b/plan/plan.go index 299623776..325a3d80f 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -52,6 +52,8 @@ type Plan struct { ExcludeRecords []string // OwnerID of records to manage OwnerID string + // Old owner ID we migrate from + OldOwnerId string } // Changes holds lists of actions to be executed by dns providers @@ -224,7 +226,8 @@ func (p *Plan) Calculate() *Plan { if records.current != nil && len(records.candidates) > 0 { update := t.resolver.ResolveUpdate(records.current, records.candidates) - if shouldUpdateTTL(update, records.current) || targetChanged(update, records.current) || p.shouldUpdateProviderSpecific(update, records.current) { + if shouldUpdateTTL(update, records.current) || targetChanged(update, records.current) || p.shouldUpdateProviderSpecific(update, records.current) || + p.isOldOwnerIdSetAndDifferent(records.current) { inheritOwner(records.current, update) changes.UpdateNew = append(changes.UpdateNew, update) changes.UpdateOld = append(changes.UpdateOld, records.current) @@ -276,6 +279,10 @@ func (p *Plan) Calculate() *Plan { return plan } +func (p *Plan) isOldOwnerIdSetAndDifferent(current *endpoint.Endpoint) bool { + return len(p.OldOwnerId) != 0 && current.Labels[endpoint.OwnerLabelKey] != p.OldOwnerId +} + func inheritOwner(from, to *endpoint.Endpoint) { if to.Labels == nil { to.Labels = map[string]string{} diff --git a/plan/plan_test.go b/plan/plan_test.go index 10ce5a992..acce96b27 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -1017,6 +1017,32 @@ func (suite *PlanTestSuite) TestDualStackToSingleStack() { validateEntries(suite.T(), changes.UpdateNew, expectNoChanges) } +func (suite *PlanTestSuite) TestRecordOwnerIdMigration() { + suite.fooA5.Labels[endpoint.OwnerLabelKey] = "bar" + current := []*endpoint.Endpoint{suite.fooA5} + desired := []*endpoint.Endpoint{suite.fooA5} + expectedCreate := []*endpoint.Endpoint{} + expectedUpdateOld := []*endpoint.Endpoint{suite.fooA5} + expectedUpdateNew := []*endpoint.Endpoint{suite.fooA5} + expectedDelete := []*endpoint.Endpoint{} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + OwnerID: suite.fooA5.Labels[endpoint.OwnerLabelKey], + OldOwnerId: "foo", + } + + 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) { suite.Run(t, new(PlanTestSuite)) } diff --git a/registry/txt.go b/registry/txt.go index b428cfe9a..d81419218 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -60,6 +60,9 @@ type TXTRegistry struct { txtEncryptEnabled bool txtEncryptAESKey []byte + //Handle Owner ID migration + oldOwnerID string + // existingTXTs is the TXT records that already exist in the zone so that // ApplyChanges() can skip re-creating them. See the struct below for details. existingTXTs *existingTXTs @@ -114,7 +117,8 @@ func (im *existingTXTs) reset() { func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string, managedRecordTypes, excludeRecordTypes []string, - txtEncryptEnabled bool, txtEncryptAESKey []byte) (*TXTRegistry, error) { + txtEncryptEnabled bool, txtEncryptAESKey []byte, + oldOwnerID string) (*TXTRegistry, error) { if ownerID == "" { return nil, errors.New("owner id cannot be empty") } @@ -148,6 +152,7 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st excludeRecordTypes: excludeRecordTypes, txtEncryptEnabled: txtEncryptEnabled, txtEncryptAESKey: txtEncryptAESKey, + oldOwnerID: oldOwnerID, existingTXTs: newExistingTXTs(), }, nil } @@ -252,6 +257,10 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error } } + if im.oldOwnerID != "" && ep.Labels[endpoint.OwnerLabelKey] == im.oldOwnerID { + ep.Labels[endpoint.OwnerLabelKey] = im.ownerID + } + // 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 { @@ -292,6 +301,11 @@ func (im *TXTRegistry) generateTXTRecordWithFilter(r *endpoint.Endpoint, filter if isAlias, found := r.GetProviderSpecificProperty("alias"); found && isAlias == "true" && recordType == endpoint.RecordTypeA { recordType = endpoint.RecordTypeCNAME } + + if im.oldOwnerID != "" && r.Labels[endpoint.OwnerLabelKey] == im.oldOwnerID { + r.Labels[endpoint.OwnerLabelKey] = im.ownerID + } + txtNew := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName, recordType), endpoint.RecordTypeTXT, r.Labels.Serialize(true, im.txtEncryptEnabled, im.txtEncryptAESKey)) if txtNew != nil { txtNew.WithSetIdentifier(r.SetIdentifier) @@ -315,6 +329,7 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) UpdateOld: endpoint.FilterEndpointsByOwnerID(im.ownerID, changes.UpdateOld), Delete: endpoint.FilterEndpointsByOwnerID(im.ownerID, changes.Delete), } + for _, r := range filteredChanges.Create { if r.Labels == nil { r.Labels = make(map[string]string) diff --git a/registry/txt_encryption_test.go b/registry/txt_encryption_test.go index 1b7f9f5c9..65cee6248 100644 --- a/registry/txt_encryption_test.go +++ b/registry/txt_encryption_test.go @@ -61,7 +61,7 @@ func TestNewTXTRegistryEncryptionConfig(t *testing.T) { }, } for _, test := range tests { - actual, err := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, test.encEnabled, test.aesKeyRaw) + actual, err := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, test.encEnabled, test.aesKeyRaw, "") if test.errorExpected { require.Error(t, err) } else { @@ -107,7 +107,7 @@ func TestGenerateTXTGenerateTextRecordEncryptionWihDecryption(t *testing.T) { for _, k := range withEncryptionKeys { t.Run(fmt.Sprintf("key '%s' with decrypted result '%s'", k, test.decrypted), func(t *testing.T) { key := []byte(k) - r, err := NewTXTRegistry(p, "", "", "owner", time.Minute, "", []string{}, []string{}, true, key) + r, err := NewTXTRegistry(p, "", "", "owner", time.Minute, "", []string{}, []string{}, true, key, "") assert.NoError(t, err, "Error creating TXT registry") txtRecords := r.generateTXTRecord(test.record) assert.Len(t, txtRecords, len(test.record.Targets)) @@ -144,7 +144,7 @@ func TestApplyRecordsWithEncryption(t *testing.T) { key := []byte("ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=") - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, key) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, key, "") _ = r.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -202,7 +202,7 @@ func TestApplyRecordsWithEncryptionKeyChanged(t *testing.T) { } for _, key := range withEncryptionKeys { - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key)) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key), "") _ = r.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"), @@ -232,7 +232,7 @@ func TestApplyRecordsOnEncryptionKeyChangeWithKeyIdLabel(t *testing.T) { } for i, key := range withEncryptionKeys { - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key)) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key), "") keyId := fmt.Sprintf("key-id-%d", i) changes := []*endpoint.Endpoint{ newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "", keyId), diff --git a/registry/txt_test.go b/registry/txt_test.go index 05cdd030d..b5146aa49 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -49,20 +49,20 @@ func TestTXTRegistry(t *testing.T) { func testTXTRegistryNew(t *testing.T) { p := inmemory.NewInMemoryProvider() - _, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}, []string{}, false, nil) + _, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}, []string{}, false, nil, "") require.Error(t, err) - _, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "", []string{}, []string{}, false, nil) + _, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "", []string{}, []string{}, false, nil, "") require.Error(t, err) - r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") require.NoError(t, err) assert.Equal(t, p, r.provider) - r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") require.NoError(t, err) - _, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil) + _, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") require.Error(t, err) _, ok := r.mapper.(affixNameMapper) @@ -71,16 +71,16 @@ func testTXTRegistryNew(t *testing.T) { assert.Equal(t, p, r.provider) aesKey := []byte(";k&l)nUC/33:{?d{3)54+,AD?]SX%yh^") - _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") require.NoError(t, err) - _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, aesKey) + _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, aesKey, "") require.NoError(t, err) - _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, nil) + _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, nil, "") require.Error(t, err) - r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, aesKey) + r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, aesKey, "") require.NoError(t, err) _, ok = r.mapper.(affixNameMapper) @@ -228,13 +228,13 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, "") records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) // Ensure prefix is case-insensitive - r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) + r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, "") records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) @@ -363,13 +363,13 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) // Ensure prefix is case-insensitive - r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) @@ -490,7 +490,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) @@ -527,12 +527,12 @@ func testTXTRegistryRecordsPrefixedTemplated(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "txt-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "txt-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, "") records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) - r, _ = NewTXTRegistry(p, "TxT-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) + r, _ = NewTXTRegistry(p, "TxT-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, "") records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) @@ -569,12 +569,12 @@ func testTXTRegistryRecordsSuffixedTemplated(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "", "txt%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "txt%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, "") records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) - r, _ = NewTXTRegistry(p, "", "TxT%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) + r, _ = NewTXTRegistry(p, "", "TxT%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, "") records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) @@ -617,7 +617,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, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -698,7 +698,7 @@ func testTXTRegistryApplyChangesWithTemplatedPrefix(t *testing.T) { p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{}, }) - r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") 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"), @@ -741,7 +741,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, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "-%{record_type}suffix", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") 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"), @@ -806,7 +806,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", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard", []string{}, []string{}, false, nil, "") changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -900,7 +900,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, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -1058,7 +1058,7 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, []string{}, false, nil, "") records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) @@ -1168,7 +1168,7 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS, endpoint.RecordTypeTXT}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS, endpoint.RecordTypeTXT}, []string{}, false, nil, "") records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) @@ -1463,7 +1463,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, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -1528,11 +1528,46 @@ func TestGenerateTXT(t *testing.T) { } p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") gotTXT := r.generateTXTRecord(record) assert.Equal(t, expectedTXT, gotTXT) } +func TestGenerateTXTWithMigration(t *testing.T) { + record := newEndpointWithOwner("foo.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, "owner") + expectedTXTBeforeMigration := []*endpoint.Endpoint{ + { + DNSName: "a-foo.test-zone.example.org", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: endpoint.RecordTypeTXT, + Labels: map[string]string{ + endpoint.OwnedRecordLabelKey: "foo.test-zone.example.org", + }, + }, + } + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") + gotTXTBeforeMigration := r.generateTXTRecord(record) + assert.Equal(t, expectedTXTBeforeMigration, gotTXTBeforeMigration) + + expectedTXTAfterMigration := []*endpoint.Endpoint{ + { + DNSName: "a-foo.test-zone.example.org", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=foobar\""}, + RecordType: endpoint.RecordTypeTXT, + Labels: map[string]string{ + endpoint.OwnedRecordLabelKey: "foo.test-zone.example.org", + }, + }, + } + + rMigrated, _ := NewTXTRegistry(p, "", "", "foobar", time.Hour, "", []string{}, []string{}, false, nil, "owner") + gotTXTAfterMigration := rMigrated.generateTXTRecord(record) + assert.Equal(t, expectedTXTAfterMigration, gotTXTAfterMigration) + +} + func TestGenerateTXTForAAAA(t *testing.T) { record := newEndpointWithOwner("foo.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, "owner") expectedTXT := []*endpoint.Endpoint{ @@ -1547,7 +1582,7 @@ func TestGenerateTXTForAAAA(t *testing.T) { } p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") gotTXT := r.generateTXTRecord(record) assert.Equal(t, expectedTXT, gotTXT) } @@ -1564,7 +1599,7 @@ func TestFailGenerateTXT(t *testing.T) { expectedTXT := []*endpoint.Endpoint{} p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") gotTXT := r.generateTXTRecord(cnameRecord) assert.Equal(t, expectedTXT, gotTXT) } @@ -1582,7 +1617,7 @@ func TestTXTRegistryApplyChangesEncrypt(t *testing.T) { }, }) - r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte("12345678901234567890123456789012")) + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte("12345678901234567890123456789012"), "") records, _ := r.Records(ctx) changes := &plan.Changes{ Delete: records, @@ -1628,7 +1663,7 @@ func TestMultiClusterDifferentRecordTypeOwnership(t *testing.T) { }, }) - r, _ := NewTXTRegistry(p, "_owner.", "", "bar", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "_owner.", "", "bar", time.Hour, "", []string{}, []string{}, false, nil, "") records, _ := r.Records(ctx) // new cluster has same ingress host as other cluster and uses CNAME ingress address @@ -1713,7 +1748,7 @@ func TestGenerateTXTRecordWithNewFormatOnly(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") records := r.generateTXTRecord(tc.endpoint) assert.Len(t, records, tc.expectedRecords, tc.description) @@ -1742,7 +1777,7 @@ func TestApplyChangesWithNewFormatOnly(t *testing.T) { p.CreateZone(testZone) ctx := context.Background() - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -1790,7 +1825,7 @@ func TestTXTRegistryRecordsWithEmptyTargets(t *testing.T) { }, }) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") hook := testutils.LogsUnderTestWithLogLevel(log.ErrorLevel, t) records, err := r.Records(ctx) require.NoError(t, err) @@ -1994,7 +2029,7 @@ func TestTXTRegistryRecreatesMissingRecords(t *testing.T) { // When: Apply changes to recreate missing A records managedRecords := []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeAAAA, endpoint.RecordTypeTXT} - registry, err := NewTXTRegistry(p, "", "", ownerId, time.Hour, "", managedRecords, nil, false, nil) + registry, err := NewTXTRegistry(p, "", "", ownerId, time.Hour, "", managedRecords, nil, false, nil, "") assert.NoError(t, err) expectedRecords := append(existing, expectedCreate...) @@ -2028,3 +2063,49 @@ func TestTXTRegistryRecreatesMissingRecords(t *testing.T) { } } } + +func TestTXTRecordMigration(t *testing.T) { + ctx := context.Background() + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + + r, _ := NewTXTRegistry(p, "%{record_type}-", "", "foo", time.Hour, "", []string{}, []string{}, false, nil, "") + + r.ApplyChanges(ctx, &plan.Changes{ + Create: []*endpoint.Endpoint{ + // records on cluster using A record for ingress address + newEndpointWithOwnerAndLabels("bar.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, "foo", endpoint.Labels{endpoint.OwnerLabelKey: "owner"}), + }, + }) + + createdRecords, _ := r.Records(ctx) + + newTXTRecord := r.generateTXTRecord(createdRecords[0]) + + expectedTXTRecords := []*endpoint.Endpoint{ + { + DNSName: "a-bar.test-zone.example.org", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=foo\""}, + RecordType: endpoint.RecordTypeTXT, + }, + } + + assert.Equal(t, expectedTXTRecords[0].Targets, newTXTRecord[0].Targets) + + r, _ = NewTXTRegistry(p, "%{record_type}-", "", "foobar", time.Hour, "", []string{}, []string{}, false, nil, "foo") + + updatedRecords, _ := r.Records(ctx) + + updatedTXTRecord := r.generateTXTRecord(updatedRecords[0]) + + expectedFinalTXT := []*endpoint.Endpoint{ + { + DNSName: "a-bar.test-zone.example.org", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=foobar\""}, + RecordType: endpoint.RecordTypeTXT, + }, + } + + assert.Equal(t, updatedTXTRecord[0].Targets, expectedFinalTXT[0].Targets) + +}