/* 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 plan import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" ) type PlanTestSuite struct { suite.Suite fooV1Cname *endpoint.Endpoint fooV2Cname *endpoint.Endpoint fooV2CnameUppercase *endpoint.Endpoint fooV2TXT *endpoint.Endpoint fooV2CnameNoLabel *endpoint.Endpoint fooV3CnameSameResource *endpoint.Endpoint fooA5 *endpoint.Endpoint bar127A *endpoint.Endpoint bar127AWithTTL *endpoint.Endpoint bar127AWithProviderSpecificTrue *endpoint.Endpoint bar127AWithProviderSpecificFalse *endpoint.Endpoint bar127AWithProviderSpecificUnset *endpoint.Endpoint bar192A *endpoint.Endpoint multiple1 *endpoint.Endpoint multiple2 *endpoint.Endpoint multiple3 *endpoint.Endpoint domainFilterFiltered1 *endpoint.Endpoint domainFilterFiltered2 *endpoint.Endpoint domainFilterFiltered3 *endpoint.Endpoint domainFilterExcluded *endpoint.Endpoint domainFilterFilteredTXT1 *endpoint.Endpoint domainFilterFilteredTXT2 *endpoint.Endpoint domainFilterExcludedTXT *endpoint.Endpoint } func (suite *PlanTestSuite) SetupTest() { suite.fooV1Cname = &endpoint.Endpoint{ DNSName: "foo", Targets: endpoint.Targets{"v1"}, RecordType: "CNAME", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/foo-v1", endpoint.OwnerLabelKey: "pwner", }, } // same resource as fooV1Cname, but target is different. It will never be picked because its target lexicographically bigger than "v1" suite.fooV3CnameSameResource = &endpoint.Endpoint{ // TODO: remove this once endpoint can support multiple targets DNSName: "foo", Targets: endpoint.Targets{"v3"}, RecordType: "CNAME", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/foo-v1", endpoint.OwnerLabelKey: "pwner", }, } suite.fooV2Cname = &endpoint.Endpoint{ DNSName: "foo", Targets: endpoint.Targets{"v2"}, RecordType: "CNAME", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/foo-v2", }, } suite.fooV2CnameUppercase = &endpoint.Endpoint{ DNSName: "foo", Targets: endpoint.Targets{"V2"}, RecordType: "CNAME", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/foo-v2", }, } suite.fooV2TXT = &endpoint.Endpoint{ DNSName: "foo", RecordType: "TXT", } suite.fooV2CnameNoLabel = &endpoint.Endpoint{ DNSName: "foo", Targets: endpoint.Targets{"v2"}, RecordType: "CNAME", } suite.fooA5 = &endpoint.Endpoint{ DNSName: "foo", Targets: endpoint.Targets{"5.5.5.5"}, RecordType: "A", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/foo-5", }, } suite.bar127A = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: "A", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/bar-127", }, } suite.bar127AWithTTL = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: "A", RecordTTL: 300, Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/bar-127", }, } suite.bar127AWithProviderSpecificTrue = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: "A", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/bar-127", }, ProviderSpecific: endpoint.ProviderSpecific{ endpoint.ProviderSpecificProperty{ Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "true", }, }, } suite.bar127AWithProviderSpecificFalse = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: "A", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/bar-127", }, ProviderSpecific: endpoint.ProviderSpecific{ endpoint.ProviderSpecificProperty{ Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "false", }, }, } suite.bar127AWithProviderSpecificUnset = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: "A", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/bar-127", }, ProviderSpecific: endpoint.ProviderSpecific{}, } suite.bar192A = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"192.168.0.1"}, RecordType: "A", Labels: map[string]string{ endpoint.ResourceLabelKey: "ingress/default/bar-192", }, } suite.multiple1 = &endpoint.Endpoint{ DNSName: "multiple", Targets: endpoint.Targets{"192.168.0.1"}, RecordType: "A", SetIdentifier: "test-set-1", } suite.multiple2 = &endpoint.Endpoint{ DNSName: "multiple", Targets: endpoint.Targets{"192.168.0.2"}, RecordType: "A", SetIdentifier: "test-set-1", } suite.multiple3 = &endpoint.Endpoint{ DNSName: "multiple", Targets: endpoint.Targets{"192.168.0.2"}, RecordType: "A", SetIdentifier: "test-set-2", } suite.domainFilterFiltered1 = &endpoint.Endpoint{ DNSName: "foo.domain.tld", Targets: endpoint.Targets{"1.2.3.4"}, RecordType: "A", } suite.domainFilterFiltered2 = &endpoint.Endpoint{ DNSName: "bar.domain.tld", Targets: endpoint.Targets{"1.2.3.5"}, RecordType: "A", } suite.domainFilterFiltered3 = &endpoint.Endpoint{ DNSName: "baz.domain.tld", Targets: endpoint.Targets{"1.2.3.6"}, RecordType: "A", } suite.domainFilterExcluded = &endpoint.Endpoint{ DNSName: "foo.ex.domain.tld", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A", } suite.domainFilterFilteredTXT1 = &endpoint.Endpoint{ DNSName: "a-foo.domain.tld", Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, RecordType: "TXT", } suite.domainFilterFilteredTXT2 = &endpoint.Endpoint{ DNSName: "cname-bar.domain.tld", Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, RecordType: "TXT", } suite.domainFilterExcludedTXT = &endpoint.Endpoint{ DNSName: "cname-bar.otherdomain.tld", Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, RecordType: "TXT", } } func (suite *PlanTestSuite) TestSyncFirstRound() { current := []*endpoint.Endpoint{} desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV2Cname, suite.bar127A} expectedCreate := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar127A} // v1 is chosen because of resolver taking "min" expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestSyncSecondRound() { current := []*endpoint.Endpoint{suite.fooV1Cname} desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooV1Cname, suite.bar127A} expectedCreate := []*endpoint.Endpoint{suite.bar127A} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestSyncSecondRoundMigration() { current := []*endpoint.Endpoint{suite.fooV2CnameNoLabel} desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooV1Cname, suite.bar127A} expectedCreate := []*endpoint.Endpoint{suite.bar127A} expectedUpdateOld := []*endpoint.Endpoint{suite.fooV2CnameNoLabel} expectedUpdateNew := []*endpoint.Endpoint{suite.fooV1Cname} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestSyncSecondRoundWithTTLChange() { current := []*endpoint.Endpoint{suite.bar127A} desired := []*endpoint.Endpoint{suite.bar127AWithTTL} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{suite.bar127A} expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithTTL} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() { current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue} desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue} expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefaultFalse() { current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse} desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, PropertyComparator: func(name, previous, current string) bool { return CompareBoolean(false, name, previous, current) }, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefualtTrue() { current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue} desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, PropertyComparator: func(name, previous, current string) bool { return CompareBoolean(true, name, previous, current) }, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() { current := []*endpoint.Endpoint{suite.fooV1Cname} desired := []*endpoint.Endpoint{suite.fooV2Cname} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{suite.fooV1Cname} expectedUpdateNew := []*endpoint.Endpoint{{ DNSName: suite.fooV2Cname.DNSName, Targets: suite.fooV2Cname.Targets, RecordType: suite.fooV2Cname.RecordType, RecordTTL: suite.fooV2Cname.RecordTTL, Labels: map[string]string{ endpoint.ResourceLabelKey: suite.fooV2Cname.Labels[endpoint.ResourceLabelKey], endpoint.OwnerLabelKey: suite.fooV1Cname.Labels[endpoint.OwnerLabelKey], }, }} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestIdempotency() { current := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV2Cname} desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV2Cname} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestDifferentTypes() { current := []*endpoint.Endpoint{suite.fooV1Cname} desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooA5} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{suite.fooV1Cname} expectedUpdateNew := []*endpoint.Endpoint{suite.fooA5} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestIgnoreTXT() { current := []*endpoint.Endpoint{suite.fooV2TXT} desired := []*endpoint.Endpoint{suite.fooV2Cname} expectedCreate := []*endpoint.Endpoint{suite.fooV2Cname} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestIgnoreTargetCase() { current := []*endpoint.Endpoint{suite.fooV2Cname} desired := []*endpoint.Endpoint{suite.fooV2CnameUppercase} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestRemoveEndpoint() { current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A} desired := []*endpoint.Endpoint{suite.fooV1Cname} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{suite.bar192A} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() { current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A} desired := []*endpoint.Endpoint{suite.fooV1Cname} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&UpsertOnlyPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } // TODO: remove once multiple-target per endpoint is supported func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() { current := []*endpoint.Endpoint{suite.fooV3CnameSameResource, suite.bar192A} desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{suite.fooV3CnameSameResource} expectedUpdateNew := []*endpoint.Endpoint{suite.fooV1Cname} expectedDelete := []*endpoint.Endpoint{suite.bar192A} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } // TODO: remove once multiple-target per endpoint is supported func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() { current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A} desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource} expectedCreate := []*endpoint.Endpoint{} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{suite.bar192A} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier() { current := []*endpoint.Endpoint{suite.multiple1} desired := []*endpoint.Endpoint{suite.multiple2, suite.multiple3} expectedCreate := []*endpoint.Endpoint{suite.multiple3} expectedUpdateOld := []*endpoint.Endpoint{suite.multiple1} expectedUpdateNew := []*endpoint.Endpoint{suite.multiple2} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestSetIdentifierUpdateCreatesAndDeletes() { current := []*endpoint.Endpoint{suite.multiple2} desired := []*endpoint.Endpoint{suite.multiple3} expectedCreate := []*endpoint.Endpoint{suite.multiple3} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{suite.multiple2} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestDomainFiltersInitial() { current := []*endpoint.Endpoint{suite.domainFilterExcluded} desired := []*endpoint.Endpoint{suite.domainFilterExcluded, suite.domainFilterFiltered1, suite.domainFilterFiltered2, suite.domainFilterFiltered3} expectedCreate := []*endpoint.Endpoint{suite.domainFilterFiltered1, suite.domainFilterFiltered2, suite.domainFilterFiltered3} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}), ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestDomainFiltersUpdate() { current := []*endpoint.Endpoint{suite.domainFilterExcluded, suite.domainFilterFiltered1, suite.domainFilterFiltered2} desired := []*endpoint.Endpoint{suite.domainFilterExcluded, suite.domainFilterFiltered1, suite.domainFilterFiltered2, suite.domainFilterFiltered3} expectedCreate := []*endpoint.Endpoint{suite.domainFilterFiltered3} expectedUpdateOld := []*endpoint.Endpoint{} expectedUpdateNew := []*endpoint.Endpoint{} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}), ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) validateEntries(suite.T(), changes.Delete, expectedDelete) } func (suite *PlanTestSuite) TestMissing() { missing := []*endpoint.Endpoint{suite.domainFilterFilteredTXT1, suite.domainFilterFilteredTXT2, suite.domainFilterExcludedTXT} expectedCreate := []*endpoint.Endpoint{suite.domainFilterFilteredTXT1, suite.domainFilterFilteredTXT2} p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Missing: missing, DomainFilter: endpoint.NewDomainFilter([]string{"domain.tld"}), ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } changes := p.Calculate().Changes validateEntries(suite.T(), changes.Create, expectedCreate) } func TestPlan(t *testing.T) { suite.Run(t, new(PlanTestSuite)) } // validateEntries validates that the list of entries matches expected. func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) { if !testutils.SameEndpoints(entries, expected) { t.Fatalf("expected %q to match %q", entries, expected) } } func TestNormalizeDNSName(t *testing.T) { records := []struct { dnsName string expect string }{ { "3AAAA.FOO.BAR.COM ", "3aaaa.foo.bar.com.", }, { " example.foo.com.", "example.foo.com.", }, { "example123.foo.com ", "example123.foo.com.", }, { "foo", "foo.", }, { "123foo.bar", "123foo.bar.", }, { "foo.com", "foo.com.", }, { "foo.com.", "foo.com.", }, { "foo123.COM", "foo123.com.", }, { "my-exaMple3.FOO.BAR.COM", "my-example3.foo.bar.com.", }, { " my-example1214.FOO-1235.BAR-foo.COM ", "my-example1214.foo-1235.bar-foo.com.", }, { "my-example-my-example-1214.FOO-1235.BAR-foo.COM", "my-example-my-example-1214.foo-1235.bar-foo.com.", }, } for _, r := range records { gotName := normalizeDNSName(r.dnsName) assert.Equal(t, r.expect, gotName) } } func TestShouldUpdateProviderSpecific(tt *testing.T) { comparator := func(name, previous, current string) bool { return previous == current } for _, test := range []struct { name string current *endpoint.Endpoint desired *endpoint.Endpoint propertyComparator func(name, previous, current string) bool shouldUpdate bool }{ { name: "skip AWS target health", current: &endpoint.Endpoint{ DNSName: "foo.com", ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "aws/evaluate-target-health", Value: "true"}, }, }, desired: &endpoint.Endpoint{ DNSName: "bar.com", ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "aws/evaluate-target-health", Value: "true"}, }, }, propertyComparator: comparator, shouldUpdate: false, }, { name: "custom property unchanged", current: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "true"}, }, }, desired: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "true"}, }, }, propertyComparator: comparator, shouldUpdate: false, }, { name: "custom property value changed", current: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "true"}, }, }, desired: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "false"}, }, }, propertyComparator: comparator, shouldUpdate: true, }, { name: "custom property key changed", current: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "true"}, }, }, desired: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "new/property", Value: "true"}, }, }, propertyComparator: comparator, shouldUpdate: true, }, { name: "desired has same key and value as current but not comparator is set", current: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "true"}, }, }, desired: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "true"}, }, }, shouldUpdate: false, }, { name: "desired has same key and different value as current but not comparator is set", current: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "true"}, }, }, desired: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "false"}, }, }, shouldUpdate: true, }, { name: "desired has different key from current but not comparator is set", current: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "custom/property", Value: "true"}, }, }, desired: &endpoint.Endpoint{ ProviderSpecific: []endpoint.ProviderSpecificProperty{ {Name: "new/property", Value: "true"}, }, }, shouldUpdate: true, }, } { tt.Run(test.name, func(t *testing.T) { plan := &Plan{ Current: []*endpoint.Endpoint{test.current}, Desired: []*endpoint.Endpoint{test.desired}, PropertyComparator: test.propertyComparator, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } b := plan.shouldUpdateProviderSpecific(test.desired, test.current) assert.Equal(t, test.shouldUpdate, b) }) } }