diff --git a/docs/flags.md b/docs/flags.md index e817662410..f2f2f3f06b 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -86,6 +86,7 @@ | `--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-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/main.go b/main.go index 25f2b5c151..79212d03a4 100644 --- a/main.go +++ b/main.go @@ -250,7 +250,7 @@ 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.CloudflareCustomHostnames, cfg.DryRun, cfg.CloudflareDNSRecordsPerPage, cfg.CloudflareRegionKey) 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 d4781909fd..f241db64f3 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -110,6 +110,7 @@ type Config struct { AzureActiveDirectoryAuthorityHost string AzureZonesCacheDuration time.Duration CloudflareProxied bool + CloudflareCustomHostnames bool CloudflareDNSRecordsPerPage int CloudflareRegionKey string CoreDNSPrefix string @@ -515,6 +516,7 @@ 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-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 05d008d1b5..c3fb002b12 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -74,6 +74,7 @@ var ( AzureResourceGroup: "", AzureSubscriptionID: "", CloudflareProxied: false, + CloudflareCustomHostnames: false, CloudflareDNSRecordsPerPage: 100, CloudflareRegionKey: "", CoreDNSPrefix: "/skydns/", @@ -179,6 +180,7 @@ var ( AzureResourceGroup: "arg", AzureSubscriptionID: "arg", CloudflareProxied: true, + CloudflareCustomHostnames: true, CloudflareDNSRecordsPerPage: 5000, CloudflareRegionKey: "us", CoreDNSPrefix: "/coredns/", @@ -287,6 +289,7 @@ func TestParseFlags(t *testing.T) { "--azure-resource-group=arg", "--azure-subscription-id=arg", "--cloudflare-proxied", + "--cloudflare-custom-hostnames", "--cloudflare-dns-records-per-page=5000", "--cloudflare-region-key=us", "--coredns-prefix=/coredns/", @@ -413,6 +416,7 @@ func TestParseFlags(t *testing.T) { "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_DNS_RECORDS_PER_PAGE": "5000", "EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us", "EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/", diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 5d373f52ff..bd2dce1b97 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -55,9 +55,6 @@ var proxyEnabled *bool = boolPtr(true) // proxyDisabled is a pointer to a bool false showing the record should not be proxied through cloudflare var proxyDisabled *bool = boolPtr(false) -// customHostnamesEnabled controls if custom hostnames feature is used, will self-disable if SaaS API fails to authenticate -var customHostnamesEnabled bool = true - var recordTypeProxyNotSupported = map[string]bool{ "LOC": true, "MX": true, @@ -155,6 +152,7 @@ type CloudFlareProvider struct { domainFilter endpoint.DomainFilter zoneIDFilter provider.ZoneIDFilter proxiedByDefault bool + customHostnames bool DryRun bool DNSRecordsPerPage int RegionKey string @@ -205,7 +203,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, customHostnames bool, dryRun bool, dnsRecordsPerPage int, regionKey string) (*CloudFlareProvider, error) { // initialize via chosen auth method and returns new API object var ( config *cloudflare.API @@ -233,6 +231,7 @@ func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter prov domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, proxiedByDefault: proxiedByDefault, + customHostnames: customHostnames, DryRun: dryRun, DNSRecordsPerPage: dnsRecordsPerPage, RegionKey: regionKey, @@ -303,9 +302,13 @@ func (p *CloudFlareProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, return nil, err } - chs, chErr := p.listCustomHostnamesWithPagination(ctx, zone.ID) - if chErr != nil { - return nil, chErr + chs := []cloudflare.CustomHostname{} + if p.customHostnames { + var chErr error + chs, chErr = p.listCustomHostnamesWithPagination(ctx, zone.ID) + if chErr != nil { + return nil, chErr + } } // As CloudFlare does not support "sets" of targets, but instead returns @@ -565,9 +568,26 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi ttl = int(endpoint.RecordTTL) } dt := time.Now() + customHostnamePrev := "" - if current != nil { - customHostnamePrev = getEndpointCustomHostname(current) + newCustomHostname := cloudflare.CustomHostname{} + if p.customHostnames { + if current != nil { + customHostnamePrev = getEndpointCustomHostname(current) + } + newCustomHostname = 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", + }, + }, + } } return &cloudFlareChange{ Action: action, @@ -589,19 +609,7 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi CreatedOn: &dt, }, CustomHostnamePrev: customHostnamePrev, - 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", - }, - }, - }, + CustomHostname: newCustomHostname, } } @@ -634,7 +642,7 @@ 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 !customHostnamesEnabled { + if !p.customHostnames { return nil, nil } var chs []cloudflare.CustomHostname @@ -647,11 +655,6 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte if apiErr.ClientRateLimited() || apiErr.StatusCode >= http.StatusInternalServerError { // Handle rate limit error as a soft error return nil, provider.NewSoftError(err) - } else if apiErr.ErrorMessageContains("Authentication error") { - // "Cloudflare for SaaS" fails toauthenticate, log once and don't use cusotm hostnames - log.Debugf("\"Cloudflare for SaaS\" is not enabled, custom hostnames will not be collected.") - customHostnamesEnabled = false - return nil, nil } } log.Errorf("zone %s failed to fetch custom hostnames. Please check if \"Cloudflare for SaaS\" is enabled and API key permissions, %v", zoneID, err) @@ -719,7 +722,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 } @@ -746,6 +749,7 @@ func groupByNameAndTypeWithCustomHostnames(records []cloudflare.DNSRecord, chs [ continue } 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 = ep.WithProviderSpecific(source.CloudflareCustomHostnameKey, customHostname) } diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go index 21d18a98ec..f367f720b7 100644 --- a/provider/cloudflare/cloudflare_test.go +++ b/provider/cloudflare/cloudflare_test.go @@ -861,6 +861,7 @@ func TestCloudflareProvider(t *testing.T) { endpoint.NewDomainFilter([]string{"bar.com"}), provider.NewZoneIDFilter([]string{""}), false, + false, true, 5000, "") @@ -878,6 +879,7 @@ func TestCloudflareProvider(t *testing.T) { endpoint.NewDomainFilter([]string{"bar.com"}), provider.NewZoneIDFilter([]string{""}), false, + false, true, 5000, "") @@ -892,6 +894,7 @@ func TestCloudflareProvider(t *testing.T) { endpoint.NewDomainFilter([]string{"bar.com"}), provider.NewZoneIDFilter([]string{""}), false, + false, true, 5000, "") @@ -905,6 +908,7 @@ func TestCloudflareProvider(t *testing.T) { endpoint.NewDomainFilter([]string{"bar.com"}), provider.NewZoneIDFilter([]string{""}), false, + false, true, 5000, "") @@ -1526,7 +1530,7 @@ 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, false, 50, "us") if err != nil { t.Fatal(err) } @@ -1557,7 +1561,7 @@ 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, false, 50, "us") if err != nil { t.Fatal(err) } @@ -1705,7 +1709,8 @@ func TestCloudflareZoneRecordsFail(t *testing.T) { customHostnames: map[string]map[string]cloudflare.CustomHostname{}, } failingProvider := &CloudFlareProvider{ - Client: client, + Client: client, + customHostnames: true, } ctx := context.Background() @@ -1718,7 +1723,8 @@ func TestCloudflareZoneRecordsFail(t *testing.T) { func TestCloudflareDNSRecordsOperationsFail(t *testing.T) { client := NewMockCloudFlareClient() provider := &CloudFlareProvider{ - Client: client, + Client: client, + customHostnames: true, } ctx := context.Background() domainFilter := endpoint.NewDomainFilter([]string{"bar.com"}) @@ -1831,7 +1837,8 @@ func TestCloudflareDNSRecordsOperationsFail(t *testing.T) { func TestCloudflareCustomHostnameOperations(t *testing.T) { client := NewMockCloudFlareClient() provider := &CloudFlareProvider{ - Client: client, + Client: client, + customHostnames: true, } ctx := context.Background() domainFilter := endpoint.NewDomainFilter([]string{"bar.com"}) @@ -2172,7 +2179,8 @@ func TestCloudflareCustomHostnameOperations(t *testing.T) { func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) { client := NewMockCloudFlareClient() provider := &CloudFlareProvider{ - Client: client, + Client: client, + customHostnames: true, } ctx := context.Background() zoneID := "001"