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{
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)

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) |
| `--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) |

View File

@ -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)

View File

@ -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
}
@ -598,8 +633,10 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
"record": change.ResourceRecord.Name,
"type": change.ResourceRecord.Type,
"ttl": change.ResourceRecord.TTL,
"action": change.Action.String(),
"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

View File

@ -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"))
}

View File

@ -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-"

View File

@ -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,
})
}
}
}