diff --git a/docs/flags.md b/docs/flags.md index 838da5066..bbe78c71d 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -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-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-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-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 | diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index 33684145e..a831f6190 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -310,7 +310,14 @@ If not set the value will default to `global`. ## Setting cloudflare-custom-hostname -Using the `external-dns.alpha.kubernetes.io/cloudflare-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: ""` 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. ## Using CRD source to manage DNS records in Cloudflare diff --git a/main.go b/main.go index 25f2b5c15..6d013215b 100644 --- a/main.go +++ b/main.go @@ -250,7 +250,18 @@ func main() { case "civo": p, err = civo.NewCivoProvider(domainFilter, cfg.DryRun) 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": p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun) case "digitalocean": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 938fd1956..f0697d498 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -43,332 +43,338 @@ var Version = "unknown" // Config is a project-wide configuration type Config struct { - APIServerURL string - KubeConfig string - RequestTimeout time.Duration - DefaultTargets []string - GlooNamespaces []string - SkipperRouteGroupVersion string - Sources []string - Namespace string - AnnotationFilter string - LabelFilter string - IngressClassNames []string - FQDNTemplate string - CombineFQDNAndAnnotation bool - IgnoreHostnameAnnotation bool - IgnoreNonHostNetworkPods bool - IgnoreIngressTLSSpec bool - IgnoreIngressRulesSpec bool - ListenEndpointEvents bool - GatewayName string - GatewayNamespace string - GatewayLabelFilter string - Compatibility string - PodSourceDomain string - PublishInternal bool - PublishHostIP bool - AlwaysPublishNotReadyAddresses bool - ConnectorSourceServer string - Provider string - ProviderCacheTime time.Duration - GoogleProject string - GoogleBatchChangeSize int - GoogleBatchChangeInterval time.Duration - GoogleZoneVisibility string - DomainFilter []string - ExcludeDomains []string - RegexDomainFilter *regexp.Regexp - RegexDomainExclusion *regexp.Regexp - ZoneNameFilter []string - ZoneIDFilter []string - TargetNetFilter []string - ExcludeTargetNets []string - AlibabaCloudConfigFile string - AlibabaCloudZoneType string - AWSZoneType string - AWSZoneTagFilter []string - AWSAssumeRole string - AWSProfiles []string - AWSAssumeRoleExternalID string `secure:"yes"` - AWSBatchChangeSize int - AWSBatchChangeSizeBytes int - AWSBatchChangeSizeValues int - AWSBatchChangeInterval time.Duration - AWSEvaluateTargetHealth bool - AWSAPIRetries int - AWSPreferCNAME bool - AWSZoneCacheDuration time.Duration - AWSSDServiceCleanup bool - AWSSDCreateTag map[string]string - AWSZoneMatchParent bool - AWSDynamoDBRegion string - AWSDynamoDBTable string - AzureConfigFile string - AzureResourceGroup string - AzureSubscriptionID string - AzureUserAssignedIdentityClientID string - AzureActiveDirectoryAuthorityHost string - AzureZonesCacheDuration time.Duration - CloudflareProxied bool - CloudflareDNSRecordsPerPage int - CloudflareRegionKey string - CoreDNSPrefix string - AkamaiServiceConsumerDomain string - AkamaiClientToken string - AkamaiClientSecret string - AkamaiAccessToken string - AkamaiEdgercPath string - AkamaiEdgercSection string - OCIConfigFile string - OCICompartmentOCID string - OCIAuthInstancePrincipal bool - OCIZoneScope string - OCIZoneCacheDuration time.Duration - InMemoryZones []string - OVHEndpoint string - OVHApiRateLimit int - PDNSServer string - PDNSServerID string - PDNSAPIKey string `secure:"yes"` - PDNSSkipTLSVerify bool - TLSCA string - TLSClientCert string - TLSClientCertKey string - Policy string - Registry string - TXTOwnerID string - TXTPrefix string - TXTSuffix string - TXTEncryptEnabled bool - TXTEncryptAESKey string `secure:"yes"` - TXTNewFormatOnly bool - Interval time.Duration - MinEventSyncInterval time.Duration - Once bool - DryRun bool - UpdateEvents bool - LogFormat string - MetricsAddress string - LogLevel string - TXTCacheInterval time.Duration - TXTWildcardReplacement string - ExoscaleEndpoint string - ExoscaleAPIKey string `secure:"yes"` - ExoscaleAPISecret string `secure:"yes"` - ExoscaleAPIEnvironment string - ExoscaleAPIZone string - CRDSourceAPIVersion string - CRDSourceKind string - ServiceTypeFilter []string - CFAPIEndpoint string - CFUsername string - CFPassword string - ResolveServiceLoadBalancerHostname bool - RFC2136Host []string - RFC2136Port int - RFC2136Zone []string - RFC2136Insecure bool - RFC2136GSSTSIG bool - RFC2136CreatePTR bool - RFC2136KerberosRealm string - RFC2136KerberosUsername string - RFC2136KerberosPassword string `secure:"yes"` - RFC2136TSIGKeyName string - RFC2136TSIGSecret string `secure:"yes"` - RFC2136TSIGSecretAlg string - RFC2136TAXFR bool - RFC2136MinTTL time.Duration - RFC2136LoadBalancingStrategy string - RFC2136BatchChangeSize int - RFC2136UseTLS bool - RFC2136SkipTLSVerify bool - NS1Endpoint string - NS1IgnoreSSL bool - NS1MinTTLSeconds int - TransIPAccountName string - TransIPPrivateKeyFile string - DigitalOceanAPIPageSize int - ManagedDNSRecordTypes []string - ExcludeDNSRecordTypes []string - GoDaddyAPIKey string `secure:"yes"` - GoDaddySecretKey string `secure:"yes"` - GoDaddyTTL int64 - GoDaddyOTE bool - OCPRouterName string - IBMCloudProxied bool - IBMCloudConfigFile string - TencentCloudConfigFile string - TencentCloudZoneType string - PiholeServer string - PiholePassword string `secure:"yes"` - PiholeTLSInsecureSkipVerify bool - PluralCluster string - PluralProvider string - WebhookProviderURL string - WebhookProviderReadTimeout time.Duration - WebhookProviderWriteTimeout time.Duration - WebhookServer bool - TraefikDisableLegacy bool - TraefikDisableNew bool - NAT64Networks []string + APIServerURL string + KubeConfig string + RequestTimeout time.Duration + DefaultTargets []string + GlooNamespaces []string + SkipperRouteGroupVersion string + Sources []string + Namespace string + AnnotationFilter string + LabelFilter string + IngressClassNames []string + FQDNTemplate string + CombineFQDNAndAnnotation bool + IgnoreHostnameAnnotation bool + IgnoreNonHostNetworkPods bool + IgnoreIngressTLSSpec bool + IgnoreIngressRulesSpec bool + ListenEndpointEvents bool + GatewayName string + GatewayNamespace string + GatewayLabelFilter string + Compatibility string + PodSourceDomain string + PublishInternal bool + PublishHostIP bool + AlwaysPublishNotReadyAddresses bool + ConnectorSourceServer string + Provider string + ProviderCacheTime time.Duration + GoogleProject string + GoogleBatchChangeSize int + GoogleBatchChangeInterval time.Duration + GoogleZoneVisibility string + DomainFilter []string + ExcludeDomains []string + RegexDomainFilter *regexp.Regexp + RegexDomainExclusion *regexp.Regexp + ZoneNameFilter []string + ZoneIDFilter []string + TargetNetFilter []string + ExcludeTargetNets []string + AlibabaCloudConfigFile string + AlibabaCloudZoneType string + AWSZoneType string + AWSZoneTagFilter []string + AWSAssumeRole string + AWSProfiles []string + AWSAssumeRoleExternalID string `secure:"yes"` + AWSBatchChangeSize int + AWSBatchChangeSizeBytes int + AWSBatchChangeSizeValues int + AWSBatchChangeInterval time.Duration + AWSEvaluateTargetHealth bool + AWSAPIRetries int + AWSPreferCNAME bool + AWSZoneCacheDuration time.Duration + AWSSDServiceCleanup bool + AWSSDCreateTag map[string]string + AWSZoneMatchParent bool + AWSDynamoDBRegion string + AWSDynamoDBTable string + AzureConfigFile string + AzureResourceGroup string + AzureSubscriptionID string + AzureUserAssignedIdentityClientID string + AzureActiveDirectoryAuthorityHost string + AzureZonesCacheDuration time.Duration + CloudflareProxied bool + CloudflareCustomHostnames bool + CloudflareCustomHostnamesMinTLSVersion string + CloudflareCustomHostnamesCertificateAuthority string + CloudflareDNSRecordsPerPage int + CloudflareRegionKey string + CoreDNSPrefix string + AkamaiServiceConsumerDomain string + AkamaiClientToken string + AkamaiClientSecret string + AkamaiAccessToken string + AkamaiEdgercPath string + AkamaiEdgercSection string + OCIConfigFile string + OCICompartmentOCID string + OCIAuthInstancePrincipal bool + OCIZoneScope string + OCIZoneCacheDuration time.Duration + InMemoryZones []string + OVHEndpoint string + OVHApiRateLimit int + PDNSServer string + PDNSServerID string + PDNSAPIKey string `secure:"yes"` + PDNSSkipTLSVerify bool + TLSCA string + TLSClientCert string + TLSClientCertKey string + Policy string + Registry string + TXTOwnerID string + TXTPrefix string + TXTSuffix string + TXTEncryptEnabled bool + TXTEncryptAESKey string `secure:"yes"` + TXTNewFormatOnly bool + Interval time.Duration + MinEventSyncInterval time.Duration + Once bool + DryRun bool + UpdateEvents bool + LogFormat string + MetricsAddress string + LogLevel string + TXTCacheInterval time.Duration + TXTWildcardReplacement string + ExoscaleEndpoint string + ExoscaleAPIKey string `secure:"yes"` + ExoscaleAPISecret string `secure:"yes"` + ExoscaleAPIEnvironment string + ExoscaleAPIZone string + CRDSourceAPIVersion string + CRDSourceKind string + ServiceTypeFilter []string + CFAPIEndpoint string + CFUsername string + CFPassword string + ResolveServiceLoadBalancerHostname bool + RFC2136Host []string + RFC2136Port int + RFC2136Zone []string + RFC2136Insecure bool + RFC2136GSSTSIG bool + RFC2136CreatePTR bool + RFC2136KerberosRealm string + RFC2136KerberosUsername string + RFC2136KerberosPassword string `secure:"yes"` + RFC2136TSIGKeyName string + RFC2136TSIGSecret string `secure:"yes"` + RFC2136TSIGSecretAlg string + RFC2136TAXFR bool + RFC2136MinTTL time.Duration + RFC2136LoadBalancingStrategy string + RFC2136BatchChangeSize int + RFC2136UseTLS bool + RFC2136SkipTLSVerify bool + NS1Endpoint string + NS1IgnoreSSL bool + NS1MinTTLSeconds int + TransIPAccountName string + TransIPPrivateKeyFile string + DigitalOceanAPIPageSize int + ManagedDNSRecordTypes []string + ExcludeDNSRecordTypes []string + GoDaddyAPIKey string `secure:"yes"` + GoDaddySecretKey string `secure:"yes"` + GoDaddyTTL int64 + GoDaddyOTE bool + OCPRouterName string + IBMCloudProxied bool + IBMCloudConfigFile string + TencentCloudConfigFile string + TencentCloudZoneType string + PiholeServer string + PiholePassword string `secure:"yes"` + PiholeTLSInsecureSkipVerify bool + PluralCluster string + PluralProvider string + WebhookProviderURL string + WebhookProviderReadTimeout time.Duration + WebhookProviderWriteTimeout time.Duration + WebhookServer bool + TraefikDisableLegacy bool + TraefikDisableNew bool + NAT64Networks []string } var defaultConfig = &Config{ - APIServerURL: "", - KubeConfig: "", - RequestTimeout: time.Second * 30, - DefaultTargets: []string{}, - GlooNamespaces: []string{"gloo-system"}, - SkipperRouteGroupVersion: "zalando.org/v1", - Sources: nil, - Namespace: "", - AnnotationFilter: "", - LabelFilter: labels.Everything().String(), - IngressClassNames: nil, - FQDNTemplate: "", - CombineFQDNAndAnnotation: false, - IgnoreHostnameAnnotation: false, - IgnoreIngressTLSSpec: false, - IgnoreIngressRulesSpec: false, - GatewayName: "", - GatewayNamespace: "", - GatewayLabelFilter: "", - Compatibility: "", - PublishInternal: false, - PublishHostIP: false, - ConnectorSourceServer: "localhost:8080", - Provider: "", - ProviderCacheTime: 0, - GoogleProject: "", - GoogleBatchChangeSize: 1000, - GoogleBatchChangeInterval: time.Second, - GoogleZoneVisibility: "", - DomainFilter: []string{}, - ZoneIDFilter: []string{}, - ExcludeDomains: []string{}, - RegexDomainFilter: regexp.MustCompile(""), - RegexDomainExclusion: regexp.MustCompile(""), - TargetNetFilter: []string{}, - ExcludeTargetNets: []string{}, - AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", - AWSZoneType: "", - AWSZoneTagFilter: []string{}, - AWSZoneMatchParent: false, - AWSAssumeRole: "", - AWSAssumeRoleExternalID: "", - AWSBatchChangeSize: 1000, - AWSBatchChangeSizeBytes: 32000, - AWSBatchChangeSizeValues: 1000, - AWSBatchChangeInterval: time.Second, - AWSEvaluateTargetHealth: true, - AWSAPIRetries: 3, - AWSPreferCNAME: false, - AWSZoneCacheDuration: 0 * time.Second, - AWSSDServiceCleanup: false, - AWSSDCreateTag: map[string]string{}, - AWSDynamoDBRegion: "", - AWSDynamoDBTable: "external-dns", - AzureConfigFile: "/etc/kubernetes/azure.json", - AzureResourceGroup: "", - AzureSubscriptionID: "", - AzureZonesCacheDuration: 0 * time.Second, - CloudflareProxied: false, - CloudflareDNSRecordsPerPage: 100, - CloudflareRegionKey: "earth", - CoreDNSPrefix: "/skydns/", - AkamaiServiceConsumerDomain: "", - AkamaiClientToken: "", - AkamaiClientSecret: "", - AkamaiAccessToken: "", - AkamaiEdgercSection: "", - AkamaiEdgercPath: "", - OCIConfigFile: "/etc/kubernetes/oci.yaml", - OCIZoneScope: "GLOBAL", - OCIZoneCacheDuration: 0 * time.Second, - InMemoryZones: []string{}, - OVHEndpoint: "ovh-eu", - OVHApiRateLimit: 20, - PDNSServer: "http://localhost:8081", - PDNSServerID: "localhost", - PDNSAPIKey: "", - PDNSSkipTLSVerify: false, - PodSourceDomain: "", - TLSCA: "", - TLSClientCert: "", - TLSClientCertKey: "", - Policy: "sync", - Registry: "txt", - TXTOwnerID: "default", - TXTPrefix: "", - TXTSuffix: "", - TXTCacheInterval: 0, - TXTWildcardReplacement: "", - MinEventSyncInterval: 5 * time.Second, - TXTEncryptEnabled: false, - TXTEncryptAESKey: "", - TXTNewFormatOnly: false, - Interval: time.Minute, - Once: false, - DryRun: false, - UpdateEvents: false, - LogFormat: "text", - MetricsAddress: ":7979", - LogLevel: logrus.InfoLevel.String(), - ExoscaleAPIEnvironment: "api", - ExoscaleAPIZone: "ch-gva-2", - ExoscaleAPIKey: "", - ExoscaleAPISecret: "", - CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", - CRDSourceKind: "DNSEndpoint", - ServiceTypeFilter: []string{}, - CFAPIEndpoint: "", - CFUsername: "", - CFPassword: "", - RFC2136Host: []string{""}, - RFC2136Port: 0, - RFC2136Zone: []string{}, - RFC2136Insecure: false, - RFC2136GSSTSIG: false, - RFC2136KerberosRealm: "", - RFC2136KerberosUsername: "", - RFC2136KerberosPassword: "", - RFC2136TSIGKeyName: "", - RFC2136TSIGSecret: "", - RFC2136TSIGSecretAlg: "", - RFC2136TAXFR: true, - RFC2136MinTTL: 0, - RFC2136BatchChangeSize: 50, - RFC2136UseTLS: false, - RFC2136LoadBalancingStrategy: "disabled", - RFC2136SkipTLSVerify: false, - NS1Endpoint: "", - NS1IgnoreSSL: false, - TransIPAccountName: "", - TransIPPrivateKeyFile: "", - DigitalOceanAPIPageSize: 50, - ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, - ExcludeDNSRecordTypes: []string{}, - GoDaddyAPIKey: "", - GoDaddySecretKey: "", - GoDaddyTTL: 600, - GoDaddyOTE: false, - IBMCloudProxied: false, - IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", - TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", - TencentCloudZoneType: "", - PiholeServer: "", - PiholePassword: "", - PiholeTLSInsecureSkipVerify: false, - PluralCluster: "", - PluralProvider: "", - WebhookProviderURL: "http://localhost:8888", - WebhookProviderReadTimeout: 5 * time.Second, - WebhookProviderWriteTimeout: 10 * time.Second, - WebhookServer: false, - TraefikDisableLegacy: false, - TraefikDisableNew: false, - NAT64Networks: []string{}, + APIServerURL: "", + KubeConfig: "", + RequestTimeout: time.Second * 30, + DefaultTargets: []string{}, + GlooNamespaces: []string{"gloo-system"}, + SkipperRouteGroupVersion: "zalando.org/v1", + Sources: nil, + Namespace: "", + AnnotationFilter: "", + LabelFilter: labels.Everything().String(), + IngressClassNames: nil, + FQDNTemplate: "", + CombineFQDNAndAnnotation: false, + IgnoreHostnameAnnotation: false, + IgnoreIngressTLSSpec: false, + IgnoreIngressRulesSpec: false, + GatewayName: "", + GatewayNamespace: "", + GatewayLabelFilter: "", + Compatibility: "", + PublishInternal: false, + PublishHostIP: false, + ConnectorSourceServer: "localhost:8080", + Provider: "", + ProviderCacheTime: 0, + GoogleProject: "", + GoogleBatchChangeSize: 1000, + GoogleBatchChangeInterval: time.Second, + GoogleZoneVisibility: "", + DomainFilter: []string{}, + ZoneIDFilter: []string{}, + ExcludeDomains: []string{}, + RegexDomainFilter: regexp.MustCompile(""), + RegexDomainExclusion: regexp.MustCompile(""), + TargetNetFilter: []string{}, + ExcludeTargetNets: []string{}, + AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", + AWSZoneType: "", + AWSZoneTagFilter: []string{}, + AWSZoneMatchParent: false, + AWSAssumeRole: "", + AWSAssumeRoleExternalID: "", + AWSBatchChangeSize: 1000, + AWSBatchChangeSizeBytes: 32000, + AWSBatchChangeSizeValues: 1000, + AWSBatchChangeInterval: time.Second, + AWSEvaluateTargetHealth: true, + AWSAPIRetries: 3, + AWSPreferCNAME: false, + AWSZoneCacheDuration: 0 * time.Second, + AWSSDServiceCleanup: false, + AWSSDCreateTag: map[string]string{}, + AWSDynamoDBRegion: "", + AWSDynamoDBTable: "external-dns", + AzureConfigFile: "/etc/kubernetes/azure.json", + AzureResourceGroup: "", + AzureSubscriptionID: "", + AzureZonesCacheDuration: 0 * time.Second, + CloudflareProxied: false, + CloudflareCustomHostnames: false, + CloudflareCustomHostnamesMinTLSVersion: "1.0", + CloudflareCustomHostnamesCertificateAuthority: "google", + CloudflareDNSRecordsPerPage: 100, + CloudflareRegionKey: "earth", + CoreDNSPrefix: "/skydns/", + AkamaiServiceConsumerDomain: "", + AkamaiClientToken: "", + AkamaiClientSecret: "", + AkamaiAccessToken: "", + AkamaiEdgercSection: "", + AkamaiEdgercPath: "", + OCIConfigFile: "/etc/kubernetes/oci.yaml", + OCIZoneScope: "GLOBAL", + OCIZoneCacheDuration: 0 * time.Second, + InMemoryZones: []string{}, + OVHEndpoint: "ovh-eu", + OVHApiRateLimit: 20, + PDNSServer: "http://localhost:8081", + PDNSServerID: "localhost", + PDNSAPIKey: "", + PDNSSkipTLSVerify: false, + PodSourceDomain: "", + TLSCA: "", + TLSClientCert: "", + TLSClientCertKey: "", + Policy: "sync", + Registry: "txt", + TXTOwnerID: "default", + TXTPrefix: "", + TXTSuffix: "", + TXTCacheInterval: 0, + TXTWildcardReplacement: "", + MinEventSyncInterval: 5 * time.Second, + TXTEncryptEnabled: false, + TXTEncryptAESKey: "", + TXTNewFormatOnly: false, + Interval: time.Minute, + Once: false, + DryRun: false, + UpdateEvents: false, + LogFormat: "text", + MetricsAddress: ":7979", + LogLevel: logrus.InfoLevel.String(), + ExoscaleAPIEnvironment: "api", + ExoscaleAPIZone: "ch-gva-2", + ExoscaleAPIKey: "", + ExoscaleAPISecret: "", + CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", + CRDSourceKind: "DNSEndpoint", + ServiceTypeFilter: []string{}, + CFAPIEndpoint: "", + CFUsername: "", + CFPassword: "", + RFC2136Host: []string{""}, + RFC2136Port: 0, + RFC2136Zone: []string{}, + RFC2136Insecure: false, + RFC2136GSSTSIG: false, + RFC2136KerberosRealm: "", + RFC2136KerberosUsername: "", + RFC2136KerberosPassword: "", + RFC2136TSIGKeyName: "", + RFC2136TSIGSecret: "", + RFC2136TSIGSecretAlg: "", + RFC2136TAXFR: true, + RFC2136MinTTL: 0, + RFC2136BatchChangeSize: 50, + RFC2136UseTLS: false, + RFC2136LoadBalancingStrategy: "disabled", + RFC2136SkipTLSVerify: false, + NS1Endpoint: "", + NS1IgnoreSSL: false, + TransIPAccountName: "", + TransIPPrivateKeyFile: "", + DigitalOceanAPIPageSize: 50, + ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + ExcludeDNSRecordTypes: []string{}, + GoDaddyAPIKey: "", + GoDaddySecretKey: "", + GoDaddyTTL: 600, + GoDaddyOTE: false, + IBMCloudProxied: false, + IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", + TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", + TencentCloudZoneType: "", + PiholeServer: "", + PiholePassword: "", + PiholeTLSInsecureSkipVerify: false, + PluralCluster: "", + PluralProvider: "", + WebhookProviderURL: "http://localhost:8888", + WebhookProviderReadTimeout: 5 * time.Second, + WebhookProviderWriteTimeout: 10 * time.Second, + WebhookServer: false, + TraefikDisableLegacy: false, + TraefikDisableNew: false, + NAT64Networks: []string{}, } // 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("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-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) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 05d008d1b..9ce16a6be 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -32,213 +32,219 @@ import ( var ( minimalConfig = &Config{ - APIServerURL: "", - KubeConfig: "", - RequestTimeout: time.Second * 30, - GlooNamespaces: []string{"gloo-system"}, - SkipperRouteGroupVersion: "zalando.org/v1", - Sources: []string{"service"}, - Namespace: "", - FQDNTemplate: "", - Compatibility: "", - Provider: "google", - GoogleProject: "", - GoogleBatchChangeSize: 1000, - GoogleBatchChangeInterval: time.Second, - GoogleZoneVisibility: "", - DomainFilter: []string{""}, - ExcludeDomains: []string{""}, - RegexDomainFilter: regexp.MustCompile(""), - RegexDomainExclusion: regexp.MustCompile(""), - ZoneNameFilter: []string{""}, - ZoneIDFilter: []string{""}, - AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", - AWSZoneType: "", - AWSZoneTagFilter: []string{""}, - AWSZoneMatchParent: false, - AWSAssumeRole: "", - AWSAssumeRoleExternalID: "", - AWSBatchChangeSize: 1000, - AWSBatchChangeSizeBytes: 32000, - AWSBatchChangeSizeValues: 1000, - AWSBatchChangeInterval: time.Second, - AWSEvaluateTargetHealth: true, - AWSAPIRetries: 3, - AWSPreferCNAME: false, - AWSProfiles: []string{""}, - AWSZoneCacheDuration: 0 * time.Second, - AWSSDServiceCleanup: false, - AWSSDCreateTag: map[string]string{}, - AWSDynamoDBTable: "external-dns", - AzureConfigFile: "/etc/kubernetes/azure.json", - AzureResourceGroup: "", - AzureSubscriptionID: "", - CloudflareProxied: false, - CloudflareDNSRecordsPerPage: 100, - CloudflareRegionKey: "", - CoreDNSPrefix: "/skydns/", - AkamaiServiceConsumerDomain: "", - AkamaiClientToken: "", - AkamaiClientSecret: "", - AkamaiAccessToken: "", - AkamaiEdgercPath: "", - AkamaiEdgercSection: "", - OCIConfigFile: "/etc/kubernetes/oci.yaml", - OCIZoneScope: "GLOBAL", - OCIZoneCacheDuration: 0 * time.Second, - InMemoryZones: []string{""}, - OVHEndpoint: "ovh-eu", - OVHApiRateLimit: 20, - PDNSServer: "http://localhost:8081", - PDNSServerID: "localhost", - PDNSAPIKey: "", - Policy: "sync", - Registry: "txt", - TXTOwnerID: "default", - TXTPrefix: "", - TXTCacheInterval: 0, - TXTNewFormatOnly: false, - Interval: time.Minute, - MinEventSyncInterval: 5 * time.Second, - Once: false, - DryRun: false, - UpdateEvents: false, - LogFormat: "text", - MetricsAddress: ":7979", - LogLevel: logrus.InfoLevel.String(), - ConnectorSourceServer: "localhost:8080", - ExoscaleAPIEnvironment: "api", - ExoscaleAPIZone: "ch-gva-2", - ExoscaleAPIKey: "", - ExoscaleAPISecret: "", - CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", - CRDSourceKind: "DNSEndpoint", - TransIPAccountName: "", - TransIPPrivateKeyFile: "", - DigitalOceanAPIPageSize: 50, - ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, - RFC2136BatchChangeSize: 50, - RFC2136Host: []string{""}, - RFC2136LoadBalancingStrategy: "disabled", - OCPRouterName: "default", - IBMCloudProxied: false, - IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", - TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", - TencentCloudZoneType: "", - WebhookProviderURL: "http://localhost:8888", - WebhookProviderReadTimeout: 5 * time.Second, - WebhookProviderWriteTimeout: 10 * time.Second, + APIServerURL: "", + KubeConfig: "", + RequestTimeout: time.Second * 30, + GlooNamespaces: []string{"gloo-system"}, + SkipperRouteGroupVersion: "zalando.org/v1", + Sources: []string{"service"}, + Namespace: "", + FQDNTemplate: "", + Compatibility: "", + Provider: "google", + GoogleProject: "", + GoogleBatchChangeSize: 1000, + GoogleBatchChangeInterval: time.Second, + GoogleZoneVisibility: "", + DomainFilter: []string{""}, + ExcludeDomains: []string{""}, + RegexDomainFilter: regexp.MustCompile(""), + RegexDomainExclusion: regexp.MustCompile(""), + ZoneNameFilter: []string{""}, + ZoneIDFilter: []string{""}, + AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", + AWSZoneType: "", + AWSZoneTagFilter: []string{""}, + AWSZoneMatchParent: false, + AWSAssumeRole: "", + AWSAssumeRoleExternalID: "", + AWSBatchChangeSize: 1000, + AWSBatchChangeSizeBytes: 32000, + AWSBatchChangeSizeValues: 1000, + AWSBatchChangeInterval: time.Second, + AWSEvaluateTargetHealth: true, + AWSAPIRetries: 3, + AWSPreferCNAME: false, + AWSProfiles: []string{""}, + AWSZoneCacheDuration: 0 * time.Second, + AWSSDServiceCleanup: false, + AWSSDCreateTag: map[string]string{}, + AWSDynamoDBTable: "external-dns", + AzureConfigFile: "/etc/kubernetes/azure.json", + AzureResourceGroup: "", + AzureSubscriptionID: "", + CloudflareProxied: false, + CloudflareCustomHostnames: false, + CloudflareCustomHostnamesMinTLSVersion: "1.0", + CloudflareCustomHostnamesCertificateAuthority: "google", + CloudflareDNSRecordsPerPage: 100, + CloudflareRegionKey: "", + CoreDNSPrefix: "/skydns/", + AkamaiServiceConsumerDomain: "", + AkamaiClientToken: "", + AkamaiClientSecret: "", + AkamaiAccessToken: "", + AkamaiEdgercPath: "", + AkamaiEdgercSection: "", + OCIConfigFile: "/etc/kubernetes/oci.yaml", + OCIZoneScope: "GLOBAL", + OCIZoneCacheDuration: 0 * time.Second, + InMemoryZones: []string{""}, + OVHEndpoint: "ovh-eu", + OVHApiRateLimit: 20, + PDNSServer: "http://localhost:8081", + PDNSServerID: "localhost", + PDNSAPIKey: "", + Policy: "sync", + Registry: "txt", + TXTOwnerID: "default", + TXTPrefix: "", + TXTCacheInterval: 0, + TXTNewFormatOnly: false, + Interval: time.Minute, + MinEventSyncInterval: 5 * time.Second, + Once: false, + DryRun: false, + UpdateEvents: false, + LogFormat: "text", + MetricsAddress: ":7979", + LogLevel: logrus.InfoLevel.String(), + ConnectorSourceServer: "localhost:8080", + ExoscaleAPIEnvironment: "api", + ExoscaleAPIZone: "ch-gva-2", + ExoscaleAPIKey: "", + ExoscaleAPISecret: "", + CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", + CRDSourceKind: "DNSEndpoint", + TransIPAccountName: "", + TransIPPrivateKeyFile: "", + DigitalOceanAPIPageSize: 50, + ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + RFC2136BatchChangeSize: 50, + RFC2136Host: []string{""}, + RFC2136LoadBalancingStrategy: "disabled", + OCPRouterName: "default", + IBMCloudProxied: false, + IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", + TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", + TencentCloudZoneType: "", + WebhookProviderURL: "http://localhost:8888", + WebhookProviderReadTimeout: 5 * time.Second, + WebhookProviderWriteTimeout: 10 * time.Second, } overriddenConfig = &Config{ - APIServerURL: "http://127.0.0.1:8080", - KubeConfig: "/some/path", - RequestTimeout: time.Second * 77, - GlooNamespaces: []string{"gloo-not-system", "gloo-second-system"}, - SkipperRouteGroupVersion: "zalando.org/v2", - Sources: []string{"service", "ingress", "connector"}, - Namespace: "namespace", - IgnoreHostnameAnnotation: true, - IgnoreNonHostNetworkPods: false, - IgnoreIngressTLSSpec: true, - IgnoreIngressRulesSpec: true, - FQDNTemplate: "{{.Name}}.service.example.com", - Compatibility: "mate", - Provider: "google", - GoogleProject: "project", - GoogleBatchChangeSize: 100, - GoogleBatchChangeInterval: time.Second * 2, - GoogleZoneVisibility: "private", - DomainFilter: []string{"example.org", "company.com"}, - ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"}, - RegexDomainFilter: regexp.MustCompile("(example\\.org|company\\.com)$"), - RegexDomainExclusion: regexp.MustCompile("xapi\\.(example\\.org|company\\.com)$"), - ZoneNameFilter: []string{"yapi.example.org", "yapi.company.com"}, - ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, - TargetNetFilter: []string{"10.0.0.0/9", "10.1.0.0/9"}, - ExcludeTargetNets: []string{"1.0.0.0/9", "1.1.0.0/9"}, - AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", - AWSZoneType: "private", - AWSZoneTagFilter: []string{"tag=foo"}, - AWSZoneMatchParent: true, - AWSAssumeRole: "some-other-role", - AWSAssumeRoleExternalID: "pg2000", - AWSBatchChangeSize: 100, - AWSBatchChangeSizeBytes: 16000, - AWSBatchChangeSizeValues: 100, - AWSBatchChangeInterval: time.Second * 2, - AWSEvaluateTargetHealth: false, - AWSAPIRetries: 13, - AWSPreferCNAME: true, - AWSProfiles: []string{"profile1", "profile2"}, - AWSZoneCacheDuration: 10 * time.Second, - AWSSDServiceCleanup: true, - AWSSDCreateTag: map[string]string{"key1": "value1", "key2": "value2"}, - AWSDynamoDBTable: "custom-table", - AzureConfigFile: "azure.json", - AzureResourceGroup: "arg", - AzureSubscriptionID: "arg", - CloudflareProxied: true, - CloudflareDNSRecordsPerPage: 5000, - CloudflareRegionKey: "us", - CoreDNSPrefix: "/coredns/", - AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", - AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46", - AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46", - AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46", - AkamaiEdgercPath: "/home/test/.edgerc", - AkamaiEdgercSection: "default", - OCIConfigFile: "oci.yaml", - OCIZoneScope: "PRIVATE", - OCIZoneCacheDuration: 30 * time.Second, - InMemoryZones: []string{"example.org", "company.com"}, - OVHEndpoint: "ovh-ca", - OVHApiRateLimit: 42, - PDNSServer: "http://ns.example.com:8081", - PDNSServerID: "localhost", - PDNSAPIKey: "some-secret-key", - PDNSSkipTLSVerify: true, - TLSCA: "/path/to/ca.crt", - TLSClientCert: "/path/to/cert.pem", - TLSClientCertKey: "/path/to/key.pem", - PodSourceDomain: "example.org", - Policy: "upsert-only", - Registry: "noop", - TXTOwnerID: "owner-1", - TXTPrefix: "associated-txt-record", - TXTCacheInterval: 12 * time.Hour, - TXTNewFormatOnly: true, - Interval: 10 * time.Minute, - MinEventSyncInterval: 50 * time.Second, - Once: true, - DryRun: true, - UpdateEvents: true, - LogFormat: "json", - MetricsAddress: "127.0.0.1:9099", - LogLevel: logrus.DebugLevel.String(), - ConnectorSourceServer: "localhost:8081", - ExoscaleAPIEnvironment: "api1", - ExoscaleAPIZone: "zone1", - ExoscaleAPIKey: "1", - ExoscaleAPISecret: "2", - CRDSourceAPIVersion: "test.k8s.io/v1alpha1", - CRDSourceKind: "Endpoint", - NS1Endpoint: "https://api.example.com/v1", - NS1IgnoreSSL: true, - TransIPAccountName: "transip", - TransIPPrivateKeyFile: "/path/to/transip.key", - DigitalOceanAPIPageSize: 100, - ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, - RFC2136BatchChangeSize: 100, - RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"}, - RFC2136LoadBalancingStrategy: "round-robin", - IBMCloudProxied: true, - IBMCloudConfigFile: "ibmcloud.json", - TencentCloudConfigFile: "tencent-cloud.json", - TencentCloudZoneType: "private", - WebhookProviderURL: "http://localhost:8888", - WebhookProviderReadTimeout: 5 * time.Second, - WebhookProviderWriteTimeout: 10 * time.Second, + APIServerURL: "http://127.0.0.1:8080", + KubeConfig: "/some/path", + RequestTimeout: time.Second * 77, + GlooNamespaces: []string{"gloo-not-system", "gloo-second-system"}, + SkipperRouteGroupVersion: "zalando.org/v2", + Sources: []string{"service", "ingress", "connector"}, + Namespace: "namespace", + IgnoreHostnameAnnotation: true, + IgnoreNonHostNetworkPods: false, + IgnoreIngressTLSSpec: true, + IgnoreIngressRulesSpec: true, + FQDNTemplate: "{{.Name}}.service.example.com", + Compatibility: "mate", + Provider: "google", + GoogleProject: "project", + GoogleBatchChangeSize: 100, + GoogleBatchChangeInterval: time.Second * 2, + GoogleZoneVisibility: "private", + DomainFilter: []string{"example.org", "company.com"}, + ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"}, + RegexDomainFilter: regexp.MustCompile("(example\\.org|company\\.com)$"), + RegexDomainExclusion: regexp.MustCompile("xapi\\.(example\\.org|company\\.com)$"), + ZoneNameFilter: []string{"yapi.example.org", "yapi.company.com"}, + ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, + TargetNetFilter: []string{"10.0.0.0/9", "10.1.0.0/9"}, + ExcludeTargetNets: []string{"1.0.0.0/9", "1.1.0.0/9"}, + AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", + AWSZoneType: "private", + AWSZoneTagFilter: []string{"tag=foo"}, + AWSZoneMatchParent: true, + AWSAssumeRole: "some-other-role", + AWSAssumeRoleExternalID: "pg2000", + AWSBatchChangeSize: 100, + AWSBatchChangeSizeBytes: 16000, + AWSBatchChangeSizeValues: 100, + AWSBatchChangeInterval: time.Second * 2, + AWSEvaluateTargetHealth: false, + AWSAPIRetries: 13, + AWSPreferCNAME: true, + AWSProfiles: []string{"profile1", "profile2"}, + AWSZoneCacheDuration: 10 * time.Second, + AWSSDServiceCleanup: true, + AWSSDCreateTag: map[string]string{"key1": "value1", "key2": "value2"}, + AWSDynamoDBTable: "custom-table", + AzureConfigFile: "azure.json", + AzureResourceGroup: "arg", + AzureSubscriptionID: "arg", + CloudflareProxied: true, + CloudflareCustomHostnames: true, + CloudflareCustomHostnamesMinTLSVersion: "1.3", + CloudflareCustomHostnamesCertificateAuthority: "google", + CloudflareDNSRecordsPerPage: 5000, + CloudflareRegionKey: "us", + CoreDNSPrefix: "/coredns/", + AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", + AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46", + AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46", + AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46", + AkamaiEdgercPath: "/home/test/.edgerc", + AkamaiEdgercSection: "default", + OCIConfigFile: "oci.yaml", + OCIZoneScope: "PRIVATE", + OCIZoneCacheDuration: 30 * time.Second, + InMemoryZones: []string{"example.org", "company.com"}, + OVHEndpoint: "ovh-ca", + OVHApiRateLimit: 42, + PDNSServer: "http://ns.example.com:8081", + PDNSServerID: "localhost", + PDNSAPIKey: "some-secret-key", + PDNSSkipTLSVerify: true, + TLSCA: "/path/to/ca.crt", + TLSClientCert: "/path/to/cert.pem", + TLSClientCertKey: "/path/to/key.pem", + PodSourceDomain: "example.org", + Policy: "upsert-only", + Registry: "noop", + TXTOwnerID: "owner-1", + TXTPrefix: "associated-txt-record", + TXTCacheInterval: 12 * time.Hour, + TXTNewFormatOnly: true, + Interval: 10 * time.Minute, + MinEventSyncInterval: 50 * time.Second, + Once: true, + DryRun: true, + UpdateEvents: true, + LogFormat: "json", + MetricsAddress: "127.0.0.1:9099", + LogLevel: logrus.DebugLevel.String(), + ConnectorSourceServer: "localhost:8081", + ExoscaleAPIEnvironment: "api1", + ExoscaleAPIZone: "zone1", + ExoscaleAPIKey: "1", + ExoscaleAPISecret: "2", + CRDSourceAPIVersion: "test.k8s.io/v1alpha1", + CRDSourceKind: "Endpoint", + NS1Endpoint: "https://api.example.com/v1", + NS1IgnoreSSL: true, + TransIPAccountName: "transip", + TransIPPrivateKeyFile: "/path/to/transip.key", + DigitalOceanAPIPageSize: 100, + ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, + RFC2136BatchChangeSize: 100, + RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"}, + RFC2136LoadBalancingStrategy: "round-robin", + IBMCloudProxied: true, + IBMCloudConfigFile: "ibmcloud.json", + TencentCloudConfigFile: "tencent-cloud.json", + 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-subscription-id=arg", "--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-region-key=us", "--coredns-prefix=/coredns/", @@ -390,112 +399,115 @@ func TestParseFlags(t *testing.T) { title: "override everything via environment variables", args: []string{}, envVars: map[string]string{ - "EXTERNAL_DNS_SERVER": "http://127.0.0.1:8080", - "EXTERNAL_DNS_KUBECONFIG": "/some/path", - "EXTERNAL_DNS_REQUEST_TIMEOUT": "77s", - "EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other", - "EXTERNAL_DNS_GLOO_NAMESPACE": "gloo-not-system\ngloo-second-system", - "EXTERNAL_DNS_SKIPPER_ROUTEGROUP_GROUPVERSION": "zalando.org/v2", - "EXTERNAL_DNS_SOURCE": "service\ningress\nconnector", - "EXTERNAL_DNS_NAMESPACE": "namespace", - "EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com", - "EXTERNAL_DNS_IGNORE_NON_HOST_NETWORK_PODS": "0", - "EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1", - "EXTERNAL_DNS_IGNORE_INGRESS_TLS_SPEC": "1", - "EXTERNAL_DNS_IGNORE_INGRESS_RULES_SPEC": "1", - "EXTERNAL_DNS_COMPATIBILITY": "mate", - "EXTERNAL_DNS_PROVIDER": "google", - "EXTERNAL_DNS_GOOGLE_PROJECT": "project", - "EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_SIZE": "100", - "EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_INTERVAL": "2s", - "EXTERNAL_DNS_GOOGLE_ZONE_VISIBILITY": "private", - "EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json", - "EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg", - "EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg", - "EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1", - "EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000", - "EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us", - "EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/", - "EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", - "EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46", - "EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46", - "EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46", - "EXTERNAL_DNS_AKAMAI_EDGERC_PATH": "/home/test/.edgerc", - "EXTERNAL_DNS_AKAMAI_EDGERC_SECTION": "default", - "EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml", - "EXTERNAL_DNS_OCI_ZONE_SCOPE": "PRIVATE", - "EXTERNAL_DNS_OCI_ZONES_CACHE_DURATION": "30s", - "EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com", - "EXTERNAL_DNS_OVH_ENDPOINT": "ovh-ca", - "EXTERNAL_DNS_OVH_API_RATE_LIMIT": "42", - "EXTERNAL_DNS_POD_SOURCE_DOMAIN": "example.org", - "EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com", - "EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com", - "EXTERNAL_DNS_REGEX_DOMAIN_FILTER": "(example\\.org|company\\.com)$", - "EXTERNAL_DNS_REGEX_DOMAIN_EXCLUSION": "xapi\\.(example\\.org|company\\.com)$", - "EXTERNAL_DNS_TARGET_NET_FILTER": "10.0.0.0/9\n10.1.0.0/9", - "EXTERNAL_DNS_EXCLUDE_TARGET_NET": "1.0.0.0/9\n1.1.0.0/9", - "EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081", - "EXTERNAL_DNS_PDNS_ID": "localhost", - "EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key", - "EXTERNAL_DNS_PDNS_SKIP_TLS_VERIFY": "1", - "EXTERNAL_DNS_RDNS_ROOT_DOMAIN": "lb.rancher.cloud", - "EXTERNAL_DNS_TLS_CA": "/path/to/ca.crt", - "EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem", - "EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem", - "EXTERNAL_DNS_ZONE_NAME_FILTER": "yapi.example.org\nyapi.company.com", - "EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2", - "EXTERNAL_DNS_AWS_ZONE_TYPE": "private", - "EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo", - "EXTERNAL_DNS_AWS_ZONE_MATCH_PARENT": "true", - "EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role", - "EXTERNAL_DNS_AWS_ASSUME_ROLE_EXTERNAL_ID": "pg2000", - "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100", - "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE_BYTES": "16000", - "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE_VALUES": "100", - "EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s", - "EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0", - "EXTERNAL_DNS_AWS_API_RETRIES": "13", - "EXTERNAL_DNS_AWS_PREFER_CNAME": "true", - "EXTERNAL_DNS_AWS_PROFILE": "profile1\nprofile2", - "EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s", - "EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true", - "EXTERNAL_DNS_AWS_SD_CREATE_TAG": "key1=value1\nkey2=value2", - "EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table", - "EXTERNAL_DNS_POLICY": "upsert-only", - "EXTERNAL_DNS_REGISTRY": "noop", - "EXTERNAL_DNS_TXT_OWNER_ID": "owner-1", - "EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record", - "EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h", - "EXTERNAL_DNS_TXT_NEW_FORMAT_ONLY": "1", - "EXTERNAL_DNS_INTERVAL": "10m", - "EXTERNAL_DNS_MIN_EVENT_SYNC_INTERVAL": "50s", - "EXTERNAL_DNS_ONCE": "1", - "EXTERNAL_DNS_DRY_RUN": "1", - "EXTERNAL_DNS_EVENTS": "1", - "EXTERNAL_DNS_LOG_FORMAT": "json", - "EXTERNAL_DNS_METRICS_ADDRESS": "127.0.0.1:9099", - "EXTERNAL_DNS_LOG_LEVEL": "debug", - "EXTERNAL_DNS_CONNECTOR_SOURCE_SERVER": "localhost:8081", - "EXTERNAL_DNS_EXOSCALE_APIENV": "api1", - "EXTERNAL_DNS_EXOSCALE_APIZONE": "zone1", - "EXTERNAL_DNS_EXOSCALE_APIKEY": "1", - "EXTERNAL_DNS_EXOSCALE_APISECRET": "2", - "EXTERNAL_DNS_CRD_SOURCE_APIVERSION": "test.k8s.io/v1alpha1", - "EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint", - "EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1", - "EXTERNAL_DNS_NS1_IGNORESSL": "1", - "EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip", - "EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key", - "EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100", - "EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nAAAA\nCNAME\nNS", - "EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100", - "EXTERNAL_DNS_RFC2136_LOAD_BALANCING_STRATEGY": "round-robin", - "EXTERNAL_DNS_RFC2136_HOST": "rfc2136-host1\nrfc2136-host2", - "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", + "EXTERNAL_DNS_SERVER": "http://127.0.0.1:8080", + "EXTERNAL_DNS_KUBECONFIG": "/some/path", + "EXTERNAL_DNS_REQUEST_TIMEOUT": "77s", + "EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other", + "EXTERNAL_DNS_GLOO_NAMESPACE": "gloo-not-system\ngloo-second-system", + "EXTERNAL_DNS_SKIPPER_ROUTEGROUP_GROUPVERSION": "zalando.org/v2", + "EXTERNAL_DNS_SOURCE": "service\ningress\nconnector", + "EXTERNAL_DNS_NAMESPACE": "namespace", + "EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com", + "EXTERNAL_DNS_IGNORE_NON_HOST_NETWORK_PODS": "0", + "EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1", + "EXTERNAL_DNS_IGNORE_INGRESS_TLS_SPEC": "1", + "EXTERNAL_DNS_IGNORE_INGRESS_RULES_SPEC": "1", + "EXTERNAL_DNS_COMPATIBILITY": "mate", + "EXTERNAL_DNS_PROVIDER": "google", + "EXTERNAL_DNS_GOOGLE_PROJECT": "project", + "EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_SIZE": "100", + "EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_INTERVAL": "2s", + "EXTERNAL_DNS_GOOGLE_ZONE_VISIBILITY": "private", + "EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json", + "EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg", + "EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg", + "EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1", + "EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES": "1", + "EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_MIN_TLS_VERSION": "1.3", + "EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_CERTIFICATE_AUTHORITY": "google", + "EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000", + "EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us", + "EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/", + "EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", + "EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46", + "EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46", + "EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46", + "EXTERNAL_DNS_AKAMAI_EDGERC_PATH": "/home/test/.edgerc", + "EXTERNAL_DNS_AKAMAI_EDGERC_SECTION": "default", + "EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml", + "EXTERNAL_DNS_OCI_ZONE_SCOPE": "PRIVATE", + "EXTERNAL_DNS_OCI_ZONES_CACHE_DURATION": "30s", + "EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com", + "EXTERNAL_DNS_OVH_ENDPOINT": "ovh-ca", + "EXTERNAL_DNS_OVH_API_RATE_LIMIT": "42", + "EXTERNAL_DNS_POD_SOURCE_DOMAIN": "example.org", + "EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com", + "EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com", + "EXTERNAL_DNS_REGEX_DOMAIN_FILTER": "(example\\.org|company\\.com)$", + "EXTERNAL_DNS_REGEX_DOMAIN_EXCLUSION": "xapi\\.(example\\.org|company\\.com)$", + "EXTERNAL_DNS_TARGET_NET_FILTER": "10.0.0.0/9\n10.1.0.0/9", + "EXTERNAL_DNS_EXCLUDE_TARGET_NET": "1.0.0.0/9\n1.1.0.0/9", + "EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081", + "EXTERNAL_DNS_PDNS_ID": "localhost", + "EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key", + "EXTERNAL_DNS_PDNS_SKIP_TLS_VERIFY": "1", + "EXTERNAL_DNS_RDNS_ROOT_DOMAIN": "lb.rancher.cloud", + "EXTERNAL_DNS_TLS_CA": "/path/to/ca.crt", + "EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem", + "EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem", + "EXTERNAL_DNS_ZONE_NAME_FILTER": "yapi.example.org\nyapi.company.com", + "EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2", + "EXTERNAL_DNS_AWS_ZONE_TYPE": "private", + "EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo", + "EXTERNAL_DNS_AWS_ZONE_MATCH_PARENT": "true", + "EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role", + "EXTERNAL_DNS_AWS_ASSUME_ROLE_EXTERNAL_ID": "pg2000", + "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100", + "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE_BYTES": "16000", + "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE_VALUES": "100", + "EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s", + "EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0", + "EXTERNAL_DNS_AWS_API_RETRIES": "13", + "EXTERNAL_DNS_AWS_PREFER_CNAME": "true", + "EXTERNAL_DNS_AWS_PROFILE": "profile1\nprofile2", + "EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s", + "EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true", + "EXTERNAL_DNS_AWS_SD_CREATE_TAG": "key1=value1\nkey2=value2", + "EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table", + "EXTERNAL_DNS_POLICY": "upsert-only", + "EXTERNAL_DNS_REGISTRY": "noop", + "EXTERNAL_DNS_TXT_OWNER_ID": "owner-1", + "EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record", + "EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h", + "EXTERNAL_DNS_TXT_NEW_FORMAT_ONLY": "1", + "EXTERNAL_DNS_INTERVAL": "10m", + "EXTERNAL_DNS_MIN_EVENT_SYNC_INTERVAL": "50s", + "EXTERNAL_DNS_ONCE": "1", + "EXTERNAL_DNS_DRY_RUN": "1", + "EXTERNAL_DNS_EVENTS": "1", + "EXTERNAL_DNS_LOG_FORMAT": "json", + "EXTERNAL_DNS_METRICS_ADDRESS": "127.0.0.1:9099", + "EXTERNAL_DNS_LOG_LEVEL": "debug", + "EXTERNAL_DNS_CONNECTOR_SOURCE_SERVER": "localhost:8081", + "EXTERNAL_DNS_EXOSCALE_APIENV": "api1", + "EXTERNAL_DNS_EXOSCALE_APIZONE": "zone1", + "EXTERNAL_DNS_EXOSCALE_APIKEY": "1", + "EXTERNAL_DNS_EXOSCALE_APISECRET": "2", + "EXTERNAL_DNS_CRD_SOURCE_APIVERSION": "test.k8s.io/v1alpha1", + "EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint", + "EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1", + "EXTERNAL_DNS_NS1_IGNORESSL": "1", + "EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip", + "EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key", + "EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100", + "EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nAAAA\nCNAME\nNS", + "EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100", + "EXTERNAL_DNS_RFC2136_LOAD_BALANCING_STRATEGY": "round-robin", + "EXTERNAL_DNS_RFC2136_HOST": "rfc2136-host1\nrfc2136-host2", + "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, }, diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 041af077c..09db7dc71 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -64,6 +64,12 @@ var recordTypeProxyNotSupported = map[string]bool{ "SRV": true, } +type CustomHostnamesConfig struct { + Enabled bool + MinTLSVersion string + CertificateAuthority string +} + var recordTypeCustomHostnameSupported = map[string]bool{ "A": true, "CNAME": true, @@ -149,20 +155,22 @@ type CloudFlareProvider struct { provider.BaseProvider Client cloudFlareDNS // only consider hosted zones managing domains ending in this suffix - domainFilter endpoint.DomainFilter - zoneIDFilter provider.ZoneIDFilter - proxiedByDefault bool - DryRun bool - DNSRecordsPerPage int - RegionKey string + domainFilter endpoint.DomainFilter + zoneIDFilter provider.ZoneIDFilter + proxiedByDefault bool + CustomHostnamesConfig CustomHostnamesConfig + DryRun bool + DNSRecordsPerPage int + RegionKey string } // cloudFlareChange differentiates between ChangActions type cloudFlareChange struct { - Action string - ResourceRecord cloudflare.DNSRecord - RegionalHostname cloudflare.RegionalHostname - CustomHostname cloudflare.CustomHostname + Action string + ResourceRecord cloudflare.DNSRecord + RegionalHostname cloudflare.RegionalHostname + CustomHostname cloudflare.CustomHostname + CustomHostnamePrev string } // 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. -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 var ( config *cloudflare.API @@ -225,13 +233,14 @@ func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter prov } provider := &CloudFlareProvider{ // Client: config, - Client: zoneService{config}, - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - proxiedByDefault: proxiedByDefault, - DryRun: dryRun, - DNSRecordsPerPage: dnsRecordsPerPage, - RegionKey: regionKey, + Client: zoneService{config}, + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + proxiedByDefault: proxiedByDefault, + CustomHostnamesConfig: customHostnamesConfig, + DryRun: dryRun, + DNSRecordsPerPage: dnsRecordsPerPage, + RegionKey: regionKey, } return provider, nil } @@ -319,7 +328,7 @@ func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Cha for _, endpoint := range changes.Create { 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) 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 { - cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, desired, a)) + cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, desired, a, current)) } 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 _, 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 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 for _, change := range changes { logFields := log.Fields{ @@ -394,34 +393,37 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud } 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 recordTypeCustomHostnameSupported[change.ResourceRecord.Type] { - chID, oldCh := p.getCustomHostnameIDbyOrigin(chs, change.ResourceRecord.Name) - if chID == "" && change.CustomHostname.Hostname != "" { - log.WithFields(logFields).Infof("Adding custom hostname %v", change.CustomHostname.Hostname) - _, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname) - if chErr != nil { - failedChange = true - log.WithFields(logFields).Errorf("failed to add custom hostname %v: %v", change.CustomHostname.Hostname, chErr) + prevCh := change.CustomHostnamePrev + newCh := change.CustomHostname.Hostname + if prevCh != "" { + prevChID, _ := p.getCustomHostnameOrigin(chs, prevCh) + if prevChID != "" && prevCh != newCh { + log.WithFields(logFields).Infof("Removing previous custom hostname %v/%v", prevChID, prevCh) + 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) - chErr := p.Client.DeleteCustomHostname(ctx, zoneID, chID) - if chErr != nil { - failedChange = true - log.WithFields(logFields).Errorf("failed to remove custom hostname %v: %v", change.CustomHostname.Hostname, chErr) - } - } else if chID != "" && change.CustomHostname.Hostname != "" && oldCh != change.CustomHostname.Hostname { - 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) + } + if newCh != "" { + if prevCh != newCh { + log.WithFields(logFields).Infof("Adding custom hostname %v", newCh) + _, chErr := p.Client.CreateCustomHostname(ctx, zoneID, change.CustomHostname) + if chErr != nil { + failedChange = true + log.WithFields(logFields).Errorf("failed to add custom hostname %v: %v", newCh, chErr) + } } } } @@ -455,14 +457,19 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud failedChange = true 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 == "" { + log.WithFields(logFields).Infof("Custom hostname %v not found", change.CustomHostname.Hostname) continue } chErr := p.Client.DeleteCustomHostname(ctx, zoneID, chID) if chErr != nil { 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 { recordParam := getCreateDNSRecordParam(*change) @@ -471,7 +478,16 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud failedChange = true 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) + 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) if chErr != nil { failedChange = true @@ -537,16 +553,16 @@ func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record 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 { - if zoneCh.CustomOriginServer == origin { - return zoneCh.ID, zoneCh.Hostname + if zoneCh.Hostname == hostname { + return zoneCh.ID, zoneCh.CustomOriginServer } } 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 proxied := shouldBeProxied(endpoint, p.proxiedByDefault) @@ -554,6 +570,19 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi ttl = int(endpoint.RecordTTL) } 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{ Action: action, ResourceRecord: cloudflare.DNSRecord{ @@ -573,19 +602,8 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi RegionKey: p.RegionKey, CreatedOn: &dt, }, - CustomHostname: cloudflare.CustomHostname{ - Hostname: getEndpointCustomHostname(endpoint), - CustomOriginServer: endpoint.DNSName, - SSL: &cloudflare.CustomHostnameSSL{ - Type: "dv", - Method: "http", - CertificateAuthority: "google", - BundleMethod: "ubiquitous", - Settings: cloudflare.CustomHostnameSSLSettings{ - MinTLSVersion: "1.0", - }, - }, - }, + CustomHostnamePrev: customHostnamePrev, + CustomHostname: newCustomHostname, } } @@ -618,6 +636,9 @@ func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Contex // listCustomHostnamesWithPagination performs automatic pagination of results on requests to cloudflare.CustomHostnames func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Context, zoneID string) ([]cloudflare.CustomHostname, error) { + if !p.CustomHostnamesConfig.Enabled { + return nil, nil + } var chs []cloudflare.CustomHostname resultInfo := cloudflare.ResultInfo{Page: 1} for { @@ -643,6 +664,18 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte 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 { 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 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 { customOriginServers[c.CustomOriginServer] = c.Hostname } @@ -721,9 +754,10 @@ func groupByNameAndTypeWithCustomHostnames(records []cloudflare.DNSRecord, chs [ if ep == nil { 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 { - ep.WithProviderSpecific(source.CloudflareCustomHostnameKey, customHostname) + ep = ep.WithProviderSpecific(source.CloudflareCustomHostnameKey, customHostname) } endpoints = append(endpoints, ep) diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go index f754ebc69..1d1ee4833 100644 --- a/provider/cloudflare/cloudflare_test.go +++ b/provider/cloudflare/cloudflare_test.go @@ -26,10 +26,12 @@ import ( "testing" cloudflare "github.com/cloudflare/cloudflare-go" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/maxatome/go-testdeep/td" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" ) @@ -112,6 +114,7 @@ func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord { switch params := rp.(type) { case cloudflare.CreateDNSRecordParams: return cloudflare.DNSRecord{ + ID: params.ID, Name: params.Name, TTL: params.TTL, Proxied: params.Proxied, @@ -120,6 +123,7 @@ func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord { } case cloudflare.UpdateDNSRecordParams: return cloudflare.DNSRecord{ + ID: params.ID, Name: params.Name, TTL: params.TTL, 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) { recordData := getDNSRecordFromRecordParams(rp) + if recordData.ID == "" { + recordData.ID = generateDNSRecordID(recordData.Type, recordData.Name, recordData.Content) + } m.Actions = append(m.Actions, MockAction{ Name: "Create", ZoneId: rc.Identifier, - RecordId: rp.ID, + RecordId: recordData.ID, RecordData: recordData, }) if zone, ok := m.Records[rc.Identifier]; ok { - zone[rp.ID] = recordData + zone[recordData.ID] = recordData } if recordData.Name == "newerror.bar.com" { @@ -156,6 +167,10 @@ func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, rc *cloudflar result := []cloudflare.DNSRecord{} if zone, ok := m.Records[rc.Identifier]; ok { 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) } } @@ -200,6 +215,9 @@ func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, rc *cloudfla }) if zone, ok := m.Records[rc.Identifier]; 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 } } @@ -226,7 +244,11 @@ func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, rc *cloudfla }) if zone, ok := m.Records[rc.Identifier]; ok { if _, ok := zone[recordID]; ok { + name := zone[recordID].Name delete(zone, recordID) + if strings.HasPrefix(name, "newerror-delete-") { + return errors.New("failed to delete erroring DNS record") + } 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) { 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 != "" { 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{} if zone, ok := m.customHostnames[zoneID]; ok { 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) } } @@ -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) { + 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 { 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 } @@ -273,9 +308,11 @@ func (m *mockCloudFlareClient) DeleteCustomHostname(ctx context.Context, zoneID if zone, ok := m.customHostnames[zoneID]; ok { if _, ok := zone[customHostnameID]; ok { delete(zone, customHostnameID) - return nil } } + if customHostnameID == "ID-newerror-delete.foo.fancybar.com" { + return fmt.Errorf("Invalid custom hostname to delete") + } return nil } @@ -342,6 +379,16 @@ func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) ( 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{}) { t.Helper() @@ -399,9 +446,11 @@ func TestCloudflareA(t *testing.T) { AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"), Type: "A", Name: "bar.com", Content: "127.0.0.1", @@ -410,9 +459,11 @@ func TestCloudflareA(t *testing.T) { }, }, { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.2"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("A", "bar.com", "127.0.0.2"), Type: "A", Name: "bar.com", Content: "127.0.0.2", @@ -436,9 +487,11 @@ func TestCloudflareCname(t *testing.T) { AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("CNAME", "cname.bar.com", "google.com"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("CNAME", "cname.bar.com", "google.com"), Type: "CNAME", Name: "cname.bar.com", Content: "google.com", @@ -447,9 +500,11 @@ func TestCloudflareCname(t *testing.T) { }, }, { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("CNAME", "cname.bar.com", "facebook.com"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("CNAME", "cname.bar.com", "facebook.com"), Type: "CNAME", Name: "cname.bar.com", Content: "facebook.com", @@ -474,9 +529,11 @@ func TestCloudflareCustomTTL(t *testing.T) { AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("A", "ttl.bar.com", "127.0.0.1"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("A", "ttl.bar.com", "127.0.0.1"), Type: "A", Name: "ttl.bar.com", Content: "127.0.0.1", @@ -500,9 +557,11 @@ func TestCloudflareProxiedDefault(t *testing.T) { AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"), Type: "A", Name: "bar.com", Content: "127.0.0.1", @@ -532,9 +591,11 @@ func TestCloudflareProxiedOverrideTrue(t *testing.T) { AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"), Type: "A", Name: "bar.com", Content: "127.0.0.1", @@ -564,9 +625,11 @@ func TestCloudflareProxiedOverrideFalse(t *testing.T) { AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"), Type: "A", Name: "bar.com", Content: "127.0.0.1", @@ -596,9 +659,11 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) { AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("A", "bar.com", "127.0.0.1"), Type: "A", Name: "bar.com", Content: "127.0.0.1", @@ -631,11 +696,12 @@ func TestCloudflareSetProxied(t *testing.T) { } for _, testCase := range testCases { + target := "127.0.0.1" endpoints := []*endpoint.Endpoint{ { RecordType: testCase.recordType, DNSName: testCase.domain, - Targets: endpoint.Targets{"127.0.0.1"}, + Targets: endpoint.Targets{target}, ProviderSpecific: endpoint.ProviderSpecific{ endpoint.ProviderSpecificProperty{ 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{ { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: expectedID, RecordData: cloudflare.DNSRecord{ + ID: expectedID, Type: testCase.recordType, Name: testCase.domain, Content: "127.0.0.1", @@ -795,7 +863,8 @@ func TestCloudflareProvider(t *testing.T) { false, true, 5000, - "") + "", + CustomHostnamesConfig{Enabled: false}) if err != nil { t.Errorf("should not fail, %s", err) } @@ -812,7 +881,8 @@ func TestCloudflareProvider(t *testing.T) { false, true, 5000, - "") + "", + CustomHostnamesConfig{Enabled: false}) if err != nil { t.Errorf("should not fail, %s", err) } @@ -826,7 +896,8 @@ func TestCloudflareProvider(t *testing.T) { false, true, 5000, - "") + "", + CustomHostnamesConfig{Enabled: false}) if err != nil { t.Errorf("should not fail, %s", err) } @@ -839,7 +910,8 @@ func TestCloudflareProvider(t *testing.T) { false, true, 5000, - "") + "", + CustomHostnamesConfig{Enabled: false}) if err == nil { t.Errorf("expected to fail") } @@ -877,9 +949,11 @@ func TestCloudflareApplyChanges(t *testing.T) { td.Cmp(t, client.Actions, []MockAction{ { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("", "new.bar.com", "target"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("", "new.bar.com", "target"), Name: "new.bar.com", Content: "target", TTL: 1, @@ -887,9 +961,11 @@ func TestCloudflareApplyChanges(t *testing.T) { }, }, { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("", "foobar.bar.com", "target-new"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("", "foobar.bar.com", "target-new"), Name: "foobar.bar.com", Content: "target-new", TTL: 1, @@ -1366,9 +1442,11 @@ func TestCloudflareComplexUpdate(t *testing.T) { RecordId: "2345678901", }, { - Name: "Create", - ZoneId: "001", + Name: "Create", + ZoneId: "001", + RecordId: generateDNSRecordID("A", "foobar.bar.com", "2.3.4.5"), RecordData: cloudflare.DNSRecord{ + ID: generateDNSRecordID("A", "foobar.bar.com", "2.3.4.5"), Name: "foobar.bar.com", Type: "A", Content: "2.3.4.5", @@ -1381,6 +1459,7 @@ func TestCloudflareComplexUpdate(t *testing.T) { ZoneId: "001", RecordId: "1234567890", RecordData: cloudflare.DNSRecord{ + ID: "1234567890", Name: "foobar.bar.com", Type: "A", Content: "1.2.3.4", @@ -1451,7 +1530,14 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) { func TestCloudFlareProvider_Region(t *testing.T) { _ = os.Setenv("CF_API_TOKEN", "abc123def") _ = os.Setenv("CF_API_EMAIL", "test@test.com") - 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 { t.Fatal(err) } @@ -1482,7 +1568,14 @@ func TestCloudFlareProvider_updateDataLocalizationRegionalHostnameParams(t *test func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { _ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx") _ = 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 { t.Fatal(err) } @@ -1492,7 +1585,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { 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" { 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) { - client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{ - "001": ExampleDomain, - }) - provider := &CloudFlareProvider{ - Client: client, +func TestCloudflareZoneRecordsFail(t *testing.T) { + client := &mockCloudFlareClient{ + Zones: map[string]string{ + "newerror-001": "bar.com", + }, + 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() - records, err := provider.Records(ctx) - if err != nil { - t.Errorf("should not fail, %s", err) + _, err := failingProvider.Records(ctx) + if err == nil { + 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"}) + 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 { Name string Endpoints []*endpoint.Endpoint 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", Endpoints: []*endpoint.Endpoint{ { 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, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), Labels: endpoint.Labels{}, @@ -1682,7 +2095,7 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) { Endpoints: []*endpoint.Endpoint{ { 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, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), 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", Endpoints: []*endpoint.Endpoint{ + { 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, RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), 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 { + 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) @@ -1726,17 +2175,106 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) { err = provider.ApplyChanges(context.Background(), planned.Changes) 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") 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 { - _, ch := provider.getCustomHostnameIDbyOrigin(chs, k) - assert.Equal(t, v, ch) + for expectedOrigin, expectedCustomHostname := range tc.ExpectedCustomHostnames { + _, ch := provider.getCustomHostnameIDbyCustomHostnameAndOrigin(chs, expectedCustomHostname, expectedOrigin) + 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") +}