From 0f7f2602a12f2c6ccc9e03d5dd70ff06f0e4cf18 Mon Sep 17 00:00:00 2001 From: Joshua Reese Date: Fri, 16 May 2025 16:06:08 -0500 Subject: [PATCH 01/33] Correctly collect existing records from all zones in RFC2136 provider. Fixes https://github.com/kubernetes-sigs/external-dns/issues/5261 --- provider/rfc2136/rfc2136.go | 2 +- provider/rfc2136/rfc2136_test.go | 25 +++++++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/provider/rfc2136/rfc2136.go b/provider/rfc2136/rfc2136.go index 8b1b58d53..037e80d2d 100644 --- a/provider/rfc2136/rfc2136.go +++ b/provider/rfc2136/rfc2136.go @@ -318,7 +318,7 @@ func (r *rfc2136Provider) List() ([]dns.RR, error) { } // If records were fetched successfully, break out of the loop if len(records) > 0 { - return records, nil + break } } diff --git a/provider/rfc2136/rfc2136_test.go b/provider/rfc2136/rfc2136_test.go index 8bdff70cd..f8edaa512 100644 --- a/provider/rfc2136/rfc2136_test.go +++ b/provider/rfc2136/rfc2136_test.go @@ -152,7 +152,24 @@ func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelo outChan := make(chan *dns.Envelope) go func() { for _, e := range r.output { - outChan <- e + + var responseEnvelope *dns.Envelope + for _, record := range e.RR { + for _, q := range m.Question { + if strings.HasSuffix(record.Header().Name, q.Name) { + if responseEnvelope == nil { + responseEnvelope = &dns.Envelope{} + } + responseEnvelope.RR = append(responseEnvelope.RR, record) + break + } + } + } + + if responseEnvelope == nil { + continue + } + outChan <- responseEnvelope } close(outChan) }() @@ -160,7 +177,7 @@ func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelo return outChan, nil } -func createRfc2136StubProvider(stub *rfc2136Stub) (provider.Provider, error) { +func createRfc2136StubProvider(stub *rfc2136Stub, zoneNames ...string) (provider.Provider, error) { tlsConfig := TLSConfig{ UseTLS: false, SkipTLSVerify: false, @@ -168,7 +185,7 @@ func createRfc2136StubProvider(stub *rfc2136Stub) (provider.Provider, error) { ClientCertFilePath: "", ClientCertKeyFilePath: "", } - return NewRfc2136Provider([]string{""}, 0, nil, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, false, "", "", "", 50, tlsConfig, "", stub) + return NewRfc2136Provider([]string{""}, 0, zoneNames, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, false, "", "", "", 50, tlsConfig, "", stub) } func createRfc2136StubProviderWithHosts(stub *rfc2136Stub) (provider.Provider, error) { @@ -505,7 +522,7 @@ func TestRfc2136GetRecords(t *testing.T) { }) assert.NoError(t, err) - provider, err := createRfc2136StubProvider(stub) + provider, err := createRfc2136StubProvider(stub, "barfoo.com", "foo.com", "bar.com", "foobar.com") assert.NoError(t, err) recs, err := provider.Records(context.Background()) From 993debe4765dd3157b40a5a194fecdfa98c65a59 Mon Sep 17 00:00:00 2001 From: "yuzeng.zyx" Date: Tue, 20 May 2025 13:41:29 +0800 Subject: [PATCH 02/33] docs: add alibaba cloud provider reference --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b4eebfd14..6e1c58064 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz - [TencentCloud DNSPod](https://cloud.tencent.com/product/cns) - [Plural](https://www.plural.sh/) - [Pi-hole](https://pi-hole.net/) +- [Alibaba Cloud Public DNS](https://www.alibabacloud.com/help/en/dns/alibaba-cloud-dns) +- [Alibaba Cloud DNS PrivateZone](https://www.alibabacloud.com/help/en/dns/introduction-to-intranet-analysis) ExternalDNS is, by default, aware of the records it is managing, therefore it can safely manage non-empty hosted zones. We strongly encourage you to set `--txt-owner-id` to a unique value that doesn't change for the lifetime of your cluster. @@ -127,7 +129,7 @@ We define the following stability levels for providers: The following table clarifies the current status of the providers according to the aforementioned stability levels: | Provider | Status | Maintainers | -| ------------------------------- | ------ | ---------------- | +|---------------------------------| ------ |------------------| | Google Cloud DNS | Stable | | | AWS Route 53 | Stable | | | AWS Cloud Map | Beta | | @@ -154,6 +156,7 @@ The following table clarifies the current status of the providers according to t | TencentCloud | Alpha | @Hyzhou | | Plural | Alpha | @michaeljguarino | | Pi-hole | Alpha | @tinyzimmer | +| Alibaba Cloud DNS | Alpha | | ## Kubernetes version compatibility From dae8c5370e4095b6596bfe0a8aff41d77238026f Mon Sep 17 00:00:00 2001 From: "yuzeng.zyx" Date: Tue, 20 May 2025 16:13:47 +0800 Subject: [PATCH 03/33] changed alibabacloud dns link --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e1c58064..64c445c91 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,7 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz - [TencentCloud DNSPod](https://cloud.tencent.com/product/cns) - [Plural](https://www.plural.sh/) - [Pi-hole](https://pi-hole.net/) -- [Alibaba Cloud Public DNS](https://www.alibabacloud.com/help/en/dns/alibaba-cloud-dns) -- [Alibaba Cloud DNS PrivateZone](https://www.alibabacloud.com/help/en/dns/introduction-to-intranet-analysis) +- [Alibaba Cloud DNS](https://www.alibabacloud.com/help/en/dns) ExternalDNS is, by default, aware of the records it is managing, therefore it can safely manage non-empty hosted zones. We strongly encourage you to set `--txt-owner-id` to a unique value that doesn't change for the lifetime of your cluster. From 40dea167e74fc5c379afe7ce950308a2f80870eb Mon Sep 17 00:00:00 2001 From: Valentin Flaux <38909103+vflaux@users.noreply.github.com> Date: Thu, 22 May 2025 13:16:20 +0200 Subject: [PATCH 04/33] chore: reduce config validation cyclop reduce cyclomatic complexity of validation.ValidateConfig function --- pkg/apis/externaldns/validation/validation.go | 121 ++++++++++------- .../externaldns/validation/validation_test.go | 124 ++++++++++++++++++ 2 files changed, 196 insertions(+), 49 deletions(-) diff --git a/pkg/apis/externaldns/validation/validation.go b/pkg/apis/externaldns/validation/validation.go index a5d06fa9b..df8edccc1 100644 --- a/pkg/apis/externaldns/validation/validation.go +++ b/pkg/apis/externaldns/validation/validation.go @@ -28,57 +28,13 @@ import ( // ValidateConfig performs validation on the Config object func ValidateConfig(cfg *externaldns.Config) error { // TODO: Should probably return field.ErrorList - if cfg.LogFormat != "text" && cfg.LogFormat != "json" { - return fmt.Errorf("unsupported log format: %s", cfg.LogFormat) - } - if len(cfg.Sources) == 0 { - return errors.New("no sources specified") - } - if cfg.Provider == "" { - return errors.New("no provider specified") + + if err := preValidateConfig(cfg); err != nil { + return err } - // Azure provider specific validations - if cfg.Provider == "azure" { - if cfg.AzureConfigFile == "" { - return errors.New("no Azure config file specified") - } - } - - // Akamai provider specific validations - if cfg.Provider == "akamai" { - if cfg.AkamaiServiceConsumerDomain == "" && cfg.AkamaiEdgercPath != "" { - return errors.New("no Akamai ServiceConsumerDomain specified") - } - if cfg.AkamaiClientToken == "" && cfg.AkamaiEdgercPath != "" { - return errors.New("no Akamai client token specified") - } - if cfg.AkamaiClientSecret == "" && cfg.AkamaiEdgercPath != "" { - return errors.New("no Akamai client secret specified") - } - if cfg.AkamaiAccessToken == "" && cfg.AkamaiEdgercPath != "" { - return errors.New("no Akamai access token specified") - } - } - - if cfg.Provider == "rfc2136" { - if cfg.RFC2136MinTTL < 0 { - return errors.New("TTL specified for rfc2136 is negative") - } - - if cfg.RFC2136Insecure && cfg.RFC2136GSSTSIG { - return errors.New("--rfc2136-insecure and --rfc2136-gss-tsig are mutually exclusive arguments") - } - - if cfg.RFC2136GSSTSIG { - if cfg.RFC2136KerberosPassword == "" || cfg.RFC2136KerberosUsername == "" || cfg.RFC2136KerberosRealm == "" { - return errors.New("--rfc2136-kerberos-realm, --rfc2136-kerberos-username, and --rfc2136-kerberos-password are required when specifying --rfc2136-gss-tsig option") - } - } - - if cfg.RFC2136BatchChangeSize < 1 { - return errors.New("batch size specified for rfc2136 cannot be less than 1") - } + if err := validateConfigForProvider(cfg); err != nil { + return err } if cfg.IgnoreHostnameAnnotation && cfg.FQDNTemplate == "" { @@ -95,3 +51,70 @@ func ValidateConfig(cfg *externaldns.Config) error { } return nil } + +func preValidateConfig(cfg *externaldns.Config) error { + if cfg.LogFormat != "text" && cfg.LogFormat != "json" { + return fmt.Errorf("unsupported log format: %s", cfg.LogFormat) + } + if len(cfg.Sources) == 0 { + return errors.New("no sources specified") + } + if cfg.Provider == "" { + return errors.New("no provider specified") + } + return nil +} + +func validateConfigForProvider(cfg *externaldns.Config) error { + switch cfg.Provider { + case "azure": + return validateConfigForAzure(cfg) + case "akamai": + return validateConfigForAkamai(cfg) + case "rfc2136": + return validateConfigForRfc2136(cfg) + default: + return nil + } +} + +func validateConfigForAzure(cfg *externaldns.Config) error { + if cfg.AzureConfigFile == "" { + return errors.New("no Azure config file specified") + } + return nil +} + +func validateConfigForAkamai(cfg *externaldns.Config) error { + if cfg.AkamaiServiceConsumerDomain == "" && cfg.AkamaiEdgercPath != "" { + return errors.New("no Akamai ServiceConsumerDomain specified") + } + if cfg.AkamaiClientToken == "" && cfg.AkamaiEdgercPath != "" { + return errors.New("no Akamai client token specified") + } + if cfg.AkamaiClientSecret == "" && cfg.AkamaiEdgercPath != "" { + return errors.New("no Akamai client secret specified") + } + if cfg.AkamaiAccessToken == "" && cfg.AkamaiEdgercPath != "" { + return errors.New("no Akamai access token specified") + } + return nil +} + +func validateConfigForRfc2136(cfg *externaldns.Config) error { + if cfg.RFC2136MinTTL < 0 { + return errors.New("TTL specified for rfc2136 is negative") + } + if cfg.RFC2136Insecure && cfg.RFC2136GSSTSIG { + return errors.New("--rfc2136-insecure and --rfc2136-gss-tsig are mutually exclusive arguments") + } + if cfg.RFC2136GSSTSIG { + if cfg.RFC2136KerberosPassword == "" || cfg.RFC2136KerberosUsername == "" || cfg.RFC2136KerberosRealm == "" { + return errors.New("--rfc2136-kerberos-realm, --rfc2136-kerberos-username, and --rfc2136-kerberos-password are required when specifying --rfc2136-gss-tsig option") + } + } + if cfg.RFC2136BatchChangeSize < 1 { + return errors.New("batch size specified for rfc2136 cannot be less than 1") + } + return nil +} diff --git a/pkg/apis/externaldns/validation/validation_test.go b/pkg/apis/externaldns/validation/validation_test.go index 72dd45460..480220e68 100644 --- a/pkg/apis/externaldns/validation/validation_test.go +++ b/pkg/apis/externaldns/validation/validation_test.go @@ -50,6 +50,28 @@ func TestValidateFlags(t *testing.T) { cfg = newValidConfig(t) cfg.Provider = "" require.Error(t, ValidateConfig(cfg)) + + cfg = newValidConfig(t) + cfg.IgnoreHostnameAnnotation = true + cfg.FQDNTemplate = "" + require.Error(t, ValidateConfig(cfg)) + + cfg = newValidConfig(t) + cfg.TXTPrefix = "foo" + cfg.TXTSuffix = "bar" + require.Error(t, ValidateConfig(cfg)) + + cfg = newValidConfig(t) + cfg.LabelFilter = "foo" + require.NoError(t, ValidateConfig(cfg)) + + cfg = newValidConfig(t) + cfg.LabelFilter = "foo=bar" + require.NoError(t, ValidateConfig(cfg)) + + cfg = newValidConfig(t) + cfg.LabelFilter = "#invalid-selector" + require.Error(t, ValidateConfig(cfg)) } func newValidConfig(t *testing.T) *externaldns.Config { @@ -227,3 +249,105 @@ func TestValidateGoodRfc2136GssTsigConfig(t *testing.T) { assert.NoError(t, err) } } + +func TestValidateBadAkamaiConfig(t *testing.T) { + invalidAkamaiConfigs := []*externaldns.Config{ + { + LogFormat: "json", + Sources: []string{"test-source"}, + Provider: "akamai", + AkamaiClientToken: "test-token", + AkamaiClientSecret: "test-secret", + AkamaiAccessToken: "test-access-token", + AkamaiEdgercPath: "/path/to/edgerc", + // Missing AkamaiServiceConsumerDomain + }, + { + LogFormat: "json", + Sources: []string{"test-source"}, + Provider: "akamai", + AkamaiServiceConsumerDomain: "test-domain", + AkamaiClientSecret: "test-secret", + AkamaiAccessToken: "test-access-token", + AkamaiEdgercPath: "/path/to/edgerc", + // Missing AkamaiClientToken + }, + { + LogFormat: "json", + Sources: []string{"test-source"}, + Provider: "akamai", + AkamaiServiceConsumerDomain: "test-domain", + AkamaiClientToken: "test-token", + AkamaiAccessToken: "test-access-token", + AkamaiEdgercPath: "/path/to/edgerc", + // Missing AkamaiClientSecret + }, + { + LogFormat: "json", + Sources: []string{"test-source"}, + Provider: "akamai", + AkamaiServiceConsumerDomain: "test-domain", + AkamaiClientToken: "test-token", + AkamaiClientSecret: "test-secret", + AkamaiEdgercPath: "/path/to/edgerc", + // Missing AkamaiAccessToken + }, + } + + for _, cfg := range invalidAkamaiConfigs { + err := ValidateConfig(cfg) + assert.Error(t, err) + } +} + +func TestValidateGoodAkamaiConfig(t *testing.T) { + validAkamaiConfigs := []*externaldns.Config{ + { + LogFormat: "json", + Sources: []string{"test-source"}, + Provider: "akamai", + AkamaiServiceConsumerDomain: "test-domain", + AkamaiClientToken: "test-token", + AkamaiClientSecret: "test-secret", + AkamaiAccessToken: "test-access-token", + AkamaiEdgercPath: "/path/to/edgerc", + }, + { + LogFormat: "json", + Sources: []string{"test-source"}, + Provider: "akamai", + // All Akamai fields can be empty if AkamaiEdgercPath is not specified + }, + } + + for _, cfg := range validAkamaiConfigs { + err := ValidateConfig(cfg) + assert.NoError(t, err) + } +} + +func TestValidateBadAzureConfig(t *testing.T) { + cfg := externaldns.NewConfig() + + cfg.LogFormat = "json" + cfg.Sources = []string{"test-source"} + cfg.Provider = "azure" + // AzureConfigFile is empty + + err := ValidateConfig(cfg) + + assert.Error(t, err) +} + +func TestValidateGoodAzureConfig(t *testing.T) { + cfg := externaldns.NewConfig() + + cfg.LogFormat = "json" + cfg.Sources = []string{"test-source"} + cfg.Provider = "azure" + cfg.AzureConfigFile = "/path/to/azure.json" + + err := ValidateConfig(cfg) + + assert.NoError(t, err) +} From 7d307ad79214795c16a7b8596efbc7ec9fae5102 Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Thu, 22 May 2025 09:34:20 -0400 Subject: [PATCH 05/33] feat(cloudflare): change defaults from google to empty string for certificateAuthority to not set the CertificateAuthority field in customHostnamesConfig --- pkg/apis/externaldns/types.go | 4 ++-- pkg/apis/externaldns/types_test.go | 4 ++-- provider/cloudflare/cloudflare.go | 9 +++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index df96f71bd..bf9d57d3c 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -254,7 +254,7 @@ var defaultConfig = &Config{ CFAPIEndpoint: "", CFPassword: "", CFUsername: "", - CloudflareCustomHostnamesCertificateAuthority: "google", + CloudflareCustomHostnamesCertificateAuthority: "", CloudflareCustomHostnames: false, CloudflareCustomHostnamesMinTLSVersion: "1.0", CloudflareDNSRecordsPerPage: 100, @@ -538,7 +538,7 @@ func App(cfg *Config) *kingpin.Application { 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-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Cerrtificate Authority will be used by default. (default: none, options: google, ssl_com, lets_encrypt, none)").Default("").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("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index fe13da1a5..3737c2d1a 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -76,7 +76,7 @@ var ( CloudflareProxied: false, CloudflareCustomHostnames: false, CloudflareCustomHostnamesMinTLSVersion: "1.0", - CloudflareCustomHostnamesCertificateAuthority: "google", + CloudflareCustomHostnamesCertificateAuthority: "", CloudflareDNSRecordsPerPage: 100, CloudflareDNSRecordsComment: "", CloudflareRegionKey: "", @@ -188,7 +188,7 @@ var ( CloudflareProxied: true, CloudflareCustomHostnames: true, CloudflareCustomHostnamesMinTLSVersion: "1.3", - CloudflareCustomHostnamesCertificateAuthority: "google", + CloudflareCustomHostnamesCertificateAuthority: "", CloudflareDNSRecordsPerPage: 5000, CloudflareRegionKey: "us", CoreDNSPrefix: "/coredns/", diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 8980adc99..2ab5cac64 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -969,15 +969,20 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte } func getCustomHostnamesSSLOptions(customHostnamesConfig CustomHostnamesConfig) *cloudflare.CustomHostnameSSL { - return &cloudflare.CustomHostnameSSL{ + ssl := &cloudflare.CustomHostnameSSL{ Type: "dv", Method: "http", - CertificateAuthority: customHostnamesConfig.CertificateAuthority, BundleMethod: "ubiquitous", Settings: cloudflare.CustomHostnameSSLSettings{ MinTLSVersion: customHostnamesConfig.MinTLSVersion, }, } + // Set CertificateAuthority if provided + // We're not able to set it at all (even with a blank) if you're not on an enterprise plan + if customHostnamesConfig.CertificateAuthority != "" { + ssl.CertificateAuthority = customHostnamesConfig.CertificateAuthority + } + return ssl } func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool { From 29a6345d5ade68e5fff2c98a66425feab37ec264 Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Thu, 22 May 2025 09:44:22 -0400 Subject: [PATCH 06/33] docs(cloudflare): add section to describe selecting a custom CA --- docs/tutorials/cloudflare.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index abfb95eed..043db64ff 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -320,6 +320,8 @@ See [Cloudflare for Platforms](https://developers.cloudflare.com/cloudflare-for- This feature is disabled by default and supports the `--cloudflare-custom-hostnames-min-tls-version` and `--cloudflare-custom-hostnames-certificate-authority` flags. +`--cloudflare-custom-hostnames-certificate-authority` defaults to not selecting a CA. If a specific CA is required use this flag to select one. + 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. From e7db933249e518c4727159ea4c3347e7f7ec24ec Mon Sep 17 00:00:00 2001 From: upsaurav12 Date: Thu, 22 May 2025 23:37:00 +0530 Subject: [PATCH 07/33] test(provider/gandi): bumped to 100% coverage --- provider/gandi/gandi_test.go | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/provider/gandi/gandi_test.go b/provider/gandi/gandi_test.go index e3b4cca19..dc50d1ab2 100644 --- a/provider/gandi/gandi_test.go +++ b/provider/gandi/gandi_test.go @@ -452,6 +452,46 @@ func TestGandiProvider_ApplyChangesWithUnknownDomainDoesNoUpdate(t *testing.T) { }) } +func TestGandiProvider_ApplyChangesConvertsApexDomain(t *testing.T) { + changes := &plan.Changes{} + mockedClient := &mockGandiClient{} + mockedProvider := &GandiProvider{ + DomainClient: mockedClient, + LiveDNSClient: mockedClient, + } + + // Add a change where DNSName equals the zone name (apex domain) + changes.Create = []*endpoint.Endpoint{ + { + DNSName: "example.com", // Matches the zone name + Targets: endpoint.Targets{"192.168.0.1"}, + RecordType: "A", + RecordTTL: 666, + }, + } + + err := mockedProvider.ApplyChanges(context.Background(), changes) + if err != nil { + t.Errorf("should not fail, %s", err) + } + + td.Cmp(t, mockedClient.Actions, []MockAction{ + { + Name: "ListDomains", + }, + { + Name: "CreateDomainRecord", + FQDN: "example.com", + Record: livedns.DomainRecord{ + RrsetType: endpoint.RecordTypeA, + RrsetName: "@", + RrsetValues: []string{"192.168.0.1"}, + RrsetTTL: 666, + }, + }, + }) +} + func TestGandiProvider_FailingCases(t *testing.T) { changes := &plan.Changes{} changes.Create = []*endpoint.Endpoint{{DNSName: "test2.example.com", Targets: endpoint.Targets{"192.168.0.1"}, RecordType: "A", RecordTTL: 666}} From 7c0881477a6a96bd4063280a18dbaad5180c9b5d Mon Sep 17 00:00:00 2001 From: vflaux <38909103+vflaux@users.noreply.github.com> Date: Fri, 23 May 2025 16:42:37 +0200 Subject: [PATCH 08/33] chore(cloudflare): move regional services logic to dedicated file (#5329) * cloudflare: move regional logic to dedicated file * cloudflare: actions as enum type --- provider/cloudflare/cloudflare.go | 208 +--------- provider/cloudflare/cloudflare_regional.go | 210 ++++++++++ .../cloudflare/cloudflare_regional_test.go | 373 ++++++++++++++++++ provider/cloudflare/cloudflare_test.go | 334 +--------------- 4 files changed, 602 insertions(+), 523 deletions(-) create mode 100644 provider/cloudflare/cloudflare_regional.go create mode 100644 provider/cloudflare/cloudflare_regional_test.go diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 8980adc99..03898ec18 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -38,13 +38,15 @@ import ( "sigs.k8s.io/external-dns/source/annotations" ) +type changeAction int + const ( // cloudFlareCreate is a ChangeAction enum value - cloudFlareCreate = "CREATE" + cloudFlareCreate changeAction = iota // cloudFlareDelete is a ChangeAction enum value - cloudFlareDelete = "DELETE" + cloudFlareDelete // cloudFlareUpdate is a ChangeAction enum value - cloudFlareUpdate = "UPDATE" + cloudFlareUpdate // defaultTTL 1 = automatic defaultTTL = 1 @@ -53,6 +55,16 @@ const ( paidZoneMaxCommentLength = 500 ) +var changeActionNames = map[changeAction]string{ + cloudFlareCreate: "CREATE", + cloudFlareDelete: "DELETE", + cloudFlareUpdate: "UPDATE", +} + +func (action changeAction) String() string { + return changeActionNames[action] +} + // We have to use pointers to bools now, as the upstream cloudflare-go library requires them // see: https://github.com/cloudflare/cloudflare-go/pull/595 @@ -78,11 +90,6 @@ type CustomHostnameIndex struct { type CustomHostnamesMap map[CustomHostnameIndex]cloudflare.CustomHostname -type DataLocalizationRegionalHostnameChange struct { - Action string - cloudflare.RegionalHostname -} - var recordTypeProxyNotSupported = map[string]bool{ "LOC": true, "MX": true, @@ -103,12 +110,6 @@ var recordTypeCustomHostnameSupported = map[string]bool{ "CNAME": true, } -var recordTypeRegionalHostnameSupported = map[string]bool{ - "A": true, - "AAAA": true, - "CNAME": true, -} - // cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly. type cloudFlareDNS interface { UserDetails(ctx context.Context) (cloudflare.User, error) @@ -157,20 +158,6 @@ func (z zoneService) UpdateDNSRecord(ctx context.Context, rc *cloudflare.Resourc return err } -func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error { - _, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp) - return err -} - -func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error { - _, err := z.service.UpdateDataLocalizationRegionalHostname(ctx, rc, rp) - return err -} - -func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error { - return z.service.DeleteDataLocalizationRegionalHostname(ctx, rc, hostname) -} - func (z zoneService) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error { return z.service.DeleteDNSRecord(ctx, rc, recordID) } @@ -250,7 +237,7 @@ type CloudFlareProvider struct { // cloudFlareChange differentiates between ChangActions type cloudFlareChange struct { - Action string + Action changeAction ResourceRecord cloudflare.DNSRecord RegionalHostname cloudflare.RegionalHostname CustomHostnames map[string]cloudflare.CustomHostname @@ -273,22 +260,6 @@ func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams } } -// createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in -func createDataLocalizationRegionalHostnameParams(rhc DataLocalizationRegionalHostnameChange) cloudflare.CreateDataLocalizationRegionalHostnameParams { - return cloudflare.CreateDataLocalizationRegionalHostnameParams{ - Hostname: rhc.Hostname, - RegionKey: rhc.RegionKey, - } -} - -// updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in -func updateDataLocalizationRegionalHostnameParams(rhc DataLocalizationRegionalHostnameChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams { - return cloudflare.UpdateDataLocalizationRegionalHostnameParams{ - Hostname: rhc.Hostname, - RegionKey: rhc.RegionKey, - } -} - // getCreateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordParams { return cloudflare.CreateDNSRecordParams{ @@ -538,129 +509,6 @@ func (p *CloudFlareProvider) submitCustomHostnameChanges(ctx context.Context, zo return !failedChange } -// submitDataLocalizationRegionalHostnameChanges applies a set of data localization regional hostname changes, returns false if it fails -func (p *CloudFlareProvider) submitDataLocalizationRegionalHostnameChanges(ctx context.Context, changes []DataLocalizationRegionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool { - failedChange := false - - for _, change := range changes { - logFields := log.Fields{ - "hostname": change.Hostname, - "region_key": change.RegionKey, - "action": change.Action, - "zone": resourceContainer.Identifier, - } - log.WithFields(logFields).Info("Changing regional hostname") - switch change.Action { - case cloudFlareCreate: - log.WithFields(logFields).Debug("Creating regional hostname") - if p.DryRun { - continue - } - regionalHostnameParam := createDataLocalizationRegionalHostnameParams(change) - err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam) - if err != nil { - var apiErr *cloudflare.Error - if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict { - log.WithFields(logFields).Debug("Regional hostname already exists, updating instead") - params := updateDataLocalizationRegionalHostnameParams(change) - err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, params) - if err != nil { - failedChange = true - log.WithFields(logFields).Errorf("failed to update regional hostname: %v", err) - } - continue - } - failedChange = true - log.WithFields(logFields).Errorf("failed to create regional hostname: %v", err) - } - case cloudFlareUpdate: - log.WithFields(logFields).Debug("Updating regional hostname") - if p.DryRun { - continue - } - regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(change) - err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam) - if err != nil { - var apiErr *cloudflare.Error - if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { - log.WithFields(logFields).Debug("Regional hostname not does not exists, creating instead") - params := createDataLocalizationRegionalHostnameParams(change) - err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, params) - if err != nil { - failedChange = true - log.WithFields(logFields).Errorf("failed to create regional hostname: %v", err) - } - continue - } - failedChange = true - log.WithFields(logFields).Errorf("failed to update regional hostname: %v", err) - } - case cloudFlareDelete: - log.WithFields(logFields).Debug("Deleting regional hostname") - if p.DryRun { - continue - } - err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, change.Hostname) - if err != nil { - var apiErr *cloudflare.Error - if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { - log.WithFields(logFields).Debug("Regional hostname does not exists, nothing to do") - continue - } - failedChange = true - log.WithFields(logFields).Errorf("failed to delete regional hostname: %v", err) - } - } - } - - return !failedChange -} - -// dataLocalizationRegionalHostnamesChanges processes a slice of cloudFlare changes and consolidates them -// into a list of data localization regional hostname changes. -// returns nil if no changes are needed -func dataLocalizationRegionalHostnamesChanges(changes []*cloudFlareChange) ([]DataLocalizationRegionalHostnameChange, error) { - regionalHostnameChanges := make(map[string]DataLocalizationRegionalHostnameChange) - for _, change := range changes { - if change.RegionalHostname.Hostname == "" { - continue - } - if change.RegionalHostname.RegionKey == "" { - return nil, fmt.Errorf("region key is empty for regional hostname %q", change.RegionalHostname.Hostname) - } - regionalHostname, ok := regionalHostnameChanges[change.RegionalHostname.Hostname] - switch change.Action { - case cloudFlareCreate, cloudFlareUpdate: - if !ok { - regionalHostnameChanges[change.RegionalHostname.Hostname] = DataLocalizationRegionalHostnameChange{ - Action: change.Action, - RegionalHostname: change.RegionalHostname, - } - continue - } - if regionalHostname.RegionKey != change.RegionalHostname.RegionKey { - return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.Hostname, regionalHostname.RegionKey, change.RegionalHostname.RegionKey) - } - if (change.Action == cloudFlareUpdate && regionalHostname.Action != cloudFlareUpdate) || - regionalHostname.Action == cloudFlareDelete { - regionalHostnameChanges[change.RegionalHostname.Hostname] = DataLocalizationRegionalHostnameChange{ - Action: cloudFlareUpdate, - RegionalHostname: change.RegionalHostname, - } - } - case cloudFlareDelete: - if !ok { - regionalHostnameChanges[change.RegionalHostname.Hostname] = DataLocalizationRegionalHostnameChange{ - Action: cloudFlareDelete, - RegionalHostname: change.RegionalHostname, - } - continue - } - } - } - return slices.Collect(maps.Values(regionalHostnameChanges)), nil -} - // submitChanges takes a zone and a collection of Changes and sends them as a single transaction. func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloudFlareChange) error { // return early if there is nothing to change @@ -844,7 +692,7 @@ func (p *CloudFlareProvider) newCustomHostname(customHostname string, origin str } } -func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.Endpoint, target string, current *endpoint.Endpoint) *cloudFlareChange { +func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoint.Endpoint, target string, current *endpoint.Endpoint) *cloudFlareChange { ttl := defaultTTL proxied := shouldBeProxied(ep, p.proxiedByDefault) @@ -862,13 +710,6 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End newCustomHostnames[v] = p.newCustomHostname(v, ep.DNSName) } } - regionalHostname := cloudflare.RegionalHostname{} - if regionKey := getRegionKey(ep, p.RegionKey); regionKey != "" { - regionalHostname = cloudflare.RegionalHostname{ - Hostname: ep.DNSName, - RegionKey: regionKey, - } - } // Load comment from program flag comment := p.DNSRecordsConfig.Comment @@ -893,7 +734,7 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End Content: target, Comment: comment, }, - RegionalHostname: regionalHostname, + RegionalHostname: p.regionalHostname(ep), CustomHostnamesPrev: prevCustomHostnames, CustomHostnames: newCustomHostnames, } @@ -1001,19 +842,6 @@ func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool { return proxied } -func getRegionKey(endpoint *endpoint.Endpoint, defaultRegionKey string) string { - if !recordTypeRegionalHostnameSupported[endpoint.RecordType] { - return "" - } - - for _, v := range endpoint.ProviderSpecific { - if v.Name == annotations.CloudflareRegionKey { - return v.Value - } - } - return defaultRegionKey -} - func getEndpointCustomHostnames(ep *endpoint.Endpoint) []string { for _, v := range ep.ProviderSpecific { if v.Name == annotations.CloudflareCustomHostnameKey { diff --git a/provider/cloudflare/cloudflare_regional.go b/provider/cloudflare/cloudflare_regional.go new file mode 100644 index 000000000..f2b355952 --- /dev/null +++ b/provider/cloudflare/cloudflare_regional.go @@ -0,0 +1,210 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudflare + +import ( + "context" + "errors" + "fmt" + "maps" + "net/http" + "slices" + + "github.com/cloudflare/cloudflare-go" + log "github.com/sirupsen/logrus" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/source/annotations" +) + +var recordTypeRegionalHostnameSupported = map[string]bool{ + "A": true, + "AAAA": true, + "CNAME": true, +} + +type regionalHostnameChange struct { + action changeAction + cloudflare.RegionalHostname +} + +func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error { + _, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp) + return err +} + +func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error { + _, err := z.service.UpdateDataLocalizationRegionalHostname(ctx, rc, rp) + return err +} + +func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error { + return z.service.DeleteDataLocalizationRegionalHostname(ctx, rc, hostname) +} + +// createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in +func createDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.CreateDataLocalizationRegionalHostnameParams { + return cloudflare.CreateDataLocalizationRegionalHostnameParams{ + Hostname: rhc.Hostname, + RegionKey: rhc.RegionKey, + } +} + +// updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in +func updateDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams { + return cloudflare.UpdateDataLocalizationRegionalHostnameParams{ + Hostname: rhc.Hostname, + RegionKey: rhc.RegionKey, + } +} + +// submitDataLocalizationRegionalHostnameChanges applies a set of data localization regional hostname changes, returns false if it fails +func (p *CloudFlareProvider) submitDataLocalizationRegionalHostnameChanges(ctx context.Context, rhChanges []regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool { + failedChange := false + + for _, rhChange := range rhChanges { + logFields := log.Fields{ + "hostname": rhChange.Hostname, + "region_key": rhChange.RegionKey, + "action": rhChange.action, + "zone": resourceContainer.Identifier, + } + log.WithFields(logFields).Info("Changing regional hostname") + switch rhChange.action { + case cloudFlareCreate: + log.WithFields(logFields).Debug("Creating regional hostname") + if p.DryRun { + continue + } + regionalHostnameParam := createDataLocalizationRegionalHostnameParams(rhChange) + err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam) + if err != nil { + var apiErr *cloudflare.Error + if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict { + log.WithFields(logFields).Debug("Regional hostname already exists, updating instead") + params := updateDataLocalizationRegionalHostnameParams(rhChange) + err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, params) + if err != nil { + failedChange = true + log.WithFields(logFields).Errorf("failed to update regional hostname: %v", err) + } + continue + } + failedChange = true + log.WithFields(logFields).Errorf("failed to create regional hostname: %v", err) + } + case cloudFlareUpdate: + log.WithFields(logFields).Debug("Updating regional hostname") + if p.DryRun { + continue + } + regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(rhChange) + err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam) + if err != nil { + var apiErr *cloudflare.Error + if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { + log.WithFields(logFields).Debug("Regional hostname not does not exists, creating instead") + params := createDataLocalizationRegionalHostnameParams(rhChange) + err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, params) + if err != nil { + failedChange = true + log.WithFields(logFields).Errorf("failed to create regional hostname: %v", err) + } + continue + } + failedChange = true + log.WithFields(logFields).Errorf("failed to update regional hostname: %v", err) + } + case cloudFlareDelete: + log.WithFields(logFields).Debug("Deleting regional hostname") + if p.DryRun { + continue + } + err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, rhChange.Hostname) + if err != nil { + var apiErr *cloudflare.Error + if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { + log.WithFields(logFields).Debug("Regional hostname does not exists, nothing to do") + continue + } + failedChange = true + log.WithFields(logFields).Errorf("failed to delete regional hostname: %v", err) + } + } + } + + return !failedChange +} + +func (p *CloudFlareProvider) regionalHostname(ep *endpoint.Endpoint) cloudflare.RegionalHostname { + if p.RegionKey == "" || !recordTypeRegionalHostnameSupported[ep.RecordType] { + return cloudflare.RegionalHostname{} + } + regionKey := p.RegionKey + if epRegionKey, exists := ep.GetProviderSpecificProperty(annotations.CloudflareRegionKey); exists { + regionKey = epRegionKey + } + return cloudflare.RegionalHostname{ + Hostname: ep.DNSName, + RegionKey: regionKey, + } +} + +// dataLocalizationRegionalHostnamesChanges processes a slice of cloudFlare changes and consolidates them +// into a list of data localization regional hostname changes. +// returns nil if no changes are needed +func dataLocalizationRegionalHostnamesChanges(changes []*cloudFlareChange) ([]regionalHostnameChange, error) { + regionalHostnameChanges := make(map[string]regionalHostnameChange) + for _, change := range changes { + if change.RegionalHostname.Hostname == "" { + continue + } + if change.RegionalHostname.RegionKey == "" { + return nil, fmt.Errorf("region key is empty for regional hostname %q", change.RegionalHostname.Hostname) + } + regionalHostname, ok := regionalHostnameChanges[change.RegionalHostname.Hostname] + switch change.Action { + case cloudFlareCreate, cloudFlareUpdate: + if !ok { + regionalHostnameChanges[change.RegionalHostname.Hostname] = regionalHostnameChange{ + action: change.Action, + RegionalHostname: change.RegionalHostname, + } + continue + } + if regionalHostname.RegionKey != change.RegionalHostname.RegionKey { + return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.Hostname, regionalHostname.RegionKey, change.RegionalHostname.RegionKey) + } + if (change.Action == cloudFlareUpdate && regionalHostname.action != cloudFlareUpdate) || + regionalHostname.action == cloudFlareDelete { + regionalHostnameChanges[change.RegionalHostname.Hostname] = regionalHostnameChange{ + action: cloudFlareUpdate, + RegionalHostname: change.RegionalHostname, + } + } + case cloudFlareDelete: + if !ok { + regionalHostnameChanges[change.RegionalHostname.Hostname] = regionalHostnameChange{ + action: cloudFlareDelete, + RegionalHostname: change.RegionalHostname, + } + continue + } + } + } + return slices.Collect(maps.Values(regionalHostnameChanges)), nil +} diff --git a/provider/cloudflare/cloudflare_regional_test.go b/provider/cloudflare/cloudflare_regional_test.go new file mode 100644 index 000000000..f8273a601 --- /dev/null +++ b/provider/cloudflare/cloudflare_regional_test.go @@ -0,0 +1,373 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudflare + +import ( + "reflect" + "slices" + "testing" + + "github.com/cloudflare/cloudflare-go" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/external-dns/endpoint" +) + +func Test_regionalHostname(t *testing.T) { + type args struct { + endpoint *endpoint.Endpoint + defaultRegionKey string + } + tests := []struct { + name string + args args + want cloudflare.RegionalHostname + }{ + { + name: "no region key", + args: args{ + endpoint: &endpoint.Endpoint{ + RecordType: "A", + DNSName: "example.com", + }, + defaultRegionKey: "", + }, + want: cloudflare.RegionalHostname{}, + }, + { + name: "default region key", + args: args{ + endpoint: &endpoint.Endpoint{ + RecordType: "A", + DNSName: "example.com", + }, + defaultRegionKey: "us", + }, + want: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "us", + }, + }, + { + name: "endpoint with region key", + args: args{ + endpoint: &endpoint.Endpoint{ + RecordType: "A", + DNSName: "example.com", + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", + Value: "eu", + }, + }, + }, + defaultRegionKey: "us", + }, + want: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "eu", + }, + }, + { + name: "endpoint with empty region key", + args: args{ + endpoint: &endpoint.Endpoint{ + RecordType: "A", + DNSName: "example.com", + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", + Value: "", + }, + }, + }, + defaultRegionKey: "us", + }, + want: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "", + }, + }, + { + name: "unsupported record type", + args: args{ + endpoint: &endpoint.Endpoint{ + RecordType: "TXT", + DNSName: "example.com", + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", + Value: "eu", + }, + }, + }, + defaultRegionKey: "us", + }, + want: cloudflare.RegionalHostname{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := CloudFlareProvider{RegionKey: tt.args.defaultRegionKey} + got := p.regionalHostname(tt.args.endpoint) + assert.Equal(t, got, tt.want) + }) + } +} + +func Test_dataLocalizationRegionalHostnamesChanges(t *testing.T) { + cmpDataLocalizationRegionalHostnameChange := func(i, j regionalHostnameChange) int { + if i.action == j.action { + return 0 + } + if i.Hostname < j.Hostname { + return -1 + } + return 1 + } + type args struct { + changes []*cloudFlareChange + } + tests := []struct { + name string + args args + want []regionalHostnameChange + wantErr bool + }{ + { + name: "empty input", + args: args{ + changes: []*cloudFlareChange{}, + }, + want: nil, + wantErr: false, + }, + { + name: "changes without RegionalHostname", + args: args{ + changes: []*cloudFlareChange{ + { + Action: cloudFlareCreate, + ResourceRecord: cloudflare.DNSRecord{ + Name: "example.com", + }, + RegionalHostname: cloudflare.RegionalHostname{}, // Empty + }, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "change with empty RegionKey", + args: args{ + changes: []*cloudFlareChange{ + { + Action: cloudFlareCreate, + ResourceRecord: cloudflare.DNSRecord{ + Name: "example.com", + }, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "", // Empty region key + }, + }, + }, + }, + wantErr: true, + }, + { + name: "conflicting region keys", + args: args{ + changes: []*cloudFlareChange{ + { + Action: cloudFlareCreate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "eu", + }, + }, + { + Action: cloudFlareCreate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "us", // Different region key for same hostname + }, + }, + }, + }, + wantErr: true, + }, + { + name: "update takes precedence over create & delete", + args: args{ + changes: []*cloudFlareChange{ + { + Action: cloudFlareCreate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "eu", + }, + }, + { + Action: cloudFlareUpdate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "eu", + }, + }, + { + Action: cloudFlareDelete, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "eu", + }, + }, + }, + }, + want: []regionalHostnameChange{ + { + action: cloudFlareUpdate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "eu", + }, + }, + }, + wantErr: false, + }, + { + name: "create after delete becomes update", + args: args{ + changes: []*cloudFlareChange{ + { + Action: cloudFlareDelete, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "eu", + }, + }, + { + Action: cloudFlareCreate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "eu", + }, + }, + }, + }, + want: []regionalHostnameChange{ + { + action: cloudFlareUpdate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example.com", + RegionKey: "eu", + }, + }, + }, + wantErr: false, + }, + { + name: "consolidate mixed actions for different hostnames", + args: args{ + changes: []*cloudFlareChange{ + { + Action: cloudFlareCreate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example1.com", + RegionKey: "eu", + }, + }, + { + Action: cloudFlareUpdate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example2.com", + RegionKey: "us", + }, + }, + { + Action: cloudFlareDelete, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example3.com", + RegionKey: "ap", + }, + }, + // duplicated actions + { + Action: cloudFlareCreate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example1.com", + RegionKey: "eu", + }, + }, + { + Action: cloudFlareUpdate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example2.com", + RegionKey: "us", + }, + }, + { + Action: cloudFlareDelete, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example3.com", + RegionKey: "ap", + }, + }, + }, + }, + want: []regionalHostnameChange{ + { + action: cloudFlareCreate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example1.com", + RegionKey: "eu", + }, + }, + { + action: cloudFlareUpdate, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example2.com", + RegionKey: "us", + }, + }, + { + action: cloudFlareDelete, + RegionalHostname: cloudflare.RegionalHostname{ + Hostname: "example3.com", + RegionKey: "ap", + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := dataLocalizationRegionalHostnamesChanges(tt.args.changes) + if (err != nil) != tt.wantErr { + t.Errorf("dataLocalizationRegionalHostnamesChanges() error = %v, wantErr %v", err, tt.wantErr) + return + } + slices.SortFunc(got, cmpDataLocalizationRegionalHostnameChange) + slices.SortFunc(tt.want, cmpDataLocalizationRegionalHostnameChange) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("dataLocalizationRegionalHostnamesChanges() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go index 893b315fe..97c20bb4d 100644 --- a/provider/cloudflare/cloudflare_test.go +++ b/provider/cloudflare/cloudflare_test.go @@ -21,13 +21,12 @@ import ( "errors" "fmt" "os" - "reflect" "slices" "sort" "strings" "testing" - cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/cloudflare-go" "github.com/maxatome/go-testdeep/td" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -2987,337 +2986,6 @@ func TestCloudflareListCustomHostnamesWithPagionation(t *testing.T) { assert.Len(t, chs, CustomHostnamesNumber) } -func Test_getRegionKey(t *testing.T) { - type args struct { - endpoint *endpoint.Endpoint - defaultRegionKey string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "no region key", - args: args{ - endpoint: &endpoint.Endpoint{ - RecordType: "A", - }, - defaultRegionKey: "", - }, - want: "", - }, - { - name: "default region key", - args: args{ - endpoint: &endpoint.Endpoint{ - RecordType: "A", - }, - defaultRegionKey: "us", - }, - want: "us", - }, - { - name: "endpoint with region key", - args: args{ - endpoint: &endpoint.Endpoint{ - RecordType: "A", - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "eu", - }, - }, - }, - defaultRegionKey: "us", - }, - want: "eu", - }, - { - name: "endpoint with empty region key", - args: args{ - endpoint: &endpoint.Endpoint{ - RecordType: "A", - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "", - }, - }, - }, - defaultRegionKey: "us", - }, - want: "", - }, - { - name: "unsupported record type", - args: args{ - endpoint: &endpoint.Endpoint{ - RecordType: "TXT", - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "eu", - }, - }, - }, - defaultRegionKey: "us", - }, - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := getRegionKey(tt.args.endpoint, tt.args.defaultRegionKey); got != tt.want { - t.Errorf("getRegionKey() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_dataLocalizationRegionalHostnamesChanges(t *testing.T) { - cmpDataLocalizationRegionalHostnameChange := func(i, j DataLocalizationRegionalHostnameChange) int { - if i.Action == j.Action { - return 0 - } - if i.Hostname < j.Hostname { - return -1 - } - return 1 - } - type args struct { - changes []*cloudFlareChange - } - tests := []struct { - name string - args args - want []DataLocalizationRegionalHostnameChange - wantErr bool - }{ - { - name: "empty input", - args: args{ - changes: []*cloudFlareChange{}, - }, - want: nil, - wantErr: false, - }, - { - name: "changes without RegionalHostname", - args: args{ - changes: []*cloudFlareChange{ - { - Action: cloudFlareCreate, - ResourceRecord: cloudflare.DNSRecord{ - Name: "example.com", - }, - RegionalHostname: cloudflare.RegionalHostname{}, // Empty - }, - }, - }, - want: nil, - wantErr: false, - }, - { - name: "change with empty RegionKey", - args: args{ - changes: []*cloudFlareChange{ - { - Action: cloudFlareCreate, - ResourceRecord: cloudflare.DNSRecord{ - Name: "example.com", - }, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "", // Empty region key - }, - }, - }, - }, - wantErr: true, - }, - { - name: "conflicting region keys", - args: args{ - changes: []*cloudFlareChange{ - { - Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", - }, - }, - { - Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "us", // Different region key for same hostname - }, - }, - }, - }, - wantErr: true, - }, - { - name: "update takes precedence over create & delete", - args: args{ - changes: []*cloudFlareChange{ - { - Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", - }, - }, - { - Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", - }, - }, - { - Action: cloudFlareDelete, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", - }, - }, - }, - }, - want: []DataLocalizationRegionalHostnameChange{ - { - Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", - }, - }, - }, - wantErr: false, - }, - { - name: "create after delete becomes update", - args: args{ - changes: []*cloudFlareChange{ - { - Action: cloudFlareDelete, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", - }, - }, - { - Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", - }, - }, - }, - }, - want: []DataLocalizationRegionalHostnameChange{ - { - Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", - }, - }, - }, - wantErr: false, - }, - { - name: "consolidate mixed actions for different hostnames", - args: args{ - changes: []*cloudFlareChange{ - { - Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example1.com", - RegionKey: "eu", - }, - }, - { - Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example2.com", - RegionKey: "us", - }, - }, - { - Action: cloudFlareDelete, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example3.com", - RegionKey: "ap", - }, - }, - // duplicated actions - { - Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example1.com", - RegionKey: "eu", - }, - }, - { - Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example2.com", - RegionKey: "us", - }, - }, - { - Action: cloudFlareDelete, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example3.com", - RegionKey: "ap", - }, - }, - }, - }, - want: []DataLocalizationRegionalHostnameChange{ - { - Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example1.com", - RegionKey: "eu", - }, - }, - { - Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example2.com", - RegionKey: "us", - }, - }, - { - Action: cloudFlareDelete, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example3.com", - RegionKey: "ap", - }, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := dataLocalizationRegionalHostnamesChanges(tt.args.changes) - if (err != nil) != tt.wantErr { - t.Errorf("dataLocalizationRegionalHostnamesChanges() error = %v, wantErr %v", err, tt.wantErr) - return - } - slices.SortFunc(got, cmpDataLocalizationRegionalHostnameChange) - slices.SortFunc(tt.want, cmpDataLocalizationRegionalHostnameChange) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("dataLocalizationRegionalHostnamesChanges() = %v, want %v", got, tt.want) - } - }) - } -} - func TestZoneHasPaidPlan(t *testing.T) { client := NewMockCloudFlareClient() cfprovider := &CloudFlareProvider{ From d4013c22e272e4ffba901f70809ae5430d0ee04a Mon Sep 17 00:00:00 2001 From: Saurav Upadhyay <116784047+upsaurav12@users.noreply.github.com> Date: Fri, 23 May 2025 20:12:44 +0530 Subject: [PATCH 09/33] test(provider/civo): Improved test coverage to 90.4% (#5455) --- provider/civo/civo_test.go | 310 +++++++++++++++++++++++++++++++------ 1 file changed, 259 insertions(+), 51 deletions(-) diff --git a/provider/civo/civo_test.go b/provider/civo/civo_test.go index e476199f2..27020a274 100644 --- a/provider/civo/civo_test.go +++ b/provider/civo/civo_test.go @@ -129,6 +129,44 @@ func TestCivoProviderRecords(t *testing.T) { assert.Equal(t, int(records[1].RecordTTL), expected[1].TTL) } +func TestCivoProviderRecordsWithError(t *testing.T) { + client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{ + { + Method: "GET", + Value: []civogo.ValueAdvanceClientForTesting{ + { + RequestBody: ``, + URL: "/v2/dns/12345/records", + ResponseBody: `[ + {"id": "1", "domain_id":"12345", "account_id": "1", "name": "", "type": "A", "value": "10.0.0.0", "ttl": 600}, + {"id": "2", "account_id": "1", "domain_id":"12345", "name": "", "type": "A", "value": "10.0.0.1", "ttl": 600} + ]`, + }, + { + RequestBody: ``, + URL: "/v2/dns", + ResponseBody: `invalid-json-data`, + }, + }, + }, + }) + + defer server.Close() + + provider := &CivoProvider{ + Client: *client, + domainFilter: endpoint.NewDomainFilter([]string{"example.com"}), + } + + _, err := client.ListDNSRecords("12345") + assert.NoError(t, err) + + endpoint, err := provider.Records(context.Background()) + assert.Error(t, err) + assert.Nil(t, endpoint) + +} + func TestCivoProviderWithoutRecords(t *testing.T) { client, server, _ := civogo.NewClientForTesting(map[string]string{ "/v2/dns/12345/records": `[]`, @@ -149,6 +187,68 @@ func TestCivoProviderWithoutRecords(t *testing.T) { assert.Empty(t, records) } +func TestCivoProcessCreateActionsLogs(t *testing.T) { + t.Run("Logs Skipping Zone, no creates found", func(t *testing.T) { + zonesByID := map[string]civogo.DNSDomain{ + "example.com": { + ID: "1", + AccountID: "1", + Name: "example.com", + }, + } + + recordsByZoneID := map[string][]civogo.DNSRecord{ + "example.com": { + { + ID: "1", + AccountID: "1", + Name: "abc", + Value: "12.12.12.1", + Type: "A", + TTL: 600, + }, + }, + } + + updateByZone := map[string][]*endpoint.Endpoint{ + "example.com": { + endpoint.NewEndpoint("abc.example.com", endpoint.RecordTypeA, "1.2.3.4"), + }, + } + var civoChanges CivoChanges + + err := processCreateActions(zonesByID, recordsByZoneID, updateByZone, &civoChanges) + require.NoError(t, err) + assert.Len(t, civoChanges.Creates, 1) + assert.Empty(t, civoChanges.Deletes) + assert.Empty(t, civoChanges.Updates) + }) + + t.Run("Records found which should not exist", func(t *testing.T) { + zonesByID := map[string]civogo.DNSDomain{ + "example.com": { + ID: "1", + AccountID: "1", + Name: "example.com", + }, + } + + recordsByZoneID := map[string][]civogo.DNSRecord{ + "example.com": {}, + } + + updateByZone := map[string][]*endpoint.Endpoint{ + "example.com": {}, + } + var civoChanges CivoChanges + + err := processCreateActions(zonesByID, recordsByZoneID, updateByZone, &civoChanges) + require.NoError(t, err) + assert.Empty(t, civoChanges.Creates) + assert.Empty(t, civoChanges.Creates) + assert.Empty(t, civoChanges.Updates) + }) +} func TestCivoProcessCreateActions(t *testing.T) { zoneByID := map[string]civogo.DNSDomain{ "example.com": { @@ -255,6 +355,41 @@ func TestCivoProcessCreateActionsWithError(t *testing.T) { assert.Equal(t, "invalid Record Type: AAAA", err.Error()) } +func TestCivoProcessUpdateActionsWithError(t *testing.T) { + zoneByID := map[string]civogo.DNSDomain{ + "example.com": { + ID: "1", + AccountID: "1", + Name: "example.com", + }, + } + + recordsByZoneID := map[string][]civogo.DNSRecord{ + "example.com": { + { + ID: "1", + AccountID: "1", + DNSDomainID: "1", + Name: "txt", + Value: "12.12.12.1", + Type: "A", + TTL: 600, + }, + }, + } + + updatesByZone := map[string][]*endpoint.Endpoint{ + "example.com": { + endpoint.NewEndpoint("foo.example.com", "AAAA", "1.2.3.4"), + endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeCNAME, "foo.example.com"), + }, + } + + var changes CivoChanges + err := processUpdateActions(zoneByID, recordsByZoneID, updatesByZone, &changes) + require.Error(t, err) +} + func TestCivoProcessUpdateActions(t *testing.T) { zoneByID := map[string]civogo.DNSDomain{ "example.com": { @@ -515,6 +650,64 @@ func TestCivoApplyChanges(t *testing.T) { assert.NoError(t, err) } +func TestCivoApplyChangesError(t *testing.T) { + client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{ + { + Method: "GET", + Value: []civogo.ValueAdvanceClientForTesting{ + { + RequestBody: "", + URL: "/v2/dns", + ResponseBody: `[{"id": "12345", "account_id": "1", "name": "example.com"}]`, + }, + { + RequestBody: "", + URL: "/v2/dns/12345/records", + ResponseBody: `[]`, + }, + }, + }, + }) + + defer server.Close() + + provider := &CivoProvider{ + Client: *client, + } + + cases := []struct { + Name string + changes *plan.Changes + }{ + { + Name: "invalid record type from processCreateActions", + changes: &plan.Changes{ + Create: []*endpoint.Endpoint{ + endpoint.NewEndpoint("bad.example.com", "AAAA", "1.2.3.4"), + }, + }, + }, + { + Name: "invalid record type from processUpdateActions", + changes: &plan.Changes{ + UpdateOld: []*endpoint.Endpoint{ + endpoint.NewEndpoint("bad.example.com", "AAAA", "1.2.3.4"), + }, + UpdateNew: []*endpoint.Endpoint{ + endpoint.NewEndpoint("bad.example.com", "AAAA", "5.6.7.8"), + }, + }, + }, + } + + for _, tt := range cases { + t.Run(tt.Name, func(t *testing.T) { + err := provider.ApplyChanges(context.Background(), tt.changes) + assert.Equal(t, "invalid Record Type: AAAA", string(err.Error())) + }) + } +} + func TestCivoProviderFetchZones(t *testing.T) { client, server, _ := civogo.NewClientForTesting(map[string]string{ "/v2/dns": `[ @@ -688,39 +881,19 @@ func TestCivo_submitChangesCreate(t *testing.T) { }, }, }, - }) - defer server.Close() - - provider := &CivoProvider{ - Client: *client, - DryRun: false, - } - - changes := CivoChanges{ - Creates: []*CivoChangeCreate{ - { - Domain: civogo.DNSDomain{ - ID: "12345", - AccountID: "1", - Name: "example.com", + { + Method: "DELETE", + Value: []civogo.ValueAdvanceClientForTesting{ + { + URL: "/v2/dns/12345/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3", + ResponseBody: `{"result": "success"}`, }, - Options: &civogo.DNSRecordConfig{ - Type: "MX", - Name: "mail", - Value: "10.0.0.1", - Priority: 10, - TTL: 600, + { + URL: "/v2/dns/12345/records/error-record-id", + ResponseBody: `{"result": "error", "error": "failed to delete record"}`, }, }, }, - } - - err := provider.submitChanges(context.Background(), changes) - assert.NoError(t, err) -} - -func TestCivo_submitChangesUpdate(t *testing.T) { - client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{ { Method: "PUT", Value: []civogo.ValueAdvanceClientForTesting{ @@ -738,6 +911,11 @@ func TestCivo_submitChangesUpdate(t *testing.T) { "ttl": 600 }`, }, + { + RequestBody: `{"type":"MX","name":"mail","value":"10.0.0.3","priority":10,"ttl":600}`, + URL: "/v2/dns/12345/records/error-record-id", + ResponseBody: `{"result": "error", "error": "failed to update record"}`, + }, }, }, }) @@ -745,36 +923,66 @@ func TestCivo_submitChangesUpdate(t *testing.T) { provider := &CivoProvider{ Client: *client, - DryRun: false, + DryRun: true, } - changes := CivoChanges{ - Updates: []*CivoChangeUpdate{ - { - Domain: civogo.DNSDomain{ID: "12345", AccountID: "1", Name: "example.com"}, - DomainRecord: civogo.DNSRecord{ - ID: "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", - AccountID: "1", - DNSDomainID: "12345", - Name: "mail", - Value: "10.0.0.1", - Type: "MX", - Priority: 10, - TTL: 600, + cases := []struct { + name string + changes *CivoChanges + expectedResult error + }{ + { + name: "changes slice is empty", + changes: &CivoChanges{}, + expectedResult: nil, + }, + { + name: "changes slice has changes and update changes", + changes: &CivoChanges{ + Creates: []*CivoChangeCreate{ + { + Domain: civogo.DNSDomain{ + ID: "12345", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: "MX", + Name: "mail", + Value: "10.0.0.1", + Priority: 10, + TTL: 600, + }, + }, }, - Options: civogo.DNSRecordConfig{ - Type: "MX", - Name: "mail", - Value: "10.0.0.2", - Priority: 10, - TTL: 600, + + Updates: []*CivoChangeUpdate{ + { + Domain: civogo.DNSDomain{ + ID: "12345", + AccountID: "2", + Name: "example.org", + }, + }, + { + Domain: civogo.DNSDomain{ + ID: "67890", + AccountID: "3", + Name: "example.COM", + }, + }, }, }, + expectedResult: nil, }, } - err := provider.submitChanges(context.Background(), changes) - assert.NoError(t, err) + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := provider.submitChanges(context.Background(), *c.changes) + assert.NoError(t, err) + }) + } } func TestCivo_submitChangesDelete(t *testing.T) { From d0e6a9075e3846ca134b137e0065aa94b62d2897 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Fri, 23 May 2025 17:16:37 +0200 Subject: [PATCH 10/33] chore(crd): move code to `apis/v1alpha1` (#5446) * chore(crd): move code to `api/v1alpha1` * fix license check * fix linter * remove obsolete exclusion on linter --- .golangci.yml | 1 - Makefile | 3 +- apis/api.go | 17 ++++ apis/v1alpha1/api.go | 20 ++++ apis/v1alpha1/dnsendpoint.go | 62 ++++++++++++ apis/v1alpha1/groupversion_info.go | 40 ++++++++ apis/v1alpha1/zz_generated.deepcopy.go | 110 ++++++++++++++++++++++ charts/external-dns/crds/dnsendpoint.yaml | 3 + config/crd/standard/dnsendpoint.yaml | 3 + endpoint/endpoint.go | 46 +-------- endpoint/zz_generated.deepcopy.go | 89 ----------------- source/crd.go | 24 ++--- source/crd_test.go | 10 +- 13 files changed, 275 insertions(+), 153 deletions(-) create mode 100644 apis/api.go create mode 100644 apis/v1alpha1/api.go create mode 100644 apis/v1alpha1/dnsendpoint.go create mode 100644 apis/v1alpha1/groupversion_info.go create mode 100644 apis/v1alpha1/zz_generated.deepcopy.go diff --git a/.golangci.yml b/.golangci.yml index b7498ef48..e96def4ef 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -95,7 +95,6 @@ formatters: exclusions: generated: lax paths: - - endpoint/zz_generated.deepcopy.go - third_party$ - builtin$ - examples$ diff --git a/Makefile b/Makefile index c0c15cd2f..0ec5f712d 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,8 @@ lint: licensecheck go-lint oas-lint #? crd: Generates CRD using controller-gen and copy it into chart .PHONY: crd crd: controller-gen-install - ${CONTROLLER_GEN} crd:crdVersions=v1 paths="./endpoint/..." output:crd:stdout > config/crd/standard/dnsendpoint.yaml + ${CONTROLLER_GEN} object crd:crdVersions=v1 paths="./endpoint/..." + ${CONTROLLER_GEN} object crd:crdVersions=v1 paths="./apis/..." output:crd:stdout > config/crd/standard/dnsendpoint.yaml cp -f config/crd/standard/dnsendpoint.yaml charts/external-dns/crds/dnsendpoint.yaml #? test: The verify target runs tasks similar to the CI tasks, but without code coverage diff --git a/apis/api.go b/apis/api.go new file mode 100644 index 000000000..a037bf3f3 --- /dev/null +++ b/apis/api.go @@ -0,0 +1,17 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apis diff --git a/apis/v1alpha1/api.go b/apis/v1alpha1/api.go new file mode 100644 index 000000000..127a77368 --- /dev/null +++ b/apis/v1alpha1/api.go @@ -0,0 +1,20 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the externaldns.k8s.io v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=externaldns.k8s.io +package v1alpha1 diff --git a/apis/v1alpha1/dnsendpoint.go b/apis/v1alpha1/dnsendpoint.go new file mode 100644 index 000000000..522bd1bee --- /dev/null +++ b/apis/v1alpha1/dnsendpoint.go @@ -0,0 +1,62 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/external-dns/endpoint" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// DNSEndpoint is a contract that a user-specified CRD must implement to be used as a source for external-dns. +// The user-specified CRD should also have the status sub-resource. +// +k8s:openapi-gen=true +// +groupName=externaldns.k8s.io +// +kubebuilder:resource:path=dnsendpoints +// +kubebuilder:subresource:status +// +kubebuilder:metadata:annotations="api-approved.kubernetes.io=https://github.com/kubernetes-sigs/external-dns/pull/2007" +// +versionName=v1alpha1 +type DNSEndpoint struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DNSEndpointSpec `json:"spec,omitempty"` + Status DNSEndpointStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// DNSEndpointList is a list of DNSEndpoint objects +type DNSEndpointList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSEndpoint `json:"items"` +} + +// DNSEndpointSpec defines the desired state of DNSEndpoint +type DNSEndpointSpec struct { + Endpoints []*endpoint.Endpoint `json:"endpoints,omitempty"` +} + +// DNSEndpointStatus defines the observed state of DNSEndpoint +type DNSEndpointStatus struct { + // The generation observed by the external-dns controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} diff --git a/apis/v1alpha1/groupversion_info.go b/apis/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..926c4bc92 --- /dev/null +++ b/apis/v1alpha1/groupversion_info.go @@ -0,0 +1,40 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the externaldns.k8s.io v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=externaldns.k8s.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "externaldns.k8s.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func init() { + SchemeBuilder.Register(&DNSEndpoint{}, &DNSEndpointList{}) +} diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..e2f5bf1b8 --- /dev/null +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,110 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/external-dns/endpoint" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSEndpoint) DeepCopyInto(out *DNSEndpoint) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSEndpoint. +func (in *DNSEndpoint) DeepCopy() *DNSEndpoint { + if in == nil { + return nil + } + out := new(DNSEndpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSEndpoint) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSEndpointList) DeepCopyInto(out *DNSEndpointList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSEndpoint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSEndpointList. +func (in *DNSEndpointList) DeepCopy() *DNSEndpointList { + if in == nil { + return nil + } + out := new(DNSEndpointList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSEndpointList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSEndpointSpec) DeepCopyInto(out *DNSEndpointSpec) { + *out = *in + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]*endpoint.Endpoint, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(endpoint.Endpoint) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSEndpointSpec. +func (in *DNSEndpointSpec) DeepCopy() *DNSEndpointSpec { + if in == nil { + return nil + } + out := new(DNSEndpointSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSEndpointStatus) DeepCopyInto(out *DNSEndpointStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSEndpointStatus. +func (in *DNSEndpointStatus) DeepCopy() *DNSEndpointStatus { + if in == nil { + return nil + } + out := new(DNSEndpointStatus) + in.DeepCopyInto(out) + return out +} diff --git a/charts/external-dns/crds/dnsendpoint.yaml b/charts/external-dns/crds/dnsendpoint.yaml index 88845aaae..83388d451 100644 --- a/charts/external-dns/crds/dnsendpoint.yaml +++ b/charts/external-dns/crds/dnsendpoint.yaml @@ -18,6 +18,9 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: + description: |- + DNSEndpoint is a contract that a user-specified CRD must implement to be used as a source for external-dns. + The user-specified CRD should also have the status sub-resource. properties: apiVersion: description: |- diff --git a/config/crd/standard/dnsendpoint.yaml b/config/crd/standard/dnsendpoint.yaml index 88845aaae..83388d451 100644 --- a/config/crd/standard/dnsendpoint.yaml +++ b/config/crd/standard/dnsendpoint.yaml @@ -18,6 +18,9 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: + description: |- + DNSEndpoint is a contract that a user-specified CRD must implement to be used as a source for external-dns. + The user-specified CRD should also have the status sub-resource. properties: apiVersion: description: |- diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index bbf67f641..86034fed6 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -24,8 +24,6 @@ import ( "strings" log "github.com/sirupsen/logrus" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( @@ -337,48 +335,6 @@ func FilterEndpointsByOwnerID(ownerID string, eps []*Endpoint) []*Endpoint { return filtered } -// DNSEndpointSpec defines the desired state of DNSEndpoint -// +kubebuilder:object:generate=true -type DNSEndpointSpec struct { - Endpoints []*Endpoint `json:"endpoints,omitempty"` -} - -// DNSEndpointStatus defines the observed state of DNSEndpoint -type DNSEndpointStatus struct { - // The generation observed by the external-dns controller. - // +optional - ObservedGeneration int64 `json:"observedGeneration,omitempty"` -} - -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// DNSEndpoint is a contract that a user-specified CRD must implement to be used as a source for external-dns. -// The user-specified CRD should also have the status sub-resource. -// +k8s:openapi-gen=true -// +groupName=externaldns.k8s.io -// +kubebuilder:resource:path=dnsendpoints -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:metadata:annotations="api-approved.kubernetes.io=https://github.com/kubernetes-sigs/external-dns/pull/2007" -// +versionName=v1alpha1 - -type DNSEndpoint struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec DNSEndpointSpec `json:"spec,omitempty"` - Status DNSEndpointStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true -// DNSEndpointList is a list of DNSEndpoint objects -type DNSEndpointList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []DNSEndpoint `json:"items"` -} - // RemoveDuplicates returns a slice holding the unique endpoints. // This function doesn't contemplate the Targets of an Endpoint // as part of the primary Key @@ -400,7 +356,7 @@ func RemoveDuplicates(endpoints []*Endpoint) []*Endpoint { return result } -// Check endpoint if is it properly formatted according to RFC standards +// CheckEndpoint Check if endpoint is properly formatted according to RFC standards func (e *Endpoint) CheckEndpoint() bool { switch recordType := e.RecordType; recordType { case RecordTypeMX: diff --git a/endpoint/zz_generated.deepcopy.go b/endpoint/zz_generated.deepcopy.go index ec07dace1..e86498d37 100644 --- a/endpoint/zz_generated.deepcopy.go +++ b/endpoint/zz_generated.deepcopy.go @@ -4,95 +4,6 @@ package endpoint -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSEndpoint) DeepCopyInto(out *DNSEndpoint) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSEndpoint. -func (in *DNSEndpoint) DeepCopy() *DNSEndpoint { - if in == nil { - return nil - } - out := new(DNSEndpoint) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSEndpoint) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSEndpointList) DeepCopyInto(out *DNSEndpointList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DNSEndpoint, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSEndpointList. -func (in *DNSEndpointList) DeepCopy() *DNSEndpointList { - if in == nil { - return nil - } - out := new(DNSEndpointList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSEndpointList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSEndpointSpec) DeepCopyInto(out *DNSEndpointSpec) { - *out = *in - if in.Endpoints != nil { - in, out := &in.Endpoints, &out.Endpoints - *out = make([]*Endpoint, len(*in)) - for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(Endpoint) - (*in).DeepCopyInto(*out) - } - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSEndpointSpec. -func (in *DNSEndpointSpec) DeepCopy() *DNSEndpointSpec { - if in == nil { - return nil - } - out := new(DNSEndpointSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Endpoint) DeepCopyInto(out *Endpoint) { *out = *in diff --git a/source/crd.go b/source/crd.go index d62805162..a603964dd 100644 --- a/source/crd.go +++ b/source/crd.go @@ -36,6 +36,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + apiv1alpha1 "sigs.k8s.io/external-dns/apis/v1alpha1" "sigs.k8s.io/external-dns/endpoint" ) @@ -53,8 +54,8 @@ type crdSource struct { func addKnownTypes(scheme *runtime.Scheme, groupVersion schema.GroupVersion) error { scheme.AddKnownTypes(groupVersion, - &endpoint.DNSEndpoint{}, - &endpoint.DNSEndpointList{}, + &apiv1alpha1.DNSEndpoint{}, + &apiv1alpha1.DNSEndpointList{}, ) metav1.AddToGroupVersion(scheme, groupVersion) return nil @@ -129,7 +130,7 @@ func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFi return sourceCrd.watch(context.TODO(), &lo) }, }, - &endpoint.DNSEndpoint{}, + &apiv1alpha1.DNSEndpoint{}, 0) sourceCrd.informer = &informer go informer.Run(wait.NeverStop) @@ -164,7 +165,7 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error endpoints := []*endpoint.Endpoint{} var ( - result *endpoint.DNSEndpointList + result *apiv1alpha1.DNSEndpointList err error ) @@ -174,7 +175,6 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error } result, err = cs.filterByAnnotations(result) - if err != nil { return nil, err } @@ -229,7 +229,7 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error return endpoints, nil } -func (cs *crdSource) setResourceLabel(crd *endpoint.DNSEndpoint, endpoints []*endpoint.Endpoint) { +func (cs *crdSource) setResourceLabel(crd *apiv1alpha1.DNSEndpoint, endpoints []*endpoint.Endpoint) { for _, ep := range endpoints { ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("crd/%s/%s", crd.Namespace, crd.Name) } @@ -244,8 +244,8 @@ func (cs *crdSource) watch(ctx context.Context, opts *metav1.ListOptions) (watch Watch(ctx) } -func (cs *crdSource) List(ctx context.Context, opts *metav1.ListOptions) (result *endpoint.DNSEndpointList, err error) { - result = &endpoint.DNSEndpointList{} +func (cs *crdSource) List(ctx context.Context, opts *metav1.ListOptions) (result *apiv1alpha1.DNSEndpointList, err error) { + result = &apiv1alpha1.DNSEndpointList{} err = cs.crdClient.Get(). Namespace(cs.namespace). Resource(cs.crdResource). @@ -255,8 +255,8 @@ func (cs *crdSource) List(ctx context.Context, opts *metav1.ListOptions) (result return } -func (cs *crdSource) UpdateStatus(ctx context.Context, dnsEndpoint *endpoint.DNSEndpoint) (result *endpoint.DNSEndpoint, err error) { - result = &endpoint.DNSEndpoint{} +func (cs *crdSource) UpdateStatus(ctx context.Context, dnsEndpoint *apiv1alpha1.DNSEndpoint) (result *apiv1alpha1.DNSEndpoint, err error) { + result = &apiv1alpha1.DNSEndpoint{} err = cs.crdClient.Put(). Namespace(dnsEndpoint.Namespace). Resource(cs.crdResource). @@ -269,7 +269,7 @@ func (cs *crdSource) UpdateStatus(ctx context.Context, dnsEndpoint *endpoint.DNS } // filterByAnnotations filters a list of dnsendpoints by a given annotation selector. -func (cs *crdSource) filterByAnnotations(dnsendpoints *endpoint.DNSEndpointList) (*endpoint.DNSEndpointList, error) { +func (cs *crdSource) filterByAnnotations(dnsendpoints *apiv1alpha1.DNSEndpointList) (*apiv1alpha1.DNSEndpointList, error) { labelSelector, err := metav1.ParseToLabelSelector(cs.annotationFilter) if err != nil { return nil, err @@ -284,7 +284,7 @@ func (cs *crdSource) filterByAnnotations(dnsendpoints *endpoint.DNSEndpointList) return dnsendpoints, nil } - filteredList := endpoint.DNSEndpointList{} + filteredList := apiv1alpha1.DNSEndpointList{} for _, dnsendpoint := range dnsendpoints.Items { // include dnsendpoint if its annotations match the selector diff --git a/source/crd_test.go b/source/crd_test.go index 53d8b322c..8386c6428 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -36,6 +36,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" + apiv1alpha1 "sigs.k8s.io/external-dns/apis/v1alpha1" "sigs.k8s.io/external-dns/endpoint" ) @@ -61,8 +62,8 @@ func fakeRESTClient(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, scheme := runtime.NewScheme() addKnownTypes(scheme, groupVersion) - dnsEndpointList := endpoint.DNSEndpointList{} - dnsEndpoint := &endpoint.DNSEndpoint{ + dnsEndpointList := apiv1alpha1.DNSEndpointList{} + dnsEndpoint := &apiv1alpha1.DNSEndpoint{ TypeMeta: metav1.TypeMeta{ APIVersion: apiVersion, Kind: kind, @@ -74,7 +75,7 @@ func fakeRESTClient(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, Labels: labels, Generation: 1, }, - Spec: endpoint.DNSEndpointSpec{ + Spec: apiv1alpha1.DNSEndpointSpec{ Endpoints: endpoints, }, } @@ -101,7 +102,7 @@ func fakeRESTClient(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s/"+name+"/status" && m == http.MethodPut: decoder := json.NewDecoder(req.Body) - var body endpoint.DNSEndpoint + var body apiv1alpha1.DNSEndpoint decoder.Decode(&body) dnsEndpoint.Status.ObservedGeneration = body.Status.ObservedGeneration return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, dnsEndpoint)}, nil @@ -468,7 +469,6 @@ func testCRDSourceEndpoints(t *testing.T) { expectError: false, }, } { - t.Run(ti.title, func(t *testing.T) { t.Parallel() From 285d9769e1ba992bb79bf5e37f59d32034174fad Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Fri, 23 May 2025 12:46:55 -0400 Subject: [PATCH 11/33] Update pkg/apis/externaldns/types.go Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- pkg/apis/externaldns/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index bf9d57d3c..1399998dd 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -538,7 +538,7 @@ func App(cfg *Config) *kingpin.Application { 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: none, options: google, ssl_com, lets_encrypt, none)").Default("").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "") + app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used by default. (default: none, options: google, ssl_com, lets_encrypt, none)").Default("").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("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment) From f78ede116212a7d9d04735524102a3cad42c5c31 Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Fri, 23 May 2025 14:55:59 -0400 Subject: [PATCH 12/33] feat(cloudflare): fix overriddenConfig for CloudflareCustomHostnamesCertificateAuthority --- pkg/apis/externaldns/types_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 3737c2d1a..7a81d9f10 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -188,7 +188,7 @@ var ( CloudflareProxied: true, CloudflareCustomHostnames: true, CloudflareCustomHostnamesMinTLSVersion: "1.3", - CloudflareCustomHostnamesCertificateAuthority: "", + CloudflareCustomHostnamesCertificateAuthority: "google", CloudflareDNSRecordsPerPage: 5000, CloudflareRegionKey: "us", CoreDNSPrefix: "/coredns/", From b55a04c0004d359a581499917e74ded8df86bb15 Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Fri, 23 May 2025 14:58:16 -0400 Subject: [PATCH 13/33] feat(cloudflare): fix formatting for getCustomHostnamesSSLOptions --- provider/cloudflare/cloudflare.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 3879e380c..dac89f627 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -811,9 +811,9 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte func getCustomHostnamesSSLOptions(customHostnamesConfig CustomHostnamesConfig) *cloudflare.CustomHostnameSSL { ssl := &cloudflare.CustomHostnameSSL{ - Type: "dv", - Method: "http", - BundleMethod: "ubiquitous", + Type: "dv", + Method: "http", + BundleMethod: "ubiquitous", Settings: cloudflare.CustomHostnameSSLSettings{ MinTLSVersion: customHostnamesConfig.MinTLSVersion, }, From 12e82b40858f6ec79a4f8b8e4bde9ca3d9c8c9dc Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Fri, 23 May 2025 15:01:00 -0400 Subject: [PATCH 14/33] feat(cloudflare): update docs --- docs/flags.md | 2 +- docs/monitoring/metrics.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/flags.md b/docs/flags.md index 867031aaf..4c997158b 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -92,7 +92,7 @@ | `--[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-custom-hostnames-certificate-authority=CLOUDFLARE-CUSTOM-HOSTNAMES-CERTIFICATE-AUTHORITY` | When using the Cloudflare provider with the Custom Hostnames, optionally specify which Certificate Authority will be used. (optional, 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) | | `--cloudflare-record-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (default: '') | diff --git a/docs/monitoring/metrics.md b/docs/monitoring/metrics.md index ae4752913..8d73b202a 100644 --- a/docs/monitoring/metrics.md +++ b/docs/monitoring/metrics.md @@ -80,6 +80,8 @@ curl https://localhost:7979/metrics | http_request_duration_seconds | | process_cpu_seconds_total | | process_max_fds | +| process_network_receive_bytes_total | +| process_network_transmit_bytes_total | | process_open_fds | | process_resident_memory_bytes | | process_start_time_seconds | From 426ea7e1fded8c4ff6182ac6a03af0bb06fb0bdc Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Fri, 23 May 2025 15:10:19 -0400 Subject: [PATCH 15/33] feat(cloudflare): update flags.md --- docs/flags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/flags.md b/docs/flags.md index 4c997158b..e6f11e56b 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -92,7 +92,7 @@ | `--[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=CLOUDFLARE-CUSTOM-HOSTNAMES-CERTIFICATE-AUTHORITY` | When using the Cloudflare provider with the Custom Hostnames, optionally specify which Certificate Authority will be used. (optional, options: google, ssl_com, lets_encrypt) | +| `--cloudflare-custom-hostnames-certificate-authority=` | When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used by default. (default: none, options: google, ssl_com, lets_encrypt, none) | | `--cloudflare-dns-records-per-page=100` | When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100) | | `--cloudflare-region-key=CLOUDFLARE-REGION-KEY` | When using the Cloudflare provider, specify the region (default: earth) | | `--cloudflare-record-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (default: '') | From a9d90790e50a10a6d88594208d56d2f1bcec151a Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Fri, 23 May 2025 15:45:03 -0400 Subject: [PATCH 16/33] feat(cloudflare): update cloudflare_regional_test.go --- provider/cloudflare/cloudflare_regional_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/provider/cloudflare/cloudflare_regional_test.go b/provider/cloudflare/cloudflare_regional_test.go index f8273a601..0a79647f6 100644 --- a/provider/cloudflare/cloudflare_regional_test.go +++ b/provider/cloudflare/cloudflare_regional_test.go @@ -24,6 +24,7 @@ import ( "github.com/cloudflare/cloudflare-go" "github.com/stretchr/testify/assert" + "sigs.k8s.io/external-dns/endpoint" ) @@ -124,7 +125,7 @@ func Test_regionalHostname(t *testing.T) { t.Run(tt.name, func(t *testing.T) { p := CloudFlareProvider{RegionKey: tt.args.defaultRegionKey} got := p.regionalHostname(tt.args.endpoint) - assert.Equal(t, got, tt.want) + assert.Equal(t, tt.want, got) }) } } From 3e4a8bb0560f5d54a2d1c93691b982c36d4b8c17 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Sat, 24 May 2025 09:04:51 +0100 Subject: [PATCH 17/33] chore(code-quality): added linter errchkjson (#5448) * chore(code-quality): added linter errchkjson Signed-off-by: ivan katliarchuk * chore(code-quality): added linter errchkjson Signed-off-by: ivan katliarchuk --------- Signed-off-by: ivan katliarchuk --- .golangci.yml | 1 + provider/akamai/akamai_test.go | 6 +++-- provider/godaddy/godaddy_test.go | 31 +++++++++++++++------- provider/ovh/ovh_test.go | 20 +++++++++++--- provider/webhook/api/httpapi.go | 5 +++- provider/webhook/api/httpapi_test.go | 39 +++++++++++++++++++++++++++- 6 files changed, 84 insertions(+), 18 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index e96def4ef..4facabf2e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,6 +12,7 @@ linters: - misspell - revive - rowserrcheck # Checks whether Rows.Err of rows is checked successfully. + - errchkjson # Checks types passed to the json encoding functions. ref: https://golangci-lint.run/usage/linters/#errchkjson - errorlint # Checking for unchecked errors in Go code https://golangci-lint.run/usage/linters/#errcheck - staticcheck - unconvert diff --git a/provider/akamai/akamai_test.go b/provider/akamai/akamai_test.go index 7d6ecdb37..f517a723c 100644 --- a/provider/akamai/akamai_test.go +++ b/provider/akamai/akamai_test.go @@ -160,7 +160,8 @@ func TestFetchZonesZoneIDFilter(t *testing.T) { stub.setOutput("zone", []interface{}{"test1.testzone.com", "test2.testzone.com"}) x, _ := c.fetchZones() - y, _ := json.Marshal(x) + y, err := json.Marshal(x) + require.NoError(t, err) if assert.NotNil(t, y) { assert.JSONEq(t, "{\"zones\":[{\"contractId\":\"contract\",\"zone\":\"test1.testzone.com\"},{\"contractId\":\"contract\",\"zone\":\"test2.testzone.com\"}]}", string(y)) } @@ -175,7 +176,8 @@ func TestFetchZonesEmpty(t *testing.T) { stub.setOutput("zone", []interface{}{}) x, _ := c.fetchZones() - y, _ := json.Marshal(x) + y, err := json.Marshal(x) + require.NoError(t, err) if assert.NotNil(t, y) { assert.JSONEq(t, "{\"zones\":[]}", string(y)) } diff --git a/provider/godaddy/godaddy_test.go b/provider/godaddy/godaddy_test.go index 57e04e21f..344391f88 100644 --- a/provider/godaddy/godaddy_test.go +++ b/provider/godaddy/godaddy_test.go @@ -26,6 +26,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" ) @@ -49,40 +50,50 @@ var ( func (c *mockGoDaddyClient) Post(endpoint string, input interface{}, output interface{}) error { log.Infof("POST: %s - %v", endpoint, input) stub := c.Called(endpoint, input) - data, _ := json.Marshal(stub.Get(0)) - json.Unmarshal(data, output) + data, err := json.Marshal(stub.Get(0)) + require.NoError(c.currentTest, err) + err = json.Unmarshal(data, output) + require.NoError(c.currentTest, err) return stub.Error(1) } func (c *mockGoDaddyClient) Patch(endpoint string, input interface{}, output interface{}) error { log.Infof("PATCH: %s - %v", endpoint, input) stub := c.Called(endpoint, input) - data, _ := json.Marshal(stub.Get(0)) - json.Unmarshal(data, output) + data, err := json.Marshal(stub.Get(0)) + require.NoError(c.currentTest, err) + err = json.Unmarshal(data, output) + require.NoError(c.currentTest, err) return stub.Error(1) } func (c *mockGoDaddyClient) Put(endpoint string, input interface{}, output interface{}) error { log.Infof("PUT: %s - %v", endpoint, input) stub := c.Called(endpoint, input) - data, _ := json.Marshal(stub.Get(0)) - json.Unmarshal(data, output) + data, err := json.Marshal(stub.Get(0)) + require.NoError(c.currentTest, err) + err = json.Unmarshal(data, output) + require.NoError(c.currentTest, err) return stub.Error(1) } func (c *mockGoDaddyClient) Get(endpoint string, output interface{}) error { log.Infof("GET: %s", endpoint) stub := c.Called(endpoint) - data, _ := json.Marshal(stub.Get(0)) - json.Unmarshal(data, output) + data, err := json.Marshal(stub.Get(0)) + require.NoError(c.currentTest, err) + err = json.Unmarshal(data, output) + require.NoError(c.currentTest, err) return stub.Error(1) } func (c *mockGoDaddyClient) Delete(endpoint string, output interface{}) error { log.Infof("DELETE: %s", endpoint) stub := c.Called(endpoint) - data, _ := json.Marshal(stub.Get(0)) - json.Unmarshal(data, output) + data, err := json.Marshal(stub.Get(0)) + require.NoError(c.currentTest, err) + err = json.Unmarshal(data, output) + require.NoError(c.currentTest, err) return stub.Error(1) } diff --git a/provider/ovh/ovh_test.go b/provider/ovh/ovh_test.go index 4b53db203..49444bf4b 100644 --- a/provider/ovh/ovh_test.go +++ b/provider/ovh/ovh_test.go @@ -41,28 +41,40 @@ type mockOvhClient struct { func (c *mockOvhClient) PostWithContext(ctx context.Context, endpoint string, input interface{}, output interface{}) error { stub := c.Called(endpoint, input) - data, _ := json.Marshal(stub.Get(0)) + data, err := json.Marshal(stub.Get(0)) + if err != nil { + return err + } json.Unmarshal(data, output) return stub.Error(1) } func (c *mockOvhClient) PutWithContext(ctx context.Context, endpoint string, input interface{}, output interface{}) error { stub := c.Called(endpoint, input) - data, _ := json.Marshal(stub.Get(0)) + data, err := json.Marshal(stub.Get(0)) + if err != nil { + return err + } json.Unmarshal(data, output) return stub.Error(1) } func (c *mockOvhClient) GetWithContext(ctx context.Context, endpoint string, output interface{}) error { stub := c.Called(endpoint) - data, _ := json.Marshal(stub.Get(0)) + data, err := json.Marshal(stub.Get(0)) + if err != nil { + return err + } json.Unmarshal(data, output) return stub.Error(1) } func (c *mockOvhClient) DeleteWithContext(ctx context.Context, endpoint string, output interface{}) error { stub := c.Called(endpoint) - data, _ := json.Marshal(stub.Get(0)) + data, err := json.Marshal(stub.Get(0)) + if err != nil { + return err + } json.Unmarshal(data, output) return stub.Error(1) } diff --git a/provider/webhook/api/httpapi.go b/provider/webhook/api/httpapi.go index 13c09bca7..fde7d3ab8 100644 --- a/provider/webhook/api/httpapi.go +++ b/provider/webhook/api/httpapi.go @@ -106,7 +106,10 @@ func (p *WebhookServer) AdjustEndpointsHandler(w http.ResponseWriter, req *http. func (p *WebhookServer) NegotiateHandler(w http.ResponseWriter, _ *http.Request) { w.Header().Set(ContentTypeHeader, MediaTypeFormatAndVersion) - json.NewEncoder(w).Encode(p.Provider.GetDomainFilter()) + err := json.NewEncoder(w).Encode(p.Provider.GetDomainFilter()) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } } // StartHTTPApi starts a HTTP server given any provider. diff --git a/provider/webhook/api/httpapi_test.go b/provider/webhook/api/httpapi_test.go index 18856753a..9c5cd096e 100644 --- a/provider/webhook/api/httpapi_test.go +++ b/provider/webhook/api/httpapi_test.go @@ -98,7 +98,7 @@ func TestRecordsHandlerRecords(t *testing.T) { // require that the res has the same endpoints as the records slice defer res.Body.Close() require.NotNil(t, res.Body) - endpoints := []*endpoint.Endpoint{} + var endpoints []*endpoint.Endpoint if err := json.NewDecoder(res.Body).Decode(&endpoints); err != nil { t.Errorf("Failed to decode response body: %s", err.Error()) } @@ -318,3 +318,40 @@ func TestStartHTTPApi(t *testing.T) { require.NoError(t, err) require.NoError(t, df.UnmarshalJSON(b)) } + +func TestNegotiateHandler_Success(t *testing.T) { + provider := &FakeWebhookProvider{ + domainFilter: endpoint.NewDomainFilter([]string{"foo.bar.com"}), + } + server := &WebhookServer{Provider: provider} + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + + server.NegotiateHandler(w, req) + res := w.Result() + defer res.Body.Close() + + require.Equal(t, http.StatusOK, res.StatusCode) + require.Equal(t, MediaTypeFormatAndVersion, res.Header.Get(ContentTypeHeader)) + + var df endpoint.DomainFilter + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, df.UnmarshalJSON(body)) + require.Equal(t, provider.domainFilter, df) +} + +func TestNegotiateHandler_FiltersWithSpecialEncodings(t *testing.T) { + provider := &FakeWebhookProvider{ + domainFilter: endpoint.NewDomainFilter([]string{"\\u001a", "\\Xfoo.\\u2028, \\u0000.com", ""}), + } + server := &WebhookServer{Provider: provider} + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + + server.NegotiateHandler(w, req) + res := w.Result() + defer res.Body.Close() + + require.Equal(t, http.StatusOK, res.StatusCode) +} From 3807e398c8b91ba7efca0886149bc61150e8c4eb Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Sat, 24 May 2025 07:42:08 -0400 Subject: [PATCH 18/33] feat(cloudflare): updating dcos for more clarity --- docs/tutorials/cloudflare.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index 043db64ff..3b20e9f64 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -312,7 +312,7 @@ If not set the value will default to `global`. ## Setting cloudflare-custom-hostname -Automatic configuration of Cloudflare custom hostnames (using A/CNAME DNS records as custom origin servers) is enabled by the --cloudflare-custom-hostnames flag and the `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: ` annotation. +Automatic configuration of Cloudflare custom hostnames (using A/CNAME DNS records as custom origin servers) is enabled by the `--cloudflare-custom-hostnames` flag and the `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: ` annotation. Multiple hostnames are supported via a comma-separated list: `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: ,`. @@ -320,7 +320,7 @@ See [Cloudflare for Platforms](https://developers.cloudflare.com/cloudflare-for- This feature is disabled by default and supports the `--cloudflare-custom-hostnames-min-tls-version` and `--cloudflare-custom-hostnames-certificate-authority` flags. -`--cloudflare-custom-hostnames-certificate-authority` defaults to not selecting a CA. If a specific CA is required use this flag to select one. +`--cloudflare-custom-hostnames-certificate-authority` defaults to `none`, which explicitly means no Certificate Authority (CA) is set when using the Cloudflare API. Specifying a custom CA is only possible for enterprise accounts. 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. From a1944d1ae4aa0db5aeed902047378b795dc072f7 Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Sat, 24 May 2025 07:43:42 -0400 Subject: [PATCH 19/33] feat(cloudflare): updating allowable and default value to none to better clarity --- pkg/apis/externaldns/types.go | 4 ++-- provider/cloudflare/cloudflare.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 1399998dd..6b80bbd00 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -254,7 +254,7 @@ var defaultConfig = &Config{ CFAPIEndpoint: "", CFPassword: "", CFUsername: "", - CloudflareCustomHostnamesCertificateAuthority: "", + CloudflareCustomHostnamesCertificateAuthority: "none", CloudflareCustomHostnames: false, CloudflareCustomHostnamesMinTLSVersion: "1.0", CloudflareDNSRecordsPerPage: 100, @@ -538,7 +538,7 @@ func App(cfg *Config) *kingpin.Application { 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 Certificate Authority will be used by default. (default: none, options: google, ssl_com, lets_encrypt, none)").Default("").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "") + app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. (default: none, options: google, ssl_com, lets_encrypt, none)").Default("none").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none") app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage) app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the region (default: earth)").StringVar(&cfg.CloudflareRegionKey) app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment) diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index dac89f627..af827daa1 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -820,7 +820,7 @@ func getCustomHostnamesSSLOptions(customHostnamesConfig CustomHostnamesConfig) * } // Set CertificateAuthority if provided // We're not able to set it at all (even with a blank) if you're not on an enterprise plan - if customHostnamesConfig.CertificateAuthority != "" { + if customHostnamesConfig.CertificateAuthority != "none" { ssl.CertificateAuthority = customHostnamesConfig.CertificateAuthority } return ssl From 58f760129996fd782eb6e9e2c0d9bbc0da0223c4 Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Sat, 24 May 2025 07:44:49 -0400 Subject: [PATCH 20/33] feat(cloudflare): updating flag with better language around options --- pkg/apis/externaldns/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 6b80bbd00..908b6b41c 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -538,7 +538,7 @@ func App(cfg *Config) *kingpin.Application { 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 Certificate Authority will be used. (default: none, options: google, ssl_com, lets_encrypt, none)").Default("none").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none") + app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. None indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none)").Default("none").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none") app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage) app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the region (default: earth)").StringVar(&cfg.CloudflareRegionKey) app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment) From 34e9aea2d506b986cd59b56fb7a6f3461e8afa65 Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Sat, 24 May 2025 07:51:04 -0400 Subject: [PATCH 21/33] feat(cloudflare): update docs with better language for none argument --- docs/flags.md | 2 +- pkg/apis/externaldns/types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/flags.md b/docs/flags.md index e6f11e56b..5c3d7c362 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -92,7 +92,7 @@ | `--[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=` | When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used by default. (default: none, options: google, ssl_com, lets_encrypt, none) | +| `--cloudflare-custom-hostnames-certificate-authority=none` | When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none) | | `--cloudflare-dns-records-per-page=100` | When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100) | | `--cloudflare-region-key=CLOUDFLARE-REGION-KEY` | When using the Cloudflare provider, specify the region (default: earth) | | `--cloudflare-record-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (default: '') | diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 908b6b41c..e034c91d6 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -538,7 +538,7 @@ func App(cfg *Config) *kingpin.Application { 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 Certificate Authority will be used. None indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none)").Default("none").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none") + app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none)").Default("none").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none") app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage) app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the region (default: earth)").StringVar(&cfg.CloudflareRegionKey) app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment) From 517fc3b91e246e09b7b88a33b2f10125970792dd Mon Sep 17 00:00:00 2001 From: Henry Arend Date: Sat, 24 May 2025 07:51:17 -0400 Subject: [PATCH 22/33] feat(cloudflare): update tests for new default value --- pkg/apis/externaldns/types_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 7a81d9f10..ebe71a7bd 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -76,7 +76,7 @@ var ( CloudflareProxied: false, CloudflareCustomHostnames: false, CloudflareCustomHostnamesMinTLSVersion: "1.0", - CloudflareCustomHostnamesCertificateAuthority: "", + CloudflareCustomHostnamesCertificateAuthority: "none", CloudflareDNSRecordsPerPage: 100, CloudflareDNSRecordsComment: "", CloudflareRegionKey: "", From 68b650173ec376300e95bf53a9b51bb38c591dc8 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur Date: Sat, 24 May 2025 19:27:13 +0200 Subject: [PATCH 23/33] chore(ci): fix testify linter --- provider/cloudflare/cloudflare_regional_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/provider/cloudflare/cloudflare_regional_test.go b/provider/cloudflare/cloudflare_regional_test.go index f8273a601..0a79647f6 100644 --- a/provider/cloudflare/cloudflare_regional_test.go +++ b/provider/cloudflare/cloudflare_regional_test.go @@ -24,6 +24,7 @@ import ( "github.com/cloudflare/cloudflare-go" "github.com/stretchr/testify/assert" + "sigs.k8s.io/external-dns/endpoint" ) @@ -124,7 +125,7 @@ func Test_regionalHostname(t *testing.T) { t.Run(tt.name, func(t *testing.T) { p := CloudFlareProvider{RegionKey: tt.args.defaultRegionKey} got := p.regionalHostname(tt.args.endpoint) - assert.Equal(t, got, tt.want) + assert.Equal(t, tt.want, got) }) } } From 756a7288da66fc8b5bb639c44847738fb51f8e7c Mon Sep 17 00:00:00 2001 From: Michel Loiseleur Date: Sun, 25 May 2025 09:42:58 +0200 Subject: [PATCH 24/33] chore!: remove unmaintained providers --- OWNERS | 2 +- README.md | 2 - controller/execute.go | 3 - docs/annotations/annotations.md | 1 - docs/flags.md | 4 +- docs/providers.md | 1 - docs/tutorials/ibmcloud.md | 277 ----- pkg/apis/externaldns/types.go | 8 +- pkg/apis/externaldns/types_test.go | 8 - provider/ibmcloud/ibmcloud.go | 1008 ------------------ provider/ibmcloud/ibmcloud_test.go | 953 ----------------- source/annotations/annotations.go | 1 - source/annotations/provider_specific.go | 6 - source/annotations/provider_specific_test.go | 13 - 14 files changed, 3 insertions(+), 2284 deletions(-) delete mode 100644 docs/tutorials/ibmcloud.md delete mode 100644 provider/ibmcloud/ibmcloud.go delete mode 100644 provider/ibmcloud/ibmcloud_test.go diff --git a/OWNERS b/OWNERS index 978723e6f..dbc97b90d 100644 --- a/OWNERS +++ b/OWNERS @@ -51,6 +51,6 @@ filters: "provider/cloudflare": labels: - provider/cloudflare - "provider/(akamai|alibabacloud|civo|designate|digitalocean|dnsimple|exoscale|gandi|godaddy|ibmcloud|linode|ns1|oci|ovh|pihole|plural|scaleway|tencentcloud|transip|ultradns)": + "provider/(akamai|alibabacloud|civo|designate|digitalocean|dnsimple|exoscale|gandi|godaddy|linode|ns1|oci|ovh|pihole|plural|scaleway|tencentcloud|transip|ultradns)": labels: - provider diff --git a/README.md b/README.md index d3aa4ec6c..aacb436ee 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,6 @@ The following table clarifies the current status of the providers according to t | UltraDNS | Alpha | | | GoDaddy | Alpha | | | Gandi | Alpha | @packi | -| IBMCloud | Alpha | @hughhuangzh | | TencentCloud | Alpha | @Hyzhou | | Plural | Alpha | @michaeljguarino | | Pi-hole | Alpha | @tinyzimmer | @@ -213,7 +212,6 @@ The following tutorials are provided: - [UltraDNS](docs/tutorials/ultradns.md) - [GoDaddy](docs/tutorials/godaddy.md) - [Gandi](docs/tutorials/gandi.md) -- [IBM Cloud](docs/tutorials/ibmcloud.md) - [Nodes as source](docs/sources/nodes.md) - [TencentCloud](docs/tutorials/tencentcloud.md) - [Plural](docs/tutorials/plural.md) diff --git a/controller/execute.go b/controller/execute.go index 6ae1cf5b3..49c83b78b 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -53,7 +53,6 @@ import ( "sigs.k8s.io/external-dns/provider/gandi" "sigs.k8s.io/external-dns/provider/godaddy" "sigs.k8s.io/external-dns/provider/google" - "sigs.k8s.io/external-dns/provider/ibmcloud" "sigs.k8s.io/external-dns/provider/inmemory" "sigs.k8s.io/external-dns/provider/linode" "sigs.k8s.io/external-dns/provider/ns1" @@ -307,8 +306,6 @@ func Execute() { APIVersion: cfg.PiholeApiVersion, }, ) - case "ibmcloud": - p, err = ibmcloud.NewIBMCloudProvider(cfg.IBMCloudConfigFile, domainFilter, zoneIDFilter, endpointsSource, cfg.IBMCloudProxied, cfg.DryRun) case "plural": p, err = plural.NewPluralProvider(cfg.PluralCluster, cfg.PluralProvider) case "tencentcloud": diff --git a/docs/annotations/annotations.md b/docs/annotations/annotations.md index 0c6154b37..c5c172f23 100644 --- a/docs/annotations/annotations.md +++ b/docs/annotations/annotations.md @@ -107,7 +107,6 @@ Some providers define their own annotations. Cloud-specific annotations have key |------------|------------------------------------------------| | AWS | `external-dns.alpha.kubernetes.io/aws-` | | CloudFlare | `external-dns.alpha.kubernetes.io/cloudflare-` | -| IBM Cloud | `external-dns.alpha.kubernetes.io/ibmcloud-` | | Scaleway | `external-dns.alpha.kubernetes.io/scw-` | Additional annotations that are currently implemented only by AWS are: diff --git a/docs/flags.md b/docs/flags.md index 867031aaf..8276d2cec 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -51,7 +51,7 @@ | `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) | | `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group | | `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group | -| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, ibmcloud, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, tencentcloud, transip, ultradns, webhook) | +| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, tencentcloud, transip, ultradns, webhook) | | `--provider-cache-time=0s` | The time to cache the DNS provider record list requests. | | `--domain-filter=` | Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional) | | `--exclude-domains=` | Exclude subdomains (optional) | @@ -120,8 +120,6 @@ | `--[no-]ns1-ignoressl` | When using the NS1 provider, specify whether to verify the SSL certificate (default: false) | | `--ns1-min-ttl=NS1-MIN-TTL` | Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this. | | `--digitalocean-api-page-size=50` | Configure the page size used when querying the DigitalOcean API. | -| `--ibmcloud-config-file="/etc/kubernetes/ibmcloud.json"` | When using the IBM Cloud provider, specify the IBM Cloud configuration file (required when --provider=ibmcloud | -| `--[no-]ibmcloud-proxied` | When using the IBM provider, specify if the proxy mode must be enabled (default: disabled) | | `--godaddy-api-key=""` | When using the GoDaddy provider, specify the API Key (required when --provider=godaddy) | | `--godaddy-api-secret=""` | When using the GoDaddy provider, specify the API secret (required when --provider=godaddy) | | `--godaddy-api-ttl=GODADDY-API-TTL` | TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is not provided. | diff --git a/docs/providers.md b/docs/providers.md index 4a9511aa2..a50344798 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -18,7 +18,6 @@ Provider supported configurations | Gandi | n/a | no | 600 | | GoDaddy | n/a | yes | 600 | | Google GCP | n/a | yes | 300 | -| IBMCloud | n/a | yes | 1 | | InMemory | n/a | n/a | n/a | | Linode | n/a | n/a | n/a | | NS1 | n/a | yes | 10 | diff --git a/docs/tutorials/ibmcloud.md b/docs/tutorials/ibmcloud.md deleted file mode 100644 index 6d3033659..000000000 --- a/docs/tutorials/ibmcloud.md +++ /dev/null @@ -1,277 +0,0 @@ -# IBMCloud - -This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using IBMCloud DNS. - -This tutorial uses [IBMCloud CLI](https://cloud.ibm.com/docs/cli?topic=cli-getting-started) for all -IBM Cloud commands and assumes that the Kubernetes cluster was created via IBM Cloud Kubernetes Service and `kubectl` commands -are being run on an orchestration node. - -## Creating a IBMCloud DNS zone - -The IBMCloud provider for ExternalDNS will find suitable zones for domains it manages; it will -not automatically create zones. -For public zone, This tutorial assume that the [IBMCloud Internet Services](https://cloud.ibm.com/catalog/services/internet-services) was provisioned and the [cis cli plugin](https://cloud.ibm.com/docs/cis?topic=cis-cli-plugin-cis-cli) was installed with IBMCloud CLI -For private zone, This tutorial assume that the [IBMCloud DNS Services](https://cloud.ibm.com/catalog/services/dns-services) was provisioned and the [dns cli plugin](https://cloud.ibm.com/docs/dns-svcs?topic=dns-svcs-cli-plugin-dns-services-cli-commands) was installed with IBMCloud CLI - -### Public Zone - -For this tutorial, we create public zone named `example.com` on IBMCloud Internet Services instance `external-dns-public` - -```sh -ibmcloud cis domain-add example.com -i external-dns-public -``` - -Follow [step](https://cloud.ibm.com/docs/cis?topic=cis-getting-started#configure-your-name-servers-with-the-registrar-or-existing-dns-provider) to active your zone - -### Private Zone - -For this tutorial, we create private zone named `example.com` on IBMCloud DNS Services instance `external-dns-private` - -```sh -ibmcloud dns zone-create example.com -i external-dns-private -``` - -## Creating configuration file - -The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this: - -```json -{ - "apiKey": "1234567890abcdefghijklmnopqrstuvwxyz", - "instanceCrn": "crn:v1:bluemix:public:internet-svcs:global:a/bcf1865e99742d38d2d5fc3fb80a5496:b950da8a-5be6-4691-810e-36388c77b0a3::" -} -``` - -You can create or find the `apiKey` in your ibmcloud IAM --> [API Keys page](https://cloud.ibm.com/iam/apikeys) - -You can find the `instanceCrn` in your service instance details - -Now you can create a file named 'ibmcloud.json' with values gathered above and with the structure of the example above. Use this file to create a Kubernetes secret: - -```sh -kubectl create secret generic ibmcloud-config-file --from-file=/local/path/to/ibmcloud.json -``` - -## Deploy ExternalDNS - -Connect your `kubectl` client to the cluster you want to test ExternalDNS with. -Then apply one of the following manifests file to deploy ExternalDNS. - -### Manifest (for clusters without RBAC enabled) - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: external-dns -spec: - strategy: - type: Recreate - selector: - matchLabels: - app: external-dns - template: - metadata: - labels: - app: external-dns - spec: - containers: - - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.17.0 - args: - - --source=service # ingress is also possible - - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - - --provider=ibmcloud - - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud - volumeMounts: - - name: ibmcloud-config-file - mountPath: /etc/kubernetes - readOnly: true - volumes: - - name: ibmcloud-config-file - secret: - secretName: ibmcloud-config-file - items: - - key: externaldns-config.json - path: ibmcloud.json -``` - -### Manifest (for clusters with RBAC enabled) - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: external-dns ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: external-dns -rules: -- apiGroups: [""] - resources: ["services","endpoints","pods"] - verbs: ["get","watch","list"] -- apiGroups: ["extensions","networking.k8s.io"] - resources: ["ingresses"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["nodes"] - verbs: ["list", "watch"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: external-dns-viewer -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: external-dns -subjects: -- kind: ServiceAccount - name: external-dns - namespace: default ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: external-dns -spec: - strategy: - type: Recreate - selector: - matchLabels: - app: external-dns - template: - metadata: - labels: - app: external-dns - spec: - serviceAccountName: external-dns - containers: - - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.17.0 - args: - - --source=service # ingress is also possible - - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - - --provider=ibmcloud - - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud public zone - volumeMounts: - - name: ibmcloud-config-file - mountPath: /etc/kubernetes - readOnly: true - volumes: - - name: ibmcloud-config-file - secret: - secretName: ibmcloud-config-file - items: - - key: externaldns-config.json - path: ibmcloud.json -``` - -## Deploying an Nginx Service - -Create a service file called `nginx.yaml` with the following contents: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx -spec: - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - image: nginx - name: nginx - ports: - - containerPort: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: nginx - annotations: - external-dns.alpha.kubernetes.io/hostname: www.example.com - external-dns.alpha.kubernetes.io/ttl: "120" #optional -spec: - selector: - app: nginx - type: LoadBalancer - ports: - - protocol: TCP - port: 80 - targetPort: 80 -``` - -Note the annotation on the service; use the hostname as the IBMCloud DNS zone created above. The annotation may also be a subdomain -of the DNS zone (e.g. 'www.example.com'). - -By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above. -This annotation is optional, if you won't set it, it will be 1 (automatic) which is 300. - -ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation -will cause ExternalDNS to remove the corresponding DNS records. - -Create the deployment and service: - -```sh -kubectl create -f nginx.yaml -``` - -Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. - -Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize -the IBMCloud DNS records. - -## Verifying IBMCloud DNS records - -Run the following command to view the A records: - -### Public Zone - -```sh -# Get the domain ID with below command on IBMCloud Internet Services instance `external-dns-public` -$ ibmcloud cis domains -i external-dns-public -# Get the records with domain ID -$ ibmcloud cis dns-records DOMAIN_ID -i external-dns-public -``` - -### Private Zone - -```sh -# Get the domain ID with below command on IBMCloud DNS Services instance `external-dns-private` -$ ibmcloud dns zones -i external-dns-private -# Get the records with domain ID -$ ibmcloud dns resource-records ZONE_ID -i external-dns-public -``` - -This should show the external IP address of the service as the A record for your domain. - -## Cleanup - -Now that we have verified that ExternalDNS will automatically manage IBMCloud DNS records, we can delete the tutorial's example: - -```sh -kubectl delete -f nginx.yaml -kubectl delete -f externaldns.yaml -``` - -## Setting proxied records on public zone - -Using the `external-dns.alpha.kubernetes.io/ibmcloud-proxied: "true"` annotation on your ingress or service, you can specify if the proxy feature of IBMCloud public DNS should be enabled for that record. This setting will override the global `--ibmcloud-proxied` setting. - -## Active priviate zone with VPC allocated - -By default, IBMCloud DNS Services don't active your private zone with new zone added. -With External DNS, you can use `external-dns.alpha.kubernetes.io/ibmcloud-vpc: "crn:v1:bluemix:public:is:us-south:a/bcf1865e99742d38d2d5fc3fb80a5496::vpc:r006-74353823-a60d-42e4-97c5-5e2551278435"` annotation on your ingress or service. -It will active your private zone with in specific VPC for that record created in. -This setting won't work if the private zone was active already. - -Note: the annotaion value is the VPC CRN, every IBM Cloud service have a valid CRN. diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index df96f71bd..d1389c3e4 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -199,8 +199,6 @@ type Config struct { GoDaddyTTL int64 GoDaddyOTE bool OCPRouterName string - IBMCloudProxied bool - IBMCloudConfigFile string TencentCloudConfigFile string TencentCloudZoneType string PiholeServer string @@ -293,8 +291,6 @@ var defaultConfig = &Config{ GoogleBatchChangeSize: 1000, GoogleProject: "", GoogleZoneVisibility: "", - IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", - IBMCloudProxied: false, IgnoreHostnameAnnotation: false, IgnoreIngressRulesSpec: false, IgnoreIngressTLSSpec: false, @@ -495,7 +491,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew) // Flags related to providers - providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"} + providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"} app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...) app.Flag("provider-cache-time", "The time to cache the DNS provider record list requests.").Default(defaultConfig.ProviderCacheTime.String()).DurationVar(&cfg.ProviderCacheTime) app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) @@ -567,8 +563,6 @@ func App(cfg *Config) *kingpin.Application { app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL) app.Flag("ns1-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.NS1MinTTLSeconds) app.Flag("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIPageSize)).IntVar(&cfg.DigitalOceanAPIPageSize) - app.Flag("ibmcloud-config-file", "When using the IBM Cloud provider, specify the IBM Cloud configuration file (required when --provider=ibmcloud").Default(defaultConfig.IBMCloudConfigFile).StringVar(&cfg.IBMCloudConfigFile) - app.Flag("ibmcloud-proxied", "When using the IBM provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.IBMCloudProxied) // GoDaddy flags app.Flag("godaddy-api-key", "When using the GoDaddy provider, specify the API Key (required when --provider=godaddy)").Default(defaultConfig.GoDaddyAPIKey).StringVar(&cfg.GoDaddyAPIKey) app.Flag("godaddy-api-secret", "When using the GoDaddy provider, specify the API secret (required when --provider=godaddy)").Default(defaultConfig.GoDaddySecretKey).StringVar(&cfg.GoDaddySecretKey) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index fe13da1a5..a41124389 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -125,8 +125,6 @@ var ( RFC2136Host: []string{""}, RFC2136LoadBalancingStrategy: "disabled", OCPRouterName: "default", - IBMCloudProxied: false, - IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", TencentCloudZoneType: "", PiholeApiVersion: "5", @@ -242,8 +240,6 @@ var ( RFC2136BatchChangeSize: 100, RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"}, RFC2136LoadBalancingStrategy: "round-robin", - IBMCloudProxied: true, - IBMCloudConfigFile: "ibmcloud.json", TencentCloudConfigFile: "tencent-cloud.json", TencentCloudZoneType: "private", PiholeApiVersion: "6", @@ -396,8 +392,6 @@ func TestParseFlags(t *testing.T) { "--rfc2136-load-balancing-strategy=round-robin", "--rfc2136-host=rfc2136-host1", "--rfc2136-host=rfc2136-host2", - "--ibmcloud-proxied", - "--ibmcloud-config-file=ibmcloud.json", "--tencent-cloud-config-file=tencent-cloud.json", "--tencent-cloud-zone-type=private", }, @@ -516,8 +510,6 @@ func TestParseFlags(t *testing.T) { "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", }, diff --git a/provider/ibmcloud/ibmcloud.go b/provider/ibmcloud/ibmcloud.go deleted file mode 100644 index 4f3d33a35..000000000 --- a/provider/ibmcloud/ibmcloud.go +++ /dev/null @@ -1,1008 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package ibmcloud - -import ( - "context" - "fmt" - "os" - "reflect" - "strconv" - "strings" - - "github.com/IBM-Cloud/ibm-cloud-cli-sdk/bluemix/crn" - "github.com/IBM/go-sdk-core/v5/core" - "github.com/IBM/networking-go-sdk/dnsrecordsv1" - "github.com/IBM/networking-go-sdk/dnssvcsv1" - "github.com/IBM/networking-go-sdk/zonesv1" - yaml "github.com/goccy/go-yaml" - - log "github.com/sirupsen/logrus" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" - "sigs.k8s.io/external-dns/source" -) - -var proxyTypeNotSupported = map[string]bool{ - "LOC": true, - "MX": true, - "NS": true, - "SPF": true, - "TXT": true, - "SRV": true, -} - -var privateTypeSupported = map[string]bool{ - "A": true, - "CNAME": true, - "TXT": true, -} - -const ( - // recordCreate is a ChangeAction enum value - recordCreate = "CREATE" - // recordDelete is a ChangeAction enum value - recordDelete = "DELETE" - // recordUpdate is a ChangeAction enum value - recordUpdate = "UPDATE" - // defaultTTL 1 = automatic - defaultTTL = 1 - - proxyFilter = "ibmcloud-proxied" - vpcFilter = "ibmcloud-vpc" - zoneStatePendingNetwork = "PENDING_NETWORK_ADD" - zoneStateActive = "ACTIVE" -) - -// Source shadow the interface source.Source. used primarily for unit testing. -type Source interface { - Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) - AddEventHandler(context.Context, func()) -} - -// ibmcloudClient is a minimal implementation of DNS API that we actually use, used primarily for unit testing. -type ibmcloudClient interface { - ListAllDDNSRecordsWithContext(ctx context.Context, listAllDNSRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (result *dnsrecordsv1.ListDnsrecordsResp, response *core.DetailedResponse, err error) - CreateDNSRecordWithContext(ctx context.Context, createDNSRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) - DeleteDNSRecordWithContext(ctx context.Context, deleteDNSRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (result *dnsrecordsv1.DeleteDnsrecordResp, response *core.DetailedResponse, err error) - UpdateDNSRecordWithContext(ctx context.Context, updateDNSRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) - ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (result *dnssvcsv1.ListDnszones, response *core.DetailedResponse, err error) - GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (result *dnssvcsv1.Dnszone, response *core.DetailedResponse, err error) - CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (result *dnssvcsv1.PermittedNetwork, response *core.DetailedResponse, err error) - ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (result *dnssvcsv1.ListResourceRecords, response *core.DetailedResponse, err error) - CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) - DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (response *core.DetailedResponse, err error) - UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) - NewResourceRecordInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordInputRdataRdataARecord, err error) - NewResourceRecordInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, err error) - NewResourceRecordInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, err error) - NewResourceRecordUpdateInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, err error) - NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, err error) - NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, err error) -} - -type ibmcloudService struct { - publicZonesService *zonesv1.ZonesV1 - publicRecordsService *dnsrecordsv1.DnsRecordsV1 - privateDNSService *dnssvcsv1.DnsSvcsV1 -} - -func (i ibmcloudService) ListAllDDNSRecordsWithContext(ctx context.Context, listAllDNSRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (result *dnsrecordsv1.ListDnsrecordsResp, response *core.DetailedResponse, err error) { - return i.publicRecordsService.ListAllDnsRecordsWithContext(ctx, listAllDNSRecordsOptions) -} - -func (i ibmcloudService) CreateDNSRecordWithContext(ctx context.Context, createDNSRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) { - return i.publicRecordsService.CreateDnsRecordWithContext(ctx, createDNSRecordOptions) -} - -func (i ibmcloudService) DeleteDNSRecordWithContext(ctx context.Context, deleteDNSRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (result *dnsrecordsv1.DeleteDnsrecordResp, response *core.DetailedResponse, err error) { - return i.publicRecordsService.DeleteDnsRecordWithContext(ctx, deleteDNSRecordOptions) -} - -func (i ibmcloudService) UpdateDNSRecordWithContext(ctx context.Context, updateDNSRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) { - return i.publicRecordsService.UpdateDnsRecordWithContext(ctx, updateDNSRecordOptions) -} - -func (i ibmcloudService) ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (result *dnssvcsv1.ListDnszones, response *core.DetailedResponse, err error) { - return i.privateDNSService.ListDnszonesWithContext(ctx, listDnszonesOptions) -} - -func (i ibmcloudService) GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (result *dnssvcsv1.Dnszone, response *core.DetailedResponse, err error) { - return i.privateDNSService.GetDnszoneWithContext(ctx, getDnszoneOptions) -} - -func (i ibmcloudService) CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (result *dnssvcsv1.PermittedNetwork, response *core.DetailedResponse, err error) { - return i.privateDNSService.CreatePermittedNetworkWithContext(ctx, createPermittedNetworkOptions) -} - -func (i ibmcloudService) ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (result *dnssvcsv1.ListResourceRecords, response *core.DetailedResponse, err error) { - return i.privateDNSService.ListResourceRecordsWithContext(ctx, listResourceRecordsOptions) -} - -func (i ibmcloudService) CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) { - return i.privateDNSService.CreateResourceRecordWithContext(ctx, createResourceRecordOptions) -} - -func (i ibmcloudService) DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (response *core.DetailedResponse, err error) { - return i.privateDNSService.DeleteResourceRecordWithContext(ctx, deleteResourceRecordOptions) -} - -func (i ibmcloudService) UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) { - return i.privateDNSService.UpdateResourceRecordWithContext(ctx, updateResourceRecordOptions) -} - -func (i ibmcloudService) NewResourceRecordInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordInputRdataRdataARecord, err error) { - return i.privateDNSService.NewResourceRecordInputRdataRdataARecord(ip) -} - -func (i ibmcloudService) NewResourceRecordInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, err error) { - return i.privateDNSService.NewResourceRecordInputRdataRdataCnameRecord(cname) -} - -func (i ibmcloudService) NewResourceRecordInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, err error) { - return i.privateDNSService.NewResourceRecordInputRdataRdataTxtRecord(text) -} - -func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, err error) { - return i.privateDNSService.NewResourceRecordUpdateInputRdataRdataARecord(ip) -} - -func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, err error) { - return i.privateDNSService.NewResourceRecordUpdateInputRdataRdataCnameRecord(cname) -} - -func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, err error) { - return i.privateDNSService.NewResourceRecordUpdateInputRdataRdataTxtRecord(text) -} - -// IBMCloudProvider is an implementation of Provider for IBM Cloud DNS. -type IBMCloudProvider struct { - provider.BaseProvider - source Source - Client ibmcloudClient - // only consider hosted zones managing domains ending in this suffix - domainFilter endpoint.DomainFilter - zoneIDFilter provider.ZoneIDFilter - instanceID string - privateZone bool - proxiedByDefault bool - DryRun bool -} - -type ibmcloudConfig struct { - Endpoint string `json:"endpoint" yaml:"endpoint"` - APIKey string `json:"apiKey" yaml:"apiKey"` - CRN string `json:"instanceCrn" yaml:"instanceCrn"` - IAMURL string `json:"iamUrl" yaml:"iamUrl"` - InstanceID string `json:"-" yaml:"-"` -} - -// ibmcloudChange differentiates between ChangActions -type ibmcloudChange struct { - Action string - PublicResourceRecord dnsrecordsv1.DnsrecordDetails - PrivateResourceRecord dnssvcsv1.ResourceRecord -} - -func getConfig(configFile string) (*ibmcloudConfig, error) { - contents, err := os.ReadFile(configFile) - if err != nil { - return nil, fmt.Errorf("failed to read IBM Cloud config file '%s': %w", configFile, err) - } - cfg := &ibmcloudConfig{} - err = yaml.Unmarshal(contents, &cfg) - if err != nil { - return nil, fmt.Errorf("failed to read IBM Cloud config file '%s': %w", configFile, err) - } - - return cfg, nil -} - -func (c *ibmcloudConfig) Validate(authenticator core.Authenticator, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter) (ibmcloudService, bool, error) { - var service ibmcloudService - isPrivate := false - log.Debugf("filters: %v, %v", domainFilter.Filters, zoneIDFilter.ZoneIDs) - if (len(domainFilter.Filters) == 0 || domainFilter.Filters[0] == "") && zoneIDFilter.ZoneIDs[0] == "" { - return service, isPrivate, fmt.Errorf("at lease one of filters: 'domain-filter', 'zone-id-filter' needed") - } - - crn, err := crn.Parse(c.CRN) - if err != nil { - return service, isPrivate, err - } - log.Infof("IBM Cloud Service: %s", crn.ServiceName) - c.InstanceID = crn.ServiceInstance - - switch { - case strings.Contains(crn.ServiceName, "internet-svcs"): - if len(domainFilter.Filters) > 1 || len(zoneIDFilter.ZoneIDs) > 1 { - return service, isPrivate, fmt.Errorf("for public zone, only one domain id filter or domain name filter allowed") - } - var zoneID string - // Public DNS service - service.publicZonesService, err = zonesv1.NewZonesV1(&zonesv1.ZonesV1Options{ - Authenticator: authenticator, - Crn: core.StringPtr(c.CRN), - }) - if err != nil { - return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud public zones client: %w", err) - } - if c.Endpoint != "" { - _ = service.publicZonesService.SetServiceURL(c.Endpoint) - } - - zonesResp, _, err := service.publicZonesService.ListZones(&zonesv1.ListZonesOptions{}) - if err != nil { - return service, isPrivate, fmt.Errorf("failed to list ibmcloud public zones: %w", err) - } - for _, zone := range zonesResp.Result { - log.Debugf("zoneName: %s, zoneID: %s", *zone.Name, *zone.ID) - if len(domainFilter.Filters) > 0 && domainFilter.Filters[0] != "" && domainFilter.Match(*zone.Name) { - log.Debugf("zone %s found.", *zone.ID) - zoneID = *zone.ID - break - } - if len(zoneIDFilter.ZoneIDs[0]) != 0 && zoneIDFilter.Match(*zone.ID) { - log.Debugf("zone %s found.", *zone.ID) - zoneID = *zone.ID - break - } - } - if len(zoneID) == 0 { - return service, isPrivate, fmt.Errorf("no matched zone found") - } - - service.publicRecordsService, err = dnsrecordsv1.NewDnsRecordsV1(&dnsrecordsv1.DnsRecordsV1Options{ - Authenticator: authenticator, - Crn: core.StringPtr(c.CRN), - ZoneIdentifier: core.StringPtr(zoneID), - }) - if err != nil { - return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud public records client: %w", err) - } - if c.Endpoint != "" { - _ = service.publicRecordsService.SetServiceURL(c.Endpoint) - } - case strings.Contains(crn.ServiceName, "dns-svcs"): - isPrivate = true - // Private DNS service - service.privateDNSService, err = dnssvcsv1.NewDnsSvcsV1(&dnssvcsv1.DnsSvcsV1Options{ - Authenticator: authenticator, - }) - if err != nil { - return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud private records client: %w", err) - } - if c.Endpoint != "" { - _ = service.privateDNSService.SetServiceURL(c.Endpoint) - } - default: - return service, isPrivate, fmt.Errorf("IBM Cloud instance crn is not provided or invalid dns crn : %s", c.CRN) - } - - return service, isPrivate, nil -} - -// NewIBMCloudProvider creates a new IBMCloud provider. -// -// Returns the provider or an error if a provider could not be created. -func NewIBMCloudProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, source source.Source, proxiedByDefault bool, dryRun bool) (*IBMCloudProvider, error) { - cfg, err := getConfig(configFile) - if err != nil { - return nil, err - } - - authenticator := &core.IamAuthenticator{ - ApiKey: cfg.APIKey, - } - if cfg.IAMURL != "" { - authenticator = &core.IamAuthenticator{ - ApiKey: cfg.APIKey, - URL: cfg.IAMURL, - } - } - - client, isPrivate, err := cfg.Validate(authenticator, domainFilter, zoneIDFilter) - if err != nil { - return nil, err - } - - return &IBMCloudProvider{ - Client: client, - source: source, - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - instanceID: cfg.InstanceID, - privateZone: isPrivate, - proxiedByDefault: proxiedByDefault, - DryRun: dryRun, - }, nil -} - -// Records gets the current records. -// -// Returns the current records or an error if the operation failed. -func (p *IBMCloudProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) { - if p.privateZone { - endpoints, err = p.privateRecords(ctx) - } else { - endpoints, err = p.publicRecords(ctx) - } - return endpoints, err -} - -// ApplyChanges applies a given set of changes in a given zone. -func (p *IBMCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - log.Debugln("applying change...") - ibmcloudChanges := []*ibmcloudChange{} - for _, et := range changes.Create { - for _, target := range et.Targets { - ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordCreate, et, target)) - } - } - - for i, desired := range changes.UpdateNew { - current := changes.UpdateOld[i] - - add, remove, leave := provider.Difference(current.Targets, desired.Targets) - - log.Debugf("add: %v, remove: %v, leave: %v", add, remove, leave) - for _, a := range add { - ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordCreate, desired, a)) - } - - for _, a := range leave { - ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordUpdate, desired, a)) - } - - for _, a := range remove { - ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordDelete, current, a)) - } - } - - for _, et := range changes.Delete { - for _, target := range et.Targets { - ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordDelete, et, target)) - } - } - - return p.submitChanges(ctx, ibmcloudChanges) -} - -// AdjustEndpoints modifies the endpoints as needed by the specific provider -func (p *IBMCloudProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { - adjustedEndpoints := []*endpoint.Endpoint{} - for _, e := range endpoints { - log.Debugf("adjusting endpont: %v", *e) - proxied := shouldBeProxied(e, p.proxiedByDefault) - if proxied { - e.RecordTTL = 0 - } - e.SetProviderSpecificProperty(proxyFilter, strconv.FormatBool(proxied)) - - adjustedEndpoints = append(adjustedEndpoints, e) - } - return adjustedEndpoints, nil -} - -// submitChanges takes a zone and a collection of Changes and sends them as a single transaction. -func (p *IBMCloudProvider) submitChanges(ctx context.Context, changes []*ibmcloudChange) error { - // return early if there is nothing to change - if len(changes) == 0 { - return nil - } - - log.Debugln("submmiting change...") - if p.privateZone { - return p.submitChangesForPrivateDNS(ctx, changes) - } - return p.submitChangesForPublicDNS(ctx, changes) -} - -// submitChangesForPublicDNS takes a zone and a collection of Changes and sends them as a single transaction on public dns. -func (p *IBMCloudProvider) submitChangesForPublicDNS(ctx context.Context, changes []*ibmcloudChange) error { - records, err := p.listAllPublicRecords(ctx) - if err != nil { - return err - } - - for _, change := range changes { - logFields := log.Fields{ - "record": *change.PublicResourceRecord.Name, - "type": *change.PublicResourceRecord.Type, - "ttl": *change.PublicResourceRecord.TTL, - "action": change.Action, - } - - if p.DryRun { - continue - } - - log.WithFields(logFields).Info("Changing record.") - - if change.Action == recordUpdate { - recordID := p.getPublicRecordID(records, change.PublicResourceRecord) - if recordID == "" { - log.WithFields(logFields).Errorf("failed to find previous record: %v", *change.PublicResourceRecord.Name) - continue - } - p.updateRecord(ctx, "", recordID, change) - } else if change.Action == recordDelete { - recordID := p.getPublicRecordID(records, change.PublicResourceRecord) - if recordID == "" { - log.WithFields(logFields).Errorf("failed to find previous record: %v", *change.PublicResourceRecord.Name) - continue - } - p.deleteRecord(ctx, "", recordID) - } else if change.Action == recordCreate { - p.createRecord(ctx, "", change) - } - } - - return nil -} - -// submitChangesForPrivateDNS takes a zone and a collection of Changes and sends them as a single transaction on private dns. -func (p *IBMCloudProvider) submitChangesForPrivateDNS(ctx context.Context, changes []*ibmcloudChange) error { - zones, err := p.privateZones(ctx) - if err != nil { - return err - } - // separate into per-zone change sets to be passed to the API. - changesByPrivateZone := p.changesByPrivateZone(ctx, zones, changes) - - for zoneID, changes := range changesByPrivateZone { - records, err := p.listAllPrivateRecords(ctx, zoneID) - if err != nil { - return err - } - - for _, change := range changes { - logFields := log.Fields{ - "record": *change.PrivateResourceRecord.Name, - "type": *change.PrivateResourceRecord.Type, - "ttl": *change.PrivateResourceRecord.TTL, - "action": change.Action, - } - - log.WithFields(logFields).Info("Changing record.") - - if p.DryRun { - continue - } - - if change.Action == recordUpdate { - recordID := p.getPrivateRecordID(records, change.PrivateResourceRecord) - if recordID == "" { - log.WithFields(logFields).Errorf("failed to find previous record: %v", change.PrivateResourceRecord) - continue - } - p.updateRecord(ctx, zoneID, recordID, change) - } else if change.Action == recordDelete { - recordID := p.getPrivateRecordID(records, change.PrivateResourceRecord) - if recordID == "" { - log.WithFields(logFields).Errorf("failed to find previous record: %v", change.PrivateResourceRecord) - continue - } - p.deleteRecord(ctx, zoneID, recordID) - } else if change.Action == recordCreate { - p.createRecord(ctx, zoneID, change) - } - } - } - - return nil -} - -// privateZones return zones in private dns -func (p *IBMCloudProvider) privateZones(ctx context.Context) ([]dnssvcsv1.Dnszone, error) { - result := []dnssvcsv1.Dnszone{} - // if there is a zoneIDfilter configured - // && if the filter isn't just a blank string (used in tests) - if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" { - log.Debugln("zoneIDFilter configured. only looking up zone IDs defined") - for _, zoneID := range p.zoneIDFilter.ZoneIDs { - log.Debugf("looking up zone %s", zoneID) - detailResponse, _, err := p.Client.GetDnszoneWithContext(ctx, &dnssvcsv1.GetDnszoneOptions{ - InstanceID: core.StringPtr(p.instanceID), - DnszoneID: core.StringPtr(zoneID), - }) - if err != nil { - log.Errorf("zone %s lookup failed, %v", zoneID, err) - continue - } - log.WithFields(log.Fields{ - "zoneName": *detailResponse.Name, - "zoneID": *detailResponse.ID, - }).Debugln("adding zone for consideration") - result = append(result, *detailResponse) - } - return result, nil - } - - log.Debugln("no zoneIDFilter configured, looking at all zones") - - zonesResponse, _, err := p.Client.ListDnszonesWithContext(ctx, &dnssvcsv1.ListDnszonesOptions{ - InstanceID: core.StringPtr(p.instanceID), - }) - if err != nil { - return nil, err - } - - for _, zone := range zonesResponse.Dnszones { - if !p.domainFilter.Match(*zone.Name) { - log.Debugf("zone %s not in domain filter", *zone.Name) - continue - } - result = append(result, zone) - } - - return result, nil -} - -// activePrivateZone active zone with new records add if not active -func (p *IBMCloudProvider) activePrivateZone(ctx context.Context, zoneID, vpc string) { - permittedNetworkVpc := &dnssvcsv1.PermittedNetworkVpc{ - VpcCrn: core.StringPtr(vpc), - } - createPermittedNetworkOptions := &dnssvcsv1.CreatePermittedNetworkOptions{ - InstanceID: core.StringPtr(p.instanceID), - DnszoneID: core.StringPtr(zoneID), - PermittedNetwork: permittedNetworkVpc, - Type: core.StringPtr("vpc"), - } - _, _, err := p.Client.CreatePermittedNetworkWithContext(ctx, createPermittedNetworkOptions) - if err != nil { - log.Errorf("failed to active zone %s in VPC %s with error: %v", zoneID, vpc, err) - } -} - -// changesByPrivateZone separates a multi-zone change into a single change per zone. -func (p *IBMCloudProvider) changesByPrivateZone(ctx context.Context, zones []dnssvcsv1.Dnszone, changeSet []*ibmcloudChange) map[string][]*ibmcloudChange { - changes := make(map[string][]*ibmcloudChange) - zoneNameIDMapper := provider.ZoneIDName{} - for _, z := range zones { - zoneNameIDMapper.Add(*z.ID, *z.Name) - changes[*z.ID] = []*ibmcloudChange{} - } - - for _, c := range changeSet { - zoneID, _ := zoneNameIDMapper.FindZone(*c.PrivateResourceRecord.Name) - if zoneID == "" { - log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", *c.PrivateResourceRecord.Name) - continue - } - changes[zoneID] = append(changes[zoneID], c) - } - - return changes -} - -func (p *IBMCloudProvider) publicRecords(ctx context.Context) ([]*endpoint.Endpoint, error) { - log.Debugf("Listing records on public zone") - dnsRecords, err := p.listAllPublicRecords(ctx) - if err != nil { - return nil, err - } - return p.groupPublicRecords(dnsRecords), nil -} - -func (p *IBMCloudProvider) listAllPublicRecords(ctx context.Context) ([]dnsrecordsv1.DnsrecordDetails, error) { - var dnsRecords []dnsrecordsv1.DnsrecordDetails - page := 1 -GETRECORDS: - listAllDNSRecordsOptions := &dnsrecordsv1.ListAllDnsRecordsOptions{ - Page: core.Int64Ptr(int64(page)), - } - records, _, err := p.Client.ListAllDDNSRecordsWithContext(ctx, listAllDNSRecordsOptions) - if err != nil { - return dnsRecords, err - } - dnsRecords = append(dnsRecords, records.Result...) - // Loop if more records exist - if *records.ResultInfo.TotalCount > int64(page*100) { - page = page + 1 - log.Debugf("More than one pages records found, page: %d", page) - goto GETRECORDS - } - return dnsRecords, nil -} - -func (p *IBMCloudProvider) groupPublicRecords(records []dnsrecordsv1.DnsrecordDetails) []*endpoint.Endpoint { - endpoints := []*endpoint.Endpoint{} - - // group supported records by name and type - groups := map[string][]dnsrecordsv1.DnsrecordDetails{} - - for _, r := range records { - if !provider.SupportedRecordType(*r.Type) { - continue - } - - groupBy := *r.Name + *r.Type - if _, ok := groups[groupBy]; !ok { - groups[groupBy] = []dnsrecordsv1.DnsrecordDetails{} - } - - groups[groupBy] = append(groups[groupBy], r) - } - - // create single endpoint with all the targets for each name/type - for _, records := range groups { - targets := make([]string, len(records)) - for i, record := range records { - targets[i] = *record.Content - } - - ep := endpoint.NewEndpointWithTTL( - *records[0].Name, - *records[0].Type, - endpoint.TTL(*records[0].TTL), - targets...).WithProviderSpecific(proxyFilter, strconv.FormatBool(*records[0].Proxied)) - - log.Debugf( - "Found %s record for '%s' with target '%s'.", - ep.RecordType, - ep.DNSName, - ep.Targets, - ) - - endpoints = append(endpoints, ep) - } - return endpoints -} - -func (p *IBMCloudProvider) privateRecords(ctx context.Context) ([]*endpoint.Endpoint, error) { - log.Debugf("Listing records on private zone") - var vpc string - zones, err := p.privateZones(ctx) - if err != nil { - return nil, err - } - sources, err := p.source.Endpoints(ctx) - if err != nil { - return nil, err - } - // Filter VPC annoation for private zone active - for _, src := range sources { - vpc = checkVPCAnnotation(src) - if len(vpc) > 0 { - log.Debugf("VPC found: %s", vpc) - break - } - } - - var endpoints []*endpoint.Endpoint - for _, zone := range zones { - if len(vpc) > 0 && *zone.State == zoneStatePendingNetwork { - log.Debugf("active zone: %s", *zone.ID) - p.activePrivateZone(ctx, *zone.ID, vpc) - } - - dnsRecords, err := p.listAllPrivateRecords(ctx, *zone.ID) - if err != nil { - return nil, err - } - endpoints = append(endpoints, p.groupPrivateRecords(dnsRecords)...) - } - - return endpoints, nil -} - -func (p *IBMCloudProvider) listAllPrivateRecords(ctx context.Context, zoneID string) ([]dnssvcsv1.ResourceRecord, error) { - var dnsRecords []dnssvcsv1.ResourceRecord - offset := 0 -GETRECORDS: - listResourceRecordsOptions := &dnssvcsv1.ListResourceRecordsOptions{ - InstanceID: core.StringPtr(p.instanceID), - DnszoneID: core.StringPtr(zoneID), - Offset: core.Int64Ptr(int64(offset)), - } - records, _, err := p.Client.ListResourceRecordsWithContext(ctx, listResourceRecordsOptions) - if err != nil { - return dnsRecords, err - } - oRecords := records.ResourceRecords - dnsRecords = append(dnsRecords, oRecords...) - // Loop if more records exist - if int64(offset+1) < *records.TotalCount && int64(offset+200) < *records.TotalCount { - offset = offset + 200 - log.Debugf("More than one pages records found, page: %d", offset/200+1) - goto GETRECORDS - } - return dnsRecords, nil -} - -func (p *IBMCloudProvider) groupPrivateRecords(records []dnssvcsv1.ResourceRecord) []*endpoint.Endpoint { - var endpoints []*endpoint.Endpoint - // group supported records by name and type - groups := map[string][]dnssvcsv1.ResourceRecord{} - for _, r := range records { - if !provider.SupportedRecordType(*r.Type) || !privateTypeSupported[*r.Type] { - continue - } - rname := *r.Name - rtype := *r.Type - groupBy := rname + rtype - if _, ok := groups[groupBy]; !ok { - groups[groupBy] = []dnssvcsv1.ResourceRecord{} - } - - groups[groupBy] = append(groups[groupBy], r) - } - - // create single endpoint with all the targets for each name/type - for _, records := range groups { - targets := make([]string, len(records)) - for i, record := range records { - data := record.Rdata - log.Debugf("record data: %v", data) - switch *record.Type { - case "A": - if !isNil(data["ip"]) { - targets[i] = data["ip"].(string) - } - case "CNAME": - if !isNil(data["cname"]) { - targets[i] = data["cname"].(string) - } - case "TXT": - if !isNil(data["text"]) { - targets[i] = data["text"].(string) - } - log.Debugf("text record data: %v", targets[i]) - } - } - - ep := endpoint.NewEndpointWithTTL( - *records[0].Name, - *records[0].Type, - endpoint.TTL(*records[0].TTL), targets...) - - log.Debugf( - "Found %s record for '%s' with target '%s'.", - ep.RecordType, - ep.DNSName, - ep.Targets, - ) - - endpoints = append(endpoints, ep) - } - return endpoints -} - -func (p *IBMCloudProvider) getPublicRecordID(records []dnsrecordsv1.DnsrecordDetails, record dnsrecordsv1.DnsrecordDetails) string { - for _, zoneRecord := range records { - if *zoneRecord.Name == *record.Name && *zoneRecord.Type == *record.Type && *zoneRecord.Content == *record.Content { - return *zoneRecord.ID - } - } - return "" -} - -func (p *IBMCloudProvider) getPrivateRecordID(records []dnssvcsv1.ResourceRecord, record dnssvcsv1.ResourceRecord) string { - for _, zoneRecord := range records { - if *zoneRecord.Name == *record.Name && *zoneRecord.Type == *record.Type { - return *zoneRecord.ID - } - } - return "" -} - -func (p *IBMCloudProvider) newIBMCloudChange(action string, endpoint *endpoint.Endpoint, target string) *ibmcloudChange { - ttl := defaultTTL - proxied := shouldBeProxied(endpoint, p.proxiedByDefault) - - if endpoint.RecordTTL.IsConfigured() { - ttl = int(endpoint.RecordTTL) - } - - if p.privateZone { - rData := make(map[string]interface{}) - switch endpoint.RecordType { - case "A": - rData[dnssvcsv1.CreateResourceRecordOptions_Type_A] = &dnssvcsv1.ResourceRecordInputRdataRdataARecord{ - Ip: core.StringPtr(target), - } - case "CNAME": - rData[dnssvcsv1.CreateResourceRecordOptions_Type_Cname] = &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{ - Cname: core.StringPtr(target), - } - case "TXT": - rData[dnssvcsv1.CreateResourceRecordOptions_Type_Txt] = &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{ - Text: core.StringPtr(target), - } - } - return &ibmcloudChange{ - Action: action, - PrivateResourceRecord: dnssvcsv1.ResourceRecord{ - Name: core.StringPtr(endpoint.DNSName), - TTL: core.Int64Ptr(int64(ttl)), - Type: core.StringPtr(endpoint.RecordType), - Rdata: rData, - }, - } - } - - return &ibmcloudChange{ - Action: action, - PublicResourceRecord: dnsrecordsv1.DnsrecordDetails{ - Name: core.StringPtr(endpoint.DNSName), - TTL: core.Int64Ptr(int64(ttl)), - Proxied: core.BoolPtr(proxied), - Type: core.StringPtr(endpoint.RecordType), - Content: core.StringPtr(target), - }, - } -} - -func (p *IBMCloudProvider) createRecord(ctx context.Context, zoneID string, change *ibmcloudChange) { - if p.privateZone { - createResourceRecordOptions := &dnssvcsv1.CreateResourceRecordOptions{ - InstanceID: core.StringPtr(p.instanceID), - DnszoneID: core.StringPtr(zoneID), - Name: change.PrivateResourceRecord.Name, - Type: change.PrivateResourceRecord.Type, - TTL: change.PrivateResourceRecord.TTL, - } - switch *change.PrivateResourceRecord.Type { - case "A": - data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) - aData, _ := p.Client.NewResourceRecordInputRdataRdataARecord(*data.Ip) - createResourceRecordOptions.SetRdata(aData) - case "CNAME": - data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) - cnameData, _ := p.Client.NewResourceRecordInputRdataRdataCnameRecord(*data.Cname) - createResourceRecordOptions.SetRdata(cnameData) - case "TXT": - data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) - txtData, _ := p.Client.NewResourceRecordInputRdataRdataTxtRecord(*data.Text) - createResourceRecordOptions.SetRdata(txtData) - } - _, _, err := p.Client.CreateResourceRecordWithContext(ctx, createResourceRecordOptions) - if err != nil { - log.Errorf("failed to create %s type record named %s: %v", *change.PrivateResourceRecord.Type, *change.PrivateResourceRecord.Name, err) - } - } else { - createDNSRecordOptions := &dnsrecordsv1.CreateDnsRecordOptions{ - Name: change.PublicResourceRecord.Name, - Type: change.PublicResourceRecord.Type, - TTL: change.PublicResourceRecord.TTL, - Content: change.PublicResourceRecord.Content, - } - _, _, err := p.Client.CreateDNSRecordWithContext(ctx, createDNSRecordOptions) - if err != nil { - log.Errorf("failed to create %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err) - } - } -} - -func (p *IBMCloudProvider) updateRecord(ctx context.Context, zoneID, recordID string, change *ibmcloudChange) { - if p.privateZone { - updateResourceRecordOptions := &dnssvcsv1.UpdateResourceRecordOptions{ - InstanceID: core.StringPtr(p.instanceID), - DnszoneID: core.StringPtr(zoneID), - RecordID: core.StringPtr(recordID), - Name: change.PrivateResourceRecord.Name, - TTL: change.PrivateResourceRecord.TTL, - } - switch *change.PrivateResourceRecord.Type { - case "A": - data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) - aData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataARecord(*data.Ip) - updateResourceRecordOptions.SetRdata(aData) - case "CNAME": - data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) - cnameData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataCnameRecord(*data.Cname) - updateResourceRecordOptions.SetRdata(cnameData) - case "TXT": - data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) - txtData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataTxtRecord(*data.Text) - updateResourceRecordOptions.SetRdata(txtData) - } - _, _, err := p.Client.UpdateResourceRecordWithContext(ctx, updateResourceRecordOptions) - if err != nil { - log.Errorf("failed to update %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err) - } - } else { - updateDNSRecordOptions := &dnsrecordsv1.UpdateDnsRecordOptions{ - DnsrecordIdentifier: &recordID, - Name: change.PublicResourceRecord.Name, - Type: change.PublicResourceRecord.Type, - TTL: change.PublicResourceRecord.TTL, - Content: change.PublicResourceRecord.Content, - Proxied: change.PublicResourceRecord.Proxied, - } - _, _, err := p.Client.UpdateDNSRecordWithContext(ctx, updateDNSRecordOptions) - if err != nil { - log.Errorf("failed to update %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err) - } - } -} - -func (p *IBMCloudProvider) deleteRecord(ctx context.Context, zoneID, recordID string) { - if p.privateZone { - deleteResourceRecordOptions := &dnssvcsv1.DeleteResourceRecordOptions{ - InstanceID: core.StringPtr(p.instanceID), - DnszoneID: core.StringPtr(zoneID), - RecordID: core.StringPtr(recordID), - } - _, err := p.Client.DeleteResourceRecordWithContext(ctx, deleteResourceRecordOptions) - if err != nil { - log.Errorf("failed to delete record %s: %v", recordID, err) - } - } else { - deleteDNSRecordOptions := &dnsrecordsv1.DeleteDnsRecordOptions{ - DnsrecordIdentifier: &recordID, - } - _, _, err := p.Client.DeleteDNSRecordWithContext(ctx, deleteDNSRecordOptions) - if err != nil { - log.Errorf("failed to delete record %s: %v", recordID, err) - } - } -} - -func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool { - proxied := proxiedByDefault - - for _, v := range endpoint.ProviderSpecific { - if v.Name == proxyFilter { - b, err := strconv.ParseBool(v.Value) - if err != nil { - log.Errorf("Failed to parse annotation [%s]: %v", proxyFilter, err) - } else { - proxied = b - } - break - } - } - - if proxyTypeNotSupported[endpoint.RecordType] || strings.Contains(endpoint.DNSName, "*") { - proxied = false - } - return proxied -} - -func checkVPCAnnotation(endpoint *endpoint.Endpoint) string { - var vpc string - for _, v := range endpoint.ProviderSpecific { - if v.Name == vpcFilter { - vpcCrn, err := crn.Parse(v.Value) - if err != nil || vpcCrn.ResourceType != "vpc" { - log.Errorf("Failed to parse vpc [%s]: %v", v.Value, err) - } else { - vpc = v.Value - } - break - } - } - return vpc -} - -// TODO: could be shared function -func isNil(i interface{}) bool { - if i == nil { - return true - } - switch reflect.TypeOf(i).Kind() { - case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: - return reflect.ValueOf(i).IsNil() - default: - return false - } -} diff --git a/provider/ibmcloud/ibmcloud_test.go b/provider/ibmcloud/ibmcloud_test.go deleted file mode 100644 index 18440e2f6..000000000 --- a/provider/ibmcloud/ibmcloud_test.go +++ /dev/null @@ -1,953 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package ibmcloud - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/IBM/go-sdk-core/v5/core" - "github.com/IBM/networking-go-sdk/dnsrecordsv1" - "github.com/stretchr/testify/require" - - "github.com/IBM/networking-go-sdk/dnssvcsv1" - - . "github.com/onsi/ginkgo" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" -) - -func NewMockIBMCloudDNSAPI() *mockIbmcloudClientInterface { - // Setup public example responses - firstPublicRecord := dnsrecordsv1.DnsrecordDetails{ - ID: core.StringPtr("123"), - Name: core.StringPtr("test.example.com"), - Type: core.StringPtr("A"), - Content: core.StringPtr("1.2.3.4"), - Proxied: core.BoolPtr(true), - TTL: core.Int64Ptr(int64(120)), - } - secondPublicRecord := dnsrecordsv1.DnsrecordDetails{ - ID: core.StringPtr("456"), - Name: core.StringPtr("test.example.com"), - Type: core.StringPtr("TXT"), - Proxied: core.BoolPtr(false), - Content: core.StringPtr("\"heritage=external-dns,external-dns/owner=tower-pdns\""), - TTL: core.Int64Ptr(int64(120)), - } - publicRecordsResult := []dnsrecordsv1.DnsrecordDetails{firstPublicRecord, secondPublicRecord} - publicRecordsResultInfo := &dnsrecordsv1.ResultInfo{ - Page: core.Int64Ptr(int64(1)), - TotalCount: core.Int64Ptr(int64(1)), - } - - publicRecordsResp := &dnsrecordsv1.ListDnsrecordsResp{ - Result: publicRecordsResult, - ResultInfo: publicRecordsResultInfo, - } - // Setup private example responses - firstPrivateZone := dnssvcsv1.Dnszone{ - ID: core.StringPtr("123"), - Name: core.StringPtr("example.com"), - State: core.StringPtr(zoneStatePendingNetwork), - } - - secondPrivateZone := dnssvcsv1.Dnszone{ - ID: core.StringPtr("456"), - Name: core.StringPtr("example1.com"), - State: core.StringPtr(zoneStateActive), - } - privateZones := []dnssvcsv1.Dnszone{firstPrivateZone, secondPrivateZone} - listZonesResp := &dnssvcsv1.ListDnszones{ - Dnszones: privateZones, - } - firstPrivateRecord := dnssvcsv1.ResourceRecord{ - ID: core.StringPtr("123"), - Name: core.StringPtr("test.example.com"), - Type: core.StringPtr("A"), - Rdata: map[string]interface{}{"ip": "1.2.3.4"}, - TTL: core.Int64Ptr(int64(120)), - } - secondPrivateRecord := dnssvcsv1.ResourceRecord{ - ID: core.StringPtr("456"), - Name: core.StringPtr("testCNAME.example.com"), - Type: core.StringPtr("CNAME"), - Rdata: map[string]interface{}{"cname": "test.example.com"}, - TTL: core.Int64Ptr(int64(120)), - } - thirdPrivateRecord := dnssvcsv1.ResourceRecord{ - ID: core.StringPtr("789"), - Name: core.StringPtr("test.example.com"), - Type: core.StringPtr("TXT"), - Rdata: map[string]interface{}{"text": "\"heritage=external-dns,external-dns/owner=tower-pdns\""}, - TTL: core.Int64Ptr(int64(120)), - } - privateRecords := []dnssvcsv1.ResourceRecord{firstPrivateRecord, secondPrivateRecord, thirdPrivateRecord} - privateRecordsResop := &dnssvcsv1.ListResourceRecords{ - ResourceRecords: privateRecords, - Offset: core.Int64Ptr(int64(0)), - TotalCount: core.Int64Ptr(int64(1)), - } - - // Setup record rData - inputARecord := &dnssvcsv1.ResourceRecordInputRdataRdataARecord{ - Ip: core.StringPtr("1.2.3.4"), - } - inputCnameRecord := &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{ - Cname: core.StringPtr("test.example.com"), - } - inputTxtRecord := &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{ - Text: core.StringPtr("test"), - } - - updateARecord := &dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord{ - Ip: core.StringPtr("1.2.3.4"), - } - updateCnameRecord := &dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord{ - Cname: core.StringPtr("test.example.com"), - } - updateTxtRecord := &dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord{ - Text: core.StringPtr("test"), - } - - // Setup mock services - mockDNSClient := &mockIbmcloudClientInterface{} - mockDNSClient.On("CreateDNSRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) - mockDNSClient.On("UpdateDNSRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) - mockDNSClient.On("DeleteDNSRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) - mockDNSClient.On("ListAllDDNSRecordsWithContext", mock.Anything, mock.Anything).Return(publicRecordsResp, nil, nil) - mockDNSClient.On("ListDnszonesWithContext", mock.Anything, mock.Anything).Return(listZonesResp, nil, nil) - mockDNSClient.On("GetDnszoneWithContext", mock.Anything, mock.Anything).Return(&firstPrivateZone, nil, nil) - mockDNSClient.On("ListResourceRecordsWithContext", mock.Anything, mock.Anything).Return(privateRecordsResop, nil, nil) - mockDNSClient.On("CreatePermittedNetworkWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) - mockDNSClient.On("CreateResourceRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) - mockDNSClient.On("DeleteResourceRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) - mockDNSClient.On("UpdateResourceRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) - mockDNSClient.On("NewResourceRecordInputRdataRdataARecord", mock.Anything).Return(inputARecord, nil) - mockDNSClient.On("NewResourceRecordInputRdataRdataCnameRecord", mock.Anything).Return(inputCnameRecord, nil) - mockDNSClient.On("NewResourceRecordInputRdataRdataTxtRecord", mock.Anything).Return(inputTxtRecord, nil) - mockDNSClient.On("NewResourceRecordUpdateInputRdataRdataARecord", mock.Anything).Return(updateARecord, nil) - mockDNSClient.On("NewResourceRecordUpdateInputRdataRdataCnameRecord", mock.Anything).Return(updateCnameRecord, nil) - mockDNSClient.On("NewResourceRecordUpdateInputRdataRdataTxtRecord", mock.Anything).Return(updateTxtRecord, nil) - - return mockDNSClient -} - -func newTestIBMCloudProvider(private bool) *IBMCloudProvider { - mockSource := &mockSource{} - endpoints := []*endpoint.Endpoint{ - { - DNSName: "new.example.com", - Targets: endpoint.Targets{"4.3.2.1"}, - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "ibmcloud-vpc", - Value: "crn:v1:staging:public:is:us-south:a/0821fa9f9ebcc7b7c9a0d6e9bf9442a4::vpc:be33cdad-9a03-4bfa-82ca-eadb9f1de688", - }, - }, - }, - } - mockSource.On("Endpoints", mock.Anything).Return(endpoints, nil, nil) - - domainFilterTest := endpoint.NewDomainFilter([]string{"example.com"}) - - return &IBMCloudProvider{ - Client: NewMockIBMCloudDNSAPI(), - source: mockSource, - domainFilter: domainFilterTest, - DryRun: false, - instanceID: "test123", - privateZone: private, - } -} - -func TestPublic_Records(t *testing.T) { - p := newTestIBMCloudProvider(false) - endpoints, err := p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 2 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %++v", *endpoint) - } - } -} - -func TestPrivate_Records(t *testing.T) { - p := newTestIBMCloudProvider(true) - endpoints, err := p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 3 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %++v", *endpoint) - } - } -} - -func TestPublic_ApplyChanges(t *testing.T) { - p := newTestIBMCloudProvider(false) - - changes := plan.Changes{ - Create: []*endpoint.Endpoint{ - { - DNSName: "newA.example.com", - RecordType: "A", - RecordTTL: 300, - Targets: endpoint.NewTargets("4.3.2.1"), - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "ibmcloud-proxied", - Value: "false", - }, - }, - }, - }, - UpdateOld: []*endpoint.Endpoint{ - { - DNSName: "test.example.com", - RecordType: "A", - RecordTTL: 180, - Targets: endpoint.NewTargets("1.2.3.4"), - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "ibmcloud-proxied", - Value: "false", - }, - }, - }, - }, - UpdateNew: []*endpoint.Endpoint{ - { - DNSName: "test.example.com", - RecordType: "A", - RecordTTL: 180, - Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "ibmcloud-proxied", - Value: "true", - }, - }, - }, - }, - Delete: []*endpoint.Endpoint{ - { - DNSName: "test.example.com", - RecordType: "TXT", - RecordTTL: 300, - Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=tower-pdns\""), - }, - }, - } - ctx := context.Background() - err := p.ApplyChanges(ctx, &changes) - if err != nil { - t.Errorf("should not fail, %s", err) - } -} - -func TestPrivate_ApplyChanges(t *testing.T) { - p := newTestIBMCloudProvider(true) - - endpointsCreate, err := p.AdjustEndpoints([]*endpoint.Endpoint{ - { - DNSName: "newA.example.com", - RecordType: "A", - RecordTTL: 120, - Targets: endpoint.NewTargets("4.3.2.1"), - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "ibmcloud-vpc", - Value: "crn:v1:staging:public:is:us-south:a/0821fa9f9ebcc7b7c9a0d6e9bf9442a4::vpc:be33cdad-9a03-4bfa-82ca-eadb9f1de688", - }, - }, - }, - { - DNSName: "newCNAME.example.com", - RecordType: "CNAME", - RecordTTL: 180, - Targets: endpoint.NewTargets("newA.example.com"), - }, - { - DNSName: "newTXT.example.com", - RecordType: "TXT", - RecordTTL: 240, - Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=tower-pdns\""), - }, - }) - require.NoError(t, err) - - endpointsUpdate, err := p.AdjustEndpoints([]*endpoint.Endpoint{ - { - DNSName: "test.example.com", - RecordType: "A", - RecordTTL: 180, - Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), - }, - }) - require.NoError(t, err) - - changes := plan.Changes{ - Create: endpointsCreate, - UpdateOld: []*endpoint.Endpoint{ - { - DNSName: "test.example.com", - RecordType: "A", - RecordTTL: 180, - Targets: endpoint.NewTargets("1.2.3.4"), - }, - }, - UpdateNew: endpointsUpdate, - Delete: []*endpoint.Endpoint{ - { - DNSName: "test.example.com", - RecordType: "TXT", - RecordTTL: 300, - Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=tower-pdns\""), - }, - }, - } - ctx := context.Background() - err = p.ApplyChanges(ctx, &changes) - if err != nil { - t.Errorf("should not fail, %s", err) - } -} - -func TestAdjustEndpoints(t *testing.T) { - p := newTestIBMCloudProvider(false) - endpoints := []*endpoint.Endpoint{ - { - DNSName: "test.example.com", - Targets: endpoint.Targets{"1.2.3.4"}, - RecordType: endpoint.RecordTypeA, - RecordTTL: 300, - Labels: endpoint.Labels{}, - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "ibmcloud-proxied", - Value: "1", - }, - }, - }, - } - - ep, err := p.AdjustEndpoints(endpoints) - assert.NoError(t, err) - - assert.Equal(t, endpoint.TTL(0), ep[0].RecordTTL) - assert.Equal(t, "test.example.com", ep[0].DNSName) - proxied, _ := ep[0].GetProviderSpecificProperty("ibmcloud-proxied") - assert.Equal(t, "true", proxied) -} - -func TestPrivateZone_withFilterID(t *testing.T) { - p := newTestIBMCloudProvider(true) - p.zoneIDFilter = provider.NewZoneIDFilter([]string{"123", "456"}) - - zones, err := p.privateZones(context.Background()) - if err != nil { - t.Errorf("should not fail, %s", err) - } else { - if len(zones) != 2 { - t.Errorf("Incorrect number of zones: %d", len(zones)) - } - for _, zone := range zones { - t.Logf("zone %s", *zone.ID) - } - } -} - -func TestPublicConfig_Validate(t *testing.T) { - // mock http server - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - defer GinkgoRecover() - time.Sleep(0) - - // Set mock response - res.Header().Set("Content-type", "application/json") - res.WriteHeader(http.StatusOK) - fmt.Fprintf(res, "%s", `{"success": true, "errors": [["Errors"]], "messages": [["Messages"]], "result": [{"id": "123", "created_on": "2014-01-01T05:20:00.12345Z", "modified_on": "2014-01-01T05:20:00.12345Z", "name": "example.com", "original_registrar": "GoDaddy", "original_dnshost": "NameCheap", "status": "active", "paused": false, "original_name_servers": ["ns1.originaldnshost.com"], "name_servers": ["ns001.name.cloud.ibm.com"]}], "result_info": {"page": 1, "per_page": 20, "count": 1, "total_count": 2000}}`) - })) - zoneIDFilterTest := provider.NewZoneIDFilter([]string{"123"}) - domainFilterTest := endpoint.NewDomainFilter([]string{"example.com"}) - cfg := &ibmcloudConfig{ - Endpoint: testServer.URL, - CRN: "crn:v1:bluemix:public:internet-svcs:global:a/bcf1865e99742d38d2d5fc3fb80a5496:a6338168-9510-4951-9d67-425612de96f0::", - } - crn := cfg.CRN - authenticator := &core.NoAuthAuthenticator{} - service, isPrivate, err := cfg.Validate(authenticator, domainFilterTest, provider.NewZoneIDFilter([]string{""})) - assert.NoError(t, err) - assert.False(t, isPrivate) - assert.Equal(t, crn, *service.publicRecordsService.Crn) - assert.Equal(t, "123", *service.publicRecordsService.ZoneIdentifier) - - service, isPrivate, err = cfg.Validate(authenticator, endpoint.NewDomainFilter([]string{""}), zoneIDFilterTest) - assert.NoError(t, err) - assert.False(t, isPrivate) - assert.Equal(t, crn, *service.publicRecordsService.Crn) - assert.Equal(t, "123", *service.publicRecordsService.ZoneIdentifier) - - testServer.Close() -} - -func TestPrivateConfig_Validate(t *testing.T) { - zoneIDFilterTest := provider.NewZoneIDFilter([]string{"123"}) - domainFilterTest := endpoint.NewDomainFilter([]string{"example.com"}) - authenticator := &core.NoAuthAuthenticator{} - cfg := &ibmcloudConfig{ - Endpoint: "XXX", - CRN: "crn:v1:bluemix:public:dns-svcs:global:a/bcf1865e99742d38d2d5fc3fb80a5496:a6338168-9510-4951-9d67-425612de96f0::", - } - _, isPrivate, err := cfg.Validate(authenticator, domainFilterTest, zoneIDFilterTest) - assert.NoError(t, err) - assert.True(t, isPrivate) -} - -// mockIbmcloudClientInterface is an autogenerated mock type for the ibmcloudClient type -type mockIbmcloudClientInterface struct { - mock.Mock -} - -// CreateDNSRecordWithContext provides a mock function with given fields: ctx, createDnsRecordOptions -func (_m *mockIbmcloudClientInterface) CreateDNSRecordWithContext(ctx context.Context, createDnsRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (*dnsrecordsv1.DnsrecordResp, *core.DetailedResponse, error) { - ret := _m.Called(ctx, createDnsRecordOptions) - - var r0 *dnsrecordsv1.DnsrecordResp - if rf, ok := ret.Get(0).(func(context.Context, *dnsrecordsv1.CreateDnsRecordOptions) *dnsrecordsv1.DnsrecordResp); ok { - r0 = rf(ctx, createDnsRecordOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnsrecordsv1.DnsrecordResp) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnsrecordsv1.CreateDnsRecordOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, createDnsRecordOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnsrecordsv1.CreateDnsRecordOptions) error); ok { - r2 = rf(ctx, createDnsRecordOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// CreatePermittedNetworkWithContext provides a mock function with given fields: ctx, createPermittedNetworkOptions -func (_m *mockIbmcloudClientInterface) CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (*dnssvcsv1.PermittedNetwork, *core.DetailedResponse, error) { - ret := _m.Called(ctx, createPermittedNetworkOptions) - - var r0 *dnssvcsv1.PermittedNetwork - if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.CreatePermittedNetworkOptions) *dnssvcsv1.PermittedNetwork); ok { - r0 = rf(ctx, createPermittedNetworkOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.PermittedNetwork) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.CreatePermittedNetworkOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, createPermittedNetworkOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.CreatePermittedNetworkOptions) error); ok { - r2 = rf(ctx, createPermittedNetworkOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// CreateResourceRecordWithContext provides a mock function with given fields: ctx, createResourceRecordOptions -func (_m *mockIbmcloudClientInterface) CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (*dnssvcsv1.ResourceRecord, *core.DetailedResponse, error) { - ret := _m.Called(ctx, createResourceRecordOptions) - - var r0 *dnssvcsv1.ResourceRecord - if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.CreateResourceRecordOptions) *dnssvcsv1.ResourceRecord); ok { - r0 = rf(ctx, createResourceRecordOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ResourceRecord) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.CreateResourceRecordOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, createResourceRecordOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.CreateResourceRecordOptions) error); ok { - r2 = rf(ctx, createResourceRecordOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// DeleteDNSRecordWithContext provides a mock function with given fields: ctx, deleteDnsRecordOptions -func (_m *mockIbmcloudClientInterface) DeleteDNSRecordWithContext(ctx context.Context, deleteDnsRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (*dnsrecordsv1.DeleteDnsrecordResp, *core.DetailedResponse, error) { - ret := _m.Called(ctx, deleteDnsRecordOptions) - - var r0 *dnsrecordsv1.DeleteDnsrecordResp - if rf, ok := ret.Get(0).(func(context.Context, *dnsrecordsv1.DeleteDnsRecordOptions) *dnsrecordsv1.DeleteDnsrecordResp); ok { - r0 = rf(ctx, deleteDnsRecordOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnsrecordsv1.DeleteDnsrecordResp) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnsrecordsv1.DeleteDnsRecordOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, deleteDnsRecordOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnsrecordsv1.DeleteDnsRecordOptions) error); ok { - r2 = rf(ctx, deleteDnsRecordOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// DeleteResourceRecordWithContext provides a mock function with given fields: ctx, deleteResourceRecordOptions -func (_m *mockIbmcloudClientInterface) DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (*core.DetailedResponse, error) { - ret := _m.Called(ctx, deleteResourceRecordOptions) - - var r0 *core.DetailedResponse - if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.DeleteResourceRecordOptions) *core.DetailedResponse); ok { - r0 = rf(ctx, deleteResourceRecordOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*core.DetailedResponse) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.DeleteResourceRecordOptions) error); ok { - r1 = rf(ctx, deleteResourceRecordOptions) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetDnszoneWithContext provides a mock function with given fields: ctx, getDnszoneOptions -func (_m *mockIbmcloudClientInterface) GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (*dnssvcsv1.Dnszone, *core.DetailedResponse, error) { - ret := _m.Called(ctx, getDnszoneOptions) - - var r0 *dnssvcsv1.Dnszone - if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.GetDnszoneOptions) *dnssvcsv1.Dnszone); ok { - r0 = rf(ctx, getDnszoneOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.Dnszone) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.GetDnszoneOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, getDnszoneOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.GetDnszoneOptions) error); ok { - r2 = rf(ctx, getDnszoneOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// ListAllDDNSRecordsWithContext provides a mock function with given fields: ctx, listAllDnsRecordsOptions -func (_m *mockIbmcloudClientInterface) ListAllDDNSRecordsWithContext(ctx context.Context, listAllDnsRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (*dnsrecordsv1.ListDnsrecordsResp, *core.DetailedResponse, error) { - ret := _m.Called(ctx, listAllDnsRecordsOptions) - - var r0 *dnsrecordsv1.ListDnsrecordsResp - if rf, ok := ret.Get(0).(func(context.Context, *dnsrecordsv1.ListAllDnsRecordsOptions) *dnsrecordsv1.ListDnsrecordsResp); ok { - r0 = rf(ctx, listAllDnsRecordsOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnsrecordsv1.ListDnsrecordsResp) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnsrecordsv1.ListAllDnsRecordsOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, listAllDnsRecordsOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnsrecordsv1.ListAllDnsRecordsOptions) error); ok { - r2 = rf(ctx, listAllDnsRecordsOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// ListDnszonesWithContext provides a mock function with given fields: ctx, listDnszonesOptions -func (_m *mockIbmcloudClientInterface) ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (*dnssvcsv1.ListDnszones, *core.DetailedResponse, error) { - ret := _m.Called(ctx, listDnszonesOptions) - - var r0 *dnssvcsv1.ListDnszones - if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.ListDnszonesOptions) *dnssvcsv1.ListDnszones); ok { - r0 = rf(ctx, listDnszonesOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ListDnszones) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.ListDnszonesOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, listDnszonesOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.ListDnszonesOptions) error); ok { - r2 = rf(ctx, listDnszonesOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// ListResourceRecordsWithContext provides a mock function with given fields: ctx, listResourceRecordsOptions -func (_m *mockIbmcloudClientInterface) ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (*dnssvcsv1.ListResourceRecords, *core.DetailedResponse, error) { - ret := _m.Called(ctx, listResourceRecordsOptions) - - var r0 *dnssvcsv1.ListResourceRecords - if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.ListResourceRecordsOptions) *dnssvcsv1.ListResourceRecords); ok { - r0 = rf(ctx, listResourceRecordsOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ListResourceRecords) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.ListResourceRecordsOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, listResourceRecordsOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.ListResourceRecordsOptions) error); ok { - r2 = rf(ctx, listResourceRecordsOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// NewResourceRecordInputRdataRdataARecord provides a mock function with given fields: ip -func (_m *mockIbmcloudClientInterface) NewResourceRecordInputRdataRdataARecord(ip string) (*dnssvcsv1.ResourceRecordInputRdataRdataARecord, error) { - ret := _m.Called(ip) - - var r0 *dnssvcsv1.ResourceRecordInputRdataRdataARecord - if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordInputRdataRdataARecord); ok { - r0 = rf(ip) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(ip) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewResourceRecordInputRdataRdataCnameRecord provides a mock function with given fields: cname -func (_m *mockIbmcloudClientInterface) NewResourceRecordInputRdataRdataCnameRecord(cname string) (*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, error) { - ret := _m.Called(cname) - - var r0 *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord - if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord); ok { - r0 = rf(cname) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(cname) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewResourceRecordInputRdataRdataTxtRecord provides a mock function with given fields: text -func (_m *mockIbmcloudClientInterface) NewResourceRecordInputRdataRdataTxtRecord(text string) (*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, error) { - ret := _m.Called(text) - - var r0 *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord - if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord); ok { - r0 = rf(text) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(text) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewResourceRecordUpdateInputRdataRdataARecord provides a mock function with given fields: ip -func (_m *mockIbmcloudClientInterface) NewResourceRecordUpdateInputRdataRdataARecord(ip string) (*dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, error) { - ret := _m.Called(ip) - - var r0 *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord - if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord); ok { - r0 = rf(ip) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(ip) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewResourceRecordUpdateInputRdataRdataCnameRecord provides a mock function with given fields: cname -func (_m *mockIbmcloudClientInterface) NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (*dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, error) { - ret := _m.Called(cname) - - var r0 *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord - if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord); ok { - r0 = rf(cname) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(cname) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewResourceRecordUpdateInputRdataRdataTxtRecord provides a mock function with given fields: text -func (_m *mockIbmcloudClientInterface) NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (*dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, error) { - ret := _m.Called(text) - - var r0 *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord - if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord); ok { - r0 = rf(text) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(text) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// UpdateDNSRecordWithContext provides a mock function with given fields: ctx, updateDnsRecordOptions -func (_m *mockIbmcloudClientInterface) UpdateDNSRecordWithContext(ctx context.Context, updateDnsRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (*dnsrecordsv1.DnsrecordResp, *core.DetailedResponse, error) { - ret := _m.Called(ctx, updateDnsRecordOptions) - - var r0 *dnsrecordsv1.DnsrecordResp - if rf, ok := ret.Get(0).(func(context.Context, *dnsrecordsv1.UpdateDnsRecordOptions) *dnsrecordsv1.DnsrecordResp); ok { - r0 = rf(ctx, updateDnsRecordOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnsrecordsv1.DnsrecordResp) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnsrecordsv1.UpdateDnsRecordOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, updateDnsRecordOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnsrecordsv1.UpdateDnsRecordOptions) error); ok { - r2 = rf(ctx, updateDnsRecordOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// UpdateResourceRecordWithContext provides a mock function with given fields: ctx, updateResourceRecordOptions -func (_m *mockIbmcloudClientInterface) UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (*dnssvcsv1.ResourceRecord, *core.DetailedResponse, error) { - ret := _m.Called(ctx, updateResourceRecordOptions) - - var r0 *dnssvcsv1.ResourceRecord - if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.UpdateResourceRecordOptions) *dnssvcsv1.ResourceRecord); ok { - r0 = rf(ctx, updateResourceRecordOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dnssvcsv1.ResourceRecord) - } - } - - var r1 *core.DetailedResponse - if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.UpdateResourceRecordOptions) *core.DetailedResponse); ok { - r1 = rf(ctx, updateResourceRecordOptions) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*core.DetailedResponse) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.UpdateResourceRecordOptions) error); ok { - r2 = rf(ctx, updateResourceRecordOptions) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -type mockSource struct { - mock.Mock -} - -// Endpoints provides a mock function with given fields: ctx -func (_m *mockSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { - ret := _m.Called(ctx) - - var r0 []*endpoint.Endpoint - if rf, ok := ret.Get(0).(func(context.Context) []*endpoint.Endpoint); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*endpoint.Endpoint) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// AddEventHandler provides a mock function with given fields: _a0, _a1 -func (_m *mockSource) AddEventHandler(_a0 context.Context, _a1 func()) { - _m.Called(_a0, _a1) -} diff --git a/source/annotations/annotations.go b/source/annotations/annotations.go index 5c98516e3..bd21ca2bc 100644 --- a/source/annotations/annotations.go +++ b/source/annotations/annotations.go @@ -26,7 +26,6 @@ const ( AWSPrefix = "external-dns.alpha.kubernetes.io/aws-" SCWPrefix = "external-dns.alpha.kubernetes.io/scw-" - IBMCloudPrefix = "external-dns.alpha.kubernetes.io/ibmcloud-" WebhookPrefix = "external-dns.alpha.kubernetes.io/webhook-" CloudflarePrefix = "external-dns.alpha.kubernetes.io/cloudflare-" diff --git a/source/annotations/provider_specific.go b/source/annotations/provider_specific.go index 99d932c31..93612ff09 100644 --- a/source/annotations/provider_specific.go +++ b/source/annotations/provider_specific.go @@ -45,12 +45,6 @@ func ProviderSpecificAnnotations(annotations map[string]string) (endpoint.Provid Name: fmt.Sprintf("scw/%s", attr), Value: v, }) - } else if strings.HasPrefix(k, IBMCloudPrefix) { - attr := strings.TrimPrefix(k, IBMCloudPrefix) - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: fmt.Sprintf("ibmcloud-%s", attr), - Value: v, - }) } else if strings.HasPrefix(k, WebhookPrefix) { // Support for wildcard annotations for webhook providers attr := strings.TrimPrefix(k, WebhookPrefix) diff --git a/source/annotations/provider_specific_test.go b/source/annotations/provider_specific_test.go index e9fd76334..c13d123cc 100644 --- a/source/annotations/provider_specific_test.go +++ b/source/annotations/provider_specific_test.go @@ -300,19 +300,6 @@ func TestGetProviderSpecificIdentifierAnnotations(t *testing.T) { }, expectedIdentifier: "id1", }, - { - title: "ibmcloud- provider specific annotations are set correctly", - annotations: map[string]string{ - "external-dns.alpha.kubernetes.io/ibmcloud-annotation-1": "value 1", - SetIdentifierKey: "id1", - "external-dns.alpha.kubernetes.io/ibmcloud-annotation-2": "value 2", - }, - expectedResult: map[string]string{ - "ibmcloud-annotation-1": "value 1", - "ibmcloud-annotation-2": "value 2", - }, - expectedIdentifier: "id1", - }, { title: "webhook- provider specific annotations are set correctly", annotations: map[string]string{ From 9742224c695674db298d2d7a5e4f865aa4d671dc Mon Sep 17 00:00:00 2001 From: u-kai <76635578+u-kai@users.noreply.github.com> Date: Sun, 25 May 2025 16:50:03 +0900 Subject: [PATCH 25/33] Avoid flaky test by replacing random value with a static value --- internal/gen/docs/metrics/main_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/gen/docs/metrics/main_test.go b/internal/gen/docs/metrics/main_test.go index 19e3b78af..dd9884e95 100644 --- a/internal/gen/docs/metrics/main_test.go +++ b/internal/gen/docs/metrics/main_test.go @@ -19,7 +19,6 @@ package main import ( "fmt" "io/fs" - "math/rand/v2" "os" "testing" @@ -57,7 +56,7 @@ func TestGenerateMarkdownTableWithSingleMetric(t *testing.T) { reg.MustRegister(metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ Namespace: "external_dns", - Subsystem: fmt.Sprintf("controller_%d", rand.IntN(100)), + Subsystem: "controller_0", Name: "verified_aaaa_records", Help: "This is just a test.", }, @@ -95,7 +94,7 @@ func TestMetricsMdExtraMetricAdded(t *testing.T) { reg.MustRegister(metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ Namespace: "external_dns", - Subsystem: fmt.Sprintf("controller_%d", rand.IntN(100)), + Subsystem: "controller_1", Name: "verified_aaaa_records", Help: "This is just a test.", }, From 640e593fcb9ba6bcec9a387a09192699e40d3d99 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur Date: Sun, 25 May 2025 10:04:19 +0200 Subject: [PATCH 26/33] remove tencentcloud provider --- OWNERS | 2 +- README.md | 4 - controller/execute.go | 3 - docs/flags.md | 4 +- docs/providers.md | 1 - docs/tutorials/tencentcloud.md | 216 ---------- go.mod | 23 +- go.sum | 37 -- pkg/apis/externaldns/types.go | 8 +- pkg/apis/externaldns/types_test.go | 8 - provider/tencentcloud/cloudapi/api.go | 60 --- provider/tencentcloud/cloudapi/clientset.go | 81 ---- provider/tencentcloud/cloudapi/mockapi.go | 234 ---------- provider/tencentcloud/cloudapi/readonlyapi.go | 78 ---- provider/tencentcloud/cloudapi/tencentapi.go | 278 ------------ provider/tencentcloud/dnspod.go | 281 ------------ provider/tencentcloud/privatedns.go | 319 -------------- provider/tencentcloud/tencent_cloud.go | 121 ------ provider/tencentcloud/tencent_cloud_test.go | 403 ------------------ 19 files changed, 5 insertions(+), 2156 deletions(-) delete mode 100644 docs/tutorials/tencentcloud.md delete mode 100644 provider/tencentcloud/cloudapi/api.go delete mode 100644 provider/tencentcloud/cloudapi/clientset.go delete mode 100644 provider/tencentcloud/cloudapi/mockapi.go delete mode 100644 provider/tencentcloud/cloudapi/readonlyapi.go delete mode 100644 provider/tencentcloud/cloudapi/tencentapi.go delete mode 100644 provider/tencentcloud/dnspod.go delete mode 100644 provider/tencentcloud/privatedns.go delete mode 100644 provider/tencentcloud/tencent_cloud.go delete mode 100644 provider/tencentcloud/tencent_cloud_test.go diff --git a/OWNERS b/OWNERS index dbc97b90d..8ee1d2f0c 100644 --- a/OWNERS +++ b/OWNERS @@ -51,6 +51,6 @@ filters: "provider/cloudflare": labels: - provider/cloudflare - "provider/(akamai|alibabacloud|civo|designate|digitalocean|dnsimple|exoscale|gandi|godaddy|linode|ns1|oci|ovh|pihole|plural|scaleway|tencentcloud|transip|ultradns)": + "provider/(akamai|alibabacloud|civo|designate|digitalocean|dnsimple|exoscale|gandi|godaddy|linode|ns1|oci|ovh|pihole|plural|scaleway|transip|ultradns)": labels: - provider diff --git a/README.md b/README.md index aacb436ee..9d69b0720 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,6 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz - [GoDaddy](https://www.godaddy.com) - [Gandi](https://www.gandi.net) - [IBM Cloud DNS](https://www.ibm.com/cloud/dns) -- [TencentCloud PrivateDNS](https://cloud.tencent.com/product/privatedns) -- [TencentCloud DNSPod](https://cloud.tencent.com/product/cns) - [Plural](https://www.plural.sh/) - [Pi-hole](https://pi-hole.net/) @@ -151,7 +149,6 @@ The following table clarifies the current status of the providers according to t | UltraDNS | Alpha | | | GoDaddy | Alpha | | | Gandi | Alpha | @packi | -| TencentCloud | Alpha | @Hyzhou | | Plural | Alpha | @michaeljguarino | | Pi-hole | Alpha | @tinyzimmer | @@ -213,7 +210,6 @@ The following tutorials are provided: - [GoDaddy](docs/tutorials/godaddy.md) - [Gandi](docs/tutorials/gandi.md) - [Nodes as source](docs/sources/nodes.md) -- [TencentCloud](docs/tutorials/tencentcloud.md) - [Plural](docs/tutorials/plural.md) - [Pi-hole](docs/tutorials/pihole.md) diff --git a/controller/execute.go b/controller/execute.go index 49c83b78b..7d76aab25 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -63,7 +63,6 @@ import ( "sigs.k8s.io/external-dns/provider/plural" "sigs.k8s.io/external-dns/provider/rfc2136" "sigs.k8s.io/external-dns/provider/scaleway" - "sigs.k8s.io/external-dns/provider/tencentcloud" "sigs.k8s.io/external-dns/provider/transip" "sigs.k8s.io/external-dns/provider/ultradns" "sigs.k8s.io/external-dns/provider/webhook" @@ -308,8 +307,6 @@ func Execute() { ) case "plural": p, err = plural.NewPluralProvider(cfg.PluralCluster, cfg.PluralProvider) - case "tencentcloud": - p, err = tencentcloud.NewTencentCloudProvider(domainFilter, zoneIDFilter, cfg.TencentCloudConfigFile, cfg.TencentCloudZoneType, cfg.DryRun) case "webhook": p, err = webhook.NewWebhookProvider(cfg.WebhookProviderURL) default: diff --git a/docs/flags.md b/docs/flags.md index 8276d2cec..a8284b7f1 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -51,7 +51,7 @@ | `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) | | `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group | | `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group | -| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, tencentcloud, transip, ultradns, webhook) | +| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, ultradns, webhook) | | `--provider-cache-time=0s` | The time to cache the DNS provider record list requests. | | `--domain-filter=` | Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional) | | `--exclude-domains=` | Exclude subdomains (optional) | @@ -87,8 +87,6 @@ | `--azure-user-assigned-identity-client-id=""` | When using the Azure provider, override the client id of user assigned identity in config file (optional) | | `--azure-zones-cache-duration=0s` | When using the Azure provider, set the zones list cache TTL (0s to disable). | | `--azure-maxretries-count=3` | When using the Azure provider, set the number of retries for API calls (When less than 0, it disables retries). (optional) | -| `--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) | diff --git a/docs/providers.md b/docs/providers.md index a50344798..f601ac972 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -28,7 +28,6 @@ Provider supported configurations | Plural | n/a | n/a | n/a | | RFC2136 | n/a | yes | n/a | | Scaleway | n/a | n/a | 300 | -| TencentCloud | n/a | n/a | n/a | | Transip | n/a | yes | 60 | | Ultradns | n/a | yes | n/a | | Webhook | n/a | n/a | n/a | diff --git a/docs/tutorials/tencentcloud.md b/docs/tutorials/tencentcloud.md deleted file mode 100644 index 60726c02e..000000000 --- a/docs/tutorials/tencentcloud.md +++ /dev/null @@ -1,216 +0,0 @@ -# Tencent Cloud - -## External Dns Version - -* Make sure to use **>=0.13.1** version of ExternalDNS for this tutorial - -## Set up PrivateDns or DNSPod - -Tencent Cloud DNSPod Service is the domain name resolution and management service for public access. -Tencent Cloud PrivateDNS Service is the domain name resolution and management service for VPC internal access. - -* If you want to use internal dns service in Tencent Cloud. - -1. Set up the args `--tencent-cloud-zone-type=private` -2. Create a DNS domain in PrivateDNS console. DNS domain which will contain the managed DNS records. - -* If you want to use public dns service in Tencent Cloud. - -1. Set up the args `--tencent-cloud-zone-type=public` -2. Create a Domain in DnsPod console. DNS domain which will contain the managed DNS records. - -## Set up CAM for API Key - -In Tencent CAM Console. you may get the secretId and secretKey pair. make sure the key pair has those Policy. - -```json -{ - "version": "2.0", - "statement": [ - { - "effect": "allow", - "action": [ - "dnspod:ModifyRecord", - "dnspod:DeleteRecord", - "dnspod:CreateRecord", - "dnspod:DescribeRecordList", - "dnspod:DescribeDomainList" - ], - "resource": [ - "*" - ] - }, - { - "effect": "allow", - "action": [ - "privatedns:DescribePrivateZoneList", - "privatedns:DescribePrivateZoneRecordList", - "privatedns:CreatePrivateZoneRecord", - "privatedns:DeletePrivateZoneRecord", - "privatedns:ModifyPrivateZoneRecord" - ], - "resource": [ - "*" - ] - } - ] -} -``` - -## Deploy ExternalDNS - -### Manifest (for clusters with RBAC enabled) - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: external-dns ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: external-dns -rules: -- apiGroups: [""] - resources: ["services","endpoints","pods"] - verbs: ["get","watch","list"] -- apiGroups: ["extensions","networking.k8s.io"] - resources: ["ingresses"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["nodes"] - verbs: ["list"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: external-dns-viewer -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: external-dns -subjects: -- kind: ServiceAccount - name: external-dns - namespace: default ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: external-dns -data: - tencent-cloud.json: | - { - "regionId": "ap-shanghai", - "secretId": "******", - "secretKey": "******", - "vpcId": "vpc-******", - "internetEndpoint": false # Default: false. Access the Tencent API through the intranet. If you need to deploy on the public network, you need to change to true - } ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: external-dns -spec: - strategy: - type: Recreate - selector: - matchLabels: - app: external-dns - template: - metadata: - labels: - app: external-dns - spec: - containers: - - args: - - --source=service - - --source=ingress - - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - - --provider=tencentcloud - - --policy=sync # set `upsert-only` would prevent ExternalDNS from deleting any records - - --tencent-cloud-zone-type=private # only look at private hosted zones. set `public` to use the public dns service. - - --tencent-cloud-config-file=/etc/kubernetes/tencent-cloud.json - image: registry.k8s.io/external-dns/external-dns:v0.17.0 - imagePullPolicy: Always - name: external-dns - resources: {} - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /etc/kubernetes - name: config-volume - readOnly: true - dnsPolicy: ClusterFirst - hostAliases: - - hostnames: - - privatedns.internal.tencentcloudapi.com - - dnspod.internal.tencentcloudapi.com - ip: 169.254.0.95 - restartPolicy: Always - schedulerName: default-scheduler - securityContext: {} - serviceAccount: external-dns - serviceAccountName: external-dns - terminationGracePeriodSeconds: 30 - volumes: - - configMap: - defaultMode: 420 - items: - - key: tencent-cloud.json - path: tencent-cloud.json - name: external-dns - name: config-volume -``` - -## Example - -### Service - -```yaml -apiVersion: v1 -kind: Service -metadata: - name: nginx - annotations: - external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.com - external-dns.alpha.kubernetes.io/internal-hostname: nginx-internal.external-dns-test.com - external-dns.alpha.kubernetes.io/ttl: "600" -spec: - type: LoadBalancer - ports: - - port: 80 - name: http - targetPort: 80 - selector: - app: nginx ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx -spec: - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - image: nginx - name: nginx - ports: - - containerPort: 80 - name: http -``` - -`nginx.external-dns-test.com` will record to the Loadbalancer VIP. -`nginx-internal.external-dns-test.com` will record to the ClusterIP. -all of the DNS Record ttl will be 600. - -> [!WARNING] -> This makes ExternalDNS safe for running in environments where there are other records managed via other means. diff --git a/go.mod b/go.mod index cb505369d..e2d2a35fa 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,6 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 github.com/F5Networks/k8s-bigip-ctlr/v2 v2.19.1 - github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.7.2 - github.com/IBM/go-sdk-core/v5 v5.19.1 - github.com/IBM/networking-go-sdk v0.51.5 github.com/Yamashou/gqlgenc v0.32.1 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 github.com/alecthomas/kingpin/v2 v2.4.0 @@ -44,7 +41,6 @@ require ( github.com/linode/linodego v1.50.0 github.com/maxatome/go-testdeep v1.14.0 github.com/miekg/dns v1.1.66 - github.com/onsi/ginkgo v1.16.5 github.com/openshift/api v0.0.0-20230607130528-611114dca681 github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3 github.com/oracle/oci-go-sdk/v65 v65.91.0 @@ -56,9 +52,6 @@ require ( github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1165 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1145 github.com/transip/gotransip/v6 v6.26.0 github.com/ultradns/ultradns-sdk-go v1.3.7 go.etcd.io/etcd/client/v3 v3.6.0 @@ -76,6 +69,7 @@ require ( k8s.io/apimachinery v0.33.1 k8s.io/client-go v0.33.1 k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/controller-runtime v0.20.4 sigs.k8s.io/gateway-api v1.3.0 ) @@ -89,7 +83,6 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect @@ -111,18 +104,11 @@ require ( github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/errors v0.22.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gofrs/flock v0.8.1 // indirect @@ -153,7 +139,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect @@ -162,8 +147,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nxadm/tail v1.4.8 // indirect - github.com/oklog/ulid v1.3.1 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/peterhellberg/link v1.1.0 // indirect @@ -187,7 +171,6 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.etcd.io/etcd/api/v3 v3.6.0 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.0 // indirect - go.mongodb.org/mongo-driver v1.17.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect @@ -208,13 +191,11 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect moul.io/http2curl v1.0.0 // indirect - sigs.k8s.io/controller-runtime v0.20.4 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect diff --git a/go.sum b/go.sum index a31ea75b6..abeae1fc8 100644 --- a/go.sum +++ b/go.sum @@ -51,12 +51,6 @@ github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q github.com/F5Networks/k8s-bigip-ctlr/v2 v2.19.1 h1:NHWjSBeXbL8mlx+0QyCl4OrUvytCZ3nkEIRqX7t97wQ= github.com/F5Networks/k8s-bigip-ctlr/v2 v2.19.1/go.mod h1:JwdtGjHFTmUM1zjzvvCotCCyP55S146IuVPOJZ7D/Jw= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.7.2 h1:eW5o8NpblAyqPjwOlZ+XISdhlYynjf7B7dsCmsvfC/s= -github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.7.2/go.mod h1:HulyrJLLc9FSZlwKQ9vu5Jq83thNlUfg1afonOdhrRA= -github.com/IBM/go-sdk-core/v5 v5.19.1 h1:sleVks1O4XjgF4YEGvyDh6PZbP6iZhlTPeDkQc8nWDs= -github.com/IBM/go-sdk-core/v5 v5.19.1/go.mod h1:Q3BYO6iDA2zweQPDGbNTtqft5tDcEpm6RTuqMlPcvbw= -github.com/IBM/networking-go-sdk v0.51.5 h1:75lKAx17y++hirXK5GcEM23mTRhHnhsv6gmhz70ex1Q= -github.com/IBM/networking-go-sdk v0.51.5/go.mod h1:wyEnRnBnROgGmSn5UrryycIrbBujHKXf0PmI1NSwcjY= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -116,8 +110,6 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -321,8 +313,6 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -360,8 +350,6 @@ github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2 github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= @@ -394,8 +382,6 @@ github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pL github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= @@ -407,20 +393,12 @@ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+ github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -632,8 +610,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -686,8 +662,6 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtB github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM= @@ -799,7 +773,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -1021,14 +994,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1145/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1165/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166 h1:WgdCsNxde/flDGxOy+dXZqm2wrUQA/4kzaaY69XC/2A= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1165 h1:bHT5sLou90UwrVm7Km2nQ7hyBk0+LK4xi7JOZXoDQ2c= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1165/go.mod h1:6pN5Nh0PuYJ+XHkjiV7SQqXgc1gecB9FHd456W9ZNdE= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1145 h1:K5N0Uxqm9kM7KU6DFBekCTKbldlXq6UD1ekOyXn4zEc= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1145/go.mod h1:mgwxarWzLxIylWtHmkoMg0dX8aqhO5lwfrHkK4IGLKE= github.com/terra-farm/udnssdk v1.3.5 h1:MNR3adfuuEK/l04+jzo8WW/0fnorY+nW515qb3vEr6I= github.com/terra-farm/udnssdk v1.3.5/go.mod h1:8RnM56yZTR7mYyUIvrDgXzdRaEyFIzqdEi7+um26Sv8= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -1092,8 +1057,6 @@ go.etcd.io/etcd/client/v3 v3.6.0/go.mod h1:Jzk/Knqe06pkOZPHXsQ0+vNDvMQrgIqJ0W8Dw go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= -go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index d1389c3e4..b04c5aa41 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -199,8 +199,6 @@ type Config struct { GoDaddyTTL int64 GoDaddyOTE bool OCPRouterName string - TencentCloudConfigFile string - TencentCloudZoneType string PiholeServer string PiholePassword string `secure:"yes"` PiholeTLSInsecureSkipVerify bool @@ -356,8 +354,6 @@ var defaultConfig = &Config{ SkipperRouteGroupVersion: "zalando.org/v1", Sources: nil, TargetNetFilter: []string{}, - TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", - TencentCloudZoneType: "", TLSCA: "", TLSClientCert: "", TLSClientCertKey: "", @@ -491,7 +487,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew) // Flags related to providers - providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"} + providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "transip", "ultradns", "webhook"} app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...) app.Flag("provider-cache-time", "The time to cache the DNS provider record list requests.").Default(defaultConfig.ProviderCacheTime.String()).DurationVar(&cfg.ProviderCacheTime) app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) @@ -528,8 +524,6 @@ func App(cfg *Config) *kingpin.Application { app.Flag("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)").Default("").StringVar(&cfg.AzureUserAssignedIdentityClientID) app.Flag("azure-zones-cache-duration", "When using the Azure provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AzureZonesCacheDuration.String()).DurationVar(&cfg.AzureZonesCacheDuration) app.Flag("azure-maxretries-count", "When using the Azure provider, set the number of retries for API calls (When less than 0, it disables retries). (optional)").Default(strconv.Itoa(defaultConfig.AzureMaxRetriesCount)).IntVar(&cfg.AzureMaxRetriesCount) - app.Flag("tencent-cloud-config-file", "When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud)").Default(defaultConfig.TencentCloudConfigFile).StringVar(&cfg.TencentCloudConfigFile) - 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) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index a41124389..e3817981c 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -125,8 +125,6 @@ var ( RFC2136Host: []string{""}, RFC2136LoadBalancingStrategy: "disabled", OCPRouterName: "default", - TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", - TencentCloudZoneType: "", PiholeApiVersion: "5", WebhookProviderURL: "http://localhost:8888", WebhookProviderReadTimeout: 5 * time.Second, @@ -240,8 +238,6 @@ var ( RFC2136BatchChangeSize: 100, RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"}, RFC2136LoadBalancingStrategy: "round-robin", - TencentCloudConfigFile: "tencent-cloud.json", - TencentCloudZoneType: "private", PiholeApiVersion: "6", WebhookProviderURL: "http://localhost:8888", WebhookProviderReadTimeout: 5 * time.Second, @@ -392,8 +388,6 @@ func TestParseFlags(t *testing.T) { "--rfc2136-load-balancing-strategy=round-robin", "--rfc2136-host=rfc2136-host1", "--rfc2136-host=rfc2136-host2", - "--tencent-cloud-config-file=tencent-cloud.json", - "--tencent-cloud-zone-type=private", }, envVars: map[string]string{}, expected: overriddenConfig, @@ -510,8 +504,6 @@ func TestParseFlags(t *testing.T) { "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_TENCENT_CLOUD_CONFIG_FILE": "tencent-cloud.json", - "EXTERNAL_DNS_TENCENT_CLOUD_ZONE_TYPE": "private", }, expected: overriddenConfig, }, diff --git a/provider/tencentcloud/cloudapi/api.go b/provider/tencentcloud/cloudapi/api.go deleted file mode 100644 index 0294f6f15..000000000 --- a/provider/tencentcloud/cloudapi/api.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cloudapi - -import ( - dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" - privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" -) - -type Action struct { - Service string `json:"service"` - Name string `json:"name"` - ReadOnly bool `json:"readOnly"` -} - -var ( - /* PrivateDNS */ - CreatePrivateZoneRecord = Action{Service: "PrivateDns", Name: "CreatePrivateZoneRecord", ReadOnly: false} - DeletePrivateZoneRecord = Action{Service: "PrivateDns", Name: "DeletePrivateZoneRecord", ReadOnly: false} - ModifyPrivateZoneRecord = Action{Service: "PrivateDns", Name: "ModifyPrivateZoneRecord", ReadOnly: false} - DescribePrivateZoneList = Action{Service: "PrivateDns", Name: "DescribePrivateZoneList", ReadOnly: true} - DescribePrivateZoneRecordList = Action{Service: "PrivateDns", Name: "DescribePrivateZoneRecordList", ReadOnly: true} - - /* DNSPod */ - DescribeDomainList = Action{Service: "DnsPod", Name: "DescribeDomainList", ReadOnly: true} - DescribeRecordList = Action{Service: "DnsPod", Name: "DescribeRecordList", ReadOnly: true} - CreateRecord = Action{Service: "DnsPod", Name: "CreateRecord", ReadOnly: false} - DeleteRecord = Action{Service: "DnsPod", Name: "DeleteRecord", ReadOnly: false} - ModifyRecord = Action{Service: "DnsPod", Name: "ModifyRecord", ReadOnly: false} -) - -type TencentAPIService interface { - // PrivateDNS - CreatePrivateZoneRecord(request *privatedns.CreatePrivateZoneRecordRequest) (response *privatedns.CreatePrivateZoneRecordResponse, err error) - DeletePrivateZoneRecord(request *privatedns.DeletePrivateZoneRecordRequest) (response *privatedns.DeletePrivateZoneRecordResponse, err error) - ModifyPrivateZoneRecord(request *privatedns.ModifyPrivateZoneRecordRequest) (response *privatedns.ModifyPrivateZoneRecordResponse, err error) - DescribePrivateZoneList(request *privatedns.DescribePrivateZoneListRequest) (response *privatedns.DescribePrivateZoneListResponse, err error) - DescribePrivateZoneRecordList(request *privatedns.DescribePrivateZoneRecordListRequest) (response *privatedns.DescribePrivateZoneRecordListResponse, err error) - - // DNSPod - DescribeDomainList(request *dnspod.DescribeDomainListRequest) (response *dnspod.DescribeDomainListResponse, err error) - DescribeRecordList(request *dnspod.DescribeRecordListRequest) (response *dnspod.DescribeRecordListResponse, err error) - CreateRecord(request *dnspod.CreateRecordRequest) (response *dnspod.CreateRecordResponse, err error) - DeleteRecord(request *dnspod.DeleteRecordRequest) (response *dnspod.DeleteRecordResponse, err error) - ModifyRecord(request *dnspod.ModifyRecordRequest) (response *dnspod.ModifyRecordResponse, err error) -} diff --git a/provider/tencentcloud/cloudapi/clientset.go b/provider/tencentcloud/cloudapi/clientset.go deleted file mode 100644 index c7f307875..000000000 --- a/provider/tencentcloud/cloudapi/clientset.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cloudapi - -import ( - "fmt" - "sync" - - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" - privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" - "go.uber.org/ratelimit" -) - -type TencentClientSetService interface { - PrivateDnsCli(action string) *privatedns.Client - DnsPodCli(action string) *dnspod.Client -} - -func NewTencentClientSetService(region string, rate int, secretId string, secretKey string, internetEndpoint bool) *defaultTencentClientSetService { - p := &defaultTencentClientSetService{ - Region: region, - RateLimit: rate, - } - cred := common.NewCredential(secretId, secretKey) - - privatednsProf := profile.NewClientProfile() - if !internetEndpoint { - privatednsProf.HttpProfile.Endpoint = "privatedns.internal.tencentcloudapi.com" - } - p.privateDnsClient, _ = privatedns.NewClient(cred, region, privatednsProf) - - dnsPodProf := profile.NewClientProfile() - if !internetEndpoint { - dnsPodProf.HttpProfile.Endpoint = "dnspod.internal.tencentcloudapi.com" - } - p.dnsPodClient, _ = dnspod.NewClient(cred, region, dnsPodProf) - - return p -} - -type defaultTencentClientSetService struct { - Region string - RateLimit int - RateLimitSyncMap sync.Map - - privateDnsClient *privatedns.Client - dnsPodClient *dnspod.Client -} - -func (p *defaultTencentClientSetService) checkRateLimit(request, method string) { - action := fmt.Sprintf("%s_%s", request, method) - if rl, ok := p.RateLimitSyncMap.LoadOrStore(action, ratelimit.New(p.RateLimit, ratelimit.WithoutSlack)); ok { - rl.(ratelimit.Limiter).Take() - } -} - -func (p *defaultTencentClientSetService) PrivateDnsCli(action string) *privatedns.Client { - p.checkRateLimit("privateDns", action) - return p.privateDnsClient -} - -func (p *defaultTencentClientSetService) DnsPodCli(action string) *dnspod.Client { - p.checkRateLimit("dnsPod", action) - return p.dnsPodClient -} diff --git a/provider/tencentcloud/cloudapi/mockapi.go b/provider/tencentcloud/cloudapi/mockapi.go deleted file mode 100644 index df5e64b46..000000000 --- a/provider/tencentcloud/cloudapi/mockapi.go +++ /dev/null @@ -1,234 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cloudapi - -import ( - "math/rand" - - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" - privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" -) - -type mockAPIService struct { - privateZones []*privatedns.PrivateZone - privateZoneRecords map[string][]*privatedns.PrivateZoneRecord - - dnspodDomains []*dnspod.DomainListItem - dnspodRecords map[string][]*dnspod.RecordListItem -} - -func NewMockService(privateZones []*privatedns.PrivateZone, privateZoneRecords map[string][]*privatedns.PrivateZoneRecord, dnspodDomains []*dnspod.DomainListItem, dnspodRecords map[string][]*dnspod.RecordListItem) *mockAPIService { - return &mockAPIService{ - privateZones: privateZones, - privateZoneRecords: privateZoneRecords, - dnspodDomains: dnspodDomains, - dnspodRecords: dnspodRecords, - } -} - -//////////////////////////////////////////////////////////////// -// PrivateDns API -//////////////////////////////////////////////////////////////// - -func (api *mockAPIService) CreatePrivateZoneRecord(request *privatedns.CreatePrivateZoneRecordRequest) (response *privatedns.CreatePrivateZoneRecordResponse, err error) { - randomRecordId := RandStringRunes(8) - if _, exist := api.privateZoneRecords[*request.ZoneId]; !exist { - api.privateZoneRecords[*request.ZoneId] = make([]*privatedns.PrivateZoneRecord, 0) - } - if request.TTL == nil { - request.TTL = common.Int64Ptr(300) - } - api.privateZoneRecords[*request.ZoneId] = append(api.privateZoneRecords[*request.ZoneId], &privatedns.PrivateZoneRecord{ - RecordId: common.StringPtr(randomRecordId), - ZoneId: request.ZoneId, - SubDomain: request.SubDomain, - RecordType: request.RecordType, - RecordValue: request.RecordValue, - TTL: request.TTL, - }) - return response, nil -} - -func (api *mockAPIService) DeletePrivateZoneRecord(request *privatedns.DeletePrivateZoneRecordRequest) (response *privatedns.DeletePrivateZoneRecordResponse, err error) { - result := make([]*privatedns.PrivateZoneRecord, 0) - if _, exist := api.privateZoneRecords[*request.ZoneId]; !exist { - return response, nil - } - for _, privateZoneRecord := range api.privateZoneRecords[*request.ZoneId] { - deleteflag := false - if len(request.RecordIdSet) != 0 { - for _, recordId := range request.RecordIdSet { - if *privateZoneRecord.RecordId == *recordId { - deleteflag = true - break - } - } - } - if request.RecordId != nil && *request.RecordId == *privateZoneRecord.RecordId { - deleteflag = true - } - if !deleteflag { - result = append(result, privateZoneRecord) - } - } - api.privateZoneRecords[*request.ZoneId] = result - return response, nil -} - -func (api *mockAPIService) ModifyPrivateZoneRecord(request *privatedns.ModifyPrivateZoneRecordRequest) (response *privatedns.ModifyPrivateZoneRecordResponse, err error) { - if _, exist := api.privateZoneRecords[*request.ZoneId]; !exist { - return response, nil - } - for _, privateZoneRecord := range api.privateZoneRecords[*request.ZoneId] { - if *privateZoneRecord.RecordId != *request.RecordId { - continue - } - privateZoneRecord.ZoneId = request.ZoneId - privateZoneRecord.SubDomain = request.SubDomain - privateZoneRecord.RecordType = request.RecordType - privateZoneRecord.RecordValue = request.RecordValue - privateZoneRecord.TTL = request.TTL - } - return response, nil -} - -func (api *mockAPIService) DescribePrivateZoneList(request *privatedns.DescribePrivateZoneListRequest) (response *privatedns.DescribePrivateZoneListResponse, err error) { - response = privatedns.NewDescribePrivateZoneListResponse() - response.Response = &privatedns.DescribePrivateZoneListResponseParams{ - TotalCount: common.Int64Ptr(int64(len(api.privateZones))), - PrivateZoneSet: api.privateZones, - } - return response, nil -} - -func (api *mockAPIService) DescribePrivateZoneRecordList(request *privatedns.DescribePrivateZoneRecordListRequest) (response *privatedns.DescribePrivateZoneRecordListResponse, err error) { - response = privatedns.NewDescribePrivateZoneRecordListResponse() - response.Response = &privatedns.DescribePrivateZoneRecordListResponseParams{} - if _, exist := api.privateZoneRecords[*request.ZoneId]; !exist { - response.Response.TotalCount = common.Int64Ptr(0) - response.Response.RecordSet = make([]*privatedns.PrivateZoneRecord, 0) - return response, nil - } - response.Response.TotalCount = common.Int64Ptr(int64(len(api.privateZoneRecords[*request.ZoneId]))) - response.Response.RecordSet = api.privateZoneRecords[*request.ZoneId] - return response, nil -} - -//////////////////////////////////////////////////////////////// -// DnsPod API -//////////////////////////////////////////////////////////////// - -func (api *mockAPIService) DescribeDomainList(request *dnspod.DescribeDomainListRequest) (response *dnspod.DescribeDomainListResponse, err error) { - response = dnspod.NewDescribeDomainListResponse() - response.Response = &dnspod.DescribeDomainListResponseParams{ - DomainCountInfo: &dnspod.DomainCountInfo{ - AllTotal: common.Uint64Ptr(uint64(len(api.dnspodDomains))), - }, - DomainList: api.dnspodDomains, - } - response.Response.DomainList = api.dnspodDomains - response.Response.DomainCountInfo = &dnspod.DomainCountInfo{ - AllTotal: common.Uint64Ptr(uint64(len(api.dnspodDomains))), - } - return response, nil -} - -func (api *mockAPIService) DescribeRecordList(request *dnspod.DescribeRecordListRequest) (response *dnspod.DescribeRecordListResponse, err error) { - response = dnspod.NewDescribeRecordListResponse() - response.Response = &dnspod.DescribeRecordListResponseParams{} - if _, exist := api.dnspodRecords[*request.Domain]; !exist { - response.Response.RecordList = make([]*dnspod.RecordListItem, 0) - response.Response.RecordCountInfo = &dnspod.RecordCountInfo{ - TotalCount: common.Uint64Ptr(uint64(0)), - } - return response, nil - } - response.Response.RecordList = api.dnspodRecords[*request.Domain] - response.Response.RecordCountInfo = &dnspod.RecordCountInfo{ - TotalCount: common.Uint64Ptr(uint64(len(api.dnspodRecords[*request.Domain]))), - } - return response, nil -} - -func (api *mockAPIService) CreateRecord(request *dnspod.CreateRecordRequest) (response *dnspod.CreateRecordResponse, err error) { - randomRecordId := RandUint64() - if _, exist := api.dnspodRecords[*request.Domain]; !exist { - api.dnspodRecords[*request.Domain] = make([]*dnspod.RecordListItem, 0) - } - if request.TTL == nil { - request.TTL = common.Uint64Ptr(300) - } - api.dnspodRecords[*request.Domain] = append(api.dnspodRecords[*request.Domain], &dnspod.RecordListItem{ - RecordId: common.Uint64Ptr(randomRecordId), - Value: request.Value, - TTL: request.TTL, - Name: request.SubDomain, - Line: request.RecordLine, - LineId: request.RecordLineId, - Type: request.RecordType, - }) - return response, nil -} - -func (api *mockAPIService) DeleteRecord(request *dnspod.DeleteRecordRequest) (response *dnspod.DeleteRecordResponse, err error) { - result := make([]*dnspod.RecordListItem, 0) - if _, exist := api.dnspodRecords[*request.Domain]; !exist { - return response, nil - } - for _, zoneRecord := range api.dnspodRecords[*request.Domain] { - deleteflag := false - if request.RecordId != nil && *request.RecordId == *zoneRecord.RecordId { - deleteflag = true - } - if !deleteflag { - result = append(result, zoneRecord) - } - } - api.dnspodRecords[*request.Domain] = result - return response, nil -} - -func (api *mockAPIService) ModifyRecord(request *dnspod.ModifyRecordRequest) (response *dnspod.ModifyRecordResponse, err error) { - if _, exist := api.dnspodRecords[*request.Domain]; !exist { - return response, nil - } - for _, zoneRecord := range api.dnspodRecords[*request.Domain] { - if *zoneRecord.RecordId != *request.RecordId { - continue - } - zoneRecord.Type = request.RecordType - zoneRecord.Name = request.SubDomain - zoneRecord.Value = request.Value - zoneRecord.TTL = request.TTL - } - return response, nil -} - -var letterRunes = []byte("abcdefghijklmnopqrstuvwxyz") - -func RandStringRunes(n int) string { - b := make([]byte, n) - for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] - } - return string(b) -} - -func RandUint64() uint64 { - return rand.Uint64() -} diff --git a/provider/tencentcloud/cloudapi/readonlyapi.go b/provider/tencentcloud/cloudapi/readonlyapi.go deleted file mode 100644 index c86662deb..000000000 --- a/provider/tencentcloud/cloudapi/readonlyapi.go +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cloudapi - -import ( - dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" - privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" -) - -type readonlyAPIService struct { - defaultTencentAPIService -} - -func NewReadOnlyAPIService(region string, rate int, secretId string, secretKey string, internetEndpoint bool) *readonlyAPIService { - apiService := NewTencentAPIService(region, rate, secretId, secretKey, internetEndpoint) - tencentAPIService := &readonlyAPIService{ - *apiService, - } - return tencentAPIService -} - -//////////////////////////////////////////////////////////////// -// PrivateDns API -//////////////////////////////////////////////////////////////// - -func (api *readonlyAPIService) CreatePrivateZoneRecord(request *privatedns.CreatePrivateZoneRecordRequest) (response *privatedns.CreatePrivateZoneRecordResponse, err error) { - apiAction := CreatePrivateZoneRecord - APIRecord(apiAction, JsonWrapper(request), "dryRun") - return response, nil -} - -func (api *readonlyAPIService) DeletePrivateZoneRecord(request *privatedns.DeletePrivateZoneRecordRequest) (response *privatedns.DeletePrivateZoneRecordResponse, err error) { - apiAction := DeletePrivateZoneRecord - APIRecord(apiAction, JsonWrapper(request), "dryRun") - return response, nil -} - -func (api *readonlyAPIService) ModifyPrivateZoneRecord(request *privatedns.ModifyPrivateZoneRecordRequest) (response *privatedns.ModifyPrivateZoneRecordResponse, err error) { - apiAction := ModifyPrivateZoneRecord - APIRecord(apiAction, JsonWrapper(request), "dryRun") - return response, nil -} - -//////////////////////////////////////////////////////////////// -// DnsPod API -//////////////////////////////////////////////////////////////// - -func (api *readonlyAPIService) CreateRecord(request *dnspod.CreateRecordRequest) (response *dnspod.CreateRecordResponse, err error) { - apiAction := CreateRecord - APIRecord(apiAction, JsonWrapper(request), "dryRun") - return response, nil -} - -func (api *readonlyAPIService) DeleteRecord(request *dnspod.DeleteRecordRequest) (response *dnspod.DeleteRecordResponse, err error) { - apiAction := DeleteRecord - APIRecord(apiAction, JsonWrapper(request), "dryRun") - return response, nil -} - -func (api *readonlyAPIService) ModifyRecord(request *dnspod.ModifyRecordRequest) (response *dnspod.ModifyRecordResponse, err error) { - apiAction := ModifyRecord - APIRecord(apiAction, JsonWrapper(request), "dryRun") - return response, nil -} diff --git a/provider/tencentcloud/cloudapi/tencentapi.go b/provider/tencentcloud/cloudapi/tencentapi.go deleted file mode 100644 index 1d3ae95e4..000000000 --- a/provider/tencentcloud/cloudapi/tencentapi.go +++ /dev/null @@ -1,278 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cloudapi - -import ( - "encoding/json" - "errors" - "fmt" - "net" - "time" - - log "github.com/sirupsen/logrus" - tclouderrors "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" - dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" - privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" -) - -type defaultTencentAPIService struct { - RetryDefault int - TaskCheckInterval time.Duration - ClientSetService TencentClientSetService -} - -func NewTencentAPIService(region string, rate int, secretId string, secretKey string, internetEndpoint bool) *defaultTencentAPIService { - tencentAPIService := &defaultTencentAPIService{ - RetryDefault: 3, - TaskCheckInterval: 3 * time.Second, - ClientSetService: NewTencentClientSetService(region, rate, secretId, secretKey, internetEndpoint), - } - return tencentAPIService -} - -// ////////////////////////////////////////////////////////////// -// PrivateDns API -// ////////////////////////////////////////////////////////////// - -func (api *defaultTencentAPIService) CreatePrivateZoneRecord(request *privatedns.CreatePrivateZoneRecordRequest) (response *privatedns.CreatePrivateZoneRecordResponse, err error) { - apiAction := CreatePrivateZoneRecord - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.PrivateDnsCli(apiAction.Name) - if response, err = client.CreatePrivateZoneRecord(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -func (api *defaultTencentAPIService) DeletePrivateZoneRecord(request *privatedns.DeletePrivateZoneRecordRequest) (response *privatedns.DeletePrivateZoneRecordResponse, err error) { - apiAction := DeletePrivateZoneRecord - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.PrivateDnsCli(apiAction.Name) - if response, err = client.DeletePrivateZoneRecord(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -func (api *defaultTencentAPIService) ModifyPrivateZoneRecord(request *privatedns.ModifyPrivateZoneRecordRequest) (response *privatedns.ModifyPrivateZoneRecordResponse, err error) { - apiAction := ModifyPrivateZoneRecord - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.PrivateDnsCli(apiAction.Name) - if response, err = client.ModifyPrivateZoneRecord(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -func (api *defaultTencentAPIService) DescribePrivateZoneList(request *privatedns.DescribePrivateZoneListRequest) (response *privatedns.DescribePrivateZoneListResponse, err error) { - apiAction := DescribePrivateZoneList - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.PrivateDnsCli(apiAction.Name) - if response, err = client.DescribePrivateZoneList(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -func (api *defaultTencentAPIService) DescribePrivateZoneRecordList(request *privatedns.DescribePrivateZoneRecordListRequest) (response *privatedns.DescribePrivateZoneRecordListResponse, err error) { - apiAction := DescribePrivateZoneRecordList - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.PrivateDnsCli(apiAction.Name) - if response, err = client.DescribePrivateZoneRecordList(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -// ////////////////////////////////////////////////////////////// -// DnsPod API -// ////////////////////////////////////////////////////////////// - -func (api *defaultTencentAPIService) DescribeDomainList(request *dnspod.DescribeDomainListRequest) (response *dnspod.DescribeDomainListResponse, err error) { - apiAction := DescribeDomainList - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.DnsPodCli(apiAction.Name) - if response, err = client.DescribeDomainList(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -func (api *defaultTencentAPIService) DescribeRecordList(request *dnspod.DescribeRecordListRequest) (response *dnspod.DescribeRecordListResponse, err error) { - apiAction := DescribeRecordList - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.DnsPodCli(apiAction.Name) - if response, err = client.DescribeRecordList(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -func (api *defaultTencentAPIService) CreateRecord(request *dnspod.CreateRecordRequest) (response *dnspod.CreateRecordResponse, err error) { - apiAction := CreateRecord - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.DnsPodCli(apiAction.Name) - if response, err = client.CreateRecord(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -func (api *defaultTencentAPIService) DeleteRecord(request *dnspod.DeleteRecordRequest) (response *dnspod.DeleteRecordResponse, err error) { - apiAction := DeleteRecord - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.DnsPodCli(apiAction.Name) - if response, err = client.DeleteRecord(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -func (api *defaultTencentAPIService) ModifyRecord(request *dnspod.ModifyRecordRequest) (response *dnspod.ModifyRecordResponse, err error) { - apiAction := ModifyRecord - for times := 1; times <= api.RetryDefault; times++ { - client := api.ClientSetService.DnsPodCli(apiAction.Name) - if response, err = client.ModifyRecord(request); err != nil { - requestJson := JsonWrapper(request) - if retry := dealWithError(apiAction, requestJson, err); retry || times == api.RetryDefault { - APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) - return nil, err - } - continue - } - break - } - APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) - return response, nil -} - -// ////////////////////////////////////////////////////////////// -// API Error Report -// ////////////////////////////////////////////////////////////// - -func dealWithError(action Action, request string, err error) bool { - log.Errorf("dealWithError %s/%s request: %s, error: %s.", action.Service, action.Name, request, err.Error()) - sdkError := &tclouderrors.TencentCloudSDKError{} - if errors.As(err, &sdkError) { - switch sdkError.Code { - case "RequestLimitExceeded": - return true - case "InternalError", "ClientError.HttpStatusCodeError": - return false - case "ClientError.NetworkError": - return false - case "AuthFailure.UnauthorizedOperation", "UnauthorizedOperation.CamNoAuth": - return false - } - return false - } - - return errors.As(err, new(net.Error)) -} - -func APIErrorRecord(apiAction Action, request string, response string, err error) { - log.Infof("APIError API: %s/%s Request: %s, Response: %s, Error: %s", apiAction.Service, apiAction.Name, request, response, err.Error()) -} - -func APIRecord(apiAction Action, request string, response string) { - message := fmt.Sprintf("APIRecord API: %s/%s Request: %s, Response: %s", apiAction.Service, apiAction.Name, request, response) - - if apiAction.ReadOnly { - // log.Info(message) - } else { - log.Info(message) - } -} - -func JsonWrapper(obj interface{}) string { - if jsonStr, jsonErr := json.Marshal(obj); jsonErr == nil { - return string(jsonStr) - } - return "json_format_error" -} diff --git a/provider/tencentcloud/dnspod.go b/provider/tencentcloud/dnspod.go deleted file mode 100644 index 30fb81ac0..000000000 --- a/provider/tencentcloud/dnspod.go +++ /dev/null @@ -1,281 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tencentcloud - -import ( - "fmt" - "strconv" - "strings" - - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" -) - -// DnsPod For Public Dns - -func (p *TencentCloudProvider) dnsRecords() ([]*endpoint.Endpoint, error) { - recordsList, err := p.recordsForDNS() - if err != nil { - return nil, err - } - - endpoints := make([]*endpoint.Endpoint, 0) - recordMap := groupDomainRecordList(recordsList) - for _, recordList := range recordMap { - name := getDnsDomain(*recordList.RecordList[0].Name, *recordList.Domain.Name) - recordType := *recordList.RecordList[0].Type - ttl := *recordList.RecordList[0].TTL - var targets []string - for _, record := range recordList.RecordList { - targets = append(targets, *record.Value) - } - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...)) - } - return endpoints, nil -} - -func (p *TencentCloudProvider) recordsForDNS() (map[uint64]*RecordListGroup, error) { - domainList, err := p.getDomainList() - if err != nil { - return nil, err - } - - recordListGroup := make(map[uint64]*RecordListGroup, 0) - for _, domain := range domainList { - records, err := p.getDomainRecordList(*domain.Name) - if err != nil { - return nil, err - } - for _, record := range records { - if *record.Type == "TXT" && strings.HasPrefix(*record.Value, "heritage=") { - record.Value = common.StringPtr(fmt.Sprintf(`"%s"`, *record.Value)) - } - } - recordListGroup[*domain.DomainId] = &RecordListGroup{ - Domain: domain, - RecordList: records, - } - } - return recordListGroup, nil -} - -func (p *TencentCloudProvider) getDomainList() ([]*dnspod.DomainListItem, error) { - request := dnspod.NewDescribeDomainListRequest() - request.Offset = common.Int64Ptr(0) - request.Limit = common.Int64Ptr(3000) - - domainList := make([]*dnspod.DomainListItem, 0) - totalCount := int64(100) - for *request.Offset < totalCount { - response, err := p.apiService.DescribeDomainList(request) - if err != nil { - return nil, err - } - if len(response.Response.DomainList) > 0 { - if !p.domainFilter.IsConfigured() { - domainList = append(domainList, response.Response.DomainList...) - } else { - for _, domain := range response.Response.DomainList { - if p.domainFilter.Match(*domain.Name) { - domainList = append(domainList, domain) - } - } - } - } - totalCount = int64(*response.Response.DomainCountInfo.AllTotal) - request.Offset = common.Int64Ptr(*request.Offset + int64(len(response.Response.DomainList))) - } - return domainList, nil -} - -func (p *TencentCloudProvider) getDomainRecordList(domain string) ([]*dnspod.RecordListItem, error) { - request := dnspod.NewDescribeRecordListRequest() - request.Domain = common.StringPtr(domain) - request.Offset = common.Uint64Ptr(0) - request.Limit = common.Uint64Ptr(3000) - - domainList := make([]*dnspod.RecordListItem, 0) - totalCount := uint64(100) - for *request.Offset < totalCount { - response, err := p.apiService.DescribeRecordList(request) - if err != nil { - return nil, err - } - if len(response.Response.RecordList) > 0 { - for _, record := range response.Response.RecordList { - if *record.Name == "@" && *record.Type == "NS" { // Special Record, Skip it. - continue - } - domainList = append(domainList, record) - } - } - totalCount = *response.Response.RecordCountInfo.TotalCount - request.Offset = common.Uint64Ptr(*request.Offset + uint64(len(response.Response.RecordList))) - } - return domainList, nil -} - -type RecordListGroup struct { - Domain *dnspod.DomainListItem - RecordList []*dnspod.RecordListItem -} - -func (p *TencentCloudProvider) applyChangesForDNS(changes *plan.Changes) error { - recordsGroupMap, err := p.recordsForDNS() - if err != nil { - return err - } - - zoneNameIDMapper := provider.ZoneIDName{} - for _, recordsGroup := range recordsGroupMap { - if recordsGroup.Domain.DomainId != nil { - zoneNameIDMapper.Add(strconv.FormatUint(*recordsGroup.Domain.DomainId, 10), *recordsGroup.Domain.Name) - } - } - - // Apply Change Delete - deleteEndpoints := make(map[string][]uint64) - for _, change := range [][]*endpoint.Endpoint{changes.Delete, changes.UpdateOld} { - for _, deleteChange := range change { - if zoneId, _ := zoneNameIDMapper.FindZone(deleteChange.DNSName); zoneId != "" { - zoneIdString, _ := strconv.ParseUint(zoneId, 10, 64) - recordListGroup := recordsGroupMap[zoneIdString] - for _, domainRecord := range recordListGroup.RecordList { - subDomain := getSubDomain(*recordListGroup.Domain.Name, deleteChange) - if *domainRecord.Name == subDomain && *domainRecord.Type == deleteChange.RecordType { - for _, target := range deleteChange.Targets { - if *domainRecord.Value == target { - if _, exist := deleteEndpoints[*recordListGroup.Domain.Name]; !exist { - deleteEndpoints[*recordListGroup.Domain.Name] = make([]uint64, 0) - } - deleteEndpoints[*recordListGroup.Domain.Name] = append(deleteEndpoints[*recordListGroup.Domain.Name], *domainRecord.RecordId) - } - } - } - } - } - } - } - - if err := p.deleteRecords(deleteEndpoints); err != nil { - return err - } - - // Apply Change Create - createEndpoints := make(map[string][]*endpoint.Endpoint) - for zoneId := range zoneNameIDMapper { - createEndpoints[zoneId] = make([]*endpoint.Endpoint, 0) - } - for _, change := range [][]*endpoint.Endpoint{changes.Create, changes.UpdateNew} { - for _, createChange := range change { - if zoneId, _ := zoneNameIDMapper.FindZone(createChange.DNSName); zoneId != "" { - createEndpoints[zoneId] = append(createEndpoints[zoneId], createChange) - } - } - } - if err := p.createRecord(recordsGroupMap, createEndpoints); err != nil { - return err - } - return nil -} - -func (p *TencentCloudProvider) createRecord(zoneMap map[uint64]*RecordListGroup, endpointsMap map[string][]*endpoint.Endpoint) error { - for zoneId, endpoints := range endpointsMap { - zoneIdString, _ := strconv.ParseUint(zoneId, 10, 64) - domain := zoneMap[zoneIdString] - for _, endpoint := range endpoints { - for _, target := range endpoint.Targets { - if endpoint.RecordType == "TXT" && strings.HasPrefix(target, `"heritage=`) { - target = strings.Trim(target, `"`) - } - if err := p.createRecords(domain.Domain, endpoint, target); err != nil { - return err - } - } - } - } - return nil -} - -func (p *TencentCloudProvider) createRecords(domain *dnspod.DomainListItem, endpoint *endpoint.Endpoint, target string) error { - request := dnspod.NewCreateRecordRequest() - - request.Domain = common.StringPtr(*domain.Name) - request.RecordType = common.StringPtr(endpoint.RecordType) - request.Value = common.StringPtr(target) - request.SubDomain = common.StringPtr(getSubDomain(*domain.Name, endpoint)) - if endpoint.RecordTTL.IsConfigured() { - request.TTL = common.Uint64Ptr(uint64(endpoint.RecordTTL)) - } - request.RecordLine = common.StringPtr("默认") - - if _, err := p.apiService.CreateRecord(request); err != nil { - return err - } - return nil -} - -func (p *TencentCloudProvider) deleteRecords(RecordIdsMap map[string][]uint64) error { - for domain, recordIds := range RecordIdsMap { - if len(recordIds) == 0 { - continue - } - if err := p.deleteRecord(domain, recordIds); err != nil { - return err - } - } - return nil -} - -func (p *TencentCloudProvider) deleteRecord(domain string, recordIds []uint64) error { - request := dnspod.NewDeleteRecordRequest() - request.Domain = common.StringPtr(domain) - - for _, recordId := range recordIds { - request.RecordId = common.Uint64Ptr(recordId) - if _, err := p.apiService.DeleteRecord(request); err != nil { - return err - } - } - return nil -} - -func groupDomainRecordList(recordListGroup map[uint64]*RecordListGroup) (endpointMap map[string]*RecordListGroup) { - endpointMap = make(map[string]*RecordListGroup) - - for _, recordGroup := range recordListGroup { - for _, record := range recordGroup.RecordList { - key := fmt.Sprintf("%s:%s.%s", *record.Type, *record.Name, *recordGroup.Domain.Name) - if *record.Name == TencentCloudEmptyPrefix { - key = fmt.Sprintf("%s:%s", *record.Type, *recordGroup.Domain.Name) - } - if _, exist := endpointMap[key]; !exist { - endpointMap[key] = &RecordListGroup{ - Domain: recordGroup.Domain, - RecordList: make([]*dnspod.RecordListItem, 0), - } - } - endpointMap[key].RecordList = append(endpointMap[key].RecordList, record) - } - } - - return endpointMap -} diff --git a/provider/tencentcloud/privatedns.go b/provider/tencentcloud/privatedns.go deleted file mode 100644 index e209f535f..000000000 --- a/provider/tencentcloud/privatedns.go +++ /dev/null @@ -1,319 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tencentcloud - -import ( - "fmt" - "strings" - - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" -) - -// PrivateZone For Internal Dns - -func (p *TencentCloudProvider) privateZoneRecords() ([]*endpoint.Endpoint, error) { - privateZones, err := p.recordForPrivateZone() - if err != nil { - return nil, err - } - - endpoints := make([]*endpoint.Endpoint, 0) - recordMap := groupPrivateZoneRecords(privateZones) - for _, recordList := range recordMap { - name := getDnsDomain(*recordList.RecordList[0].SubDomain, *recordList.Zone.Domain) - recordType := *recordList.RecordList[0].RecordType - ttl := *recordList.RecordList[0].TTL - var targets []string - for _, record := range recordList.RecordList { - targets = append(targets, *record.RecordValue) - } - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...)) - } - return endpoints, nil -} - -func (p *TencentCloudProvider) recordForPrivateZone() (map[string]*PrivateZoneRecordListGroup, error) { - privateZones, err := p.getPrivateZones() - if err != nil { - return nil, err - } - - recordListGroup := make(map[string]*PrivateZoneRecordListGroup, 0) - for _, zone := range privateZones { - records, err := p.getPrivateZoneRecords(*zone.ZoneId) - if err != nil { - return nil, err - } - - for _, record := range records { - if *record.RecordType == "TXT" && strings.HasPrefix(*record.RecordValue, "heritage=") { - record.RecordValue = common.StringPtr(fmt.Sprintf("\"%s\"", *record.RecordValue)) - } - } - recordListGroup[*zone.ZoneId] = &PrivateZoneRecordListGroup{ - Zone: zone, - RecordList: records, - } - } - - return recordListGroup, nil -} - -func (p *TencentCloudProvider) getPrivateZones() ([]*privatedns.PrivateZone, error) { - filters := make([]*privatedns.Filter, 1) - filters[0] = &privatedns.Filter{ - Name: common.StringPtr("Vpc"), - Values: []*string{ - common.StringPtr(p.vpcID), - }, - } - - if p.zoneIDFilter.IsConfigured() { - zoneIDs := make([]*string, len(p.zoneIDFilter.ZoneIDs)) - for index, zoneId := range p.zoneIDFilter.ZoneIDs { - zoneIDs[index] = common.StringPtr(zoneId) - } - filters = append(filters, &privatedns.Filter{ - Name: common.StringPtr("ZoneId"), - Values: zoneIDs, - }) - } - - request := privatedns.NewDescribePrivateZoneListRequest() - request.Filters = filters - request.Offset = common.Int64Ptr(0) - request.Limit = common.Int64Ptr(100) - - privateZones := make([]*privatedns.PrivateZone, 0) - totalCount := int64(100) - for *request.Offset < totalCount { - response, err := p.apiService.DescribePrivateZoneList(request) - if err != nil { - return nil, err - } - if len(response.Response.PrivateZoneSet) > 0 { - privateZones = append(privateZones, response.Response.PrivateZoneSet...) - } - totalCount = *response.Response.TotalCount - request.Offset = common.Int64Ptr(*request.Offset + int64(len(response.Response.PrivateZoneSet))) - } - - privateZonesFilter := make([]*privatedns.PrivateZone, 0) - for _, privateZone := range privateZones { - if !p.domainFilter.Match(*privateZone.Domain) { - continue - } - privateZonesFilter = append(privateZonesFilter, privateZone) - } - return privateZonesFilter, nil -} - -func (p *TencentCloudProvider) getPrivateZoneRecords(zoneId string) ([]*privatedns.PrivateZoneRecord, error) { - request := privatedns.NewDescribePrivateZoneRecordListRequest() - request.ZoneId = common.StringPtr(zoneId) - request.Offset = common.Int64Ptr(0) - request.Limit = common.Int64Ptr(100) - - privateZoneRecords := make([]*privatedns.PrivateZoneRecord, 0) - totalCount := int64(100) - for *request.Offset < totalCount { - response, err := p.apiService.DescribePrivateZoneRecordList(request) - if err != nil { - return nil, err - } - if len(response.Response.RecordSet) > 0 { - privateZoneRecords = append(privateZoneRecords, response.Response.RecordSet...) - } - totalCount = *response.Response.TotalCount - request.Offset = common.Int64Ptr(*request.Offset + int64(len(response.Response.RecordSet))) - } - return privateZoneRecords, nil -} - -type PrivateZoneRecordListGroup struct { - Zone *privatedns.PrivateZone - RecordList []*privatedns.PrivateZoneRecord -} - -// Returns nil if the operation was successful or an error if the operation failed. -func (p *TencentCloudProvider) applyChangesForPrivateZone(changes *plan.Changes) error { - zoneGroups, err := p.recordForPrivateZone() - if err != nil { - return err - } - - // In PrivateDns Service. A Zone has at least one record. The last rule cannot be deleted. - for _, zoneGroup := range zoneGroups { - if !containsBaseRecord(zoneGroup.RecordList) { - err := p.createPrivateZoneRecord(zoneGroup.Zone, &endpoint.Endpoint{ - DNSName: *zoneGroup.Zone.Domain, - RecordType: "TXT", - }, "tencent_provider_record") - if err != nil { - return err - } - } - } - - zoneNameIDMapper := provider.ZoneIDName{} - for _, zoneGroup := range zoneGroups { - if zoneGroup.Zone.ZoneId != nil { - zoneNameIDMapper.Add(*zoneGroup.Zone.ZoneId, *zoneGroup.Zone.Domain) - } - } - - // Apply Change Delete - deleteEndpoints := make(map[string][]string) - for _, change := range [][]*endpoint.Endpoint{changes.Delete, changes.UpdateOld} { - for _, deleteChange := range change { - if zoneId, _ := zoneNameIDMapper.FindZone(deleteChange.DNSName); zoneId != "" { - zoneGroup := zoneGroups[zoneId] - for _, zoneRecord := range zoneGroup.RecordList { - subDomain := getSubDomain(*zoneGroup.Zone.Domain, deleteChange) - if *zoneRecord.SubDomain == subDomain && *zoneRecord.RecordType == deleteChange.RecordType { - for _, target := range deleteChange.Targets { - if *zoneRecord.RecordValue == target { - if _, exist := deleteEndpoints[zoneId]; !exist { - deleteEndpoints[zoneId] = make([]string, 0) - } - deleteEndpoints[zoneId] = append(deleteEndpoints[zoneId], *zoneRecord.RecordId) - } - } - } - } - } - } - } - - if err := p.deletePrivateZoneRecords(deleteEndpoints); err != nil { - return err - } - - // Apply Change Create - createEndpoints := make(map[string][]*endpoint.Endpoint) - for _, change := range [][]*endpoint.Endpoint{changes.Create, changes.UpdateNew} { - for _, createChange := range change { - if zoneId, _ := zoneNameIDMapper.FindZone(createChange.DNSName); zoneId != "" { - if _, exist := createEndpoints[zoneId]; !exist { - createEndpoints[zoneId] = make([]*endpoint.Endpoint, 0) - } - createEndpoints[zoneId] = append(createEndpoints[zoneId], createChange) - } - } - } - if err := p.createPrivateZoneRecords(zoneGroups, createEndpoints); err != nil { - return err - } - return nil -} - -func containsBaseRecord(records []*privatedns.PrivateZoneRecord) bool { - for _, record := range records { - if *record.SubDomain == TencentCloudEmptyPrefix && *record.RecordType == "TXT" && *record.RecordValue == "tencent_provider_record" { - return true - } - } - return false -} - -func (p *TencentCloudProvider) createPrivateZoneRecords(zoneGroups map[string]*PrivateZoneRecordListGroup, endpointsMap map[string][]*endpoint.Endpoint) error { - for zoneId, endpoints := range endpointsMap { - zoneGroup := zoneGroups[zoneId] - for _, endpoint := range endpoints { - for _, target := range endpoint.Targets { - if endpoint.RecordType == "TXT" && strings.HasPrefix(target, "\"heritage=") { - target = strings.Trim(target, "\"") - } - if err := p.createPrivateZoneRecord(zoneGroup.Zone, endpoint, target); err != nil { - return err - } - } - } - } - return nil -} - -func (p *TencentCloudProvider) deletePrivateZoneRecords(zoneRecordIdsMap map[string][]string) error { - for zoneId, zoneRecordIds := range zoneRecordIdsMap { - if len(zoneRecordIds) == 0 { - continue - } - if err := p.deletePrivateZoneRecord(zoneId, zoneRecordIds); err != nil { - return err - } - } - return nil -} - -func (p *TencentCloudProvider) createPrivateZoneRecord(zone *privatedns.PrivateZone, endpoint *endpoint.Endpoint, target string) error { - request := privatedns.NewCreatePrivateZoneRecordRequest() - request.ZoneId = common.StringPtr(*zone.ZoneId) - request.RecordType = common.StringPtr(endpoint.RecordType) - request.RecordValue = common.StringPtr(target) - request.SubDomain = common.StringPtr(getSubDomain(*zone.Domain, endpoint)) - if endpoint.RecordTTL.IsConfigured() { - request.TTL = common.Int64Ptr(int64(endpoint.RecordTTL)) - } - - if _, err := p.apiService.CreatePrivateZoneRecord(request); err != nil { - return err - } - return nil -} - -func (p *TencentCloudProvider) deletePrivateZoneRecord(zoneId string, zoneRecordIds []string) error { - recordIds := make([]*string, len(zoneRecordIds)) - for index, recordId := range zoneRecordIds { - recordIds[index] = common.StringPtr(recordId) - } - - request := privatedns.NewDeletePrivateZoneRecordRequest() - request.ZoneId = common.StringPtr(zoneId) - request.RecordIdSet = recordIds - - if _, err := p.apiService.DeletePrivateZoneRecord(request); err != nil { - return err - } - return nil -} - -func groupPrivateZoneRecords(zoneRecords map[string]*PrivateZoneRecordListGroup) (endpointMap map[string]*PrivateZoneRecordListGroup) { - endpointMap = make(map[string]*PrivateZoneRecordListGroup) - - for _, recordGroup := range zoneRecords { - for _, record := range recordGroup.RecordList { - key := fmt.Sprintf("%s:%s.%s", *record.RecordType, *record.SubDomain, *recordGroup.Zone.Domain) - if *record.SubDomain == TencentCloudEmptyPrefix { - key = fmt.Sprintf("%s:%s", *record.RecordType, *recordGroup.Zone.Domain) - } - if _, exist := endpointMap[key]; !exist { - endpointMap[key] = &PrivateZoneRecordListGroup{ - Zone: recordGroup.Zone, - RecordList: make([]*privatedns.PrivateZoneRecord, 0), - } - } - endpointMap[key].RecordList = append(endpointMap[key].RecordList, record) - } - } - - return endpointMap -} diff --git a/provider/tencentcloud/tencent_cloud.go b/provider/tencentcloud/tencent_cloud.go deleted file mode 100644 index f4435a011..000000000 --- a/provider/tencentcloud/tencent_cloud.go +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tencentcloud - -import ( - "context" - "encoding/json" - "fmt" - "os" - "strings" - - log "github.com/sirupsen/logrus" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" - "sigs.k8s.io/external-dns/provider/tencentcloud/cloudapi" -) - -const ( - TencentCloudEmptyPrefix = "@" - DefaultAPIRate = 9 -) - -func NewTencentCloudProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, configFile string, zoneType string, dryRun bool) (*TencentCloudProvider, error) { - cfg := tencentCloudConfig{} - if configFile != "" { - contents, err := os.ReadFile(configFile) - if err != nil { - return nil, fmt.Errorf("failed to read Tencent Cloud config file '%s': %w", configFile, err) - } - err = json.Unmarshal(contents, &cfg) - if err != nil { - return nil, fmt.Errorf("failed to parse Tencent Cloud config file '%s': %w", configFile, err) - } - } - - var apiService cloudapi.TencentAPIService = cloudapi.NewTencentAPIService(cfg.RegionId, DefaultAPIRate, cfg.SecretId, cfg.SecretKey, cfg.InternetEndpoint) - if dryRun { - apiService = cloudapi.NewReadOnlyAPIService(cfg.RegionId, DefaultAPIRate, cfg.SecretId, cfg.SecretKey, cfg.InternetEndpoint) - } - - tencentCloudProvider := &TencentCloudProvider{ - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - apiService: apiService, - vpcID: cfg.VPCId, - privateZone: zoneType == "private", - } - - return tencentCloudProvider, nil -} - -type TencentCloudProvider struct { - provider.BaseProvider - apiService cloudapi.TencentAPIService - domainFilter endpoint.DomainFilter - zoneIDFilter provider.ZoneIDFilter // Private Zone only - vpcID string // Private Zone only - privateZone bool -} - -type tencentCloudConfig struct { - RegionId string `json:"regionId" yaml:"regionId"` - SecretId string `json:"secretId" yaml:"secretId"` - SecretKey string `json:"secretKey" yaml:"secretKey"` - VPCId string `json:"vpcId" yaml:"vpcId"` - InternetEndpoint bool `json:"internetEndpoint" yaml:"internetEndpoint"` -} - -func (p *TencentCloudProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - if p.privateZone { - return p.privateZoneRecords() - } - return p.dnsRecords() -} - -func (p *TencentCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - if !changes.HasChanges() { - return nil - } - - log.Infof("apply changes. %s", cloudapi.JsonWrapper(changes)) - - if p.privateZone { - return p.applyChangesForPrivateZone(changes) - } - return p.applyChangesForDNS(changes) -} - -func getSubDomain(domain string, endpoint *endpoint.Endpoint) string { - name := endpoint.DNSName - name = name[:len(name)-len(domain)] - name = strings.TrimSuffix(name, ".") - - if name == "" { - return TencentCloudEmptyPrefix - } - return name -} - -func getDnsDomain(subDomain string, domain string) string { - if subDomain == TencentCloudEmptyPrefix { - return domain - } - return subDomain + "." + domain -} diff --git a/provider/tencentcloud/tencent_cloud_test.go b/provider/tencentcloud/tencent_cloud_test.go deleted file mode 100644 index 06355a391..000000000 --- a/provider/tencentcloud/tencent_cloud_test.go +++ /dev/null @@ -1,403 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tencentcloud - -import ( - "context" - "testing" - - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" - "sigs.k8s.io/external-dns/provider/tencentcloud/cloudapi" - - privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" -) - -func NewMockTencentCloudProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneType string) *TencentCloudProvider { - cfg := tencentCloudConfig{ - RegionId: "ap-shanghai", - VPCId: "vpc-abcdefg", - } - - zoneId1 := common.StringPtr(cloudapi.RandStringRunes(8)) - - privateZones := []*privatedns.PrivateZone{ - { - ZoneId: zoneId1, - Domain: common.StringPtr("external-dns-test.com"), - VpcSet: []*privatedns.VpcInfo{ - { - UniqVpcId: common.StringPtr("vpc-abcdefg"), - Region: common.StringPtr("ap-shanghai"), - }, - }, - }, - } - - zoneRecordId1 := common.StringPtr(cloudapi.RandStringRunes(8)) - zoneRecordId2 := common.StringPtr(cloudapi.RandStringRunes(8)) - privateZoneRecords := map[string][]*privatedns.PrivateZoneRecord{ - *zoneId1: { - { - ZoneId: zoneId1, - RecordId: zoneRecordId1, - SubDomain: common.StringPtr("nginx"), - RecordType: common.StringPtr("TXT"), - RecordValue: common.StringPtr("heritage=external-dns,external-dns/owner=default"), - TTL: common.Int64Ptr(300), - }, - { - ZoneId: zoneId1, - RecordId: zoneRecordId2, - SubDomain: common.StringPtr("nginx"), - RecordType: common.StringPtr("A"), - RecordValue: common.StringPtr("10.10.10.10"), - TTL: common.Int64Ptr(300), - }, - }, - } - - dnsDomainId1 := common.Uint64Ptr(cloudapi.RandUint64()) - dnsPodDomains := []*dnspod.DomainListItem{ - { - DomainId: dnsDomainId1, - Name: common.StringPtr("external-dns-test.com"), - }, - } - dnsDomainRecordId1 := common.Uint64Ptr(cloudapi.RandUint64()) - dnsDomainRecordId2 := common.Uint64Ptr(cloudapi.RandUint64()) - dnspodRecords := map[string][]*dnspod.RecordListItem{ - "external-dns-test.com": { - { - RecordId: dnsDomainRecordId1, - Value: common.StringPtr("heritage=external-dns,external-dns/owner=default"), - Name: common.StringPtr("nginx"), - Type: common.StringPtr("TXT"), - TTL: common.Uint64Ptr(300), - }, - { - RecordId: dnsDomainRecordId2, - Name: common.StringPtr("nginx"), - Type: common.StringPtr("A"), - Value: common.StringPtr("10.10.10.10"), - TTL: common.Uint64Ptr(300), - }, - }, - } - - var apiService cloudapi.TencentAPIService = cloudapi.NewMockService(privateZones, privateZoneRecords, dnsPodDomains, dnspodRecords) - - tencentCloudProvider := &TencentCloudProvider{ - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - apiService: apiService, - vpcID: cfg.VPCId, - privateZone: zoneType == "private", - } - - return tencentCloudProvider -} - -func TestTencentPrivateProvider_Records(t *testing.T) { - p := NewMockTencentCloudProvider(endpoint.NewDomainFilter([]string{"external-dns-test.com"}), provider.NewZoneIDFilter([]string{}), "private") - endpoints, err := p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 2 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } - - // Test for Create、UpdateOld、UpdateNew、Delete - // The base record will be created. - changes := &plan.Changes{ - Create: []*endpoint.Endpoint{ - { - DNSName: "redis.external-dns-test.com", - RecordType: "A", - RecordTTL: 300, - Targets: endpoint.NewTargets("4.3.2.1"), - }, - }, - UpdateOld: []*endpoint.Endpoint{ - { - DNSName: "nginx.external-dns-test.com", - RecordType: "A", - RecordTTL: 300, - Targets: endpoint.NewTargets("10.10.10.10"), - }, - }, - UpdateNew: []*endpoint.Endpoint{ - { - DNSName: "tencent.external-dns-test.com", - RecordType: "A", - RecordTTL: 600, - Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), - }, - }, - Delete: []*endpoint.Endpoint{ - { - DNSName: "nginx.external-dns-test.com", - RecordType: "TXT", - RecordTTL: 300, - Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=default\""), - }, - }, - } - if err := p.ApplyChanges(context.Background(), changes); err != nil { - t.Errorf("Failed to get records: %v", err) - } - endpoints, err = p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 3 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } - - // Test for Delete one target - changes = &plan.Changes{ - Delete: []*endpoint.Endpoint{ - { - DNSName: "tencent.external-dns-test.com", - RecordType: "A", - RecordTTL: 600, - Targets: endpoint.NewTargets("5.6.7.8"), - }, - }, - } - if err := p.ApplyChanges(context.Background(), changes); err != nil { - t.Errorf("Failed to get records: %v", err) - } - endpoints, err = p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 3 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } - - // Test for Delete another target - changes = &plan.Changes{ - Create: []*endpoint.Endpoint{ - { - DNSName: "redis.external-dns-test.com", - RecordType: "A", - RecordTTL: 300, - Targets: endpoint.NewTargets("5.6.7.8"), - }, - }, - } - if err := p.ApplyChanges(context.Background(), changes); err != nil { - t.Errorf("Failed to get records: %v", err) - } - endpoints, err = p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 3 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } - - // Test for Delete another target - changes = &plan.Changes{ - Delete: []*endpoint.Endpoint{ - { - DNSName: "tencent.external-dns-test.com", - RecordType: "A", - RecordTTL: 600, - Targets: endpoint.NewTargets("1.2.3.4"), - }, - }, - } - if err := p.ApplyChanges(context.Background(), changes); err != nil { - t.Errorf("Failed to get records: %v", err) - } - endpoints, err = p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 2 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } -} - -func TestTencentPublicProvider_Records(t *testing.T) { - p := NewMockTencentCloudProvider(endpoint.NewDomainFilter([]string{"external-dns-test.com"}), provider.NewZoneIDFilter([]string{}), "public") - endpoints, err := p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 2 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } - - // Test for Create、UpdateOld、UpdateNew、Delete - changes := &plan.Changes{ - Create: []*endpoint.Endpoint{ - { - DNSName: "redis.external-dns-test.com", - RecordType: "A", - RecordTTL: 300, - Targets: endpoint.NewTargets("4.3.2.1"), - }, - }, - UpdateOld: []*endpoint.Endpoint{ - { - DNSName: "nginx.external-dns-test.com", - RecordType: "A", - RecordTTL: 300, - Targets: endpoint.NewTargets("10.10.10.10"), - }, - }, - UpdateNew: []*endpoint.Endpoint{ - { - DNSName: "tencent.external-dns-test.com", - RecordType: "A", - RecordTTL: 600, - Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), - }, - }, - Delete: []*endpoint.Endpoint{ - { - DNSName: "nginx.external-dns-test.com", - RecordType: "TXT", - RecordTTL: 300, - Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=default\""), - }, - }, - } - if err := p.ApplyChanges(context.Background(), changes); err != nil { - t.Errorf("Failed to get records: %v", err) - } - endpoints, err = p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 2 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } - - // Test for Delete one target - changes = &plan.Changes{ - Delete: []*endpoint.Endpoint{ - { - DNSName: "tencent.external-dns-test.com", - RecordType: "A", - RecordTTL: 600, - Targets: endpoint.NewTargets("5.6.7.8"), - }, - }, - } - if err := p.ApplyChanges(context.Background(), changes); err != nil { - t.Errorf("Failed to get records: %v", err) - } - endpoints, err = p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 2 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } - - // Test for Delete another target - changes = &plan.Changes{ - Create: []*endpoint.Endpoint{ - { - DNSName: "redis.external-dns-test.com", - RecordType: "A", - RecordTTL: 300, - Targets: endpoint.NewTargets("5.6.7.8"), - }, - }, - } - if err := p.ApplyChanges(context.Background(), changes); err != nil { - t.Errorf("Failed to get records: %v", err) - } - endpoints, err = p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 2 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } - - // Test for Delete another target - changes = &plan.Changes{ - Delete: []*endpoint.Endpoint{ - { - DNSName: "tencent.external-dns-test.com", - RecordType: "A", - RecordTTL: 600, - Targets: endpoint.NewTargets("1.2.3.4"), - }, - }, - } - if err := p.ApplyChanges(context.Background(), changes); err != nil { - t.Errorf("Failed to get records: %v", err) - } - endpoints, err = p.Records(context.Background()) - if err != nil { - t.Errorf("Failed to get records: %v", err) - } else { - if len(endpoints) != 1 { - t.Errorf("Incorrect number of records: %d", len(endpoints)) - } - for _, endpoint := range endpoints { - t.Logf("Endpoint for %+v", *endpoint) - } - } -} From a48b75b2667bd803ce1de46e5b5c9e893f8c7fef Mon Sep 17 00:00:00 2001 From: Michel Loiseleur Date: Sun, 25 May 2025 10:09:11 +0200 Subject: [PATCH 27/33] remove ultradns provider --- OWNERS | 2 +- README.md | 2 - controller/execute.go | 3 - docs/advanced/ttl.md | 5 - docs/flags.md | 2 +- docs/providers.md | 1 - docs/tutorials/ultradns.md | 665 ------------------------- go.mod | 4 - go.sum | 8 - pkg/apis/externaldns/types.go | 2 +- provider/ultradns/ultradns.go | 498 ------------------- provider/ultradns/ultradns_test.go | 756 ----------------------------- 12 files changed, 3 insertions(+), 1945 deletions(-) delete mode 100644 docs/tutorials/ultradns.md delete mode 100644 provider/ultradns/ultradns.go delete mode 100644 provider/ultradns/ultradns_test.go diff --git a/OWNERS b/OWNERS index 8ee1d2f0c..b9d702e10 100644 --- a/OWNERS +++ b/OWNERS @@ -51,6 +51,6 @@ filters: "provider/cloudflare": labels: - provider/cloudflare - "provider/(akamai|alibabacloud|civo|designate|digitalocean|dnsimple|exoscale|gandi|godaddy|linode|ns1|oci|ovh|pihole|plural|scaleway|transip|ultradns)": + "provider/(akamai|alibabacloud|civo|designate|digitalocean|dnsimple|exoscale|gandi|godaddy|linode|ns1|oci|ovh|pihole|plural|scaleway|transip)": labels: - provider diff --git a/README.md b/README.md index 9d69b0720..0adde245c 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,6 @@ The following table clarifies the current status of the providers according to t | TransIP | Alpha | | | OVHcloud | Beta | @rbeuque74 | | Scaleway DNS | Alpha | @Sh4d1 | -| UltraDNS | Alpha | | | GoDaddy | Alpha | | | Gandi | Alpha | @packi | | Plural | Alpha | @michaeljguarino | @@ -206,7 +205,6 @@ The following tutorials are provided: - [TransIP](docs/tutorials/transip.md) - [OVHcloud](docs/tutorials/ovh.md) - [Scaleway](docs/tutorials/scaleway.md) -- [UltraDNS](docs/tutorials/ultradns.md) - [GoDaddy](docs/tutorials/godaddy.md) - [Gandi](docs/tutorials/gandi.md) - [Nodes as source](docs/sources/nodes.md) diff --git a/controller/execute.go b/controller/execute.go index 7d76aab25..c4dcd5612 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -64,7 +64,6 @@ import ( "sigs.k8s.io/external-dns/provider/rfc2136" "sigs.k8s.io/external-dns/provider/scaleway" "sigs.k8s.io/external-dns/provider/transip" - "sigs.k8s.io/external-dns/provider/ultradns" "sigs.k8s.io/external-dns/provider/webhook" webhookapi "sigs.k8s.io/external-dns/provider/webhook/api" "sigs.k8s.io/external-dns/registry" @@ -187,8 +186,6 @@ func Execute() { p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.AzureMaxRetriesCount, cfg.DryRun) case "azure-private-dns": p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.AzureMaxRetriesCount, cfg.DryRun) - case "ultradns": - p, err = ultradns.NewUltraDNSProvider(domainFilter, cfg.DryRun) case "civo": p, err = civo.NewCivoProvider(domainFilter, cfg.DryRun) case "cloudflare": diff --git a/docs/advanced/ttl.md b/docs/advanced/ttl.md index 5cc19bebf..5cc837e55 100644 --- a/docs/advanced/ttl.md +++ b/docs/advanced/ttl.md @@ -43,7 +43,6 @@ TTL must be a positive value. - [x] Linode - [x] TransIP - [x] RFC2136 -- [x] UltraDNS PRs welcome! @@ -90,7 +89,3 @@ The Linode Provider default TTL is used when the TTL is 0. The default is 24 hou ### TransIP Provider The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s. - -### UltraDNS - -The UltraDNS provider minimal TTL is used when the TTL is not provided. The default TTL is account level default TTL, if defined, otherwise 24 hours. diff --git a/docs/flags.md b/docs/flags.md index a8284b7f1..231b5b18e 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -51,7 +51,7 @@ | `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) | | `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group | | `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group | -| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, ultradns, webhook) | +| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, webhook) | | `--provider-cache-time=0s` | The time to cache the DNS provider record list requests. | | `--domain-filter=` | Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional) | | `--exclude-domains=` | Exclude subdomains (optional) | diff --git a/docs/providers.md b/docs/providers.md index f601ac972..228622939 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -29,5 +29,4 @@ Provider supported configurations | RFC2136 | n/a | yes | n/a | | Scaleway | n/a | n/a | 300 | | Transip | n/a | yes | 60 | -| Ultradns | n/a | yes | n/a | | Webhook | n/a | n/a | n/a | diff --git a/docs/tutorials/ultradns.md b/docs/tutorials/ultradns.md deleted file mode 100644 index b83d15319..000000000 --- a/docs/tutorials/ultradns.md +++ /dev/null @@ -1,665 +0,0 @@ -# UltraDNS - -This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using UltraDNS. - -For this tutorial, please make sure that you are using a version **> 0.7.2** of ExternalDNS. - -## Managing DNS with UltraDNS - -If you would like to read-up on the UltraDNS service, you can find additional details here: [Introduction to UltraDNS](https://docs.ultradns.com/) - -Before proceeding, please create a new DNS Zone that you will create your records in for this tutorial process. For the examples in this tutorial, we will be using `example.com` as our Zone. - -## Setting Up UltraDNS Credentials - -The following environment variables will be needed to run ExternalDNS with UltraDNS. - -`ULTRADNS_USERNAME`,`ULTRADNS_PASSWORD`, &`ULTRADNS_BASEURL` -`ULTRADNS_ACCOUNTNAME`(optional variable). - -## Deploying ExternalDNS - -Connect your `kubectl` client to the cluster you want to test ExternalDNS with. -Then, apply one of the following manifests file to deploy ExternalDNS. - -- Note: We are assuming the zone is already present within UltraDNS. -- Note: While creating CNAMES as target endpoints, the `--txt-prefix` option is mandatory. - -### Manifest (for clusters without RBAC enabled) - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: external-dns -spec: - strategy: - type: Recreate - selector: - matchLabels: - app: external-dns - template: - metadata: - labels: - app: external-dns - spec: - containers: - - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.17.0 - args: - - --source=service - - --source=ingress # ingress is also possible - - --domain-filter=example.com # (Recommended) We recommend to use this filter as it minimize the time to propagate changes, as there are less number of zones to look into.. - - --provider=ultradns - - --txt-prefix=txt- - env: - - name: ULTRADNS_USERNAME - value: "" - - name: ULTRADNS_PASSWORD # The password is required to be BASE64 encrypted. - value: "" - - name: ULTRADNS_BASEURL - value: "https://api.ultradns.com/" - - name: ULTRADNS_ACCOUNTNAME - value: "" -``` - -### Manifest (for clusters with RBAC enabled) - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: external-dns ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: external-dns -rules: -- apiGroups: [""] - resources: ["services","endpoints","pods"] - verbs: ["get","watch","list"] -- apiGroups: ["extensions"] - resources: ["ingresses"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["nodes"] - verbs: ["list","watch"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: external-dns-viewer -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: external-dns -subjects: -- kind: ServiceAccount - name: external-dns - namespace: default ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: external-dns -spec: - strategy: - type: Recreate - selector: - matchLabels: - app: external-dns - template: - metadata: - labels: - app: external-dns - spec: - serviceAccountName: external-dns - containers: - - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.17.0 - args: - - --source=service - - --source=ingress - - --domain-filter=example.com #(Recommended) We recommend to use this filter as it minimize the time to propagate changes, as there are less number of zones to look into.. - - --provider=ultradns - - --txt-prefix=txt- - env: - - name: ULTRADNS_USERNAME - value: "" - - name: ULTRADNS_PASSWORD # The password is required to be BASE64 encrypted. - value: "" - - name: ULTRADNS_BASEURL - value: "https://api.ultradns.com/" - - name: ULTRADNS_ACCOUNTNAME - value: "" -``` - -## Deploying an Nginx Service - -Create a service file called 'nginx.yaml' with the following contents: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx -spec: - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - image: nginx - name: nginx - ports: - - containerPort: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: nginx - annotations: - external-dns.alpha.kubernetes.io/hostname: my-app.example.com. -spec: - selector: - app: nginx - type: LoadBalancer - ports: - - protocol: TCP - port: 80 - targetPort: 80 -``` - -Please note the annotation on the service. Use the same hostname as the UltraDNS zone created above. - -ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. - -## Creating the Deployment and Service - -```console -kubectl create -f nginx.yaml -kubectl create -f external-dns.yaml -``` - -Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. - -Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and will synchronize the UltraDNS records. - -## Verifying UltraDNS Records - -Please verify on the [UltraDNS UI](https://portal.ultradns.com/login) that the records are created under the zone "example.com". - -For more information on UltraDNS UI, refer to (https://docs.ultradns.com/Content/MSP_User_Guide/Content/User%20Guides/MSP_User_Guide/Navigation/Moving%20Around%20the%20UI.htm#_Toc2780722). - -Select the zone that was created above (or select the appropriate zone if a different zone was used.) - -The external IP address will be displayed as a CNAME record for your zone. - -## Cleaning Up the Deployment and Service - -Now that we have verified that ExternalDNS will automatically manage your UltraDNS records, you can delete example zones that you created in this tutorial: - -```sh -kubectl delete service -f nginx.yaml -kubectl delete service -f externaldns.yaml -``` - -## Examples to Manage your Records - -### Creating Multiple A Records Target - -- First, you want to create a service file called 'apple-banana-echo.yaml' - -```yaml ---- -kind: Pod -apiVersion: v1 -metadata: - name: example-app - labels: - app: apple -spec: - containers: - - name: example-app - image: hashicorp/http-echo - args: - - "-text=apple" ---- -kind: Service -apiVersion: v1 -metadata: - name: example-service -spec: - selector: - app: apple - ports: - - port: 5678 # Default port for image -``` - -- Then, create service file called 'expose-apple-banana-app.yaml' to expose the services. For more information to deploy ingress controller, refer to (https://kubernetes.github.io/ingress-nginx/deploy/) - -```yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: example-ingress - annotations: - ingress.kubernetes.io/rewrite-target: / - ingress.kubernetes.io/scheme: internet-facing - external-dns.alpha.kubernetes.io/hostname: apple.example.com. - external-dns.alpha.kubernetes.io/target: 10.10.10.1,10.10.10.23 -spec: - rules: - - http: - paths: - - path: /apple - pathType: Prefix - backend: - service: - name: example-service - port: - number: 5678 -``` - -- Then, create the deployment and service: - -```console -kubectl create -f apple-banana-echo.yaml -kubectl create -f expose-apple-banana-app.yaml -kubectl create -f external-dns.yaml -``` - -- Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. -- Please verify on the [UltraDNS UI](https://portal.ultradns.com/login) that the records have been created under the zone "example.com". -- Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone "example.com": - -```console -kubectl delete -f apple-banana-echo.yaml -kubectl delete -f expose-apple-banana-app.yaml -kubectl delete -f external-dns.yaml -``` - -### Creating CNAME Record - -- Please note, that prior to deploying the external-dns service, you will need to add the option –txt-prefix=txt- into external-dns.yaml. If this not provided, your records will not be created. -- First, create a service file called 'apple-banana-echo.yaml' - - _Config File Example – kubernetes cluster is on-premise not on cloud_ - - ```yaml - --- - kind: Pod - apiVersion: v1 - metadata: - name: example-app - labels: - app: apple - spec: - containers: - - name: example-app - image: hashicorp/http-echo - args: - - "-text=apple" - --- - kind: Service - apiVersion: v1 - metadata: - name: example-service - spec: - selector: - app: apple - ports: - - port: 5678 # Default port for image - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: example-ingress - annotations: - ingress.kubernetes.io/rewrite-target: / - ingress.kubernetes.io/scheme: internet-facing - external-dns.alpha.kubernetes.io/hostname: apple.example.com. - external-dns.alpha.kubernetes.io/target: apple.cname.com. - spec: - rules: - - http: - paths: - - path: /apple - backend: - service: - name: example-service - port: - number: 5678 - ``` - - - _Config File Example – Kubernetes cluster service from different cloud vendors_ - - ```yaml - --- - kind: Pod - apiVersion: v1 - metadata: - name: example-app - labels: - app: apple - spec: - containers: - - name: example-app - image: hashicorp/http-echo - args: - - "-text=apple" - --- - kind: Service - apiVersion: v1 - metadata: - name: example-service - annotations: - external-dns.alpha.kubernetes.io/hostname: my-app.example.com. - spec: - selector: - app: apple - type: LoadBalancer - ports: - - protocol: TCP - port: 5678 - targetPort: 5678 - ``` - -- Then, create the deployment and service: - -```console -kubectl create -f apple-banana-echo.yaml -kubectl create -f external-dns.yaml -``` - -- Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. -- Please verify on the [UltraDNS UI](https://portal.ultradns.com/login), that the records have been created under the zone "example.com". -- Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone "example.com": - -```console -kubectl delete -f apple-banana-echo.yaml -kubectl delete -f external-dns.yaml -``` - -### Creating Multiple Types Of Records - -- Please note, that prior to deploying the external-dns service, you will need to add the option –txt-prefix=txt- into external-dns.yaml. Since you will also be created a CNAME record, If this not provided, your records will not be created. -- First, create a service file called 'apple-banana-echo.yaml' - - _Config File Example – kubernetes cluster is on-premise not on cloud_ - - ```yaml - --- - kind: Pod - apiVersion: v1 - metadata: - name: example-app - labels: - app: apple - spec: - containers: - - name: example-app - image: hashicorp/http-echo - args: - - "-text=apple" - --- - kind: Service - apiVersion: v1 - metadata: - name: example-service - spec: - selector: - app: apple - ports: - - port: 5678 # Default port for image - --- - kind: Pod - apiVersion: v1 - metadata: - name: example-app1 - labels: - app: apple1 - spec: - containers: - - name: example-app1 - image: hashicorp/http-echo - args: - - "-text=apple" - --- - kind: Service - apiVersion: v1 - metadata: - name: example-service1 - spec: - selector: - app: apple1 - ports: - - port: 5679 # Default port for image - --- - kind: Pod - apiVersion: v1 - metadata: - name: example-app2 - labels: - app: apple2 - spec: - containers: - - name: example-app2 - image: hashicorp/http-echo - args: - - "-text=apple" - --- - kind: Service - apiVersion: v1 - metadata: - name: example-service2 - spec: - selector: - app: apple2 - ports: - - port: 5680 # Default port for image - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: example-ingress - annotations: - ingress.kubernetes.io/rewrite-target: / - ingress.kubernetes.io/scheme: internet-facing - external-dns.alpha.kubernetes.io/hostname: apple.example.com. - external-dns.alpha.kubernetes.io/target: apple.cname.com. - spec: - rules: - - http: - paths: - - path: /apple - backend: - service: - name: example-service - port: - number: 5678 - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: example-ingress1 - annotations: - ingress.kubernetes.io/rewrite-target: / - ingress.kubernetes.io/scheme: internet-facing - external-dns.alpha.kubernetes.io/hostname: apple-banana.example.com. - external-dns.alpha.kubernetes.io/target: 10.10.10.3 - spec: - rules: - - http: - paths: - - path: /apple - backend: - service: - name: example-service1 - port: - number: 5679 - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: example-ingress2 - annotations: - ingress.kubernetes.io/rewrite-target: / - ingress.kubernetes.io/scheme: internet-facing - external-dns.alpha.kubernetes.io/hostname: banana.example.com. - external-dns.alpha.kubernetes.io/target: 10.10.10.3,10.10.10.20 - spec: - rules: - - http: - paths: - - path: /apple - backend: - service: - name: example-service2 - port: - number: 5680 - ``` - - - _Config File Example – Kubernetes cluster service from different cloud vendors_ - - ```yaml - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: nginx - spec: - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - image: nginx - name: nginx - ports: - - containerPort: 80 - --- - apiVersion: v1 - kind: Service - metadata: - name: nginx - annotations: - external-dns.alpha.kubernetes.io/hostname: my-app.example.com. - spec: - selector: - app: nginx - type: LoadBalancer - ports: - - protocol: TCP - port: 80 - targetPort: 80 - --- - kind: Pod - apiVersion: v1 - metadata: - name: example-app - labels: - app: apple - spec: - containers: - - name: example-app - image: hashicorp/http-echo - args: - - "-text=apple" - --- - kind: Service - apiVersion: v1 - metadata: - name: example-service - spec: - selector: - app: apple - ports: - - port: 5678 # Default port for image - --- - kind: Pod - apiVersion: v1 - metadata: - name: example-app1 - labels: - app: apple1 - spec: - containers: - - name: example-app1 - image: hashicorp/http-echo - args: - - "-text=apple" - --- - apiVersion: extensions/v1beta1 - kind: Service - apiVersion: v1 - metadata: - name: example-service1 - spec: - selector: - app: apple1 - ports: - - port: 5679 # Default port for image - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: example-ingress - annotations: - ingress.kubernetes.io/rewrite-target: / - ingress.kubernetes.io/scheme: internet-facing - external-dns.alpha.kubernetes.io/hostname: apple.example.com. - external-dns.alpha.kubernetes.io/target: 10.10.10.3,10.10.10.25 - spec: - rules: - - http: - paths: - - path: /apple - backend: - service: - name: example-service - port: - number: 5678 - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: example-ingress1 - annotations: - ingress.kubernetes.io/rewrite-target: / - ingress.kubernetes.io/scheme: internet-facing - external-dns.alpha.kubernetes.io/hostname: apple-banana.example.com. - external-dns.alpha.kubernetes.io/target: 10.10.10.3 - spec: - rules: - - http: - paths: - - path: /apple - backend: - service: - name: example-service1 - port: - number: 5679 - ``` - -- Then, create the deployment and service: - -```console -kubectl create -f apple-banana-echo.yaml -kubectl create -f external-dns.yaml -``` - -- Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. -- Please verify on the [UltraDNS UI](https://portal.ultradns.com/login), that the records have been created under the zone "example.com". -- Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone "example.com": - -```console -kubectl delete -f apple-banana-echo.yaml -kubectl delete -f external-dns.yaml``` diff --git a/go.mod b/go.mod index e2d2a35fa..ffb29504f 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,6 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 github.com/transip/gotransip/v6 v6.26.0 - github.com/ultradns/ultradns-sdk-go v1.3.7 go.etcd.io/etcd/client/v3 v3.6.0 go.uber.org/ratelimit v0.3.1 golang.org/x/net v0.40.0 @@ -102,7 +101,6 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deepmap/oapi-codegen v1.9.1 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect - github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -143,7 +141,6 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -164,7 +161,6 @@ require ( github.com/sosodev/duration v1.3.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/terra-farm/udnssdk v1.3.5 // indirect github.com/vektah/gqlparser/v2 v2.5.25 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index abeae1fc8..798796933 100644 --- a/go.sum +++ b/go.sum @@ -298,8 +298,6 @@ github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwo github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 h1:jmwW6QWvUO2OPe22YfgFvBaaZlSr8Rlrac5lZvG6IdM= @@ -740,8 +738,6 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -994,8 +990,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/terra-farm/udnssdk v1.3.5 h1:MNR3adfuuEK/l04+jzo8WW/0fnorY+nW515qb3vEr6I= -github.com/terra-farm/udnssdk v1.3.5/go.mod h1:8RnM56yZTR7mYyUIvrDgXzdRaEyFIzqdEi7+um26Sv8= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -1012,8 +1006,6 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/ultradns/ultradns-sdk-go v1.3.7 h1:P4CaM+npeXIybbLL27ezR316NnyILI1Y8IvfZtNE+Co= -github.com/ultradns/ultradns-sdk-go v1.3.7/go.mod h1:43vmy6GEvRuVMpGEWfJ/JoEM6RIqUQI1/tb8JqZR1zI= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index b04c5aa41..48a2ba3e2 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -487,7 +487,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew) // Flags related to providers - providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "transip", "ultradns", "webhook"} + providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "transip", "webhook"} app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...) app.Flag("provider-cache-time", "The time to cache the DNS provider record list requests.").Default(defaultConfig.ProviderCacheTime.String()).DurationVar(&cfg.ProviderCacheTime) app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) diff --git a/provider/ultradns/ultradns.go b/provider/ultradns/ultradns.go deleted file mode 100644 index 1b2f2297a..000000000 --- a/provider/ultradns/ultradns.go +++ /dev/null @@ -1,498 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package ultradns - -import ( - "context" - "encoding/base64" - "fmt" - "os" - "strconv" - "strings" - "time" - - log "github.com/sirupsen/logrus" - udnssdk "github.com/ultradns/ultradns-sdk-go" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" -) - -const ( - ultradnsCreate = "CREATE" - ultradnsDelete = "DELETE" - ultradnsUpdate = "UPDATE" - sbPoolPriority = 1 - sbPoolOrder = "ROUND_ROBIN" - rdPoolOrder = "ROUND_ROBIN" -) - -var ( - sbPoolActOnProbes = true - ultradnsPoolType = "rdpool" - accountName string - sbPoolRunProbes = true - // Setting custom headers for ultradns api calls - customHeader = []udnssdk.CustomHeader{ - { - Key: "UltraClient", - Value: "kube-client", - }, - } -) - -// UltraDNSProvider struct -type UltraDNSProvider struct { - provider.BaseProvider - client udnssdk.Client - domainFilter endpoint.DomainFilter - dryRun bool -} - -// UltraDNSChanges struct -type UltraDNSChanges struct { - Action string - ResourceRecordSetUltraDNS udnssdk.RRSet -} - -// NewUltraDNSProvider initializes a new UltraDNS DNS based provider -func NewUltraDNSProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*UltraDNSProvider, error) { - username, ok := os.LookupEnv("ULTRADNS_USERNAME") - udnssdk.SetCustomHeader = customHeader - if !ok { - return nil, fmt.Errorf("no username found") - } - - base64password, ok := os.LookupEnv("ULTRADNS_PASSWORD") - if !ok { - return nil, fmt.Errorf("no password found") - } - - // Base64 Standard Decoding - password, err := base64.StdEncoding.DecodeString(base64password) - if err != nil { - fmt.Printf("Error decoding string: %s ", err.Error()) - return nil, err - } - - baseURL, ok := os.LookupEnv("ULTRADNS_BASEURL") - if !ok { - return nil, fmt.Errorf("no baseurl found") - } - accountName, ok = os.LookupEnv("ULTRADNS_ACCOUNTNAME") - if !ok { - accountName = "" - } - - probeValue, ok := os.LookupEnv("ULTRADNS_ENABLE_PROBING") - if ok { - if (probeValue != "true") && (probeValue != "false") { - return nil, fmt.Errorf("please set proper probe value, the values can be either true or false") - } - sbPoolRunProbes, _ = strconv.ParseBool(probeValue) - } - - actOnProbeValue, ok := os.LookupEnv("ULTRADNS_ENABLE_ACTONPROBE") - if ok { - if (actOnProbeValue != "true") && (actOnProbeValue != "false") { - return nil, fmt.Errorf("please set proper act on probe value, the values can be either true or false") - } - sbPoolActOnProbes, _ = strconv.ParseBool(actOnProbeValue) - } - - poolValue, ok := os.LookupEnv("ULTRADNS_POOL_TYPE") - if ok { - if (poolValue != "sbpool") && (poolValue != "rdpool") { - return nil, fmt.Errorf(" please set proper ULTRADNS_POOL_TYPE, supported types are sbpool or rdpool") - } - ultradnsPoolType = poolValue - } - - client, err := udnssdk.NewClient(username, string(password), baseURL) - if err != nil { - return nil, fmt.Errorf("connection cannot be established") - } - - return &UltraDNSProvider{ - client: *client, - domainFilter: domainFilter, - dryRun: dryRun, - }, nil -} - -// Zones returns list of hosted zones -func (p *UltraDNSProvider) Zones(ctx context.Context) ([]udnssdk.Zone, error) { - zoneKey := &udnssdk.ZoneKey{} - var err error - - if p.domainFilter.IsConfigured() { - zonesAppender := []udnssdk.Zone{} - for _, zone := range p.domainFilter.Filters { - zoneKey.Zone = zone - zoneKey.AccountName = accountName - zones, err := p.fetchZones(ctx, zoneKey) - if err != nil { - return nil, err - } - - zonesAppender = append(zonesAppender, zones...) - } - return zonesAppender, nil - } - zoneKey.AccountName = accountName - zones, err := p.fetchZones(ctx, zoneKey) - if err != nil { - return nil, err - } - - return zones, nil -} - -func (p *UltraDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - var endpoints []*endpoint.Endpoint - - zones, err := p.Zones(ctx) - if err != nil { - return nil, err - } - - for _, zone := range zones { - log.Infof("zones : %v", zone) - var rrsetType string - var ownerName string - rrsetKey := udnssdk.RRSetKey{ - Zone: zone.Properties.Name, - Type: rrsetType, - Name: ownerName, - } - - if zone.Properties.ResourceRecordCount != 0 { - records, err := p.fetchRecords(ctx, rrsetKey) - if err != nil { - return nil, err - } - - for _, r := range records { - recordTypeArray := strings.Fields(r.RRType) - if provider.SupportedRecordType(recordTypeArray[0]) { - log.Infof("owner name %s", r.OwnerName) - name := r.OwnerName - - // root name is identified by the empty string and should be - // translated to zone name for the endpoint entry. - if r.OwnerName == "" { - name = zone.Properties.Name - } - - endPointTTL := endpoint.NewEndpointWithTTL(name, recordTypeArray[0], endpoint.TTL(r.TTL), r.RData...) - endpoints = append(endpoints, endPointTTL) - } - } - } - } - log.Infof("endpoints %v", endpoints) - return endpoints, nil -} - -func (p *UltraDNSProvider) fetchRecords(ctx context.Context, k udnssdk.RRSetKey) ([]udnssdk.RRSet, error) { - // Logic to paginate through all available results - maxerrs := 5 - waittime := 5 * time.Second - - var rrsets []udnssdk.RRSet - errcnt := 0 - offset := 0 - limit := 1000 - - for { - reqRrsets, ri, res, err := p.client.RRSets.SelectWithOffsetWithLimit(k, offset, limit) - if err != nil { - if res != nil && res.StatusCode >= 500 { - errcnt = errcnt + 1 - if errcnt < maxerrs { - time.Sleep(waittime) - continue - } - } - return rrsets, err - } - rrsets = append(rrsets, reqRrsets...) - - if ri.ReturnedCount+ri.Offset >= ri.TotalCount { - return rrsets, nil - } - offset = ri.ReturnedCount + ri.Offset - continue - } -} - -func (p *UltraDNSProvider) fetchZones(ctx context.Context, zoneKey *udnssdk.ZoneKey) ([]udnssdk.Zone, error) { - // Logic to paginate through all available results - offset := 0 - limit := 1000 - maxerrs := 5 - waittime := 5 * time.Second - - zones := []udnssdk.Zone{} - - errcnt := 0 - - for { - reqZones, ri, res, err := p.client.Zone.SelectWithOffsetWithLimit(zoneKey, offset, limit) - if err != nil { - if res != nil && res.StatusCode >= 500 { - errcnt = errcnt + 1 - if errcnt < maxerrs { - time.Sleep(waittime) - continue - } - } - return zones, err - } - - zones = append(zones, reqZones...) - if ri.ReturnedCount+ri.Offset >= ri.TotalCount { - return zones, nil - } - offset = ri.ReturnedCount + ri.Offset - continue - } -} - -func (p *UltraDNSProvider) submitChanges(ctx context.Context, changes []*UltraDNSChanges) error { - cnameownerName := "cname" - txtownerName := "txt" - if len(changes) == 0 { - log.Infof("All records are already up to date") - return nil - } - - zones, err := p.Zones(ctx) - if err != nil { - return err - } - zoneChanges := seperateChangeByZone(zones, changes) - - for zoneName, changes := range zoneChanges { - for _, change := range changes { - switch change.ResourceRecordSetUltraDNS.RRType { - case "CNAME": - cnameownerName = change.ResourceRecordSetUltraDNS.OwnerName - case "TXT": - txtownerName = change.ResourceRecordSetUltraDNS.OwnerName - } - - if cnameownerName == txtownerName { - rrsetKey := udnssdk.RRSetKey{ - Zone: zoneName, - Type: endpoint.RecordTypeCNAME, - Name: change.ResourceRecordSetUltraDNS.OwnerName, - } - err := p.getSpecificRecord(ctx, rrsetKey) - if err != nil { - return err - } - if !p.dryRun { - _, err = p.client.RRSets.Delete(rrsetKey) - if err != nil { - return err - } - } - return fmt.Errorf("the 'cname' and 'txt' record name cannot be same please recreate external-dns with - --txt-prefix=") - } - rrsetKey := udnssdk.RRSetKey{ - Zone: zoneName, - Type: change.ResourceRecordSetUltraDNS.RRType, - Name: change.ResourceRecordSetUltraDNS.OwnerName, - } - record := udnssdk.RRSet{} - if (change.ResourceRecordSetUltraDNS.RRType == "A" || change.ResourceRecordSetUltraDNS.RRType == "AAAA") && (len(change.ResourceRecordSetUltraDNS.RData) >= 2) { - if ultradnsPoolType == "sbpool" && change.ResourceRecordSetUltraDNS.RRType == "A" { - sbPoolObject, _ := p.newSBPoolObjectCreation(ctx, change) - record = udnssdk.RRSet{ - RRType: change.ResourceRecordSetUltraDNS.RRType, - OwnerName: change.ResourceRecordSetUltraDNS.OwnerName, - RData: change.ResourceRecordSetUltraDNS.RData, - TTL: change.ResourceRecordSetUltraDNS.TTL, - Profile: sbPoolObject.RawProfile(), - } - } else if ultradnsPoolType == "rdpool" { - rdPoolObject, _ := p.newRDPoolObjectCreation(ctx, change) - record = udnssdk.RRSet{ - RRType: change.ResourceRecordSetUltraDNS.RRType, - OwnerName: change.ResourceRecordSetUltraDNS.OwnerName, - RData: change.ResourceRecordSetUltraDNS.RData, - TTL: change.ResourceRecordSetUltraDNS.TTL, - Profile: rdPoolObject.RawProfile(), - } - } else { - return fmt.Errorf("we do not support Multiple target 'aaaa' records in sb pool please contact to neustar for further details") - } - } else { - record = udnssdk.RRSet{ - RRType: change.ResourceRecordSetUltraDNS.RRType, - OwnerName: change.ResourceRecordSetUltraDNS.OwnerName, - RData: change.ResourceRecordSetUltraDNS.RData, - TTL: change.ResourceRecordSetUltraDNS.TTL, - } - } - - log.WithFields(log.Fields{ - "record": record.OwnerName, - "type": record.RRType, - "ttl": record.TTL, - "action": change.Action, - "zone": zoneName, - "profile": record.Profile, - }).Info("Changing record.") - - switch change.Action { - case ultradnsCreate: - if !p.dryRun { - res, err := p.client.RRSets.Create(rrsetKey, record) - _ = res - if err != nil { - return err - } - } - - case ultradnsDelete: - err := p.getSpecificRecord(ctx, rrsetKey) - if err != nil { - return err - } - - if !p.dryRun { - _, err = p.client.RRSets.Delete(rrsetKey) - if err != nil { - return err - } - } - case ultradnsUpdate: - err := p.getSpecificRecord(ctx, rrsetKey) - if err != nil { - return err - } - - if !p.dryRun { - _, err = p.client.RRSets.Update(rrsetKey, record) - if err != nil { - return err - } - } - } - } - } - - return nil -} - -func (p *UltraDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - combinedChanges := make([]*UltraDNSChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) - log.Infof("value of changes %v,%v,%v", changes.Create, changes.UpdateNew, changes.Delete) - combinedChanges = append(combinedChanges, newUltraDNSChanges(ultradnsCreate, changes.Create)...) - combinedChanges = append(combinedChanges, newUltraDNSChanges(ultradnsUpdate, changes.UpdateNew)...) - combinedChanges = append(combinedChanges, newUltraDNSChanges(ultradnsDelete, changes.Delete)...) - - return p.submitChanges(ctx, combinedChanges) -} - -func newUltraDNSChanges(action string, endpoints []*endpoint.Endpoint) []*UltraDNSChanges { - changes := make([]*UltraDNSChanges, 0, len(endpoints)) - var ttl int - for _, e := range endpoints { - if e.RecordTTL.IsConfigured() { - ttl = int(e.RecordTTL) - } - - // Adding suffix dot to the record name - recordName := fmt.Sprintf("%s.", e.DNSName) - change := &UltraDNSChanges{ - Action: action, - ResourceRecordSetUltraDNS: udnssdk.RRSet{ - RRType: e.RecordType, - OwnerName: recordName, - RData: e.Targets, - TTL: ttl, - }, - } - changes = append(changes, change) - } - return changes -} - -func seperateChangeByZone(zones []udnssdk.Zone, changes []*UltraDNSChanges) map[string][]*UltraDNSChanges { - change := make(map[string][]*UltraDNSChanges) - zoneNameID := provider.ZoneIDName{} - for _, z := range zones { - zoneNameID.Add(z.Properties.Name, z.Properties.Name) - change[z.Properties.Name] = []*UltraDNSChanges{} - } - - for _, c := range changes { - zone, _ := zoneNameID.FindZone(c.ResourceRecordSetUltraDNS.OwnerName) - if zone == "" { - log.Infof("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSetUltraDNS.OwnerName) - continue - } - change[zone] = append(change[zone], c) - } - return change -} - -func (p *UltraDNSProvider) getSpecificRecord(ctx context.Context, rrsetKey udnssdk.RRSetKey) (err error) { - _, err = p.client.RRSets.Select(rrsetKey) - if err != nil { - return fmt.Errorf("no record was found for %v", rrsetKey) - } - - return nil -} - -// Creation of SBPoolObject -func (p *UltraDNSProvider) newSBPoolObjectCreation(ctx context.Context, change *UltraDNSChanges) (sbPool udnssdk.SBPoolProfile, err error) { - sbpoolRDataList := []udnssdk.SBRDataInfo{} - for range change.ResourceRecordSetUltraDNS.RData { - rrdataInfo := udnssdk.SBRDataInfo{ - RunProbes: sbPoolRunProbes, - Priority: sbPoolPriority, - State: "NORMAL", - Threshold: 1, - Weight: nil, - } - sbpoolRDataList = append(sbpoolRDataList, rrdataInfo) - } - sbPoolObject := udnssdk.SBPoolProfile{ - Context: udnssdk.SBPoolSchema, - Order: sbPoolOrder, - Description: change.ResourceRecordSetUltraDNS.OwnerName, - MaxActive: len(change.ResourceRecordSetUltraDNS.RData), - MaxServed: len(change.ResourceRecordSetUltraDNS.RData), - RDataInfo: sbpoolRDataList, - RunProbes: sbPoolRunProbes, - ActOnProbes: sbPoolActOnProbes, - } - return sbPoolObject, nil -} - -// Creation of RDPoolObject -func (p *UltraDNSProvider) newRDPoolObjectCreation(ctx context.Context, change *UltraDNSChanges) (rdPool udnssdk.RDPoolProfile, err error) { - rdPoolObject := udnssdk.RDPoolProfile{ - Context: udnssdk.RDPoolSchema, - Order: rdPoolOrder, - Description: change.ResourceRecordSetUltraDNS.OwnerName, - } - return rdPoolObject, nil -} diff --git a/provider/ultradns/ultradns_test.go b/provider/ultradns/ultradns_test.go deleted file mode 100644 index d0b9bddcd..000000000 --- a/provider/ultradns/ultradns_test.go +++ /dev/null @@ -1,756 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package ultradns - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "os" - "reflect" - _ "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - udnssdk "github.com/ultradns/ultradns-sdk-go" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" -) - -type mockUltraDNSZone struct { - client *udnssdk.Client -} - -func (m *mockUltraDNSZone) SelectWithOffsetWithLimit(k *udnssdk.ZoneKey, offset int, limit int) (zones []udnssdk.Zone, ResultInfo udnssdk.ResultInfo, resp *http.Response, err error) { - zones = []udnssdk.Zone{} - zone := udnssdk.Zone{} - zoneJson := ` - { - "properties": { - "name":"test-ultradns-provider.com.", - "accountName":"teamrest", - "type":"PRIMARY", - "dnssecStatus":"UNSIGNED", - "status":"ACTIVE", - "owner":"teamrest", - "resourceRecordCount":7, - "lastModifiedDateTime":"" - } - }` - if err := json.Unmarshal([]byte(zoneJson), &zone); err != nil { - log.Fatal(err) - } - - zones = append(zones, zone) - return zones, udnssdk.ResultInfo{}, nil, nil -} - -type mockUltraDNSRecord struct { - client *udnssdk.Client -} - -func (m *mockUltraDNSRecord) Create(_ udnssdk.RRSetKey, _ udnssdk.RRSet) (*http.Response, error) { - return &http.Response{}, nil -} - -func (m *mockUltraDNSRecord) Select(_ udnssdk.RRSetKey) ([]udnssdk.RRSet, error) { - return []udnssdk.RRSet{{ - OwnerName: "test-ultradns-provider.com.", - RRType: endpoint.RecordTypeA, - RData: []string{"1.1.1.1"}, - TTL: 86400, - }}, nil -} - -func (m *mockUltraDNSRecord) SelectWithOffset(k udnssdk.RRSetKey, offset int) ([]udnssdk.RRSet, udnssdk.ResultInfo, *http.Response, error) { - return nil, udnssdk.ResultInfo{}, nil, nil -} - -func (m *mockUltraDNSRecord) Update(udnssdk.RRSetKey, udnssdk.RRSet) (*http.Response, error) { - return &http.Response{}, nil -} - -func (m *mockUltraDNSRecord) Delete(k udnssdk.RRSetKey) (*http.Response, error) { - return &http.Response{}, nil -} - -func (m *mockUltraDNSRecord) SelectWithOffsetWithLimit(k udnssdk.RRSetKey, offset int, limit int) (rrsets []udnssdk.RRSet, ResultInfo udnssdk.ResultInfo, resp *http.Response, err error) { - return []udnssdk.RRSet{{ - OwnerName: "test-ultradns-provider.com.", - RRType: endpoint.RecordTypeA, - RData: []string{"1.1.1.1"}, - TTL: 86400, - }}, udnssdk.ResultInfo{}, nil, nil -} - -// NewUltraDNSProvider Test scenario -func TestNewUltraDNSProvider(t *testing.T) { - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _, err := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.NoError(t, err) - - _ = os.Unsetenv("ULTRADNS_PASSWORD") - _ = os.Unsetenv("ULTRADNS_USERNAME") - _ = os.Unsetenv("ULTRADNS_BASEURL") - _ = os.Unsetenv("ULTRADNS_ACCOUNTNAME") - _, err = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.Errorf(t, err, "Expected to fail %s", "formatted") -} - -// zones function test scenario -func TestUltraDNSProvider_Zones(t *testing.T) { - mocked := mockUltraDNSZone{} - provider := &UltraDNSProvider{ - client: udnssdk.Client{ - Zone: &mocked, - }, - } - - zoneKey := &udnssdk.ZoneKey{ - Zone: "", - AccountName: "teamrest", - } - - expected, _, _, err := provider.client.Zone.SelectWithOffsetWithLimit(zoneKey, 0, 1000) - require.NoError(t, err) - zones, err := provider.Zones(context.Background()) - require.NoError(t, err) - assert.True(t, reflect.DeepEqual(expected, zones)) -} - -// Records function test case -func TestUltraDNSProvider_Records(t *testing.T) { - mocked := mockUltraDNSRecord{} - mockedDomain := mockUltraDNSZone{} - - provider := &UltraDNSProvider{ - client: udnssdk.Client{ - RRSets: &mocked, - Zone: &mockedDomain, - }, - } - rrsetKey := udnssdk.RRSetKey{} - expected, _, _, err := provider.client.RRSets.SelectWithOffsetWithLimit(rrsetKey, 0, 1000) - records, err := provider.Records(context.Background()) - require.NoError(t, err) - for _, v := range records { - assert.Equal(t, fmt.Sprintf("%s.", v.DNSName), expected[0].OwnerName) - assert.Equal(t, v.RecordType, expected[0].RRType) - assert.Equal(t, int(v.RecordTTL), expected[0].TTL) - } -} - -// ApplyChanges function testcase -func TestUltraDNSProvider_ApplyChanges(t *testing.T) { - changes := &plan.Changes{} - mocked := mockUltraDNSRecord{nil} - mockedDomain := mockUltraDNSZone{nil} - - provider := &UltraDNSProvider{ - client: udnssdk.Client{ - RRSets: &mocked, - Zone: &mockedDomain, - }, - } - - changes.Create = []*endpoint.Endpoint{ - {DNSName: "test-ultradns-provider.com", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A"}, - {DNSName: "ttl.test-ultradns-provider.com", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A", RecordTTL: 100}, - } - changes.Create = []*endpoint.Endpoint{{DNSName: "test-ultradns-provider.com", Targets: endpoint.Targets{"1.1.1.2"}, RecordType: "A"}} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test-ultradns-provider.com", Targets: endpoint.Targets{"1.1.2.2"}, RecordType: "A", RecordTTL: 100}} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test-ultradns-provider.com", Targets: endpoint.Targets{"1.1.2.2", "1.1.2.3", "1.1.2.4"}, RecordType: "A", RecordTTL: 100}} - changes.Delete = []*endpoint.Endpoint{{DNSName: "test-ultradns-provider.com", Targets: endpoint.Targets{"1.1.2.2", "1.1.2.3", "1.1.2.4"}, RecordType: "A", RecordTTL: 100}} - changes.Delete = []*endpoint.Endpoint{{DNSName: "ttl.test-ultradns-provider.com", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A", RecordTTL: 100}} - err := provider.ApplyChanges(context.Background(), changes) - assert.NoErrorf(t, err, "Should not fail %s", "formatted") -} - -// Testing function getSpecificRecord -func TestUltraDNSProvider_getSpecificRecord(t *testing.T) { - mocked := mockUltraDNSRecord{nil} - mockedDomain := mockUltraDNSZone{nil} - - provider := &UltraDNSProvider{ - client: udnssdk.Client{ - RRSets: &mocked, - Zone: &mockedDomain, - }, - } - - recordSetKey := udnssdk.RRSetKey{ - Zone: "test-ultradns-provider.com.", - Type: "A", - Name: "teamrest", - } - err := provider.getSpecificRecord(context.Background(), recordSetKey) - assert.NoError(t, err) -} - -// Fail case scenario testing where CNAME and TXT Record name are same -func TestUltraDNSProvider_ApplyChangesCNAME(t *testing.T) { - changes := &plan.Changes{} - mocked := mockUltraDNSRecord{nil} - mockedDomain := mockUltraDNSZone{nil} - - provider := &UltraDNSProvider{ - client: udnssdk.Client{ - RRSets: &mocked, - Zone: &mockedDomain, - }, - } - - changes.Create = []*endpoint.Endpoint{ - {DNSName: "test-ultradns-provider.com", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "CNAME"}, - {DNSName: "test-ultradns-provider.com", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "TXT"}, - } - - err := provider.ApplyChanges(context.Background(), changes) - assert.Error(t, err) -} - -// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be available "kubernetes-ultradns-provider-test.com" -func TestUltraDNSProvider_ApplyChanges_Integration(t *testing.T) { - _, ok := os.LookupEnv("ULTRADNS_INTEGRATION") - if !ok { - log.Printf("Skipping test") - } else { - - providerUltradns, err := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false) - changes := &plan.Changes{} - changes.Create = []*endpoint.Endpoint{ - {DNSName: "kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A"}, - {DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, RecordType: "AAAA", RecordTTL: 100}, - } - - err = providerUltradns.ApplyChanges(context.Background(), changes) - require.NoError(t, err) - - rrsetKey := udnssdk.RRSetKey{ - Zone: "kubernetes-ultradns-provider-test.com.", - Name: "kubernetes-ultradns-provider-test.com.", - Type: "A", - } - - rrsets, _ := providerUltradns.client.RRSets.Select(rrsetKey) - assert.Equal(t, "1.1.1.1", rrsets[0].RData[0]) - - rrsetKey = udnssdk.RRSetKey{ - Zone: "kubernetes-ultradns-provider-test.com.", - Name: "ttl.kubernetes-ultradns-provider-test.com.", - Type: "AAAA", - } - - rrsets, _ = providerUltradns.client.RRSets.Select(rrsetKey) - assert.Equal(t, "2001:db8:85a3:0:0:8a2e:370:7334", rrsets[0].RData[0]) - - changes = &plan.Changes{} - changes.UpdateNew = []*endpoint.Endpoint{ - {DNSName: "kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.2.2"}, RecordType: "A", RecordTTL: 100}, - {DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"2001:0db8:85a3:0000:0000:8a2e:0370:7335"}, RecordType: "AAAA", RecordTTL: 100}, - } - err = providerUltradns.ApplyChanges(context.Background(), changes) - require.NoError(t, err) - - rrsetKey = udnssdk.RRSetKey{ - Zone: "kubernetes-ultradns-provider-test.com.", - Name: "kubernetes-ultradns-provider-test.com.", - Type: "A", - } - - rrsets, _ = providerUltradns.client.RRSets.Select(rrsetKey) - assert.Equal(t, "1.1.2.2", rrsets[0].RData[0]) - - rrsetKey = udnssdk.RRSetKey{ - Zone: "kubernetes-ultradns-provider-test.com.", - Name: "ttl.kubernetes-ultradns-provider-test.com.", - Type: "AAAA", - } - - rrsets, _ = providerUltradns.client.RRSets.Select(rrsetKey) - assert.Equal(t, "2001:db8:85a3:0:0:8a2e:370:7335", rrsets[0].RData[0]) - - changes = &plan.Changes{} - changes.Delete = []*endpoint.Endpoint{ - {DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"2001:0db8:85a3:0000:0000:8a2e:0370:7335"}, RecordType: "AAAA", RecordTTL: 100}, - {DNSName: "kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.2.2"}, RecordType: "A", RecordTTL: 100}, - } - - err = providerUltradns.ApplyChanges(context.Background(), changes) - require.NoError(t, err) - - resp, _ := providerUltradns.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/AAAA/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "404 Not Found", resp.Status) - - resp, _ = providerUltradns.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "404 Not Found", resp.Status) - - } -} - -// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be available "kubernetes-ultradns-provider-test.com" for multiple target -func TestUltraDNSProvider_ApplyChanges_MultipleTarget_integeration(t *testing.T) { - _, ok := os.LookupEnv("ULTRADNS_INTEGRATION") - if !ok { - log.Printf("Skipping test") - } else { - - provider, err := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false) - changes := &plan.Changes{} - changes.Create = []*endpoint.Endpoint{ - {DNSName: "kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.1.1", "1.1.2.2"}, RecordType: "A"}, - } - - err = provider.ApplyChanges(context.Background(), changes) - assert.NoError(t, err) - - rrsetKey := udnssdk.RRSetKey{ - Zone: "kubernetes-ultradns-provider-test.com.", - Name: "kubernetes-ultradns-provider-test.com.", - Type: "A", - } - - rrsets, _ := provider.client.RRSets.Select(rrsetKey) - assert.Equal(t, []string{"1.1.1.1", "1.1.2.2"}, rrsets[0].RData) - - changes = &plan.Changes{} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.2.2", "192.168.0.24", "1.2.3.4"}, RecordType: "A", RecordTTL: 100}} - - err = provider.ApplyChanges(context.Background(), changes) - require.NoError(t, err) - - rrsetKey = udnssdk.RRSetKey{ - Zone: "kubernetes-ultradns-provider-test.com.", - Name: "kubernetes-ultradns-provider-test.com.", - Type: "A", - } - - rrsets, _ = provider.client.RRSets.Select(rrsetKey) - assert.Equal(t, []string{"1.1.2.2", "192.168.0.24", "1.2.3.4"}, rrsets[0].RData) - - changes = &plan.Changes{} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.2.2"}, RecordType: "A", RecordTTL: 100}} - - err = provider.ApplyChanges(context.Background(), changes) - - assert.NoError(t, err) - - rrsetKey = udnssdk.RRSetKey{ - Zone: "kubernetes-ultradns-provider-test.com.", - Name: "kubernetes-ultradns-provider-test.com.", - Type: "A", - } - - rrsets, _ = provider.client.RRSets.Select(rrsetKey) - assert.Equal(t, []string{"1.1.2.2"}, rrsets[0].RData) - - changes = &plan.Changes{} - changes.Delete = []*endpoint.Endpoint{{DNSName: "kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.2.2", "192.168.0.24"}, RecordType: "A"}} - - err = provider.ApplyChanges(context.Background(), changes) - - assert.NoError(t, err) - - resp, _ := provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "404 Not Found", resp.Status) - - } -} - -// Test case to check sbpool creation -func TestUltraDNSProvider_newSBPoolObjectCreation(t *testing.T) { - mocked := mockUltraDNSRecord{nil} - mockedDomain := mockUltraDNSZone{nil} - - provider := &UltraDNSProvider{ - client: udnssdk.Client{ - RRSets: &mocked, - Zone: &mockedDomain, - }, - } - sbpoolRDataList := []udnssdk.SBRDataInfo{} - changes := &plan.Changes{} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "kubernetes-ultradns-provider-test.com.", Targets: endpoint.Targets{"1.1.2.2", "192.168.0.24"}, RecordType: "A", RecordTTL: 100}} - changesList := &UltraDNSChanges{ - Action: "UPDATE", - ResourceRecordSetUltraDNS: udnssdk.RRSet{ - RRType: "A", - OwnerName: "kubernetes-ultradns-provider-test.com.", - RData: []string{"1.1.2.2", "192.168.0.24"}, - TTL: 100, - }, - } - - for range changesList.ResourceRecordSetUltraDNS.RData { - - rrdataInfo := udnssdk.SBRDataInfo{ - RunProbes: true, - Priority: 1, - State: "NORMAL", - Threshold: 1, - Weight: nil, - } - sbpoolRDataList = append(sbpoolRDataList, rrdataInfo) - } - sbPoolObject := udnssdk.SBPoolProfile{ - Context: udnssdk.SBPoolSchema, - Order: "ROUND_ROBIN", - Description: "kubernetes-ultradns-provider-test.com.", - MaxActive: 2, - MaxServed: 2, - RDataInfo: sbpoolRDataList, - RunProbes: true, - ActOnProbes: true, - } - - actualSBPoolObject, _ := provider.newSBPoolObjectCreation(context.Background(), changesList) - assert.Equal(t, sbPoolObject, actualSBPoolObject) -} - -// Testcase to check fail scenario for multiple AAAA targets -func TestUltraDNSProvider_MultipleTargetAAAA(t *testing.T) { - _, ok := os.LookupEnv("ULTRADNS_INTEGRATION") - if !ok { - log.Printf("Skipping test") - } else { - _ = os.Setenv("ULTRADNS_POOL_TYPE", "sbpool") - - provider, _ := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false) - changes := &plan.Changes{} - changes.Create = []*endpoint.Endpoint{ - {DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"2001:0db8:85a3:0000:0000:8a2e:0370:7334", "2001:0db8:85a3:0000:0000:8a2e:0370:7335"}, RecordType: "AAAA", RecordTTL: 100}, - } - err := provider.ApplyChanges(context.Background(), changes) - assert.Errorf(t, err, "We wanted it to fail since multiple AAAA targets are not allowed %s", "formatted") - - resp, _ := provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/AAAA/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "404 Not Found", resp.Status) - _ = os.Unsetenv("ULTRADNS_POOL_TYPE") - } -} - -// Testcase to check fail scenario for multiple AAAA targets -func TestUltraDNSProvider_MultipleTargetAAAARDPool(t *testing.T) { - _, ok := os.LookupEnv("ULTRADNS_INTEGRATION") - if !ok { - log.Printf("Skipping test") - } else { - _ = os.Setenv("ULTRADNS_POOL_TYPE", "rdpool") - provider, _ := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false) - changes := &plan.Changes{} - changes.Create = []*endpoint.Endpoint{ - {DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"2001:0db8:85a3:0000:0000:8a2e:0370:7334", "2001:0db8:85a3:0000:0000:8a2e:0370:7335"}, RecordType: "AAAA", RecordTTL: 100}, - } - err := provider.ApplyChanges(context.Background(), changes) - require.NoErrorf(t, err, " multiple AAAA targets are allowed when pool is RDPool %s", "formatted") - - resp, _ := provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/AAAA/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "200 OK", resp.Status) - - changes = &plan.Changes{} - changes.Delete = []*endpoint.Endpoint{{DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"2001:0db8:85a3:0000:0000:8a2e:0370:7334", "2001:0db8:85a3:0000:0000:8a2e:0370:7335"}, RecordType: "AAAA"}} - - err = provider.ApplyChanges(context.Background(), changes) - require.NoError(t, err) - - resp, _ = provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "404 Not Found", resp.Status) - } -} - -// Test case to check multiple CNAME targets. -func TestUltraDNSProvider_MultipleTargetCNAME(t *testing.T) { - _, ok := os.LookupEnv("ULTRADNS_INTEGRATION") - if !ok { - log.Printf("Skipping test") - } else { - provider, err := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false) - changes := &plan.Changes{} - - changes.Create = []*endpoint.Endpoint{ - {DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"nginx.loadbalancer.com.", "nginx1.loadbalancer.com."}, RecordType: "CNAME", RecordTTL: 100}, - } - err = provider.ApplyChanges(context.Background(), changes) - - assert.Errorf(t, err, "We wanted it to fail since multiple CNAME targets are not allowed %s", "formatted") - - resp, _ := provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/CNAME/kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "404 Not Found", resp.Status) - } -} - -// Testing creation of RD Pool -func TestUltraDNSProvider_newRDPoolObjectCreation(t *testing.T) { - mocked := mockUltraDNSRecord{nil} - mockedDomain := mockUltraDNSZone{nil} - - provider := &UltraDNSProvider{ - client: udnssdk.Client{ - RRSets: &mocked, - Zone: &mockedDomain, - }, - } - changes := &plan.Changes{} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "kubernetes-ultradns-provider-test.com.", Targets: endpoint.Targets{"1.1.2.2", "192.168.0.24"}, RecordType: "A", RecordTTL: 100}} - changesList := &UltraDNSChanges{ - Action: "UPDATE", - ResourceRecordSetUltraDNS: udnssdk.RRSet{ - RRType: "A", - OwnerName: "kubernetes-ultradns-provider-test.com.", - RData: []string{"1.1.2.2", "192.168.0.24"}, - TTL: 100, - }, - } - rdPoolObject := udnssdk.RDPoolProfile{ - Context: udnssdk.RDPoolSchema, - Order: "ROUND_ROBIN", - Description: "kubernetes-ultradns-provider-test.com.", - } - - actualRDPoolObject, _ := provider.newRDPoolObjectCreation(context.Background(), changesList) - assert.Equal(t, rdPoolObject, actualRDPoolObject) -} - -// Testing Failure scenarios over NewUltraDNS Provider -func TestNewUltraDNSProvider_FailCases(t *testing.T) { - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _ = os.Setenv("ULTRADNS_POOL_TYPE", "xyz") - _, err := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.Errorf(t, err, "Pool Type other than given type not working %s", "formatted") - - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _ = os.Setenv("ULTRADNS_ENABLE_PROBING", "adefg") - _, err = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.Errorf(t, err, "Probe value other than given values not working %s", "formatted") - - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _ = os.Setenv("ULTRADNS_ENABLE_ACTONPROBE", "adefg") - _, err = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.Errorf(t, err, "ActOnProbe value other than given values not working %s", "formatted") - - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Unsetenv("ULTRADNS_PASSWORD") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _, err = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.Errorf(t, err, "Expected to give error if password is not set %s", "formatted") - - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "") - _ = os.Unsetenv("ULTRADNS_BASEURL") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _, err = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.Errorf(t, err, "Expected to give error if baseurl is not set %s", "formatted") - - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "") - _ = os.Unsetenv("ULTRADNS_ACCOUNTNAME") - _ = os.Unsetenv("ULTRADNS_ENABLE_ACTONPROBE") - _ = os.Unsetenv("ULTRADNS_ENABLE_PROBING") - _ = os.Unsetenv("ULTRADNS_POOL_TYPE") - _, accounterr := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.NoError(t, accounterr) -} - -// Testing success scenarios for newly introduced environment variables -func TestNewUltraDNSProvider_NewEnvVariableSuccessCases(t *testing.T) { - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _ = os.Setenv("ULTRADNS_POOL_TYPE", "rdpool") - _, err := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.NoErrorf(t, err, "Pool Type not working in proper scenario %s", "formatted") - - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _ = os.Setenv("ULTRADNS_ENABLE_PROBING", "false") - _, err1 := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.NoErrorf(t, err1, "Probe given value is not working %s", "formatted") - - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _ = os.Setenv("ULTRADNS_ENABLE_ACTONPROBE", "true") - _, err2 := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.NoErrorf(t, err2, "ActOnProbe given value is not working %s", "formatted") -} - -// Base64 Bad string decoding scenario -func TestNewUltraDNSProvider_Base64DecodeFailcase(t *testing.T) { - _ = os.Setenv("ULTRADNS_USERNAME", "") - _ = os.Setenv("ULTRADNS_PASSWORD", "12345") - _ = os.Setenv("ULTRADNS_BASEURL", "") - _ = os.Setenv("ULTRADNS_ACCOUNTNAME", "") - _ = os.Setenv("ULTRADNS_ENABLE_ACTONPROBE", "true") - _, err := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"test-ultradns-provider.com"}), true) - assert.Errorf(t, err, "Base64 decode should fail in this case %s", "formatted") -} - -func TestUltraDNSProvider_PoolConversionCase(t *testing.T) { - _, ok := os.LookupEnv("ULTRADNS_INTEGRATION") - if !ok { - log.Printf("Skipping test") - } else { - // Creating SBPool Record - _ = os.Setenv("ULTRADNS_POOL_TYPE", "sbpool") - provider, _ := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false) - changes := &plan.Changes{} - changes.Create = []*endpoint.Endpoint{{DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.1.1", "1.2.3.4"}, RecordType: "A", RecordTTL: 100}} - err := provider.ApplyChanges(context.Background(), changes) - assert.NoErrorf(t, err, " multiple A record creation with SBPool %s", "formatted") - - resp, _ := provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "200 OK", resp.Status) - - // Converting to RD Pool - _ = os.Setenv("ULTRADNS_POOL_TYPE", "rdpool") - provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false) - changes = &plan.Changes{} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.1.1", "1.2.3.5"}, RecordType: "A"}} - err = provider.ApplyChanges(context.Background(), changes) - assert.NoError(t, err) - resp, _ = provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "200 OK", resp.Status) - - // Converting back to SB Pool - _ = os.Setenv("ULTRADNS_POOL_TYPE", "sbpool") - provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false) - changes = &plan.Changes{} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.1.1", "1.2.3.4"}, RecordType: "A"}} - err = provider.ApplyChanges(context.Background(), changes) - assert.NoError(t, err) - resp, _ = provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "200 OK", resp.Status) - - // Deleting Record - changes = &plan.Changes{} - changes.Delete = []*endpoint.Endpoint{{DNSName: "ttl.kubernetes-ultradns-provider-test.com", Targets: endpoint.Targets{"1.1.1.1", "1.2.3.4"}, RecordType: "A"}} - err = provider.ApplyChanges(context.Background(), changes) - assert.NoError(t, err) - resp, _ = provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{}) - assert.Equal(t, "404 Not Found", resp.Status) - } -} - -func TestUltraDNSProvider_DomainFilter(t *testing.T) { - _, ok := os.LookupEnv("ULTRADNS_INTEGRATION") - if !ok { - log.Printf("Skipping test") - } else { - provider, _ := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com", "kubernetes-ultradns-provider-test.com"}), true) - zones, err := provider.Zones(context.Background()) - assert.Equal(t, "kubernetes-ultradns-provider-test.com.", zones[0].Properties.Name) - assert.Equal(t, "kubernetes-ultradns-provider-test.com.", zones[1].Properties.Name) - assert.NoErrorf(t, err, " Multiple domain filter failed %s", "formatted") - - provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{}), true) - zones, err = provider.Zones(context.Background()) - assert.NoErrorf(t, err, " Multiple domain filter failed %s", "formatted") - - } -} - -func TestUltraDNSProvider_DomainFiltersZonesFailCase(t *testing.T) { - _, ok := os.LookupEnv("ULTRADNS_INTEGRATION") - if !ok { - log.Printf("Skipping test") - } else { - provider, _ := NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com", "kubernetes-uldsvdsvadvvdsvadvstradns-provider-test.com"}), true) - _, err := provider.Zones(context.Background()) - assert.Errorf(t, err, " Multiple domain filter failed %s", "formatted") - } -} - -// zones function with domain filter test scenario -func TestUltraDNSProvider_DomainFilterZonesMocked(t *testing.T) { - mocked := mockUltraDNSZone{} - provider := &UltraDNSProvider{ - client: udnssdk.Client{ - Zone: &mocked, - }, - domainFilter: endpoint.NewDomainFilter([]string{"test-ultradns-provider.com."}), - } - - zoneKey := &udnssdk.ZoneKey{ - Zone: "test-ultradns-provider.com.", - AccountName: "", - } - - // When AccountName not given - expected, _, _, err := provider.client.Zone.SelectWithOffsetWithLimit(zoneKey, 0, 1000) - assert.NoError(t, err) - zones, err := provider.Zones(context.Background()) - assert.NoError(t, err) - assert.True(t, reflect.DeepEqual(expected, zones)) - accountName = "teamrest" - // When AccountName is set - provider = &UltraDNSProvider{ - client: udnssdk.Client{ - Zone: &mocked, - }, - domainFilter: endpoint.NewDomainFilter([]string{"test-ultradns-provider.com."}), - } - - zoneKey = &udnssdk.ZoneKey{ - Zone: "test-ultradns-provider.com.", - AccountName: "teamrest", - } - - expected, _, _, err = provider.client.Zone.SelectWithOffsetWithLimit(zoneKey, 0, 1000) - assert.NoError(t, err) - zones, err = provider.Zones(context.Background()) - assert.NoError(t, err) - assert.True(t, reflect.DeepEqual(expected, zones)) - - // When zone is not given but account is provided - provider = &UltraDNSProvider{ - client: udnssdk.Client{ - Zone: &mocked, - }, - } - - zoneKey = &udnssdk.ZoneKey{ - AccountName: "teamrest", - } - - expected, _, _, err = provider.client.Zone.SelectWithOffsetWithLimit(zoneKey, 0, 1000) - assert.NoError(t, err) - zones, err = provider.Zones(context.Background()) - assert.NoError(t, err) - assert.True(t, reflect.DeepEqual(expected, zones)) -} From 58d7d02c7637f844c700917b994c66b6349e222c Mon Sep 17 00:00:00 2001 From: Michelangelo Date: Sat, 24 May 2025 12:11:10 +0200 Subject: [PATCH 28/33] add desec.io webhook provider --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d3aa4ec6c..3cf7d3471 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Known providers using webhooks: | Anexia | https://github.com/anexia/k8s-external-dns-webhook | | Bizfly Cloud | https://github.com/bizflycloud/external-dns-bizflycloud-webhook | | ClouDNS | https://github.com/rwunderer/external-dns-cloudns-webhook | +| deSEC | https://github.com/michelangelomo/external-dns-desec-provider | | Dreamhost | https://github.com/asymingt/external-dns-dreamhost-webhook | | Efficient IP | https://github.com/EfficientIP-Labs/external-dns-efficientip-webhook | | Gcore | https://github.com/G-Core/external-dns-gcore-webhook | From 2b7d236734abac32f1bd7a8ef59ca07dbd77a030 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Sun, 25 May 2025 13:47:16 +0100 Subject: [PATCH 29/33] chore(source): move cache informer to dedicated folder Signed-off-by: ivan katliarchuk --- source/ambassador_host.go | 18 +++-- source/contour_httpproxy.go | 11 ++- source/f5_transportserver.go | 8 +- source/f5_virtualserver.go | 7 +- source/gateway.go | 25 +++--- source/informers/informers.go | 72 +++++++++++++++++ source/informers/informers_test.go | 126 +++++++++++++++++++++++++++++ source/ingress.go | 4 +- source/istio_gateway.go | 8 +- source/istio_virtualservice.go | 5 +- source/kong_tcpingress.go | 7 +- source/node.go | 3 +- source/openshift_route.go | 3 +- source/pod.go | 6 +- source/service.go | 4 +- source/source.go | 44 ---------- source/source_test.go | 88 ++++++++++++-------- source/traefik_proxy.go | 21 ++--- 18 files changed, 323 insertions(+), 137 deletions(-) create mode 100644 source/informers/informers.go create mode 100644 source/informers/informers_test.go diff --git a/source/ambassador_host.go b/source/ambassador_host.go index 077a3f582..3de5d5bbe 100644 --- a/source/ambassador_host.go +++ b/source/ambassador_host.go @@ -32,20 +32,22 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/informers" + kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/informers" ) -// ambHostAnnotation is the annotation in the Host that maps to a Service -const ambHostAnnotation = "external-dns.ambassador-service" - -// groupName is the group name for the Ambassador API -const groupName = "getambassador.io" +const ( + // ambHostAnnotation is the annotation in the Host that maps to a Service + ambHostAnnotation = "external-dns.ambassador-service" + // groupName is the group name for the Ambassador API + groupName = "getambassador.io" +) var schemeGroupVersion = schema.GroupVersion{Group: groupName, Version: "v2"} @@ -59,7 +61,7 @@ type ambassadorHostSource struct { kubeClient kubernetes.Interface namespace string annotationFilter string - ambassadorHostInformer informers.GenericInformer + ambassadorHostInformer kubeinformers.GenericInformer unstructuredConverter *unstructuredConverter labelSelector labels.Selector } @@ -91,7 +93,7 @@ func NewAmbassadorHostSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(context.Background(), informerFactory); err != nil { return nil, err } diff --git a/source/contour_httpproxy.go b/source/contour_httpproxy.go index 7977ea840..b0a57171c 100644 --- a/source/contour_httpproxy.go +++ b/source/contour_httpproxy.go @@ -30,13 +30,13 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/informers" + kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/tools/cache" - "sigs.k8s.io/external-dns/source/fqdn" - "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/fqdn" + "sigs.k8s.io/external-dns/source/informers" ) // HTTPProxySource is an implementation of Source for ProjectContour HTTPProxy objects. @@ -49,7 +49,7 @@ type httpProxySource struct { fqdnTemplate *template.Template combineFQDNAnnotation bool ignoreHostnameAnnotation bool - httpProxyInformer informers.GenericInformer + httpProxyInformer kubeinformers.GenericInformer unstructuredConverter *UnstructuredConverter } @@ -84,7 +84,7 @@ func NewContourHTTPProxySource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(context.Background(), informerFactory); err != nil { return nil, err } @@ -113,7 +113,6 @@ func (sc *httpProxySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, return nil, err } - // Convert to []*projectcontour.HTTPProxy var httpProxies []*projectcontour.HTTPProxy for _, hp := range hps { unstructuredHP, ok := hp.(*unstructured.Unstructured) diff --git a/source/f5_transportserver.go b/source/f5_transportserver.go index c9417d5bf..70b5736e9 100644 --- a/source/f5_transportserver.go +++ b/source/f5_transportserver.go @@ -30,13 +30,15 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/informers" + kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/cache" f5 "github.com/F5Networks/k8s-bigip-ctlr/v2/config/apis/cis/v1" + "sigs.k8s.io/external-dns/source/informers" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" ) @@ -50,7 +52,7 @@ var f5TransportServerGVR = schema.GroupVersionResource{ // transportServerSource is an implementation of Source for F5 TransportServer objects. type f5TransportServerSource struct { dynamicKubeClient dynamic.Interface - transportServerInformer informers.GenericInformer + transportServerInformer kubeinformers.GenericInformer kubeClient kubernetes.Interface annotationFilter string namespace string @@ -77,7 +79,7 @@ func NewF5TransportServerSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(context.Background(), informerFactory); err != nil { return nil, err } diff --git a/source/f5_virtualserver.go b/source/f5_virtualserver.go index 9ff80b90b..febc5dd6f 100644 --- a/source/f5_virtualserver.go +++ b/source/f5_virtualserver.go @@ -31,7 +31,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/informers" + kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/cache" @@ -40,6 +40,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/informers" ) var f5VirtualServerGVR = schema.GroupVersionResource{ @@ -51,7 +52,7 @@ var f5VirtualServerGVR = schema.GroupVersionResource{ // virtualServerSource is an implementation of Source for F5 VirtualServer objects. type f5VirtualServerSource struct { dynamicKubeClient dynamic.Interface - virtualServerInformer informers.GenericInformer + virtualServerInformer kubeinformers.GenericInformer kubeClient kubernetes.Interface annotationFilter string namespace string @@ -78,7 +79,7 @@ func NewF5VirtualServerSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(context.Background(), informerFactory); err != nil { return nil, err } diff --git a/source/gateway.go b/source/gateway.go index 48315bbe2..6b72c56f4 100644 --- a/source/gateway.go +++ b/source/gateway.go @@ -32,16 +32,17 @@ import ( "k8s.io/apimachinery/pkg/util/wait" kubeinformers "k8s.io/client-go/informers" coreinformers "k8s.io/client-go/informers/core/v1" - cache "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/cache" v1 "sigs.k8s.io/gateway-api/apis/v1" - v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + "sigs.k8s.io/gateway-api/apis/v1beta1" gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned" - informers "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions" + gwinformers "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions" informers_v1beta1 "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions/apis/v1beta1" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" "sigs.k8s.io/external-dns/source/fqdn" + "sigs.k8s.io/external-dns/source/informers" ) const ( @@ -64,25 +65,25 @@ type gatewayRoute interface { RouteStatus() v1.RouteStatus } -type newGatewayRouteInformerFunc func(informers.SharedInformerFactory) gatewayRouteInformer +type newGatewayRouteInformerFunc func(gwinformers.SharedInformerFactory) gatewayRouteInformer type gatewayRouteInformer interface { List(namespace string, selector labels.Selector) ([]gatewayRoute, error) Informer() cache.SharedIndexInformer } -func newGatewayInformerFactory(client gateway.Interface, namespace string, labelSelector labels.Selector) informers.SharedInformerFactory { - var opts []informers.SharedInformerOption +func newGatewayInformerFactory(client gateway.Interface, namespace string, labelSelector labels.Selector) gwinformers.SharedInformerFactory { + var opts []gwinformers.SharedInformerOption if namespace != "" { - opts = append(opts, informers.WithNamespace(namespace)) + opts = append(opts, gwinformers.WithNamespace(namespace)) } if labelSelector != nil && !labelSelector.Empty() { lbls := labelSelector.String() - opts = append(opts, informers.WithTweakListOptions(func(o *metav1.ListOptions) { + opts = append(opts, gwinformers.WithTweakListOptions(func(o *metav1.ListOptions) { o.LabelSelector = lbls })) } - return informers.NewSharedInformerFactoryWithOptions(client, 0, opts...) + return gwinformers.NewSharedInformerFactoryWithOptions(client, 0, opts...) } type gatewayRouteSource struct { @@ -154,14 +155,14 @@ func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string, if rtInformerFactory != informerFactory { rtInformerFactory.Start(wait.NeverStop) - if err := waitForCacheSync(ctx, rtInformerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, rtInformerFactory); err != nil { return nil, err } } - if err := waitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { return nil, err } - if err := waitForCacheSync(ctx, kubeInformerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, kubeInformerFactory); err != nil { return nil, err } diff --git a/source/informers/informers.go b/source/informers/informers.go new file mode 100644 index 000000000..c0fef473a --- /dev/null +++ b/source/informers/informers.go @@ -0,0 +1,72 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package informers + +import ( + "context" + "fmt" + "reflect" + "time" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + defaultRequestTimeout = 60 +) + +type informerFactory interface { + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool +} + +func WaitForCacheSync(ctx context.Context, factory informerFactory) error { + timeout := defaultRequestTimeout * time.Second + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + for typ, done := range factory.WaitForCacheSync(ctx.Done()) { + if !done { + select { + case <-ctx.Done(): + return fmt.Errorf("failed to sync %v: %w with timeout %s", typ, ctx.Err(), timeout) + default: + return fmt.Errorf("failed to sync %v with timeout %s", typ, timeout) + } + } + } + return nil +} + +type dynamicInformerFactory interface { + WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool +} + +func WaitForDynamicCacheSync(ctx context.Context, factory dynamicInformerFactory) error { + timeout := defaultRequestTimeout * time.Second + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + for typ, done := range factory.WaitForCacheSync(ctx.Done()) { + if !done { + select { + case <-ctx.Done(): + return fmt.Errorf("failed to sync %v: %w with timeout %s", typ, ctx.Err(), timeout) + default: + return fmt.Errorf("failed to sync %v with timeout %s", typ, timeout) + } + } + } + return nil +} diff --git a/source/informers/informers_test.go b/source/informers/informers_test.go new file mode 100644 index 000000000..2268efb38 --- /dev/null +++ b/source/informers/informers_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package informers + +import ( + "context" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type mockInformerFactory struct { + syncResults map[reflect.Type]bool +} + +func (m *mockInformerFactory) WaitForCacheSync(_ <-chan struct{}) map[reflect.Type]bool { + return m.syncResults +} + +type mockDynamicInformerFactory struct { + syncResults map[schema.GroupVersionResource]bool +} + +func (m *mockDynamicInformerFactory) WaitForCacheSync(_ <-chan struct{}) map[schema.GroupVersionResource]bool { + return m.syncResults +} + +func TestWaitForCacheSync(t *testing.T) { + tests := []struct { + name string + syncResults map[reflect.Type]bool + expectError bool + errorMsg string + }{ + { + name: "all caches synced", + syncResults: map[reflect.Type]bool{reflect.TypeOf(""): true}, + }, + { + name: "some caches not synced", + syncResults: map[reflect.Type]bool{reflect.TypeOf(""): false}, + expectError: true, + errorMsg: "failed to sync string with timeout 1m0s", + }, + { + name: "context timeout", + syncResults: map[reflect.Type]bool{reflect.TypeOf(""): false}, + expectError: true, + errorMsg: "failed to sync string with timeout 1m0s", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + factory := &mockInformerFactory{syncResults: tt.syncResults} + err := WaitForCacheSync(ctx, factory) + + if tt.expectError { + assert.Error(t, err) + assert.Errorf(t, err, tt.errorMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestWaitForDynamicCacheSync(t *testing.T) { + tests := []struct { + name string + syncResults map[schema.GroupVersionResource]bool + expectError bool + errorMsg string + }{ + { + name: "all caches synced", + syncResults: map[schema.GroupVersionResource]bool{schema.GroupVersionResource{}: true}, + }, + { + name: "some caches not synced", + syncResults: map[schema.GroupVersionResource]bool{schema.GroupVersionResource{}: false}, + expectError: true, + errorMsg: "failed to sync string with timeout 1m0s", + }, + { + name: "context timeout", + syncResults: map[schema.GroupVersionResource]bool{schema.GroupVersionResource{}: false}, + expectError: true, + errorMsg: "failed to sync string with timeout 1m0s", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + factory := &mockDynamicInformerFactory{syncResults: tt.syncResults} + err := WaitForDynamicCacheSync(ctx, factory) + + if tt.expectError { + assert.Error(t, err) + assert.Errorf(t, err, tt.errorMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/source/ingress.go b/source/ingress.go index f00c170f4..03070bc46 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -33,6 +33,8 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/external-dns/source/informers" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" "sigs.k8s.io/external-dns/source/fqdn" @@ -102,7 +104,7 @@ func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, name informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil { return nil, err } diff --git a/source/istio_gateway.go b/source/istio_gateway.go index ceed678c9..72f49f4f3 100644 --- a/source/istio_gateway.go +++ b/source/istio_gateway.go @@ -35,10 +35,10 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" - "sigs.k8s.io/external-dns/source/fqdn" - "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/fqdn" + "sigs.k8s.io/external-dns/source/informers" ) // IstioGatewayIngressSource is the annotation used to determine if the gateway is implemented by an Ingress object @@ -104,10 +104,10 @@ func NewIstioGatewaySource( istioInformerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil { return nil, err } - if err := waitForCacheSync(context.Background(), istioInformerFactory); err != nil { + if err := informers.WaitForCacheSync(context.Background(), istioInformerFactory); err != nil { return nil, err } diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go index 1e0a75067..007021b4a 100644 --- a/source/istio_virtualservice.go +++ b/source/istio_virtualservice.go @@ -40,6 +40,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" "sigs.k8s.io/external-dns/source/fqdn" + "sigs.k8s.io/external-dns/source/informers" ) // IstioMeshGateway is the built in gateway for all sidecars @@ -114,10 +115,10 @@ func NewIstioVirtualServiceSource( istioInformerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil { return nil, err } - if err := waitForCacheSync(context.Background(), istioInformerFactory); err != nil { + if err := informers.WaitForCacheSync(context.Background(), istioInformerFactory); err != nil { return nil, err } diff --git a/source/kong_tcpingress.go b/source/kong_tcpingress.go index f65e178e3..6bdc5886b 100644 --- a/source/kong_tcpingress.go +++ b/source/kong_tcpingress.go @@ -31,13 +31,14 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/informers" + kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/informers" ) var kongGroupdVersionResource = schema.GroupVersionResource{ @@ -51,7 +52,7 @@ type kongTCPIngressSource struct { annotationFilter string ignoreHostnameAnnotation bool dynamicKubeClient dynamic.Interface - kongTCPIngressInformer informers.GenericInformer + kongTCPIngressInformer kubeinformers.GenericInformer kubeClient kubernetes.Interface namespace string unstructuredConverter *unstructuredConverter @@ -77,7 +78,7 @@ func NewKongTCPIngressSource(ctx context.Context, dynamicKubeClient dynamic.Inte informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(context.Background(), informerFactory); err != nil { return nil, err } diff --git a/source/node.go b/source/node.go index 38ecb5457..33e7ea69e 100644 --- a/source/node.go +++ b/source/node.go @@ -32,6 +32,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" "sigs.k8s.io/external-dns/source/fqdn" + "sigs.k8s.io/external-dns/source/informers" ) const warningMsg = "The default behavior of exposing internal IPv6 addresses will change in the next minor version. Use --no-expose-internal-ipv6 flag to opt-in to the new behavior." @@ -70,7 +71,7 @@ func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotat informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil { return nil, err } diff --git a/source/openshift_route.go b/source/openshift_route.go index 1168d054d..40e12fec8 100644 --- a/source/openshift_route.go +++ b/source/openshift_route.go @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" "sigs.k8s.io/external-dns/source/fqdn" + "sigs.k8s.io/external-dns/source/informers" ) // ocpRouteSource is an implementation of Source for OpenShift Route objects. @@ -87,7 +88,7 @@ func NewOcpRouteSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil { return nil, err } diff --git a/source/pod.go b/source/pod.go index f9326371a..22a150a3c 100644 --- a/source/pod.go +++ b/source/pod.go @@ -19,8 +19,6 @@ package source import ( "context" - "sigs.k8s.io/external-dns/endpoint" - log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" @@ -29,7 +27,9 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/informers" ) type podSource struct { @@ -64,7 +64,7 @@ func NewPodSource(ctx context.Context, kubeClient kubernetes.Interface, namespac informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil { return nil, err } diff --git a/source/service.go b/source/service.go index 96764d347..ae9cb8df8 100644 --- a/source/service.go +++ b/source/service.go @@ -33,6 +33,8 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/external-dns/source/informers" + "sigs.k8s.io/external-dns/source/annotations" "sigs.k8s.io/external-dns/endpoint" @@ -112,7 +114,7 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := waitForCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil { return nil, err } diff --git a/source/source.go b/source/source.go index a18f85ff0..aaa2d1dc1 100644 --- a/source/source.go +++ b/source/source.go @@ -18,14 +18,10 @@ package source import ( "context" - "fmt" - "reflect" - "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" @@ -84,43 +80,3 @@ type eventHandlerFunc func() func (fn eventHandlerFunc) OnAdd(obj interface{}, isInInitialList bool) { fn() } func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{}) { fn() } func (fn eventHandlerFunc) OnDelete(obj interface{}) { fn() } - -type informerFactory interface { - WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool -} - -func waitForCacheSync(ctx context.Context, factory informerFactory) error { - ctx, cancel := context.WithTimeout(ctx, 60*time.Second) - defer cancel() - for typ, done := range factory.WaitForCacheSync(ctx.Done()) { - if !done { - select { - case <-ctx.Done(): - return fmt.Errorf("failed to sync %v: %w", typ, ctx.Err()) - default: - return fmt.Errorf("failed to sync %v", typ) - } - } - } - return nil -} - -type dynamicInformerFactory interface { - WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool -} - -func waitForDynamicCacheSync(ctx context.Context, factory dynamicInformerFactory) error { - ctx, cancel := context.WithTimeout(ctx, 60*time.Second) - defer cancel() - for typ, done := range factory.WaitForCacheSync(ctx.Done()) { - if !done { - select { - case <-ctx.Done(): - return fmt.Errorf("failed to sync %v: %w", typ, ctx.Err()) - default: - return fmt.Errorf("failed to sync %v", typ) - } - } - } - return nil -} diff --git a/source/source_test.go b/source/source_test.go index fcc735747..0eba3f94f 100644 --- a/source/source_test.go +++ b/source/source_test.go @@ -17,58 +17,76 @@ limitations under the License. package source import ( - "context" - "reflect" "testing" - "time" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/labels" ) -type mockInformerFactory struct { - syncResults map[reflect.Type]bool -} - -func (m *mockInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { - return m.syncResults -} - -func TestWaitForCacheSync(t *testing.T) { +func TestGetLabelSelector(t *testing.T) { tests := []struct { - name string - syncResults map[reflect.Type]bool - expectError bool + name string + annotationFilter string + expectError bool + expectedSelector string }{ { - name: "all caches synced", - syncResults: map[reflect.Type]bool{reflect.TypeOf(""): true}, - expectError: false, + name: "Valid label selector", + annotationFilter: "key1=value1,key2=value2", + expectedSelector: "key1=value1,key2=value2", }, { - name: "some caches not synced", - syncResults: map[reflect.Type]bool{reflect.TypeOf(""): false}, - expectError: true, + name: "Invalid label selector", + annotationFilter: "key1==value1", + expectedSelector: "key1=value1", }, { - name: "context timeout", - syncResults: map[reflect.Type]bool{reflect.TypeOf(""): false}, - expectError: true, + name: "Empty label selector", + annotationFilter: "", + expectedSelector: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) - defer cancel() - - factory := &mockInformerFactory{syncResults: tt.syncResults} - err := waitForCacheSync(ctx, factory) - - if tt.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } + selector, err := getLabelSelector(tt.annotationFilter) + assert.NoError(t, err) + assert.Equal(t, tt.expectedSelector, selector.String()) + }) + } +} + +func TestMatchLabelSelector(t *testing.T) { + tests := []struct { + name string + selector labels.Selector + srcAnnotations map[string]string + expectedMatch bool + }{ + { + name: "Matching label selector", + selector: labels.SelectorFromSet(labels.Set{"key1": "value1"}), + srcAnnotations: map[string]string{"key1": "value1", "key2": "value2"}, + expectedMatch: true, + }, + { + name: "Non-matching label selector", + selector: labels.SelectorFromSet(labels.Set{"key1": "value1"}), + srcAnnotations: map[string]string{"key2": "value2"}, + expectedMatch: false, + }, + { + name: "Empty label selector", + selector: labels.NewSelector(), + srcAnnotations: map[string]string{"key1": "value1"}, + expectedMatch: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := matchLabelSelector(tt.selector, tt.srcAnnotations) + assert.Equal(t, tt.expectedMatch, result) }) } } diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go index 212b604c4..48fb2335d 100644 --- a/source/traefik_proxy.go +++ b/source/traefik_proxy.go @@ -32,13 +32,14 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/informers" + kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/informers" ) var ( @@ -83,12 +84,12 @@ type traefikSource struct { annotationFilter string ignoreHostnameAnnotation bool dynamicKubeClient dynamic.Interface - ingressRouteInformer informers.GenericInformer - ingressRouteTcpInformer informers.GenericInformer - ingressRouteUdpInformer informers.GenericInformer - oldIngressRouteInformer informers.GenericInformer - oldIngressRouteTcpInformer informers.GenericInformer - oldIngressRouteUdpInformer informers.GenericInformer + ingressRouteInformer kubeinformers.GenericInformer + ingressRouteTcpInformer kubeinformers.GenericInformer + ingressRouteUdpInformer kubeinformers.GenericInformer + oldIngressRouteInformer kubeinformers.GenericInformer + oldIngressRouteTcpInformer kubeinformers.GenericInformer + oldIngressRouteUdpInformer kubeinformers.GenericInformer kubeClient kubernetes.Interface namespace string unstructuredConverter *unstructuredConverter @@ -98,8 +99,8 @@ func NewTraefikSource(ctx context.Context, dynamicKubeClient dynamic.Interface, // Use shared informer to listen for add/update/delete of Host in the specified namespace. // Set resync period to 0, to prevent processing when nothing has changed. informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil) - var ingressRouteInformer, ingressRouteTcpInformer, ingressRouteUdpInformer informers.GenericInformer - var oldIngressRouteInformer, oldIngressRouteTcpInformer, oldIngressRouteUdpInformer informers.GenericInformer + var ingressRouteInformer, ingressRouteTcpInformer, ingressRouteUdpInformer kubeinformers.GenericInformer + var oldIngressRouteInformer, oldIngressRouteTcpInformer, oldIngressRouteUdpInformer kubeinformers.GenericInformer // Add default resource event handlers to properly initialize informers. if !disableNew { @@ -146,7 +147,7 @@ func NewTraefikSource(ctx context.Context, dynamicKubeClient dynamic.Interface, informerFactory.Start((ctx.Done())) // wait for the local cache to be populated. - if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(context.Background(), informerFactory); err != nil { return nil, err } From 29c204037d1f2c40234eb8a6a64f07d4247f9f65 Mon Sep 17 00:00:00 2001 From: smilutinovic-ionos Date: Tue, 27 May 2025 14:39:17 +0200 Subject: [PATCH 30/33] add IONOS Cloud webhook reference --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 29c9834b9..047901581 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,7 @@ The following tutorials are provided: - [Using Google's Default Ingress Controller](docs/tutorials/gke.md) - [Using the Nginx Ingress Controller](docs/tutorials/gke-nginx.md) - [Headless Services](docs/tutorials/hostport.md) +- [IONOS Cloud](docs/tutorials/ionoscloud.md) - [Istio Gateway Source](docs/sources/istio.md) - [Linode](docs/tutorials/linode.md) - [NS1](docs/tutorials/ns1.md) From 8977f3f90425ffa9d3d4735b9749821fac725b1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 20:02:23 +0000 Subject: [PATCH 31/33] chore(deps): bump the dev-dependencies group across 1 directory with 3 updates Bumps the dev-dependencies group with 3 updates in the / directory: [renovatebot/github-action](https://github.com/renovatebot/github-action), [actions/setup-python](https://github.com/actions/setup-python) and [action-stars/install-tool-from-github-release](https://github.com/action-stars/install-tool-from-github-release). Updates `renovatebot/github-action` from 41.0.22 to 42.0.4 - [Release notes](https://github.com/renovatebot/github-action/releases) - [Changelog](https://github.com/renovatebot/github-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/renovatebot/github-action/compare/v41.0.22...v42.0.4) Updates `actions/setup-python` from 3 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v5) Updates `action-stars/install-tool-from-github-release` from 0.2.4 to 0.2.5 - [Release notes](https://github.com/action-stars/install-tool-from-github-release/releases) - [Changelog](https://github.com/action-stars/install-tool-from-github-release/blob/main/CHANGELOG.md) - [Commits](https://github.com/action-stars/install-tool-from-github-release/compare/ece2623611b240002e0dd73a0d685505733122f6...f2e83e089fa618aa7e9fd3452fbcf4fe1598ede2) --- updated-dependencies: - dependency-name: renovatebot/github-action dependency-version: 42.0.4 dependency-type: direct:production update-type: version-update:semver-major dependency-group: dev-dependencies - dependency-name: actions/setup-python dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: dev-dependencies - dependency-name: action-stars/install-tool-from-github-release dependency-version: 0.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-update.yaml | 2 +- .github/workflows/lint-test-chart.yaml | 4 ++-- .github/workflows/lint.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependency-update.yaml b/.github/workflows/dependency-update.yaml index 67ad0cb72..7fc4a1dc6 100644 --- a/.github/workflows/dependency-update.yaml +++ b/.github/workflows/dependency-update.yaml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4.2.2 # https://github.com/renovatebot/github-action - name: self-hosted renovate - uses: renovatebot/github-action@v41.0.22 + uses: renovatebot/github-action@v42.0.4 with: # https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint-test-chart.yaml b/.github/workflows/lint-test-chart.yaml index fa8fcf62a..dc5811c41 100644 --- a/.github/workflows/lint-test-chart.yaml +++ b/.github/workflows/lint-test-chart.yaml @@ -38,7 +38,7 @@ jobs: fi - name: Install Helm Docs - uses: action-stars/install-tool-from-github-release@ece2623611b240002e0dd73a0d685505733122f6 # v0.2.4 + uses: action-stars/install-tool-from-github-release@f2e83e089fa618aa7e9fd3452fbcf4fe1598ede2 # v0.2.5 with: github_token: ${{ secrets.GITHUB_TOKEN }} owner: norwoodj @@ -65,7 +65,7 @@ jobs: helm unittest -f 'tests/*_test.yaml' --color charts/external-dns - name: Install Artifact Hub CLI - uses: action-stars/install-tool-from-github-release@ece2623611b240002e0dd73a0d685505733122f6 # v0.2.4 + uses: action-stars/install-tool-from-github-release@f2e83e089fa618aa7e9fd3452fbcf4fe1598ede2 # v0.2.5 with: github_token: ${{ github.token }} owner: artifacthub diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index c1b26d401..71bbc18d4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -56,7 +56,7 @@ jobs: with: file_glob: 'api/*.yaml' - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v5 # https://github.com/pre-commit/action - name: Verify with pre-commit uses: pre-commit/action@v3.0.1 From 95c2c72d22f71815e6689d176992b5c1ca6ccaf2 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Thu, 29 May 2025 21:04:18 +0100 Subject: [PATCH 32/33] fix(provider): aws-sd provider null pointer (#5404) Signed-off-by: ivan katliarchuk --- endpoint/labels.go | 4 +- provider/awssd/aws_sd.go | 130 ++++---- provider/awssd/aws_sd_test.go | 488 ++++++++++++++--------------- provider/awssd/fixtures_test.go | 275 ++++++++++++++++ source/informers/informers_test.go | 6 +- 5 files changed, 585 insertions(+), 318 deletions(-) create mode 100644 provider/awssd/fixtures_test.go diff --git a/endpoint/labels.go b/endpoint/labels.go index 7ee9cf8d8..f5e9ee33d 100644 --- a/endpoint/labels.go +++ b/endpoint/labels.go @@ -90,8 +90,8 @@ func NewLabelsFromStringPlain(labelText string) (Labels, error) { func NewLabelsFromString(labelText string, aesKey []byte) (Labels, error) { if len(aesKey) != 0 { decryptedText, encryptionNonce, err := DecryptText(strings.Trim(labelText, "\""), aesKey) - // in case if we have decryption error, just try process original text - // decryption errors should be ignored here, because we can already have plain-text labels in registry + // in case if we have a decryption error, try process original text + // decryption errors should be ignored here, because we can already have plain-text labels in the registry if err == nil { labels, err := NewLabelsFromStringPlain(decryptedText) if err == nil { diff --git a/provider/awssd/aws_sd.go b/provider/awssd/aws_sd.go index 44511f0b9..7fb9f7cd8 100644 --- a/provider/awssd/aws_sd.go +++ b/provider/awssd/aws_sd.go @@ -37,6 +37,9 @@ import ( const ( defaultTTL = 300 + // https://github.com/aws/aws-sdk-go-v2/blob/cf8509382340d6afdc93612550d56d685181bbb3/service/servicediscovery/api_op_ListServices.go#L42 + maxResults = 100 + sdNamespaceTypePublic = "public" sdNamespaceTypePrivate = "private" @@ -117,7 +120,7 @@ func newSdNamespaceFilter(namespaceTypeConfig string) sdtypes.NamespaceFilter { } } -// awsTags converts user supplied tags to AWS format +// awsTags converts user-supplied tags to AWS format func awsTags(tags map[string]string) []sdtypes.Tag { awsTags := make([]sdtypes.Tag, 0, len(tags)) for k, v := range tags { @@ -155,6 +158,11 @@ func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp continue } + if srv.Description == nil { + log.Warnf("Skipping service %q as owner id not configured", *srv.Name) + continue + } + endpoints = append(endpoints, p.instancesToEndpoint(ns, srv, resp.Instances)) } } @@ -167,6 +175,7 @@ func (p *AWSSDProvider) instancesToEndpoint(ns *sdtypes.NamespaceSummary, srv *s recordName := *srv.Name + "." + *ns.Name labels := endpoint.NewLabels() + labels[endpoint.AWSSDDescriptionLabel] = *srv.Description newEndpoint := &endpoint.Endpoint{ @@ -288,7 +297,7 @@ func (p *AWSSDProvider) submitCreates(ctx context.Context, namespaces []*sdtypes if err != nil { return err } - // update local list of services + // update a local list of services services[*srv.Name] = srv } else if ch.RecordTTL.IsConfigured() && *srv.DnsConfig.DnsRecords[0].TTL != int64(ch.RecordTTL) { // update service when TTL differ @@ -360,7 +369,7 @@ func (p *AWSSDProvider) ListNamespaces(ctx context.Context) ([]*sdtypes.Namespac return namespaces, nil } -// ListServicesByNamespaceID returns list of services in given namespace. +// ListServicesByNamespaceID returns a list of services in a given namespace. func (p *AWSSDProvider) ListServicesByNamespaceID(ctx context.Context, namespaceID *string) (map[string]*sdtypes.Service, error) { services := make([]sdtypes.ServiceSummary, 0) @@ -369,7 +378,7 @@ func (p *AWSSDProvider) ListServicesByNamespaceID(ctx context.Context, namespace Name: sdtypes.ServiceFilterNameNamespaceId, Values: []string{*namespaceID}, }}, - MaxResults: aws.Int32(100), + MaxResults: aws.Int32(maxResults), }) for paginator.HasMorePages() { resp, err := paginator.NextPage(ctx) @@ -412,32 +421,32 @@ func (p *AWSSDProvider) CreateService(ctx context.Context, namespaceID *string, ttl = int64(ep.RecordTTL) } - if !p.dryRun { - out, err := p.client.CreateService(ctx, &sd.CreateServiceInput{ - Name: srvName, - Description: aws.String(ep.Labels[endpoint.AWSSDDescriptionLabel]), - DnsConfig: &sdtypes.DnsConfig{ - RoutingPolicy: routingPolicy, - DnsRecords: []sdtypes.DnsRecord{{ - Type: srvType, - TTL: aws.Int64(ttl), - }}, - }, - NamespaceId: namespaceID, - Tags: p.tags, - }) - if err != nil { - return nil, err - } - - return out.Service, nil + if p.dryRun { + // return a mock service summary in case of a dry run + return &sdtypes.Service{Id: aws.String("dry-run-service"), Name: aws.String("dry-run-service")}, nil } - // return mock service summary in case of dry run - return &sdtypes.Service{Id: aws.String("dry-run-service"), Name: aws.String("dry-run-service")}, nil + out, err := p.client.CreateService(ctx, &sd.CreateServiceInput{ + Name: srvName, + Description: aws.String(ep.Labels[endpoint.AWSSDDescriptionLabel]), + DnsConfig: &sdtypes.DnsConfig{ + RoutingPolicy: routingPolicy, + DnsRecords: []sdtypes.DnsRecord{{ + Type: srvType, + TTL: aws.Int64(ttl), + }}, + }, + NamespaceId: namespaceID, + Tags: p.tags, + }) + if err != nil { + return nil, err + } + + return out.Service, nil } -// UpdateService updates the specified service with information from provided endpoint. +// UpdateService updates the specified service with information from the provided endpoint. func (p *AWSSDProvider) UpdateService(ctx context.Context, service *sdtypes.Service, ep *endpoint.Endpoint) error { log.Infof("Updating service \"%s\"", *service.Name) @@ -448,45 +457,52 @@ func (p *AWSSDProvider) UpdateService(ctx context.Context, service *sdtypes.Serv ttl = int64(ep.RecordTTL) } - if !p.dryRun { - _, err := p.client.UpdateService(ctx, &sd.UpdateServiceInput{ - Id: service.Id, - Service: &sdtypes.ServiceChange{ - Description: aws.String(ep.Labels[endpoint.AWSSDDescriptionLabel]), - DnsConfig: &sdtypes.DnsConfigChange{ - DnsRecords: []sdtypes.DnsRecord{{ - Type: srvType, - TTL: aws.Int64(ttl), - }}, - }, - }, - }) - if err != nil { - return err - } + if p.dryRun { + return nil } - return nil + _, err := p.client.UpdateService(ctx, &sd.UpdateServiceInput{ + Id: service.Id, + Service: &sdtypes.ServiceChange{ + Description: aws.String(ep.Labels[endpoint.AWSSDDescriptionLabel]), + DnsConfig: &sdtypes.DnsConfigChange{ + DnsRecords: []sdtypes.DnsRecord{{ + Type: srvType, + TTL: aws.Int64(ttl), + }}, + }, + }, + }) + return err } // DeleteService deletes empty Service from AWS API if its owner id match func (p *AWSSDProvider) DeleteService(ctx context.Context, service *sdtypes.Service) error { log.Debugf("Check if service \"%s\" owner id match and it can be deleted", *service.Name) - if !p.dryRun && p.cleanEmptyService { - // convert ownerID string to service description format - label := endpoint.NewLabels() - label[endpoint.OwnerLabelKey] = p.ownerID - label[endpoint.AWSSDDescriptionLabel] = label.SerializePlain(false) - if strings.HasPrefix(*service.Description, label[endpoint.AWSSDDescriptionLabel]) { - log.Infof("Deleting service \"%s\"", *service.Name) - _, err := p.client.DeleteService(ctx, &sd.DeleteServiceInput{ - Id: aws.String(*service.Id), - }) - return err - } - log.Debugf("Skipping service removal %s because owner id does not match, found: \"%s\", required: \"%s\"", *service.Name, *service.Description, label[endpoint.AWSSDDescriptionLabel]) + if p.dryRun || !p.cleanEmptyService { + return nil } + + // convert ownerID string to the service description format + label := endpoint.NewLabels() + label[endpoint.OwnerLabelKey] = p.ownerID + label[endpoint.AWSSDDescriptionLabel] = label.SerializePlain(false) + + if service.Description == nil { + log.Debugf("Skipping service removal %q because owner id (service.Description) not set, when should be %q", *service.Name, label[endpoint.AWSSDDescriptionLabel]) + return nil + } + + if strings.HasPrefix(*service.Description, label[endpoint.AWSSDDescriptionLabel]) { + log.Infof("Deleting service \"%s\"", *service.Name) + _, err := p.client.DeleteService(ctx, &sd.DeleteServiceInput{ + Id: aws.String(*service.Id), + }) + return err + } + log.Debugf("Skipping service removal %q because owner id does not match, found: %q, required: %q", *service.Name, *service.Description, label[endpoint.AWSSDDescriptionLabel]) + return nil } @@ -619,7 +635,7 @@ func (p *AWSSDProvider) routingPolicyFromEndpoint(ep *endpoint.Endpoint) sdtypes return sdtypes.RoutingPolicyWeighted } -// determine service type (A, AAAA, CNAME) from given endpoint +// determine the service type (A, AAAA, CNAME) from a given endpoint func (p *AWSSDProvider) serviceTypeFromEndpoint(ep *endpoint.Endpoint) sdtypes.RecordType { switch ep.RecordType { case endpoint.RecordTypeCNAME: diff --git a/provider/awssd/aws_sd_test.go b/provider/awssd/aws_sd_test.go index d49760415..f3cacb8c9 100644 --- a/provider/awssd/aws_sd_test.go +++ b/provider/awssd/aws_sd_test.go @@ -18,16 +18,12 @@ package awssd import ( "context" - "errors" - "math/rand" "reflect" - "strconv" "testing" - "time" "github.com/aws/aws-sdk-go-v2/aws" - sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery" sdtypes "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,231 +32,6 @@ import ( "sigs.k8s.io/external-dns/plan" ) -// Compile time checks for interface conformance -var _ AWSSDClient = &AWSSDClientStub{} - -var ( - ErrNamespaceNotFound = errors.New("Namespace not found") -) - -type AWSSDClientStub struct { - // map[namespace_id]namespace - namespaces map[string]*sdtypes.Namespace - - // map[namespace_id] => map[service_id]instance - services map[string]map[string]*sdtypes.Service - - // map[service_id] => map[inst_id]instance - instances map[string]map[string]*sdtypes.Instance - - // []inst_id - deregistered []string -} - -func (s *AWSSDClientStub) CreateService(ctx context.Context, input *sd.CreateServiceInput, optFns ...func(*sd.Options)) (*sd.CreateServiceOutput, error) { - srv := &sdtypes.Service{ - Id: aws.String(strconv.Itoa(rand.Intn(10000))), - DnsConfig: input.DnsConfig, - Name: input.Name, - Description: input.Description, - CreateDate: aws.Time(time.Now()), - CreatorRequestId: input.CreatorRequestId, - } - - nsServices, ok := s.services[*input.NamespaceId] - if !ok { - nsServices = make(map[string]*sdtypes.Service) - s.services[*input.NamespaceId] = nsServices - } - nsServices[*srv.Id] = srv - - return &sd.CreateServiceOutput{ - Service: srv, - }, nil -} - -func (s *AWSSDClientStub) DeregisterInstance(ctx context.Context, input *sd.DeregisterInstanceInput, optFns ...func(options *sd.Options)) (*sd.DeregisterInstanceOutput, error) { - serviceInstances := s.instances[*input.ServiceId] - delete(serviceInstances, *input.InstanceId) - s.deregistered = append(s.deregistered, *input.InstanceId) - - return &sd.DeregisterInstanceOutput{}, nil -} - -func (s *AWSSDClientStub) GetService(ctx context.Context, input *sd.GetServiceInput, optFns ...func(options *sd.Options)) (*sd.GetServiceOutput, error) { - for _, entry := range s.services { - srv, ok := entry[*input.Id] - if ok { - return &sd.GetServiceOutput{ - Service: srv, - }, nil - } - } - - return nil, errors.New("service not found") -} - -func (s *AWSSDClientStub) DiscoverInstances(ctx context.Context, input *sd.DiscoverInstancesInput, opts ...func(options *sd.Options)) (*sd.DiscoverInstancesOutput, error) { - instances := make([]sdtypes.HttpInstanceSummary, 0) - - var foundNs bool - for _, ns := range s.namespaces { - if *ns.Name == *input.NamespaceName { - foundNs = true - - for _, srv := range s.services[*ns.Id] { - if *srv.Name == *input.ServiceName { - for _, inst := range s.instances[*srv.Id] { - instances = append(instances, *instanceToHTTPInstanceSummary(inst)) - } - } - } - } - } - - if !foundNs { - return nil, ErrNamespaceNotFound - } - - return &sd.DiscoverInstancesOutput{ - Instances: instances, - }, nil -} - -func (s *AWSSDClientStub) ListNamespaces(ctx context.Context, input *sd.ListNamespacesInput, optFns ...func(options *sd.Options)) (*sd.ListNamespacesOutput, error) { - namespaces := make([]sdtypes.NamespaceSummary, 0) - - for _, ns := range s.namespaces { - if len(input.Filters) > 0 && input.Filters[0].Name == sdtypes.NamespaceFilterNameType { - if ns.Type != sdtypes.NamespaceType(input.Filters[0].Values[0]) { - // skip namespaces not matching filter - continue - } - } - namespaces = append(namespaces, *namespaceToNamespaceSummary(ns)) - } - - return &sd.ListNamespacesOutput{ - Namespaces: namespaces, - }, nil -} - -func (s *AWSSDClientStub) ListServices(ctx context.Context, input *sd.ListServicesInput, optFns ...func(options *sd.Options)) (*sd.ListServicesOutput, error) { - services := make([]sdtypes.ServiceSummary, 0) - - // get namespace filter - if len(input.Filters) == 0 || input.Filters[0].Name != sdtypes.ServiceFilterNameNamespaceId { - return nil, errors.New("missing namespace filter") - } - nsID := input.Filters[0].Values[0] - - for _, srv := range s.services[nsID] { - services = append(services, *serviceToServiceSummary(srv)) - } - - return &sd.ListServicesOutput{ - Services: services, - }, nil -} - -func (s *AWSSDClientStub) RegisterInstance(ctx context.Context, input *sd.RegisterInstanceInput, optFns ...func(options *sd.Options)) (*sd.RegisterInstanceOutput, error) { - srvInstances, ok := s.instances[*input.ServiceId] - if !ok { - srvInstances = make(map[string]*sdtypes.Instance) - s.instances[*input.ServiceId] = srvInstances - } - - srvInstances[*input.InstanceId] = &sdtypes.Instance{ - Id: input.InstanceId, - Attributes: input.Attributes, - CreatorRequestId: input.CreatorRequestId, - } - - return &sd.RegisterInstanceOutput{}, nil -} - -func (s *AWSSDClientStub) UpdateService(ctx context.Context, input *sd.UpdateServiceInput, optFns ...func(options *sd.Options)) (*sd.UpdateServiceOutput, error) { - out, err := s.GetService(ctx, &sd.GetServiceInput{Id: input.Id}) - if err != nil { - return nil, err - } - - origSrv := out.Service - updateSrv := input.Service - - origSrv.Description = updateSrv.Description - origSrv.DnsConfig.DnsRecords = updateSrv.DnsConfig.DnsRecords - - return &sd.UpdateServiceOutput{}, nil -} - -func (s *AWSSDClientStub) DeleteService(ctx context.Context, input *sd.DeleteServiceInput, optFns ...func(options *sd.Options)) (*sd.DeleteServiceOutput, error) { - out, err := s.GetService(ctx, &sd.GetServiceInput{Id: input.Id}) - if err != nil { - return nil, err - } - - service := out.Service - namespace := s.services[*service.NamespaceId] - delete(namespace, *input.Id) - - return &sd.DeleteServiceOutput{}, nil -} - -func newTestAWSSDProvider(api AWSSDClient, domainFilter endpoint.DomainFilter, namespaceTypeFilter, ownerID string) *AWSSDProvider { - return &AWSSDProvider{ - client: api, - dryRun: false, - namespaceFilter: domainFilter, - namespaceTypeFilter: newSdNamespaceFilter(namespaceTypeFilter), - cleanEmptyService: true, - ownerID: ownerID, - } -} - -func instanceToHTTPInstanceSummary(instance *sdtypes.Instance) *sdtypes.HttpInstanceSummary { - if instance == nil { - return nil - } - - return &sdtypes.HttpInstanceSummary{ - InstanceId: instance.Id, - Attributes: instance.Attributes, - } -} - -func namespaceToNamespaceSummary(namespace *sdtypes.Namespace) *sdtypes.NamespaceSummary { - if namespace == nil { - return nil - } - - return &sdtypes.NamespaceSummary{ - Id: namespace.Id, - Type: namespace.Type, - Name: namespace.Name, - Arn: namespace.Arn, - } -} - -func serviceToServiceSummary(service *sdtypes.Service) *sdtypes.ServiceSummary { - if service == nil { - return nil - } - - return &sdtypes.ServiceSummary{ - Arn: service.Arn, - CreateDate: service.CreateDate, - Description: service.Description, - DnsConfig: service.DnsConfig, - HealthCheckConfig: service.HealthCheckConfig, - HealthCheckCustomConfig: service.HealthCheckCustomConfig, - Id: service.Id, - InstanceCount: service.InstanceCount, - Name: service.Name, - Type: service.Type, - } -} - func TestAWSSDProvider_Records(t *testing.T) { namespaces := map[string]*sdtypes.Namespace{ "private": { @@ -324,6 +95,19 @@ func TestAWSSDProvider_Records(t *testing.T) { }}, }, }, + "aaaa-srv-not-managed-without-owner-id": { + Id: aws.String("aaaa-srv"), + Name: aws.String("service5"), + Description: nil, + DnsConfig: &sdtypes.DnsConfig{ + NamespaceId: aws.String("private"), + RoutingPolicy: sdtypes.RoutingPolicyWeighted, + DnsRecords: []sdtypes.DnsRecord{{ + Type: sdtypes.RecordTypeAaaa, + TTL: aws.Int64(100), + }}, + }, + }, }, } @@ -414,9 +198,10 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) { ctx := context.Background() // apply creates - provider.ApplyChanges(ctx, &plan.Changes{ + err := provider.ApplyChanges(ctx, &plan.Changes{ Create: expectedEndpoints, }) + assert.NoError(t, err) // make sure services were created assert.Len(t, api.services["private"], 3) @@ -431,9 +216,10 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) { ctx = context.Background() // apply deletes - provider.ApplyChanges(ctx, &plan.Changes{ + err = provider.ApplyChanges(ctx, &plan.Changes{ Delete: expectedEndpoints, }) + assert.NoError(t, err) // make sure all instances are gone endpoints, _ = provider.Records(ctx) @@ -616,7 +402,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) { provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "") // A type - provider.CreateService(context.Background(), aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{ + _, err := provider.CreateService(context.Background(), aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{ Labels: map[string]string{ endpoint.AWSSDDescriptionLabel: "A-srv", }, @@ -624,6 +410,8 @@ func TestAWSSDProvider_CreateService(t *testing.T) { RecordTTL: 60, Targets: endpoint.Targets{"1.2.3.4"}, }) + assert.NoError(t, err) + expectedServices["A-srv"] = &sdtypes.Service{ Name: aws.String("A-srv"), Description: aws.String("A-srv"), @@ -638,7 +426,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) { } // AAAA type - provider.CreateService(context.Background(), aws.String("private"), aws.String("AAAA-srv"), &endpoint.Endpoint{ + _, err = provider.CreateService(context.Background(), aws.String("private"), aws.String("AAAA-srv"), &endpoint.Endpoint{ Labels: map[string]string{ endpoint.AWSSDDescriptionLabel: "AAAA-srv", }, @@ -646,6 +434,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) { RecordTTL: 60, Targets: endpoint.Targets{"::1234:5678:"}, }) + assert.NoError(t, err) expectedServices["AAAA-srv"] = &sdtypes.Service{ Name: aws.String("AAAA-srv"), Description: aws.String("AAAA-srv"), @@ -660,7 +449,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) { } // CNAME type - provider.CreateService(context.Background(), aws.String("private"), aws.String("CNAME-srv"), &endpoint.Endpoint{ + _, err = provider.CreateService(context.Background(), aws.String("private"), aws.String("CNAME-srv"), &endpoint.Endpoint{ Labels: map[string]string{ endpoint.AWSSDDescriptionLabel: "CNAME-srv", }, @@ -668,6 +457,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) { RecordTTL: 80, Targets: endpoint.Targets{"cname.target.com"}, }) + assert.NoError(t, err) expectedServices["CNAME-srv"] = &sdtypes.Service{ Name: aws.String("CNAME-srv"), Description: aws.String("CNAME-srv"), @@ -682,7 +472,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) { } // ALIAS type - provider.CreateService(context.Background(), aws.String("private"), aws.String("ALIAS-srv"), &endpoint.Endpoint{ + _, err = provider.CreateService(context.Background(), aws.String("private"), aws.String("ALIAS-srv"), &endpoint.Endpoint{ Labels: map[string]string{ endpoint.AWSSDDescriptionLabel: "ALIAS-srv", }, @@ -690,6 +480,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) { RecordTTL: 100, Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"}, }) + assert.NoError(t, err) expectedServices["ALIAS-srv"] = &sdtypes.Service{ Name: aws.String("ALIAS-srv"), Description: aws.String("ALIAS-srv"), @@ -703,21 +494,68 @@ func TestAWSSDProvider_CreateService(t *testing.T) { NamespaceId: aws.String("private"), } - validateAWSSDServicesMapsEqual(t, expectedServices, api.services["private"]) + testHelperAWSSDServicesMapsEqual(t, expectedServices, api.services["private"]) } -func validateAWSSDServicesMapsEqual(t *testing.T, expected map[string]*sdtypes.Service, services map[string]*sdtypes.Service) { - require.Len(t, services, len(expected)) - - for _, srv := range services { - validateAWSSDServicesEqual(t, expected[*srv.Name], srv) +func TestAWSSDProvider_CreateServiceDryRun(t *testing.T) { + namespaces := map[string]*sdtypes.Namespace{ + "private": { + Id: aws.String("private"), + Name: aws.String("private.com"), + Type: sdtypes.NamespaceTypeDnsPrivate, + }, } + + api := &AWSSDClientStub{ + namespaces: namespaces, + services: make(map[string]map[string]*sdtypes.Service), + } + + provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "") + provider.dryRun = true + + service, err := provider.CreateService(context.Background(), aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{ + Labels: map[string]string{ + endpoint.AWSSDDescriptionLabel: "A-srv", + }, + RecordType: endpoint.RecordTypeA, + RecordTTL: 60, + Targets: endpoint.Targets{"1.2.3.4"}, + }) + assert.NoError(t, err) + + assert.NotNil(t, service) + assert.Equal(t, "dry-run-service", *service.Name) } -func validateAWSSDServicesEqual(t *testing.T, expected *sdtypes.Service, srv *sdtypes.Service) { - assert.Equal(t, *expected.Description, *srv.Description) - assert.Equal(t, *expected.Name, *srv.Name) - assert.True(t, reflect.DeepEqual(*expected.DnsConfig, *srv.DnsConfig)) +func TestAWSSDProvider_CreateService_LabelNotSet(t *testing.T) { + namespaces := map[string]*sdtypes.Namespace{ + "private": { + Id: aws.String("private"), + Name: aws.String("private.com"), + Type: sdtypes.NamespaceTypeDnsPrivate, + }, + } + + api := &AWSSDClientStub{ + namespaces: namespaces, + services: make(map[string]map[string]*sdtypes.Service), + } + + provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "owner-123") + + service, err := provider.CreateService(context.Background(), aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{ + Labels: map[string]string{ + "wrong-unsupported-label": "A-srv", + }, + RecordType: endpoint.RecordTypeA, + RecordTTL: 60, + Targets: endpoint.Targets{"1.2.3.4"}, + }) + + assert.NoError(t, err) + assert.NotNil(t, service) + assert.Empty(t, *service.Description) } func TestAWSSDProvider_UpdateService(t *testing.T) { @@ -754,14 +592,63 @@ func TestAWSSDProvider_UpdateService(t *testing.T) { provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "") // update service with different TTL - provider.UpdateService(context.Background(), services["private"]["srv1"], &endpoint.Endpoint{ + err := provider.UpdateService(context.Background(), services["private"]["srv1"], &endpoint.Endpoint{ RecordType: endpoint.RecordTypeA, RecordTTL: 100, }) + assert.NoError(t, err) + assert.Len(t, api.services["private"], 1) assert.Equal(t, int64(100), *api.services["private"]["srv1"].DnsConfig.DnsRecords[0].TTL) } +func TestAWSSDProvider_UpdateService_DryRun(t *testing.T) { + namespaces := map[string]*sdtypes.Namespace{ + "private": { + Id: aws.String("private"), + Name: aws.String("private.com"), + Type: sdtypes.NamespaceTypeDnsPrivate, + }, + } + + services := map[string]map[string]*sdtypes.Service{ + "private": { + "srv1": { + Id: aws.String("srv1"), + Name: aws.String("service1"), + NamespaceId: aws.String("private"), + DnsConfig: &sdtypes.DnsConfig{ + RoutingPolicy: sdtypes.RoutingPolicyMultivalue, + DnsRecords: []sdtypes.DnsRecord{{ + Type: sdtypes.RecordTypeA, + TTL: aws.Int64(60), + }}, + }, + }, + }, + } + + api := &AWSSDClientStub{ + namespaces: namespaces, + services: services, + } + + provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "") + provider.dryRun = true + + // update service with different TTL + err := provider.UpdateService(context.Background(), services["private"]["srv1"], &endpoint.Endpoint{ + RecordType: endpoint.RecordTypeAAAA, + RecordTTL: 100, + }) + + assert.NoError(t, err) + assert.Len(t, api.services["private"], 1) + // records should not be updated + assert.NotEqual(t, 100, api.services["private"]["srv1"].DnsConfig.DnsRecords[0].TTL) + assert.NotEqual(t, endpoint.RecordTypeAAAA, api.services["private"]["srv1"].DnsConfig.DnsRecords[0].Type) +} + func TestAWSSDProvider_DeleteService(t *testing.T) { namespaces := map[string]*sdtypes.Namespace{ "private": { @@ -791,6 +678,12 @@ func TestAWSSDProvider_DeleteService(t *testing.T) { Name: aws.String("service3"), NamespaceId: aws.String("private"), }, + "srv4": { + Id: aws.String("srv4"), + Description: nil, + Name: aws.String("service4"), + NamespaceId: aws.String("private"), + }, }, } @@ -803,24 +696,105 @@ func TestAWSSDProvider_DeleteService(t *testing.T) { // delete first service err := provider.DeleteService(context.Background(), services["private"]["srv1"]) - require.NoError(t, err) - assert.Len(t, api.services["private"], 2) + assert.NoError(t, err) + assert.Len(t, api.services["private"], 3) // delete third service - err1 := provider.DeleteService(context.Background(), services["private"]["srv3"]) - require.NoError(t, err1) - assert.Len(t, api.services["private"], 1) + err = provider.DeleteService(context.Background(), services["private"]["srv3"]) + assert.NoError(t, err) + assert.Len(t, api.services["private"], 2) - expectedServices := map[string]*sdtypes.Service{ + // delete service with no description + err = provider.DeleteService(context.Background(), services["private"]["srv4"]) + assert.NoError(t, err) + + expected := map[string]*sdtypes.Service{ "srv2": { Id: aws.String("srv2"), Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"), Name: aws.String("service2"), NamespaceId: aws.String("private"), }, + "srv4": { + Id: aws.String("srv4"), + Description: nil, + Name: aws.String("service4"), + NamespaceId: aws.String("private"), + }, } - assert.Equal(t, expectedServices, api.services["private"]) + assert.Equal(t, expected, api.services["private"]) +} + +func TestAWSSDProvider_DeleteServiceEmptyDescription_Logging(t *testing.T) { + namespaces := map[string]*sdtypes.Namespace{ + "private": { + Id: aws.String("private"), + Name: aws.String("private.com"), + Type: sdtypes.NamespaceTypeDnsPrivate, + }, + } + + services := map[string]map[string]*sdtypes.Service{ + "private": { + "srv1": { + Id: aws.String("srv1"), + Description: nil, + Name: aws.String("service1"), + NamespaceId: aws.String("private"), + }, + }, + } + + logs := testutils.LogsUnderTestWithLogLevel(log.DebugLevel, t) + + api := &AWSSDClientStub{ + namespaces: namespaces, + services: services, + } + + provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "owner-id") + + // delete service + err := provider.DeleteService(context.Background(), services["private"]["srv1"]) + assert.NoError(t, err) + assert.Len(t, api.services["private"], 1) + + testutils.TestHelperLogContainsWithLogLevel("Skipping service removal \"service1\" because owner id (service.Description) not set, when should be", log.DebugLevel, logs, t) +} + +func TestAWSSDProvider_DeleteServiceDryRun(t *testing.T) { + namespaces := map[string]*sdtypes.Namespace{ + "private": { + Id: aws.String("private"), + Name: aws.String("private.com"), + Type: sdtypes.NamespaceTypeDnsPrivate, + }, + } + + services := map[string]map[string]*sdtypes.Service{ + "private": { + "srv1": { + Id: aws.String("srv1"), + Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"), + Name: aws.String("service1"), + NamespaceId: aws.String("private"), + }, + }, + } + + api := &AWSSDClientStub{ + namespaces: namespaces, + services: services, + } + + provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "owner-id") + provider.dryRun = true + + // delete first service + err := provider.DeleteService(context.Background(), services["private"]["srv1"]) + assert.NoError(t, err) + assert.Len(t, api.services["private"], 1) } func TestAWSSDProvider_RegisterInstance(t *testing.T) { @@ -897,12 +871,13 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) { expectedInstances := make(map[string]*sdtypes.Instance) // IPv4-based instance - provider.RegisterInstance(context.Background(), services["private"]["a-srv"], &endpoint.Endpoint{ + err := provider.RegisterInstance(context.Background(), services["private"]["a-srv"], &endpoint.Endpoint{ RecordType: endpoint.RecordTypeA, DNSName: "service1.private.com.", RecordTTL: 300, Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"}, }) + assert.NoError(t, err) expectedInstances["1.2.3.4"] = &sdtypes.Instance{ Id: aws.String("1.2.3.4"), Attributes: map[string]string{ @@ -917,12 +892,13 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) { } // AWS ELB instance (ALIAS) - provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{ + err = provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{ RecordType: endpoint.RecordTypeCNAME, DNSName: "service1.private.com.", RecordTTL: 300, Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com", "load-balancer.us-west-2.elb.amazonaws.com"}, }) + assert.NoError(t, err) expectedInstances["load-balancer.us-east-1.elb.amazonaws.com"] = &sdtypes.Instance{ Id: aws.String("load-balancer.us-east-1.elb.amazonaws.com"), Attributes: map[string]string{ diff --git a/provider/awssd/fixtures_test.go b/provider/awssd/fixtures_test.go new file mode 100644 index 000000000..8d949632f --- /dev/null +++ b/provider/awssd/fixtures_test.go @@ -0,0 +1,275 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awssd + +import ( + "context" + "errors" + "math/rand" + "reflect" + "strconv" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/servicediscovery" + "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/external-dns/endpoint" + + sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery" + sdtypes "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types" +) + +var ( + // Compile time checks for interface conformance + _ AWSSDClient = &AWSSDClientStub{} + ErrNamespaceNotFound = errors.New("namespace not found") +) + +type AWSSDClientStub struct { + // map[namespace_id]namespace + namespaces map[string]*types.Namespace + + // map[namespace_id] => map[service_id]instance + services map[string]map[string]*types.Service + + // map[service_id] => map[inst_id]instance + instances map[string]map[string]*types.Instance + + // []inst_id + deregistered []string +} + +func (s *AWSSDClientStub) CreateService(_ context.Context, input *servicediscovery.CreateServiceInput, _ ...func(*servicediscovery.Options)) (*servicediscovery.CreateServiceOutput, error) { + srv := &types.Service{ + Id: aws.String(strconv.Itoa(rand.Intn(10000))), + DnsConfig: input.DnsConfig, + Name: input.Name, + Description: input.Description, + CreateDate: aws.Time(time.Now()), + CreatorRequestId: input.CreatorRequestId, + } + + nsServices, ok := s.services[*input.NamespaceId] + if !ok { + nsServices = make(map[string]*types.Service) + s.services[*input.NamespaceId] = nsServices + } + nsServices[*srv.Id] = srv + + return &servicediscovery.CreateServiceOutput{ + Service: srv, + }, nil +} + +func (s *AWSSDClientStub) DeregisterInstance(_ context.Context, input *servicediscovery.DeregisterInstanceInput, _ ...func(options *servicediscovery.Options)) (*servicediscovery.DeregisterInstanceOutput, error) { + serviceInstances := s.instances[*input.ServiceId] + delete(serviceInstances, *input.InstanceId) + s.deregistered = append(s.deregistered, *input.InstanceId) + + return &servicediscovery.DeregisterInstanceOutput{}, nil +} + +func (s *AWSSDClientStub) GetService(_ context.Context, input *servicediscovery.GetServiceInput, _ ...func(options *servicediscovery.Options)) (*servicediscovery.GetServiceOutput, error) { + for _, entry := range s.services { + srv, ok := entry[*input.Id] + if ok { + return &servicediscovery.GetServiceOutput{ + Service: srv, + }, nil + } + } + + return nil, errors.New("service not found") +} + +func (s *AWSSDClientStub) DiscoverInstances(_ context.Context, input *sd.DiscoverInstancesInput, _ ...func(options *sd.Options)) (*sd.DiscoverInstancesOutput, error) { + instances := make([]sdtypes.HttpInstanceSummary, 0) + + var foundNs bool + for _, ns := range s.namespaces { + if *ns.Name == *input.NamespaceName { + foundNs = true + + for _, srv := range s.services[*ns.Id] { + if *srv.Name == *input.ServiceName { + for _, inst := range s.instances[*srv.Id] { + instances = append(instances, *instanceToHTTPInstanceSummary(inst)) + } + } + } + } + } + + if !foundNs { + return nil, ErrNamespaceNotFound + } + + return &sd.DiscoverInstancesOutput{ + Instances: instances, + }, nil +} + +func (s *AWSSDClientStub) ListNamespaces(_ context.Context, input *sd.ListNamespacesInput, _ ...func(options *sd.Options)) (*sd.ListNamespacesOutput, error) { + namespaces := make([]sdtypes.NamespaceSummary, 0) + + for _, ns := range s.namespaces { + if len(input.Filters) > 0 && input.Filters[0].Name == sdtypes.NamespaceFilterNameType { + if ns.Type != sdtypes.NamespaceType(input.Filters[0].Values[0]) { + // skip namespaces not matching filter + continue + } + } + namespaces = append(namespaces, *namespaceToNamespaceSummary(ns)) + } + + return &sd.ListNamespacesOutput{ + Namespaces: namespaces, + }, nil +} + +func (s *AWSSDClientStub) ListServices(_ context.Context, input *sd.ListServicesInput, _ ...func(options *sd.Options)) (*sd.ListServicesOutput, error) { + services := make([]sdtypes.ServiceSummary, 0) + + // get namespace filter + if len(input.Filters) == 0 || input.Filters[0].Name != sdtypes.ServiceFilterNameNamespaceId { + return nil, errors.New("missing namespace filter") + } + nsID := input.Filters[0].Values[0] + + for _, srv := range s.services[nsID] { + services = append(services, *serviceToServiceSummary(srv)) + } + + return &sd.ListServicesOutput{ + Services: services, + }, nil +} + +func (s *AWSSDClientStub) RegisterInstance(ctx context.Context, input *sd.RegisterInstanceInput, _ ...func(options *sd.Options)) (*sd.RegisterInstanceOutput, error) { + srvInstances, ok := s.instances[*input.ServiceId] + if !ok { + srvInstances = make(map[string]*sdtypes.Instance) + s.instances[*input.ServiceId] = srvInstances + } + + srvInstances[*input.InstanceId] = &sdtypes.Instance{ + Id: input.InstanceId, + Attributes: input.Attributes, + CreatorRequestId: input.CreatorRequestId, + } + + return &sd.RegisterInstanceOutput{}, nil +} + +func (s *AWSSDClientStub) UpdateService(ctx context.Context, input *sd.UpdateServiceInput, _ ...func(options *sd.Options)) (*sd.UpdateServiceOutput, error) { + out, err := s.GetService(ctx, &sd.GetServiceInput{Id: input.Id}) + if err != nil { + return nil, err + } + + origSrv := out.Service + updateSrv := input.Service + + origSrv.Description = updateSrv.Description + origSrv.DnsConfig.DnsRecords = updateSrv.DnsConfig.DnsRecords + + return &sd.UpdateServiceOutput{}, nil +} + +func (s *AWSSDClientStub) DeleteService(ctx context.Context, input *sd.DeleteServiceInput, _ ...func(options *sd.Options)) (*sd.DeleteServiceOutput, error) { + out, err := s.GetService(ctx, &sd.GetServiceInput{Id: input.Id}) + if err != nil { + return nil, err + } + + service := out.Service + namespace := s.services[*service.NamespaceId] + delete(namespace, *input.Id) + + return &sd.DeleteServiceOutput{}, nil +} + +func newTestAWSSDProvider(api AWSSDClient, domainFilter endpoint.DomainFilter, namespaceTypeFilter, ownerID string) *AWSSDProvider { + return &AWSSDProvider{ + client: api, + dryRun: false, + namespaceFilter: domainFilter, + namespaceTypeFilter: newSdNamespaceFilter(namespaceTypeFilter), + cleanEmptyService: true, + ownerID: ownerID, + } +} + +func instanceToHTTPInstanceSummary(instance *sdtypes.Instance) *sdtypes.HttpInstanceSummary { + if instance == nil { + return nil + } + + return &sdtypes.HttpInstanceSummary{ + InstanceId: instance.Id, + Attributes: instance.Attributes, + } +} + +func namespaceToNamespaceSummary(namespace *sdtypes.Namespace) *sdtypes.NamespaceSummary { + if namespace == nil { + return nil + } + + return &sdtypes.NamespaceSummary{ + Id: namespace.Id, + Type: namespace.Type, + Name: namespace.Name, + Arn: namespace.Arn, + } +} + +func serviceToServiceSummary(service *sdtypes.Service) *sdtypes.ServiceSummary { + if service == nil { + return nil + } + + return &sdtypes.ServiceSummary{ + Arn: service.Arn, + CreateDate: service.CreateDate, + Description: service.Description, + DnsConfig: service.DnsConfig, + HealthCheckConfig: service.HealthCheckConfig, + HealthCheckCustomConfig: service.HealthCheckCustomConfig, + Id: service.Id, + InstanceCount: service.InstanceCount, + Name: service.Name, + Type: service.Type, + } +} + +func testHelperAWSSDServicesMapsEqual(t *testing.T, expected map[string]*sdtypes.Service, services map[string]*sdtypes.Service) { + require.Len(t, services, len(expected)) + + for _, srv := range services { + testHelperAWSSDServicesEqual(t, expected[*srv.Name], srv) + } +} + +func testHelperAWSSDServicesEqual(t *testing.T, expected *sdtypes.Service, srv *sdtypes.Service) { + assert.Equal(t, *expected.Description, *srv.Description) + assert.Equal(t, *expected.Name, *srv.Name) + assert.True(t, reflect.DeepEqual(*expected.DnsConfig, *srv.DnsConfig)) +} diff --git a/source/informers/informers_test.go b/source/informers/informers_test.go index 2268efb38..8d5fd05b0 100644 --- a/source/informers/informers_test.go +++ b/source/informers/informers_test.go @@ -92,17 +92,17 @@ func TestWaitForDynamicCacheSync(t *testing.T) { }{ { name: "all caches synced", - syncResults: map[schema.GroupVersionResource]bool{schema.GroupVersionResource{}: true}, + syncResults: map[schema.GroupVersionResource]bool{{}: true}, }, { name: "some caches not synced", - syncResults: map[schema.GroupVersionResource]bool{schema.GroupVersionResource{}: false}, + syncResults: map[schema.GroupVersionResource]bool{{}: false}, expectError: true, errorMsg: "failed to sync string with timeout 1m0s", }, { name: "context timeout", - syncResults: map[schema.GroupVersionResource]bool{schema.GroupVersionResource{}: false}, + syncResults: map[schema.GroupVersionResource]bool{{}: false}, expectError: true, errorMsg: "failed to sync string with timeout 1m0s", }, From 17de6134ca89d39917e833b4e058a5cf870cc957 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Thu, 29 May 2025 22:28:17 +0200 Subject: [PATCH 33/33] chore(ci): rework label in OWNERS (#5481) --- OWNERS | 29 ----------------------------- docs/OWNERS | 4 ++++ provider/OWNERS | 4 ++++ source/OWNERS | 4 ++++ 4 files changed, 12 insertions(+), 29 deletions(-) create mode 100644 docs/OWNERS create mode 100644 provider/OWNERS create mode 100644 source/OWNERS diff --git a/OWNERS b/OWNERS index b9d702e10..0261f6680 100644 --- a/OWNERS +++ b/OWNERS @@ -25,32 +25,3 @@ emeritus_approvers: - linki - njuettner - seanmalloy - -filters: - "source/": - labels: - - source - "provider/aws(|sd)": - labels: - - provider/aws - "provider/azure": - labels: - - provider/azure - "provider/google": - labels: - - provider/google - "provider/coredns": - labels: - - provider/coredns - "provider/rfc2136": - labels: - - provider/rfc2136 - "provider/pdns": - labels: - - provider/powerdns - "provider/cloudflare": - labels: - - provider/cloudflare - "provider/(akamai|alibabacloud|civo|designate|digitalocean|dnsimple|exoscale|gandi|godaddy|linode|ns1|oci|ovh|pihole|plural|scaleway|transip)": - labels: - - provider diff --git a/docs/OWNERS b/docs/OWNERS new file mode 100644 index 000000000..2e6e4d56e --- /dev/null +++ b/docs/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- docs diff --git a/provider/OWNERS b/provider/OWNERS new file mode 100644 index 000000000..4b565139b --- /dev/null +++ b/provider/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- provider diff --git a/source/OWNERS b/source/OWNERS new file mode 100644 index 000000000..ffe27446c --- /dev/null +++ b/source/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- source