Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new flags to allow migration of OwnerID #4823

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ type Controller struct {
ExcludeRecordTypes []string
// MinEventSyncInterval is used as window for batching events
MinEventSyncInterval time.Duration
// migrate txt-owner flag
TXTOwnerMigrate bool
// old txt-owner whitch needed to modify
TXTOwnerOld string
}

// RunOnce runs a single iteration of a reconciliation loop.
Expand Down Expand Up @@ -242,13 +246,15 @@ func (c *Controller) RunOnce(ctx context.Context) error {
registryFilter := c.Registry.GetDomainFilter()

plan := &plan.Plan{
Policies: []plan.Policy{c.Policy},
Current: records,
Desired: endpoints,
DomainFilter: endpoint.MatchAllDomainFilters{c.DomainFilter, registryFilter},
ManagedRecords: c.ManagedRecordTypes,
ExcludeRecords: c.ExcludeRecordTypes,
OwnerID: c.Registry.OwnerID(),
Policies: []plan.Policy{c.Policy},
Current: records,
Desired: endpoints,
DomainFilter: endpoint.MatchAllDomainFilters{c.DomainFilter, registryFilter},
ManagedRecords: c.ManagedRecordTypes,
ExcludeRecords: c.ExcludeRecordTypes,
OwnerID: c.Registry.OwnerID(),
TXTOwnerMigrate: c.TXTOwnerMigrate,
TXTOwnerOld: c.TXTOwnerOld,
}

plan = plan.Calculate()
Expand Down
10 changes: 10 additions & 0 deletions docs/registry/txt.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,13 @@ 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.

## Owner migration

It is possible to migrate the owner of your records when using the TXT registry.
troll-os marked this conversation as resolved.
Show resolved Hide resolved

Simply use the following flags when setting up your instance : `--migrate-txt-owner`, which is the boolean enabling
troll-os marked this conversation as resolved.
Show resolved Hide resolved
the migration feature, `--from-txt-owner=<the owner you want to migrate from>` and set `--txt-owner-id` to
the new value you want it to be.

Take not that if you didn't set `--txt-owner-id` previously, the value automatically set by ExternalDNS is 'default'.
troll-os marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 6 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ func main() {
if cfg.DryRun {
log.Info("running in dry-run mode. No changes to DNS records will be made.")
}
if cfg.TXTOwnerMigrate {
log.Infof("modify the previous txt-owner (%s) to the current txt-owner (%s)", cfg.TXTOwnerOld, cfg.TXTOwnerID)
}

ll, err := log.ParseLevel(cfg.LogLevel)
if err != nil {
Expand Down Expand Up @@ -387,7 +390,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, 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.TXTOwnerMigrate, cfg.TXTOwnerOld)
case "aws-sd":
r, err = registry.NewAWSSDRegistry(p, cfg.TXTOwnerID)
default:
Expand All @@ -412,6 +415,8 @@ func main() {
ManagedRecordTypes: cfg.ManagedDNSRecordTypes,
ExcludeRecordTypes: cfg.ExcludeDNSRecordTypes,
MinEventSyncInterval: cfg.MinEventSyncInterval,
TXTOwnerMigrate: cfg.TXTOwnerMigrate,
TXTOwnerOld: cfg.TXTOwnerOld,
}

if cfg.Once {
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ type Config struct {
Policy string
Registry string
TXTOwnerID string
TXTOwnerOld string
TXTOwnerMigrate bool
TXTPrefix string
TXTSuffix string
TXTEncryptEnabled bool
Expand Down Expand Up @@ -290,6 +292,8 @@ var defaultConfig = &Config{
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
TXTOwnerOld: "default",
TXTOwnerMigrate: false,
TXTPrefix: "",
TXTSuffix: "",
TXTCacheInterval: 0,
Expand Down Expand Up @@ -572,6 +576,8 @@ func (cfg *Config) ParseFlags(args []string) error {
// Flags related to the registry
app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, dynamodb, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "dynamodb", "aws-sd")
app.Flag("txt-owner-id", "When using the TXT or DynamoDB registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
app.Flag("from-txt-owner", "Old txt-owner-id that needs to be overwritten (default: default)").StringVar(&cfg.TXTOwnerOld)
app.Flag("migrate-txt-owner", "When enabled, modify the previous txt-owner to the current txt-owner (default: disabled)").BoolVar(&cfg.TXTOwnerMigrate)
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Could contain record type template like '%{record_type}-prefix-'. Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Could contain record type template like '-%{record_type}-suffix'. Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix)
app.Flag("txt-wildcard-replacement", "When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional)").Default(defaultConfig.TXTWildcardReplacement).StringVar(&cfg.TXTWildcardReplacement)
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/externaldns/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ var (
Policy: "upsert-only",
Registry: "noop",
TXTOwnerID: "owner-1",
TXTOwnerOld: "owner-old",
TXTOwnerMigrate: true,
TXTPrefix: "associated-txt-record",
TXTCacheInterval: 12 * time.Hour,
Interval: 10 * time.Minute,
Expand Down Expand Up @@ -333,6 +335,8 @@ func TestParseFlags(t *testing.T) {
"--policy=upsert-only",
"--registry=noop",
"--txt-owner-id=owner-1",
"--migrate-txt-owner",
"--from-txt-owner=owner-old",
"--txt-prefix=associated-txt-record",
"--txt-cache-interval=12h",
"--dynamodb-table=custom-table",
Expand Down Expand Up @@ -445,6 +449,8 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
"EXTERNAL_DNS_FROM_TXT_OWNER": "owner-old",
"EXTERNAL_DNS_MIGRATE_TXT_OWNER": "true",
"EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record",
"EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h",
"EXTERNAL_DNS_INTERVAL": "10m",
Expand Down
28 changes: 27 additions & 1 deletion plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ type Plan struct {
ExcludeRecords []string
// OwnerID of records to manage
OwnerID string
// modify owner flag
TXTOwnerMigrate bool
// old txt-owner whitch needed to modify
TXTOwnerOld string
}

// Changes holds lists of actions to be executed by dns providers
Expand Down Expand Up @@ -179,6 +183,7 @@ func (p *Plan) Calculate() *Plan {
}

changes := &Changes{}
var hasMig bool

for key, row := range t.rows {
// dns name not taken
Expand Down Expand Up @@ -221,6 +226,23 @@ func (p *Plan) Calculate() *Plan {
if records.current != nil && len(records.candidates) > 0 {
update := t.resolver.ResolveUpdate(records.current, records.candidates)

// Change the specified old txt-owner to the new txt-owner (if TXTOwnerMigrate==true and set the from-txt-owner)
if p.TXTOwnerMigrate && records.current.Labels[endpoint.OwnerLabelKey] == p.TXTOwnerOld {
hasMig = true
oldRecord := records.current
changes.UpdateOld = append(changes.UpdateOld, oldRecord)
recordToMigrate := update
recordToMigrate.Labels[endpoint.OwnerLabelKey] = p.OwnerID
log.WithFields(log.Fields{
"previousOwner": oldRecord.Labels[endpoint.OwnerLabelKey],
"newOwner": p.OwnerID,
"dnsName": records.current.DNSName,
"recordType": records.current.RecordType,
}).Info("Found record to migrate")
changes.UpdateNew = append(changes.UpdateNew, update)
continue
}

if shouldUpdateTTL(update, records.current) || targetChanged(update, records.current) || p.shouldUpdateProviderSpecific(update, records.current) {
inheritOwner(records.current, update)
changes.UpdateNew = append(changes.UpdateNew, update)
Expand Down Expand Up @@ -253,7 +275,11 @@ func (p *Plan) Calculate() *Plan {
if p.OwnerID != "" {
changes.Delete = endpoint.FilterEndpointsByOwnerID(p.OwnerID, changes.Delete)
changes.Delete = endpoint.RemoveDuplicates(changes.Delete)
changes.UpdateOld = endpoint.FilterEndpointsByOwnerID(p.OwnerID, changes.UpdateOld)
// When running a migration we don't want to filter out endpoint duplicates on ownerId value so that old records
// are deleted correctly
if !hasMig {
changes.UpdateOld = endpoint.FilterEndpointsByOwnerID(p.OwnerID, changes.UpdateOld)
}
changes.UpdateNew = endpoint.FilterEndpointsByOwnerID(p.OwnerID, changes.UpdateNew)
}

Expand Down
45 changes: 45 additions & 0 deletions plan/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type PlanTestSuite struct {
domainFilterFiltered2 *endpoint.Endpoint
domainFilterFiltered3 *endpoint.Endpoint
domainFilterExcluded *endpoint.Endpoint
migrateTXTOwnerOld *endpoint.Endpoint
migrateTXTOwnerNew *endpoint.Endpoint
}

func (suite *PlanTestSuite) SetupTest() {
Expand Down Expand Up @@ -243,6 +245,24 @@ func (suite *PlanTestSuite) SetupTest() {
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: "A",
}
suite.migrateTXTOwnerOld = &endpoint.Endpoint{
DNSName: "migrated.domain.tld",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-v1",
endpoint.OwnerLabelKey: "owner-old",
},
}
suite.migrateTXTOwnerNew = &endpoint.Endpoint{
DNSName: "migrated.domain.tld",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-v1",
endpoint.OwnerLabelKey: "owner",
},
}
}

func (suite *PlanTestSuite) TestSyncFirstRound() {
Expand Down Expand Up @@ -742,6 +762,31 @@ func (suite *PlanTestSuite) TestIgnoreTargetCase() {
validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestMigrateTXTRecord() {
current := []*endpoint.Endpoint{suite.migrateTXTOwnerOld}
desired := []*endpoint.Endpoint{suite.migrateTXTOwnerNew}
expectedCreate := []*endpoint.Endpoint{}
expectedUpdateOld := []*endpoint.Endpoint{suite.migrateTXTOwnerOld}
expectedUpdateNew := []*endpoint.Endpoint{suite.migrateTXTOwnerNew}
expectedDelete := []*endpoint.Endpoint{}

p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
TXTOwnerMigrate: true,
TXTOwnerOld: "owner-old",
OwnerID: "owner",
ManagedRecords: []string{endpoint.RecordTypeA},
}

changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Create, expectedCreate)
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
validateEntries(suite.T(), changes.Delete, expectedDelete)
}

func (suite *PlanTestSuite) TestRemoveEndpoint() {
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A}
desired := []*endpoint.Endpoint{suite.fooV1Cname}
Expand Down
11 changes: 10 additions & 1 deletion provider/ovh/ovh.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -192,10 +193,11 @@ func (p *OVHProvider) change(change ovhChange) error {
log.Debugf("OVH: Add an entry to %s", change.String())
return p.client.Post(fmt.Sprintf("/domain/zone/%s/record", change.Zone), change.ovhRecordFields, nil)
case ovhDelete:
log.Debugf("OVH: Deleting entry %s", change.String())
if change.ID == 0 {
log.Debugf("OVH: No ID found when deleting entry: %s", change.String())
return ErrRecordToMutateNotFound
}
log.Debugf("OVH: Delete an entry to %s", change.String())
return p.client.Delete(fmt.Sprintf("/domain/zone/%s/record/%d", change.Zone, change.ID), nil)
}
return nil
Expand Down Expand Up @@ -313,12 +315,19 @@ func (p *OVHProvider) record(zone *string, id uint64, records chan<- ovhRecord)
record := ovhRecord{}

log.Debugf("OVH: Getting record %d for %s", id, *zone)
recordTargetPattern := regexp.MustCompile(`^\".*\"$`)

p.apiRateLimiter.Take()
if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record/%d", *zone, id), &record); err != nil {
return err
}
if provider.SupportedRecordType(record.FieldType) {
// For some reason, the OVH API record targets are sometimes not serialized and formatted as
// expected by ExternalDNS, so we make sure that the record target is evaluated properly to avoid
// an error on changes that require an ID
if !recordTargetPattern.MatchString(record.Target) && record.FieldType == endpoint.RecordTypeTXT {
record.Target = fmt.Sprintf("\"%s\"", record.Target)
}
log.Debugf("OVH: Record %d for %s is %+v", id, *zone, record)
records <- record
}
Expand Down
12 changes: 11 additions & 1 deletion registry/txt.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,14 @@ type TXTRegistry struct {
// encrypt text records
txtEncryptEnabled bool
txtEncryptAESKey []byte

//Handle Owner ID migration
isMigrationEnabled bool
oldOwnerID string
}

// NewTXTRegistry returns new TXTRegistry object
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string, managedRecordTypes, excludeRecordTypes []string, txtEncryptEnabled bool, txtEncryptAESKey []byte) (*TXTRegistry, error) {
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string, managedRecordTypes, excludeRecordTypes []string, txtEncryptEnabled bool, txtEncryptAESKey []byte, isMigrationEnabled bool, oldOwnerID string) (*TXTRegistry, error) {
if ownerID == "" {
return nil, errors.New("owner id cannot be empty")
}
Expand Down Expand Up @@ -88,6 +92,8 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
excludeRecordTypes: excludeRecordTypes,
txtEncryptEnabled: txtEncryptEnabled,
txtEncryptAESKey: txtEncryptAESKey,
isMigrationEnabled: isMigrationEnabled,
oldOwnerID: oldOwnerID,
}, nil
}

Expand Down Expand Up @@ -250,6 +256,10 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
UpdateOld: endpoint.FilterEndpointsByOwnerID(im.ownerID, changes.UpdateOld),
Delete: endpoint.FilterEndpointsByOwnerID(im.ownerID, changes.Delete),
}
if im.isMigrationEnabled {
filteredChanges.UpdateOld = append(filteredChanges.UpdateOld, endpoint.FilterEndpointsByOwnerID(im.oldOwnerID, changes.UpdateOld)...)
filteredChanges.Delete = append(filteredChanges.Delete, endpoint.FilterEndpointsByOwnerID(im.oldOwnerID, changes.Delete)...)
}
Comment on lines +259 to +262
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the purpose of those lines?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to apply the changes which should include the deletion of the old entries in the registry, otherwise we're left we unmanaged and unwanted records

for _, r := range filteredChanges.Create {
if r.Labels == nil {
r.Labels = make(map[string]string)
Expand Down
Loading