mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-07 01:56:57 +02:00
Merge branch 'master' of github.com:7onn/external-dns into 7onn/cloudflare-tags
This commit is contained in:
commit
2177040b5f
2
.github/workflows/dependency-update.yaml
vendored
2
.github/workflows/dependency-update.yaml
vendored
@ -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 }}
|
||||
|
4
.github/workflows/lint-test-chart.yaml
vendored
4
.github/workflows/lint-test-chart.yaml
vendored
@ -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
|
||||
|
2
.github/workflows/lint.yaml
vendored
2
.github/workflows/lint.yaml
vendored
@ -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
|
||||
|
@ -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$
|
||||
|
3
Makefile
3
Makefile
@ -66,7 +66,8 @@ lint: licensecheck go-lint oas-lint
|
||||
#? crd: Generates CRD using controller-gen and copy it into chart
|
||||
.PHONY: crd
|
||||
crd: controller-gen-install
|
||||
${CONTROLLER_GEN} crd:crdVersions=v1 paths="./endpoint/..." output:crd:stdout > config/crd/standard/dnsendpoint.yaml
|
||||
${CONTROLLER_GEN} object crd:crdVersions=v1 paths="./endpoint/..."
|
||||
${CONTROLLER_GEN} object crd:crdVersions=v1 paths="./apis/..." output:crd:stdout > config/crd/standard/dnsendpoint.yaml
|
||||
cp -f config/crd/standard/dnsendpoint.yaml charts/external-dns/crds/dnsendpoint.yaml
|
||||
|
||||
#? test: The verify target runs tasks similar to the CI tasks, but without code coverage
|
||||
|
29
OWNERS
29
OWNERS
@ -25,32 +25,3 @@ emeritus_approvers:
|
||||
- linki
|
||||
- njuettner
|
||||
- seanmalloy
|
||||
|
||||
filters:
|
||||
"source/":
|
||||
labels:
|
||||
- source
|
||||
"provider/aws(|sd)":
|
||||
labels:
|
||||
- provider/aws
|
||||
"provider/azure":
|
||||
labels:
|
||||
- provider/azure
|
||||
"provider/google":
|
||||
labels:
|
||||
- provider/google
|
||||
"provider/coredns":
|
||||
labels:
|
||||
- provider/coredns
|
||||
"provider/rfc2136":
|
||||
labels:
|
||||
- provider/rfc2136
|
||||
"provider/pdns":
|
||||
labels:
|
||||
- provider/powerdns
|
||||
"provider/cloudflare":
|
||||
labels:
|
||||
- provider/cloudflare
|
||||
"provider/(akamai|alibabacloud|civo|designate|digitalocean|dnsimple|exoscale|gandi|godaddy|ibmcloud|linode|ns1|oci|ovh|pihole|plural|scaleway|tencentcloud|transip|ultradns)":
|
||||
labels:
|
||||
- provider
|
||||
|
14
README.md
14
README.md
@ -63,10 +63,9 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz
|
||||
- [GoDaddy](https://www.godaddy.com)
|
||||
- [Gandi](https://www.gandi.net)
|
||||
- [IBM Cloud DNS](https://www.ibm.com/cloud/dns)
|
||||
- [TencentCloud PrivateDNS](https://cloud.tencent.com/product/privatedns)
|
||||
- [TencentCloud DNSPod](https://cloud.tencent.com/product/cns)
|
||||
- [Plural](https://www.plural.sh/)
|
||||
- [Pi-hole](https://pi-hole.net/)
|
||||
- [Alibaba Cloud DNS](https://www.alibabacloud.com/help/en/dns)
|
||||
|
||||
ExternalDNS is, by default, aware of the records it is managing, therefore it can safely manage non-empty hosted zones.
|
||||
We strongly encourage you to set `--txt-owner-id` to a unique value that doesn't change for the lifetime of your cluster.
|
||||
@ -91,6 +90,7 @@ Known providers using webhooks:
|
||||
| Anexia | https://github.com/anexia/k8s-external-dns-webhook |
|
||||
| Bizfly Cloud | https://github.com/bizflycloud/external-dns-bizflycloud-webhook |
|
||||
| ClouDNS | https://github.com/rwunderer/external-dns-cloudns-webhook |
|
||||
| deSEC | https://github.com/michelangelomo/external-dns-desec-provider |
|
||||
| Dreamhost | https://github.com/asymingt/external-dns-dreamhost-webhook |
|
||||
| Efficient IP | https://github.com/EfficientIP-Labs/external-dns-efficientip-webhook |
|
||||
| Gcore | https://github.com/G-Core/external-dns-gcore-webhook |
|
||||
@ -128,7 +128,7 @@ We define the following stability levels for providers:
|
||||
The following table clarifies the current status of the providers according to the aforementioned stability levels:
|
||||
|
||||
| Provider | Status | Maintainers |
|
||||
| ------------------------------- | ------ | ---------------- |
|
||||
|---------------------------------| ------ |------------------|
|
||||
| Google Cloud DNS | Stable | |
|
||||
| AWS Route 53 | Stable | |
|
||||
| AWS Cloud Map | Beta | |
|
||||
@ -148,13 +148,11 @@ The following table clarifies the current status of the providers according to t
|
||||
| TransIP | Alpha | |
|
||||
| OVHcloud | Beta | @rbeuque74 |
|
||||
| Scaleway DNS | Alpha | @Sh4d1 |
|
||||
| UltraDNS | Alpha | |
|
||||
| GoDaddy | Alpha | |
|
||||
| Gandi | Alpha | @packi |
|
||||
| IBMCloud | Alpha | @hughhuangzh |
|
||||
| TencentCloud | Alpha | @Hyzhou |
|
||||
| Plural | Alpha | @michaeljguarino |
|
||||
| Pi-hole | Alpha | @tinyzimmer |
|
||||
| Alibaba Cloud DNS | Alpha | |
|
||||
|
||||
## Kubernetes version compatibility
|
||||
|
||||
@ -198,6 +196,7 @@ The following tutorials are provided:
|
||||
- [Using Google's Default Ingress Controller](docs/tutorials/gke.md)
|
||||
- [Using the Nginx Ingress Controller](docs/tutorials/gke-nginx.md)
|
||||
- [Headless Services](docs/tutorials/hostport.md)
|
||||
- [IONOS Cloud](docs/tutorials/ionoscloud.md)
|
||||
- [Istio Gateway Source](docs/sources/istio.md)
|
||||
- [Linode](docs/tutorials/linode.md)
|
||||
- [NS1](docs/tutorials/ns1.md)
|
||||
@ -210,12 +209,9 @@ The following tutorials are provided:
|
||||
- [TransIP](docs/tutorials/transip.md)
|
||||
- [OVHcloud](docs/tutorials/ovh.md)
|
||||
- [Scaleway](docs/tutorials/scaleway.md)
|
||||
- [UltraDNS](docs/tutorials/ultradns.md)
|
||||
- [GoDaddy](docs/tutorials/godaddy.md)
|
||||
- [Gandi](docs/tutorials/gandi.md)
|
||||
- [IBM Cloud](docs/tutorials/ibmcloud.md)
|
||||
- [Nodes as source](docs/sources/nodes.md)
|
||||
- [TencentCloud](docs/tutorials/tencentcloud.md)
|
||||
- [Plural](docs/tutorials/plural.md)
|
||||
- [Pi-hole](docs/tutorials/pihole.md)
|
||||
|
||||
|
17
apis/api.go
Normal file
17
apis/api.go
Normal 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
20
apis/v1alpha1/api.go
Normal 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
|
62
apis/v1alpha1/dnsendpoint.go
Normal file
62
apis/v1alpha1/dnsendpoint.go
Normal 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"`
|
||||
}
|
40
apis/v1alpha1/groupversion_info.go
Normal file
40
apis/v1alpha1/groupversion_info.go
Normal 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{})
|
||||
}
|
110
apis/v1alpha1/zz_generated.deepcopy.go
Normal file
110
apis/v1alpha1/zz_generated.deepcopy.go
Normal 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
|
||||
}
|
@ -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: |-
|
||||
|
@ -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: |-
|
||||
|
@ -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
4
docs/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- docs
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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. |
|
||||
|
@ -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 |
|
||||
|
@ -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 |
|
||||
|
@ -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.
|
||||
|
@ -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.
|
@ -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.
|
@ -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```
|
@ -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:
|
||||
|
@ -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 {
|
||||
|
@ -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
27
go.mod
@ -9,9 +9,6 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0
|
||||
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.19.1
|
||||
github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.7.2
|
||||
github.com/IBM/go-sdk-core/v5 v5.19.1
|
||||
github.com/IBM/networking-go-sdk v0.51.5
|
||||
github.com/Yamashou/gqlgenc v0.32.1
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0
|
||||
@ -44,7 +41,6 @@ require (
|
||||
github.com/linode/linodego v1.50.0
|
||||
github.com/maxatome/go-testdeep v1.14.0
|
||||
github.com/miekg/dns v1.1.66
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/openshift/api v0.0.0-20230607130528-611114dca681
|
||||
github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3
|
||||
github.com/oracle/oci-go-sdk/v65 v65.91.0
|
||||
@ -56,11 +52,7 @@ require (
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1165
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1145
|
||||
github.com/transip/gotransip/v6 v6.26.0
|
||||
github.com/ultradns/ultradns-sdk-go v1.3.7
|
||||
go.etcd.io/etcd/client/v3 v3.6.0
|
||||
go.uber.org/ratelimit v0.3.1
|
||||
golang.org/x/net v0.40.0
|
||||
@ -76,6 +68,7 @@ require (
|
||||
k8s.io/apimachinery v0.33.1
|
||||
k8s.io/client-go v0.33.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
sigs.k8s.io/controller-runtime v0.20.4
|
||||
sigs.k8s.io/gateway-api v1.3.0
|
||||
)
|
||||
|
||||
@ -89,7 +82,6 @@ require (
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
@ -109,20 +101,12 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deepmap/oapi-codegen v1.9.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/errors v0.22.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
@ -153,17 +137,14 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/peterhellberg/link v1.1.0 // indirect
|
||||
@ -180,14 +161,12 @@ require (
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/terra-farm/udnssdk v1.3.5 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.5.25 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
@ -208,13 +187,11 @@ require (
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
|
||||
moul.io/http2curl v1.0.0 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.20.4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||
|
45
go.sum
45
go.sum
@ -51,12 +51,6 @@ github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q
|
||||
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.19.1 h1:NHWjSBeXbL8mlx+0QyCl4OrUvytCZ3nkEIRqX7t97wQ=
|
||||
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.19.1/go.mod h1:JwdtGjHFTmUM1zjzvvCotCCyP55S146IuVPOJZ7D/Jw=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||
github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.7.2 h1:eW5o8NpblAyqPjwOlZ+XISdhlYynjf7B7dsCmsvfC/s=
|
||||
github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.7.2/go.mod h1:HulyrJLLc9FSZlwKQ9vu5Jq83thNlUfg1afonOdhrRA=
|
||||
github.com/IBM/go-sdk-core/v5 v5.19.1 h1:sleVks1O4XjgF4YEGvyDh6PZbP6iZhlTPeDkQc8nWDs=
|
||||
github.com/IBM/go-sdk-core/v5 v5.19.1/go.mod h1:Q3BYO6iDA2zweQPDGbNTtqft5tDcEpm6RTuqMlPcvbw=
|
||||
github.com/IBM/networking-go-sdk v0.51.5 h1:75lKAx17y++hirXK5GcEM23mTRhHnhsv6gmhz70ex1Q=
|
||||
github.com/IBM/networking-go-sdk v0.51.5/go.mod h1:wyEnRnBnROgGmSn5UrryycIrbBujHKXf0PmI1NSwcjY=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
|
||||
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
@ -116,8 +110,6 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
@ -306,8 +298,6 @@ github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwo
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 h1:jmwW6QWvUO2OPe22YfgFvBaaZlSr8Rlrac5lZvG6IdM=
|
||||
@ -321,8 +311,6 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
@ -360,8 +348,6 @@ github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2
|
||||
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
|
||||
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
|
||||
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
|
||||
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
@ -394,8 +380,6 @@ github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pL
|
||||
github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||
github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
|
||||
github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
|
||||
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
|
||||
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
@ -407,20 +391,12 @@ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+
|
||||
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
|
||||
github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
@ -632,8 +608,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
@ -686,8 +660,6 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtB
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
||||
github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
|
||||
@ -766,8 +738,6 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@ -799,7 +769,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
@ -1021,16 +990,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1145/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1165/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166 h1:WgdCsNxde/flDGxOy+dXZqm2wrUQA/4kzaaY69XC/2A=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1165 h1:bHT5sLou90UwrVm7Km2nQ7hyBk0+LK4xi7JOZXoDQ2c=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1165/go.mod h1:6pN5Nh0PuYJ+XHkjiV7SQqXgc1gecB9FHd456W9ZNdE=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1145 h1:K5N0Uxqm9kM7KU6DFBekCTKbldlXq6UD1ekOyXn4zEc=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1145/go.mod h1:mgwxarWzLxIylWtHmkoMg0dX8aqhO5lwfrHkK4IGLKE=
|
||||
github.com/terra-farm/udnssdk v1.3.5 h1:MNR3adfuuEK/l04+jzo8WW/0fnorY+nW515qb3vEr6I=
|
||||
github.com/terra-farm/udnssdk v1.3.5/go.mod h1:8RnM56yZTR7mYyUIvrDgXzdRaEyFIzqdEi7+um26Sv8=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
@ -1047,8 +1006,6 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/ultradns/ultradns-sdk-go v1.3.7 h1:P4CaM+npeXIybbLL27ezR316NnyILI1Y8IvfZtNE+Co=
|
||||
github.com/ultradns/ultradns-sdk-go v1.3.7/go.mod h1:43vmy6GEvRuVMpGEWfJ/JoEM6RIqUQI1/tb8JqZR1zI=
|
||||
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
@ -1092,8 +1049,6 @@ go.etcd.io/etcd/client/v3 v3.6.0/go.mod h1:Jzk/Knqe06pkOZPHXsQ0+vNDvMQrgIqJ0W8Dw
|
||||
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||
go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||
go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||
go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
|
||||
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
|
@ -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.",
|
||||
},
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
4
provider/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- provider
|
@ -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))
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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{
|
||||
|
275
provider/awssd/fixtures_test.go
Normal file
275
provider/awssd/fixtures_test.go
Normal 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))
|
||||
}
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
210
provider/cloudflare/cloudflare_regional.go
Normal file
210
provider/cloudflare/cloudflare_regional.go
Normal 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
|
||||
}
|
374
provider/cloudflare/cloudflare_regional_test.go
Normal file
374
provider/cloudflare/cloudflare_regional_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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{
|
||||
|
@ -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}}
|
||||
|
@ -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
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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"
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
@ -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.
|
||||
|
@ -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
4
source/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- source
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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-"
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
72
source/informers/informers.go
Normal file
72
source/informers/informers.go
Normal 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
|
||||
}
|
126
source/informers/informers_test.go
Normal file
126
source/informers/informers_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user