diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49b563348..5b2cc0e6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,16 @@ on: pull_request: branches: [ master ] +permissions: + contents: read # to fetch code (actions/checkout) + jobs: build: + permissions: + contents: read # to fetch code (actions/checkout) + checks: write # to create a new check based on the results (shogo82148/actions-goveralls) + name: Build runs-on: ubuntu-latest steps: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cfc08b081..f21f176d5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,8 +5,12 @@ on: tags: - "v*" +permissions: {} jobs: release_docs: + permissions: + contents: write # for mike to push + name: Release Docs runs-on: ubuntu-latest steps: diff --git a/.github/workflows/lint-test-chart.yaml b/.github/workflows/lint-test-chart.yaml index 5aa6be865..090399861 100644 --- a/.github/workflows/lint-test-chart.yaml +++ b/.github/workflows/lint-test-chart.yaml @@ -38,7 +38,7 @@ jobs: python-version: "3.x" - name: Set-up chart-testing - uses: helm/chart-testing-action@dae259e86a35ff09145c0805e2d7dd3f7207064a + uses: helm/chart-testing-action@afea100a513515fbd68b0e72a7bb0ae34cb62aec - name: Run chart-testing (list-changed) id: list-changed diff --git a/.github/workflows/release-chart.yaml b/.github/workflows/release-chart.yaml index 299e66b53..21c29e1df 100644 --- a/.github/workflows/release-chart.yaml +++ b/.github/workflows/release-chart.yaml @@ -7,8 +7,13 @@ on: paths: - "charts/external-dns/Chart.yaml" +permissions: {} jobs: release: + + permissions: + contents: write # to push chart release and create a release (helm/chart-releaser-action) + if: github.repository == 'kubernetes-sigs/external-dns' runs-on: ubuntu-latest defaults: @@ -53,7 +58,7 @@ jobs: version: latest - name: Run chart-releaser - uses: helm/chart-releaser-action@a3454e46a6f5ac4811069a381e646961dda2e1bf + uses: helm/chart-releaser-action@98bccfd32b0f76149d188912ac8e45ddd3f8695f env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_RELEASE_NAME_TEMPLATE: "external-dns-helm-chart-{{ .Version }}" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 0c8766e0b..08bb97159 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -1,6 +1,10 @@ name: trivy vulnerability scanner on: push: + +permissions: + contents: read # to fetch code (actions/checkout) + jobs: build: name: Build diff --git a/README.md b/README.md index e16381d2e..5cedc1799 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz * [AWS Cloud Map](https://docs.aws.amazon.com/cloud-map/) * [AzureDNS](https://azure.microsoft.com/en-us/services/dns) * [BlueCat](https://bluecatnetworks.com) +* [Civo](https://www.civo.com) * [CloudFlare](https://www.cloudflare.com/dns) * [RcodeZero](https://www.rcodezero.at/) * [DigitalOcean](https://www.digitalocean.com/products/networking) @@ -101,6 +102,7 @@ The following table clarifies the current status of the providers according to t | Akamai Edge DNS | Beta | | | AzureDNS | Beta | | | BlueCat | Alpha | @seanmalloy @vinny-sabatini | +| Civo | Alpha | @alejandrojnm | | CloudFlare | Beta | | | RcodeZero | Alpha | | | DigitalOcean | Alpha | | @@ -160,6 +162,7 @@ The following tutorials are provided: * [Kube Ingress AWS Controller](docs/tutorials/kube-ingress-aws.md) * [Azure DNS](docs/tutorials/azure.md) * [Azure Private DNS](docs/tutorials/azure-private-dns.md) +* [Civo](docs/tutorials/civo.md) * [Cloudflare](docs/tutorials/cloudflare.md) * [BlueCat](docs/tutorials/bluecat.md) * [CoreDNS](docs/tutorials/coredns.md) diff --git a/docs/tutorials/civo.md b/docs/tutorials/civo.md new file mode 100644 index 000000000..e6b6c1874 --- /dev/null +++ b/docs/tutorials/civo.md @@ -0,0 +1,187 @@ +# Setting up ExternalDNS for Services on Civo + +This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Civo DNS Manager. + +Make sure to use **>0.12.2** version of ExternalDNS for this tutorial. + +## Managing DNS with Civo + +If you want to learn about how to use Civo DNS Manager read the following tutorials: + +[An Introduction to Managing DNS](https://www.civo.com/learn/configure-dns) + +## Get Civo Token + +Copy the token in the settings fo your account + +The environment variable `CIVO_TOKEN` will be needed to run ExternalDNS with Civo. + +## 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.12.2 + 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=civo + env: + - name: CIVO_TOKEN + value: "YOUR_CIVO_API_TOKEN" +``` + +### Manifest (for clusters with RBAC enabled) + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +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/v1beta1 +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.12.2 + 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=civo + env: + - name: CIVO_TOKEN + value: "YOUR_CIVO_API_TOKEN" +``` + +## 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 +``` + +Note the annotation on the service; use the same hostname as the Civo DNS 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. + +Create the deployment and service: + +```console +$ 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 Civo DNS records. + +## Verifying Civo DNS records + +Check your [Civo UI](https://www.civo.com/account/dns) to view the records for your Civo DNS zone. + +Click on the zone for the one created above if a different domain was used. + +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 Civo DNS records, we can delete the tutorial's example: + +``` +$ kubectl delete service -f nginx.yaml +$ kubectl delete service -f externaldns.yaml +``` \ No newline at end of file diff --git a/docs/tutorials/tencentcloud.md b/docs/tutorials/tencentcloud.md index 76033d410..067a3b7c4 100644 --- a/docs/tutorials/tencentcloud.md +++ b/docs/tutorials/tencentcloud.md @@ -1,7 +1,7 @@ # Setting up ExternalDNS for Tencent Cloud ## External Dns Version -* Make sure to use **>=1.7.2** version of ExternalDNS for this tutorial +* Make sure to use **>=0.13.1** version of ExternalDNS for this tutorial ## Set up PrivateDns or DNSPod @@ -101,7 +101,8 @@ data: "regionId": "ap-shanghai", "secretId": "******", "secretKey": "******", - "vpcId": "vpc-******" + "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 @@ -128,7 +129,7 @@ spec: - --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: k8s.gcr.io/external-dns/external-dns:v1.7.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.1 imagePullPolicy: Always name: external-dns resources: {} @@ -139,6 +140,11 @@ spec: 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: {} diff --git a/endpoint/domain_filter.go b/endpoint/domain_filter.go index 6ab76d221..1fe36aee0 100644 --- a/endpoint/domain_filter.go +++ b/endpoint/domain_filter.go @@ -73,9 +73,11 @@ type DomainFilter struct { // prepareFilters provides consistent trimming for filters/exclude params func prepareFilters(filters []string) []string { - fs := make([]string, len(filters)) - for i, domain := range filters { - fs[i] = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(domain), ".")) + var fs []string + for _, filter := range filters { + if domain := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(filter), ".")); domain != "" { + fs = append(fs, domain) + } } return fs } @@ -98,7 +100,7 @@ func NewRegexDomainFilter(regexDomainFilter *regexp.Regexp, regexDomainExclusion // Match checks whether a domain can be found in the DomainFilter. // RegexFilter takes precedence over Filters func (df DomainFilter) Match(domain string) bool { - if df.regex != nil && df.regex.String() != "" { + if df.regex != nil && df.regex.String() != "" || df.regexExclusion != nil && df.regexExclusion.String() != "" { return matchRegex(df.regex, df.regexExclusion, domain) } @@ -113,12 +115,13 @@ func matchFilter(filters []string, domain string, emptyval bool) bool { return emptyval } + strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) for _, filter := range filters { - strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) - if filter == "" { - return emptyval - } else if strings.HasPrefix(filter, ".") && strings.HasSuffix(strippedDomain, filter) { + continue + } + + if strings.HasPrefix(filter, ".") && strings.HasSuffix(strippedDomain, filter) { return true } else if strings.Count(strippedDomain, ".") == strings.Count(filter, ".") { if strippedDomain == filter { @@ -146,35 +149,32 @@ func matchRegex(regex *regexp.Regexp, negativeRegex *regexp.Regexp, domain strin // MatchParent checks wether DomainFilter matches a given parent domain. func (df DomainFilter) MatchParent(domain string) bool { - if !df.IsConfigured() { + if matchFilter(df.exclude, domain, false) { + return false + } + if len(df.Filters) == 0 { return true } + strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) for _, filter := range df.Filters { - if strings.HasPrefix(filter, ".") { + if filter == "" || strings.HasPrefix(filter, ".") { // We don't check parents if the filter is prefixed with "." continue } - - if filter == "" { - return true - } - - strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) - if strings.HasSuffix(filter, "."+strippedDomain) && !matchFilter(df.exclude, domain, false) { + if strings.HasSuffix(filter, "."+strippedDomain) { return true } } - return false } -// IsConfigured returns true if DomainFilter is configured, false otherwise +// IsConfigured returns true if any inclusion or exclusion rules have been specified. func (df DomainFilter) IsConfigured() bool { if df.regex != nil && df.regex.String() != "" { return true - } else if len(df.Filters) == 1 { - return df.Filters[0] != "" + } else if df.regexExclusion != nil && df.regexExclusion.String() != "" { + return true } - return len(df.Filters) > 0 + return len(df.Filters) > 0 || len(df.exclude) > 0 } diff --git a/endpoint/domain_filter_test.go b/endpoint/domain_filter_test.go index 64647f71c..7cfd78126 100644 --- a/endpoint/domain_filter_test.go +++ b/endpoint/domain_filter_test.go @@ -379,15 +379,15 @@ func TestPrepareFiltersStripsWhitespaceAndDotSuffix(t *testing.T) { }{ { []string{}, - []string{}, + nil, }, { []string{""}, - []string{""}, + nil, }, { []string{" ", " ", ""}, - []string{"", "", ""}, + nil, }, { []string{" foo ", " bar. ", "baz."}, @@ -429,7 +429,7 @@ func TestDomainFilterIsConfigured(t *testing.T) { { []string{"", ""}, []string{""}, - true, + false, }, { []string{" . "}, @@ -446,6 +446,11 @@ func TestDomainFilterIsConfigured(t *testing.T) { []string{" thisdoesntmatter.com "}, true, }, + { + []string{""}, + []string{" thisdoesntmatter.com "}, + true, + }, } { t.Run("test IsConfigured", func(t *testing.T) { df := NewDomainFilterWithExclusions(tt.filters, tt.exclude) diff --git a/go.mod b/go.mod index 67013dc46..3ab6922a0 100644 --- a/go.mod +++ b/go.mod @@ -8,16 +8,17 @@ require ( github.com/Azure/go-autorest/autorest v0.11.27 github.com/Azure/go-autorest/autorest/adal v0.9.20 github.com/Azure/go-autorest/autorest/to v0.4.0 - github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.11.0 + github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.0.0 github.com/IBM/go-sdk-core/v5 v5.8.0 github.com/IBM/networking-go-sdk v0.32.0 github.com/StackExchange/dnscontrol v0.2.8 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.1 github.com/alecthomas/kingpin v2.2.5+incompatible github.com/aliyun/alibaba-cloud-sdk-go v1.61.1742 - github.com/ans-group/sdk-go v1.8.1 - github.com/aws/aws-sdk-go v1.44.81 + github.com/ans-group/sdk-go v1.10.4 + github.com/aws/aws-sdk-go v1.44.119 github.com/bodgit/tsig v1.2.0 + github.com/civo/civogo v0.3.14 github.com/cloudflare/cloudflare-go v0.50.0 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/datawire/ambassador v1.6.0 @@ -60,7 +61,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.4 go.etcd.io/etcd/client/v3 v3.5.2 go.uber.org/ratelimit v0.2.0 - golang.org/x/net v0.0.0-20220722155237-a158d28d115b + golang.org/x/net v0.1.0 golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 google.golang.org/api v0.93.0 @@ -128,7 +129,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.1 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/imdario/mergo v0.3.13 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.0.0 // indirect @@ -173,9 +174,9 @@ require ( go.uber.org/zap v1.19.1 // indirect golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/sys v0.1.0 // indirect + golang.org/x/term v0.1.0 // indirect + golang.org/x/text v0.4.0 // indirect golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect golang.org/x/tools v0.1.12 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 173fd4eb5..7e732f3f5 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.11.0 h1:75KEvjN+5lXcFzvW7RBoZY1YALJSNctcXFEfUyFz5Vo= -github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.11.0/go.mod h1:+GAqrO/rFsYnhzTIxYLXCHxHVZyrtzBLyKjV6hi73YQ= +github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.0.0 h1:2gzVSELk4I4ncZNrsaKI6fvZ3to60iYnig+lTFcGCEM= +github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.0.0/go.mod h1:P9YNyJaJazc49fLNFG4uQ61VZVptykWqNU2vWLWcxu0= github.com/IBM/go-sdk-core/v5 v5.8.0 h1:Bn9BxTaKYKWpd+BDpVsL6XOOJl4QDgxux4gSdWi31vE= github.com/IBM/go-sdk-core/v5 v5.8.0/go.mod h1:+YbdhrjCHC84ls4MeBp+Hj4NZCni+tDAc0XQUqRO9Jc= github.com/IBM/networking-go-sdk v0.32.0 h1:QWd7CxC+Wzap+zWFfXMjbqB5LpvrB1KvNtIbKrWIkhA= @@ -200,8 +200,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U= github.com/ans-group/go-durationstring v1.2.0 h1:UJIuQATkp0t1rBvZsHRwki33YHV9E+Ulro+3NbMB7MM= github.com/ans-group/go-durationstring v1.2.0/go.mod h1:QGF9Mdpq9058QXaut8r55QWu6lcHX6i/GvF1PZVkV6o= -github.com/ans-group/sdk-go v1.8.1 h1:PHPcYHujevWvCMiI7ujXvvrYHf5zpHIcZw7FfJ1rXw0= -github.com/ans-group/sdk-go v1.8.1/go.mod h1:XSKXEDfKobnDtZoyia5DhJxxaDMcCjr76e1KJ9dU/xc= +github.com/ans-group/sdk-go v1.10.4 h1:wZzojt99wtVIEHs8zNQzp1Xhqme5tD5NqMM1VLmG6xQ= +github.com/ans-group/sdk-go v1.10.4/go.mod h1:XSKXEDfKobnDtZoyia5DhJxxaDMcCjr76e1KJ9dU/xc= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/aokoli/goutils v1.1.0/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= @@ -231,6 +231,8 @@ github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/ github.com/aws/aws-sdk-go v1.40.14/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.44.81 h1:C8oBZ+a+ka0qk3Q24MohQIFq0tkbO8IAu5tfpAMKVWE= github.com/aws/aws-sdk-go v1.44.81/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.119 h1:TPkpDsanBMcZaF5wHwpKhjkapRV/b7d2qdC+a+IPbmY= +github.com/aws/aws-sdk-go v1.44.119/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= @@ -275,6 +277,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/civo/civogo v0.3.14 h1:W+o+hFXtEhWyJOmZOm2C5s8OEorSXGP6eyPYOa69NA8= +github.com/civo/civogo v0.3.14/go.mod h1:SbS06e0JPgIF27r1sLC97gjU1xWmONQeHgzF1hfLpak= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.20.0/go.mod h1:sPWL/lIC6biLEdyGZwBQ1rGQKF1FhM7N60fuNiFdYTI= @@ -863,8 +867,9 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/infobloxopen/infoblox-go-client/v2 v2.1.2-0.20220407114022-6f4c71443168 h1:EXKtVoP/44ckXpw3v2/vrtMEdKx/PA+YBl+REoV27XQ= @@ -1116,6 +1121,7 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -1128,6 +1134,7 @@ github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7 github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1681,6 +1688,8 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1848,12 +1857,16 @@ golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1865,6 +1878,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2251,6 +2266,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/kustomize/external-dns-deployment.yaml b/kustomize/external-dns-deployment.yaml index 212e16614..989587b61 100644 --- a/kustomize/external-dns-deployment.yaml +++ b/kustomize/external-dns-deployment.yaml @@ -16,7 +16,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: k8s.gcr.io/external-dns/external-dns + image: registry.k8s.io/external-dns/external-dns args: - --source=service - --source=ingress diff --git a/kustomize/kustomization.yaml b/kustomize/kustomization.yaml index de788c174..294b5a2d8 100644 --- a/kustomize/kustomization.yaml +++ b/kustomize/kustomization.yaml @@ -3,7 +3,7 @@ kind: Kustomization images: - name: registry.k8s.io/external-dns/external-dns - newTag: v0.12.2 + newTag: v0.13.1 resources: - ./external-dns-deployment.yaml diff --git a/main.go b/main.go index 086add11c..8533807c2 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ import ( "sigs.k8s.io/external-dns/provider/awssd" "sigs.k8s.io/external-dns/provider/azure" "sigs.k8s.io/external-dns/provider/bluecat" + "sigs.k8s.io/external-dns/provider/civo" "sigs.k8s.io/external-dns/provider/cloudflare" "sigs.k8s.io/external-dns/provider/coredns" "sigs.k8s.io/external-dns/provider/designate" @@ -228,6 +229,8 @@ func main() { p, err = vultr.NewVultrProvider(ctx, domainFilter, cfg.DryRun) case "ultradns": p, err = ultradns.NewUltraDNSProvider(domainFilter, cfg.DryRun) + case "civo": + p, err = civo.NewCivoProvider(domainFilter, cfg.DryRun) case "cloudflare": p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun) case "rcodezero": diff --git a/provider/alibabacloud/alibaba_cloud.go b/provider/alibabacloud/alibaba_cloud.go index c75b73340..d866cc62b 100644 --- a/provider/alibabacloud/alibaba_cloud.go +++ b/provider/alibabacloud/alibaba_cloud.go @@ -373,7 +373,7 @@ func (p *AlibabaCloudProvider) records() ([]alidns.Record, error) { log.Infof("Retrieving Alibaba Cloud DNS Domain Records") var results []alidns.Record - if (len(p.domainFilter.Filters) == 1 && p.domainFilter.Filters[0] == "") || len(p.domainFilter.Filters) == 0 { + if len(p.domainFilter.Filters) == 0 { domainNames, tmpErr := p.getDomainList() if tmpErr != nil { log.Errorf("AlibabaCloudProvider getDomainList error %v", tmpErr) diff --git a/provider/civo/civo.go b/provider/civo/civo.go new file mode 100644 index 000000000..bd61bd98e --- /dev/null +++ b/provider/civo/civo.go @@ -0,0 +1,538 @@ +/* +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 civo + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/civo/civogo" + log "github.com/sirupsen/logrus" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/pkg/apis/externaldns" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +// CivoProvider is an implementation of Provider for Civo's DNS. +type CivoProvider struct { + provider.BaseProvider + Client civogo.Client + domainFilter endpoint.DomainFilter + DryRun bool +} + +// CivoChanges All API calls calculated from the plan +type CivoChanges struct { + Creates []*CivoChangeCreate + Deletes []*CivoChangeDelete + Updates []*CivoChangeUpdate +} + +// CivoChangeCreate Civo Domain Record Creates +type CivoChangeCreate struct { + Domain civogo.DNSDomain + Options *civogo.DNSRecordConfig +} + +// CivoChangeUpdate Civo Domain Record Updates +type CivoChangeUpdate struct { + Domain civogo.DNSDomain + DomainRecord civogo.DNSRecord + Options civogo.DNSRecordConfig +} + +// CivoChangeDelete Civo Domain Record Deletes +type CivoChangeDelete struct { + Domain civogo.DNSDomain + DomainRecord civogo.DNSRecord +} + +// NewCivoProvider initializes a new Civo DNS based Provider. +func NewCivoProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*CivoProvider, error) { + token, ok := os.LookupEnv("CIVO_TOKEN") + if !ok { + return nil, fmt.Errorf("no token found") + } + + // Declare a default region just for the client is not used for anything else + // as the DNS API is global and not region based + region := "LON1" + + civoClient, err := civogo.NewClient(token, region) + if err != nil { + return nil, err + } + + userAgent := &civogo.Component{ + Name: "external-dns", + Version: externaldns.Version, + } + civoClient.SetUserAgent(userAgent) + + provider := &CivoProvider{ + Client: *civoClient, + domainFilter: domainFilter, + DryRun: dryRun, + } + return provider, nil +} + +// Zones returns the list of hosted zones. +func (p *CivoProvider) Zones(ctx context.Context) ([]civogo.DNSDomain, error) { + zones, err := p.fetchZones(ctx) + if err != nil { + return nil, err + } + + return zones, nil +} + +// Records returns the list of records in a given zone. +func (p *CivoProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { + zones, err := p.Zones(ctx) + if err != nil { + return nil, err + } + + var endpoints []*endpoint.Endpoint + + for _, zone := range zones { + records, err := p.fetchRecords(ctx, zone.ID) + if err != nil { + return nil, err + } + + for _, r := range records { + toUpper := strings.ToUpper(string(r.Type)) + if provider.SupportedRecordType(toUpper) { + name := fmt.Sprintf("%s.%s", r.Name, zone.Name) + + // root name is identified by the empty string and should be + // translated to zone name for the endpoint entry. + if r.Name == "" { + name = zone.Name + } + + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, toUpper, endpoint.TTL(r.TTL), r.Value)) + } + } + } + + return endpoints, nil +} + +func (p *CivoProvider) fetchRecords(ctx context.Context, domainID string) ([]civogo.DNSRecord, error) { + records, err := p.Client.ListDNSRecords(domainID) + if err != nil { + return nil, err + } + + return records, nil +} + +func (p *CivoProvider) fetchZones(ctx context.Context) ([]civogo.DNSDomain, error) { + var zones []civogo.DNSDomain + + allZones, err := p.Client.ListDNSDomains() + if err != nil { + return nil, err + } + + for _, zone := range allZones { + if !p.domainFilter.Match(zone.Name) { + continue + } + + zones = append(zones, zone) + } + + return zones, nil +} + +// submitChanges takes a zone and a collection of Changes and sends them as a single transaction. +func (p *CivoProvider) submitChanges(ctx context.Context, changes CivoChanges) error { + for _, change := range changes.Creates { + logFields := log.Fields{ + "Type": change.Options.Type, + "Name": change.Options.Name, + "Value": change.Options.Value, + "Priority": change.Options.Priority, + "TTL": change.Options.TTL, + "action": "Create", + } + + log.WithFields(logFields).Info("Creating record.") + + if p.DryRun { + log.WithFields(logFields).Info("Would create record.") + } else if _, err := p.Client.CreateDNSRecord(change.Domain.ID, change.Options); err != nil { + log.WithFields(logFields).Errorf( + "Failed to Create record: %v", + err, + ) + } + } + + for _, change := range changes.Deletes { + logFields := log.Fields{ + "Type": change.DomainRecord.Type, + "Name": change.DomainRecord.Name, + "Value": change.DomainRecord.Value, + "Priority": change.DomainRecord.Priority, + "TTL": change.DomainRecord.TTL, + "action": "Delete", + } + + log.WithFields(logFields).Info("Deleting record.") + + if p.DryRun { + log.WithFields(logFields).Info("Would delete record.") + } else if _, err := p.Client.DeleteDNSRecord(&change.DomainRecord); err != nil { + log.WithFields(logFields).Errorf( + "Failed to Delete record: %v", + err, + ) + } + } + + for _, change := range changes.Updates { + logFields := log.Fields{ + "Type": change.DomainRecord.Type, + "Name": change.DomainRecord.Name, + "Value": change.DomainRecord.Value, + "Priority": change.DomainRecord.Priority, + "TTL": change.DomainRecord.TTL, + "action": "Update", + } + + log.WithFields(logFields).Info("Updating record.") + + if p.DryRun { + log.WithFields(logFields).Info("Would update record.") + } else if _, err := p.Client.UpdateDNSRecord(&change.DomainRecord, &change.Options); err != nil { + log.WithFields(logFields).Errorf( + "Failed to Update record: %v", + err, + ) + } + } + + return nil +} + +// processCreateActions return a list of changes to create records. +func processCreateActions(zonesByID map[string]civogo.DNSDomain, recordsByZoneID map[string][]civogo.DNSRecord, createsByZone map[string][]*endpoint.Endpoint, civoChange *CivoChanges) error { + for zoneID, creates := range createsByZone { + zone := zonesByID[zoneID] + + if len(creates) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "zoneName": zone.Name, + }).Info("Skipping Zone, no creates found.") + continue + } + + records := recordsByZoneID[zoneID] + + // Generate Create + for _, ep := range creates { + matchedRecords := getRecordID(records, zone, *ep) + + if len(matchedRecords) != 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "zoneName": zone.Name, + "dnsName": ep.DNSName, + "recordType": ep.RecordType, + }).Warn("Records found which should not exist") + } + + recordType, err := convertRecordType(ep.RecordType) + if err != nil { + return err + } + + for _, target := range ep.Targets { + civoChange.Creates = append(civoChange.Creates, &CivoChangeCreate{ + Domain: zone, + Options: &civogo.DNSRecordConfig{ + Value: target, + Name: getStrippedRecordName(zone, *ep), + Type: recordType, + Priority: 0, + TTL: int(ep.RecordTTL), + }, + }) + } + } + } + + return nil +} + +// processUpdateActions return a list of changes to update records. +func processUpdateActions(zonesByID map[string]civogo.DNSDomain, recordsByZoneID map[string][]civogo.DNSRecord, updatesByZone map[string][]*endpoint.Endpoint, civoChange *CivoChanges) error { + for zoneID, updates := range updatesByZone { + zone := zonesByID[zoneID] + + if len(updates) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "zoneName": zone.Name, + }).Debug("Skipping Zone, no updates found.") + continue + } + + records := recordsByZoneID[zoneID] + + for _, ep := range updates { + matchedRecords := getRecordID(records, zone, *ep) + if len(matchedRecords) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "zoneName": zone.Name, + "recordType": ep.RecordType, + }).Warn("Update Records not found.") + } + + recordType, err := convertRecordType(ep.RecordType) + if err != nil { + return err + } + + matchedRecordsByTarget := make(map[string]civogo.DNSRecord) + for _, record := range matchedRecords { + matchedRecordsByTarget[record.Value] = record + } + + for _, target := range ep.Targets { + if record, ok := matchedRecordsByTarget[target]; ok { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "zoneName": zone.Name, + "recordType": ep.RecordType, + "target": target, + }).Warn("Updating Existing Target") + + civoChange.Updates = append(civoChange.Updates, &CivoChangeUpdate{ + Domain: zone, + DomainRecord: record, + Options: civogo.DNSRecordConfig{ + Value: target, + Name: getStrippedRecordName(zone, *ep), + Type: recordType, + Priority: 0, + TTL: int(ep.RecordTTL), + }, + }) + + delete(matchedRecordsByTarget, target) + } else { + // Record did not previously exist, create new 'target' + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "zoneName": zone.Name, + "recordType": ep.RecordType, + "target": target, + }).Warn("Creating New Target") + + civoChange.Creates = append(civoChange.Creates, &CivoChangeCreate{ + Domain: zone, + Options: &civogo.DNSRecordConfig{ + Value: target, + Name: getStrippedRecordName(zone, *ep), + Type: recordType, + Priority: 0, + TTL: int(ep.RecordTTL), + }, + }) + } + } + + // Any remaining records have been removed, delete them + for _, record := range matchedRecordsByTarget { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "recordType": ep.RecordType, + "target": record.Value, + }).Warn("Deleting target") + + civoChange.Deletes = append(civoChange.Deletes, &CivoChangeDelete{ + Domain: zone, + DomainRecord: record, + }) + } + } + } + + return nil +} + +// processDeleteActions return a list of changes to delete records. +func processDeleteActions(zonesByID map[string]civogo.DNSDomain, recordsByZoneID map[string][]civogo.DNSRecord, deletesByZone map[string][]*endpoint.Endpoint, civoChange *CivoChanges) error { + for zoneID, deletes := range deletesByZone { + zone := zonesByID[zoneID] + + if len(deletes) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "zoneName": zone.Name, + }).Debug("Skipping Zone, no deletes found.") + continue + } + + records := recordsByZoneID[zoneID] + + for _, ep := range deletes { + matchedRecords := getRecordID(records, zone, *ep) + + if len(matchedRecords) == 0 { + log.WithFields(log.Fields{ + "zoneID": zoneID, + "dnsName": ep.DNSName, + "zoneName": zone.Name, + "recordType": ep.RecordType, + }).Warn("Records to Delete not found.") + } + + for _, record := range matchedRecords { + civoChange.Deletes = append(civoChange.Deletes, &CivoChangeDelete{ + Domain: zone, + DomainRecord: record, + }) + } + } + } + return nil +} + +// ApplyChanges applies a given set of changes in a given zone. +func (p *CivoProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + var civoChange CivoChanges + recordsByZoneID := make(map[string][]civogo.DNSRecord) + + zones, err := p.fetchZones(ctx) + + if err != nil { + return err + } + + zonesByID := make(map[string]civogo.DNSDomain) + + zoneNameIDMapper := provider.ZoneIDName{} + + for _, z := range zones { + zoneNameIDMapper.Add(z.ID, z.Name) + zonesByID[z.ID] = z + } + + // Fetch records for each zone + for _, zone := range zones { + records, err := p.fetchRecords(ctx, zone.ID) + + if err != nil { + return err + } + + recordsByZoneID[zone.ID] = append(recordsByZoneID[zone.ID], records...) + } + + createsByZone := endpointsByZone(zoneNameIDMapper, changes.Create) + updatesByZone := endpointsByZone(zoneNameIDMapper, changes.UpdateNew) + deletesByZone := endpointsByZone(zoneNameIDMapper, changes.Delete) + + // Generate Creates + err = processCreateActions(zonesByID, recordsByZoneID, createsByZone, &civoChange) + if err != nil { + return err + } + + // Generate Updates + err = processUpdateActions(zonesByID, recordsByZoneID, updatesByZone, &civoChange) + if err != nil { + return err + } + + // Generate Deletes + err = processDeleteActions(zonesByID, recordsByZoneID, deletesByZone, &civoChange) + if err != nil { + return err + } + + return p.submitChanges(ctx, civoChange) +} + +func endpointsByZone(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]*endpoint.Endpoint { + endpointsByZone := make(map[string][]*endpoint.Endpoint) + + for _, ep := range endpoints { + zoneID, _ := zoneNameIDMapper.FindZone(ep.DNSName) + if zoneID == "" { + log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", ep.DNSName) + continue + } + endpointsByZone[zoneID] = append(endpointsByZone[zoneID], ep) + } + + return endpointsByZone +} + +func convertRecordType(recordType string) (civogo.DNSRecordType, error) { + switch recordType { + case "A": + return civogo.DNSRecordTypeA, nil + case "CNAME": + return civogo.DNSRecordTypeCName, nil + case "TXT": + return civogo.DNSRecordTypeTXT, nil + case "SRV": + return civogo.DNSRecordTypeSRV, nil + default: + return "", fmt.Errorf("invalid Record Type: %s", recordType) + } +} + +func getStrippedRecordName(zone civogo.DNSDomain, ep endpoint.Endpoint) string { + if ep.DNSName == zone.Name { + return "" + } + + return strings.TrimSuffix(ep.DNSName, "."+zone.Name) +} + +func getRecordID(records []civogo.DNSRecord, zone civogo.DNSDomain, ep endpoint.Endpoint) []civogo.DNSRecord { + var matchedRecords []civogo.DNSRecord + + for _, record := range records { + stripedName := getStrippedRecordName(zone, ep) + toUpper := strings.ToUpper(string(record.Type)) + if record.Name == stripedName && toUpper == ep.RecordType { + matchedRecords = append(matchedRecords, record) + } + } + + return matchedRecords +} diff --git a/provider/civo/civo_test.go b/provider/civo/civo_test.go new file mode 100644 index 000000000..6d637d077 --- /dev/null +++ b/provider/civo/civo_test.go @@ -0,0 +1,874 @@ +/* +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 civo + +import ( + "context" + "fmt" + "os" + "reflect" + "strings" + "testing" + + "github.com/civo/civogo" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" +) + +func TestNewCivoProvider(t *testing.T) { + _ = os.Setenv("CIVO_TOKEN", "xxxxxxxxxxxxxxx") + _, err := NewCivoProvider(endpoint.NewDomainFilter([]string{"test.civo.com"}), true) + require.NoError(t, err) + + _ = os.Unsetenv("CIVO_TOKEN") +} + +func TestNewCivoProviderNoToken(t *testing.T) { + _, err := NewCivoProvider(endpoint.NewDomainFilter([]string{"test.civo.com"}), true) + assert.Error(t, err) + + assert.Equal(t, "no token found", err.Error()) +} + +func TestCivoProviderZones(t *testing.T) { + client, server, _ := civogo.NewClientForTesting(map[string]string{ + "/v2/dns": `[ + {"id": "12345", "account_id": "1", "name": "example.com"}, + {"id": "12346", "account_id": "1", "name": "example.net"} + ]`, + }) + defer server.Close() + provider := &CivoProvider{ + Client: *client, + } + + expected, err := client.ListDNSDomains() + assert.NoError(t, err) + + zones, err := provider.Zones(context.Background()) + assert.NoError(t, err) + + // Check if the return is a DNSDomain type + assert.Equal(t, reflect.TypeOf(zones), reflect.TypeOf(expected)) + assert.ElementsMatch(t, zones, expected) +} + +func TestCivoProviderZonesWithError(t *testing.T) { + client, server, _ := civogo.NewClientForTesting(map[string]string{ + "/v2/dns-error": `[]`, + }) + defer server.Close() + provider := &CivoProvider{ + Client: *client, + } + + _, err := provider.Zones(context.Background()) + assert.Error(t, err) +} + +func TestCivoProviderRecords(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": "www", "type": "A", "value": "10.0.0.0", "ttl": 600}, + {"id": "2", "account_id": "1", "domain_id":"12345", "name": "mail", "type": "A", "value": "10.0.0.1", "ttl": 600} + ]`, + }, + { + RequestBody: ``, + URL: "/v2/dns", + ResponseBody: `[ + {"id": "12345", "account_id": "1", "name": "example.com"}, + {"id": "12346", "account_id": "1", "name": "example.net"} + ]`, + }, + }, + }, + }) + + defer server.Close() + provider := &CivoProvider{ + Client: *client, + domainFilter: endpoint.NewDomainFilter([]string{"example.com"}), + } + + expected, err := client.ListDNSRecords("12345") + assert.NoError(t, err) + + records, err := provider.Records(context.Background()) + assert.NoError(t, err) + + assert.Equal(t, strings.TrimSuffix(records[0].DNSName, ".example.com"), expected[0].Name) + assert.Equal(t, records[0].RecordType, string(expected[0].Type)) + assert.Equal(t, int(records[0].RecordTTL), expected[0].TTL) + + assert.Equal(t, strings.TrimSuffix(records[1].DNSName, ".example.com"), expected[1].Name) + assert.Equal(t, records[1].RecordType, string(expected[1].Type)) + assert.Equal(t, int(records[1].RecordTTL), expected[1].TTL) +} + +func TestCivoProviderWithoutRecords(t *testing.T) { + client, server, _ := civogo.NewClientForTesting(map[string]string{ + "/v2/dns/12345/records": `[]`, + "/v2/dns": `[ + {"id": "12345", "account_id": "1", "name": "example.com"}, + {"id": "12346", "account_id": "1", "name": "example.net"} + ]`, + }) + defer server.Close() + provider := &CivoProvider{ + Client: *client, + domainFilter: endpoint.NewDomainFilter([]string{"example.com"}), + } + + records, err := provider.Records(context.Background()) + assert.NoError(t, err) + + assert.Equal(t, len(records), 0) +} + +func TestCivoProcessCreateActions(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, + }, + }, + } + + createsByZone := map[string][]*endpoint.Endpoint{ + "example.com": { + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeCNAME, "foo.example.com"), + }, + } + + var changes CivoChanges + err := processCreateActions(zoneByID, recordsByZoneID, createsByZone, &changes) + require.NoError(t, err) + + assert.Equal(t, 2, len(changes.Creates)) + assert.Equal(t, 0, len(changes.Updates)) + assert.Equal(t, 0, len(changes.Deletes)) + + expectedCreates := []*CivoChangeCreate{ + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: "A", + Name: "foo", + Value: "1.2.3.4", + }, + }, + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: "CNAME", + Name: "txt", + Value: "foo.example.com", + }, + }, + } + + if !elementsMatch(t, expectedCreates, changes.Creates) { + assert.Failf(t, "diff: %s", cmp.Diff(expectedCreates, changes.Creates)) + } +} + +func TestCivoProcessCreateActionsWithError(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, + }, + }, + } + + createsByZone := 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 := processCreateActions(zoneByID, recordsByZoneID, createsByZone, &changes) + require.Error(t, err) + assert.Equal(t, "invalid Record Type: AAAA", err.Error()) +} + +func TestCivoProcessUpdateActions(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: "1.2.3.4", + Type: "A", + TTL: 600, + }, + { + ID: "2", + AccountID: "1", + DNSDomainID: "1", + Name: "foo", + Value: "foo.example.com", + Type: "CNAME", + TTL: 600, + }, + { + ID: "3", + AccountID: "1", + DNSDomainID: "1", + Name: "bar", + Value: "10.10.10.1", + Type: "A", + TTL: 600, + }, + }, + } + + updatesByZone := map[string][]*endpoint.Endpoint{ + "example.com": { + endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeA, "10.20.30.40"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeCNAME, "bar.example.com"), + }, + } + + var changes CivoChanges + err := processUpdateActions(zoneByID, recordsByZoneID, updatesByZone, &changes) + require.NoError(t, err) + + assert.Equal(t, 2, len(changes.Creates)) + assert.Equal(t, 0, len(changes.Updates)) + assert.Equal(t, 2, len(changes.Deletes)) + + expectedUpdate := []*CivoChangeCreate{ + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: "A", + Name: "txt", + Value: "10.20.30.40", + }, + }, + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: "CNAME", + Name: "foo", + Value: "bar.example.com", + }, + }, + } + + if !elementsMatch(t, expectedUpdate, changes.Creates) { + assert.Failf(t, "diff: %s", cmp.Diff(expectedUpdate, changes.Creates)) + } + + expectedDelete := []*CivoChangeDelete{ + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "1", + AccountID: "1", + DNSDomainID: "1", + Name: "txt", + Value: "1.2.3.4", + Type: "A", + Priority: 0, + TTL: 600, + }, + }, + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "2", + AccountID: "1", + DNSDomainID: "1", + Name: "foo", + Value: "foo.example.com", + Type: "CNAME", + Priority: 0, + TTL: 600, + }, + }, + } + + if !elementsMatch(t, expectedDelete, changes.Deletes) { + assert.Failf(t, "diff: %s", cmp.Diff(expectedDelete, changes.Deletes)) + } +} + +func TestCivoProcessDeleteAction(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: "1.2.3.4", + Type: "A", + TTL: 600, + }, + { + ID: "2", + AccountID: "1", + DNSDomainID: "1", + Name: "foo", + Value: "5.6.7.8", + Type: "A", + TTL: 600, + }, + { + ID: "3", + AccountID: "1", + DNSDomainID: "1", + Name: "bar", + Value: "10.10.10.1", + Type: "A", + TTL: 600, + }, + }, + } + + deleteByDomain := map[string][]*endpoint.Endpoint{ + "example.com": { + endpoint.NewEndpoint("txt.example.com", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "5.6.7.8"), + }, + } + + var changes CivoChanges + err := processDeleteActions(zoneByID, recordsByZoneID, deleteByDomain, &changes) + require.NoError(t, err) + + assert.Equal(t, 0, len(changes.Creates)) + assert.Equal(t, 0, len(changes.Updates)) + assert.Equal(t, 2, len(changes.Deletes)) + + expectedDelete := []*CivoChangeDelete{ + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "1", + AccountID: "1", + DNSDomainID: "1", + Name: "txt", + Value: "1.2.3.4", + Type: "A", + TTL: 600, + }, + }, + { + Domain: civogo.DNSDomain{ + ID: "1", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "2", + AccountID: "1", + DNSDomainID: "1", + Type: "A", + Name: "foo", + Value: "5.6.7.8", + TTL: 600, + }, + }, + } + + if !elementsMatch(t, expectedDelete, changes.Deletes) { + assert.Failf(t, "diff: %s", cmp.Diff(expectedDelete, changes.Deletes)) + } +} + +func TestCivoApplyChanges(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() + + changes := &plan.Changes{} + provider := &CivoProvider{ + Client: *client, + } + changes.Create = []*endpoint.Endpoint{ + {DNSName: "new.ext-dns-test.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeA}, + {DNSName: "new.ext-dns-test-with-ttl.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeA, RecordTTL: 100}, + } + changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"target"}}} + changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.example.de", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"target-old"}}} + changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Targets: endpoint.Targets{"target-new"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100}} + err := provider.ApplyChanges(context.Background(), changes) + assert.NoError(t, err) +} + +func TestCivoProviderFetchZones(t *testing.T) { + client, server, _ := civogo.NewClientForTesting(map[string]string{ + "/v2/dns": `[ + {"id": "12345", "account_id": "1", "name": "example.com"}, + {"id": "12346", "account_id": "1", "name": "example.net"} + ]`, + }) + defer server.Close() + provider := &CivoProvider{ + Client: *client, + } + + expected, err := client.ListDNSDomains() + if err != nil { + t.Errorf("should not fail, %s", err) + } + zones, err := provider.fetchZones(context.Background()) + if err != nil { + t.Fatal(err) + } + assert.ElementsMatch(t, zones, expected) +} +func TestCivoProviderFetchZonesWithFilter(t *testing.T) { + client, server, _ := civogo.NewClientForTesting(map[string]string{ + "/v2/dns": `[ + {"id": "12345", "account_id": "1", "name": "example.com"}, + {"id": "12346", "account_id": "1", "name": "example.net"} + ]`, + }) + defer server.Close() + provider := &CivoProvider{ + Client: *client, + domainFilter: endpoint.NewDomainFilter([]string{".com"}), + } + + expected := []civogo.DNSDomain{ + {ID: "12345", Name: "example.com", AccountID: "1"}, + } + + actual, err := provider.fetchZones(context.Background()) + if err != nil { + t.Fatal(err) + } + assert.ElementsMatch(t, expected, actual) +} + +func TestCivoProviderFetchRecords(t *testing.T) { + client, server, _ := civogo.NewClientForTesting(map[string]string{ + "/v2/dns/12345/records": `[ + {"id": "1", "domain_id":"12345", "account_id": "1", "name": "www", "type": "A", "value": "10.0.0.0", "ttl": 600}, + {"id": "2", "account_id": "1", "domain_id":"12345", "name": "mail", "type": "A", "value": "10.0.0.1", "ttl": 600} + ]`, + }) + defer server.Close() + provider := &CivoProvider{ + Client: *client, + } + + expected, err := client.ListDNSRecords("12345") + assert.NoError(t, err) + + actual, err := provider.fetchRecords(context.Background(), "12345") + assert.NoError(t, err) + + assert.ElementsMatch(t, expected, actual) +} + +func TestCivoProviderFetchRecordsWithError(t *testing.T) { + client, server, _ := civogo.NewClientForTesting(map[string]string{ + "/v2/dns/12345/records": `[ + {"id": "1", "domain_id":"12345", "account_id": "1", "name": "www", "type": "A", "value": "10.0.0.0", "ttl": 600}, + {"id": "2", "account_id": "1", "domain_id":"12345", "name": "mail", "type": "A", "value": "10.0.0.1", "ttl": 600} + ]`, + }) + defer server.Close() + provider := &CivoProvider{ + Client: *client, + } + + _, err := provider.fetchRecords(context.Background(), "235698") + assert.Error(t, err) +} + +func TestCivo_getStrippedRecordName(t *testing.T) { + assert.Equal(t, "", getStrippedRecordName(civogo.DNSDomain{ + Name: "foo.com", + }, endpoint.Endpoint{ + DNSName: "foo.com", + })) + + assert.Equal(t, "api", getStrippedRecordName(civogo.DNSDomain{ + Name: "foo.com", + }, endpoint.Endpoint{ + DNSName: "api.foo.com", + })) +} + +func TestCivo_convertRecordType(t *testing.T) { + record, err := convertRecordType("A") + recordA := civogo.DNSRecordType(civogo.DNSRecordTypeA) + require.NoError(t, err) + assert.Equal(t, recordA, record) + + record, err = convertRecordType("CNAME") + recordCName := civogo.DNSRecordType(civogo.DNSRecordTypeCName) + require.NoError(t, err) + assert.Equal(t, recordCName, record) + + record, err = convertRecordType("TXT") + recordTXT := civogo.DNSRecordType(civogo.DNSRecordTypeTXT) + require.NoError(t, err) + assert.Equal(t, recordTXT, record) + + record, err = convertRecordType("SRV") + recordSRV := civogo.DNSRecordType(civogo.DNSRecordTypeSRV) + require.NoError(t, err) + assert.Equal(t, recordSRV, record) + + _, err = convertRecordType("INVALID") + require.Error(t, err) + + assert.Equal(t, "invalid Record Type: INVALID", err.Error()) +} + +func TestCivoProviderGetRecordID(t *testing.T) { + zone := civogo.DNSDomain{ + ID: "12345", + Name: "test.com", + } + + record := []civogo.DNSRecord{{ + ID: "1", + Type: "A", + Name: "www", + Value: "10.0.0.0", + DNSDomainID: "12345", + TTL: 600, + }, { + ID: "2", + Type: "A", + Name: "api", + Value: "10.0.0.1", + DNSDomainID: "12345", + TTL: 600, + }} + + endPoint := endpoint.Endpoint{DNSName: "www.test.com", Targets: endpoint.Targets{"10.0.0.0"}, RecordType: "A"} + id := getRecordID(record, zone, endPoint) + + assert.Equal(t, id[0].ID, record[0].ID) +} + +func TestCivo_submitChangesCreate(t *testing.T) { + client, server, _ := civogo.NewAdvancedClientForTesting([]civogo.ConfigAdvanceClientForTesting{ + { + Method: "POST", + Value: []civogo.ValueAdvanceClientForTesting{ + { + RequestBody: `{"type":"MX","name":"mail","value":"10.0.0.1","priority":10,"ttl":600}`, + URL: "/v2/dns/12345/records", + ResponseBody: `{ + "id": "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", + "account_id": "1", + "domain_id": "12345", + "name": "mail", + "value": "10.0.0.1", + "type": "MX", + "priority": 10, + "ttl": 600 + }`, + }, + }, + }, + }) + defer server.Close() + + provider := &CivoProvider{ + Client: *client, + DryRun: false, + } + + 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, + }, + }, + }, + } + + 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{ + { + RequestBody: `{"type":"MX","name":"mail","value":"10.0.0.2","priority":10,"ttl":600}`, + URL: "/v2/dns/12345/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3", + ResponseBody: `{ + "id": "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", + "account_id": "1", + "domain_id": "12345", + "name": "mail", + "value": "10.0.0.2", + "type": "MX", + "priority": 10, + "ttl": 600 + }`, + }, + }, + }, + }) + defer server.Close() + + provider := &CivoProvider{ + Client: *client, + DryRun: false, + } + + 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, + }, + Options: civogo.DNSRecordConfig{ + Type: "MX", + Name: "mail", + Value: "10.0.0.2", + Priority: 10, + TTL: 600, + }, + }, + }, + } + + err := provider.submitChanges(context.Background(), changes) + assert.NoError(t, err) +} + +func TestCivo_submitChangesDelete(t *testing.T) { + client, server, _ := civogo.NewClientForTesting(map[string]string{ + "/v2/dns/12345/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3": `{"result": "success"}`, + }) + defer server.Close() + + provider := &CivoProvider{ + Client: *client, + DryRun: false, + } + + changes := CivoChanges{ + Deletes: []*CivoChangeDelete{ + { + 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.2", + Type: "MX", + Priority: 10, + TTL: 600, + }, + }, + }, + } + + err := provider.submitChanges(context.Background(), changes) + assert.NoError(t, err) +} + +// This function is an adapted copy of the testify package's ElementsMatch function with the +// call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to +// other structs. It also ignores ordering when comparing unlike cmp.Equal. +func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { + if listA == nil && listB == nil { + return true + } else if listA == nil { + return isEmpty(listB) + } else if listB == nil { + return isEmpty(listA) + } + + aKind := reflect.TypeOf(listA).Kind() + bKind := reflect.TypeOf(listB).Kind() + + if aKind != reflect.Array && aKind != reflect.Slice { + return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listA, aKind), msgAndArgs...) + } + + if bKind != reflect.Array && bKind != reflect.Slice { + return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listB, bKind), msgAndArgs...) + } + + aValue := reflect.ValueOf(listA) + bValue := reflect.ValueOf(listB) + + aLen := aValue.Len() + bLen := bValue.Len() + + if aLen != bLen { + return assert.Fail(t, fmt.Sprintf("lengths don't match: %d != %d", aLen, bLen), msgAndArgs...) + } + + // Mark indexes in bValue that we already used + visited := make([]bool, bLen) + for i := 0; i < aLen; i++ { + element := aValue.Index(i).Interface() + found := false + for j := 0; j < bLen; j++ { + if visited[j] { + continue + } + if cmp.Equal(bValue.Index(j).Interface(), element) { + visited[j] = true + found = true + break + } + } + if !found { + return assert.Fail(t, fmt.Sprintf("element %s appears more times in %s than in %s", element, aValue, bValue), msgAndArgs...) + } + } + + return true +} + +func isEmpty(xs interface{}) bool { + if xs != nil { + objValue := reflect.ValueOf(xs) + return objValue.Len() == 0 + } + return true +} diff --git a/provider/google/google.go b/provider/google/google.go index 4c68e7c76..d6c53ef6a 100644 --- a/provider/google/google.go +++ b/provider/google/google.go @@ -188,17 +188,13 @@ func (p *GoogleProvider) Zones(ctx context.Context) (map[string]*dns.ManagedZone return nil } - log.Debugf("Matching zones against domain filters: %v", p.domainFilter.Filters) + log.Debugf("Matching zones against domain filters: %v", p.domainFilter) if err := p.managedZonesClient.List(p.project).Pages(ctx, f); err != nil { return nil, err } if len(zones) == 0 { - if p.domainFilter.IsConfigured() { - log.Warnf("No zones in the project, %s, match domain filters: %v", p.project, p.domainFilter.Filters) - } else { - log.Warnf("No zones found in the project, %s", p.project) - } + log.Warnf("No zones in the project, %s, match domain filters: %v", p.project, p.domainFilter) } for _, zone := range zones { diff --git a/provider/ibmcloud/ibmcloud.go b/provider/ibmcloud/ibmcloud.go index ff58ae9f7..d6aabd322 100644 --- a/provider/ibmcloud/ibmcloud.go +++ b/provider/ibmcloud/ibmcloud.go @@ -218,7 +218,7 @@ func (c *ibmcloudConfig) Validate(authenticator core.Authenticator, domainFilter var service ibmcloudService isPrivate := false log.Debugf("filters: %v, %v", domainFilter.Filters, zoneIDFilter.ZoneIDs) - if domainFilter.Filters[0] == "" && zoneIDFilter.ZoneIDs[0] == "" { + if (len(domainFilter.Filters) == 0 || domainFilter.Filters[0] == "") && zoneIDFilter.ZoneIDs[0] == "" { return service, isPrivate, fmt.Errorf("at lease one of filters: 'domain-filter', 'zone-id-filter' needed") } @@ -253,7 +253,7 @@ func (c *ibmcloudConfig) Validate(authenticator core.Authenticator, domainFilter } for _, zone := range zonesResp.Result { log.Debugf("zoneName: %s, zoneID: %s", *zone.Name, *zone.ID) - if len(domainFilter.Filters[0]) != 0 && domainFilter.Match(*zone.Name) { + if len(domainFilter.Filters) > 0 && domainFilter.Filters[0] != "" && domainFilter.Match(*zone.Name) { log.Debugf("zone %s found.", *zone.ID) zoneID = *zone.ID break diff --git a/provider/oci/oci.go b/provider/oci/oci.go index aad32e10a..c4817b411 100644 --- a/provider/oci/oci.go +++ b/provider/oci/oci.go @@ -138,11 +138,7 @@ func (p *OCIProvider) zones(ctx context.Context) (map[string]dns.ZoneSummary, er } if len(zones) == 0 { - if p.domainFilter.IsConfigured() { - log.Warnf("No zones in compartment %q match domain filters %v", p.cfg.CompartmentID, p.domainFilter.Filters) - } else { - log.Warnf("No zones found in compartment %q", p.cfg.CompartmentID) - } + log.Warnf("No zones in compartment %q match domain filters %v", p.cfg.CompartmentID, p.domainFilter) } return zones, nil diff --git a/source/kong_tcpingress_test.go b/source/kong_tcpingress_test.go index a304c5b5f..9686da678 100644 --- a/source/kong_tcpingress_test.go +++ b/source/kong_tcpingress_test.go @@ -31,7 +31,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" ) -// This is a compile-time validation that glooSource is a Source. +// This is a compile-time validation that kongTCPIngressSource is a Source. var _ Source = &kongTCPIngressSource{} const defaultKongNamespace = "kong"