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

Support wildcard records - Optional ability to replace the asterisk in generated registry TXT records with another string #1695

Merged
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
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,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)
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement)
case "aws-sd":
r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID)
default:
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ type Config struct {
MetricsAddress string
LogLevel string
TXTCacheInterval time.Duration
TXTWildcardReplacement string
ExoscaleEndpoint string
ExoscaleAPIKey string `secure:"yes"`
ExoscaleAPISecret string `secure:"yes"`
Expand Down Expand Up @@ -210,6 +211,7 @@ var defaultConfig = &Config{
TXTPrefix: "",
TXTSuffix: "",
TXTCacheInterval: 0,
TXTWildcardReplacement: "",
Interval: time.Minute,
Once: false,
DryRun: false,
Expand Down Expand Up @@ -401,6 +403,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix)
app.Flag("txt-wildcard-replacement", "When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional)").Default(defaultConfig.TXTWildcardReplacement).StringVar(&cfg.TXTWildcardReplacement)

// Flags related to the main control loop
app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)
Expand Down
41 changes: 30 additions & 11 deletions registry/txt.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,15 @@ type TXTRegistry struct {
recordsCache []*endpoint.Endpoint
recordsCacheRefreshTime time.Time
cacheInterval time.Duration

// optional string to use to replace the asterisk in wildcard entries - without using this,
// 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
}

// NewTXTRegistry returns new TXTRegistry object
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration) (*TXTRegistry, error) {
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string) (*TXTRegistry, error) {
if ownerID == "" {
return nil, errors.New("owner id cannot be empty")
}
Expand All @@ -52,13 +57,14 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
return nil, errors.New("txt-prefix and txt-suffix are mutual exclusive")
}

mapper := newaffixNameMapper(txtPrefix, txtSuffix)
mapper := newaffixNameMapper(txtPrefix, txtSuffix, txtWildcardReplacement)

return &TXTRegistry{
provider: provider,
ownerID: ownerID,
mapper: mapper,
cacheInterval: cacheInterval,
provider: provider,
ownerID: ownerID,
mapper: mapper,
cacheInterval: cacheInterval,
wildcardReplacement: txtWildcardReplacement,
}, nil
}

Expand Down Expand Up @@ -107,7 +113,13 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
if ep.Labels == nil {
ep.Labels = endpoint.NewLabels()
}
key := fmt.Sprintf("%s::%s", ep.DNSName, ep.SetIdentifier)
dnsNameSplit := strings.Split(ep.DNSName, ".")
// If specified, replace a leading asterisk in the generated txt record name with some other string
if im.wildcardReplacement != "" && dnsNameSplit[0] == "*" {
dnsNameSplit[0] = im.wildcardReplacement
}
dnsName := strings.Join(dnsNameSplit, ".")
key := fmt.Sprintf("%s::%s", dnsName, ep.SetIdentifier)
if labels, ok := labelMap[key]; ok {
for k, v := range labels {
ep.Labels[k] = v
Expand Down Expand Up @@ -211,14 +223,15 @@ type nameMapper interface {
}

type affixNameMapper struct {
prefix string
suffix string
prefix string
suffix string
wildcardReplacement string
}

var _ nameMapper = affixNameMapper{}

func newaffixNameMapper(prefix string, suffix string) affixNameMapper {
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix)}
func newaffixNameMapper(prefix string, suffix string, wildcardReplacement string) affixNameMapper {
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix), wildcardReplacement: strings.ToLower(wildcardReplacement)}
}

func (pr affixNameMapper) toEndpointName(txtDNSName string) string {
Expand All @@ -238,6 +251,12 @@ func (pr affixNameMapper) toEndpointName(txtDNSName string) string {

func (pr affixNameMapper) toTXTName(endpointDNSName string) string {
DNSName := strings.SplitN(endpointDNSName, ".", 2)

// If specified, replace a leading asterisk in the generated txt record name with some other string
if pr.wildcardReplacement != "" && DNSName[0] == "*" {
DNSName[0] = pr.wildcardReplacement
}

if len(DNSName) < 2 {
return pr.prefix + DNSName[0] + pr.suffix
}
Expand Down
41 changes: 27 additions & 14 deletions registry/txt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,28 @@ func TestTXTRegistry(t *testing.T) {

func testTXTRegistryNew(t *testing.T) {
p := inmemory.NewInMemoryProvider()
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour)
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "")
require.Error(t, err)

_, err = NewTXTRegistry(p, "", "txt", "", time.Hour)
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "")
require.Error(t, err)

r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour)
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "")
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, "")
require.NoError(t, err)

_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour)
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "")
require.Error(t, err)

_, ok := r.mapper.(affixNameMapper)
require.True(t, ok)
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, "")
require.NoError(t, err)

_, ok = r.mapper.(affixNameMapper)
Expand Down Expand Up @@ -97,6 +97,8 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("*.wildcard.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("txt.wc.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
},
})
expectedRecords := []*endpoint.Endpoint{
Expand Down Expand Up @@ -169,15 +171,23 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "*.wildcard.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner",
},
},
}

r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc")
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, "")
records, _ = r.Records(ctx)

assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
Expand Down Expand Up @@ -276,13 +286,13 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) {
},
}

r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "")
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, "")
records, _ = r.Records(ctx)

assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
Expand Down Expand Up @@ -357,7 +367,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
}

r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "")
records, _ := r.Records(ctx)

assert.True(t, testutils.SameEndpoints(records, expectedRecords))
Expand Down Expand Up @@ -394,7 +404,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
newEndpointWithOwner("txt.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, "")

changes := &plan.Changes{
Create: []*endpoint.Endpoint{
Expand Down Expand Up @@ -488,13 +498,14 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) {
newEndpointWithOwner("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)
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard")

changes := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
Expand All @@ -517,6 +528,8 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) {
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"),
newEndpointWithOwner("example-txt", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"),
newEndpointWithOwner("wildcard-txt.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
Expand Down Expand Up @@ -578,7 +591,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
newEndpointWithOwner("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, "")

changes := &plan.Changes{
Create: []*endpoint.Endpoint{
Expand Down