improve cloudflare regional hostname implementation (#5309)

- add flag to enable regional hostname feature
- support deletion of regional hostname on annotation edit
- correctly support differences detection with cloudflare state
- increased tests coverage

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>
This commit is contained in:
vflaux 2025-06-22 12:22:52 +02:00 committed by GitHub
parent 104157f8b1
commit 7108979df1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1260 additions and 494 deletions

View File

@ -215,7 +215,10 @@ func buildProvider(
zoneIDFilter, zoneIDFilter,
cfg.CloudflareProxied, cfg.CloudflareProxied,
cfg.DryRun, cfg.DryRun,
cfg.CloudflareRegionKey, cloudflare.RegionalServicesConfig{
Enabled: cfg.CloudflareRegionalServices,
RegionKey: cfg.CloudflareRegionKey,
},
cloudflare.CustomHostnamesConfig{ cloudflare.CustomHostnamesConfig{
Enabled: cfg.CloudflareCustomHostnames, Enabled: cfg.CloudflareCustomHostnames,
MinTLSVersion: cfg.CloudflareCustomHostnamesMinTLSVersion, MinTLSVersion: cfg.CloudflareCustomHostnamesMinTLSVersion,

View File

@ -93,7 +93,8 @@
| `--cloudflare-custom-hostnames-min-tls-version=1.0` | When using the Cloudflare provider with the Custom Hostnames, specify which Minimum TLS Version will be used by default. (default: 1.0, options: 1.0, 1.1, 1.2, 1.3) | | `--cloudflare-custom-hostnames-min-tls-version=1.0` | When using the Cloudflare provider with the Custom Hostnames, specify which Minimum TLS Version will be used by default. (default: 1.0, options: 1.0, 1.1, 1.2, 1.3) |
| `--cloudflare-custom-hostnames-certificate-authority=none` | When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none) | | `--cloudflare-custom-hostnames-certificate-authority=none` | When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none) |
| `--cloudflare-dns-records-per-page=100` | When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100) | | `--cloudflare-dns-records-per-page=100` | When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100) |
| `--cloudflare-region-key=CLOUDFLARE-REGION-KEY` | When using the Cloudflare provider, specify the region (default: earth) | | `--[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-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (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) |

View File

@ -128,6 +128,7 @@ spec:
- --provider=cloudflare - --provider=cloudflare
- --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...) - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...)
- --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request
- --cloudflare-regional-services # (optional) enable the regional hostname feature that configure which region can decrypt HTTPS requests
- --cloudflare-region-key="eu" # (optional) configure which region can decrypt HTTPS requests - --cloudflare-region-key="eu" # (optional) configure which region can decrypt HTTPS requests
- --cloudflare-record-comment="provisioned by external-dns" # (optional) configure comments for provisioned records; <=100 chars for free zones; <=500 chars for paid zones - --cloudflare-record-comment="provisioned by external-dns" # (optional) configure comments for provisioned records; <=100 chars for free zones; <=500 chars for paid zones
env: env:
@ -205,6 +206,7 @@ spec:
- --provider=cloudflare - --provider=cloudflare
- --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...) - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...)
- --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request
- --cloudflare-regional-services # (optional) enable the regional hostname feature that configure which region can decrypt HTTPS requests
- --cloudflare-region-key="eu" # (optional) configure which region can decrypt HTTPS requests - --cloudflare-region-key="eu" # (optional) configure which region can decrypt HTTPS requests
- --cloudflare-record-comment="provisioned by external-dns" # (optional) configure comments for provisioned records; <=100 chars for free zones; <=500 chars for paid zones - --cloudflare-record-comment="provisioned by external-dns" # (optional) configure comments for provisioned records; <=100 chars for free zones; <=500 chars for paid zones
env: env:
@ -303,13 +305,19 @@ kubectl delete -f externaldns.yaml
Using the `external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"` annotation on your ingress, you can specify if the proxy feature of Cloudflare should be enabled for that record. This setting will override the global `--cloudflare-proxied` setting. Using the `external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"` annotation on your ingress, you can specify if the proxy feature of Cloudflare should be enabled for that record. This setting will override the global `--cloudflare-proxied` setting.
## Setting cloudflare-region-key to configure regional services ## Setting cloudlfare regional services
Using the `external-dns.alpha.kubernetes.io/cloudflare-region-key` annotation on your ingress, you can restrict which data centers can decrypt and serve HTTPS traffic. With Cloudflare regional services you can restrict which data centers can decrypt and serve HTTPS traffic.
Configuration of Cloudflare Regional Services is enabled by the `--cloudflare-regional-services` flag.
A default region can be defined using the `--cloudflare-region-key` flag.
Using the `external-dns.alpha.kubernetes.io/cloudflare-region-key` annotation on your ingress, you can specify the region for that record.
An empty string will result in no regional hostname configured.
**Accepted values for region key include:** **Accepted values for region key include:**
- `earth` (default): All data centers (global)
- `eu`: European Union data centers only - `eu`: European Union data centers only
- `us`: United States data centers only - `us`: United States data centers only
- `ap`: Asia-Pacific data centers only - `ap`: Asia-Pacific data centers only
@ -321,14 +329,11 @@ Using the `external-dns.alpha.kubernetes.io/cloudflare-region-key` annotation on
- `br`: Brazil data centers only - `br`: Brazil data centers only
- `za`: South Africa data centers only - `za`: South Africa data centers only
- `ae`: United Arab Emirates data centers only - `ae`: United Arab Emirates data centers only
- `global`: Alias for `earth`
For the most up-to-date list and details, see the [Cloudflare Regional Services documentation](https://developers.cloudflare.com/data-localization/regional-services/get-started/). For the most up-to-date list and details, see the [Cloudflare Regional Services documentation](https://developers.cloudflare.com/data-localization/regional-services/get-started/).
Currently, requires SuperAdmin or Admin role. Currently, requires SuperAdmin or Admin role.
If not set the value will default to `global`.
## Setting cloudflare-custom-hostname ## Setting cloudflare-custom-hostname
Automatic configuration of Cloudflare custom hostnames (using A/CNAME DNS records as custom origin servers) is enabled by the `--cloudflare-custom-hostnames` flag and the `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: <custom hostname>` annotation. Automatic configuration of Cloudflare custom hostnames (using A/CNAME DNS records as custom origin servers) is enabled by the `--cloudflare-custom-hostnames` flag and the `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: <custom hostname>` annotation.

View File

@ -113,6 +113,7 @@ type Config struct {
CloudflareDNSRecordsComment string CloudflareDNSRecordsComment string
CloudflareCustomHostnamesMinTLSVersion string CloudflareCustomHostnamesMinTLSVersion string
CloudflareCustomHostnamesCertificateAuthority string CloudflareCustomHostnamesCertificateAuthority string
CloudflareRegionalServices bool
CloudflareRegionKey string CloudflareRegionKey string
CloudflareRecordComment string CloudflareRecordComment string
CoreDNSPrefix string CoreDNSPrefix string
@ -256,6 +257,7 @@ var defaultConfig = &Config{
CloudflareCustomHostnamesMinTLSVersion: "1.0", CloudflareCustomHostnamesMinTLSVersion: "1.0",
CloudflareDNSRecordsPerPage: 100, CloudflareDNSRecordsPerPage: 100,
CloudflareProxied: false, CloudflareProxied: false,
CloudflareRegionalServices: false,
CloudflareRegionKey: "earth", CloudflareRegionKey: "earth",
CombineFQDNAndAnnotation: false, CombineFQDNAndAnnotation: false,
@ -533,7 +535,8 @@ func App(cfg *Config) *kingpin.Application {
app.Flag("cloudflare-custom-hostnames-min-tls-version", "When using the Cloudflare provider with the Custom Hostnames, specify which Minimum TLS Version will be used by default. (default: 1.0, options: 1.0, 1.1, 1.2, 1.3)").Default("1.0").EnumVar(&cfg.CloudflareCustomHostnamesMinTLSVersion, "1.0", "1.1", "1.2", "1.3") app.Flag("cloudflare-custom-hostnames-min-tls-version", "When using the Cloudflare provider with the Custom Hostnames, specify which Minimum TLS Version will be used by default. (default: 1.0, options: 1.0, 1.1, 1.2, 1.3)").Default("1.0").EnumVar(&cfg.CloudflareCustomHostnamesMinTLSVersion, "1.0", "1.1", "1.2", "1.3")
app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none)").Default("none").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none") app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none)").Default("none").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none")
app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage) app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage)
app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the region (default: earth)").StringVar(&cfg.CloudflareRegionKey) 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.CloudflareRecordComment) app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment)
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)

View File

@ -186,6 +186,7 @@ var (
CloudflareCustomHostnamesMinTLSVersion: "1.3", CloudflareCustomHostnamesMinTLSVersion: "1.3",
CloudflareCustomHostnamesCertificateAuthority: "google", CloudflareCustomHostnamesCertificateAuthority: "google",
CloudflareDNSRecordsPerPage: 5000, CloudflareDNSRecordsPerPage: 5000,
CloudflareRegionalServices: true,
CloudflareRegionKey: "us", CloudflareRegionKey: "us",
CoreDNSPrefix: "/coredns/", CoreDNSPrefix: "/coredns/",
AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
@ -296,6 +297,7 @@ func TestParseFlags(t *testing.T) {
"--cloudflare-custom-hostnames-min-tls-version=1.3", "--cloudflare-custom-hostnames-min-tls-version=1.3",
"--cloudflare-custom-hostnames-certificate-authority=google", "--cloudflare-custom-hostnames-certificate-authority=google",
"--cloudflare-dns-records-per-page=5000", "--cloudflare-dns-records-per-page=5000",
"--cloudflare-regional-services",
"--cloudflare-region-key=us", "--cloudflare-region-key=us",
"--coredns-prefix=/coredns/", "--coredns-prefix=/coredns/",
"--akamai-serviceconsumerdomain=oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", "--akamai-serviceconsumerdomain=oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
@ -424,6 +426,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_MIN_TLS_VERSION": "1.3", "EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_MIN_TLS_VERSION": "1.3",
"EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_CERTIFICATE_AUTHORITY": "google", "EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_CERTIFICATE_AUTHORITY": "google",
"EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000", "EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000",
"EXTERNAL_DNS_CLOUDFLARE_REGIONAL_SERVICES": "1",
"EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us", "EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us",
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/", "EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
"EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", "EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",

View File

@ -119,6 +119,7 @@ type cloudFlareDNS interface {
CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error)
DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error
UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error
ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error)
CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error
UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error
DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error
@ -222,7 +223,7 @@ type CloudFlareProvider struct {
DryRun bool DryRun bool
CustomHostnamesConfig CustomHostnamesConfig CustomHostnamesConfig CustomHostnamesConfig
DNSRecordsConfig DNSRecordsConfig DNSRecordsConfig DNSRecordsConfig
RegionKey string RegionalServicesConfig RegionalServicesConfig
} }
// cloudFlareChange differentiates between ChangActions // cloudFlareChange differentiates between ChangActions
@ -279,7 +280,15 @@ func convertCloudflareError(err error) error {
} }
// NewCloudFlareProvider initializes a new CloudFlare DNS based Provider. // NewCloudFlareProvider initializes a new CloudFlare DNS based Provider.
func NewCloudFlareProvider(domainFilter *endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, proxiedByDefault bool, dryRun bool, regionKey string, customHostnamesConfig CustomHostnamesConfig, dnsRecordsConfig DNSRecordsConfig) (*CloudFlareProvider, error) { func NewCloudFlareProvider(
domainFilter *endpoint.DomainFilter,
zoneIDFilter provider.ZoneIDFilter,
proxiedByDefault bool,
dryRun bool,
regionalServicesConfig RegionalServicesConfig,
customHostnamesConfig CustomHostnamesConfig,
dnsRecordsConfig DNSRecordsConfig,
) (*CloudFlareProvider, error) {
// initialize via chosen auth method and returns new API object // initialize via chosen auth method and returns new API object
var ( var (
config *cloudflare.API config *cloudflare.API
@ -302,6 +311,10 @@ func NewCloudFlareProvider(domainFilter *endpoint.DomainFilter, zoneIDFilter pro
return nil, fmt.Errorf("failed to initialize cloudflare provider: %w", err) return nil, fmt.Errorf("failed to initialize cloudflare provider: %w", err)
} }
if regionalServicesConfig.RegionKey != "" {
regionalServicesConfig.Enabled = true
}
return &CloudFlareProvider{ return &CloudFlareProvider{
Client: zoneService{config}, Client: zoneService{config},
domainFilter: domainFilter, domainFilter: domainFilter,
@ -309,7 +322,7 @@ func NewCloudFlareProvider(domainFilter *endpoint.DomainFilter, zoneIDFilter pro
proxiedByDefault: proxiedByDefault, proxiedByDefault: proxiedByDefault,
CustomHostnamesConfig: customHostnamesConfig, CustomHostnamesConfig: customHostnamesConfig,
DryRun: dryRun, DryRun: dryRun,
RegionKey: regionKey, RegionalServicesConfig: regionalServicesConfig,
DNSRecordsConfig: dnsRecordsConfig, DNSRecordsConfig: dnsRecordsConfig,
}, nil }, nil
} }
@ -379,7 +392,13 @@ func (p *CloudFlareProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
// As CloudFlare does not support "sets" of targets, but instead returns // 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 // 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. // and record to allow the planner to calculate the correct plan. See #992.
endpoints = append(endpoints, groupByNameAndTypeWithCustomHostnames(records, chs)...) zoneEndpoints := groupByNameAndTypeWithCustomHostnames(records, chs)
if err := p.addEnpointsProviderSpecificRegionKeyProperty(ctx, zone.ID, zoneEndpoints); err != nil {
return nil, err
}
endpoints = append(endpoints, zoneEndpoints...)
} }
return endpoints, nil return endpoints, nil
@ -595,16 +614,21 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
} }
} }
if regionalHostnamesChanges, err := dataLocalizationRegionalHostnamesChanges(zoneChanges); err == nil { if p.RegionalServicesConfig.Enabled {
if !p.submitDataLocalizationRegionalHostnameChanges(ctx, regionalHostnamesChanges, resourceContainer) { desiredRegionalHostnames, err := desiredRegionalHostnames(zoneChanges)
if err != nil {
return fmt.Errorf("failed to build desired regional hostnames: %w", err)
}
if len(desiredRegionalHostnames) > 0 {
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, resourceContainer)
if err != nil {
return fmt.Errorf("could not fetch regional hostnames from zone, %w", err)
}
regionalHostnamesChanges := regionalHostnamesChanges(desiredRegionalHostnames, regionalHostnames)
if !p.submitRegionalHostnameChanges(ctx, regionalHostnamesChanges, resourceContainer) {
failedChange = true failedChange = true
} }
} else {
logFields := log.Fields{
"zone": zoneID,
} }
log.WithFields(logFields).Errorf("failed to build data localization regional hostname changes: %v", err)
failedChange = true
} }
if failedChange { if failedChange {
@ -640,6 +664,13 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]
e.DeleteProviderSpecificProperty(annotations.CloudflareCustomHostnameKey) e.DeleteProviderSpecificProperty(annotations.CloudflareCustomHostnameKey)
} }
if p.RegionalServicesConfig.Enabled {
// Add default region key if not set
if _, ok := e.GetProviderSpecificProperty(annotations.CloudflareRegionKey); !ok {
e.SetProviderSpecificProperty(annotations.CloudflareRegionKey, p.RegionalServicesConfig.RegionKey)
}
}
adjustedEndpoints = append(adjustedEndpoints, e) adjustedEndpoints = append(adjustedEndpoints, e)
} }
return adjustedEndpoints, nil return adjustedEndpoints, nil

View File

@ -18,30 +18,40 @@ package cloudflare
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"maps" "maps"
"net/http"
"slices" "slices"
"github.com/cloudflare/cloudflare-go" cloudflare "github.com/cloudflare/cloudflare-go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations" "sigs.k8s.io/external-dns/source/annotations"
) )
type RegionalServicesConfig struct {
Enabled bool
RegionKey string
}
var recordTypeRegionalHostnameSupported = map[string]bool{ var recordTypeRegionalHostnameSupported = map[string]bool{
"A": true, "A": true,
"AAAA": true, "AAAA": true,
"CNAME": true, "CNAME": true,
} }
// RegionalHostnamesMap is a map of regional hostnames keyed by hostname.
type RegionalHostnamesMap map[string]cloudflare.RegionalHostname
type regionalHostnameChange struct { type regionalHostnameChange struct {
action changeAction action changeAction
cloudflare.RegionalHostname cloudflare.RegionalHostname
} }
func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) {
return z.service.ListDataLocalizationRegionalHostnames(ctx, rc, rp)
}
func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error { func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error {
_, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp) _, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp)
return err return err
@ -72,89 +82,78 @@ func updateDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cl
} }
} }
// submitDataLocalizationRegionalHostnameChanges applies a set of data localization regional hostname changes, returns false if it fails // submitRegionalHostnameChanges applies a set of regional hostname changes, returns false if at least one fails
func (p *CloudFlareProvider) submitDataLocalizationRegionalHostnameChanges(ctx context.Context, rhChanges []regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool { func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, rhChanges []regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool {
failedChange := false failedChange := false
for _, rhChange := range rhChanges { for _, rhChange := range rhChanges {
logFields := log.Fields{ if !p.submitRegionalHostnameChange(ctx, rhChange, resourceContainer) {
"hostname": rhChange.Hostname,
"region_key": rhChange.RegionKey,
"action": rhChange.action,
"zone": resourceContainer.Identifier,
}
log.WithFields(logFields).Info("Changing regional hostname")
switch rhChange.action {
case cloudFlareCreate:
log.WithFields(logFields).Debug("Creating regional hostname")
if p.DryRun {
continue
}
regionalHostnameParam := createDataLocalizationRegionalHostnameParams(rhChange)
err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam)
if err != nil {
var apiErr *cloudflare.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict {
log.WithFields(logFields).Debug("Regional hostname already exists, updating instead")
params := updateDataLocalizationRegionalHostnameParams(rhChange)
err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, params)
if err != nil {
failedChange = true failedChange = true
log.WithFields(logFields).Errorf("failed to update regional hostname: %v", err)
}
continue
}
failedChange = true
log.WithFields(logFields).Errorf("failed to create regional hostname: %v", err)
}
case cloudFlareUpdate:
log.WithFields(logFields).Debug("Updating regional hostname")
if p.DryRun {
continue
}
regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(rhChange)
err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam)
if err != nil {
var apiErr *cloudflare.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
log.WithFields(logFields).Debug("Regional hostname not does not exists, creating instead")
params := createDataLocalizationRegionalHostnameParams(rhChange)
err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, params)
if err != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to create regional hostname: %v", err)
}
continue
}
failedChange = true
log.WithFields(logFields).Errorf("failed to update regional hostname: %v", err)
}
case cloudFlareDelete:
log.WithFields(logFields).Debug("Deleting regional hostname")
if p.DryRun {
continue
}
err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, rhChange.Hostname)
if err != nil {
var apiErr *cloudflare.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
log.WithFields(logFields).Debug("Regional hostname does not exists, nothing to do")
continue
}
failedChange = true
log.WithFields(logFields).Errorf("failed to delete regional hostname: %v", err)
}
} }
} }
return !failedChange return !failedChange
} }
// submitRegionalHostnameChange applies a single regional hostname change, returns false if it fails
func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, rhChange regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool {
changeLog := log.WithFields(log.Fields{
"hostname": rhChange.Hostname,
"region_key": rhChange.RegionKey,
"action": rhChange.action,
"zone": resourceContainer.Identifier,
})
if p.DryRun {
changeLog.Debug("Dry run: skipping regional hostname change", rhChange.action)
return true
}
switch rhChange.action {
case cloudFlareCreate:
changeLog.Debug("Creating regional hostname")
regionalHostnameParam := createDataLocalizationRegionalHostnameParams(rhChange)
if err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil {
changeLog.Errorf("failed to create regional hostname: %v", err)
return false
}
case cloudFlareUpdate:
changeLog.Debug("Updating regional hostname")
regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(rhChange)
if err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil {
changeLog.Errorf("failed to update regional hostname: %v", err)
return false
}
case cloudFlareDelete:
changeLog.Debug("Deleting regional hostname")
if err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, rhChange.Hostname); err != nil {
changeLog.Errorf("failed to delete regional hostname: %v", err)
return false
}
}
return true
}
func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, resourceContainer *cloudflare.ResourceContainer) (RegionalHostnamesMap, error) {
rhs, err := p.Client.ListDataLocalizationRegionalHostnames(ctx, resourceContainer, cloudflare.ListDataLocalizationRegionalHostnamesParams{})
if err != nil {
return nil, convertCloudflareError(err)
}
rhsMap := make(RegionalHostnamesMap)
for _, r := range rhs {
rhsMap[r.Hostname] = r
}
return rhsMap, nil
}
// regionalHostname returns a RegionalHostname for the given endpoint.
//
// If the regional services feature is not enabled or the record type does not support regional hostnames,
// it returns an empty RegionalHostname.
// If the endpoint has a specific region key set, it uses that; otherwise, it defaults to the region key configured in the provider.
func (p *CloudFlareProvider) regionalHostname(ep *endpoint.Endpoint) cloudflare.RegionalHostname { func (p *CloudFlareProvider) regionalHostname(ep *endpoint.Endpoint) cloudflare.RegionalHostname {
if p.RegionKey == "" || !recordTypeRegionalHostnameSupported[ep.RecordType] { if !p.RegionalServicesConfig.Enabled || !recordTypeRegionalHostnameSupported[ep.RecordType] {
return cloudflare.RegionalHostname{} return cloudflare.RegionalHostname{}
} }
regionKey := p.RegionKey regionKey := p.RegionalServicesConfig.RegionKey
if epRegionKey, exists := ep.GetProviderSpecificProperty(annotations.CloudflareRegionKey); exists { if epRegionKey, exists := ep.GetProviderSpecificProperty(annotations.CloudflareRegionKey); exists {
regionKey = epRegionKey regionKey = epRegionKey
} }
@ -164,47 +163,109 @@ func (p *CloudFlareProvider) regionalHostname(ep *endpoint.Endpoint) cloudflare.
} }
} }
// dataLocalizationRegionalHostnamesChanges processes a slice of cloudFlare changes and consolidates them // addEnpointsProviderSpecificRegionKeyProperty fetch the regional hostnames on cloudflare and
// into a list of data localization regional hostname changes. // adds Cloudflare-specific region keys to the provided endpoints.
// returns nil if no changes are needed //
func dataLocalizationRegionalHostnamesChanges(changes []*cloudFlareChange) ([]regionalHostnameChange, error) { // Do nothing if the regional services feature is not enabled.
regionalHostnameChanges := make(map[string]regionalHostnameChange) // Defaults to the region key configured in the provider config if not found in the regional hostnames.
func (p *CloudFlareProvider) addEnpointsProviderSpecificRegionKeyProperty(ctx context.Context, zoneID string, endpoints []*endpoint.Endpoint) error {
if !p.RegionalServicesConfig.Enabled {
return nil
}
// Filter endpoints to only those that support regional hostnames
// so we can skip regional hostname lookups if not needed.
var supportedEndpoints []*endpoint.Endpoint
for _, ep := range endpoints {
if recordTypeRegionalHostnameSupported[ep.RecordType] {
supportedEndpoints = append(supportedEndpoints, ep)
}
}
if len(supportedEndpoints) == 0 {
return nil
}
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, cloudflare.ZoneIdentifier(zoneID))
if err != nil {
return err
}
for _, ep := range supportedEndpoints {
if rh, found := regionalHostnames[ep.DNSName]; found {
ep.SetProviderSpecificProperty(annotations.CloudflareRegionKey, rh.RegionKey)
}
}
return nil
}
// desiredRegionalHostnames builds a list of desired regional hostnames from changes.
//
// If there is a delete and a create or update action for the same hostname,
// The create or update takes precedence.
// Returns an error for conflicting region keys.
func desiredRegionalHostnames(changes []*cloudFlareChange) ([]cloudflare.RegionalHostname, error) {
rhs := make(map[string]cloudflare.RegionalHostname)
for _, change := range changes { for _, change := range changes {
if change.RegionalHostname.Hostname == "" { if change.RegionalHostname.Hostname == "" {
continue continue
} }
if change.RegionalHostname.RegionKey == "" { rh, found := rhs[change.RegionalHostname.Hostname]
return nil, fmt.Errorf("region key is empty for regional hostname %q", change.RegionalHostname.Hostname) if !found {
} if change.Action == cloudFlareDelete {
regionalHostname, ok := regionalHostnameChanges[change.RegionalHostname.Hostname] rhs[change.RegionalHostname.Hostname] = cloudflare.RegionalHostname{
switch change.Action { Hostname: change.RegionalHostname.Hostname,
case cloudFlareCreate, cloudFlareUpdate: RegionKey: "", // Indicate that this regional hostname should not exists
if !ok {
regionalHostnameChanges[change.RegionalHostname.Hostname] = regionalHostnameChange{
action: change.Action,
RegionalHostname: change.RegionalHostname,
} }
continue continue
} }
if regionalHostname.RegionKey != change.RegionalHostname.RegionKey { rhs[change.RegionalHostname.Hostname] = change.RegionalHostname
return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.Hostname, regionalHostname.RegionKey, change.RegionalHostname.RegionKey)
}
if (change.Action == cloudFlareUpdate && regionalHostname.action != cloudFlareUpdate) ||
regionalHostname.action == cloudFlareDelete {
regionalHostnameChanges[change.RegionalHostname.Hostname] = regionalHostnameChange{
action: cloudFlareUpdate,
RegionalHostname: change.RegionalHostname,
}
}
case cloudFlareDelete:
if !ok {
regionalHostnameChanges[change.RegionalHostname.Hostname] = regionalHostnameChange{
action: cloudFlareDelete,
RegionalHostname: change.RegionalHostname,
}
continue continue
} }
if change.Action == cloudFlareDelete {
// A previous regional hostname exists so we can skip this delete action
continue
}
if rh.RegionKey == "" {
// If the existing regional hostname has no region key, we can overwrite it
rhs[change.RegionalHostname.Hostname] = change.RegionalHostname
continue
}
if rh.RegionKey != change.RegionalHostname.RegionKey {
return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.Hostname, rh.RegionKey, change.RegionalHostname.RegionKey)
} }
} }
return slices.Collect(maps.Values(regionalHostnameChanges)), nil return slices.Collect(maps.Values(rhs)), nil
}
// regionalHostnamesChanges build a list of changes needed to synchronize the current regional hostnames state with the desired state.
func regionalHostnamesChanges(desired []cloudflare.RegionalHostname, regionalHostnames RegionalHostnamesMap) []regionalHostnameChange {
changes := make([]regionalHostnameChange, 0)
for _, rh := range desired {
current, found := regionalHostnames[rh.Hostname]
if rh.RegionKey == "" {
// If the region key is empty, we don't want a regional hostname
if !found {
continue
}
changes = append(changes, regionalHostnameChange{
action: cloudFlareDelete,
RegionalHostname: rh,
})
continue
}
if !found {
changes = append(changes, regionalHostnameChange{
action: cloudFlareCreate,
RegionalHostname: rh,
})
continue
}
if rh.RegionKey != current.RegionKey {
changes = append(changes, regionalHostnameChange{
action: cloudFlareUpdate,
RegionalHostname: rh,
})
}
}
return changes
} }

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,6 @@ import (
"github.com/maxatome/go-testdeep/td" "github.com/maxatome/go-testdeep/td"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
@ -55,6 +54,7 @@ type mockCloudFlareClient struct {
listZonesContextError error listZonesContextError error
dnsRecordsError error dnsRecordsError error
customHostnames map[string][]cloudflare.CustomHostname customHostnames map[string][]cloudflare.CustomHostname
regionalHostnames map[string][]cloudflare.RegionalHostname
} }
var ExampleDomain = []cloudflare.DNSRecord{ var ExampleDomain = []cloudflare.DNSRecord{
@ -96,6 +96,7 @@ func NewMockCloudFlareClient() *mockCloudFlareClient {
"002": {}, "002": {},
}, },
customHostnames: map[string][]cloudflare.CustomHostname{}, customHostnames: map[string][]cloudflare.CustomHostname{},
regionalHostnames: map[string][]cloudflare.RegionalHostname{},
} }
} }
@ -227,43 +228,6 @@ func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, rc *cloudfla
return nil return nil
} }
func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error {
m.Actions = append(m.Actions, MockAction{
Name: "CreateDataLocalizationRegionalHostname",
ZoneId: rc.Identifier,
RecordId: "",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: rp.Hostname,
RegionKey: rp.RegionKey,
},
})
return nil
}
func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error {
m.Actions = append(m.Actions, MockAction{
Name: "UpdateDataLocalizationRegionalHostname",
ZoneId: rc.Identifier,
RecordId: "",
RecordData: cloudflare.DNSRecord{
Name: rp.Hostname,
},
})
return nil
}
func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error {
m.Actions = append(m.Actions, MockAction{
Name: "DeleteDataLocalizationRegionalHostname",
ZoneId: rc.Identifier,
RecordId: "",
RecordData: cloudflare.DNSRecord{
Name: hostname,
},
})
return nil
}
func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error { func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error {
m.Actions = append(m.Actions, MockAction{ m.Actions = append(m.Actions, MockAction{
Name: "Delete", Name: "Delete",
@ -756,110 +720,6 @@ func TestCloudflareSetProxied(t *testing.T) {
} }
} }
func TestCloudflareRegionalHostname(t *testing.T) {
endpoints := []*endpoint.Endpoint{
{
RecordType: "A",
DNSName: "bar.com",
Targets: endpoint.Targets{"127.0.0.1", "127.0.0.2"},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key",
Value: "eu",
},
},
},
}
AssertActions(t, &CloudFlareProvider{RegionKey: "us"}, endpoints, []MockAction{
{
Name: "Create",
ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
Type: "A",
Name: "bar.com",
Content: "127.0.0.1",
TTL: 1,
Proxied: proxyDisabled,
},
},
{
Name: "Create",
ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
Type: "A",
Name: "bar.com",
Content: "127.0.0.2",
TTL: 1,
Proxied: proxyDisabled,
},
},
{
Name: "CreateDataLocalizationRegionalHostname",
ZoneId: "001",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "bar.com",
RegionKey: "eu",
},
},
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareRegionalHostnameDefaults(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{RegionKey: "us"}, endpoints, []MockAction{
{
Name: "Create",
ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
Type: "A",
Name: "bar.com",
Content: "127.0.0.1",
TTL: 1,
Proxied: proxyDisabled,
},
},
{
Name: "Create",
ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
Type: "A",
Name: "bar.com",
Content: "127.0.0.2",
TTL: 1,
Proxied: proxyDisabled,
},
},
{
Name: "CreateDataLocalizationRegionalHostname",
ZoneId: "001",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "bar.com",
RegionKey: "us",
},
},
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareZones(t *testing.T) { func TestCloudflareZones(t *testing.T) {
provider := &CloudFlareProvider{ provider := &CloudFlareProvider{
Client: NewMockCloudFlareClient(), Client: NewMockCloudFlareClient(),
@ -1089,7 +949,7 @@ func TestCloudflareProvider(t *testing.T) {
provider.NewZoneIDFilter([]string{""}), provider.NewZoneIDFilter([]string{""}),
false, false,
true, true,
"", RegionalServicesConfig{Enabled: false},
CustomHostnamesConfig{Enabled: false}, CustomHostnamesConfig{Enabled: false},
DNSRecordsConfig{PerPage: 5000, Comment: ""}, DNSRecordsConfig{PerPage: 5000, Comment: ""},
) )
@ -1751,24 +1611,20 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
} }
func TestCloudFlareProvider_Region(t *testing.T) { func TestCloudFlareProvider_Region(t *testing.T) {
_ = os.Setenv("CF_API_TOKEN", "abc123def") t.Setenv("CF_API_TOKEN", "abc123def")
_ = os.Setenv("CF_API_EMAIL", "test@test.com") t.Setenv("CF_API_EMAIL", "test@test.com")
provider, err := NewCloudFlareProvider( provider, err := NewCloudFlareProvider(
endpoint.NewDomainFilter([]string{"example.com"}), endpoint.NewDomainFilter([]string{"example.com"}),
provider.ZoneIDFilter{}, provider.ZoneIDFilter{},
true, true,
false, false,
"us", RegionalServicesConfig{Enabled: false, RegionKey: "us"},
CustomHostnamesConfig{Enabled: false}, CustomHostnamesConfig{Enabled: false},
DNSRecordsConfig{PerPage: 50, Comment: ""}, DNSRecordsConfig{PerPage: 50, Comment: ""},
) )
if err != nil { assert.NoError(t, err, "should not fail to create provider")
t.Fatal(err) assert.True(t, provider.RegionalServicesConfig.Enabled, "expect regional services to be enabled")
} assert.Equal(t, "us", provider.RegionalServicesConfig.RegionKey, "expected region key to be 'us'")
if provider.RegionKey != "us" {
t.Errorf("expected region key to be 'us', but got '%s'", provider.RegionKey)
}
} }
func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
@ -1780,7 +1636,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
provider.ZoneIDFilter{}, provider.ZoneIDFilter{},
true, true,
false, false,
"us", RegionalServicesConfig{Enabled: true, RegionKey: "us"},
CustomHostnamesConfig{Enabled: false}, CustomHostnamesConfig{Enabled: false},
DNSRecordsConfig{PerPage: 50}, DNSRecordsConfig{PerPage: 50},
) )
@ -1823,7 +1679,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
provider.ZoneIDFilter{}, provider.ZoneIDFilter{},
true, true,
false, false,
"us", RegionalServicesConfig{Enabled: true, RegionKey: "us"},
CustomHostnamesConfig{Enabled: false}, CustomHostnamesConfig{Enabled: false},
DNSRecordsConfig{PerPage: 50, Comment: paidValidCommentBuilder.String()}, DNSRecordsConfig{PerPage: 50, Comment: paidValidCommentBuilder.String()},
) )