mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
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:
parent
104157f8b1
commit
7108979df1
@ -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,
|
||||
|
@ -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) |
|
||||
|
@ -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:
|
||||
@ -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:
|
||||
@ -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
|
||||
@ -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.
|
||||
|
@ -113,6 +113,7 @@ type Config struct {
|
||||
CloudflareDNSRecordsComment string
|
||||
CloudflareCustomHostnamesMinTLSVersion string
|
||||
CloudflareCustomHostnamesCertificateAuthority string
|
||||
CloudflareRegionalServices bool
|
||||
CloudflareRegionKey string
|
||||
CloudflareRecordComment string
|
||||
CoreDNSPrefix string
|
||||
@ -256,6 +257,7 @@ var defaultConfig = &Config{
|
||||
CloudflareCustomHostnamesMinTLSVersion: "1.0",
|
||||
CloudflareDNSRecordsPerPage: 100,
|
||||
CloudflareProxied: false,
|
||||
CloudflareRegionalServices: false,
|
||||
CloudflareRegionKey: "earth",
|
||||
|
||||
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-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)
|
||||
|
@ -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",
|
||||
@ -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",
|
||||
@ -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",
|
||||
|
@ -119,6 +119,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
|
||||
@ -222,7 +223,7 @@ type CloudFlareProvider struct {
|
||||
DryRun bool
|
||||
CustomHostnamesConfig CustomHostnamesConfig
|
||||
DNSRecordsConfig DNSRecordsConfig
|
||||
RegionKey string
|
||||
RegionalServicesConfig RegionalServicesConfig
|
||||
}
|
||||
|
||||
// cloudFlareChange differentiates between ChangActions
|
||||
@ -279,7 +280,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
|
||||
@ -302,6 +311,10 @@ 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,
|
||||
@ -309,7 +322,7 @@ func NewCloudFlareProvider(domainFilter *endpoint.DomainFilter, zoneIDFilter pro
|
||||
proxiedByDefault: proxiedByDefault,
|
||||
CustomHostnamesConfig: customHostnamesConfig,
|
||||
DryRun: dryRun,
|
||||
RegionKey: regionKey,
|
||||
RegionalServicesConfig: regionalServicesConfig,
|
||||
DNSRecordsConfig: dnsRecordsConfig,
|
||||
}, 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
|
||||
// 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
|
||||
@ -595,16 +614,21 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
|
||||
}
|
||||
}
|
||||
|
||||
if regionalHostnamesChanges, err := dataLocalizationRegionalHostnamesChanges(zoneChanges); err == nil {
|
||||
if !p.submitDataLocalizationRegionalHostnameChanges(ctx, regionalHostnamesChanges, resourceContainer) {
|
||||
if p.RegionalServicesConfig.Enabled {
|
||||
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
|
||||
}
|
||||
} else {
|
||||
logFields := log.Fields{
|
||||
"zone": zoneID,
|
||||
}
|
||||
log.WithFields(logFields).Errorf("failed to build data localization regional hostname changes: %v", err)
|
||||
failedChange = true
|
||||
}
|
||||
|
||||
if failedChange {
|
||||
@ -640,6 +664,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
|
||||
|
@ -18,30 +18,40 @@ package cloudflare
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
cloudflare "github.com/cloudflare/cloudflare-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/source/annotations"
|
||||
)
|
||||
|
||||
type RegionalServicesConfig struct {
|
||||
Enabled bool
|
||||
RegionKey string
|
||||
}
|
||||
|
||||
var recordTypeRegionalHostnameSupported = map[string]bool{
|
||||
"A": true,
|
||||
"AAAA": true,
|
||||
"CNAME": true,
|
||||
}
|
||||
|
||||
// RegionalHostnamesMap is a map of regional hostnames keyed by hostname.
|
||||
type RegionalHostnamesMap map[string]cloudflare.RegionalHostname
|
||||
|
||||
type regionalHostnameChange struct {
|
||||
action changeAction
|
||||
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 {
|
||||
_, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp)
|
||||
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
|
||||
func (p *CloudFlareProvider) submitDataLocalizationRegionalHostnameChanges(ctx context.Context, rhChanges []regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool {
|
||||
// submitRegionalHostnameChanges applies a set of regional hostname changes, returns false if at least one fails
|
||||
func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, rhChanges []regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool {
|
||||
failedChange := false
|
||||
|
||||
for _, rhChange := range rhChanges {
|
||||
logFields := log.Fields{
|
||||
"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 {
|
||||
if !p.submitRegionalHostnameChange(ctx, rhChange, resourceContainer) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if p.RegionKey == "" || !recordTypeRegionalHostnameSupported[ep.RecordType] {
|
||||
if !p.RegionalServicesConfig.Enabled || !recordTypeRegionalHostnameSupported[ep.RecordType] {
|
||||
return cloudflare.RegionalHostname{}
|
||||
}
|
||||
regionKey := p.RegionKey
|
||||
regionKey := p.RegionalServicesConfig.RegionKey
|
||||
if epRegionKey, exists := ep.GetProviderSpecificProperty(annotations.CloudflareRegionKey); exists {
|
||||
regionKey = epRegionKey
|
||||
}
|
||||
@ -164,47 +163,109 @@ func (p *CloudFlareProvider) regionalHostname(ep *endpoint.Endpoint) cloudflare.
|
||||
}
|
||||
}
|
||||
|
||||
// dataLocalizationRegionalHostnamesChanges processes a slice of cloudFlare changes and consolidates them
|
||||
// into a list of data localization regional hostname changes.
|
||||
// returns nil if no changes are needed
|
||||
func dataLocalizationRegionalHostnamesChanges(changes []*cloudFlareChange) ([]regionalHostnameChange, error) {
|
||||
regionalHostnameChanges := make(map[string]regionalHostnameChange)
|
||||
// addEnpointsProviderSpecificRegionKeyProperty fetch the regional hostnames on cloudflare and
|
||||
// adds Cloudflare-specific region keys to the provided endpoints.
|
||||
//
|
||||
// Do nothing if the regional services feature is not enabled.
|
||||
// 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 {
|
||||
if change.RegionalHostname.Hostname == "" {
|
||||
continue
|
||||
}
|
||||
if change.RegionalHostname.RegionKey == "" {
|
||||
return nil, fmt.Errorf("region key is empty for regional hostname %q", change.RegionalHostname.Hostname)
|
||||
}
|
||||
regionalHostname, ok := regionalHostnameChanges[change.RegionalHostname.Hostname]
|
||||
switch change.Action {
|
||||
case cloudFlareCreate, cloudFlareUpdate:
|
||||
if !ok {
|
||||
regionalHostnameChanges[change.RegionalHostname.Hostname] = regionalHostnameChange{
|
||||
action: change.Action,
|
||||
RegionalHostname: change.RegionalHostname,
|
||||
rh, found := rhs[change.RegionalHostname.Hostname]
|
||||
if !found {
|
||||
if change.Action == cloudFlareDelete {
|
||||
rhs[change.RegionalHostname.Hostname] = cloudflare.RegionalHostname{
|
||||
Hostname: change.RegionalHostname.Hostname,
|
||||
RegionKey: "", // Indicate that this regional hostname should not exists
|
||||
}
|
||||
continue
|
||||
}
|
||||
if regionalHostname.RegionKey != change.RegionalHostname.RegionKey {
|
||||
return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.Hostname, regionalHostname.RegionKey, change.RegionalHostname.RegionKey)
|
||||
rhs[change.RegionalHostname.Hostname] = change.RegionalHostname
|
||||
continue
|
||||
}
|
||||
if (change.Action == cloudFlareUpdate && regionalHostname.action != cloudFlareUpdate) ||
|
||||
regionalHostname.action == cloudFlareDelete {
|
||||
regionalHostnameChanges[change.RegionalHostname.Hostname] = regionalHostnameChange{
|
||||
action: cloudFlareUpdate,
|
||||
RegionalHostname: change.RegionalHostname,
|
||||
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)
|
||||
}
|
||||
}
|
||||
case cloudFlareDelete:
|
||||
if !ok {
|
||||
regionalHostnameChanges[change.RegionalHostname.Hostname] = regionalHostnameChange{
|
||||
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: change.RegionalHostname,
|
||||
}
|
||||
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 slices.Collect(maps.Values(regionalHostnameChanges)), nil
|
||||
return changes
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -30,7 +30,6 @@ import (
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/internal/testutils"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
@ -55,6 +54,7 @@ type mockCloudFlareClient struct {
|
||||
listZonesContextError error
|
||||
dnsRecordsError error
|
||||
customHostnames map[string][]cloudflare.CustomHostname
|
||||
regionalHostnames map[string][]cloudflare.RegionalHostname
|
||||
}
|
||||
|
||||
var ExampleDomain = []cloudflare.DNSRecord{
|
||||
@ -96,6 +96,7 @@ func NewMockCloudFlareClient() *mockCloudFlareClient {
|
||||
"002": {},
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
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) {
|
||||
provider := &CloudFlareProvider{
|
||||
Client: NewMockCloudFlareClient(),
|
||||
@ -1089,7 +949,7 @@ func TestCloudflareProvider(t *testing.T) {
|
||||
provider.NewZoneIDFilter([]string{""}),
|
||||
false,
|
||||
true,
|
||||
"",
|
||||
RegionalServicesConfig{Enabled: false},
|
||||
CustomHostnamesConfig{Enabled: false},
|
||||
DNSRecordsConfig{PerPage: 5000, Comment: ""},
|
||||
)
|
||||
@ -1751,24 +1611,20 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCloudFlareProvider_Region(t *testing.T) {
|
||||
_ = os.Setenv("CF_API_TOKEN", "abc123def")
|
||||
_ = os.Setenv("CF_API_EMAIL", "test@test.com")
|
||||
t.Setenv("CF_API_TOKEN", "abc123def")
|
||||
t.Setenv("CF_API_EMAIL", "test@test.com")
|
||||
provider, err := NewCloudFlareProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.ZoneIDFilter{},
|
||||
true,
|
||||
false,
|
||||
"us",
|
||||
RegionalServicesConfig{Enabled: false, RegionKey: "us"},
|
||||
CustomHostnamesConfig{Enabled: false},
|
||||
DNSRecordsConfig{PerPage: 50, Comment: ""},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if provider.RegionKey != "us" {
|
||||
t.Errorf("expected region key to be 'us', but got '%s'", provider.RegionKey)
|
||||
}
|
||||
assert.NoError(t, err, "should not fail to create provider")
|
||||
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'")
|
||||
}
|
||||
|
||||
func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
|
||||
@ -1780,7 +1636,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
|
||||
provider.ZoneIDFilter{},
|
||||
true,
|
||||
false,
|
||||
"us",
|
||||
RegionalServicesConfig{Enabled: true, RegionKey: "us"},
|
||||
CustomHostnamesConfig{Enabled: false},
|
||||
DNSRecordsConfig{PerPage: 50},
|
||||
)
|
||||
@ -1823,7 +1679,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
|
||||
provider.ZoneIDFilter{},
|
||||
true,
|
||||
false,
|
||||
"us",
|
||||
RegionalServicesConfig{Enabled: true, RegionKey: "us"},
|
||||
CustomHostnamesConfig{Enabled: false},
|
||||
DNSRecordsConfig{PerPage: 50, Comment: paidValidCommentBuilder.String()},
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user