/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package registry import ( "context" "reflect" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" log "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/provider/inmemory" ) const ( testZone = "test-zone.example.org" ) func TestTXTRegistry(t *testing.T) { t.Run("TestNewTXTRegistry", testTXTRegistryNew) t.Run("TestRecords", testTXTRegistryRecords) t.Run("TestApplyChanges", testTXTRegistryApplyChanges) t.Run("TestMissingRecords", testTXTRegistryMissingRecords) } func testTXTRegistryNew(t *testing.T) { p := inmemory.NewInMemoryProvider() _, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}, []string{}, false, nil) require.Error(t, err) _, 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) require.NoError(t, err) assert.Equal(t, p, r.provider) 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) require.Error(t, err) _, ok := r.mapper.(affixNameMapper) require.True(t, ok) assert.Equal(t, "owner", r.ownerID) 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) require.NoError(t, err) _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, aesKey) require.NoError(t, err) _, 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) require.NoError(t, err) _, ok = r.mapper.(affixNameMapper) assert.True(t, ok) } func testTXTRegistryRecords(t *testing.T) { t.Run("With prefix", testTXTRegistryRecordsPrefixed) t.Run("With suffix", testTXTRegistryRecordsSuffixed) t.Run("No prefix", testTXTRegistryRecordsNoPrefix) t.Run("With templated prefix", testTXTRegistryRecordsPrefixedTemplated) t.Run("With templated suffix", testTXTRegistryRecordsSuffixedTemplated) } func testTXTRegistryRecordsPrefixed(t *testing.T) { ctx := context.Background() p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwnerAndLabels("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"foo": "somefoo"}), newEndpointWithOwnerAndLabels("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"bar": "somebar"}), newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwnerAndLabels("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"tar": "sometar"}), newEndpointWithOwner("TxT.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), // case-insensitive TXT prefix newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"), 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, ""), newEndpointWithOwner("dualstack.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""), newEndpointWithOwner("txt.dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""), newEndpointWithOwner("txt.aaaa-dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""), newEndpointWithOwner("txt.mx-mail.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ { DNSName: "foo.test-zone.example.org", Targets: endpoint.Targets{"foo.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", "foo": "somefoo", }, }, { DNSName: "bar.test-zone.example.org", Targets: endpoint.Targets{"my-domain.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", "bar": "somebar", }, }, { DNSName: "txt.bar.test-zone.example.org", Targets: endpoint.Targets{"baz.test-zone.example.org"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "qux.test-zone.example.org", Targets: endpoint.Targets{"random"}, RecordType: endpoint.RecordTypeTXT, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "tar.test-zone.example.org", Targets: endpoint.Targets{"tar.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner-2", "tar": "sometar", }, }, { DNSName: "foobar.test-zone.example.org", Targets: endpoint.Targets{"foobar.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "multiple.test-zone.example.org", Targets: endpoint.Targets{"lb1.loadbalancer.com"}, SetIdentifier: "test-set-1", RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "multiple.test-zone.example.org", Targets: endpoint.Targets{"lb2.loadbalancer.com"}, SetIdentifier: "test-set-2", RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "*.wildcard.test-zone.example.org", Targets: endpoint.Targets{"foo.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, { DNSName: "dualstack.test-zone.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, { DNSName: "dualstack.test-zone.example.org", Targets: endpoint.Targets{"2001:DB8::1"}, RecordType: endpoint.RecordTypeAAAA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner-2", }, }, { DNSName: "mail.test-zone.example.org", Targets: endpoint.Targets{"10 onemail.example.com"}, RecordType: endpoint.RecordTypeMX, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, } 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) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) } func testTXTRegistryRecordsSuffixed(t *testing.T) { ctx := context.Background() p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwnerAndLabels("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"foo": "somefoo"}), newEndpointWithOwnerAndLabels("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"bar": "somebar"}), newEndpointWithOwner("bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("bar-txt.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwnerAndLabels("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"tar": "sometar"}), newEndpointWithOwner("tar-TxT.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), // case-insensitive TXT prefix newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"), 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("dualstack.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""), newEndpointWithOwner("dualstack-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""), newEndpointWithOwner("aaaa-dualstack-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""), newEndpointWithOwner("mx-mail-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ { DNSName: "foo.test-zone.example.org", Targets: endpoint.Targets{"foo.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", "foo": "somefoo", }, }, { DNSName: "bar.test-zone.example.org", Targets: endpoint.Targets{"my-domain.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", "bar": "somebar", }, }, { DNSName: "bar-txt.test-zone.example.org", Targets: endpoint.Targets{"baz.test-zone.example.org"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "qux.test-zone.example.org", Targets: endpoint.Targets{"random"}, RecordType: endpoint.RecordTypeTXT, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "tar.test-zone.example.org", Targets: endpoint.Targets{"tar.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner-2", "tar": "sometar", }, }, { DNSName: "foobar.test-zone.example.org", Targets: endpoint.Targets{"foobar.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "multiple.test-zone.example.org", Targets: endpoint.Targets{"lb1.loadbalancer.com"}, SetIdentifier: "test-set-1", RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "multiple.test-zone.example.org", Targets: endpoint.Targets{"lb2.loadbalancer.com"}, SetIdentifier: "test-set-2", RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "dualstack.test-zone.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, { DNSName: "dualstack.test-zone.example.org", Targets: endpoint.Targets{"2001:DB8::1"}, RecordType: endpoint.RecordTypeAAAA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner-2", }, }, { DNSName: "mail.test-zone.example.org", Targets: endpoint.Targets{"10 onemail.example.com"}, RecordType: endpoint.RecordTypeMX, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, } 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) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) } func testTXTRegistryRecordsNoPrefix(t *testing.T) { p := inmemory.NewInMemoryProvider() ctx := context.Background() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("alias.test-zone.example.org", "my-domain.com", endpoint.RecordTypeA, "").WithProviderSpecific("alias", "true"), newEndpointWithOwner("cname-alias.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("dualstack.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""), newEndpointWithOwner("dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""), newEndpointWithOwner("aaaa-dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""), newEndpointWithOwner("mx-mail.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ { DNSName: "foo.test-zone.example.org", Targets: endpoint.Targets{"foo.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "bar.test-zone.example.org", Targets: endpoint.Targets{"my-domain.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "alias.test-zone.example.org", Targets: endpoint.Targets{"my-domain.com"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, ProviderSpecific: []endpoint.ProviderSpecificProperty{ { Name: "alias", Value: "true", }, }, }, { DNSName: "txt.bar.test-zone.example.org", Targets: endpoint.Targets{"baz.test-zone.example.org"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", endpoint.ResourceLabelKey: "ingress/default/my-ingress", }, }, { DNSName: "qux.test-zone.example.org", Targets: endpoint.Targets{"random"}, RecordType: endpoint.RecordTypeTXT, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "tar.test-zone.example.org", Targets: endpoint.Targets{"tar.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "", }, }, { DNSName: "foobar.test-zone.example.org", Targets: endpoint.Targets{"foobar.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, { DNSName: "dualstack.test-zone.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, { DNSName: "dualstack.test-zone.example.org", Targets: endpoint.Targets{"2001:DB8::1"}, RecordType: endpoint.RecordTypeAAAA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner-2", }, }, { DNSName: "mail.test-zone.example.org", Targets: endpoint.Targets{"10 onemail.example.com"}, RecordType: endpoint.RecordTypeMX, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, } r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) } func testTXTRegistryRecordsPrefixedTemplated(t *testing.T) { ctx := context.Background() p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""), newEndpointWithOwner("txt-a.foo.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""), newEndpointWithOwner("txt-mx.mail.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ { DNSName: "foo.test-zone.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, { DNSName: "mail.test-zone.example.org", Targets: endpoint.Targets{"10 onemail.example.com"}, RecordType: endpoint.RecordTypeMX, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, } 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) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) } func testTXTRegistryRecordsSuffixedTemplated(t *testing.T) { ctx := context.Background() p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("bar.test-zone.example.org", "8.8.8.8", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bartxtcname.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""), newEndpointWithOwner("mailtxt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ { DNSName: "bar.test-zone.example.org", Targets: endpoint.Targets{"8.8.8.8"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, { DNSName: "mail.test-zone.example.org", Targets: endpoint.Targets{"10 onemail.example.com"}, RecordType: endpoint.RecordTypeMX, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, } 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) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) } func testTXTRegistryApplyChanges(t *testing.T) { t.Run("With Prefix", testTXTRegistryApplyChangesWithPrefix) t.Run("With Templated Prefix", testTXTRegistryApplyChangesWithTemplatedPrefix) t.Run("With Templated Suffix", testTXTRegistryApplyChangesWithTemplatedSuffix) t.Run("With Suffix", testTXTRegistryApplyChangesWithSuffix) t.Run("No prefix", testTXTRegistryApplyChangesNoPrefix) } func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctxEndpoints := []*endpoint.Endpoint{} ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) } p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.cname-tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"), newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), newEndpointWithOwner("txt.cname-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("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), 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) 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"), newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"), }, UpdateNew: []*endpoint.Endpoint{ newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"), newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"), }, UpdateOld: []*endpoint.Endpoint{ newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"), }, } expected := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwnerAndOwnedRecord("txt.cname-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"), newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), newEndpointWithOwnerAndOwnedRecord("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-3"), newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwnerAndOwnedRecord("txt.cname-example", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "example"), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("txt.cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"), newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"), newEndpointWithOwnerAndOwnedRecord("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-1"), }, UpdateNew: []*endpoint.Endpoint{ newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"), newEndpointWithOwnerAndOwnedRecord("txt.cname-tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"), newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"), newEndpointWithOwnerAndOwnedRecord("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"), }, UpdateOld: []*endpoint.Endpoint{ newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("txt.cname-tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"), newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"), newEndpointWithOwnerAndOwnedRecord("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"), }, } p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { mExpected := map[string][]*endpoint.Endpoint{ "Create": expected.Create, "UpdateNew": expected.UpdateNew, "UpdateOld": expected.UpdateOld, "Delete": expected.Delete, } mGot := map[string][]*endpoint.Endpoint{ "Create": got.Create, "UpdateNew": got.UpdateNew, "UpdateOld": got.UpdateOld, "Delete": got.Delete, } assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) assert.Nil(t, ctx.Value(provider.RecordsContextKey)) } err := r.ApplyChanges(ctx, changes) require.NoError(t, err) } func testTXTRegistryApplyChangesWithTemplatedPrefix(t *testing.T) { p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctxEndpoints := []*endpoint.Endpoint{} ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) } p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{}, }) 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"), }, Delete: []*endpoint.Endpoint{}, UpdateOld: []*endpoint.Endpoint{}, UpdateNew: []*endpoint.Endpoint{}, } expected := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwnerAndOwnedRecord("prefixcname.new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"), }, } p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { mExpected := map[string][]*endpoint.Endpoint{ "Create": expected.Create, "UpdateNew": expected.UpdateNew, "UpdateOld": expected.UpdateOld, "Delete": expected.Delete, } mGot := map[string][]*endpoint.Endpoint{ "Create": got.Create, "UpdateNew": got.UpdateNew, "UpdateOld": got.UpdateOld, "Delete": got.Delete, } assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) assert.Nil(t, ctx.Value(provider.RecordsContextKey)) } err := r.ApplyChanges(ctx, changes) require.NoError(t, err) } func testTXTRegistryApplyChangesWithTemplatedSuffix(t *testing.T) { p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctxEndpoints := []*endpoint.Endpoint{} ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) 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) 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"), }, Delete: []*endpoint.Endpoint{}, UpdateOld: []*endpoint.Endpoint{}, UpdateNew: []*endpoint.Endpoint{}, } expected := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwnerAndOwnedRecord("new-record-1-cnamesuffix.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"), }, } p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { mExpected := map[string][]*endpoint.Endpoint{ "Create": expected.Create, "UpdateNew": expected.UpdateNew, "UpdateOld": expected.UpdateOld, "Delete": expected.Delete, } mGot := map[string][]*endpoint.Endpoint{ "Create": got.Create, "UpdateNew": got.UpdateNew, "UpdateOld": got.UpdateOld, "Delete": got.Delete, } assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) assert.Nil(t, ctx.Value(provider.RecordsContextKey)) } err := r.ApplyChanges(ctx, changes) require.NoError(t, err) } func testTXTRegistryApplyChangesWithSuffix(t *testing.T) { p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctxEndpoints := []*endpoint.Endpoint{} ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) } p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar-txt.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("cname-bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("cname-tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("cname-foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"), newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), newEndpointWithOwner("cname-multiple-txt.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-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), 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) 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"), newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"), }, UpdateNew: []*endpoint.Endpoint{ newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"), newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"), }, UpdateOld: []*endpoint.Endpoint{ newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"), }, } expected := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwnerAndOwnedRecord("cname-new-record-1-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"), newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), newEndpointWithOwnerAndOwnedRecord("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-3"), newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwnerAndOwnedRecord("cname-example-txt", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "example"), newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"), newEndpointWithOwnerAndOwnedRecord("cname-wildcard-txt.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "*.wildcard.test-zone.example.org"), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("cname-foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"), newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"), newEndpointWithOwnerAndOwnedRecord("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-1"), }, UpdateNew: []*endpoint.Endpoint{ newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"), newEndpointWithOwnerAndOwnedRecord("cname-tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"), newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"), newEndpointWithOwnerAndOwnedRecord("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"), }, UpdateOld: []*endpoint.Endpoint{ newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("cname-tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"), newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"), newEndpointWithOwnerAndOwnedRecord("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"), }, } p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { mExpected := map[string][]*endpoint.Endpoint{ "Create": expected.Create, "UpdateNew": expected.UpdateNew, "UpdateOld": expected.UpdateOld, "Delete": expected.Delete, } mGot := map[string][]*endpoint.Endpoint{ "Create": got.Create, "UpdateNew": got.UpdateNew, "UpdateOld": got.UpdateOld, "Delete": got.Delete, } assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) assert.Nil(t, ctx.Value(provider.RecordsContextKey)) } err := r.ApplyChanges(ctx, changes) require.NoError(t, err) } func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctxEndpoints := []*endpoint.Endpoint{} ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) } p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), 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) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("new-alias.test-zone.example.org", "my-domain.com", endpoint.RecordTypeA, "").WithProviderSpecific("alias", "true"), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), }, UpdateNew: []*endpoint.Endpoint{ newEndpointWithOwner("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2"), }, UpdateOld: []*endpoint.Endpoint{ newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2"), }, } expected := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("cname-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"), newEndpointWithOwner("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("cname-example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "example"), newEndpointWithOwner("new-alias.test-zone.example.org", "my-domain.com", endpoint.RecordTypeA, "owner").WithProviderSpecific("alias", "true"), newEndpointWithOwnerAndOwnedRecord("cname-new-alias.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-alias.test-zone.example.org").WithProviderSpecific("alias", "true"), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"), }, UpdateNew: []*endpoint.Endpoint{}, UpdateOld: []*endpoint.Endpoint{}, } p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { mExpected := map[string][]*endpoint.Endpoint{ "Create": expected.Create, "UpdateNew": expected.UpdateNew, "UpdateOld": expected.UpdateOld, "Delete": expected.Delete, } mGot := map[string][]*endpoint.Endpoint{ "Create": got.Create, "UpdateNew": got.UpdateNew, "UpdateOld": got.UpdateOld, "Delete": got.Delete, } assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) assert.Nil(t, ctx.Value(provider.RecordsContextKey)) } err := r.ApplyChanges(ctx, changes) require.NoError(t, err) } func testTXTRegistryMissingRecords(t *testing.T) { t.Run("No prefix", testTXTRegistryMissingRecordsNoPrefix) t.Run("With Prefix", testTXTRegistryMissingRecordsWithPrefix) } func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) { ctx := context.Background() p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("oldformat.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("oldformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("oldformat2.test-zone.example.org", "bar.loadbalancer.com", endpoint.RecordTypeA, ""), newEndpointWithOwner("oldformat2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("newformat.test-zone.example.org", "foobar.nameserver.com", endpoint.RecordTypeNS, ""), newEndpointWithOwner("ns-newformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("newformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("noheritage.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("oldformat-otherowner.test-zone.example.org", "bar.loadbalancer.com", endpoint.RecordTypeA, ""), newEndpointWithOwner("oldformat-otherowner.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=otherowner\"", endpoint.RecordTypeTXT, ""), endpoint.NewEndpoint("unmanaged1.test-zone.example.org", endpoint.RecordTypeA, "unmanaged1.loadbalancer.com"), endpoint.NewEndpoint("unmanaged2.test-zone.example.org", endpoint.RecordTypeCNAME, "unmanaged2.loadbalancer.com"), newEndpointWithOwner("this-is-a-63-characters-long-label-that-we-do-expect-will-work.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("this-is-a-63-characters-long-label-that-we-do-expect-will-work.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ { DNSName: "oldformat.test-zone.example.org", Targets: endpoint.Targets{"foo.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ // owner was added from the TXT record's target endpoint.OwnerLabelKey: "owner", }, ProviderSpecific: []endpoint.ProviderSpecificProperty{ { Name: "txt/force-update", Value: "true", }, }, }, { DNSName: "oldformat2.test-zone.example.org", Targets: endpoint.Targets{"bar.loadbalancer.com"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, ProviderSpecific: []endpoint.ProviderSpecificProperty{ { Name: "txt/force-update", Value: "true", }, }, }, { DNSName: "newformat.test-zone.example.org", Targets: endpoint.Targets{"foobar.nameserver.com"}, RecordType: endpoint.RecordTypeNS, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, // Only TXT records with the wrong heritage are returned by Records() { DNSName: "noheritage.test-zone.example.org", Targets: endpoint.Targets{"random"}, RecordType: endpoint.RecordTypeTXT, Labels: map[string]string{ // No owner because it's not external-dns heritage endpoint.OwnerLabelKey: "", }, }, { DNSName: "oldformat-otherowner.test-zone.example.org", Targets: endpoint.Targets{"bar.loadbalancer.com"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ // Records() retrieves all the records of the zone, no matter the owner endpoint.OwnerLabelKey: "otherowner", }, }, { DNSName: "unmanaged1.test-zone.example.org", Targets: endpoint.Targets{"unmanaged1.loadbalancer.com"}, RecordType: endpoint.RecordTypeA, }, { DNSName: "unmanaged2.test-zone.example.org", Targets: endpoint.Targets{"unmanaged2.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "this-is-a-63-characters-long-label-that-we-do-expect-will-work.test-zone.example.org", Targets: endpoint.Targets{"foo.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, } 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)) } func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { ctx := context.Background() p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("oldformat.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.oldformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("oldformat2.test-zone.example.org", "bar.loadbalancer.com", endpoint.RecordTypeA, ""), newEndpointWithOwner("txt.oldformat2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("newformat.test-zone.example.org", "foobar.nameserver.com", endpoint.RecordTypeNS, ""), newEndpointWithOwner("txt.ns-newformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("oldformat3.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.oldformat3.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.newformat.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("noheritage.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("oldformat-otherowner.test-zone.example.org", "bar.loadbalancer.com", endpoint.RecordTypeA, ""), newEndpointWithOwner("txt.oldformat-otherowner.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=otherowner\"", endpoint.RecordTypeTXT, ""), endpoint.NewEndpoint("unmanaged1.test-zone.example.org", endpoint.RecordTypeA, "unmanaged1.loadbalancer.com"), endpoint.NewEndpoint("unmanaged2.test-zone.example.org", endpoint.RecordTypeCNAME, "unmanaged2.loadbalancer.com"), }, }) expectedRecords := []*endpoint.Endpoint{ { DNSName: "oldformat.test-zone.example.org", Targets: endpoint.Targets{"foo.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ // owner was added from the TXT record's target endpoint.OwnerLabelKey: "owner", }, ProviderSpecific: []endpoint.ProviderSpecificProperty{ { Name: "txt/force-update", Value: "true", }, }, }, { DNSName: "oldformat2.test-zone.example.org", Targets: endpoint.Targets{"bar.loadbalancer.com"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, ProviderSpecific: []endpoint.ProviderSpecificProperty{ { Name: "txt/force-update", Value: "true", }, }, }, { DNSName: "oldformat3.test-zone.example.org", Targets: endpoint.Targets{"random"}, RecordType: endpoint.RecordTypeTXT, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, ProviderSpecific: []endpoint.ProviderSpecificProperty{ { Name: "txt/force-update", Value: "true", }, }, }, { DNSName: "newformat.test-zone.example.org", Targets: endpoint.Targets{"foobar.nameserver.com"}, RecordType: endpoint.RecordTypeNS, Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, }, { DNSName: "noheritage.test-zone.example.org", Targets: endpoint.Targets{"random"}, RecordType: endpoint.RecordTypeTXT, Labels: map[string]string{ // No owner because it's not external-dns heritage endpoint.OwnerLabelKey: "", }, }, { DNSName: "oldformat-otherowner.test-zone.example.org", Targets: endpoint.Targets{"bar.loadbalancer.com"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ // All the records of the zone are retrieved, no matter the owner endpoint.OwnerLabelKey: "otherowner", }, }, { DNSName: "unmanaged1.test-zone.example.org", Targets: endpoint.Targets{"unmanaged1.loadbalancer.com"}, RecordType: endpoint.RecordTypeA, }, { DNSName: "unmanaged2.test-zone.example.org", Targets: endpoint.Targets{"unmanaged2.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, }, } 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)) } func TestCacheMethods(t *testing.T) { cache := []*endpoint.Endpoint{ newEndpointWithOwner("thing.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing1.com", "1.2.3.6", "A", "owner"), newEndpointWithOwner("thing2.com", "1.2.3.4", "CNAME", "owner"), newEndpointWithOwner("thing3.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"), } registry := &TXTRegistry{ recordsCache: cache, cacheInterval: time.Hour, } expectedCacheAfterAdd := []*endpoint.Endpoint{ newEndpointWithOwner("thing.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing1.com", "1.2.3.6", "A", "owner"), newEndpointWithOwner("thing2.com", "1.2.3.4", "CNAME", "owner"), newEndpointWithOwner("thing3.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing4.com", "2001:DB8::1", "AAAA", "owner"), newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"), } expectedCacheAfterUpdate := []*endpoint.Endpoint{ newEndpointWithOwner("thing1.com", "1.2.3.6", "A", "owner"), newEndpointWithOwner("thing2.com", "1.2.3.4", "CNAME", "owner"), newEndpointWithOwner("thing3.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"), newEndpointWithOwner("thing.com", "1.2.3.6", "A", "owner2"), newEndpointWithOwner("thing4.com", "2001:DB8::2", "AAAA", "owner"), } expectedCacheAfterDelete := []*endpoint.Endpoint{ newEndpointWithOwner("thing1.com", "1.2.3.6", "A", "owner"), newEndpointWithOwner("thing2.com", "1.2.3.4", "CNAME", "owner"), newEndpointWithOwner("thing3.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"), } // test add cache registry.addToCache(newEndpointWithOwner("thing4.com", "2001:DB8::1", "AAAA", "owner")) registry.addToCache(newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner")) if !reflect.DeepEqual(expectedCacheAfterAdd, registry.recordsCache) { t.Fatalf("expected endpoints should match endpoints from cache: expected %v, but got %v", expectedCacheAfterAdd, registry.recordsCache) } // test update cache registry.removeFromCache(newEndpointWithOwner("thing.com", "1.2.3.4", "A", "owner")) registry.addToCache(newEndpointWithOwner("thing.com", "1.2.3.6", "A", "owner2")) registry.removeFromCache(newEndpointWithOwner("thing4.com", "2001:DB8::1", "AAAA", "owner")) registry.addToCache(newEndpointWithOwner("thing4.com", "2001:DB8::2", "AAAA", "owner")) // ensure it was updated if !reflect.DeepEqual(expectedCacheAfterUpdate, registry.recordsCache) { t.Fatalf("expected endpoints should match endpoints from cache: expected %v, but got %v", expectedCacheAfterUpdate, registry.recordsCache) } // test deleting a record registry.removeFromCache(newEndpointWithOwner("thing.com", "1.2.3.6", "A", "owner2")) registry.removeFromCache(newEndpointWithOwner("thing4.com", "2001:DB8::2", "AAAA", "owner")) // ensure it was deleted if !reflect.DeepEqual(expectedCacheAfterDelete, registry.recordsCache) { t.Fatalf("expected endpoints should match endpoints from cache: expected %v, but got %v", expectedCacheAfterDelete, registry.recordsCache) } } func TestDropPrefix(t *testing.T) { mapper := newaffixNameMapper("foo-%{record_type}-", "", "") expectedOutput := "test.example.com" tests := []string{ "foo-cname-test.example.com", "foo-a-test.example.com", "foo--test.example.com", } for _, tc := range tests { t.Run(tc, func(t *testing.T) { actualOutput, _ := mapper.dropAffixExtractType(tc) assert.Equal(t, expectedOutput, actualOutput) }) } } func TestDropSuffix(t *testing.T) { mapper := newaffixNameMapper("", "-%{record_type}-foo", "") expectedOutput := "test.example.com" tests := []string{ "test-a-foo.example.com", "test--foo.example.com", } for _, tc := range tests { t.Run(tc, func(t *testing.T) { r := strings.SplitN(tc, ".", 2) rClean, _ := mapper.dropAffixExtractType(r[0]) actualOutput := rClean + "." + r[1] assert.Equal(t, expectedOutput, actualOutput) }) } } func TestExtractRecordTypeDefaultPosition(t *testing.T) { tests := []struct { input string expectedName string expectedType string }{ { input: "ns-zone.example.com", expectedName: "zone.example.com", expectedType: "NS", }, { input: "aaaa-zone.example.com", expectedName: "zone.example.com", expectedType: "AAAA", }, { input: "ptr-zone.example.com", expectedName: "ptr-zone.example.com", expectedType: "", }, { input: "zone.example.com", expectedName: "zone.example.com", expectedType: "", }, } for _, tc := range tests { t.Run(tc.input, func(t *testing.T) { actualName, actualType := extractRecordTypeDefaultPosition(tc.input) assert.Equal(t, tc.expectedName, actualName) assert.Equal(t, tc.expectedType, actualType) }) } } func TestToEndpointNameNewTXT(t *testing.T) { tests := []struct { name string mapper affixNameMapper domain string txtDomain string recordType string }{ { name: "prefix", mapper: newaffixNameMapper("foo", "", ""), domain: "example.com", recordType: "A", txtDomain: "fooa-example.com", }, { name: "suffix", mapper: newaffixNameMapper("", "foo", ""), domain: "example", recordType: "AAAA", txtDomain: "aaaa-examplefoo", }, { name: "suffix", mapper: newaffixNameMapper("", "foo", ""), domain: "example.com", recordType: "AAAA", txtDomain: "aaaa-examplefoo.com", }, { name: "prefix with dash", mapper: newaffixNameMapper("foo-", "", ""), domain: "example.com", recordType: "A", txtDomain: "foo-a-example.com", }, { name: "suffix with dash", mapper: newaffixNameMapper("", "-foo", ""), domain: "example.com", recordType: "CNAME", txtDomain: "cname-example-foo.com", }, { name: "prefix with dot", mapper: newaffixNameMapper("foo.", "", ""), domain: "example.com", recordType: "CNAME", txtDomain: "foo.cname-example.com", }, { name: "suffix with dot", mapper: newaffixNameMapper("", ".foo", ""), domain: "example.com", recordType: "CNAME", txtDomain: "cname-example.foo.com", }, { name: "prefix with multiple dots", mapper: newaffixNameMapper("foo.bar.", "", ""), domain: "example.com", recordType: "CNAME", txtDomain: "foo.bar.cname-example.com", }, { name: "suffix with multiple dots", mapper: newaffixNameMapper("", ".foo.bar.test", ""), domain: "example.com", recordType: "CNAME", txtDomain: "cname-example.foo.bar.test.com", }, { name: "templated prefix", mapper: newaffixNameMapper("%{record_type}-foo", "", ""), domain: "example.com", recordType: "A", txtDomain: "a-fooexample.com", }, { name: "templated suffix", mapper: newaffixNameMapper("", "foo-%{record_type}", ""), domain: "example.com", recordType: "A", txtDomain: "examplefoo-a.com", }, { name: "templated prefix with dot", mapper: newaffixNameMapper("%{record_type}foo.", "", ""), domain: "example.com", recordType: "CNAME", txtDomain: "cnamefoo.example.com", }, { name: "templated suffix with dot", mapper: newaffixNameMapper("", ".foo%{record_type}", ""), domain: "example.com", recordType: "A", txtDomain: "example.fooa.com", }, { name: "templated prefix with multiple dots", mapper: newaffixNameMapper("bar.%{record_type}.foo.", "", ""), domain: "example.com", recordType: "CNAME", txtDomain: "bar.cname.foo.example.com", }, { name: "templated suffix with multiple dots", mapper: newaffixNameMapper("", ".foo%{record_type}.bar", ""), domain: "example.com", recordType: "A", txtDomain: "example.fooa.bar.com", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { txtDomain := tc.mapper.toTXTName(tc.domain, tc.recordType) assert.Equal(t, tc.txtDomain, txtDomain) domain, _ := tc.mapper.toEndpointName(txtDomain) assert.Equal(t, tc.domain, domain) }) } } func TestNewTXTScheme(t *testing.T) { p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctxEndpoints := []*endpoint.Endpoint{} ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) } p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), 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) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), }, UpdateNew: []*endpoint.Endpoint{ newEndpointWithOwner("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2"), }, UpdateOld: []*endpoint.Endpoint{ newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2"), }, } expected := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("cname-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"), newEndpointWithOwner("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("cname-example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "example"), }, Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"), }, UpdateNew: []*endpoint.Endpoint{}, UpdateOld: []*endpoint.Endpoint{}, } p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { mExpected := map[string][]*endpoint.Endpoint{ "Create": expected.Create, "UpdateNew": expected.UpdateNew, "UpdateOld": expected.UpdateOld, "Delete": expected.Delete, } mGot := map[string][]*endpoint.Endpoint{ "Create": got.Create, "UpdateNew": got.UpdateNew, "UpdateOld": got.UpdateOld, "Delete": got.Delete, } assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) assert.Nil(t, ctx.Value(provider.RecordsContextKey)) } err := r.ApplyChanges(ctx, changes) require.NoError(t, err) } func TestGenerateTXT(t *testing.T) { record := newEndpointWithOwner("foo.test-zone.example.org", "new-foo.loadbalancer.com", endpoint.RecordTypeCNAME, "owner") expectedTXT := []*endpoint.Endpoint{ { DNSName: "cname-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) gotTXT := r.generateTXTRecord(record) assert.Equal(t, expectedTXT, gotTXT) } func TestGenerateTXTForAAAA(t *testing.T) { record := newEndpointWithOwner("foo.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, "owner") expectedTXT := []*endpoint.Endpoint{ { DNSName: "aaaa-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) gotTXT := r.generateTXTRecord(record) assert.Equal(t, expectedTXT, gotTXT) } func TestFailGenerateTXT(t *testing.T) { cnameRecord := &endpoint.Endpoint{ DNSName: "foo-some-really-big-name-not-supported-and-will-fail-000000000000000000.test-zone.example.org", Targets: endpoint.Targets{"new-foo.loadbalancer.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{}, } // A bad DNS name returns empty expected TXT expectedTXT := []*endpoint.Endpoint{} p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) gotTXT := r.generateTXTRecord(cnameRecord) assert.Equal(t, expectedTXT, gotTXT) } func TestTXTRegistryApplyChangesEncrypt(t *testing.T) { p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctxEndpoints := []*endpoint.Endpoint{} ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwnerAndOwnedRecord("txt.cname-foobar.test-zone.example.org", "\"h8UQ6jelUFUsEIn7SbFktc2MYXPx/q8lySqI4VwfVtVaIbb2nkHWV/88KKbuLtu7fJNzMir8ELVeVnRSY01KdiIuj7ledqZe5ailEjQaU5Z6uEKd5pgs6sH8\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"), }, }) r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte("12345678901234567890123456789012")) records, _ := r.Records(ctx) changes := &plan.Changes{ Delete: records, } // ensure that encryption nonce gets reused when deleting records expected := &plan.Changes{ Delete: []*endpoint.Endpoint{ newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), newEndpointWithOwnerAndOwnedRecord("txt.cname-foobar.test-zone.example.org", "\"h8UQ6jelUFUsEIn7SbFktc2MYXPx/q8lySqI4VwfVtVaIbb2nkHWV/88KKbuLtu7fJNzMir8ELVeVnRSY01KdiIuj7ledqZe5ailEjQaU5Z6uEKd5pgs6sH8\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"), }, } p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { mExpected := map[string][]*endpoint.Endpoint{ "Delete": expected.Delete, } mGot := map[string][]*endpoint.Endpoint{ "Delete": got.Delete, } assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) assert.Nil(t, ctx.Value(provider.RecordsContextKey)) } err := r.ApplyChanges(ctx, changes) require.NoError(t, err) } // TestMultiClusterDifferentRecordTypeOwnership validates the registry handles environments where the same zone is managed by // external-dns in different clusters and the ingress record type is different. For example one uses A records and the other // uses CNAME. In this environment the first cluster that establishes the owner record should maintain ownership even // if the same ingress host is deployed to the other. With the introduction of Dual Record support each record type // was treated independently and would cause each cluster to fight over ownership. This tests ensure that the default // Dual Stack record support only treats AAAA records independently and while keeping A and CNAME record ownership intact. func TestMultiClusterDifferentRecordTypeOwnership(t *testing.T) { ctx := context.Background() p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ // records on cluster using A record for ingress address newEndpointWithOwner("bar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=cat,external-dns/resource=ingress/default/foo\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("bar.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, ""), }, }) 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 cname := &endpoint.Endpoint{ DNSName: "bar.test-zone.example.org", Targets: endpoint.Targets{"cluster-b"}, RecordType: "CNAME", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/foo-127", }, } desired := []*endpoint.Endpoint{cname} pl := &plan.Plan{ Policies: []plan.Policy{&plan.SyncPolicy{}}, Current: records, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, } changes := pl.Calculate() p.OnApplyChanges = func(ctx context.Context, changes *plan.Changes) { got := map[string][]*endpoint.Endpoint{ "Create": changes.Create, "UpdateNew": changes.UpdateNew, "UpdateOld": changes.UpdateOld, "Delete": changes.Delete, } expected := map[string][]*endpoint.Endpoint{ "Create": {}, "UpdateNew": {}, "UpdateOld": {}, "Delete": {}, } testutils.SamePlanChanges(got, expected) } err := r.ApplyChanges(ctx, changes.Changes) if err != nil { t.Error(err) } } func TestGenerateTXTRecordWithNewFormatOnly(t *testing.T) { p := inmemory.NewInMemoryProvider() testCases := []struct { name string endpoint *endpoint.Endpoint expectedRecords int expectedPrefix string description string }{ { name: "legacy format enabled - standard record", endpoint: newEndpointWithOwner("foo.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, "owner"), expectedRecords: 1, expectedPrefix: "a-", description: "Should generate only new format TXT records", }, { name: "new format only - standard record", endpoint: newEndpointWithOwner("foo.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, "owner"), expectedRecords: 1, expectedPrefix: "a-", description: "Should only generate new format TXT record", }, { name: "legacy format enabled - AAAA record", endpoint: newEndpointWithOwner("foo.test-zone.example.org", "2001:db8::1", endpoint.RecordTypeAAAA, "owner"), expectedRecords: 1, expectedPrefix: "aaaa-", description: "Should only generate new format for AAAA records regardless of setting", }, { name: "new format only - AAAA record", endpoint: newEndpointWithOwner("foo.test-zone.example.org", "2001:db8::1", endpoint.RecordTypeAAAA, "owner"), expectedRecords: 1, expectedPrefix: "aaaa-", description: "Should only generate new format for AAAA records", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) records := r.generateTXTRecord(tc.endpoint) assert.Len(t, records, tc.expectedRecords, tc.description) for _, record := range records { assert.Equal(t, endpoint.RecordTypeTXT, record.RecordType) } if tc.endpoint.RecordType == endpoint.RecordTypeAAAA { hasNewFormat := false for _, record := range records { if strings.HasPrefix(record.DNSName, tc.expectedPrefix) { hasNewFormat = true break } } assert.True(t, hasNewFormat, "Should have at least one record with prefix %s when using new format", tc.expectedPrefix) } }) } } func TestApplyChangesWithNewFormatOnly(t *testing.T) { p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctx := context.Background() r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("new-record.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, "owner"), }, } err := r.ApplyChanges(ctx, changes) require.NoError(t, err) records, err := p.Records(ctx) require.NoError(t, err) var txtRecords []*endpoint.Endpoint for _, record := range records { if record.RecordType == endpoint.RecordTypeTXT { txtRecords = append(txtRecords, record) } } assert.Len(t, txtRecords, 1, "Should only create one TXT record in new format") if len(txtRecords) > 0 { assert.True(t, strings.HasPrefix(txtRecords[0].DNSName, "a-"), "TXT record should have 'a-' prefix when using new format only") } } func TestTXTRegistryRecordsWithEmptyTargets(t *testing.T) { ctx := context.Background() p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ { DNSName: "empty-targets.test-zone.example.org", RecordType: endpoint.RecordTypeTXT, Targets: endpoint.Targets{}, }, { DNSName: "valid-targets.test-zone.example.org", RecordType: endpoint.RecordTypeTXT, Targets: endpoint.Targets{"target1"}, }, }, }) 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) expectedRecords := []*endpoint.Endpoint{ { DNSName: "valid-targets.test-zone.example.org", Targets: endpoint.Targets{"target1"}, RecordType: endpoint.RecordTypeTXT, Labels: map[string]string{}, }, } assert.True(t, testutils.SameEndpoints(records, expectedRecords)) testutils.TestHelperLogContains("TXT record has no targets empty-targets.test-zone.example.org", hook, t) }