cloudflare: bugfix - do not attempt to create unconfigured empty custom hostnames; improve tests; streamline logic (#5146)

improve test coverage

test the edge case when the custom hostname has changed during the record deletion

don't use custom hostnames if Cloudflare for SaaS fails to authenticate

Use new --cloudflare-custom-hostnames flag to enable cloudflare custom hostnames support

custom hostnames flags --cloudflare-custom-hostnames-min-tls-version and --cloudflare-custom-hostnames-certificate-authority support

markdown lint

Update cloudflare.md
This commit is contained in:
mrozentsvayg 2025-03-12 09:59:48 -07:00 committed by GitHub
parent a3f4188965
commit 44f1008ee1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1387 additions and 773 deletions

View File

@ -87,6 +87,9 @@
| `--tencent-cloud-config-file="/etc/kubernetes/tencent-cloud.json"` | When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud) | | `--tencent-cloud-config-file="/etc/kubernetes/tencent-cloud.json"` | When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud) |
| `--tencent-cloud-zone-type=` | When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private) | | `--tencent-cloud-zone-type=` | When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private) |
| `--[no-]cloudflare-proxied` | When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled) | | `--[no-]cloudflare-proxied` | When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled) |
| `--[no-]cloudflare-custom-hostnames` | When using the Cloudflare provider, specify if the Custom Hostnames feature will be used. Requires "Cloudflare for SaaS" enabled. (default: disabled) |
| `--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=google` | When using the Cloudflare provider with the Custom Hostnames, specify which Cerrtificate Authority will be used by default. (default: google, options: google, ssl_com, lets_encrypt) |
| `--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) | | `--cloudflare-region-key=CLOUDFLARE-REGION-KEY` | When using the Cloudflare provider, specify the region (default: earth) |
| `--coredns-prefix="/skydns/"` | When using the CoreDNS provider, specify the prefix name | | `--coredns-prefix="/skydns/"` | When using the CoreDNS provider, specify the prefix name |

View File

@ -310,7 +310,14 @@ If not set the value will default to `global`.
## Setting cloudflare-custom-hostname ## Setting cloudflare-custom-hostname
Using the `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: "<custom hostname>"` annotation, you can have [custom hostnames](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/) automatically managed for A/CNAME record as a custom origin. You can automatically configure custom hostnames for A/CNAME DNS records (as custom origins) using the `--cloudflare-custom-hostnames` flag and the `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: "<custom hostname>"` annotation.
See [Cloudflare for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/) for more information on custom hostnames.
This feature is disabled by default and supports the `--cloudflare-custom-hostnames-min-tls-version` and `--cloudflare-custom-hostnames-certificate-authority` flags.
The custom hostname DNS must resolve to the Cloudflare DNS record (`external-dns.alpha.kubernetes.io/hostname`) for automatic certificate validation via the HTTP method. It's important to note that the TXT method does not allow automatic validation and is not supported.
Requires [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) product and "SSL and Certificates" API permission. Requires [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) product and "SSL and Certificates" API permission.
## Using CRD source to manage DNS records in Cloudflare ## Using CRD source to manage DNS records in Cloudflare

13
main.go
View File

@ -250,7 +250,18 @@ func main() {
case "civo": case "civo":
p, err = civo.NewCivoProvider(domainFilter, cfg.DryRun) p, err = civo.NewCivoProvider(domainFilter, cfg.DryRun)
case "cloudflare": case "cloudflare":
p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareProxied, cfg.DryRun, cfg.CloudflareDNSRecordsPerPage, cfg.CloudflareRegionKey) p, err = cloudflare.NewCloudFlareProvider(
domainFilter,
zoneIDFilter,
cfg.CloudflareProxied,
cfg.DryRun,
cfg.CloudflareDNSRecordsPerPage,
cfg.CloudflareRegionKey,
cloudflare.CustomHostnamesConfig{
Enabled: cfg.CloudflareCustomHostnames,
MinTLSVersion: cfg.CloudflareCustomHostnamesMinTLSVersion,
CertificateAuthority: cfg.CloudflareCustomHostnamesCertificateAuthority,
})
case "google": case "google":
p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun) p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun)
case "digitalocean": case "digitalocean":

View File

@ -43,332 +43,338 @@ var Version = "unknown"
// Config is a project-wide configuration // Config is a project-wide configuration
type Config struct { type Config struct {
APIServerURL string APIServerURL string
KubeConfig string KubeConfig string
RequestTimeout time.Duration RequestTimeout time.Duration
DefaultTargets []string DefaultTargets []string
GlooNamespaces []string GlooNamespaces []string
SkipperRouteGroupVersion string SkipperRouteGroupVersion string
Sources []string Sources []string
Namespace string Namespace string
AnnotationFilter string AnnotationFilter string
LabelFilter string LabelFilter string
IngressClassNames []string IngressClassNames []string
FQDNTemplate string FQDNTemplate string
CombineFQDNAndAnnotation bool CombineFQDNAndAnnotation bool
IgnoreHostnameAnnotation bool IgnoreHostnameAnnotation bool
IgnoreNonHostNetworkPods bool IgnoreNonHostNetworkPods bool
IgnoreIngressTLSSpec bool IgnoreIngressTLSSpec bool
IgnoreIngressRulesSpec bool IgnoreIngressRulesSpec bool
ListenEndpointEvents bool ListenEndpointEvents bool
GatewayName string GatewayName string
GatewayNamespace string GatewayNamespace string
GatewayLabelFilter string GatewayLabelFilter string
Compatibility string Compatibility string
PodSourceDomain string PodSourceDomain string
PublishInternal bool PublishInternal bool
PublishHostIP bool PublishHostIP bool
AlwaysPublishNotReadyAddresses bool AlwaysPublishNotReadyAddresses bool
ConnectorSourceServer string ConnectorSourceServer string
Provider string Provider string
ProviderCacheTime time.Duration ProviderCacheTime time.Duration
GoogleProject string GoogleProject string
GoogleBatchChangeSize int GoogleBatchChangeSize int
GoogleBatchChangeInterval time.Duration GoogleBatchChangeInterval time.Duration
GoogleZoneVisibility string GoogleZoneVisibility string
DomainFilter []string DomainFilter []string
ExcludeDomains []string ExcludeDomains []string
RegexDomainFilter *regexp.Regexp RegexDomainFilter *regexp.Regexp
RegexDomainExclusion *regexp.Regexp RegexDomainExclusion *regexp.Regexp
ZoneNameFilter []string ZoneNameFilter []string
ZoneIDFilter []string ZoneIDFilter []string
TargetNetFilter []string TargetNetFilter []string
ExcludeTargetNets []string ExcludeTargetNets []string
AlibabaCloudConfigFile string AlibabaCloudConfigFile string
AlibabaCloudZoneType string AlibabaCloudZoneType string
AWSZoneType string AWSZoneType string
AWSZoneTagFilter []string AWSZoneTagFilter []string
AWSAssumeRole string AWSAssumeRole string
AWSProfiles []string AWSProfiles []string
AWSAssumeRoleExternalID string `secure:"yes"` AWSAssumeRoleExternalID string `secure:"yes"`
AWSBatchChangeSize int AWSBatchChangeSize int
AWSBatchChangeSizeBytes int AWSBatchChangeSizeBytes int
AWSBatchChangeSizeValues int AWSBatchChangeSizeValues int
AWSBatchChangeInterval time.Duration AWSBatchChangeInterval time.Duration
AWSEvaluateTargetHealth bool AWSEvaluateTargetHealth bool
AWSAPIRetries int AWSAPIRetries int
AWSPreferCNAME bool AWSPreferCNAME bool
AWSZoneCacheDuration time.Duration AWSZoneCacheDuration time.Duration
AWSSDServiceCleanup bool AWSSDServiceCleanup bool
AWSSDCreateTag map[string]string AWSSDCreateTag map[string]string
AWSZoneMatchParent bool AWSZoneMatchParent bool
AWSDynamoDBRegion string AWSDynamoDBRegion string
AWSDynamoDBTable string AWSDynamoDBTable string
AzureConfigFile string AzureConfigFile string
AzureResourceGroup string AzureResourceGroup string
AzureSubscriptionID string AzureSubscriptionID string
AzureUserAssignedIdentityClientID string AzureUserAssignedIdentityClientID string
AzureActiveDirectoryAuthorityHost string AzureActiveDirectoryAuthorityHost string
AzureZonesCacheDuration time.Duration AzureZonesCacheDuration time.Duration
CloudflareProxied bool CloudflareProxied bool
CloudflareDNSRecordsPerPage int CloudflareCustomHostnames bool
CloudflareRegionKey string CloudflareCustomHostnamesMinTLSVersion string
CoreDNSPrefix string CloudflareCustomHostnamesCertificateAuthority string
AkamaiServiceConsumerDomain string CloudflareDNSRecordsPerPage int
AkamaiClientToken string CloudflareRegionKey string
AkamaiClientSecret string CoreDNSPrefix string
AkamaiAccessToken string AkamaiServiceConsumerDomain string
AkamaiEdgercPath string AkamaiClientToken string
AkamaiEdgercSection string AkamaiClientSecret string
OCIConfigFile string AkamaiAccessToken string
OCICompartmentOCID string AkamaiEdgercPath string
OCIAuthInstancePrincipal bool AkamaiEdgercSection string
OCIZoneScope string OCIConfigFile string
OCIZoneCacheDuration time.Duration OCICompartmentOCID string
InMemoryZones []string OCIAuthInstancePrincipal bool
OVHEndpoint string OCIZoneScope string
OVHApiRateLimit int OCIZoneCacheDuration time.Duration
PDNSServer string InMemoryZones []string
PDNSServerID string OVHEndpoint string
PDNSAPIKey string `secure:"yes"` OVHApiRateLimit int
PDNSSkipTLSVerify bool PDNSServer string
TLSCA string PDNSServerID string
TLSClientCert string PDNSAPIKey string `secure:"yes"`
TLSClientCertKey string PDNSSkipTLSVerify bool
Policy string TLSCA string
Registry string TLSClientCert string
TXTOwnerID string TLSClientCertKey string
TXTPrefix string Policy string
TXTSuffix string Registry string
TXTEncryptEnabled bool TXTOwnerID string
TXTEncryptAESKey string `secure:"yes"` TXTPrefix string
TXTNewFormatOnly bool TXTSuffix string
Interval time.Duration TXTEncryptEnabled bool
MinEventSyncInterval time.Duration TXTEncryptAESKey string `secure:"yes"`
Once bool TXTNewFormatOnly bool
DryRun bool Interval time.Duration
UpdateEvents bool MinEventSyncInterval time.Duration
LogFormat string Once bool
MetricsAddress string DryRun bool
LogLevel string UpdateEvents bool
TXTCacheInterval time.Duration LogFormat string
TXTWildcardReplacement string MetricsAddress string
ExoscaleEndpoint string LogLevel string
ExoscaleAPIKey string `secure:"yes"` TXTCacheInterval time.Duration
ExoscaleAPISecret string `secure:"yes"` TXTWildcardReplacement string
ExoscaleAPIEnvironment string ExoscaleEndpoint string
ExoscaleAPIZone string ExoscaleAPIKey string `secure:"yes"`
CRDSourceAPIVersion string ExoscaleAPISecret string `secure:"yes"`
CRDSourceKind string ExoscaleAPIEnvironment string
ServiceTypeFilter []string ExoscaleAPIZone string
CFAPIEndpoint string CRDSourceAPIVersion string
CFUsername string CRDSourceKind string
CFPassword string ServiceTypeFilter []string
ResolveServiceLoadBalancerHostname bool CFAPIEndpoint string
RFC2136Host []string CFUsername string
RFC2136Port int CFPassword string
RFC2136Zone []string ResolveServiceLoadBalancerHostname bool
RFC2136Insecure bool RFC2136Host []string
RFC2136GSSTSIG bool RFC2136Port int
RFC2136CreatePTR bool RFC2136Zone []string
RFC2136KerberosRealm string RFC2136Insecure bool
RFC2136KerberosUsername string RFC2136GSSTSIG bool
RFC2136KerberosPassword string `secure:"yes"` RFC2136CreatePTR bool
RFC2136TSIGKeyName string RFC2136KerberosRealm string
RFC2136TSIGSecret string `secure:"yes"` RFC2136KerberosUsername string
RFC2136TSIGSecretAlg string RFC2136KerberosPassword string `secure:"yes"`
RFC2136TAXFR bool RFC2136TSIGKeyName string
RFC2136MinTTL time.Duration RFC2136TSIGSecret string `secure:"yes"`
RFC2136LoadBalancingStrategy string RFC2136TSIGSecretAlg string
RFC2136BatchChangeSize int RFC2136TAXFR bool
RFC2136UseTLS bool RFC2136MinTTL time.Duration
RFC2136SkipTLSVerify bool RFC2136LoadBalancingStrategy string
NS1Endpoint string RFC2136BatchChangeSize int
NS1IgnoreSSL bool RFC2136UseTLS bool
NS1MinTTLSeconds int RFC2136SkipTLSVerify bool
TransIPAccountName string NS1Endpoint string
TransIPPrivateKeyFile string NS1IgnoreSSL bool
DigitalOceanAPIPageSize int NS1MinTTLSeconds int
ManagedDNSRecordTypes []string TransIPAccountName string
ExcludeDNSRecordTypes []string TransIPPrivateKeyFile string
GoDaddyAPIKey string `secure:"yes"` DigitalOceanAPIPageSize int
GoDaddySecretKey string `secure:"yes"` ManagedDNSRecordTypes []string
GoDaddyTTL int64 ExcludeDNSRecordTypes []string
GoDaddyOTE bool GoDaddyAPIKey string `secure:"yes"`
OCPRouterName string GoDaddySecretKey string `secure:"yes"`
IBMCloudProxied bool GoDaddyTTL int64
IBMCloudConfigFile string GoDaddyOTE bool
TencentCloudConfigFile string OCPRouterName string
TencentCloudZoneType string IBMCloudProxied bool
PiholeServer string IBMCloudConfigFile string
PiholePassword string `secure:"yes"` TencentCloudConfigFile string
PiholeTLSInsecureSkipVerify bool TencentCloudZoneType string
PluralCluster string PiholeServer string
PluralProvider string PiholePassword string `secure:"yes"`
WebhookProviderURL string PiholeTLSInsecureSkipVerify bool
WebhookProviderReadTimeout time.Duration PluralCluster string
WebhookProviderWriteTimeout time.Duration PluralProvider string
WebhookServer bool WebhookProviderURL string
TraefikDisableLegacy bool WebhookProviderReadTimeout time.Duration
TraefikDisableNew bool WebhookProviderWriteTimeout time.Duration
NAT64Networks []string WebhookServer bool
TraefikDisableLegacy bool
TraefikDisableNew bool
NAT64Networks []string
} }
var defaultConfig = &Config{ var defaultConfig = &Config{
APIServerURL: "", APIServerURL: "",
KubeConfig: "", KubeConfig: "",
RequestTimeout: time.Second * 30, RequestTimeout: time.Second * 30,
DefaultTargets: []string{}, DefaultTargets: []string{},
GlooNamespaces: []string{"gloo-system"}, GlooNamespaces: []string{"gloo-system"},
SkipperRouteGroupVersion: "zalando.org/v1", SkipperRouteGroupVersion: "zalando.org/v1",
Sources: nil, Sources: nil,
Namespace: "", Namespace: "",
AnnotationFilter: "", AnnotationFilter: "",
LabelFilter: labels.Everything().String(), LabelFilter: labels.Everything().String(),
IngressClassNames: nil, IngressClassNames: nil,
FQDNTemplate: "", FQDNTemplate: "",
CombineFQDNAndAnnotation: false, CombineFQDNAndAnnotation: false,
IgnoreHostnameAnnotation: false, IgnoreHostnameAnnotation: false,
IgnoreIngressTLSSpec: false, IgnoreIngressTLSSpec: false,
IgnoreIngressRulesSpec: false, IgnoreIngressRulesSpec: false,
GatewayName: "", GatewayName: "",
GatewayNamespace: "", GatewayNamespace: "",
GatewayLabelFilter: "", GatewayLabelFilter: "",
Compatibility: "", Compatibility: "",
PublishInternal: false, PublishInternal: false,
PublishHostIP: false, PublishHostIP: false,
ConnectorSourceServer: "localhost:8080", ConnectorSourceServer: "localhost:8080",
Provider: "", Provider: "",
ProviderCacheTime: 0, ProviderCacheTime: 0,
GoogleProject: "", GoogleProject: "",
GoogleBatchChangeSize: 1000, GoogleBatchChangeSize: 1000,
GoogleBatchChangeInterval: time.Second, GoogleBatchChangeInterval: time.Second,
GoogleZoneVisibility: "", GoogleZoneVisibility: "",
DomainFilter: []string{}, DomainFilter: []string{},
ZoneIDFilter: []string{}, ZoneIDFilter: []string{},
ExcludeDomains: []string{}, ExcludeDomains: []string{},
RegexDomainFilter: regexp.MustCompile(""), RegexDomainFilter: regexp.MustCompile(""),
RegexDomainExclusion: regexp.MustCompile(""), RegexDomainExclusion: regexp.MustCompile(""),
TargetNetFilter: []string{}, TargetNetFilter: []string{},
ExcludeTargetNets: []string{}, ExcludeTargetNets: []string{},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "", AWSZoneType: "",
AWSZoneTagFilter: []string{}, AWSZoneTagFilter: []string{},
AWSZoneMatchParent: false, AWSZoneMatchParent: false,
AWSAssumeRole: "", AWSAssumeRole: "",
AWSAssumeRoleExternalID: "", AWSAssumeRoleExternalID: "",
AWSBatchChangeSize: 1000, AWSBatchChangeSize: 1000,
AWSBatchChangeSizeBytes: 32000, AWSBatchChangeSizeBytes: 32000,
AWSBatchChangeSizeValues: 1000, AWSBatchChangeSizeValues: 1000,
AWSBatchChangeInterval: time.Second, AWSBatchChangeInterval: time.Second,
AWSEvaluateTargetHealth: true, AWSEvaluateTargetHealth: true,
AWSAPIRetries: 3, AWSAPIRetries: 3,
AWSPreferCNAME: false, AWSPreferCNAME: false,
AWSZoneCacheDuration: 0 * time.Second, AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false, AWSSDServiceCleanup: false,
AWSSDCreateTag: map[string]string{}, AWSSDCreateTag: map[string]string{},
AWSDynamoDBRegion: "", AWSDynamoDBRegion: "",
AWSDynamoDBTable: "external-dns", AWSDynamoDBTable: "external-dns",
AzureConfigFile: "/etc/kubernetes/azure.json", AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "", AzureResourceGroup: "",
AzureSubscriptionID: "", AzureSubscriptionID: "",
AzureZonesCacheDuration: 0 * time.Second, AzureZonesCacheDuration: 0 * time.Second,
CloudflareProxied: false, CloudflareProxied: false,
CloudflareDNSRecordsPerPage: 100, CloudflareCustomHostnames: false,
CloudflareRegionKey: "earth", CloudflareCustomHostnamesMinTLSVersion: "1.0",
CoreDNSPrefix: "/skydns/", CloudflareCustomHostnamesCertificateAuthority: "google",
AkamaiServiceConsumerDomain: "", CloudflareDNSRecordsPerPage: 100,
AkamaiClientToken: "", CloudflareRegionKey: "earth",
AkamaiClientSecret: "", CoreDNSPrefix: "/skydns/",
AkamaiAccessToken: "", AkamaiServiceConsumerDomain: "",
AkamaiEdgercSection: "", AkamaiClientToken: "",
AkamaiEdgercPath: "", AkamaiClientSecret: "",
OCIConfigFile: "/etc/kubernetes/oci.yaml", AkamaiAccessToken: "",
OCIZoneScope: "GLOBAL", AkamaiEdgercSection: "",
OCIZoneCacheDuration: 0 * time.Second, AkamaiEdgercPath: "",
InMemoryZones: []string{}, OCIConfigFile: "/etc/kubernetes/oci.yaml",
OVHEndpoint: "ovh-eu", OCIZoneScope: "GLOBAL",
OVHApiRateLimit: 20, OCIZoneCacheDuration: 0 * time.Second,
PDNSServer: "http://localhost:8081", InMemoryZones: []string{},
PDNSServerID: "localhost", OVHEndpoint: "ovh-eu",
PDNSAPIKey: "", OVHApiRateLimit: 20,
PDNSSkipTLSVerify: false, PDNSServer: "http://localhost:8081",
PodSourceDomain: "", PDNSServerID: "localhost",
TLSCA: "", PDNSAPIKey: "",
TLSClientCert: "", PDNSSkipTLSVerify: false,
TLSClientCertKey: "", PodSourceDomain: "",
Policy: "sync", TLSCA: "",
Registry: "txt", TLSClientCert: "",
TXTOwnerID: "default", TLSClientCertKey: "",
TXTPrefix: "", Policy: "sync",
TXTSuffix: "", Registry: "txt",
TXTCacheInterval: 0, TXTOwnerID: "default",
TXTWildcardReplacement: "", TXTPrefix: "",
MinEventSyncInterval: 5 * time.Second, TXTSuffix: "",
TXTEncryptEnabled: false, TXTCacheInterval: 0,
TXTEncryptAESKey: "", TXTWildcardReplacement: "",
TXTNewFormatOnly: false, MinEventSyncInterval: 5 * time.Second,
Interval: time.Minute, TXTEncryptEnabled: false,
Once: false, TXTEncryptAESKey: "",
DryRun: false, TXTNewFormatOnly: false,
UpdateEvents: false, Interval: time.Minute,
LogFormat: "text", Once: false,
MetricsAddress: ":7979", DryRun: false,
LogLevel: logrus.InfoLevel.String(), UpdateEvents: false,
ExoscaleAPIEnvironment: "api", LogFormat: "text",
ExoscaleAPIZone: "ch-gva-2", MetricsAddress: ":7979",
ExoscaleAPIKey: "", LogLevel: logrus.InfoLevel.String(),
ExoscaleAPISecret: "", ExoscaleAPIEnvironment: "api",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", ExoscaleAPIZone: "ch-gva-2",
CRDSourceKind: "DNSEndpoint", ExoscaleAPIKey: "",
ServiceTypeFilter: []string{}, ExoscaleAPISecret: "",
CFAPIEndpoint: "", CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
CFUsername: "", CRDSourceKind: "DNSEndpoint",
CFPassword: "", ServiceTypeFilter: []string{},
RFC2136Host: []string{""}, CFAPIEndpoint: "",
RFC2136Port: 0, CFUsername: "",
RFC2136Zone: []string{}, CFPassword: "",
RFC2136Insecure: false, RFC2136Host: []string{""},
RFC2136GSSTSIG: false, RFC2136Port: 0,
RFC2136KerberosRealm: "", RFC2136Zone: []string{},
RFC2136KerberosUsername: "", RFC2136Insecure: false,
RFC2136KerberosPassword: "", RFC2136GSSTSIG: false,
RFC2136TSIGKeyName: "", RFC2136KerberosRealm: "",
RFC2136TSIGSecret: "", RFC2136KerberosUsername: "",
RFC2136TSIGSecretAlg: "", RFC2136KerberosPassword: "",
RFC2136TAXFR: true, RFC2136TSIGKeyName: "",
RFC2136MinTTL: 0, RFC2136TSIGSecret: "",
RFC2136BatchChangeSize: 50, RFC2136TSIGSecretAlg: "",
RFC2136UseTLS: false, RFC2136TAXFR: true,
RFC2136LoadBalancingStrategy: "disabled", RFC2136MinTTL: 0,
RFC2136SkipTLSVerify: false, RFC2136BatchChangeSize: 50,
NS1Endpoint: "", RFC2136UseTLS: false,
NS1IgnoreSSL: false, RFC2136LoadBalancingStrategy: "disabled",
TransIPAccountName: "", RFC2136SkipTLSVerify: false,
TransIPPrivateKeyFile: "", NS1Endpoint: "",
DigitalOceanAPIPageSize: 50, NS1IgnoreSSL: false,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, TransIPAccountName: "",
ExcludeDNSRecordTypes: []string{}, TransIPPrivateKeyFile: "",
GoDaddyAPIKey: "", DigitalOceanAPIPageSize: 50,
GoDaddySecretKey: "", ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
GoDaddyTTL: 600, ExcludeDNSRecordTypes: []string{},
GoDaddyOTE: false, GoDaddyAPIKey: "",
IBMCloudProxied: false, GoDaddySecretKey: "",
IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", GoDaddyTTL: 600,
TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", GoDaddyOTE: false,
TencentCloudZoneType: "", IBMCloudProxied: false,
PiholeServer: "", IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json",
PiholePassword: "", TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json",
PiholeTLSInsecureSkipVerify: false, TencentCloudZoneType: "",
PluralCluster: "", PiholeServer: "",
PluralProvider: "", PiholePassword: "",
WebhookProviderURL: "http://localhost:8888", PiholeTLSInsecureSkipVerify: false,
WebhookProviderReadTimeout: 5 * time.Second, PluralCluster: "",
WebhookProviderWriteTimeout: 10 * time.Second, PluralProvider: "",
WebhookServer: false, WebhookProviderURL: "http://localhost:8888",
TraefikDisableLegacy: false, WebhookProviderReadTimeout: 5 * time.Second,
TraefikDisableNew: false, WebhookProviderWriteTimeout: 10 * time.Second,
NAT64Networks: []string{}, WebhookServer: false,
TraefikDisableLegacy: false,
TraefikDisableNew: false,
NAT64Networks: []string{},
} }
// NewConfig returns new Config object // NewConfig returns new Config object
@ -518,6 +524,9 @@ func App(cfg *Config) *kingpin.Application {
app.Flag("tencent-cloud-zone-type", "When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private)").Default(defaultConfig.TencentCloudZoneType).EnumVar(&cfg.TencentCloudZoneType, "", "public", "private") app.Flag("tencent-cloud-zone-type", "When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private)").Default(defaultConfig.TencentCloudZoneType).EnumVar(&cfg.TencentCloudZoneType, "", "public", "private")
app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied) app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
app.Flag("cloudflare-custom-hostnames", "When using the Cloudflare provider, specify if the Custom Hostnames feature will be used. Requires \"Cloudflare for SaaS\" enabled. (default: disabled)").BoolVar(&cfg.CloudflareCustomHostnames)
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 Cerrtificate Authority will be used by default. (default: google, options: google, ssl_com, lets_encrypt)").Default("google").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt")
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-region-key", "When using the Cloudflare provider, specify the region (default: earth)").StringVar(&cfg.CloudflareRegionKey)
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

@ -32,213 +32,219 @@ import (
var ( var (
minimalConfig = &Config{ minimalConfig = &Config{
APIServerURL: "", APIServerURL: "",
KubeConfig: "", KubeConfig: "",
RequestTimeout: time.Second * 30, RequestTimeout: time.Second * 30,
GlooNamespaces: []string{"gloo-system"}, GlooNamespaces: []string{"gloo-system"},
SkipperRouteGroupVersion: "zalando.org/v1", SkipperRouteGroupVersion: "zalando.org/v1",
Sources: []string{"service"}, Sources: []string{"service"},
Namespace: "", Namespace: "",
FQDNTemplate: "", FQDNTemplate: "",
Compatibility: "", Compatibility: "",
Provider: "google", Provider: "google",
GoogleProject: "", GoogleProject: "",
GoogleBatchChangeSize: 1000, GoogleBatchChangeSize: 1000,
GoogleBatchChangeInterval: time.Second, GoogleBatchChangeInterval: time.Second,
GoogleZoneVisibility: "", GoogleZoneVisibility: "",
DomainFilter: []string{""}, DomainFilter: []string{""},
ExcludeDomains: []string{""}, ExcludeDomains: []string{""},
RegexDomainFilter: regexp.MustCompile(""), RegexDomainFilter: regexp.MustCompile(""),
RegexDomainExclusion: regexp.MustCompile(""), RegexDomainExclusion: regexp.MustCompile(""),
ZoneNameFilter: []string{""}, ZoneNameFilter: []string{""},
ZoneIDFilter: []string{""}, ZoneIDFilter: []string{""},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "", AWSZoneType: "",
AWSZoneTagFilter: []string{""}, AWSZoneTagFilter: []string{""},
AWSZoneMatchParent: false, AWSZoneMatchParent: false,
AWSAssumeRole: "", AWSAssumeRole: "",
AWSAssumeRoleExternalID: "", AWSAssumeRoleExternalID: "",
AWSBatchChangeSize: 1000, AWSBatchChangeSize: 1000,
AWSBatchChangeSizeBytes: 32000, AWSBatchChangeSizeBytes: 32000,
AWSBatchChangeSizeValues: 1000, AWSBatchChangeSizeValues: 1000,
AWSBatchChangeInterval: time.Second, AWSBatchChangeInterval: time.Second,
AWSEvaluateTargetHealth: true, AWSEvaluateTargetHealth: true,
AWSAPIRetries: 3, AWSAPIRetries: 3,
AWSPreferCNAME: false, AWSPreferCNAME: false,
AWSProfiles: []string{""}, AWSProfiles: []string{""},
AWSZoneCacheDuration: 0 * time.Second, AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false, AWSSDServiceCleanup: false,
AWSSDCreateTag: map[string]string{}, AWSSDCreateTag: map[string]string{},
AWSDynamoDBTable: "external-dns", AWSDynamoDBTable: "external-dns",
AzureConfigFile: "/etc/kubernetes/azure.json", AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "", AzureResourceGroup: "",
AzureSubscriptionID: "", AzureSubscriptionID: "",
CloudflareProxied: false, CloudflareProxied: false,
CloudflareDNSRecordsPerPage: 100, CloudflareCustomHostnames: false,
CloudflareRegionKey: "", CloudflareCustomHostnamesMinTLSVersion: "1.0",
CoreDNSPrefix: "/skydns/", CloudflareCustomHostnamesCertificateAuthority: "google",
AkamaiServiceConsumerDomain: "", CloudflareDNSRecordsPerPage: 100,
AkamaiClientToken: "", CloudflareRegionKey: "",
AkamaiClientSecret: "", CoreDNSPrefix: "/skydns/",
AkamaiAccessToken: "", AkamaiServiceConsumerDomain: "",
AkamaiEdgercPath: "", AkamaiClientToken: "",
AkamaiEdgercSection: "", AkamaiClientSecret: "",
OCIConfigFile: "/etc/kubernetes/oci.yaml", AkamaiAccessToken: "",
OCIZoneScope: "GLOBAL", AkamaiEdgercPath: "",
OCIZoneCacheDuration: 0 * time.Second, AkamaiEdgercSection: "",
InMemoryZones: []string{""}, OCIConfigFile: "/etc/kubernetes/oci.yaml",
OVHEndpoint: "ovh-eu", OCIZoneScope: "GLOBAL",
OVHApiRateLimit: 20, OCIZoneCacheDuration: 0 * time.Second,
PDNSServer: "http://localhost:8081", InMemoryZones: []string{""},
PDNSServerID: "localhost", OVHEndpoint: "ovh-eu",
PDNSAPIKey: "", OVHApiRateLimit: 20,
Policy: "sync", PDNSServer: "http://localhost:8081",
Registry: "txt", PDNSServerID: "localhost",
TXTOwnerID: "default", PDNSAPIKey: "",
TXTPrefix: "", Policy: "sync",
TXTCacheInterval: 0, Registry: "txt",
TXTNewFormatOnly: false, TXTOwnerID: "default",
Interval: time.Minute, TXTPrefix: "",
MinEventSyncInterval: 5 * time.Second, TXTCacheInterval: 0,
Once: false, TXTNewFormatOnly: false,
DryRun: false, Interval: time.Minute,
UpdateEvents: false, MinEventSyncInterval: 5 * time.Second,
LogFormat: "text", Once: false,
MetricsAddress: ":7979", DryRun: false,
LogLevel: logrus.InfoLevel.String(), UpdateEvents: false,
ConnectorSourceServer: "localhost:8080", LogFormat: "text",
ExoscaleAPIEnvironment: "api", MetricsAddress: ":7979",
ExoscaleAPIZone: "ch-gva-2", LogLevel: logrus.InfoLevel.String(),
ExoscaleAPIKey: "", ConnectorSourceServer: "localhost:8080",
ExoscaleAPISecret: "", ExoscaleAPIEnvironment: "api",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", ExoscaleAPIZone: "ch-gva-2",
CRDSourceKind: "DNSEndpoint", ExoscaleAPIKey: "",
TransIPAccountName: "", ExoscaleAPISecret: "",
TransIPPrivateKeyFile: "", CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
DigitalOceanAPIPageSize: 50, CRDSourceKind: "DNSEndpoint",
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, TransIPAccountName: "",
RFC2136BatchChangeSize: 50, TransIPPrivateKeyFile: "",
RFC2136Host: []string{""}, DigitalOceanAPIPageSize: 50,
RFC2136LoadBalancingStrategy: "disabled", ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
OCPRouterName: "default", RFC2136BatchChangeSize: 50,
IBMCloudProxied: false, RFC2136Host: []string{""},
IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", RFC2136LoadBalancingStrategy: "disabled",
TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", OCPRouterName: "default",
TencentCloudZoneType: "", IBMCloudProxied: false,
WebhookProviderURL: "http://localhost:8888", IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json",
WebhookProviderReadTimeout: 5 * time.Second, TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json",
WebhookProviderWriteTimeout: 10 * time.Second, TencentCloudZoneType: "",
WebhookProviderURL: "http://localhost:8888",
WebhookProviderReadTimeout: 5 * time.Second,
WebhookProviderWriteTimeout: 10 * time.Second,
} }
overriddenConfig = &Config{ overriddenConfig = &Config{
APIServerURL: "http://127.0.0.1:8080", APIServerURL: "http://127.0.0.1:8080",
KubeConfig: "/some/path", KubeConfig: "/some/path",
RequestTimeout: time.Second * 77, RequestTimeout: time.Second * 77,
GlooNamespaces: []string{"gloo-not-system", "gloo-second-system"}, GlooNamespaces: []string{"gloo-not-system", "gloo-second-system"},
SkipperRouteGroupVersion: "zalando.org/v2", SkipperRouteGroupVersion: "zalando.org/v2",
Sources: []string{"service", "ingress", "connector"}, Sources: []string{"service", "ingress", "connector"},
Namespace: "namespace", Namespace: "namespace",
IgnoreHostnameAnnotation: true, IgnoreHostnameAnnotation: true,
IgnoreNonHostNetworkPods: false, IgnoreNonHostNetworkPods: false,
IgnoreIngressTLSSpec: true, IgnoreIngressTLSSpec: true,
IgnoreIngressRulesSpec: true, IgnoreIngressRulesSpec: true,
FQDNTemplate: "{{.Name}}.service.example.com", FQDNTemplate: "{{.Name}}.service.example.com",
Compatibility: "mate", Compatibility: "mate",
Provider: "google", Provider: "google",
GoogleProject: "project", GoogleProject: "project",
GoogleBatchChangeSize: 100, GoogleBatchChangeSize: 100,
GoogleBatchChangeInterval: time.Second * 2, GoogleBatchChangeInterval: time.Second * 2,
GoogleZoneVisibility: "private", GoogleZoneVisibility: "private",
DomainFilter: []string{"example.org", "company.com"}, DomainFilter: []string{"example.org", "company.com"},
ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"}, ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"},
RegexDomainFilter: regexp.MustCompile("(example\\.org|company\\.com)$"), RegexDomainFilter: regexp.MustCompile("(example\\.org|company\\.com)$"),
RegexDomainExclusion: regexp.MustCompile("xapi\\.(example\\.org|company\\.com)$"), RegexDomainExclusion: regexp.MustCompile("xapi\\.(example\\.org|company\\.com)$"),
ZoneNameFilter: []string{"yapi.example.org", "yapi.company.com"}, ZoneNameFilter: []string{"yapi.example.org", "yapi.company.com"},
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
TargetNetFilter: []string{"10.0.0.0/9", "10.1.0.0/9"}, TargetNetFilter: []string{"10.0.0.0/9", "10.1.0.0/9"},
ExcludeTargetNets: []string{"1.0.0.0/9", "1.1.0.0/9"}, ExcludeTargetNets: []string{"1.0.0.0/9", "1.1.0.0/9"},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "private", AWSZoneType: "private",
AWSZoneTagFilter: []string{"tag=foo"}, AWSZoneTagFilter: []string{"tag=foo"},
AWSZoneMatchParent: true, AWSZoneMatchParent: true,
AWSAssumeRole: "some-other-role", AWSAssumeRole: "some-other-role",
AWSAssumeRoleExternalID: "pg2000", AWSAssumeRoleExternalID: "pg2000",
AWSBatchChangeSize: 100, AWSBatchChangeSize: 100,
AWSBatchChangeSizeBytes: 16000, AWSBatchChangeSizeBytes: 16000,
AWSBatchChangeSizeValues: 100, AWSBatchChangeSizeValues: 100,
AWSBatchChangeInterval: time.Second * 2, AWSBatchChangeInterval: time.Second * 2,
AWSEvaluateTargetHealth: false, AWSEvaluateTargetHealth: false,
AWSAPIRetries: 13, AWSAPIRetries: 13,
AWSPreferCNAME: true, AWSPreferCNAME: true,
AWSProfiles: []string{"profile1", "profile2"}, AWSProfiles: []string{"profile1", "profile2"},
AWSZoneCacheDuration: 10 * time.Second, AWSZoneCacheDuration: 10 * time.Second,
AWSSDServiceCleanup: true, AWSSDServiceCleanup: true,
AWSSDCreateTag: map[string]string{"key1": "value1", "key2": "value2"}, AWSSDCreateTag: map[string]string{"key1": "value1", "key2": "value2"},
AWSDynamoDBTable: "custom-table", AWSDynamoDBTable: "custom-table",
AzureConfigFile: "azure.json", AzureConfigFile: "azure.json",
AzureResourceGroup: "arg", AzureResourceGroup: "arg",
AzureSubscriptionID: "arg", AzureSubscriptionID: "arg",
CloudflareProxied: true, CloudflareProxied: true,
CloudflareDNSRecordsPerPage: 5000, CloudflareCustomHostnames: true,
CloudflareRegionKey: "us", CloudflareCustomHostnamesMinTLSVersion: "1.3",
CoreDNSPrefix: "/coredns/", CloudflareCustomHostnamesCertificateAuthority: "google",
AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", CloudflareDNSRecordsPerPage: 5000,
AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46", CloudflareRegionKey: "us",
AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46", CoreDNSPrefix: "/coredns/",
AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46", AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
AkamaiEdgercPath: "/home/test/.edgerc", AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46",
AkamaiEdgercSection: "default", AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46",
OCIConfigFile: "oci.yaml", AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46",
OCIZoneScope: "PRIVATE", AkamaiEdgercPath: "/home/test/.edgerc",
OCIZoneCacheDuration: 30 * time.Second, AkamaiEdgercSection: "default",
InMemoryZones: []string{"example.org", "company.com"}, OCIConfigFile: "oci.yaml",
OVHEndpoint: "ovh-ca", OCIZoneScope: "PRIVATE",
OVHApiRateLimit: 42, OCIZoneCacheDuration: 30 * time.Second,
PDNSServer: "http://ns.example.com:8081", InMemoryZones: []string{"example.org", "company.com"},
PDNSServerID: "localhost", OVHEndpoint: "ovh-ca",
PDNSAPIKey: "some-secret-key", OVHApiRateLimit: 42,
PDNSSkipTLSVerify: true, PDNSServer: "http://ns.example.com:8081",
TLSCA: "/path/to/ca.crt", PDNSServerID: "localhost",
TLSClientCert: "/path/to/cert.pem", PDNSAPIKey: "some-secret-key",
TLSClientCertKey: "/path/to/key.pem", PDNSSkipTLSVerify: true,
PodSourceDomain: "example.org", TLSCA: "/path/to/ca.crt",
Policy: "upsert-only", TLSClientCert: "/path/to/cert.pem",
Registry: "noop", TLSClientCertKey: "/path/to/key.pem",
TXTOwnerID: "owner-1", PodSourceDomain: "example.org",
TXTPrefix: "associated-txt-record", Policy: "upsert-only",
TXTCacheInterval: 12 * time.Hour, Registry: "noop",
TXTNewFormatOnly: true, TXTOwnerID: "owner-1",
Interval: 10 * time.Minute, TXTPrefix: "associated-txt-record",
MinEventSyncInterval: 50 * time.Second, TXTCacheInterval: 12 * time.Hour,
Once: true, TXTNewFormatOnly: true,
DryRun: true, Interval: 10 * time.Minute,
UpdateEvents: true, MinEventSyncInterval: 50 * time.Second,
LogFormat: "json", Once: true,
MetricsAddress: "127.0.0.1:9099", DryRun: true,
LogLevel: logrus.DebugLevel.String(), UpdateEvents: true,
ConnectorSourceServer: "localhost:8081", LogFormat: "json",
ExoscaleAPIEnvironment: "api1", MetricsAddress: "127.0.0.1:9099",
ExoscaleAPIZone: "zone1", LogLevel: logrus.DebugLevel.String(),
ExoscaleAPIKey: "1", ConnectorSourceServer: "localhost:8081",
ExoscaleAPISecret: "2", ExoscaleAPIEnvironment: "api1",
CRDSourceAPIVersion: "test.k8s.io/v1alpha1", ExoscaleAPIZone: "zone1",
CRDSourceKind: "Endpoint", ExoscaleAPIKey: "1",
NS1Endpoint: "https://api.example.com/v1", ExoscaleAPISecret: "2",
NS1IgnoreSSL: true, CRDSourceAPIVersion: "test.k8s.io/v1alpha1",
TransIPAccountName: "transip", CRDSourceKind: "Endpoint",
TransIPPrivateKeyFile: "/path/to/transip.key", NS1Endpoint: "https://api.example.com/v1",
DigitalOceanAPIPageSize: 100, NS1IgnoreSSL: true,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, TransIPAccountName: "transip",
RFC2136BatchChangeSize: 100, TransIPPrivateKeyFile: "/path/to/transip.key",
RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"}, DigitalOceanAPIPageSize: 100,
RFC2136LoadBalancingStrategy: "round-robin", ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS},
IBMCloudProxied: true, RFC2136BatchChangeSize: 100,
IBMCloudConfigFile: "ibmcloud.json", RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"},
TencentCloudConfigFile: "tencent-cloud.json", RFC2136LoadBalancingStrategy: "round-robin",
TencentCloudZoneType: "private", IBMCloudProxied: true,
WebhookProviderURL: "http://localhost:8888", IBMCloudConfigFile: "ibmcloud.json",
WebhookProviderReadTimeout: 5 * time.Second, TencentCloudConfigFile: "tencent-cloud.json",
WebhookProviderWriteTimeout: 10 * time.Second, TencentCloudZoneType: "private",
WebhookProviderURL: "http://localhost:8888",
WebhookProviderReadTimeout: 5 * time.Second,
WebhookProviderWriteTimeout: 10 * time.Second,
} }
) )
@ -287,6 +293,9 @@ func TestParseFlags(t *testing.T) {
"--azure-resource-group=arg", "--azure-resource-group=arg",
"--azure-subscription-id=arg", "--azure-subscription-id=arg",
"--cloudflare-proxied", "--cloudflare-proxied",
"--cloudflare-custom-hostnames",
"--cloudflare-custom-hostnames-min-tls-version=1.3",
"--cloudflare-custom-hostnames-certificate-authority=google",
"--cloudflare-dns-records-per-page=5000", "--cloudflare-dns-records-per-page=5000",
"--cloudflare-region-key=us", "--cloudflare-region-key=us",
"--coredns-prefix=/coredns/", "--coredns-prefix=/coredns/",
@ -390,112 +399,115 @@ func TestParseFlags(t *testing.T) {
title: "override everything via environment variables", title: "override everything via environment variables",
args: []string{}, args: []string{},
envVars: map[string]string{ envVars: map[string]string{
"EXTERNAL_DNS_SERVER": "http://127.0.0.1:8080", "EXTERNAL_DNS_SERVER": "http://127.0.0.1:8080",
"EXTERNAL_DNS_KUBECONFIG": "/some/path", "EXTERNAL_DNS_KUBECONFIG": "/some/path",
"EXTERNAL_DNS_REQUEST_TIMEOUT": "77s", "EXTERNAL_DNS_REQUEST_TIMEOUT": "77s",
"EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other", "EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other",
"EXTERNAL_DNS_GLOO_NAMESPACE": "gloo-not-system\ngloo-second-system", "EXTERNAL_DNS_GLOO_NAMESPACE": "gloo-not-system\ngloo-second-system",
"EXTERNAL_DNS_SKIPPER_ROUTEGROUP_GROUPVERSION": "zalando.org/v2", "EXTERNAL_DNS_SKIPPER_ROUTEGROUP_GROUPVERSION": "zalando.org/v2",
"EXTERNAL_DNS_SOURCE": "service\ningress\nconnector", "EXTERNAL_DNS_SOURCE": "service\ningress\nconnector",
"EXTERNAL_DNS_NAMESPACE": "namespace", "EXTERNAL_DNS_NAMESPACE": "namespace",
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com", "EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
"EXTERNAL_DNS_IGNORE_NON_HOST_NETWORK_PODS": "0", "EXTERNAL_DNS_IGNORE_NON_HOST_NETWORK_PODS": "0",
"EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1", "EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1",
"EXTERNAL_DNS_IGNORE_INGRESS_TLS_SPEC": "1", "EXTERNAL_DNS_IGNORE_INGRESS_TLS_SPEC": "1",
"EXTERNAL_DNS_IGNORE_INGRESS_RULES_SPEC": "1", "EXTERNAL_DNS_IGNORE_INGRESS_RULES_SPEC": "1",
"EXTERNAL_DNS_COMPATIBILITY": "mate", "EXTERNAL_DNS_COMPATIBILITY": "mate",
"EXTERNAL_DNS_PROVIDER": "google", "EXTERNAL_DNS_PROVIDER": "google",
"EXTERNAL_DNS_GOOGLE_PROJECT": "project", "EXTERNAL_DNS_GOOGLE_PROJECT": "project",
"EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_SIZE": "100", "EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_INTERVAL": "2s", "EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_INTERVAL": "2s",
"EXTERNAL_DNS_GOOGLE_ZONE_VISIBILITY": "private", "EXTERNAL_DNS_GOOGLE_ZONE_VISIBILITY": "private",
"EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json", "EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json",
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg", "EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg", "EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1", "EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000", "EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES": "1",
"EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us", "EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_MIN_TLS_VERSION": "1.3",
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/", "EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_CERTIFICATE_AUTHORITY": "google",
"EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", "EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000",
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46", "EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us",
"EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46", "EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
"EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46", "EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
"EXTERNAL_DNS_AKAMAI_EDGERC_PATH": "/home/test/.edgerc", "EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_EDGERC_SECTION": "default", "EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml", "EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_OCI_ZONE_SCOPE": "PRIVATE", "EXTERNAL_DNS_AKAMAI_EDGERC_PATH": "/home/test/.edgerc",
"EXTERNAL_DNS_OCI_ZONES_CACHE_DURATION": "30s", "EXTERNAL_DNS_AKAMAI_EDGERC_SECTION": "default",
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com", "EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml",
"EXTERNAL_DNS_OVH_ENDPOINT": "ovh-ca", "EXTERNAL_DNS_OCI_ZONE_SCOPE": "PRIVATE",
"EXTERNAL_DNS_OVH_API_RATE_LIMIT": "42", "EXTERNAL_DNS_OCI_ZONES_CACHE_DURATION": "30s",
"EXTERNAL_DNS_POD_SOURCE_DOMAIN": "example.org", "EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
"EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com", "EXTERNAL_DNS_OVH_ENDPOINT": "ovh-ca",
"EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com", "EXTERNAL_DNS_OVH_API_RATE_LIMIT": "42",
"EXTERNAL_DNS_REGEX_DOMAIN_FILTER": "(example\\.org|company\\.com)$", "EXTERNAL_DNS_POD_SOURCE_DOMAIN": "example.org",
"EXTERNAL_DNS_REGEX_DOMAIN_EXCLUSION": "xapi\\.(example\\.org|company\\.com)$", "EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com",
"EXTERNAL_DNS_TARGET_NET_FILTER": "10.0.0.0/9\n10.1.0.0/9", "EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com",
"EXTERNAL_DNS_EXCLUDE_TARGET_NET": "1.0.0.0/9\n1.1.0.0/9", "EXTERNAL_DNS_REGEX_DOMAIN_FILTER": "(example\\.org|company\\.com)$",
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081", "EXTERNAL_DNS_REGEX_DOMAIN_EXCLUSION": "xapi\\.(example\\.org|company\\.com)$",
"EXTERNAL_DNS_PDNS_ID": "localhost", "EXTERNAL_DNS_TARGET_NET_FILTER": "10.0.0.0/9\n10.1.0.0/9",
"EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key", "EXTERNAL_DNS_EXCLUDE_TARGET_NET": "1.0.0.0/9\n1.1.0.0/9",
"EXTERNAL_DNS_PDNS_SKIP_TLS_VERIFY": "1", "EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
"EXTERNAL_DNS_RDNS_ROOT_DOMAIN": "lb.rancher.cloud", "EXTERNAL_DNS_PDNS_ID": "localhost",
"EXTERNAL_DNS_TLS_CA": "/path/to/ca.crt", "EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key",
"EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem", "EXTERNAL_DNS_PDNS_SKIP_TLS_VERIFY": "1",
"EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem", "EXTERNAL_DNS_RDNS_ROOT_DOMAIN": "lb.rancher.cloud",
"EXTERNAL_DNS_ZONE_NAME_FILTER": "yapi.example.org\nyapi.company.com", "EXTERNAL_DNS_TLS_CA": "/path/to/ca.crt",
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2", "EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private", "EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem",
"EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo", "EXTERNAL_DNS_ZONE_NAME_FILTER": "yapi.example.org\nyapi.company.com",
"EXTERNAL_DNS_AWS_ZONE_MATCH_PARENT": "true", "EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role", "EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_AWS_ASSUME_ROLE_EXTERNAL_ID": "pg2000", "EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100", "EXTERNAL_DNS_AWS_ZONE_MATCH_PARENT": "true",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE_BYTES": "16000", "EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE_VALUES": "100", "EXTERNAL_DNS_AWS_ASSUME_ROLE_EXTERNAL_ID": "pg2000",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s", "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0", "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE_BYTES": "16000",
"EXTERNAL_DNS_AWS_API_RETRIES": "13", "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE_VALUES": "100",
"EXTERNAL_DNS_AWS_PREFER_CNAME": "true", "EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s",
"EXTERNAL_DNS_AWS_PROFILE": "profile1\nprofile2", "EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0",
"EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s", "EXTERNAL_DNS_AWS_API_RETRIES": "13",
"EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true", "EXTERNAL_DNS_AWS_PREFER_CNAME": "true",
"EXTERNAL_DNS_AWS_SD_CREATE_TAG": "key1=value1\nkey2=value2", "EXTERNAL_DNS_AWS_PROFILE": "profile1\nprofile2",
"EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table", "EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s",
"EXTERNAL_DNS_POLICY": "upsert-only", "EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true",
"EXTERNAL_DNS_REGISTRY": "noop", "EXTERNAL_DNS_AWS_SD_CREATE_TAG": "key1=value1\nkey2=value2",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1", "EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table",
"EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record", "EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h", "EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_NEW_FORMAT_ONLY": "1", "EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
"EXTERNAL_DNS_INTERVAL": "10m", "EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record",
"EXTERNAL_DNS_MIN_EVENT_SYNC_INTERVAL": "50s", "EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h",
"EXTERNAL_DNS_ONCE": "1", "EXTERNAL_DNS_TXT_NEW_FORMAT_ONLY": "1",
"EXTERNAL_DNS_DRY_RUN": "1", "EXTERNAL_DNS_INTERVAL": "10m",
"EXTERNAL_DNS_EVENTS": "1", "EXTERNAL_DNS_MIN_EVENT_SYNC_INTERVAL": "50s",
"EXTERNAL_DNS_LOG_FORMAT": "json", "EXTERNAL_DNS_ONCE": "1",
"EXTERNAL_DNS_METRICS_ADDRESS": "127.0.0.1:9099", "EXTERNAL_DNS_DRY_RUN": "1",
"EXTERNAL_DNS_LOG_LEVEL": "debug", "EXTERNAL_DNS_EVENTS": "1",
"EXTERNAL_DNS_CONNECTOR_SOURCE_SERVER": "localhost:8081", "EXTERNAL_DNS_LOG_FORMAT": "json",
"EXTERNAL_DNS_EXOSCALE_APIENV": "api1", "EXTERNAL_DNS_METRICS_ADDRESS": "127.0.0.1:9099",
"EXTERNAL_DNS_EXOSCALE_APIZONE": "zone1", "EXTERNAL_DNS_LOG_LEVEL": "debug",
"EXTERNAL_DNS_EXOSCALE_APIKEY": "1", "EXTERNAL_DNS_CONNECTOR_SOURCE_SERVER": "localhost:8081",
"EXTERNAL_DNS_EXOSCALE_APISECRET": "2", "EXTERNAL_DNS_EXOSCALE_APIENV": "api1",
"EXTERNAL_DNS_CRD_SOURCE_APIVERSION": "test.k8s.io/v1alpha1", "EXTERNAL_DNS_EXOSCALE_APIZONE": "zone1",
"EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint", "EXTERNAL_DNS_EXOSCALE_APIKEY": "1",
"EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1", "EXTERNAL_DNS_EXOSCALE_APISECRET": "2",
"EXTERNAL_DNS_NS1_IGNORESSL": "1", "EXTERNAL_DNS_CRD_SOURCE_APIVERSION": "test.k8s.io/v1alpha1",
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip", "EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint",
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key", "EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1",
"EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100", "EXTERNAL_DNS_NS1_IGNORESSL": "1",
"EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nAAAA\nCNAME\nNS", "EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
"EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100", "EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
"EXTERNAL_DNS_RFC2136_LOAD_BALANCING_STRATEGY": "round-robin", "EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100",
"EXTERNAL_DNS_RFC2136_HOST": "rfc2136-host1\nrfc2136-host2", "EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nAAAA\nCNAME\nNS",
"EXTERNAL_DNS_IBMCLOUD_PROXIED": "1", "EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_IBMCLOUD_CONFIG_FILE": "ibmcloud.json", "EXTERNAL_DNS_RFC2136_LOAD_BALANCING_STRATEGY": "round-robin",
"EXTERNAL_DNS_TENCENT_CLOUD_CONFIG_FILE": "tencent-cloud.json", "EXTERNAL_DNS_RFC2136_HOST": "rfc2136-host1\nrfc2136-host2",
"EXTERNAL_DNS_TENCENT_CLOUD_ZONE_TYPE": "private", "EXTERNAL_DNS_IBMCLOUD_PROXIED": "1",
"EXTERNAL_DNS_IBMCLOUD_CONFIG_FILE": "ibmcloud.json",
"EXTERNAL_DNS_TENCENT_CLOUD_CONFIG_FILE": "tencent-cloud.json",
"EXTERNAL_DNS_TENCENT_CLOUD_ZONE_TYPE": "private",
}, },
expected: overriddenConfig, expected: overriddenConfig,
}, },

View File

@ -64,6 +64,12 @@ var recordTypeProxyNotSupported = map[string]bool{
"SRV": true, "SRV": true,
} }
type CustomHostnamesConfig struct {
Enabled bool
MinTLSVersion string
CertificateAuthority string
}
var recordTypeCustomHostnameSupported = map[string]bool{ var recordTypeCustomHostnameSupported = map[string]bool{
"A": true, "A": true,
"CNAME": true, "CNAME": true,
@ -149,20 +155,22 @@ type CloudFlareProvider struct {
provider.BaseProvider provider.BaseProvider
Client cloudFlareDNS Client cloudFlareDNS
// only consider hosted zones managing domains ending in this suffix // only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
proxiedByDefault bool proxiedByDefault bool
DryRun bool CustomHostnamesConfig CustomHostnamesConfig
DNSRecordsPerPage int DryRun bool
RegionKey string DNSRecordsPerPage int
RegionKey string
} }
// cloudFlareChange differentiates between ChangActions // cloudFlareChange differentiates between ChangActions
type cloudFlareChange struct { type cloudFlareChange struct {
Action string Action string
ResourceRecord cloudflare.DNSRecord ResourceRecord cloudflare.DNSRecord
RegionalHostname cloudflare.RegionalHostname RegionalHostname cloudflare.RegionalHostname
CustomHostname cloudflare.CustomHostname CustomHostname cloudflare.CustomHostname
CustomHostnamePrev string
} }
// RecordParamsTypes is a typeset of the possible Record Params that can be passed to cloudflare-go library // RecordParamsTypes is a typeset of the possible Record Params that can be passed to cloudflare-go library
@ -201,7 +209,7 @@ func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordPar
} }
// 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, dnsRecordsPerPage int, regionKey string) (*CloudFlareProvider, error) { func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, proxiedByDefault bool, dryRun bool, dnsRecordsPerPage int, regionKey string, customHostnamesConfig CustomHostnamesConfig) (*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
@ -225,13 +233,14 @@ func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter prov
} }
provider := &CloudFlareProvider{ provider := &CloudFlareProvider{
// Client: config, // Client: config,
Client: zoneService{config}, Client: zoneService{config},
domainFilter: domainFilter, domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter, zoneIDFilter: zoneIDFilter,
proxiedByDefault: proxiedByDefault, proxiedByDefault: proxiedByDefault,
DryRun: dryRun, CustomHostnamesConfig: customHostnamesConfig,
DNSRecordsPerPage: dnsRecordsPerPage, DryRun: dryRun,
RegionKey: regionKey, DNSRecordsPerPage: dnsRecordsPerPage,
RegionKey: regionKey,
} }
return provider, nil return provider, nil
} }
@ -319,7 +328,7 @@ func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Cha
for _, endpoint := range changes.Create { for _, endpoint := range changes.Create {
for _, target := range endpoint.Targets { for _, target := range endpoint.Targets {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, endpoint, target)) cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, endpoint, target, nil))
} }
} }
@ -329,21 +338,21 @@ func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Cha
add, remove, leave := provider.Difference(current.Targets, desired.Targets) add, remove, leave := provider.Difference(current.Targets, desired.Targets)
for _, a := range remove { for _, a := range remove {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, current, a)) cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, current, a, current))
} }
for _, a := range add { for _, a := range add {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, desired, a)) cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, desired, a, current))
} }
for _, a := range leave { for _, a := range leave {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareUpdate, desired, a)) cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareUpdate, desired, a, current))
} }
} }
for _, endpoint := range changes.Delete { for _, endpoint := range changes.Delete {
for _, target := range endpoint.Targets { for _, target := range endpoint.Targets {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, endpoint, target)) cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, endpoint, target, nil))
} }
} }
@ -367,16 +376,6 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
var failedZones []string var failedZones []string
for zoneID, changes := range changesByZone { for zoneID, changes := range changesByZone {
records, err := p.listDNSRecordsWithAutoPagination(ctx, zoneID)
if err != nil {
return fmt.Errorf("could not fetch records from zone, %v", err)
}
chs, chErr := p.listCustomHostnamesWithPagination(ctx, zoneID)
if chErr != nil {
return fmt.Errorf("could not fetch custom hostnames from zone, %v", chErr)
}
var failedChange bool var failedChange bool
for _, change := range changes { for _, change := range changes {
logFields := log.Fields{ logFields := log.Fields{
@ -394,34 +393,37 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
} }
resourceContainer := cloudflare.ZoneIdentifier(zoneID) resourceContainer := cloudflare.ZoneIdentifier(zoneID)
records, err := p.listDNSRecordsWithAutoPagination(ctx, zoneID)
if err != nil {
return fmt.Errorf("could not fetch records from zone, %v", err)
}
chs, chErr := p.listCustomHostnamesWithPagination(ctx, zoneID)
if chErr != nil {
return fmt.Errorf("could not fetch custom hostnames from zone, %v", chErr)
}
if change.Action == cloudFlareUpdate { if change.Action == cloudFlareUpdate {
if recordTypeCustomHostnameSupported[change.ResourceRecord.Type] { if recordTypeCustomHostnameSupported[change.ResourceRecord.Type] {
chID, oldCh := p.getCustomHostnameIDbyOrigin(chs, change.ResourceRecord.Name) prevCh := change.CustomHostnamePrev
if chID == "" && change.CustomHostname.Hostname != "" { newCh := change.CustomHostname.Hostname
log.WithFields(logFields).Infof("Adding custom hostname %v", change.CustomHostname.Hostname) if prevCh != "" {
_, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname) prevChID, _ := p.getCustomHostnameOrigin(chs, prevCh)
if chErr != nil { if prevChID != "" && prevCh != newCh {
failedChange = true log.WithFields(logFields).Infof("Removing previous custom hostname %v/%v", prevChID, prevCh)
log.WithFields(logFields).Errorf("failed to add custom hostname %v: %v", change.CustomHostname.Hostname, chErr) chErr := p.Client.DeleteCustomHostname(ctx, zoneID, prevChID)
if chErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to remove previous custom hostname %v/%v: %v", prevChID, prevCh, chErr)
}
} }
} else if chID != "" && oldCh != "" && change.CustomHostname.Hostname == "" { }
log.WithFields(logFields).Infof("Removing custom hostname %v", change.CustomHostname.Hostname) if newCh != "" {
chErr := p.Client.DeleteCustomHostname(ctx, zoneID, chID) if prevCh != newCh {
if chErr != nil { log.WithFields(logFields).Infof("Adding custom hostname %v", newCh)
failedChange = true _, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname)
log.WithFields(logFields).Errorf("failed to remove custom hostname %v: %v", change.CustomHostname.Hostname, chErr) if chErr != nil {
} failedChange = true
} else if chID != "" && change.CustomHostname.Hostname != "" && oldCh != change.CustomHostname.Hostname { log.WithFields(logFields).Errorf("failed to add custom hostname %v: %v", newCh, chErr)
log.WithFields(logFields).Infof("Replacing custom hostname: %v/%v to %v", chID, oldCh, change.CustomHostname.Hostname) }
chDelErr := p.Client.DeleteCustomHostname(ctx, zoneID, chID)
if chDelErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to remove replacing custom hostname %v/%v: %v", chID, oldCh, chDelErr)
}
_, chAddErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname)
if chAddErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to add replacing custom hostname %v: %v", change.CustomHostname.Hostname, chAddErr)
} }
} }
} }
@ -455,14 +457,19 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
failedChange = true failedChange = true
log.WithFields(logFields).Errorf("failed to delete record: %v", err) log.WithFields(logFields).Errorf("failed to delete record: %v", err)
} }
chID, oldCh := p.getCustomHostnameIDbyOrigin(chs, change.ResourceRecord.Name) if change.CustomHostname.Hostname == "" {
continue
}
log.WithFields(logFields).Infof("Deleting custom hostname %v", change.CustomHostname.Hostname)
chID, _ := p.getCustomHostnameOrigin(chs, change.CustomHostname.Hostname)
if chID == "" { if chID == "" {
log.WithFields(logFields).Infof("Custom hostname %v not found", change.CustomHostname.Hostname)
continue continue
} }
chErr := p.Client.DeleteCustomHostname(ctx, zoneID, chID) chErr := p.Client.DeleteCustomHostname(ctx, zoneID, chID)
if chErr != nil { if chErr != nil {
failedChange = true failedChange = true
log.WithFields(logFields).Errorf("failed to delete custom hostname %v/%v: %v", chID, oldCh, chErr) log.WithFields(logFields).Errorf("failed to delete custom hostname %v/%v: %v", chID, change.CustomHostname.Hostname, chErr)
} }
} else if change.Action == cloudFlareCreate { } else if change.Action == cloudFlareCreate {
recordParam := getCreateDNSRecordParam(*change) recordParam := getCreateDNSRecordParam(*change)
@ -471,7 +478,16 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
failedChange = true failedChange = true
log.WithFields(logFields).Errorf("failed to create record: %v", err) log.WithFields(logFields).Errorf("failed to create record: %v", err)
} }
if change.CustomHostname.Hostname == "" {
continue
}
log.WithFields(logFields).Infof("Creating custom hostname %v", change.CustomHostname.Hostname) log.WithFields(logFields).Infof("Creating custom hostname %v", change.CustomHostname.Hostname)
chID, chOrigin := p.getCustomHostnameOrigin(chs, change.CustomHostname.Hostname)
if chID != "" {
failedChange = true
log.WithFields(logFields).Errorf("failed to create custom hostname, %v already exists for origin %v", change.CustomHostname.Hostname, chOrigin)
continue
}
_, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname) _, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname)
if chErr != nil { if chErr != nil {
failedChange = true failedChange = true
@ -537,16 +553,16 @@ func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record
return "" return ""
} }
func (p *CloudFlareProvider) getCustomHostnameIDbyOrigin(chs []cloudflare.CustomHostname, origin string) (string, string) { func (p *CloudFlareProvider) getCustomHostnameOrigin(chs []cloudflare.CustomHostname, hostname string) (string, string) {
for _, zoneCh := range chs { for _, zoneCh := range chs {
if zoneCh.CustomOriginServer == origin { if zoneCh.Hostname == hostname {
return zoneCh.ID, zoneCh.Hostname return zoneCh.ID, zoneCh.CustomOriginServer
} }
} }
return "", "" return "", ""
} }
func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoint.Endpoint, target string) *cloudFlareChange { func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoint.Endpoint, target string, current *endpoint.Endpoint) *cloudFlareChange {
ttl := defaultCloudFlareRecordTTL ttl := defaultCloudFlareRecordTTL
proxied := shouldBeProxied(endpoint, p.proxiedByDefault) proxied := shouldBeProxied(endpoint, p.proxiedByDefault)
@ -554,6 +570,19 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi
ttl = int(endpoint.RecordTTL) ttl = int(endpoint.RecordTTL)
} }
dt := time.Now() dt := time.Now()
customHostnamePrev := ""
newCustomHostname := cloudflare.CustomHostname{}
if p.CustomHostnamesConfig.Enabled {
if current != nil {
customHostnamePrev = getEndpointCustomHostname(current)
}
newCustomHostname = cloudflare.CustomHostname{
Hostname: getEndpointCustomHostname(endpoint),
CustomOriginServer: endpoint.DNSName,
SSL: getCustomHostnamesSSLOptions(endpoint, p.CustomHostnamesConfig),
}
}
return &cloudFlareChange{ return &cloudFlareChange{
Action: action, Action: action,
ResourceRecord: cloudflare.DNSRecord{ ResourceRecord: cloudflare.DNSRecord{
@ -573,19 +602,8 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi
RegionKey: p.RegionKey, RegionKey: p.RegionKey,
CreatedOn: &dt, CreatedOn: &dt,
}, },
CustomHostname: cloudflare.CustomHostname{ CustomHostnamePrev: customHostnamePrev,
Hostname: getEndpointCustomHostname(endpoint), CustomHostname: newCustomHostname,
CustomOriginServer: endpoint.DNSName,
SSL: &cloudflare.CustomHostnameSSL{
Type: "dv",
Method: "http",
CertificateAuthority: "google",
BundleMethod: "ubiquitous",
Settings: cloudflare.CustomHostnameSSLSettings{
MinTLSVersion: "1.0",
},
},
},
} }
} }
@ -618,6 +636,9 @@ func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Contex
// listCustomHostnamesWithPagination performs automatic pagination of results on requests to cloudflare.CustomHostnames // listCustomHostnamesWithPagination performs automatic pagination of results on requests to cloudflare.CustomHostnames
func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Context, zoneID string) ([]cloudflare.CustomHostname, error) { func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Context, zoneID string) ([]cloudflare.CustomHostname, error) {
if !p.CustomHostnamesConfig.Enabled {
return nil, nil
}
var chs []cloudflare.CustomHostname var chs []cloudflare.CustomHostname
resultInfo := cloudflare.ResultInfo{Page: 1} resultInfo := cloudflare.ResultInfo{Page: 1}
for { for {
@ -643,6 +664,18 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte
return chs, nil return chs, nil
} }
func getCustomHostnamesSSLOptions(endpoint *endpoint.Endpoint, customHostnamesConfig CustomHostnamesConfig) *cloudflare.CustomHostnameSSL {
return &cloudflare.CustomHostnameSSL{
Type: "dv",
Method: "http",
CertificateAuthority: customHostnamesConfig.CertificateAuthority,
BundleMethod: "ubiquitous",
Settings: cloudflare.CustomHostnameSSLSettings{
MinTLSVersion: customHostnamesConfig.MinTLSVersion,
},
}
}
func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool { func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool {
proxied := proxiedByDefault proxied := proxiedByDefault
@ -695,7 +728,7 @@ func groupByNameAndTypeWithCustomHostnames(records []cloudflare.DNSRecord, chs [
// map custom origin to custom hostname, custom origin should match to a dns record // map custom origin to custom hostname, custom origin should match to a dns record
customOriginServers := map[string]string{} customOriginServers := map[string]string{}
// only one latest custom hostname for a dns record would work // only one latest custom hostname for a dns record would work; noop (chs is empty) if custom hostnames feature is not in use
for _, c := range chs { for _, c := range chs {
customOriginServers[c.CustomOriginServer] = c.Hostname customOriginServers[c.CustomOriginServer] = c.Hostname
} }
@ -721,9 +754,10 @@ func groupByNameAndTypeWithCustomHostnames(records []cloudflare.DNSRecord, chs [
if ep == nil { if ep == nil {
continue continue
} }
ep.WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(proxied)) ep = ep.WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(proxied))
// noop (customOriginServers is empty) if custom hostnames feature is not in use
if customHostname, ok := customOriginServers[records[0].Name]; ok { if customHostname, ok := customOriginServers[records[0].Name]; ok {
ep.WithProviderSpecific(source.CloudflareCustomHostnameKey, customHostname) ep = ep.WithProviderSpecific(source.CloudflareCustomHostnameKey, customHostname)
} }
endpoints = append(endpoints, ep) endpoints = append(endpoints, ep)

View File

@ -26,10 +26,12 @@ import (
"testing" "testing"
cloudflare "github.com/cloudflare/cloudflare-go" cloudflare "github.com/cloudflare/cloudflare-go"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/maxatome/go-testdeep/td" "github.com/maxatome/go-testdeep/td"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/provider"
) )
@ -112,6 +114,7 @@ func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
switch params := rp.(type) { switch params := rp.(type) {
case cloudflare.CreateDNSRecordParams: case cloudflare.CreateDNSRecordParams:
return cloudflare.DNSRecord{ return cloudflare.DNSRecord{
ID: params.ID,
Name: params.Name, Name: params.Name,
TTL: params.TTL, TTL: params.TTL,
Proxied: params.Proxied, Proxied: params.Proxied,
@ -120,6 +123,7 @@ func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
} }
case cloudflare.UpdateDNSRecordParams: case cloudflare.UpdateDNSRecordParams:
return cloudflare.DNSRecord{ return cloudflare.DNSRecord{
ID: params.ID,
Name: params.Name, Name: params.Name,
TTL: params.TTL, TTL: params.TTL,
Proxied: params.Proxied, Proxied: params.Proxied,
@ -131,16 +135,23 @@ func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
} }
} }
func generateDNSRecordID(rrtype string, name string, content string) string {
return fmt.Sprintf("%s-%s-%s", name, rrtype, content)
}
func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) { func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) {
recordData := getDNSRecordFromRecordParams(rp) recordData := getDNSRecordFromRecordParams(rp)
if recordData.ID == "" {
recordData.ID = generateDNSRecordID(recordData.Type, recordData.Name, recordData.Content)
}
m.Actions = append(m.Actions, MockAction{ m.Actions = append(m.Actions, MockAction{
Name: "Create", Name: "Create",
ZoneId: rc.Identifier, ZoneId: rc.Identifier,
RecordId: rp.ID, RecordId: recordData.ID,
RecordData: recordData, RecordData: recordData,
}) })
if zone, ok := m.Records[rc.Identifier]; ok { if zone, ok := m.Records[rc.Identifier]; ok {
zone[rp.ID] = recordData zone[recordData.ID] = recordData
} }
if recordData.Name == "newerror.bar.com" { if recordData.Name == "newerror.bar.com" {
@ -156,6 +167,10 @@ func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, rc *cloudflar
result := []cloudflare.DNSRecord{} result := []cloudflare.DNSRecord{}
if zone, ok := m.Records[rc.Identifier]; ok { if zone, ok := m.Records[rc.Identifier]; ok {
for _, record := range zone { for _, record := range zone {
if strings.HasPrefix(record.Name, "newerror-list-") {
m.DeleteDNSRecord(ctx, rc, record.ID)
return nil, &cloudflare.ResultInfo{}, errors.New("failed to list erroring DNS record")
}
result = append(result, record) result = append(result, record)
} }
} }
@ -200,6 +215,9 @@ func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, rc *cloudfla
}) })
if zone, ok := m.Records[rc.Identifier]; ok { if zone, ok := m.Records[rc.Identifier]; ok {
if _, ok := zone[rp.ID]; ok { if _, ok := zone[rp.ID]; ok {
if strings.HasPrefix(recordData.Name, "newerror-update-") {
return errors.New("failed to update erroring DNS record")
}
zone[rp.ID] = recordData zone[rp.ID] = recordData
} }
} }
@ -226,7 +244,11 @@ func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, rc *cloudfla
}) })
if zone, ok := m.Records[rc.Identifier]; ok { if zone, ok := m.Records[rc.Identifier]; ok {
if _, ok := zone[recordID]; ok { if _, ok := zone[recordID]; ok {
name := zone[recordID].Name
delete(zone, recordID) delete(zone, recordID)
if strings.HasPrefix(name, "newerror-delete-") {
return errors.New("failed to delete erroring DNS record")
}
return nil return nil
} }
} }
@ -240,6 +262,10 @@ func (m *mockCloudFlareClient) UserDetails(ctx context.Context) (cloudflare.User
func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) { func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) {
var err error = nil var err error = nil
if strings.HasPrefix(zoneID, "newerror-") {
return nil, cloudflare.ResultInfo{}, errors.New("failed to list custom hostnames")
}
if page != 1 || filter.Hostname != "" { if page != 1 || filter.Hostname != "" {
err = errors.New("pages and filters are not supported for custom hostnames mock test") err = errors.New("pages and filters are not supported for custom hostnames mock test")
} }
@ -247,6 +273,10 @@ func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID strin
result := []cloudflare.CustomHostname{} result := []cloudflare.CustomHostname{}
if zone, ok := m.customHostnames[zoneID]; ok { if zone, ok := m.customHostnames[zoneID]; ok {
for _, ch := range zone { for _, ch := range zone {
if strings.HasPrefix(ch.Hostname, "newerror-list-") {
m.DeleteCustomHostname(ctx, zoneID, ch.ID)
return nil, cloudflare.ResultInfo{}, errors.New("failed to list erroring custom hostname")
}
result = append(result, ch) result = append(result, ch)
} }
} }
@ -262,10 +292,15 @@ func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID strin
} }
func (m *mockCloudFlareClient) CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error) { func (m *mockCloudFlareClient) CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error) {
if ch.Hostname == "" || ch.CustomOriginServer == "" || ch.Hostname == "newerror-create.foo.fancybar.com" {
return nil, fmt.Errorf("Invalid custom hostname or origin hostname")
}
if _, ok := m.customHostnames[zoneID]; !ok { if _, ok := m.customHostnames[zoneID]; !ok {
m.customHostnames[zoneID] = map[string]cloudflare.CustomHostname{} m.customHostnames[zoneID] = map[string]cloudflare.CustomHostname{}
} }
m.customHostnames[zoneID][ch.ID] = ch var newCustomHostname cloudflare.CustomHostname = ch
newCustomHostname.ID = fmt.Sprintf("ID-%s", ch.Hostname)
m.customHostnames[zoneID][newCustomHostname.ID] = newCustomHostname
return &cloudflare.CustomHostnameResponse{}, nil return &cloudflare.CustomHostnameResponse{}, nil
} }
@ -273,9 +308,11 @@ func (m *mockCloudFlareClient) DeleteCustomHostname(ctx context.Context, zoneID
if zone, ok := m.customHostnames[zoneID]; ok { if zone, ok := m.customHostnames[zoneID]; ok {
if _, ok := zone[customHostnameID]; ok { if _, ok := zone[customHostnameID]; ok {
delete(zone, customHostnameID) delete(zone, customHostnameID)
return nil
} }
} }
if customHostnameID == "ID-newerror-delete.foo.fancybar.com" {
return fmt.Errorf("Invalid custom hostname to delete")
}
return nil return nil
} }
@ -342,6 +379,16 @@ func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) (
return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID) return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
} }
func (p *CloudFlareProvider) getCustomHostnameIDbyCustomHostnameAndOrigin(chs []cloudflare.CustomHostname, customHostname string, origin string) (string, string) {
for _, zoneCh := range chs {
if zoneCh.Hostname == customHostname && zoneCh.CustomOriginServer == origin {
return zoneCh.ID, zoneCh.Hostname
}
}
return "", ""
}
func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, managedRecords []string, args ...interface{}) { func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, managedRecords []string, args ...interface{}) {
t.Helper() t.Helper()
@ -399,9 +446,11 @@ func TestCloudflareA(t *testing.T) {
AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
Type: "A", Type: "A",
Name: "bar.com", Name: "bar.com",
Content: "127.0.0.1", Content: "127.0.0.1",
@ -410,9 +459,11 @@ func TestCloudflareA(t *testing.T) {
}, },
}, },
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
Type: "A", Type: "A",
Name: "bar.com", Name: "bar.com",
Content: "127.0.0.2", Content: "127.0.0.2",
@ -436,9 +487,11 @@ func TestCloudflareCname(t *testing.T) {
AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("CNAME", "cname.bar.com", "google.com"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("CNAME", "cname.bar.com", "google.com"),
Type: "CNAME", Type: "CNAME",
Name: "cname.bar.com", Name: "cname.bar.com",
Content: "google.com", Content: "google.com",
@ -447,9 +500,11 @@ func TestCloudflareCname(t *testing.T) {
}, },
}, },
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("CNAME", "cname.bar.com", "facebook.com"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("CNAME", "cname.bar.com", "facebook.com"),
Type: "CNAME", Type: "CNAME",
Name: "cname.bar.com", Name: "cname.bar.com",
Content: "facebook.com", Content: "facebook.com",
@ -474,9 +529,11 @@ func TestCloudflareCustomTTL(t *testing.T) {
AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("A", "ttl.bar.com", "127.0.0.1"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "ttl.bar.com", "127.0.0.1"),
Type: "A", Type: "A",
Name: "ttl.bar.com", Name: "ttl.bar.com",
Content: "127.0.0.1", Content: "127.0.0.1",
@ -500,9 +557,11 @@ func TestCloudflareProxiedDefault(t *testing.T) {
AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
Type: "A", Type: "A",
Name: "bar.com", Name: "bar.com",
Content: "127.0.0.1", Content: "127.0.0.1",
@ -532,9 +591,11 @@ func TestCloudflareProxiedOverrideTrue(t *testing.T) {
AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
Type: "A", Type: "A",
Name: "bar.com", Name: "bar.com",
Content: "127.0.0.1", Content: "127.0.0.1",
@ -564,9 +625,11 @@ func TestCloudflareProxiedOverrideFalse(t *testing.T) {
AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
Type: "A", Type: "A",
Name: "bar.com", Name: "bar.com",
Content: "127.0.0.1", Content: "127.0.0.1",
@ -596,9 +659,11 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
Type: "A", Type: "A",
Name: "bar.com", Name: "bar.com",
Content: "127.0.0.1", Content: "127.0.0.1",
@ -631,11 +696,12 @@ func TestCloudflareSetProxied(t *testing.T) {
} }
for _, testCase := range testCases { for _, testCase := range testCases {
target := "127.0.0.1"
endpoints := []*endpoint.Endpoint{ endpoints := []*endpoint.Endpoint{
{ {
RecordType: testCase.recordType, RecordType: testCase.recordType,
DNSName: testCase.domain, DNSName: testCase.domain,
Targets: endpoint.Targets{"127.0.0.1"}, Targets: endpoint.Targets{target},
ProviderSpecific: endpoint.ProviderSpecific{ ProviderSpecific: endpoint.ProviderSpecific{
endpoint.ProviderSpecificProperty{ endpoint.ProviderSpecificProperty{
Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied",
@ -644,12 +710,14 @@ func TestCloudflareSetProxied(t *testing.T) {
}, },
}, },
} }
expectedID := fmt.Sprintf("%s-%s-%s", testCase.domain, testCase.recordType, target)
AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: expectedID,
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: expectedID,
Type: testCase.recordType, Type: testCase.recordType,
Name: testCase.domain, Name: testCase.domain,
Content: "127.0.0.1", Content: "127.0.0.1",
@ -795,7 +863,8 @@ func TestCloudflareProvider(t *testing.T) {
false, false,
true, true,
5000, 5000,
"") "",
CustomHostnamesConfig{Enabled: false})
if err != nil { if err != nil {
t.Errorf("should not fail, %s", err) t.Errorf("should not fail, %s", err)
} }
@ -812,7 +881,8 @@ func TestCloudflareProvider(t *testing.T) {
false, false,
true, true,
5000, 5000,
"") "",
CustomHostnamesConfig{Enabled: false})
if err != nil { if err != nil {
t.Errorf("should not fail, %s", err) t.Errorf("should not fail, %s", err)
} }
@ -826,7 +896,8 @@ func TestCloudflareProvider(t *testing.T) {
false, false,
true, true,
5000, 5000,
"") "",
CustomHostnamesConfig{Enabled: false})
if err != nil { if err != nil {
t.Errorf("should not fail, %s", err) t.Errorf("should not fail, %s", err)
} }
@ -839,7 +910,8 @@ func TestCloudflareProvider(t *testing.T) {
false, false,
true, true,
5000, 5000,
"") "",
CustomHostnamesConfig{Enabled: false})
if err == nil { if err == nil {
t.Errorf("expected to fail") t.Errorf("expected to fail")
} }
@ -877,9 +949,11 @@ func TestCloudflareApplyChanges(t *testing.T) {
td.Cmp(t, client.Actions, []MockAction{ td.Cmp(t, client.Actions, []MockAction{
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("", "new.bar.com", "target"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("", "new.bar.com", "target"),
Name: "new.bar.com", Name: "new.bar.com",
Content: "target", Content: "target",
TTL: 1, TTL: 1,
@ -887,9 +961,11 @@ func TestCloudflareApplyChanges(t *testing.T) {
}, },
}, },
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("", "foobar.bar.com", "target-new"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("", "foobar.bar.com", "target-new"),
Name: "foobar.bar.com", Name: "foobar.bar.com",
Content: "target-new", Content: "target-new",
TTL: 1, TTL: 1,
@ -1366,9 +1442,11 @@ func TestCloudflareComplexUpdate(t *testing.T) {
RecordId: "2345678901", RecordId: "2345678901",
}, },
{ {
Name: "Create", Name: "Create",
ZoneId: "001", ZoneId: "001",
RecordId: generateDNSRecordID("A", "foobar.bar.com", "2.3.4.5"),
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: generateDNSRecordID("A", "foobar.bar.com", "2.3.4.5"),
Name: "foobar.bar.com", Name: "foobar.bar.com",
Type: "A", Type: "A",
Content: "2.3.4.5", Content: "2.3.4.5",
@ -1381,6 +1459,7 @@ func TestCloudflareComplexUpdate(t *testing.T) {
ZoneId: "001", ZoneId: "001",
RecordId: "1234567890", RecordId: "1234567890",
RecordData: cloudflare.DNSRecord{ RecordData: cloudflare.DNSRecord{
ID: "1234567890",
Name: "foobar.bar.com", Name: "foobar.bar.com",
Type: "A", Type: "A",
Content: "1.2.3.4", Content: "1.2.3.4",
@ -1451,7 +1530,14 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
func TestCloudFlareProvider_Region(t *testing.T) { func TestCloudFlareProvider_Region(t *testing.T) {
_ = os.Setenv("CF_API_TOKEN", "abc123def") _ = os.Setenv("CF_API_TOKEN", "abc123def")
_ = os.Setenv("CF_API_EMAIL", "test@test.com") _ = os.Setenv("CF_API_EMAIL", "test@test.com")
provider, err := NewCloudFlareProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.ZoneIDFilter{}, true, false, 50, "us") provider, err := NewCloudFlareProvider(
endpoint.NewDomainFilter([]string{"example.com"}),
provider.ZoneIDFilter{},
true,
false,
50,
"us",
CustomHostnamesConfig{Enabled: false})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1482,7 +1568,14 @@ func TestCloudFlareProvider_updateDataLocalizationRegionalHostnameParams(t *test
func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
_ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx") _ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx")
_ = os.Setenv("CF_API_EMAIL", "test@test.com") _ = os.Setenv("CF_API_EMAIL", "test@test.com")
provider, err := NewCloudFlareProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.ZoneIDFilter{}, true, false, 50, "us") provider, err := NewCloudFlareProvider(
endpoint.NewDomainFilter([]string{"example.com"}),
provider.ZoneIDFilter{},
true,
false,
50,
"us",
CustomHostnamesConfig{Enabled: false})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1492,7 +1585,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
Targets: []string{"192.0.2.1"}, Targets: []string{"192.0.2.1"},
} }
change := provider.newCloudFlareChange(cloudFlareCreate, endpoint, endpoint.Targets[0]) change := provider.newCloudFlareChange(cloudFlareCreate, endpoint, endpoint.Targets[0], nil)
if change.RegionalHostname.RegionKey != "us" { if change.RegionalHostname.RegionKey != "us" {
t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.RegionKey) t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.RegionKey)
} }
@ -1621,33 +1714,353 @@ func TestCloudFlareProvider_submitChangesApex(t *testing.T) {
} }
} }
func TestCloudflareCustomHostnameOperations(t *testing.T) { func TestCloudflareZoneRecordsFail(t *testing.T) {
client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{ client := &mockCloudFlareClient{
"001": ExampleDomain, Zones: map[string]string{
}) "newerror-001": "bar.com",
provider := &CloudFlareProvider{ },
Client: client, Records: map[string]map[string]cloudflare.DNSRecord{},
customHostnames: map[string]map[string]cloudflare.CustomHostname{},
}
failingProvider := &CloudFlareProvider{
Client: client,
CustomHostnamesConfig: CustomHostnamesConfig{Enabled: true},
} }
ctx := context.Background() ctx := context.Background()
records, err := provider.Records(ctx) _, err := failingProvider.Records(ctx)
if err != nil { if err == nil {
t.Errorf("should not fail, %s", err) t.Errorf("should fail - invalid zone id, %s", err)
}
}
func TestCloudflareDNSRecordsOperationsFail(t *testing.T) {
client := NewMockCloudFlareClient()
provider := &CloudFlareProvider{
Client: client,
CustomHostnamesConfig: CustomHostnamesConfig{Enabled: true},
}
ctx := context.Background()
domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
testFailCases := []struct {
Name string
Endpoints []*endpoint.Endpoint
ExpectedCustomHostnames map[string]string
shouldFail bool
}{
{
Name: "failing to create dns record",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "newerror.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
},
},
shouldFail: true,
},
{
Name: "failing to list DNS record",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "newerror-list-1.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
},
},
shouldFail: true,
},
{
Name: "create failing to update DNS record",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "newerror-update-1.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
},
},
shouldFail: false,
},
{
Name: "failing to update DNS record",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "newerror-update-1.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 1234,
Labels: endpoint.Labels{},
},
},
shouldFail: true,
},
{
Name: "create failing to delete DNS record",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "newerror-delete-1.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 1234,
Labels: endpoint.Labels{},
},
},
shouldFail: false,
},
{
Name: "failing to delete erroring DNS record",
Endpoints: []*endpoint.Endpoint{},
shouldFail: true,
},
} }
for _, tc := range testFailCases {
records, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
}
endpoints, err := provider.AdjustEndpoints(tc.Endpoints)
assert.NoError(t, err)
plan := &plan.Plan{
Current: records,
Desired: endpoints,
DomainFilter: endpoint.MatchAllDomainFilters{&domainFilter},
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
planned := plan.Calculate()
err = provider.ApplyChanges(context.Background(), planned.Changes)
if err == nil && tc.shouldFail {
t.Errorf("should fail - %s, %s", tc.Name, err)
} else if err != nil && !tc.shouldFail {
t.Errorf("should not fail - %s, %s", tc.Name, err)
}
}
}
func TestCloudflareCustomHostnameOperations(t *testing.T) {
client := NewMockCloudFlareClient()
provider := &CloudFlareProvider{
Client: client,
CustomHostnamesConfig: CustomHostnamesConfig{Enabled: true},
}
ctx := context.Background()
domainFilter := endpoint.NewDomainFilter([]string{"bar.com"}) domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
testFailCases := []struct {
Name string
Endpoints []*endpoint.Endpoint
ExpectedCustomHostnames map[string]string
shouldFail bool
}{
{
Name: "failing to create custom hostname on record creation",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "create.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "newerror-create.foo.fancybar.com",
},
},
},
},
shouldFail: true,
},
{
Name: "add custom hostname to more than one endpoint",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "fail.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4", "2.3.4.5"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "fail.foo.fancybar.com",
},
},
},
},
shouldFail: true,
},
{
Name: "failing to update custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "fail.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "newerror-create.foo.fancybar.com",
},
},
},
},
shouldFail: true,
},
{
Name: "adding failing to list custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "fail.list.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "newerror-list-1.foo.fancybar.com",
},
},
},
},
shouldFail: true,
},
{
Name: "adding normal custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "b.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "b.foo.fancybar.com",
},
},
},
},
shouldFail: false,
},
{
Name: "updating to erroring custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "b.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "newerror-create.foo.fancybar.com",
},
},
},
},
shouldFail: true,
},
{
Name: "set to custom hostname which would error on removing",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "b.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "newerror-delete.foo.fancybar.com",
},
},
},
},
shouldFail: false,
},
{
Name: "delete erroring on remove custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "b.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
},
},
shouldFail: true,
},
{
Name: "create erroring to remove custom hostname on record deletion",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "b.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "newerror-delete.foo.fancybar.com",
},
},
},
},
shouldFail: false,
},
{
Name: "failing to remove custom hostname on record deletion",
Endpoints: []*endpoint.Endpoint{},
shouldFail: true,
},
}
testCases := []struct { testCases := []struct {
Name string Name string
Endpoints []*endpoint.Endpoint Endpoints []*endpoint.Endpoint
ExpectedCustomHostnames map[string]string ExpectedCustomHostnames map[string]string
}{ }{
{
Name: "add A record without custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "nocustomhostname.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
},
},
ExpectedCustomHostnames: map[string]string{
"nocustomhostname.foo.bar.com": "",
},
},
{ {
Name: "add custom hostname", Name: "add custom hostname",
Endpoints: []*endpoint.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
DNSName: "a.foo.bar.com", DNSName: "a.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4", "2.3.4.5"}, Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA, RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{}, Labels: endpoint.Labels{},
@ -1682,7 +2095,7 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
Endpoints: []*endpoint.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
DNSName: "a.foo.bar.com", DNSName: "a.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4", "2.3.4.5"}, Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA, RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{}, Labels: endpoint.Labels{},
@ -1694,24 +2107,60 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
}, },
}, },
}, },
ExpectedCustomHostnames: map[string]string{"a.foo.bar.com": "a2.foo.fancybar.com"}, ExpectedCustomHostnames: map[string]string{
"a.foo.bar.com": "a2.foo.fancybar.com",
},
}, },
{ {
Name: "delete custom hostname", Name: "delete custom hostname",
Endpoints: []*endpoint.Endpoint{ Endpoints: []*endpoint.Endpoint{
{ {
DNSName: "a.foo.bar.com", DNSName: "a.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4", "2.3.4.5"}, Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA, RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{}, Labels: endpoint.Labels{},
}, },
}, },
ExpectedCustomHostnames: map[string]string{"a.foo.bar.com": ""}, ExpectedCustomHostnames: map[string]string{
"a.foo.bar.com": "",
},
}, },
} }
for _, tc := range testFailCases {
records, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
}
endpoints, err := provider.AdjustEndpoints(tc.Endpoints)
assert.NoError(t, err)
plan := &plan.Plan{
Current: records,
Desired: endpoints,
DomainFilter: endpoint.MatchAllDomainFilters{&domainFilter},
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
planned := plan.Calculate()
err = provider.ApplyChanges(context.Background(), planned.Changes)
if err == nil && tc.shouldFail {
t.Errorf("should fail - %s, %s", tc.Name, err)
} else if err != nil && !tc.shouldFail {
t.Errorf("should not fail - %s, %s", tc.Name, err)
}
}
for _, tc := range testCases { for _, tc := range testCases {
records, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
}
endpoints, err := provider.AdjustEndpoints(tc.Endpoints) endpoints, err := provider.AdjustEndpoints(tc.Endpoints)
assert.NoError(t, err) assert.NoError(t, err)
@ -1726,17 +2175,106 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) {
err = provider.ApplyChanges(context.Background(), planned.Changes) err = provider.ApplyChanges(context.Background(), planned.Changes)
if err != nil { if err != nil {
t.Errorf("should not fail, %s", err) t.Errorf("should not fail - %s, %s", tc.Name, err)
} }
chs, chErr := provider.listCustomHostnamesWithPagination(ctx, "001") chs, chErr := provider.listCustomHostnamesWithPagination(ctx, "001")
if chErr != nil { if chErr != nil {
t.Errorf("should not fail, %s", chErr) t.Errorf("should not fail - %s, %s", tc.Name, chErr)
} }
for k, v := range tc.ExpectedCustomHostnames { for expectedOrigin, expectedCustomHostname := range tc.ExpectedCustomHostnames {
_, ch := provider.getCustomHostnameIDbyOrigin(chs, k) _, ch := provider.getCustomHostnameIDbyCustomHostnameAndOrigin(chs, expectedCustomHostname, expectedOrigin)
assert.Equal(t, v, ch) assert.Equal(t, expectedCustomHostname, ch)
} }
} }
} }
func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) {
client := NewMockCloudFlareClient()
provider := &CloudFlareProvider{
Client: client,
CustomHostnamesConfig: CustomHostnamesConfig{Enabled: true},
}
ctx := context.Background()
zoneID := "001"
domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
testCases := []struct {
Name string
Endpoints []*endpoint.Endpoint
ExpectedCustomHostnames map[string]string
preApplyHook bool
}{
{
Name: "create DNS record with custom hostname",
Endpoints: []*endpoint.Endpoint{
{
DNSName: "create.foo.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL),
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname",
Value: "newerror-getCustomHostnameOrigin.foo.fancybar.com",
},
},
},
},
preApplyHook: false,
},
{
Name: "remove DNS record with unexpectedly missing custom hostname",
Endpoints: []*endpoint.Endpoint{},
preApplyHook: true,
},
}
b := testutils.LogsToBuffer(log.InfoLevel, t)
for _, tc := range testCases {
records, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
}
endpoints, err := provider.AdjustEndpoints(tc.Endpoints)
assert.NoError(t, err)
plan := &plan.Plan{
Current: records,
Desired: endpoints,
DomainFilter: endpoint.MatchAllDomainFilters{&domainFilter},
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
planned := plan.Calculate()
// manually corrupt custom hostname before the deletion step
// the purpose is to cause getCustomHostnameOrigin() to fail on change.Action == cloudFlareDelete
if tc.preApplyHook {
chs, chErr := provider.listCustomHostnamesWithPagination(ctx, zoneID)
if chErr != nil {
t.Errorf("should not fail - %s, %s", tc.Name, chErr)
}
chID, _ := provider.getCustomHostnameOrigin(chs, "newerror-getCustomHostnameOrigin.foo.fancybar.com")
if chID != "" {
t.Logf("corrupting custom hostname %v", chID)
oldCh := client.customHostnames[zoneID][chID]
ch := cloudflare.CustomHostname{
Hostname: "corrupted-newerror-getCustomHostnameOrigin.foo.fancybar.com",
CustomOriginServer: oldCh.CustomOriginServer,
SSL: oldCh.SSL,
}
client.customHostnames[zoneID][chID] = ch
}
}
err = provider.ApplyChanges(context.Background(), planned.Changes)
if err != nil {
t.Errorf("should not fail - %s, %s", tc.Name, err)
}
}
assert.Contains(t, b.String(), "level=info msg=\"Custom hostname newerror-getCustomHostnameOrigin.foo.fancybar.com not found\" action=DELETE record=create.foo.bar.com")
}