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>
This commit is contained in:
troll-os 2025-09-29 08:20:19 +00:00 committed by GitHub
parent 7fd53667de
commit 41b1154cdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 272 additions and 43 deletions

View File

@ -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()

View File

@ -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:

View File

@ -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) |

View File

@ -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.

View File

@ -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)

View File

@ -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",

View File

@ -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{}

View File

@ -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))
}

View File

@ -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)

View File

@ -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),

View File

@ -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)
}