This commit is contained in:
tom 2025-07-28 06:11:23 -07:00 committed by GitHub
commit 59fe3da341
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 148 additions and 11 deletions

View File

@ -229,6 +229,7 @@ func buildProvider(
cloudflare.DNSRecordsConfig{ cloudflare.DNSRecordsConfig{
PerPage: cfg.CloudflareDNSRecordsPerPage, PerPage: cfg.CloudflareDNSRecordsPerPage,
Comment: cfg.CloudflareDNSRecordsComment, Comment: cfg.CloudflareDNSRecordsComment,
Tags: cfg.CloudflareDNSRecordsTags,
}) })
case "google": case "google":
p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun) p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun)

View File

@ -96,6 +96,7 @@
| `--[no-]cloudflare-regional-services` | When using the Cloudflare provider, specify if Regional Services feature will be used (default: disabled) | | `--[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-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-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 | | `--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-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) | | `--akamai-client-token=""` | When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified) |

View File

@ -111,6 +111,7 @@ type Config struct {
CloudflareCustomHostnames bool CloudflareCustomHostnames bool
CloudflareDNSRecordsPerPage int CloudflareDNSRecordsPerPage int
CloudflareDNSRecordsComment string CloudflareDNSRecordsComment string
CloudflareDNSRecordsTags string
CloudflareCustomHostnamesMinTLSVersion string CloudflareCustomHostnamesMinTLSVersion string
CloudflareCustomHostnamesCertificateAuthority string CloudflareCustomHostnamesCertificateAuthority string
CloudflareRegionalServices bool 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-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-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-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("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) 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)

View File

@ -170,6 +170,7 @@ func (z zoneService) CreateCustomHostname(ctx context.Context, zoneID string, ch
type DNSRecordsConfig struct { type DNSRecordsConfig struct {
PerPage int PerPage int
Comment string Comment string
Tags string
} }
func (c *DNSRecordsConfig) trimAndValidateComment(dnsName, comment string, paidZone func(string) bool) 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 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 { func (p *CloudFlareProvider) ZoneHasPaidPlan(hostname string) bool {
zone, err := publicsuffix.EffectiveTLDPlusOne(hostname) zone, err := publicsuffix.EffectiveTLDPlusOne(hostname)
if err != nil { if err != nil {
log.Errorf("Failed to get effective TLD+1 for hostname %s %v", hostname, err) log.Errorf("Failed to get effective TLD+1 for hostname %s %v", hostname, err)
return false return false
} }
if paidZone, ok := p.PaidZones[zone]; ok {
return paidZone
}
zoneID, err := p.Client.ZoneIDByName(zone) zoneID, err := p.Client.ZoneIDByName(zone)
if err != nil { if err != nil {
log.Errorf("Failed to get zone %s by name %v", zone, err) 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 false
} }
return zoneDetails.Plan.IsSubscribed p.PaidZones[zone] = zoneDetails.Plan.IsSubscribed
return p.PaidZones[zone]
} }
// CloudFlareProvider is an implementation of Provider for CloudFlare DNS. // CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
@ -223,6 +254,7 @@ type CloudFlareProvider struct {
CustomHostnamesConfig CustomHostnamesConfig CustomHostnamesConfig CustomHostnamesConfig
DNSRecordsConfig DNSRecordsConfig DNSRecordsConfig DNSRecordsConfig
RegionalServicesConfig RegionalServicesConfig RegionalServicesConfig RegionalServicesConfig
PaidZones map[string]bool
} }
// cloudFlareChange differentiates between ChangeActions // cloudFlareChange differentiates between ChangeActions
@ -248,7 +280,8 @@ func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams
Type: cfc.ResourceRecord.Type, Type: cfc.ResourceRecord.Type,
Content: cfc.ResourceRecord.Content, Content: cfc.ResourceRecord.Content,
Priority: cfc.ResourceRecord.Priority, Priority: cfc.ResourceRecord.Priority,
Comment: cloudflare.StringPtr(cfc.ResourceRecord.Comment), Comment: &cfc.ResourceRecord.Comment,
Tags: cfc.ResourceRecord.Tags,
} }
return params return params
@ -264,6 +297,7 @@ func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordPar
Content: cfc.ResourceRecord.Content, Content: cfc.ResourceRecord.Content,
Priority: cfc.ResourceRecord.Priority, Priority: cfc.ResourceRecord.Priority,
Comment: cfc.ResourceRecord.Comment, Comment: cfc.ResourceRecord.Comment,
Tags: cfc.ResourceRecord.Tags,
} }
return params return params
@ -339,6 +373,7 @@ func NewCloudFlareProvider(
DryRun: dryRun, DryRun: dryRun,
RegionalServicesConfig: regionalServicesConfig, RegionalServicesConfig: regionalServicesConfig,
DNSRecordsConfig: dnsRecordsConfig, DNSRecordsConfig: dnsRecordsConfig,
PaidZones: make(map[string]bool),
}, nil }, nil
} }
@ -595,11 +630,13 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
for _, change := range zoneChanges { for _, change := range zoneChanges {
logFields := log.Fields{ logFields := log.Fields{
"record": change.ResourceRecord.Name, "record": change.ResourceRecord.Name,
"type": change.ResourceRecord.Type, "type": change.ResourceRecord.Type,
"ttl": change.ResourceRecord.TTL, "ttl": change.ResourceRecord.TTL,
"action": change.Action.String(), "action": change.Action,
"zone": zoneID, "zone": zoneID,
"comment": change.ResourceRecord.Comment,
"tags": change.ResourceRecord.Tags,
} }
log.WithFields(logFields).Info("Changing record.") 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) adjustedEndpoints = append(adjustedEndpoints, e)
} }
return adjustedEndpoints, nil 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{ return &cloudFlareChange{
Action: action, Action: action,
ResourceRecord: cloudflare.DNSRecord{ ResourceRecord: cloudflare.DNSRecord{
@ -821,6 +873,7 @@ func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoi
Content: target, Content: target,
Comment: comment, Comment: comment,
Priority: priority, Priority: priority,
Tags: p.DNSRecordsConfig.validTags(ep.DNSName, p.ZoneHasPaidPlan, tagsFromAnnotation),
}, },
RegionalHostname: p.regionalHostname(ep), RegionalHostname: p.regionalHostname(ep),
CustomHostnamesPrev: prevCustomHostnames, CustomHostnamesPrev: prevCustomHostnames,
@ -996,6 +1049,10 @@ func (p *CloudFlareProvider) groupByNameAndTypeWithCustomHostnames(records DNSRe
e = e.WithProviderSpecific(annotations.CloudflareRecordCommentKey, records[0].Comment) 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) endpoints = append(endpoints, e)
} }
return endpoints return endpoints

View File

@ -21,6 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"reflect"
"slices" "slices"
"sort" "sort"
"strings" "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) { func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) {
@ -2578,6 +2646,7 @@ func TestZoneHasPaidPlan(t *testing.T) {
Client: client, Client: client,
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}), domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
zoneIDFilter: provider.NewZoneIDFilter([]string{""}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
PaidZones: make(map[string]bool),
} }
assert.False(t, cfprovider.ZoneHasPaidPlan("subdomain.foo.com")) assert.False(t, cfprovider.ZoneHasPaidPlan("subdomain.foo.com"))
@ -2589,6 +2658,7 @@ func TestZoneHasPaidPlan(t *testing.T) {
Client: client, Client: client,
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}), domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
zoneIDFilter: provider.NewZoneIDFilter([]string{""}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
PaidZones: make(map[string]bool),
} }
assert.False(t, cfproviderWithZoneError.ZoneHasPaidPlan("subdomain.foo.com")) assert.False(t, cfproviderWithZoneError.ZoneHasPaidPlan("subdomain.foo.com"))
} }

View File

@ -23,10 +23,11 @@ const (
AnnotationKeyPrefix = "external-dns.alpha.kubernetes.io/" AnnotationKeyPrefix = "external-dns.alpha.kubernetes.io/"
// CloudflareProxiedKey The annotation used for determining if traffic will go through Cloudflare // CloudflareProxiedKey The annotation used for determining if traffic will go through Cloudflare
CloudflareProxiedKey = AnnotationKeyPrefix + "cloudflare-proxied" CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
CloudflareCustomHostnameKey = AnnotationKeyPrefix + "cloudflare-custom-hostname" CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname"
CloudflareRegionKey = AnnotationKeyPrefix + "cloudflare-region-key" CloudflareRegionKey = "external-dns.alpha.kubernetes.io/cloudflare-region-key"
CloudflareRecordCommentKey = AnnotationKeyPrefix + "cloudflare-record-comment" CloudflareRecordCommentKey = "external-dns.alpha.kubernetes.io/cloudflare-record-comment"
CloudflareRecordTagsKey = "external-dns.alpha.kubernetes.io/cloudflare-record-tags"
AWSPrefix = AnnotationKeyPrefix + "aws-" AWSPrefix = AnnotationKeyPrefix + "aws-"
SCWPrefix = AnnotationKeyPrefix + "scw-" SCWPrefix = AnnotationKeyPrefix + "scw-"

View File

@ -73,6 +73,11 @@ func ProviderSpecificAnnotations(annotations map[string]string) (endpoint.Provid
Name: CloudflareRecordCommentKey, Name: CloudflareRecordCommentKey,
Value: v, Value: v,
}) })
} else if strings.Contains(k, CloudflareRecordTagsKey) {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: CloudflareRecordTagsKey,
Value: v,
})
} }
} }
} }