diff --git a/provider/cloudflare.go b/provider/cloudflare.go index 85ff411c6c..cecb810c39 100644 --- a/provider/cloudflare.go +++ b/provider/cloudflare.go @@ -181,11 +181,10 @@ func (p *CloudFlareProvider) Records() ([]*endpoint.Endpoint, error) { return nil, err } - for _, r := range records { - if supportedRecordType(r.Type) { - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Content).WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(r.Proxied))) - } - } + // As CloudFlare does not support "sets" of targets, but instead returns + // a single entry for each name/type/target, we have to group by name + // and record to allow the planner to calculate the correct plan. See #992. + endpoints = append(endpoints, groupByNameAndType(records)...) } return endpoints, nil @@ -354,3 +353,40 @@ func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool { } return proxied } + +func groupByNameAndType(records []cloudflare.DNSRecord) []*endpoint.Endpoint { + endpoints := []*endpoint.Endpoint{} + + // group supported records by name and type + groups := map[string][]cloudflare.DNSRecord{} + + for _, r := range records { + if !supportedRecordType(r.Type) { + continue + } + + groupBy := r.Name + r.Type + if _, ok := groups[groupBy]; !ok { + groups[groupBy] = []cloudflare.DNSRecord{} + } + + groups[groupBy] = append(groups[groupBy], r) + } + + // create single endpoint with all the targets for each name/type + for _, records := range groups { + targets := make([]string, len(records)) + for i, record := range records { + targets[i] = record.Content + } + endpoints = append(endpoints, + endpoint.NewEndpointWithTTL( + records[0].Name, + records[0].Type, + endpoint.TTL(records[0].TTL), + targets...). + WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(records[0].Proxied))) + } + + return endpoints +} diff --git a/provider/cloudflare_test.go b/provider/cloudflare_test.go index 73e32d59a7..1c3b688546 100644 --- a/provider/cloudflare_test.go +++ b/provider/cloudflare_test.go @@ -595,3 +595,225 @@ func validateCloudFlareZones(t *testing.T, zones []cloudflare.Zone, expected []c assert.Equal(t, expected[i].Name, zone.Name) } } + +func TestGroupByNameAndType(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, + }, + }, + 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, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + }, + 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, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + }, + 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, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + }, + 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, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: "NOT SUPPORTED", + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + }, + 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.Equal(t, groupByNameAndType(tc.Records), tc.ExpectedEndpoints) + } +}