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 diff --git a/.golangci.yml b/.golangci.yml index b7498ef48..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 @@ -95,7 +96,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/OWNERS b/OWNERS index 978723e6f..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|ibmcloud|linode|ns1|oci|ovh|pihole|plural|scaleway|tencentcloud|transip|ultradns)": - labels: - - provider diff --git a/README.md b/README.md index d3aa4ec6c..b66de266b 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,9 @@ 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/) +- [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. @@ -91,6 +90,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 | @@ -128,7 +128,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 | | @@ -148,13 +148,11 @@ 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 | -| IBMCloud | Alpha | @hughhuangzh | -| TencentCloud | Alpha | @Hyzhou | | Plural | Alpha | @michaeljguarino | | Pi-hole | Alpha | @tinyzimmer | +| Alibaba Cloud DNS | Alpha | | ## Kubernetes version compatibility @@ -198,6 +196,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) @@ -210,12 +209,9 @@ 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) -- [IBM Cloud](docs/tutorials/ibmcloud.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/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/controller/execute.go b/controller/execute.go index 0c51b0cb9..1749cfc2f 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" @@ -64,9 +63,7 @@ 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" webhookapi "sigs.k8s.io/external-dns/provider/webhook/api" "sigs.k8s.io/external-dns/registry" @@ -189,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": @@ -308,12 +303,8 @@ 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": - 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/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/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/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 a43c0e484..a380b6528 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, 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) | @@ -87,12 +87,10 @@ | `--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) | -| `--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=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: '') | @@ -121,8 +119,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/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 | diff --git a/docs/providers.md b/docs/providers.md index 4a9511aa2..228622939 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 | @@ -29,7 +28,5 @@ 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/cloudflare.md b/docs/tutorials/cloudflare.md index abfb95eed..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,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 `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. Requires [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) product and "SSL and Certificates" API permission. 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/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/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/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/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/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/go.mod b/go.mod index cb505369d..ffb29504f 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,11 +52,7 @@ 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 go.uber.org/ratelimit v0.3.1 golang.org/x/net v0.40.0 @@ -76,6 +68,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 +82,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 @@ -109,20 +101,12 @@ 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/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,17 +137,14 @@ 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 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 - 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 @@ -180,14 +161,12 @@ 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 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 +187,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..798796933 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= @@ -306,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= @@ -321,8 +311,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 +348,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 +380,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 +391,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 +608,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 +660,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= @@ -766,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= @@ -799,7 +769,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,16 +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/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= 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= @@ -1047,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= @@ -1092,8 +1049,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/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.", }, diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 35d562225..73b36ce12 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -199,10 +199,6 @@ type Config struct { GoDaddyTTL int64 GoDaddyOTE bool OCPRouterName string - IBMCloudProxied bool - IBMCloudConfigFile string - TencentCloudConfigFile string - TencentCloudZoneType string PiholeServer string PiholePassword string `secure:"yes"` PiholeTLSInsecureSkipVerify bool @@ -254,7 +250,7 @@ var defaultConfig = &Config{ CFAPIEndpoint: "", CFPassword: "", CFUsername: "", - CloudflareCustomHostnamesCertificateAuthority: "google", + CloudflareCustomHostnamesCertificateAuthority: "none", CloudflareCustomHostnames: false, CloudflareCustomHostnamesMinTLSVersion: "1.0", CloudflareDNSRecordsPerPage: 100, @@ -293,8 +289,6 @@ var defaultConfig = &Config{ GoogleBatchChangeSize: 1000, GoogleProject: "", GoogleZoneVisibility: "", - IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", - IBMCloudProxied: false, IgnoreHostnameAnnotation: false, IgnoreIngressRulesSpec: false, IgnoreIngressTLSSpec: false, @@ -360,8 +354,6 @@ var defaultConfig = &Config{ SkipperRouteGroupVersion: "zalando.org/v1", Sources: nil, TargetNetFilter: []string{}, - TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", - TencentCloudZoneType: "", TLSCA: "", TLSClientCert: "", TLSClientCertKey: "", @@ -495,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", "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", "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) @@ -532,13 +524,11 @@ 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) 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 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.CloudflareDNSRecordsComment) @@ -568,8 +558,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..357672dc3 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: "none", CloudflareDNSRecordsPerPage: 100, CloudflareDNSRecordsComment: "", CloudflareRegionKey: "", @@ -125,10 +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", WebhookProviderURL: "http://localhost:8888", WebhookProviderReadTimeout: 5 * time.Second, @@ -242,10 +238,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", WebhookProviderURL: "http://localhost:8888", WebhookProviderReadTimeout: 5 * time.Second, @@ -396,10 +388,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", }, envVars: map[string]string{}, expected: overriddenConfig, @@ -516,10 +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_IBMCLOUD_PROXIED": "1", - "EXTERNAL_DNS_IBMCLOUD_CONFIG_FILE": "ibmcloud.json", - "EXTERNAL_DNS_TENCENT_CLOUD_CONFIG_FILE": "tencent-cloud.json", - "EXTERNAL_DNS_TENCENT_CLOUD_ZONE_TYPE": "private", }, expected: overriddenConfig, }, diff --git a/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) +} 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/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/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/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) { diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index ae5615e93..3d3e8de18 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) } @@ -267,7 +254,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 @@ -290,22 +277,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{ @@ -558,129 +529,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 @@ -864,7 +712,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) @@ -882,13 +730,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 @@ -922,7 +763,7 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End Comment: comment, Tags: p.DNSRecordsConfig.validTags(ep.DNSName, p.ZoneHasPaidPlan), }, - RegionalHostname: regionalHostname, + RegionalHostname: p.regionalHostname(ep), CustomHostnamesPrev: prevCustomHostnames, CustomHostnames: newCustomHostnames, } @@ -998,15 +839,20 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte } func getCustomHostnamesSSLOptions(customHostnamesConfig CustomHostnamesConfig) *cloudflare.CustomHostnameSSL { - return &cloudflare.CustomHostnameSSL{ - Type: "dv", - Method: "http", - CertificateAuthority: customHostnamesConfig.CertificateAuthority, - BundleMethod: "ubiquitous", + ssl := &cloudflare.CustomHostnameSSL{ + Type: "dv", + Method: "http", + 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 != "none" { + ssl.CertificateAuthority = customHostnamesConfig.CertificateAuthority + } + return ssl } func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool { @@ -1030,19 +876,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..0a79647f6 --- /dev/null +++ b/provider/cloudflare/cloudflare_regional_test.go @@ -0,0 +1,374 @@ +/* +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, tt.want, got) + }) + } +} + +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 0e8317246..e84295f97 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" @@ -3054,337 +3053,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{ diff --git a/provider/gandi/gandi_test.go b/provider/gandi/gandi_test.go index 6442cc3cb..72c73aeb4 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}} 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/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/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/rfc2136/rfc2136.go b/provider/rfc2136/rfc2136.go index 522d86905..cb7245db0 100644 --- a/provider/rfc2136/rfc2136.go +++ b/provider/rfc2136/rfc2136.go @@ -306,7 +306,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 da14ae525..fb5016ed9 100644 --- a/provider/rfc2136/rfc2136_test.go +++ b/provider/rfc2136/rfc2136_test.go @@ -153,7 +153,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) }() @@ -161,7 +178,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, @@ -169,7 +186,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) { @@ -506,7 +523,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()) 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) - } - } -} 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)) -} 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) +} 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 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/annotations/annotations.go b/source/annotations/annotations.go index 826a949d7..9e06cf2d5 100644 --- a/source/annotations/annotations.go +++ b/source/annotations/annotations.go @@ -27,7 +27,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{ 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/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() 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..8d5fd05b0 --- /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{{}: true}, + }, + { + name: "some caches not synced", + syncResults: map[schema.GroupVersionResource]bool{{}: false}, + expectError: true, + errorMsg: "failed to sync string with timeout 1m0s", + }, + { + name: "context timeout", + syncResults: map[schema.GroupVersionResource]bool{{}: 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 }