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

improve test coverage

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

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

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

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

markdown lint

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

View File

@ -87,6 +87,9 @@
| `--tencent-cloud-config-file="/etc/kubernetes/tencent-cloud.json"` | When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud) |
| `--tencent-cloud-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 |

View File

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

13
main.go
View File

@ -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":

View File

@ -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)

View File

@ -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,
},

View File

@ -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)

View File

@ -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")
}