Skip to content

feat(cloudflare): improve cloudflare regional hostnames implementation #5309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion controller/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,10 @@ func buildProvider(
zoneIDFilter,
cfg.CloudflareProxied,
cfg.DryRun,
cfg.CloudflareRegionKey,
cloudflare.RegionalServicesConfig{
Enabled: cfg.CloudflareRegionalServices,
RegionKey: cfg.CloudflareRegionKey,
},
cloudflare.CustomHostnamesConfig{
Enabled: cfg.CloudflareCustomHostnames,
MinTLSVersion: cfg.CloudflareCustomHostnamesMinTLSVersion,
Expand Down
3 changes: 2 additions & 1 deletion docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-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-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: '') |
| `--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) |
Expand Down
17 changes: 11 additions & 6 deletions docs/tutorials/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ spec:
- --provider=cloudflare
- --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-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-record-comment="provisioned by external-dns" # (optional) configure comments for provisioned records; <=100 chars for free zones; <=500 chars for paid zones
env:
Expand Down Expand Up @@ -205,6 +206,7 @@ spec:
- --provider=cloudflare
- --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-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-record-comment="provisioned by external-dns" # (optional) configure comments for provisioned records; <=100 chars for free zones; <=500 chars for paid zones
env:
Expand Down Expand Up @@ -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.

## 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:**

- `earth` (default): All data centers (global)
- `eu`: European Union data centers only
- `us`: United States data centers only
- `ap`: Asia-Pacific data centers only
Expand All @@ -321,14 +329,11 @@ Using the `external-dns.alpha.kubernetes.io/cloudflare-region-key` annotation on
- `br`: Brazil data centers only
- `za`: South Africa 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/).

Currently, requires SuperAdmin or Admin role.

If not set the value will default to `global`.

## 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.
Expand Down
5 changes: 4 additions & 1 deletion pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ type Config struct {
CloudflareDNSRecordsComment string
CloudflareCustomHostnamesMinTLSVersion string
CloudflareCustomHostnamesCertificateAuthority string
CloudflareRegionalServices bool
CloudflareRegionKey string
CloudflareRecordComment string
CoreDNSPrefix string
Expand Down Expand Up @@ -256,6 +257,7 @@ var defaultConfig = &Config{
CloudflareCustomHostnamesMinTLSVersion: "1.0",
CloudflareDNSRecordsPerPage: 100,
CloudflareProxied: false,
CloudflareRegionalServices: false,
CloudflareRegionKey: "earth",

CombineFQDNAndAnnotation: false,
Expand Down Expand Up @@ -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-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-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("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ var (
CloudflareCustomHostnamesMinTLSVersion: "1.3",
CloudflareCustomHostnamesCertificateAuthority: "google",
CloudflareDNSRecordsPerPage: 5000,
CloudflareRegionalServices: true,
CloudflareRegionKey: "us",
CoreDNSPrefix: "/coredns/",
AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
Expand Down Expand Up @@ -296,6 +297,7 @@ func TestParseFlags(t *testing.T) {
"--cloudflare-custom-hostnames-min-tls-version=1.3",
"--cloudflare-custom-hostnames-certificate-authority=google",
"--cloudflare-dns-records-per-page=5000",
"--cloudflare-regional-services",
"--cloudflare-region-key=us",
"--coredns-prefix=/coredns/",
"--akamai-serviceconsumerdomain=oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
Expand Down Expand Up @@ -424,6 +426,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_MIN_TLS_VERSION": "1.3",
"EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_CERTIFICATE_AUTHORITY": "google",
"EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000",
"EXTERNAL_DNS_CLOUDFLARE_REGIONAL_SERVICES": "1",
"EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us",
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
"EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
Expand Down
81 changes: 56 additions & 25 deletions provider/cloudflare/cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ type cloudFlareDNS interface {
CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error)
DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) 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
UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error
DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error
Expand Down Expand Up @@ -226,13 +227,13 @@ type CloudFlareProvider struct {
provider.BaseProvider
Client cloudFlareDNS
// only consider hosted zones managing domains ending in this suffix
domainFilter *endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
proxiedByDefault bool
DryRun bool
CustomHostnamesConfig CustomHostnamesConfig
DNSRecordsConfig DNSRecordsConfig
RegionKey string
domainFilter *endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
proxiedByDefault bool
DryRun bool
CustomHostnamesConfig CustomHostnamesConfig
DNSRecordsConfig DNSRecordsConfig
RegionalServicesConfig RegionalServicesConfig
}

// cloudFlareChange differentiates between ChangActions
Expand Down Expand Up @@ -289,7 +290,15 @@ func convertCloudflareError(err error) error {
}

// 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
var (
config *cloudflare.API
Expand All @@ -312,15 +321,19 @@ func NewCloudFlareProvider(domainFilter *endpoint.DomainFilter, zoneIDFilter pro
return nil, fmt.Errorf("failed to initialize cloudflare provider: %w", err)
}

if regionalServicesConfig.RegionKey != "" {
regionalServicesConfig.Enabled = true
}

return &CloudFlareProvider{
Client: zoneService{config},
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
proxiedByDefault: proxiedByDefault,
CustomHostnamesConfig: customHostnamesConfig,
DryRun: dryRun,
RegionKey: regionKey,
DNSRecordsConfig: dnsRecordsConfig,
Client: zoneService{config},
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
proxiedByDefault: proxiedByDefault,
CustomHostnamesConfig: customHostnamesConfig,
DryRun: dryRun,
RegionalServicesConfig: regionalServicesConfig,
DNSRecordsConfig: dnsRecordsConfig,
}, nil
}

Expand Down Expand Up @@ -389,7 +402,13 @@ func (p *CloudFlareProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
// 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, 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
Expand Down Expand Up @@ -605,16 +624,21 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
}
}

if regionalHostnamesChanges, err := dataLocalizationRegionalHostnamesChanges(zoneChanges); err == nil {
if !p.submitDataLocalizationRegionalHostnameChanges(ctx, regionalHostnamesChanges, resourceContainer) {
failedChange = true
if p.RegionalServicesConfig.Enabled {
desiredRegionalHostnames, err := desiredRegionalHostnames(zoneChanges)
if err != nil {
return fmt.Errorf("failed to build desired regional hostnames: %w", err)
}
} else {
logFields := log.Fields{
"zone": zoneID,
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
}
}
log.WithFields(logFields).Errorf("failed to build data localization regional hostname changes: %v", err)
failedChange = true
}

if failedChange {
Expand Down Expand Up @@ -650,6 +674,13 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]
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)
}
return adjustedEndpoints, nil
Expand Down
Loading
Loading