diff --git a/controller/execute.go b/controller/execute.go index a69a1a7ae..ac4b97ffe 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -229,6 +229,7 @@ func buildProvider( cloudflare.DNSRecordsConfig{ PerPage: cfg.CloudflareDNSRecordsPerPage, Comment: cfg.CloudflareDNSRecordsComment, + Tags: cfg.CloudflareDNSRecordsTags, }) case "google": p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun) diff --git a/docs/flags.md b/docs/flags.md index 59f928882..cccf6739e 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -96,6 +96,7 @@ | `--[no-]cloudflare-regional-services` | When using the Cloudflare provider, specify if Regional Services feature will be used (default: disabled) | | `--cloudflare-region-key=CLOUDFLARE-REGION-KEY` | When using the Cloudflare provider, specify the default region for Regional Services. Any value other than an empty string will enable the Regional Services feature (optional) | | `--cloudflare-record-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (default: '') | +| `--cloudflare-record-tags=""` | When using the Cloudflare provider for a paid zone, specify the tags for the DNS records as a comma-separated string (default: '') | | `--coredns-prefix="/skydns/"` | When using the CoreDNS provider, specify the prefix name | | `--akamai-serviceconsumerdomain=""` | When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified) | | `--akamai-client-token=""` | When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified) | diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index c4df4e35f..4b042962e 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -111,6 +111,7 @@ type Config struct { CloudflareCustomHostnames bool CloudflareDNSRecordsPerPage int CloudflareDNSRecordsComment string + CloudflareDNSRecordsTags string CloudflareCustomHostnamesMinTLSVersion string CloudflareCustomHostnamesCertificateAuthority string CloudflareRegionalServices bool @@ -535,6 +536,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("cloudflare-regional-services", "When using the Cloudflare provider, specify if Regional Services feature will be used (default: disabled)").Default(strconv.FormatBool(defaultConfig.CloudflareRegionalServices)).BoolVar(&cfg.CloudflareRegionalServices) app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the default region for Regional Services. Any value other than an empty string will enable the Regional Services feature (optional)").StringVar(&cfg.CloudflareRegionKey) app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareDNSRecordsComment) + app.Flag("cloudflare-record-tags", "When using the Cloudflare provider for a paid zone, specify the tags for the DNS records as a comma-separated string (default: '')").Default("").StringVar(&cfg.CloudflareDNSRecordsTags) app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix) app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain) diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 8914032cc..1b6e9be56 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -170,6 +170,7 @@ func (z zoneService) CreateCustomHostname(ctx context.Context, zoneID string, ch type DNSRecordsConfig struct { PerPage int Comment string + Tags string } func (c *DNSRecordsConfig) trimAndValidateComment(dnsName, comment string, paidZone func(string) bool) string { @@ -190,12 +191,41 @@ func (c *DNSRecordsConfig) trimAndValidateComment(dnsName, comment string, paidZ return comment } +func (c *DNSRecordsConfig) validTags(dnsName string, paidZone func(string) bool, tagsFromAnnotation []string) []string { + if len(tagsFromAnnotation) > 0 || c.Tags != "" { + paidZone := paidZone(dnsName) + if !paidZone { + log.Warnf("DNS record tags are not supported for free zones. Skipping for %s", dnsName) + c.Tags = "" + return nil + } + } + + if len(tagsFromAnnotation) > 0 { + sort.Strings(tagsFromAnnotation) + return tagsFromAnnotation + } + + if c.Tags != "" { + tags := strings.Split(c.Tags, ",") + sort.Strings(tags) + return tags + } + + return nil +} + func (p *CloudFlareProvider) ZoneHasPaidPlan(hostname string) bool { zone, err := publicsuffix.EffectiveTLDPlusOne(hostname) if err != nil { log.Errorf("Failed to get effective TLD+1 for hostname %s %v", hostname, err) return false } + + if paidZone, ok := p.PaidZones[zone]; ok { + return paidZone + } + zoneID, err := p.Client.ZoneIDByName(zone) if err != nil { log.Errorf("Failed to get zone %s by name %v", zone, err) @@ -208,7 +238,8 @@ func (p *CloudFlareProvider) ZoneHasPaidPlan(hostname string) bool { return false } - return zoneDetails.Plan.IsSubscribed + p.PaidZones[zone] = zoneDetails.Plan.IsSubscribed + return p.PaidZones[zone] } // CloudFlareProvider is an implementation of Provider for CloudFlare DNS. @@ -223,6 +254,7 @@ type CloudFlareProvider struct { CustomHostnamesConfig CustomHostnamesConfig DNSRecordsConfig DNSRecordsConfig RegionalServicesConfig RegionalServicesConfig + PaidZones map[string]bool } // cloudFlareChange differentiates between ChangeActions @@ -248,7 +280,8 @@ func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams Type: cfc.ResourceRecord.Type, Content: cfc.ResourceRecord.Content, Priority: cfc.ResourceRecord.Priority, - Comment: cloudflare.StringPtr(cfc.ResourceRecord.Comment), + Comment: &cfc.ResourceRecord.Comment, + Tags: cfc.ResourceRecord.Tags, } return params @@ -264,6 +297,7 @@ func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordPar Content: cfc.ResourceRecord.Content, Priority: cfc.ResourceRecord.Priority, Comment: cfc.ResourceRecord.Comment, + Tags: cfc.ResourceRecord.Tags, } return params @@ -339,6 +373,7 @@ func NewCloudFlareProvider( DryRun: dryRun, RegionalServicesConfig: regionalServicesConfig, DNSRecordsConfig: dnsRecordsConfig, + PaidZones: make(map[string]bool), }, nil } @@ -595,11 +630,13 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud for _, change := range zoneChanges { logFields := log.Fields{ - "record": change.ResourceRecord.Name, - "type": change.ResourceRecord.Type, - "ttl": change.ResourceRecord.TTL, - "action": change.Action.String(), - "zone": zoneID, + "record": change.ResourceRecord.Name, + "type": change.ResourceRecord.Type, + "ttl": change.ResourceRecord.TTL, + "action": change.Action, + "zone": zoneID, + "comment": change.ResourceRecord.Comment, + "tags": change.ResourceRecord.Tags, } log.WithFields(logFields).Info("Changing record.") @@ -716,6 +753,14 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([] } } + // if _, ok := e.GetProviderSpecificProperty(annotations.CloudflareRecordCommentKey); !ok { + // e.SetProviderSpecificProperty(annotations.CloudflareRecordCommentKey, p.DNSRecordsConfig.Comment) + // } + + // if _, ok := e.GetProviderSpecificProperty(annotations.CloudflareRecordTagsKey); !ok { + // e.SetProviderSpecificProperty(annotations.CloudflareRecordTagsKey, p.DNSRecordsConfig.Tags) + // } + adjustedEndpoints = append(adjustedEndpoints, e) } return adjustedEndpoints, nil @@ -809,6 +854,13 @@ func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoi } } + var tagsFromAnnotation []string + // Load tags from program flag + if val, ok := ep.GetProviderSpecificProperty(annotations.CloudflareRecordTagsKey); ok { + // Replace comment with Ingress annotation + tagsFromAnnotation = strings.Split(val, ",") + } + return &cloudFlareChange{ Action: action, ResourceRecord: cloudflare.DNSRecord{ @@ -821,6 +873,7 @@ func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoi Content: target, Comment: comment, Priority: priority, + Tags: p.DNSRecordsConfig.validTags(ep.DNSName, p.ZoneHasPaidPlan, tagsFromAnnotation), }, RegionalHostname: p.regionalHostname(ep), CustomHostnamesPrev: prevCustomHostnames, @@ -996,6 +1049,10 @@ func (p *CloudFlareProvider) groupByNameAndTypeWithCustomHostnames(records DNSRe e = e.WithProviderSpecific(annotations.CloudflareRecordCommentKey, records[0].Comment) } + if len(records[0].Tags) > 0 { + e = e.WithProviderSpecific(annotations.CloudflareRecordTagsKey, strings.Join(records[0].Tags, ",")) + } + endpoints = append(endpoints, e) } return endpoints diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go index 79b670000..0602eb695 100644 --- a/provider/cloudflare/cloudflare_test.go +++ b/provider/cloudflare/cloudflare_test.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "os" + "reflect" "slices" "sort" "strings" @@ -1913,6 +1914,73 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { } }) } + + tagsTestCases := []struct { + name string + provider *CloudFlareProvider + endpoint *endpoint.Endpoint + expected []string + }{ + { + name: "For free Zones setting Tags, expect them to be ignored", + provider: p, + endpoint: &endpoint.Endpoint{ + DNSName: "example.com", + RecordType: "A", + Targets: []string{"192.0.2.1"}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: annotations.CloudflareRecordTagsKey, + Value: "tag1,tag2", + }, + }, + }, + expected: nil, + }, + { + name: "For paid Zones setting tags, expect them to be set", + provider: paidProvider, + endpoint: &endpoint.Endpoint{ + DNSName: "bar.com", + RecordType: "A", + Targets: []string{"192.0.2.1"}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: annotations.CloudflareRecordTagsKey, + Value: "tag1,tag2", + }, + }, + }, + expected: []string{"tag1", "tag2"}, + }, + { + name: "For paid Zones settings tags not alphabetically sorted, expect them to be sorted", + provider: paidProvider, + endpoint: &endpoint.Endpoint{ + DNSName: "bar.com", + RecordType: "A", + Targets: []string{"192.0.2.1"}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: annotations.CloudflareRecordTagsKey, + Value: "tag2,tag1", + }, + }, + }, + expected: []string{"tag1", "tag2"}, + }, + } + + for _, test := range tagsTestCases { + t.Run(test.name, func(t *testing.T) { + change, _ := test.provider.newCloudFlareChange(cloudFlareCreate, test.endpoint, test.endpoint.Targets[0], nil) + if test.expected == nil && len(change.ResourceRecord.Tags) != 0 { + t.Errorf("expected tags to be %v, but got %v", test.expected, change.ResourceRecord.Tags) + } else if !reflect.DeepEqual(change.ResourceRecord.Tags, test.expected) { + t.Errorf("expected tags to be %v, but got %v", test.expected, change.ResourceRecord.Tags) + } + }) + } } func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) { @@ -2578,6 +2646,7 @@ func TestZoneHasPaidPlan(t *testing.T) { Client: client, domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}), + PaidZones: make(map[string]bool), } assert.False(t, cfprovider.ZoneHasPaidPlan("subdomain.foo.com")) @@ -2589,6 +2658,7 @@ func TestZoneHasPaidPlan(t *testing.T) { Client: client, domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}), + PaidZones: make(map[string]bool), } assert.False(t, cfproviderWithZoneError.ZoneHasPaidPlan("subdomain.foo.com")) } diff --git a/source/annotations/annotations.go b/source/annotations/annotations.go index abfce1359..63687b831 100644 --- a/source/annotations/annotations.go +++ b/source/annotations/annotations.go @@ -23,10 +23,11 @@ const ( AnnotationKeyPrefix = "external-dns.alpha.kubernetes.io/" // CloudflareProxiedKey The annotation used for determining if traffic will go through Cloudflare - CloudflareProxiedKey = AnnotationKeyPrefix + "cloudflare-proxied" - CloudflareCustomHostnameKey = AnnotationKeyPrefix + "cloudflare-custom-hostname" - CloudflareRegionKey = AnnotationKeyPrefix + "cloudflare-region-key" - CloudflareRecordCommentKey = AnnotationKeyPrefix + "cloudflare-record-comment" + CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied" + CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname" + CloudflareRegionKey = "external-dns.alpha.kubernetes.io/cloudflare-region-key" + CloudflareRecordCommentKey = "external-dns.alpha.kubernetes.io/cloudflare-record-comment" + CloudflareRecordTagsKey = "external-dns.alpha.kubernetes.io/cloudflare-record-tags" AWSPrefix = AnnotationKeyPrefix + "aws-" SCWPrefix = AnnotationKeyPrefix + "scw-" diff --git a/source/annotations/provider_specific.go b/source/annotations/provider_specific.go index 93612ff09..9562c773b 100644 --- a/source/annotations/provider_specific.go +++ b/source/annotations/provider_specific.go @@ -73,6 +73,11 @@ func ProviderSpecificAnnotations(annotations map[string]string) (endpoint.Provid Name: CloudflareRecordCommentKey, Value: v, }) + } else if strings.Contains(k, CloudflareRecordTagsKey) { + providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ + Name: CloudflareRecordTagsKey, + Value: v, + }) } } }