Merge branch 'master' of github.com:7onn/external-dns into 7onn/cloudflare-tags

This commit is contained in:
Tom M G 2025-05-31 23:28:28 +02:00
commit 2177040b5f
No known key found for this signature in database
GPG Key ID: 90674BFE48C97FB0
89 changed files with 2430 additions and 7676 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

29
OWNERS
View File

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

View File

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

17
apis/api.go Normal file
View File

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

20
apis/v1alpha1/api.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

4
docs/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- docs

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: <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: <custom hostname>` annotation.
Multiple hostnames are supported via a comma-separated list: `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: <custom hostname 1>,<custom hostname 2>`.
@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

27
go.mod
View File

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

45
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

4
provider/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- provider

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

4
source/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- source

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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