external-dns/plan/plan_test.go

1206 lines
42 KiB
Go

/*
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 (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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
fooAAAA *endpoint.Endpoint
dsA *endpoint.Endpoint
dsAAAA *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
}
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.fooAAAA = &endpoint.Endpoint{
DNSName: "foo",
Targets: endpoint.Targets{"2001:DB8::1"},
RecordType: "AAAA",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-AAAA",
},
}
suite.dsA = &endpoint.Endpoint{
DNSName: "ds",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/ds",
},
}
suite.dsAAAA = &endpoint.Endpoint{
DNSName: "ds",
Targets: endpoint.Targets{"2001:DB8::1"},
RecordType: "AAAA",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/ds-AAAAA",
},
}
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: "alias",
Value: "false",
},
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",
},
endpoint.ProviderSpecificProperty{
Name: "alias",
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{
endpoint.ProviderSpecificProperty{
Name: "alias",
Value: "false",
},
},
}
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",
}
}
func TestPlan_ChangesJson_DecodeEncode(t *testing.T) {
ch := &Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "foo",
},
},
Update: []*Update{
{
Old: &endpoint.Endpoint{
DNSName: "bar",
},
New: &endpoint.Endpoint{
DNSName: "baz",
},
},
},
Delete: []*endpoint.Endpoint{
{
DNSName: "qux",
},
},
}
jsonBytes, err := json.Marshal(ch)
require.NoError(t, err)
assert.JSONEq(t,
`{"create":[{"dnsName":"foo"}],"updateOld":[{"dnsName":"bar"}],"updateNew":[{"dnsName":"baz"}],"delete":[{"dnsName":"qux"}]}`,
string(jsonBytes))
var changes Changes
err = json.NewDecoder(bytes.NewBuffer(jsonBytes)).Decode(&changes)
require.NoError(t, err)
assert.Equal(t, ch, &changes)
}
func TestPlan_ChangesJson_DecodeMixedCase(t *testing.T) {
input := `{"Create":[{"dnsName":"foo"}],"UpdateOld":[{"dnsName":"bar"}],"updateNew":[{"dnsName":"baz"}],"Delete":[{"dnsName":"qux"}]}`
var changes Changes
err := json.NewDecoder(strings.NewReader(input)).Decode(&changes)
require.NoError(t, err)
assert.Len(t, changes.Create, 1)
}
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) TestSyncSecondRoundWithProviderSpecificNoChange() {
current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
suite.False(changes.HasChanges())
}
func (suite *PlanTestSuite) TestHasChanges() {
current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
suite.True(changes.HasChanges())
}
func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificRemoval() {
current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
expectedCreate := []*endpoint.Endpoint{}
expectedUpdateOld := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
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) TestSyncSecondRoundWithProviderSpecificAddition() {
current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
expectedCreate := []*endpoint.Endpoint{}
expectedUpdateOld := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
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) 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) TestRecordTypeChange() {
current := []*endpoint.Endpoint{suite.fooV1Cname}
desired := []*endpoint.Endpoint{suite.fooA5}
expectedCreate := []*endpoint.Endpoint{suite.fooA5}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
OwnerID: suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
}
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) TestExistingCNameWithDualStackDesired() {
current := []*endpoint.Endpoint{suite.fooV1Cname}
desired := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
expectedCreate := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
OwnerID: suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
}
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) TestExistingDualStackWithCNameDesired() {
suite.fooA5.Labels[endpoint.OwnerLabelKey] = "nerf"
suite.fooAAAA.Labels[endpoint.OwnerLabelKey] = "nerf"
current := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
desired := []*endpoint.Endpoint{suite.fooV2Cname}
expectedCreate := []*endpoint.Endpoint{suite.fooV2Cname}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
OwnerID: suite.fooA5.Labels[endpoint.OwnerLabelKey],
}
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)
}
// TestExistingOwnerNotMatchingDualStackDesired validates that if there is an existing
// record for a domain but there is no ownership claim over it and there are desired
// records no changes are planed. Only domains that have explicit ownership claims should
// be updated.
func (suite *PlanTestSuite) TestExistingOwnerNotMatchingDualStackDesired() {
suite.fooA5.Labels = nil
current := []*endpoint.Endpoint{suite.fooA5}
desired := []*endpoint.Endpoint{suite.fooV2Cname}
expectedCreate := []*endpoint.Endpoint{}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
OwnerID: "pwner",
}
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)
}
// TestConflictingCurrentNonConflictingDesired is a bit of a corner case as it would indicate
// that the provider is not following valid DNS rules or there may be some
// caching issues. In this case since the desired records are not conflicting
// the updates will end up with the conflict resolved.
func (suite *PlanTestSuite) TestConflictingCurrentNonConflictingDesired() {
suite.fooA5.Labels[endpoint.OwnerLabelKey] = suite.fooV1Cname.Labels[endpoint.OwnerLabelKey]
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5}
desired := []*endpoint.Endpoint{suite.fooA5}
expectedCreate := []*endpoint.Endpoint{}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
OwnerID: suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
}
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)
}
// TestConflictingCurrentNoDesired is a bit of a corner case as it would indicate
// that the provider is not following valid DNS rules or there may be some
// caching issues. In this case there are no desired enpoint candidates so plan
// on deleting the records.
func (suite *PlanTestSuite) TestConflictingCurrentNoDesired() {
suite.fooA5.Labels[endpoint.OwnerLabelKey] = suite.fooV1Cname.Labels[endpoint.OwnerLabelKey]
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5}
desired := []*endpoint.Endpoint{}
expectedCreate := []*endpoint.Endpoint{}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
OwnerID: suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
}
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)
}
// TestCurrentWithConflictingDesired simulates where the desired records result in conflicting records types.
// This could be the result of multiple sources generating conflicting records types. In this case the conflict
// resolver should prefer the A and AAAA record candidate and delete the other records.
func (suite *PlanTestSuite) TestCurrentWithConflictingDesired() {
suite.fooV1Cname.Labels[endpoint.OwnerLabelKey] = "nerf"
current := []*endpoint.Endpoint{suite.fooV1Cname}
desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5, suite.fooAAAA}
expectedCreate := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{suite.fooV1Cname}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
OwnerID: suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
}
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)
}
// TestNoCurrentWithConflictingDesired simulates where the desired records result in conflicting records types.
// This could be the result of multiple sources generating conflicting records types. In this case there the
// conflict resolver should prefer the A and AAAA record and drop the other candidate record types.
func (suite *PlanTestSuite) TestNoCurrentWithConflictingDesired() {
current := []*endpoint.Endpoint{}
desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooA5, suite.fooAAAA}
expectedCreate := []*endpoint.Endpoint{suite.fooA5, suite.fooAAAA}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, 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) TestExcludeTXT() {
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, endpoint.RecordTypeTXT},
ExcludeRecords: []string{endpoint.RecordTypeTXT},
}
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)
}
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{}
domainFilter := endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"})
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.MatchAllDomainFilters{domainFilter},
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{}
domainFilter := endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"})
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.MatchAllDomainFilters{domainFilter},
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) TestAAAARecords() {
current := []*endpoint.Endpoint{}
desired := []*endpoint.Endpoint{suite.fooAAAA}
expectedCreate := []*endpoint.Endpoint{suite.fooAAAA}
expectNoChanges := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Create, expectedCreate)
validateEntries(suite.T(), changes.Delete, expectNoChanges)
validateEntries(suite.T(), changes.UpdateOld(), expectNoChanges)
validateEntries(suite.T(), changes.UpdateNew(), expectNoChanges)
}
func (suite *PlanTestSuite) TestDualStackRecords() {
current := []*endpoint.Endpoint{}
desired := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
expectedCreate := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
expectNoChanges := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Create, expectedCreate)
validateEntries(suite.T(), changes.Delete, expectNoChanges)
validateEntries(suite.T(), changes.UpdateOld(), expectNoChanges)
validateEntries(suite.T(), changes.UpdateNew(), expectNoChanges)
}
func (suite *PlanTestSuite) TestDualStackRecordsDelete() {
current := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
desired := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
expectNoChanges := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Delete, expectedDelete)
validateEntries(suite.T(), changes.Create, expectNoChanges)
validateEntries(suite.T(), changes.UpdateOld(), expectNoChanges)
validateEntries(suite.T(), changes.UpdateNew(), expectNoChanges)
}
func (suite *PlanTestSuite) TestDualStackToSingleStack() {
current := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA}
desired := []*endpoint.Endpoint{suite.dsA}
expectedDelete := []*endpoint.Endpoint{suite.dsAAAA}
expectNoChanges := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Delete, expectedDelete)
validateEntries(suite.T(), changes.Create, expectNoChanges)
validateEntries(suite.T(), changes.UpdateOld(), expectNoChanges)
validateEntries(suite.T(), changes.UpdateNew(), expectNoChanges)
}
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(tt *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.",
},
{
"_foo.com.",
"_foo.com.",
},
{
"\u005Ffoo.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.",
},
{
"點看.org.",
"xn--c1yn36f.org.",
},
{
"nordic-ø.xn--kitty-點看pd34d.com",
"xn--nordic--w1a.xn--xn--kitty-pd34d-hn01b3542b.com.",
},
{
"nordic-ø.kitty😸.com.",
"xn--nordic--w1a.xn--kitty-pd34d.com.",
},
{
" nordic-ø.kitty😸.COM",
"xn--nordic--w1a.xn--kitty-pd34d.com.",
},
{
"xn--nordic--w1a.kitty😸.com.",
"xn--nordic--w1a.xn--kitty-pd34d.com.",
},
{
"*.example.com.",
"*.example.com.",
},
{
"*.example.com",
"*.example.com.",
},
}
for _, r := range records {
tt.Run(r.dnsName, func(t *testing.T) {
gotName := normalizeDNSName(r.dnsName)
assert.Equal(t, r.expect, gotName)
})
}
}
func TestShouldUpdateProviderSpecific(tt *testing.T) {
for _, test := range []struct {
name string
current *endpoint.Endpoint
desired *endpoint.Endpoint
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"},
},
},
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"},
},
},
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"},
},
},
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"},
},
},
shouldUpdate: true,
},
} {
tt.Run(test.name, func(t *testing.T) {
plan := &Plan{
Current: []*endpoint.Endpoint{test.current},
Desired: []*endpoint.Endpoint{test.desired},
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
b := plan.shouldUpdateProviderSpecific(test.desired, test.current)
assert.Equal(t, test.shouldUpdate, b)
})
}
}