Allow for custom property comparators

Fixes issue #1463

Co-authored-by: Alastair Houghton <alastair@alastairs-place.net>
This commit is contained in:
Adam Stankiewicz 2020-05-12 15:59:38 +02:00
parent 2ef1503572
commit f008e894df
39 changed files with 290 additions and 38 deletions

View File

@ -135,10 +135,11 @@ func (c *Controller) RunOnce(ctx context.Context) error {
sourceEndpointsTotal.Set(float64(len(endpoints))) sourceEndpointsTotal.Set(float64(len(endpoints)))
plan := &plan.Plan{ plan := &plan.Plan{
Policies: []plan.Policy{c.Policy}, Policies: []plan.Policy{c.Policy},
Current: records, Current: records,
Desired: endpoints, Desired: endpoints,
DomainFilter: c.DomainFilter, DomainFilter: c.DomainFilter,
PropertyComparator: c.Registry.PropertyValuesEqual,
} }
plan = plan.Calculate() plan = plan.Calculate()

View File

@ -35,6 +35,7 @@ import (
// mockProvider returns mock endpoints and validates changes. // mockProvider returns mock endpoints and validates changes.
type mockProvider struct { type mockProvider struct {
provider.BaseProvider
RecordsStore []*endpoint.Endpoint RecordsStore []*endpoint.Endpoint
ExpectChanges *plan.Changes ExpectChanges *plan.Changes
} }

View File

@ -18,11 +18,15 @@ package plan
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
) )
type PropertyComparator func(name string, previous string, current string) bool
// Plan can convert a list of desired and current records to a series of create, // Plan can convert a list of desired and current records to a series of create,
// update and delete actions. // update and delete actions.
type Plan struct { type Plan struct {
@ -37,6 +41,8 @@ type Plan struct {
Changes *Changes Changes *Changes
// DomainFilter matches DNS names // DomainFilter matches DNS names
DomainFilter endpoint.DomainFilter DomainFilter endpoint.DomainFilter
// Property comparator compares custom properties of providers
PropertyComparator PropertyComparator
} }
// Changes holds lists of actions to be executed by dns providers // Changes holds lists of actions to be executed by dns providers
@ -135,7 +141,7 @@ func (p *Plan) Calculate() *Plan {
if row.current != nil && len(row.candidates) > 0 { //dns name is taken if row.current != nil && len(row.candidates) > 0 { //dns name is taken
update := t.resolver.ResolveUpdate(row.current, row.candidates) update := t.resolver.ResolveUpdate(row.current, row.candidates)
// compare "update" to "current" to figure out if actual update is required // compare "update" to "current" to figure out if actual update is required
if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || shouldUpdateProviderSpecific(update, row.current) { if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || p.shouldUpdateProviderSpecific(update, row.current) {
inheritOwner(row.current, update) inheritOwner(row.current, update)
changes.UpdateNew = append(changes.UpdateNew, update) changes.UpdateNew = append(changes.UpdateNew, update)
changes.UpdateOld = append(changes.UpdateOld, row.current) changes.UpdateOld = append(changes.UpdateOld, row.current)
@ -178,45 +184,40 @@ func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool {
return desired.RecordTTL != current.RecordTTL return desired.RecordTTL != current.RecordTTL
} }
func shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool { func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool {
if current.ProviderSpecific == nil && len(desired.ProviderSpecific) == 0 { desiredProperties := map[string]endpoint.ProviderSpecificProperty{}
return false
}
for _, c := range current.ProviderSpecific {
// don't consider target health when detecting changes
// see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954
if c.Name == "aws/evaluate-target-health" {
continue
}
found := false if desired.ProviderSpecific != nil {
for _, d := range desired.ProviderSpecific { for _, d := range desired.ProviderSpecific {
if d.Name == c.Name { desiredProperties[d.Name] = d
if d.Value != c.Value { }
// provider-specific attribute updated }
if current.ProviderSpecific != nil {
for _, c := range current.ProviderSpecific {
// don't consider target health when detecting changes
// see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954
if c.Name == "aws/evaluate-target-health" {
continue
}
if d, ok := desiredProperties[c.Name]; ok {
if p.PropertyComparator != nil {
if !p.PropertyComparator(c.Name, c.Value, d.Value) {
return true
}
} else if c.Value != d.Value {
return true
}
} else {
if p.PropertyComparator != nil {
if !p.PropertyComparator(c.Name, c.Value, "") {
return true
}
} else if c.Value != "" {
return true return true
} }
found = true
break
} }
} }
if !found {
// provider-specific attribute deleted
return true
}
}
for _, d := range desired.ProviderSpecific {
found := false
for _, c := range current.ProviderSpecific {
if d.Name == c.Name {
found = true
break
}
}
if !found {
// provider-specific attribute added
return true
}
} }
return false return false
@ -260,3 +261,27 @@ func normalizeDNSName(dnsName string) string {
} }
return s return s
} }
func CompareBoolean(defaultValue bool, name, current, previous string) bool {
var err error
v1, v2 := defaultValue, defaultValue
if previous != "" {
v1, err = strconv.ParseBool(previous)
if err != nil {
log.Errorf("Failed to parse previous property [%s]: %v", name, previous)
v1 = defaultValue
}
}
if current != "" {
v2, err = strconv.ParseBool(current)
if err != nil {
log.Errorf("Failed to parse current property [%s]: %v", name, current)
v2 = defaultValue
}
}
return v1 == v2
}

View File

@ -38,6 +38,7 @@ type PlanTestSuite struct {
bar127AWithTTL *endpoint.Endpoint bar127AWithTTL *endpoint.Endpoint
bar127AWithProviderSpecificTrue *endpoint.Endpoint bar127AWithProviderSpecificTrue *endpoint.Endpoint
bar127AWithProviderSpecificFalse *endpoint.Endpoint bar127AWithProviderSpecificFalse *endpoint.Endpoint
bar127AWithProviderSpecificUnset *endpoint.Endpoint
bar192A *endpoint.Endpoint bar192A *endpoint.Endpoint
multiple1 *endpoint.Endpoint multiple1 *endpoint.Endpoint
multiple2 *endpoint.Endpoint multiple2 *endpoint.Endpoint
@ -138,6 +139,15 @@ func (suite *PlanTestSuite) SetupTest() {
}, },
}, },
} }
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{ suite.bar192A = &endpoint.Endpoint{
DNSName: "bar", DNSName: "bar",
Targets: endpoint.Targets{"192.168.0.1"}, Targets: endpoint.Targets{"192.168.0.1"},
@ -291,6 +301,54 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() {
validateEntries(suite.T(), changes.Delete, expectedDelete) 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)
},
}
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() { func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
current := []*endpoint.Endpoint{suite.fooV1Cname} current := []*endpoint.Endpoint{suite.fooV1Cname}
desired := []*endpoint.Endpoint{suite.fooV2Cname} desired := []*endpoint.Endpoint{suite.fooV2Cname}

View File

@ -61,6 +61,7 @@ type AkamaiConfig struct {
// AkamaiProvider implements the DNS provider for Akamai. // AkamaiProvider implements the DNS provider for Akamai.
type AkamaiProvider struct { type AkamaiProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
config edgegrid.Config config edgegrid.Config

View File

@ -67,6 +67,7 @@ type AlibabaCloudPrivateZoneAPI interface {
// AlibabaCloudProvider implements the DNS provider for Alibaba Cloud. // AlibabaCloudProvider implements the DNS provider for Alibaba Cloud.
type AlibabaCloudProvider struct { type AlibabaCloudProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter // Private Zone only zoneIDFilter provider.ZoneIDFilter // Private Zone only
MaxChangeCount int MaxChangeCount int

View File

@ -114,6 +114,7 @@ type Route53API interface {
// AWSProvider is an implementation of Provider for AWS Route53. // AWSProvider is an implementation of Provider for AWS Route53.
type AWSProvider struct { type AWSProvider struct {
provider.BaseProvider
client Route53API client Route53API
dryRun bool dryRun bool
batchChangeSize int batchChangeSize int

View File

@ -37,6 +37,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/apis/externaldns" "sigs.k8s.io/external-dns/pkg/apis/externaldns"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -73,6 +74,7 @@ type AWSSDClient interface {
// AWSSDProvider is an implementation of Provider for AWS Cloud Map. // AWSSDProvider is an implementation of Provider for AWS Cloud Map.
type AWSSDProvider struct { type AWSSDProvider struct {
provider.BaseProvider
client AWSSDClient client AWSSDClient
dryRun bool dryRun bool
// only consider namespaces ending in this suffix // only consider namespaces ending in this suffix

View File

@ -67,6 +67,7 @@ type RecordSetsClient interface {
// AzureProvider implements the DNS provider for Microsoft's Azure cloud platform. // AzureProvider implements the DNS provider for Microsoft's Azure cloud platform.
type AzureProvider struct { type AzureProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
dryRun bool dryRun bool

View File

@ -46,6 +46,7 @@ type PrivateRecordSetsClient interface {
// AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service // AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service
type AzurePrivateDNSProvider struct { type AzurePrivateDNSProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
dryRun bool dryRun bool

View File

@ -101,6 +101,7 @@ func (z zoneService) ListZonesContext(ctx context.Context, opts ...cloudflare.Re
// CloudFlareProvider is an implementation of Provider for CloudFlare DNS. // CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
type CloudFlareProvider struct { type CloudFlareProvider struct {
provider.BaseProvider
Client cloudFlareDNS Client cloudFlareDNS
// only consider hosted zones managing domains ending in this suffix // only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
@ -211,6 +212,14 @@ func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Cha
return p.submitChanges(ctx, combinedChanges) return p.submitChanges(ctx, combinedChanges)
} }
func (p *CloudFlareProvider) PropertyValuesEqual(name string, previous string, current string) bool {
if name == source.CloudflareProxiedKey {
return plan.CompareBoolean(p.proxiedByDefault, name, previous, current)
}
return p.BaseProvider.PropertyValuesEqual(name, previous, current)
}
// submitChanges takes a zone and a collection of Changes and sends them as a single transaction. // submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloudFlareChange) error { func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloudFlareChange) error {
// return early if there is nothing to change // return early if there is nothing to change

View File

@ -903,6 +903,98 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
} }
} }
func TestProviderPropertiesIdempotency(t *testing.T) {
testCases := []struct {
ProviderProxiedByDefault bool
RecordsAreProxied bool
ShouldBeUpdated bool
}{
{
ProviderProxiedByDefault: false,
RecordsAreProxied: false,
ShouldBeUpdated: false,
},
{
ProviderProxiedByDefault: true,
RecordsAreProxied: true,
ShouldBeUpdated: false,
},
{
ProviderProxiedByDefault: true,
RecordsAreProxied: false,
ShouldBeUpdated: true,
},
{
ProviderProxiedByDefault: false,
RecordsAreProxied: true,
ShouldBeUpdated: true,
},
}
for _, test := range testCases {
client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
"001": {
{
ID: "1234567890",
ZoneID: "001",
Name: "foobar.bar.com",
Type: endpoint.RecordTypeA,
TTL: 120,
Content: "1.2.3.4",
Proxied: test.RecordsAreProxied,
},
},
})
provider := &CloudFlareProvider{
Client: client,
proxiedByDefault: test.ProviderProxiedByDefault,
}
ctx := context.Background()
current, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
}
assert.Equal(t, 1, len(current))
desired := []*endpoint.Endpoint{}
for _, c := range current {
// Copy all except ProviderSpecific fields
desired = append(desired, &endpoint.Endpoint{
DNSName: c.DNSName,
Targets: c.Targets,
RecordType: c.RecordType,
SetIdentifier: c.SetIdentifier,
RecordTTL: c.RecordTTL,
Labels: c.Labels,
})
}
plan := plan.Plan{
Current: current,
Desired: desired,
PropertyComparator: provider.PropertyValuesEqual,
}
plan = *plan.Calculate()
assert.NotNil(t, plan.Changes, "should have plan")
if plan.Changes == nil {
return
}
assert.Equal(t, 0, len(plan.Changes.Create), "should not have creates")
assert.Equal(t, 0, len(plan.Changes.Delete), "should not have deletes")
if test.ShouldBeUpdated {
assert.Equal(t, 1, len(plan.Changes.UpdateNew), "should not have new updates")
assert.Equal(t, 1, len(plan.Changes.UpdateOld), "should not have old updates")
} else {
assert.Equal(t, 0, len(plan.Changes.UpdateNew), "should not have new updates")
assert.Equal(t, 0, len(plan.Changes.UpdateOld), "should not have old updates")
}
}
}
func TestCloudflareComplexUpdate(t *testing.T) { func TestCloudflareComplexUpdate(t *testing.T) {
client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{ client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
"001": ExampleDomain, "001": ExampleDomain,

View File

@ -57,6 +57,7 @@ type coreDNSClient interface {
} }
type coreDNSProvider struct { type coreDNSProvider struct {
provider.BaseProvider
dryRun bool dryRun bool
coreDNSPrefix string coreDNSPrefix string
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter

View File

@ -227,6 +227,7 @@ func (c designateClient) DeleteRecordSet(zoneID, recordSetID string) error {
// designate provider type // designate provider type
type designateProvider struct { type designateProvider struct {
provider.BaseProvider
client designateClientInterface client designateClientInterface
// only consider hosted zones managing domains ending in this suffix // only consider hosted zones managing domains ending in this suffix

View File

@ -45,6 +45,7 @@ const (
// DigitalOceanProvider is an implementation of Provider for Digital Ocean's DNS. // DigitalOceanProvider is an implementation of Provider for Digital Ocean's DNS.
type DigitalOceanProvider struct { type DigitalOceanProvider struct {
provider.BaseProvider
Client godo.DomainsService Client godo.DomainsService
// only consider hosted zones managing domains ending in this suffix // only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter

View File

@ -77,6 +77,7 @@ func (z dnsimpleZoneService) UpdateRecord(ctx context.Context, accountID string,
} }
type dnsimpleProvider struct { type dnsimpleProvider struct {
provider.BaseProvider
client dnsimpleZoneServiceInterface client dnsimpleZoneServiceInterface
identity dnsimpleIdentityService identity dnsimpleIdentityService
accountID string accountID string

View File

@ -104,6 +104,7 @@ func (snap *ZoneSnapshot) StoreRecordsForSerial(zone string, serial int, records
// DynProvider is the actual interface impl. // DynProvider is the actual interface impl.
type dynProviderState struct { type dynProviderState struct {
provider.BaseProvider
DynConfig DynConfig
LastLoginErrorTime int64 LastLoginErrorTime int64

View File

@ -25,6 +25,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
// EgoscaleClientI for replaceable implementation // EgoscaleClientI for replaceable implementation
@ -38,6 +39,7 @@ type EgoscaleClientI interface {
// ExoscaleProvider initialized as dns provider with no records // ExoscaleProvider initialized as dns provider with no records
type ExoscaleProvider struct { type ExoscaleProvider struct {
provider.BaseProvider
domain endpoint.DomainFilter domain endpoint.DomainFilter
client EgoscaleClientI client EgoscaleClientI
filter *zoneFilter filter *zoneFilter

View File

@ -99,6 +99,7 @@ func (c changesService) Create(project string, managedZone string, change *dns.C
// GoogleProvider is an implementation of Provider for Google CloudDNS. // GoogleProvider is an implementation of Provider for Google CloudDNS.
type GoogleProvider struct { type GoogleProvider struct {
provider.BaseProvider
// The Google project to work in // The Google project to work in
project string project string
// Enabled dry-run will print any modifying actions rather than execute them. // Enabled dry-run will print any modifying actions rather than execute them.

View File

@ -49,6 +49,7 @@ type InfobloxConfig struct {
// InfobloxProvider implements the DNS provider for Infoblox. // InfobloxProvider implements the DNS provider for Infoblox.
type InfobloxProvider struct { type InfobloxProvider struct {
provider.BaseProvider
client ibclient.IBConnector client ibclient.IBConnector
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter zoneIDFilter provider.ZoneIDFilter

View File

@ -25,6 +25,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
var ( var (
@ -43,6 +44,7 @@ var (
// InMemoryProvider - dns provider only used for testing purposes // InMemoryProvider - dns provider only used for testing purposes
// initialized as dns provider with no records // initialized as dns provider with no records
type InMemoryProvider struct { type InMemoryProvider struct {
provider.BaseProvider
domain endpoint.DomainFilter domain endpoint.DomainFilter
client *inMemoryClient client *inMemoryClient
filter *filter filter *filter

View File

@ -44,6 +44,7 @@ type LinodeDomainClient interface {
// LinodeProvider is an implementation of Provider for Digital Ocean's DNS. // LinodeProvider is an implementation of Provider for Digital Ocean's DNS.
type LinodeProvider struct { type LinodeProvider struct {
provider.BaseProvider
Client LinodeDomainClient Client LinodeDomainClient
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
DryRun bool DryRun bool

View File

@ -94,6 +94,7 @@ type NS1Config struct {
// NS1Provider is the NS1 provider // NS1Provider is the NS1 provider
type NS1Provider struct { type NS1Provider struct {
provider.BaseProvider
client NS1DomainClient client NS1DomainClient
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter zoneIDFilter provider.ZoneIDFilter

View File

@ -53,6 +53,7 @@ type OCIConfig struct {
// OCIProvider is an implementation of Provider for Oracle Cloud Infrastructure // OCIProvider is an implementation of Provider for Oracle Cloud Infrastructure
// (OCI) DNS. // (OCI) DNS.
type OCIProvider struct { type OCIProvider struct {
provider.BaseProvider
client ociDNSClient client ociDNSClient
cfg OCIConfig cfg OCIConfig

View File

@ -46,6 +46,8 @@ var (
// OVHProvider is an implementation of Provider for OVH DNS. // OVHProvider is an implementation of Provider for OVH DNS.
type OVHProvider struct { type OVHProvider struct {
provider.BaseProvider
client ovhClient client ovhClient
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter

View File

@ -222,6 +222,7 @@ func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *htt
// PDNSProvider is an implementation of the Provider interface for PowerDNS // PDNSProvider is an implementation of the Provider interface for PowerDNS
type PDNSProvider struct { type PDNSProvider struct {
provider.BaseProvider
client PDNSAPIProvider client PDNSAPIProvider
} }

View File

@ -29,6 +29,14 @@ import (
type Provider interface { type Provider interface {
Records(ctx context.Context) ([]*endpoint.Endpoint, error) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
ApplyChanges(ctx context.Context, changes *plan.Changes) error ApplyChanges(ctx context.Context, changes *plan.Changes) error
PropertyValuesEqual(name string, previous string, current string) bool
}
type BaseProvider struct {
}
func (b BaseProvider) PropertyValuesEqual(name, previous, current string) bool {
return previous == current
} }
type contextKey struct { type contextKey struct {

View File

@ -22,6 +22,7 @@ import (
"testing" "testing"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -44,3 +45,12 @@ func TestEnsureTrailingDot(t *testing.T) {
} }
} }
} }
func TestBaseProviderPropertyEquality(t *testing.T) {
p := BaseProvider{}
assert.True(t, p.PropertyValuesEqual("some.property", "", ""), "Both properties not present")
assert.False(t, p.PropertyValuesEqual("some.property", "", "Foo"), "First property missing")
assert.False(t, p.PropertyValuesEqual("some.property", "Foo", ""), "Second property missing")
assert.True(t, p.PropertyValuesEqual("some.property", "Foo", "Foo"), "Properties the same")
assert.False(t, p.PropertyValuesEqual("some.property", "Foo", "Bar"), "Attributes differ")
}

View File

@ -33,6 +33,7 @@ import (
// RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS. // RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS.
type RcodeZeroProvider struct { type RcodeZeroProvider struct {
provider.BaseProvider
Client *rc0.Client Client *rc0.Client
DomainFilter endpoint.DomainFilter DomainFilter endpoint.DomainFilter

View File

@ -35,6 +35,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -66,6 +67,7 @@ type RDNSConfig struct {
// RDNSProvider is an implementation of Provider for Rancher DNS(RDNS). // RDNSProvider is an implementation of Provider for Rancher DNS(RDNS).
type RDNSProvider struct { type RDNSProvider struct {
provider.BaseProvider
client RDNSClient client RDNSClient
dryRun bool dryRun bool
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter

View File

@ -35,6 +35,7 @@ import (
// rfc2136 provider type // rfc2136 provider type
type rfc2136Provider struct { type rfc2136Provider struct {
provider.BaseProvider
nameserver string nameserver string
zoneName string zoneName string
tsigKeyName string tsigKeyName string

View File

@ -39,6 +39,7 @@ const (
// TransIPProvider is an implementation of Provider for TransIP. // TransIPProvider is an implementation of Provider for TransIP.
type TransIPProvider struct { type TransIPProvider struct {
provider.BaseProvider
client gotransip.SOAPClient client gotransip.SOAPClient
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
dryRun bool dryRun bool

View File

@ -48,6 +48,7 @@ type vinyldnsZoneInterface interface {
} }
type vinyldnsProvider struct { type vinyldnsProvider struct {
provider.BaseProvider
client vinyldnsZoneInterface client vinyldnsZoneInterface
zoneFilter provider.ZoneIDFilter zoneFilter provider.ZoneIDFilter
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter

View File

@ -39,6 +39,7 @@ const (
// VultrProvider is an implementation of Provider for Vultr DNS. // VultrProvider is an implementation of Provider for Vultr DNS.
type VultrProvider struct { type VultrProvider struct {
provider.BaseProvider
client govultr.Client client govultr.Client
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter

View File

@ -87,3 +87,7 @@ func (sdr *AWSSDRegistry) updateLabels(endpoints []*endpoint.Endpoint) {
ep.Labels[endpoint.AWSSDDescriptionLabel] = ep.Labels.Serialize(false) ep.Labels[endpoint.AWSSDDescriptionLabel] = ep.Labels.Serialize(false)
} }
} }
func (sdr *AWSSDRegistry) PropertyValuesEqual(name string, previous string, current string) bool {
return sdr.provider.PropertyValuesEqual(name, previous, current)
}

View File

@ -26,9 +26,11 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type inMemoryProvider struct { type inMemoryProvider struct {
provider.BaseProvider
endpoints []*endpoint.Endpoint endpoints []*endpoint.Endpoint
onApplyChanges func(changes *plan.Changes) onApplyChanges func(changes *plan.Changes)
} }

View File

@ -45,3 +45,8 @@ func (im *NoopRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, erro
func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
return im.provider.ApplyChanges(ctx, changes) return im.provider.ApplyChanges(ctx, changes)
} }
// PropertyValuesEqual compares two property values for equality
func (im *NoopRegistry) PropertyValuesEqual(attribute string, previous string, current string) bool {
return im.provider.PropertyValuesEqual(attribute, previous, current)
}

View File

@ -32,6 +32,7 @@ import (
type Registry interface { type Registry interface {
Records(ctx context.Context) ([]*endpoint.Endpoint, error) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
ApplyChanges(ctx context.Context, changes *plan.Changes) error ApplyChanges(ctx context.Context, changes *plan.Changes) error
PropertyValuesEqual(attribute string, previous string, current string) bool
} }
//TODO(ideahitme): consider moving this to Plan //TODO(ideahitme): consider moving this to Plan

View File

@ -187,6 +187,11 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
return im.provider.ApplyChanges(ctx, filteredChanges) return im.provider.ApplyChanges(ctx, filteredChanges)
} }
// PropertyValuesEqual compares two attribute values for equality
func (im *TXTRegistry) PropertyValuesEqual(name string, previous string, current string) bool {
return im.provider.PropertyValuesEqual(name, previous, current)
}
/** /**
TXT registry specific private methods TXT registry specific private methods
*/ */