/* 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 cloudflare import ( "context" "errors" "os" "testing" cloudflare "github.com/cloudflare/cloudflare-go" "github.com/stretchr/testify/assert" "github.com/maxatome/go-testdeep/td" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" ) type MockAction struct { Name string ZoneId string RecordId string RecordData cloudflare.DNSRecord } type mockCloudFlareClient struct { User cloudflare.User Zones map[string]string Records map[string]map[string]cloudflare.DNSRecord Actions []MockAction listZonesError error dnsRecordsError error } var ExampleDomain = []cloudflare.DNSRecord{ { ID: "1234567890", ZoneID: "001", Name: "foobar.bar.com", Type: endpoint.RecordTypeA, TTL: 120, Content: "1.2.3.4", Proxied: proxyDisabled, }, { ID: "2345678901", ZoneID: "001", Name: "foobar.bar.com", Type: endpoint.RecordTypeA, TTL: 120, Content: "3.4.5.6", Proxied: proxyDisabled, }, { ID: "1231231233", ZoneID: "002", Name: "bar.foo.com", Type: endpoint.RecordTypeA, TTL: 1, Content: "2.3.4.5", Proxied: proxyDisabled, }, } func NewMockCloudFlareClient() *mockCloudFlareClient { return &mockCloudFlareClient{ User: cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, Zones: map[string]string{ "001": "bar.com", "002": "foo.com", }, Records: map[string]map[string]cloudflare.DNSRecord{ "001": {}, "002": {}, }, } } func NewMockCloudFlareClientWithRecords(records map[string][]cloudflare.DNSRecord) *mockCloudFlareClient { m := NewMockCloudFlareClient() for zoneID, zoneRecords := range records { if zone, ok := m.Records[zoneID]; ok { for _, record := range zoneRecords { zone[record.ID] = record } } } return m } func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { m.Actions = append(m.Actions, MockAction{ Name: "Create", ZoneId: zoneID, RecordId: rr.ID, RecordData: rr, }) if zone, ok := m.Records[zoneID]; ok { zone[rr.ID] = rr } return nil, nil } func (m *mockCloudFlareClient) DNSRecords(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { if m.dnsRecordsError != nil { return nil, m.dnsRecordsError } result := []cloudflare.DNSRecord{} if zone, ok := m.Records[zoneID]; ok { for _, record := range zone { result = append(result, record) } return result, nil } return result, nil } func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, zoneID, recordID string, rr cloudflare.DNSRecord) error { m.Actions = append(m.Actions, MockAction{ Name: "Update", ZoneId: zoneID, RecordId: recordID, RecordData: rr, }) if zone, ok := m.Records[zoneID]; ok { if _, ok := zone[recordID]; ok { zone[recordID] = rr } } return nil } func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error { m.Actions = append(m.Actions, MockAction{ Name: "Delete", ZoneId: zoneID, RecordId: recordID, }) if zone, ok := m.Records[zoneID]; ok { if _, ok := zone[recordID]; ok { delete(zone, recordID) return nil } } return nil } func (m *mockCloudFlareClient) UserDetails(ctx context.Context) (cloudflare.User, error) { return m.User, nil } func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) { for id, name := range m.Zones { if name == zoneName { return id, nil } } return "", errors.New("Unknown zone: " + zoneName) } func (m *mockCloudFlareClient) ListZones(ctx context.Context, zoneID ...string) ([]cloudflare.Zone, error) { if m.listZonesError != nil { return nil, m.listZonesError } result := []cloudflare.Zone{} for zoneID, zoneName := range m.Zones { result = append(result, cloudflare.Zone{ ID: zoneID, Name: zoneName, }) } return result, nil } func (m *mockCloudFlareClient) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) { if m.listZonesError != nil { return cloudflare.ZonesResponse{}, m.listZonesError } result := []cloudflare.Zone{} for zoneId, zoneName := range m.Zones { result = append(result, cloudflare.Zone{ ID: zoneId, Name: zoneName, }) } return cloudflare.ZonesResponse{ Result: result, ResultInfo: cloudflare.ResultInfo{ Page: 1, TotalPages: 1, }, }, nil } func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) { for id, zoneName := range m.Zones { if zoneID == id { return cloudflare.Zone{ ID: zoneID, Name: zoneName, }, nil } } return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID) } func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, managedRecords []string, args ...interface{}) { t.Helper() var client *mockCloudFlareClient if provider.Client == nil { client = NewMockCloudFlareClient() provider.Client = client } else { client = provider.Client.(*mockCloudFlareClient) } ctx := context.Background() records, err := provider.Records(ctx) if err != nil { t.Fatalf("cannot fetch records, %s", err) } plan := &plan.Plan{ Current: records, Desired: endpoints, DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}), ManagedRecords: managedRecords, } changes := plan.Calculate().Changes // Records other than A, CNAME and NS are not supported by planner, just create them for _, endpoint := range endpoints { if endpoint.RecordType != "A" && endpoint.RecordType != "CNAME" && endpoint.RecordType != "NS" { changes.Create = append(changes.Create, endpoint) } } err = provider.ApplyChanges(context.Background(), changes) if err != nil { t.Fatalf("cannot apply changes, %s", err) } td.Cmp(t, client.Actions, actions, args...) } func TestCloudflareA(t *testing.T) { endpoints := []*endpoint.Endpoint{ { RecordType: "A", DNSName: "bar.com", Targets: endpoint.Targets{"127.0.0.1", "127.0.0.2"}, }, } AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: "A", Name: "bar.com", Content: "127.0.0.1", TTL: 1, Proxied: proxyDisabled, }, }, { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: "A", Name: "bar.com", Content: "127.0.0.2", TTL: 1, Proxied: proxyDisabled, }, }, }, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, ) } func TestCloudflareCname(t *testing.T) { endpoints := []*endpoint.Endpoint{ { RecordType: "CNAME", DNSName: "cname.bar.com", Targets: endpoint.Targets{"google.com", "facebook.com"}, }, } AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: "CNAME", Name: "cname.bar.com", Content: "google.com", TTL: 1, Proxied: proxyDisabled, }, }, { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: "CNAME", Name: "cname.bar.com", Content: "facebook.com", TTL: 1, Proxied: proxyDisabled, }, }, }, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, ) } func TestCloudflareCustomTTL(t *testing.T) { endpoints := []*endpoint.Endpoint{ { RecordType: "A", DNSName: "ttl.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, RecordTTL: 120, }, } AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: "A", Name: "ttl.bar.com", Content: "127.0.0.1", TTL: 120, Proxied: proxyDisabled, }, }, }, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, ) } func TestCloudflareProxiedDefault(t *testing.T) { endpoints := []*endpoint.Endpoint{ { RecordType: "A", DNSName: "bar.com", Targets: endpoint.Targets{"127.0.0.1"}, }, } AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: "A", Name: "bar.com", Content: "127.0.0.1", TTL: 1, Proxied: proxyEnabled, }, }, }, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, ) } func TestCloudflareProxiedOverrideTrue(t *testing.T) { endpoints := []*endpoint.Endpoint{ { RecordType: "A", DNSName: "bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ endpoint.ProviderSpecificProperty{ Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "true", }, }, }, } AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: "A", Name: "bar.com", Content: "127.0.0.1", TTL: 1, Proxied: proxyEnabled, }, }, }, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, ) } func TestCloudflareProxiedOverrideFalse(t *testing.T) { endpoints := []*endpoint.Endpoint{ { RecordType: "A", DNSName: "bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ endpoint.ProviderSpecificProperty{ Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "false", }, }, }, } AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: "A", Name: "bar.com", Content: "127.0.0.1", TTL: 1, Proxied: proxyDisabled, }, }, }, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, ) } func TestCloudflareProxiedOverrideIllegal(t *testing.T) { endpoints := []*endpoint.Endpoint{ { RecordType: "A", DNSName: "bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ endpoint.ProviderSpecificProperty{ Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "asfasdfa", }, }, }, } AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: "A", Name: "bar.com", Content: "127.0.0.1", TTL: 1, Proxied: proxyEnabled, }, }, }, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, ) } func TestCloudflareSetProxied(t *testing.T) { var proxied *bool = proxyEnabled var notProxied *bool = proxyDisabled testCases := []struct { recordType string domain string proxiable *bool }{ {"A", "bar.com", proxied}, {"CNAME", "bar.com", proxied}, {"TXT", "bar.com", notProxied}, {"MX", "bar.com", notProxied}, {"NS", "bar.com", notProxied}, {"SPF", "bar.com", notProxied}, {"SRV", "bar.com", notProxied}, {"A", "*.bar.com", proxied}, {"CNAME", "*.docs.bar.com", proxied}, } for _, testCase := range testCases { endpoints := []*endpoint.Endpoint{ { RecordType: testCase.recordType, DNSName: testCase.domain, Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ endpoint.ProviderSpecificProperty{ Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "true", }, }, }, } AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Type: testCase.recordType, Name: testCase.domain, Content: "127.0.0.1", TTL: 1, Proxied: testCase.proxiable, }, }, }, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, testCase.recordType+" record on "+testCase.domain) } } func TestCloudflareZones(t *testing.T) { provider := &CloudFlareProvider{ Client: NewMockCloudFlareClient(), domainFilter: endpoint.NewDomainFilter([]string{"bar.com"}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}), } zones, err := provider.Zones(context.Background()) if err != nil { t.Fatal(err) } assert.Equal(t, 1, len(zones)) assert.Equal(t, "bar.com", zones[0].Name) } func TestCloudFlareZonesWithIDFilter(t *testing.T) { client := NewMockCloudFlareClient() client.listZonesError = errors.New("shouldn't need to list zones when ZoneIDFilter in use") provider := &CloudFlareProvider{ Client: client, domainFilter: endpoint.NewDomainFilter([]string{"bar.com", "foo.com"}), zoneIDFilter: provider.NewZoneIDFilter([]string{"001"}), } zones, err := provider.Zones(context.Background()) if err != nil { t.Fatal(err) } // foo.com should *not* be returned as it doesn't match ZoneID filter assert.Equal(t, 1, len(zones)) assert.Equal(t, "bar.com", zones[0].Name) } func TestCloudflareRecords(t *testing.T) { client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{ "001": ExampleDomain, }) provider := &CloudFlareProvider{ Client: client, } ctx := context.Background() records, err := provider.Records(ctx) if err != nil { t.Errorf("should not fail, %s", err) } assert.Equal(t, 2, len(records)) client.dnsRecordsError = errors.New("failed to list dns records") _, err = provider.Records(ctx) if err == nil { t.Errorf("expected to fail") } client.dnsRecordsError = nil client.listZonesError = errors.New("failed to list zones") _, err = provider.Records(ctx) if err == nil { t.Errorf("expected to fail") } } func TestCloudflareProvider(t *testing.T) { _ = os.Setenv("CF_API_TOKEN", "abc123def") _, err := NewCloudFlareProvider( endpoint.NewDomainFilter([]string{"bar.com"}), provider.NewZoneIDFilter([]string{""}), 25, false, true) if err != nil { t.Errorf("should not fail, %s", err) } _ = os.Unsetenv("CF_API_TOKEN") _ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx") _ = os.Setenv("CF_API_EMAIL", "test@test.com") _, err = NewCloudFlareProvider( endpoint.NewDomainFilter([]string{"bar.com"}), provider.NewZoneIDFilter([]string{""}), 1, false, true) if err != nil { t.Errorf("should not fail, %s", err) } _ = os.Unsetenv("CF_API_KEY") _ = os.Unsetenv("CF_API_EMAIL") _, err = NewCloudFlareProvider( endpoint.NewDomainFilter([]string{"bar.com"}), provider.NewZoneIDFilter([]string{""}), 50, false, true) if err == nil { t.Errorf("expected to fail") } } func TestCloudflareApplyChanges(t *testing.T) { changes := &plan.Changes{} client := NewMockCloudFlareClient() provider := &CloudFlareProvider{ Client: client, } changes.Create = []*endpoint.Endpoint{{ DNSName: "new.bar.com", Targets: endpoint.Targets{"target"}, }, { DNSName: "new.ext-dns-test.unrelated.to", Targets: endpoint.Targets{"target"}, }} changes.Delete = []*endpoint.Endpoint{{ DNSName: "foobar.bar.com", Targets: endpoint.Targets{"target"}, }} changes.UpdateOld = []*endpoint.Endpoint{{ DNSName: "foobar.bar.com", Targets: endpoint.Targets{"target-old"}, }} changes.UpdateNew = []*endpoint.Endpoint{{ DNSName: "foobar.bar.com", Targets: endpoint.Targets{"target-new"}, }} err := provider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("should not fail, %s", err) } td.Cmp(t, client.Actions, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Name: "new.bar.com", Content: "target", TTL: 1, Proxied: proxyDisabled, }, }, { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Name: "foobar.bar.com", Content: "target-new", TTL: 1, Proxied: proxyDisabled, }, }, }) // empty changes changes.Create = []*endpoint.Endpoint{} changes.Delete = []*endpoint.Endpoint{} changes.UpdateOld = []*endpoint.Endpoint{} changes.UpdateNew = []*endpoint.Endpoint{} err = provider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("should not fail, %s", err) } } func TestCloudflareGetRecordID(t *testing.T) { p := &CloudFlareProvider{} records := []cloudflare.DNSRecord{ { Name: "foo.com", Type: endpoint.RecordTypeCNAME, Content: "foobar", ID: "1", }, { Name: "bar.de", Type: endpoint.RecordTypeA, ID: "2", }, { Name: "bar.de", Type: endpoint.RecordTypeA, Content: "1.2.3.4", ID: "2", }, } assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{ Name: "foo.com", Type: endpoint.RecordTypeA, Content: "foobar", })) assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{ Name: "foo.com", Type: endpoint.RecordTypeCNAME, Content: "fizfuz", })) assert.Equal(t, "1", p.getRecordID(records, cloudflare.DNSRecord{ Name: "foo.com", Type: endpoint.RecordTypeCNAME, Content: "foobar", })) assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{ Name: "bar.de", Type: endpoint.RecordTypeA, Content: "2.3.4.5", })) assert.Equal(t, "2", p.getRecordID(records, cloudflare.DNSRecord{ Name: "bar.de", Type: endpoint.RecordTypeA, Content: "1.2.3.4", })) } func TestCloudflareGroupByNameAndType(t *testing.T) { testCases := []struct { Name string Records []cloudflare.DNSRecord ExpectedEndpoints []*endpoint.Endpoint }{ { Name: "empty", Records: []cloudflare.DNSRecord{}, ExpectedEndpoints: []*endpoint.Endpoint{}, }, { Name: "single record - single target", Records: []cloudflare.DNSRecord{ { Name: "foo.com", Type: endpoint.RecordTypeA, Content: "10.10.10.1", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, }, ExpectedEndpoints: []*endpoint.Endpoint{ { DNSName: "foo.com", Targets: endpoint.Targets{"10.10.10.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ { Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "false", }, }, }, }, }, { Name: "single record - multiple targets", Records: []cloudflare.DNSRecord{ { Name: "foo.com", Type: endpoint.RecordTypeA, Content: "10.10.10.1", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, { Name: "foo.com", Type: endpoint.RecordTypeA, Content: "10.10.10.2", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, }, ExpectedEndpoints: []*endpoint.Endpoint{ { DNSName: "foo.com", Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ { Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "false", }, }, }, }, }, { Name: "multiple record - multiple targets", Records: []cloudflare.DNSRecord{ { Name: "foo.com", Type: endpoint.RecordTypeA, Content: "10.10.10.1", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, { Name: "foo.com", Type: endpoint.RecordTypeA, Content: "10.10.10.2", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, { Name: "bar.de", Type: endpoint.RecordTypeA, Content: "10.10.10.1", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, { Name: "bar.de", Type: endpoint.RecordTypeA, Content: "10.10.10.2", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, }, ExpectedEndpoints: []*endpoint.Endpoint{ { DNSName: "foo.com", Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ { Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "false", }, }, }, { DNSName: "bar.de", Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ { Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "false", }, }, }, }, }, { Name: "multiple record - mixed single/multiple targets", Records: []cloudflare.DNSRecord{ { Name: "foo.com", Type: endpoint.RecordTypeA, Content: "10.10.10.1", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, { Name: "foo.com", Type: endpoint.RecordTypeA, Content: "10.10.10.2", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, { Name: "bar.de", Type: endpoint.RecordTypeA, Content: "10.10.10.1", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, }, ExpectedEndpoints: []*endpoint.Endpoint{ { DNSName: "foo.com", Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ { Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "false", }, }, }, { DNSName: "bar.de", Targets: endpoint.Targets{"10.10.10.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ { Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "false", }, }, }, }, }, { Name: "unsupported record type", Records: []cloudflare.DNSRecord{ { Name: "foo.com", Type: endpoint.RecordTypeA, Content: "10.10.10.1", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, { Name: "foo.com", Type: endpoint.RecordTypeA, Content: "10.10.10.2", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, { Name: "bar.de", Type: "NOT SUPPORTED", Content: "10.10.10.1", TTL: defaultCloudFlareRecordTTL, Proxied: proxyDisabled, }, }, ExpectedEndpoints: []*endpoint.Endpoint{ { DNSName: "foo.com", Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ { Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "false", }, }, }, }, }, } for _, tc := range testCases { assert.ElementsMatch(t, groupByNameAndType(tc.Records), tc.ExpectedEndpoints) } } func TestProviderPropertiesIdempotency(t *testing.T) { testCases := []struct { Name string ProviderProxiedByDefault bool RecordsAreProxied *bool ShouldBeUpdated bool }{ { Name: "ProxyDefault: false, ShouldBeProxied: false, ExpectUpdates: false", ProviderProxiedByDefault: false, RecordsAreProxied: proxyDisabled, ShouldBeUpdated: false, }, { Name: "ProxyDefault: true, ShouldBeProxied: true, ExpectUpdates: false", ProviderProxiedByDefault: true, RecordsAreProxied: proxyEnabled, ShouldBeUpdated: false, }, { Name: "ProxyDefault: true, ShouldBeProxied: false, ExpectUpdates: true", ProviderProxiedByDefault: true, RecordsAreProxied: proxyDisabled, ShouldBeUpdated: true, }, { Name: "ProxyDefault: false, ShouldBeProxied: true, ExpectUpdates: true", ProviderProxiedByDefault: false, RecordsAreProxied: proxyEnabled, ShouldBeUpdated: true, }, } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { 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, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } 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) { client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{ "001": ExampleDomain, }) provider := &CloudFlareProvider{ Client: client, } ctx := context.Background() records, err := provider.Records(ctx) if err != nil { t.Errorf("should not fail, %s", err) } plan := &plan.Plan{ Current: records, Desired: []*endpoint.Endpoint{ { DNSName: "foobar.bar.com", Targets: endpoint.Targets{"1.2.3.4", "2.3.4.5"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ { Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "true", }, }, }, }, DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}), ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } planned := plan.Calculate() err = provider.ApplyChanges(context.Background(), planned.Changes) if err != nil { t.Errorf("should not fail, %s", err) } td.CmpDeeply(t, client.Actions, []MockAction{ { Name: "Create", ZoneId: "001", RecordData: cloudflare.DNSRecord{ Name: "foobar.bar.com", Type: "A", Content: "2.3.4.5", TTL: 1, Proxied: proxyEnabled, }, }, { Name: "Update", ZoneId: "001", RecordId: "1234567890", RecordData: cloudflare.DNSRecord{ Name: "foobar.bar.com", Type: "A", Content: "1.2.3.4", TTL: 1, Proxied: proxyEnabled, }, }, { Name: "Delete", ZoneId: "001", RecordId: "2345678901", }, }) } func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) { client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{ "001": { { ID: "1234567890", ZoneID: "001", Name: "foobar.bar.com", Type: endpoint.RecordTypeA, TTL: 1, Content: "1.2.3.4", Proxied: proxyEnabled, }, }, }) provider := &CloudFlareProvider{ Client: client, } records, err := provider.Records(context.Background()) if err != nil { t.Errorf("should not fail, %s", err) } endpoints := []*endpoint.Endpoint{ { DNSName: "foobar.bar.com", Targets: endpoint.Targets{"1.2.3.4"}, RecordType: endpoint.RecordTypeA, RecordTTL: 300, Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ { Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Value: "true", }, }, }, } provider.AdjustEndpoints(endpoints) plan := &plan.Plan{ Current: records, Desired: endpoints, DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}), ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, } planned := plan.Calculate() assert.Equal(t, 0, len(planned.Changes.Create), "no new changes should be here") assert.Equal(t, 0, len(planned.Changes.UpdateNew), "no new changes should be here") assert.Equal(t, 0, len(planned.Changes.UpdateOld), "no new changes should be here") assert.Equal(t, 0, len(planned.Changes.Delete), "no new changes should be here") }