Merge remote-tracking branch 'origin/master' into ingress-class-filtering

This commit is contained in:
Dave Salisbury 2022-02-14 16:36:26 +11:00
commit 1ceec80ec5
54 changed files with 1980 additions and 474 deletions

View File

@ -16,7 +16,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.16
go-version: ^1.17
id: go
- name: Check out code into the Go module directory

View File

@ -14,7 +14,7 @@
# builder image
ARG ARCH
FROM golang:1.16 as builder
FROM golang:1.17 as builder
ARG ARCH
WORKDIR /sigs.k8s.io/external-dns
@ -27,7 +27,7 @@ COPY . .
RUN make test build.$ARCH
# final image
FROM $ARCH/alpine:3.14
FROM $ARCH/alpine:3.15
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /sigs.k8s.io/external-dns/build/external-dns /bin/external-dns

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
FROM golang:1.16 as builder
FROM golang:1.17 as builder
WORKDIR /sigs.k8s.io/external-dns

View File

@ -105,7 +105,7 @@ build.push/multiarch:
image="$(IMAGE):$(VERSION)-$${arch}" ;\
# pre-pull due to https://github.com/kubernetes-sigs/cluster-addons/pull/84/files ;\
docker pull $${arch}/alpine:3.14 ;\
docker pull golang:1.16 ;\
docker pull golang:1.17 ;\
DOCKER_BUILDKIT=1 docker build --rm --tag $${image} --build-arg VERSION="$(VERSION)" --build-arg ARCH="$${arch}" . ;\
docker push $${image} ;\
arch_specific_tags+=( "--amend $${image}" ) ;\

View File

@ -52,6 +52,7 @@ ExternalDNS' allows you to keep selected zones (via `--domain-filter`) synchroni
* [Akamai Edge DNS](https://learn.akamai.com/en-us/products/cloud_security/edge_dns.html)
* [GoDaddy](https://www.godaddy.com)
* [Gandi](https://www.gandi.net)
* [UKFast SafeDNS](https://my.ukfast.co.uk/safedns/)
From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API.
@ -109,13 +110,17 @@ The following table clarifies the current status of the providers according to t
| UltraDNS | Alpha | |
| GoDaddy | Alpha | |
| Gandi | Alpha | @packi |
| SafeDNS | Alpha | @assureddt |
## Kubernetes version compatibility
| ExternalDNS | <= 0.9.x | >= 0.10.0 |
| ------------------ | :----------------: | :----------------: |
| Kubernetes <= 1.18 | :white_check_mark: | :x: |
| Kubernetes >= 1.19 | :x: | :white_check_mark: |
A [breaking change](https://github.com/kubernetes-sigs/external-dns/pull/2281) was added in external-dns v0.10.0.
| ExternalDNS | <= 0.9.x | >= 0.10.0 |
| ------------------------------ | :----------------: | :----------------: |
| Kubernetes <= 1.18 | :white_check_mark: | :x: |
| Kubernetes >= 1.19 and <= 1.21 | :white_check_mark: | :white_check_mark: |
| Kubernetes >= 1.22 | :x: | :white_check_mark: |
## Running ExternalDNS:
@ -128,6 +133,7 @@ The are two ways of running ExternalDNS:
The following tutorials are provided:
* [Akamai Edge DNS](docs/tutorials/akamai-edgedns.md)
* [Alibaba Cloud](docs/tutorials/alibabacloud.md)
* AWS
* [ALB Ingress Controller](docs/tutorials/alb-ingress.md)
@ -171,6 +177,7 @@ The following tutorials are provided:
* [UltraDNS](docs/tutorials/ultradns.md)
* [GoDaddy](docs/tutorials/godaddy.md)
* [Gandi](docs/tutorials/gandi.md)
* [SafeDNS](docs/tutorials/safedns.md)
### Running Locally

View File

@ -2,7 +2,7 @@ apiVersion: v2
name: external-dns
description: ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.
type: application
version: 1.7.0
version: 1.7.1
appVersion: 0.10.2
keywords:
- kubernetes

View File

@ -33,6 +33,7 @@ The following table lists the configurable parameters of the _ExternalDNS_ chart
| `serviceAccount.name` | Service account to be used. If not set and `serviceAccount.create` is `true`, a name is generated using the full name template. | `""` |
| `rbac.create` | If `true`, create the RBAC resources. | `true` |
| `rbac.additionalPermissions` | Additional permissions to be added to the cluster role. | `{}` |
| `deploymentAnnotations` | Annotations to add to the Deployment. | `{}` |
| `podLabels` | Labels to add to the pod. | `{}` |
| `podAnnotations` | Annotations to add to the pod. | `{}` |
| `podSecurityContext` | Security context for the pod, this supports the full [PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#podsecuritycontext-v1-core) API. | _see values.yaml_ |
@ -54,6 +55,7 @@ The following table lists the configurable parameters of the _ExternalDNS_ chart
| `nodeSelector` | Node labels for pod assignment. | `{}` |
| `tolerations` | Tolerations for pod assignment, this supports the full [Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#toleration-v1-core) API. | `[]` |
| `affinity` | Affinity settings for pod assignment, this supports the full [Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#affinity-v1-core) API. | `{}` |
| `topologySpreadConstraints` | TopologySpreadConstraint settings for pod assignment, this supports the full [TopologySpreadConstraints](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#topologyspreadconstraint-v1-core) API. | `[]` |
| `logLevel` | Verbosity of the logs, available values are: `panic`, `debug`, `info`, `warn`, `error`, `fatal`. | `info` |
| `logFormat` | Formats of the logs, available values are: `text`, `json`. | `text` |
| `interval` | The interval for DNS updates. | `1m` |

View File

@ -30,7 +30,7 @@ rules:
verbs: ["get","watch","list"]
{{- end }}
{{- if has "istio-gateway" .Values.sources }}
{{- if or (has "istio-gateway" .Values.sources) (has "istio-virtualservice" .Values.sources) }}
- apiGroups: ["networking.istio.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]

View File

@ -4,6 +4,10 @@ metadata:
name: {{ include "external-dns.fullname" . }}
labels:
{{- include "external-dns.labels" . | nindent 4 }}
{{- with .Values.deploymentAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
replicas: 1
selector:
@ -67,6 +71,9 @@ spec:
{{- if .Values.txtPrefix }}
- --txt-prefix={{ .Values.txtPrefix }}
{{- end }}
{{- if and (eq .Values.txtPrefix "") (ne .Values.txtSuffix "") }}
- --txt-suffix={{ .Values.txtSuffix }}
{{- end }}
{{- end }}
{{- range .Values.domainFilters }}
- --domain-filter={{ . }}
@ -103,6 +110,10 @@ spec:
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}

View File

@ -26,8 +26,12 @@ rbac:
create: true
additionalPermissions: {}
# Annotations to add to the Deployment
deploymentAnnotations: {}
podLabels: {}
# Annotations to add to the Pod
podAnnotations: {}
podSecurityContext:
@ -88,6 +92,8 @@ tolerations: []
affinity: {}
topologySpreadConstraints: []
logLevel: info
logFormat: text
@ -103,6 +109,7 @@ policy: upsert-only
registry: txt
txtOwnerId: ""
txtPrefix: ""
txtSuffix: ""
domainFilters: []

View File

@ -1,7 +1,7 @@
# Quick Start
- [Git](https://git-scm.com/downloads)
- [Go 1.16+](https://golang.org/dl/)
- [Go 1.17+](https://golang.org/dl/)
- [Go modules](https://github.com/golang/go/wiki/Modules)
- [golangci-lint](https://github.com/golangci/golangci-lint)
- [Docker](https://docs.docker.com/install/)

View File

@ -122,8 +122,4 @@ A single loop iteration of external-dns operation:
1. DNS Provider should use batch operations
2. DNS Provider should be called with CREATE operation (not UPSERT!) when the record does not yet exist!
### Resources:
1. Registry implementation draft:
[Flow](https://lh3.googleusercontent.com/BNUZZQ8XivYkXyYVPDgPCoZpwYv0pOyoyfBKbOnYJGsqueeB-EUXfzBZLk7xP-E_GDo7YHiTlA4XgPEs6ao_Ex0TY2SN66-yg5iRmn5Tc2EXVqs_yS9CtumhE1T4krZc4Z8_1gHOirDxCegU-Fk0K3fvg-J3UpzdKmGDG-JZwdzRyP4WyORWUQilJO9jErh-HP8AtM8p2ZjiqN9B3-VXdYuHbsiR6EHNFw43aOQAk52muDf2AgjqX2YUSbN9eO0Akt39ien3euT2HsZJlPvm5s8v2a_ZqTSW0DVcGaRhLQbZXcogSEP-ebbuGunuVbz45Ws8X6zJhZpASNQ-jknhGZEhZkSAQdwvihZpTsDdUuJx9RFDXNwA0lEaE_xediW119uJGywSNc6w8hnJZ6Xo49YQStuGbJKRAieQMvEhZXofiqCKyOUXSlsO7j9iE-rzis0JRSHWB8acA3AlcXqBj9D70AHfRHC_HfBLw9lcusy4dInmK2OCzGqXV11PoqibiZPqh-oNED31pToZQk4NB1xbOuUC_Tjf8UR_xAyhJ3yKzS09K898uCf-87Ra4iqRDCz3N35b=w2560-h1260)

View File

@ -0,0 +1,210 @@
# Setting up ExternalDNS for Services on UKFast's SafeDNS
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using SafeDNS.
Make sure to use **>=0.11.0** version of ExternalDNS for this tutorial.
## Managing DNS with SafeDNS
If you want to learn about how to use the SafeDNS service read the following tutorials:
To learn more about the use of SafeDNS in general, see the following page:
[UKFast's SafeDNS documentation](https://docs.ukfast.co.uk/domains/safedns/index.html).
## Creating SafeDNS credentials
Generate a fresh API token for use with ExternalDNS, following the instructions
at the UKFast developer [Getting-Started](https://developers.ukfast.io/getting-started)
page. You will need to grant read/write access to the SafeDNS API. No access to
any other UKFast service is required.
The environment variable `SAFEDNS_TOKEN` must have a value of this token to run
ExternalDNS with SafeDNS integration.
## 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
# You will need to check what the latest version is yourself:
# https://github.com/kubernetes-sigs/external-dns/releases
image: k8s.gcr.io/external-dns/external-dns:vX.Y.Z
args:
- --source=service # ingress is also possible
# (optional) limit to only example.com domains; change to match the
# zone created above.
- --domain-filter=example.com
- --provider=safedns
env:
- name: SAFEDNS_TOKEN
value: "SAFEDNSTOKENSAFEDNSTOKEN"
```
### 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: 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: k8s.gcr.io/external-dns/external-dns:v0.11.0
args:
- --source=service # ingress is also possible
# (optional) limit to only example.com domains; change to match the
# zone created above.
- --domain-filter=example.com
- --provider=safedns
env:
- name: SAFEDNS_TOKEN
value: "SAFEDNSTOKENSAFEDNSTOKEN"
```
## 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 a hostname that matches the domain
filter specified 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 SafeDNS records.
## Verifying SafeDNS records
Check your [SafeDNS UI](https://my.ukfast.co.uk/safedns/index.php) and select
the appropriate domain to view the records for your SafeDNS zone.
This should show the external IP address of the service as the A record for your
domain.
Alternatively, you can perform a DNS lookup for the hostname specified:
```console
$ dig +short my-app.example.com
an.ip.addr.ess
```
## Cleanup
Now that we have verified that ExternalDNS will automatically manage SafeDNS
records, we can delete the tutorial's example:
```
$ kubectl delete service -f nginx.yaml
$ kubectl delete service -f externaldns.yaml
```

View File

@ -383,7 +383,7 @@ metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
external-dns.alpha.kubernetes.io/ttl: 60
external-dns.alpha.kubernetes.io/ttl: "60"
spec:
...
```

View File

@ -7,8 +7,72 @@ Install the BlueCat Gateway product and deploy the [community gateway workflows]
## Configuration Options
The options for configuring the Bluecat Provider are available through the json file provided to External-DNS via the flag `--bluecat-config-file`. The
BlueCat Gateway username and password can be supplied using the configuration file or environment variables `BLUECAT_USERNAME` and `BLUECAT_PASSWORD`.
There are two ways to pass configuration options to the Bluecat Provider JSON configuration file and command line flags. The JSON configuration file option
is deprecated and will eventually be removed.
BlueCat provider supports getting the proxy URL from the environment variables. The format is the one specified by golang's [http.ProxyFromEnvironment](https://pkg.go.dev/net/http#ProxyFromEnvironment).
### Using CLI Flags
When using CLI flags to configure the Bluecat Provider the BlueCat Gateway credentials are passed in using environment variables `BLUECAT_USERNAME` and `BLUECAT_PASSWORD`.
#### Deploy
Setup up namespace, deployment, and service account:
```
kubectl create namespace bluecat-example
kubectl create secret generic bluecat-credentials --from-literal=username=bluecatuser --from-literal=password=bluecatpassword -n bluecat-example
cat << EOF > ~/bluecat.yml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
selector:
matchLabels:
app: external-dns
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
args:
- --log-level=debug
- --source=service
- --provider=bluecat
- --txt-owner-id=bluecat-example
- --bluecat-dns-configuration=Example
- --bluecat-dns-view=Internal
- --bluecat-gateway-host=https://bluecatgw.example.com
- --bluecat-root-zone=example.com
env:
- name: BLUECAT_USERNAME
valueFrom:
secretKeyRef:
name: bluecat-credentials
key: username
- name: BLUECAT_PASSWORD
valueFrom:
secretKeyRef:
name: bluecat-credentials
key: password
EOF
kubectl apply -f ~/bluecat.yml -n bluecat-example
```
### Using JSON Configuration file (DEPRECATED)
The options for configuring the Bluecat Provider are available through the JSON file provided to External-DNS via the flag `--bluecat-config-file`.
| Key | Required |
| ----------------- | ------------------ |
@ -18,13 +82,11 @@ BlueCat Gateway username and password can be supplied using the configuration fi
| dnsConfiguration | Yes |
| dnsView | Yes |
| rootZone | Yes |
| dnsServerName | No |
| dnsDeployType | No |
| skipTLSVerify | No (default false) |
### HTTP proxy
BlueCat provider supports getting the proxy URL from the environment variables. The format is the one specified by golang's [http.ProxyFromEnvironment](https://pkg.go.dev/net/http#ProxyFromEnvironment).
## Deploy
#### Deploy
Setup configuration file as k8s `Secret`.
```
cat << EOF > ~/bluecat.json

148
go.mod
View File

@ -1,84 +1,164 @@
module sigs.k8s.io/external-dns
go 1.16
go 1.17
require (
cloud.google.com/go v0.97.0
cloud.google.com/go/compute v1.2.0
git.blindage.org/21h/hcloud-dns v0.0.0-20200807003420-f768ffe03f8d
github.com/Azure/azure-sdk-for-go v46.4.0+incompatible
github.com/Azure/azure-sdk-for-go v61.4.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.21
github.com/Azure/go-autorest/autorest/adal v0.9.16
github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1
github.com/StackExchange/dnscontrol v0.2.8
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
github.com/alecthomas/colour v0.1.0 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1
github.com/alecthomas/kingpin v2.2.5+incompatible
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.357
github.com/aws/aws-sdk-go v1.40.53
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1483
github.com/aws/aws-sdk-go v1.42.52
github.com/bodgit/tsig v0.0.2
github.com/cloudflare/cloudflare-go v0.25.0
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/datawire/ambassador v1.6.0
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
github.com/digitalocean/godo v1.69.1
github.com/dnsimple/dnsimple-go v0.60.0
github.com/exoscale/egoscale v0.80.1
github.com/digitalocean/godo v1.75.0
github.com/dnsimple/dnsimple-go v0.71.1
github.com/exoscale/egoscale v1.19.0
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
github.com/go-gandi/go-gandi v0.0.0-20200921091836-0d8a64b9cc09
github.com/go-logr/logr v1.1.0 // indirect
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f
github.com/google/go-cmp v0.5.6
github.com/google/go-cmp v0.5.7
github.com/gophercloud/gophercloud v0.22.0
github.com/hooklift/gowsdl v0.5.0
github.com/infobloxopen/infoblox-go-client v1.1.1
github.com/json-iterator/go v1.1.12 // indirect
github.com/linki/instrumented_http v0.3.0
github.com/linode/linodego v0.32.2
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/maxatome/go-testdeep v1.10.1
github.com/miekg/dns v1.1.36-0.20210109083720-731b191cabd1
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/nesv/go-dynect v0.6.0
github.com/nic-at/rc0go v1.1.1
github.com/onsi/gomega v1.14.0 // indirect
github.com/openshift/api v0.0.0-20200605231317-fb2a6ca106ae
github.com/openshift/client-go v0.0.0-20200608144219-584632b8fc73
github.com/oracle/oci-go-sdk v21.4.0+incompatible
github.com/oracle/oci-go-sdk v24.3.0+incompatible
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014
github.com/pkg/errors v0.9.1
github.com/projectcontour/contour v1.18.2
github.com/projectcontour/contour v1.20.0
github.com/prometheus/client_golang v1.11.0
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f
github.com/sirupsen/logrus v1.8.1
github.com/smartystreets/gunit v1.3.4 // indirect
github.com/stretchr/testify v1.7.0
github.com/terra-farm/udnssdk v1.3.5 // indirect
github.com/transip/gotransip/v6 v6.6.2
github.com/ukfast/sdk-go v1.4.23
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556
github.com/vultr/govultr/v2 v2.9.0
go.etcd.io/etcd/api/v3 v3.5.0
go.etcd.io/etcd/client/v3 v3.5.0
go.uber.org/ratelimit v0.2.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/api v0.58.0
gopkg.in/ini.v1 v1.62.0 // indirect
google.golang.org/api v0.66.0
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
gopkg.in/yaml.v2 v2.4.0
istio.io/api v0.0.0-20210128181506-0c4b8e54850f
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
k8s.io/client-go v0.22.2
k8s.io/klog/v2 v2.20.0 // indirect
k8s.io/utils v0.0.0-20210820185131-d34e5cb4466e // indirect
k8s.io/api v0.23.1
k8s.io/apimachinery v0.23.3
k8s.io/client-go v0.23.1
)
require (
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Masterminds/semver v1.4.2 // indirect
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
github.com/alecthomas/colour v0.1.0 // indirect
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/go-playground/locales v0.12.1 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect
github.com/gofrs/uuid v3.2.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/imdario/mergo v0.3.12 // 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
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.1 // indirect
github.com/jcmturner/rpc/v2 v2.0.2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.28.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/smartystreets/gunit v1.3.4 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/terra-farm/udnssdk v1.3.5 // indirect
github.com/ukfast/go-durationstring v1.0.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44 // indirect
google.golang.org/grpc v1.42.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/go-playground/validator.v9 v9.27.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/resty.v1 v1.12.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect
sigs.k8s.io/controller-runtime v0.11.0 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

530
go.sum

File diff suppressed because it is too large Load Diff

36
main.go
View File

@ -62,6 +62,7 @@ import (
"sigs.k8s.io/external-dns/provider/rcode0"
"sigs.k8s.io/external-dns/provider/rdns"
"sigs.k8s.io/external-dns/provider/rfc2136"
"sigs.k8s.io/external-dns/provider/safedns"
"sigs.k8s.io/external-dns/provider/scaleway"
"sigs.k8s.io/external-dns/provider/transip"
"sigs.k8s.io/external-dns/provider/ultradns"
@ -136,7 +137,7 @@ func main() {
}
// Lookup all the selected sources by names and pass them the desired configuration.
sources, err := source.ByNames(&source.SingletonClientGenerator{
sources, err := source.ByNames(ctx, &source.SingletonClientGenerator{
KubeConfig: cfg.KubeConfig,
APIServerURL: cfg.APIServerURL,
// If update events are enabled, disable timeout.
@ -206,13 +207,13 @@ func main() {
log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
cfg.Registry = "aws-sd"
}
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun)
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID)
case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "azure-private-dns":
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "bluecat":
p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, domainFilter, zoneIDFilter, cfg.DryRun)
p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, cfg.BluecatDNSConfiguration, cfg.BluecatDNSServerName, cfg.BluecatDNSDeployType, cfg.BluecatDNSView, cfg.BluecatGatewayHost, cfg.BluecatRootZone, domainFilter, zoneIDFilter, cfg.DryRun, cfg.BluecatSkipTLSVerify)
case "vinyldns":
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "vultr":
@ -238,19 +239,20 @@ func main() {
case "infoblox":
p, err = infoblox.NewInfobloxProvider(
infoblox.InfobloxConfig{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
Host: cfg.InfobloxGridHost,
Port: cfg.InfobloxWapiPort,
Username: cfg.InfobloxWapiUsername,
Password: cfg.InfobloxWapiPassword,
Version: cfg.InfobloxWapiVersion,
SSLVerify: cfg.InfobloxSSLVerify,
View: cfg.InfobloxView,
MaxResults: cfg.InfobloxMaxResults,
DryRun: cfg.DryRun,
FQDNRexEx: cfg.InfobloxFQDNRegEx,
CreatePTR: cfg.InfobloxCreatePTR,
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
Host: cfg.InfobloxGridHost,
Port: cfg.InfobloxWapiPort,
Username: cfg.InfobloxWapiUsername,
Password: cfg.InfobloxWapiPassword,
Version: cfg.InfobloxWapiVersion,
SSLVerify: cfg.InfobloxSSLVerify,
View: cfg.InfobloxView,
MaxResults: cfg.InfobloxMaxResults,
DryRun: cfg.DryRun,
FQDNRexEx: cfg.InfobloxFQDNRegEx,
CreatePTR: cfg.InfobloxCreatePTR,
CacheDuration: cfg.InfobloxCacheDuration,
},
)
case "dyn":
@ -324,6 +326,8 @@ func main() {
p, err = godaddy.NewGoDaddyProvider(ctx, domainFilter, cfg.GoDaddyTTL, cfg.GoDaddyAPIKey, cfg.GoDaddySecretKey, cfg.GoDaddyOTE, cfg.DryRun)
case "gandi":
p, err = gandi.NewGandiProvider(ctx, domainFilter, cfg.DryRun)
case "safedns":
p, err = safedns.NewSafeDNSProvider(domainFilter, cfg.DryRun)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}

View File

@ -88,11 +88,19 @@ type Config struct {
AWSAPIRetries int
AWSPreferCNAME bool
AWSZoneCacheDuration time.Duration
AWSSDServiceCleanup bool
AzureConfigFile string
AzureResourceGroup string
AzureSubscriptionID string
AzureUserAssignedIdentityClientID string
BluecatDNSConfiguration string
BluecatConfigFile string
BluecatDNSView string
BluecatGatewayHost string
BluecatRootZone string
BluecatDNSServerName string
BluecatDNSDeployType string
BluecatSkipTLSVerify bool
CloudflareProxied bool
CloudflareZonesPerPage int
CoreDNSPrefix string
@ -113,6 +121,7 @@ type Config struct {
InfobloxMaxResults int
InfobloxFQDNRegEx string
InfobloxCreatePTR bool
InfobloxCacheDuration int
DynCustomerName string
DynUsername string
DynPassword string `secure:"yes"`
@ -220,10 +229,12 @@ var defaultConfig = &Config{
AWSAPIRetries: 3,
AWSPreferCNAME: false,
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
BluecatDNSDeployType: "no-deploy",
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
CoreDNSPrefix: "/skydns/",
@ -244,6 +255,7 @@ var defaultConfig = &Config{
InfobloxMaxResults: 0,
InfobloxFQDNRegEx: "",
InfobloxCreatePTR: false,
InfobloxCacheDuration: 0,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
InMemoryZones: []string{},
OVHEndpoint: "ovh-eu",
@ -388,7 +400,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("default-targets", "Set globally default IP address that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets)
// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi", "safedns")
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
app.Flag("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default(defaultConfig.RegexDomainFilter.String()).RegexpVar(&cfg.RegexDomainFilter)
@ -410,11 +422,22 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("aws-api-retries", "When using the AWS provider, set the maximum number of retries for API calls before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries)
app.Flag("aws-prefer-cname", "When using the AWS provider, prefer using CNAME instead of ALIAS (default: disabled)").BoolVar(&cfg.AWSPreferCNAME)
app.Flag("aws-zones-cache-duration", "When using the AWS provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AWSZoneCacheDuration.String()).DurationVar(&cfg.AWSZoneCacheDuration)
app.Flag("aws-sd-service-cleanup", "When using the AWS CloudMap provider, delete empty Services without endpoints (default: disabled)").BoolVar(&cfg.AWSSDServiceCleanup)
app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile)
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (required when --provider=azure-private-dns)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
app.Flag("azure-subscription-id", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure-private-dns)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID)
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("bluecat-config-file", "When using the Bluecat provider, specify the Bluecat configuration file (required when --provider=bluecat").Default(defaultConfig.BluecatConfigFile).StringVar(&cfg.BluecatConfigFile)
// Flags related to BlueCat provider
app.Flag("bluecat-dns-configuration", "When using the Bluecat provider, specify the Bluecat DNS configuration string (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatDNSConfiguration)
app.Flag("bluecat-config-file", "When using the Bluecat provider, specify the Bluecat configuration file (optional when --provider=bluecat)").Default(defaultConfig.BluecatConfigFile).StringVar(&cfg.BluecatConfigFile)
app.Flag("bluecat-dns-view", "When using the Bluecat provider, specify the Bluecat DNS view string (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatDNSView)
app.Flag("bluecat-gateway-host", "When using the Bluecat provider, specify the Bluecat Gateway Host (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatGatewayHost)
app.Flag("bluecat-root-zone", "When using the Bluecat provider, specify the Bluecat root zone (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatRootZone)
app.Flag("bluecat-skip-tls-verify", "When using the Bluecat provider, specify to skip TLS verification (optional when --provider=bluecat) (default: false)").BoolVar(&cfg.BluecatSkipTLSVerify)
app.Flag("bluecat-dns-server-name", "When using the Bluecat provider, specify the Bluecat DNS Server to initiate deploys against. This is only used if --bluecat-dns-deploy-type is not 'no-deploy' (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatDNSServerName)
app.Flag("bluecat-dns-deploy-type", "When using the Bluecat provider, specify the type of DNS deployment to initiate after records are updated. Valid options are 'full-deploy' and 'no-deploy'. Deploy will only execute if --bluecat-dns-server-name is set (optional when --provider=bluecat)").Default(defaultConfig.BluecatDNSDeployType).StringVar(&cfg.BluecatDNSDeployType)
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-zones-per-page", "When using the Cloudflare provider, specify how many zones per page listed, max. possible 50 (default: 50)").Default(strconv.Itoa(defaultConfig.CloudflareZonesPerPage)).IntVar(&cfg.CloudflareZonesPerPage)
app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)
@ -434,6 +457,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("infoblox-max-results", "Add _max_results as query parameter to the URL on all API requests. The default is 0 which means _max_results is not set and the default of the server is used.").Default(strconv.Itoa(defaultConfig.InfobloxMaxResults)).IntVar(&cfg.InfobloxMaxResults)
app.Flag("infoblox-fqdn-regex", "Apply this regular expression as a filter for obtaining zone_auth objects. This is disabled by default.").Default(defaultConfig.InfobloxFQDNRegEx).StringVar(&cfg.InfobloxFQDNRegEx)
app.Flag("infoblox-create-ptr", "When using the Infoblox provider, create a ptr entry in addition to an entry").Default(strconv.FormatBool(defaultConfig.InfobloxCreatePTR)).BoolVar(&cfg.InfobloxCreatePTR)
app.Flag("infoblox-cache-duration", "When using the Infoblox provider, set the record TTL (0s to disable).").Default(strconv.Itoa(defaultConfig.InfobloxCacheDuration)).IntVar(&cfg.InfobloxCacheDuration)
app.Flag("dyn-customer-name", "When using the Dyn provider, specify the Customer Name").Default("").StringVar(&cfg.DynCustomerName)
app.Flag("dyn-username", "When using the Dyn provider, specify the Username").Default("").StringVar(&cfg.DynUsername)
app.Flag("dyn-password", "When using the Dyn provider, specify the password").Default("").StringVar(&cfg.DynPassword)

View File

@ -63,10 +63,18 @@ var (
AWSAPIRetries: 3,
AWSPreferCNAME: false,
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
BluecatDNSConfiguration: "",
BluecatDNSServerName: "",
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
BluecatDNSView: "",
BluecatGatewayHost: "",
BluecatRootZone: "",
BluecatDNSDeployType: defaultConfig.BluecatDNSDeployType,
BluecatSkipTLSVerify: false,
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
CoreDNSPrefix: "/skydns/",
@ -153,10 +161,18 @@ var (
AWSAPIRetries: 13,
AWSPreferCNAME: true,
AWSZoneCacheDuration: 10 * time.Second,
AWSSDServiceCleanup: true,
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
AzureSubscriptionID: "arg",
BluecatDNSConfiguration: "arg",
BluecatDNSServerName: "arg",
BluecatConfigFile: "bluecat.json",
BluecatDNSView: "arg",
BluecatGatewayHost: "arg",
BluecatRootZone: "arg",
BluecatDNSDeployType: "full-deploy",
BluecatSkipTLSVerify: true,
CloudflareProxied: true,
CloudflareZonesPerPage: 20,
CoreDNSPrefix: "/coredns/",
@ -209,7 +225,7 @@ var (
TransIPAccountName: "transip",
TransIPPrivateKeyFile: "/path/to/transip.key",
DigitalOceanAPIPageSize: 100,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS},
RFC2136BatchChangeSize: 100,
}
)
@ -257,7 +273,14 @@ func TestParseFlags(t *testing.T) {
"--azure-config-file=azure.json",
"--azure-resource-group=arg",
"--azure-subscription-id=arg",
"--bluecat-dns-configuration=arg",
"--bluecat-config-file=bluecat.json",
"--bluecat-dns-view=arg",
"--bluecat-dns-server-name=arg",
"--bluecat-gateway-host=arg",
"--bluecat-root-zone=arg",
"--bluecat-dns-deploy-type=full-deploy",
"--bluecat-skip-tls-verify",
"--cloudflare-proxied",
"--cloudflare-zones-per-page=20",
"--coredns-prefix=/coredns/",
@ -304,6 +327,7 @@ func TestParseFlags(t *testing.T) {
"--aws-api-retries=13",
"--aws-prefer-cname",
"--aws-zones-cache-duration=10s",
"--aws-sd-service-cleanup",
"--no-aws-evaluate-target-health",
"--policy=upsert-only",
"--registry=noop",
@ -330,6 +354,9 @@ func TestParseFlags(t *testing.T) {
"--transip-account=transip",
"--transip-keyfile=/path/to/transip.key",
"--digitalocean-api-page-size=100",
"--managed-record-types=A",
"--managed-record-types=CNAME",
"--managed-record-types=NS",
"--rfc2136-batch-change-size=100",
},
envVars: map[string]string{},
@ -360,7 +387,14 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json",
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
"EXTERNAL_DNS_BLUECAT_DNS_CONFIGURATION": "arg",
"EXTERNAL_DNS_BLUECAT_DNS_SERVER_NAME": "arg",
"EXTERNAL_DNS_BLUECAT_DNS_DEPLOY_TYPE": "full-deploy",
"EXTERNAL_DNS_BLUECAT_CONFIG_FILE": "bluecat.json",
"EXTERNAL_DNS_BLUECAT_DNS_VIEW": "arg",
"EXTERNAL_DNS_BLUECAT_GATEWAY_HOST": "arg",
"EXTERNAL_DNS_BLUECAT_ROOT_ZONE": "arg",
"EXTERNAL_DNS_BLUECAT_SKIP_TLS_VERIFY": "1",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_CLOUDFLARE_ZONES_PER_PAGE": "20",
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
@ -404,6 +438,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AWS_API_RETRIES": "13",
"EXTERNAL_DNS_AWS_PREFER_CNAME": "true",
"EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s",
"EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
@ -429,6 +464,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
"EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100",
"EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nCNAME\nNS",
"EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100",
},
expected: overriddenConfig,

View File

@ -223,7 +223,7 @@ func (p AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint
return endpoints, err
}
for _, zone := range zones.Zones {
recordsets, err := p.client.GetRecordsets(zone.Zone, dns.RecordsetQueryArgs{})
recordsets, err := p.client.GetRecordsets(zone.Zone, dns.RecordsetQueryArgs{ShowAll: true})
if err != nil {
log.Errorf("Recordsets retrieval for zone: '%s' failed! %s", zone.Zone, err.Error())
continue
@ -242,7 +242,7 @@ func (p AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint
continue
}
var temp interface{} = int64(recordset.TTL)
var ttl endpoint.TTL = endpoint.TTL(temp.(int64))
var ttl = endpoint.TTL(temp.(int64))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(recordset.Name,
recordset.Type,
ttl,

View File

@ -94,7 +94,7 @@ func (m *MockAlibabaCloudDNSAPI) UpdateDomainRecord(request *alidns.UpdateDomain
}
func (m *MockAlibabaCloudDNSAPI) DescribeDomains(request *alidns.DescribeDomainsRequest) (response *alidns.DescribeDomainsResponse, err error) {
var result alidns.Domains
var result alidns.DomainsInDescribeDomains
for _, record := range m.records {
domain := alidns.Domain{}
domain.DomainName = record.DomainName
@ -209,7 +209,7 @@ func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(request *pvtz.Describe
response = pvtz.CreateDescribeZoneInfoResponse()
response.ZoneId = m.zone.ZoneId
response.ZoneName = m.zone.ZoneName
response.BindVpcs = pvtz.BindVpcs{Vpc: m.zone.Vpcs.Vpc}
response.BindVpcs = pvtz.BindVpcsInDescribeZoneInfo{Vpc: m.zone.Vpcs.Vpc}
return response, nil
}

View File

@ -119,6 +119,8 @@ var (
"elb.af-south-1.amazonaws.com": "Z203XCE67M25HM",
// Global Accelerator
"awsglobalaccelerator.com": "Z2BJ6XQ5FK7U4H",
// Cloudfront
"cloudfront.net": "Z2FDTNDATAQYW2",
}
)
@ -290,10 +292,7 @@ func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone
// wildcardUnescape converts \\052.abc back to *.abc
// Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk
func wildcardUnescape(s string) string {
if strings.Contains(s, "\\052") {
s = strings.Replace(s, "\\052", "*", 1)
}
return s
return strings.Replace(s, "\\052", "*", 1)
}
// Records returns the list of records in a given hosted zone.
@ -312,9 +311,6 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos
for _, r := range resp.ResourceRecordSets {
newEndpoints := make([]*endpoint.Endpoint, 0)
// TODO(linki, ownership): Remove once ownership system is in place.
// See: https://github.com/kubernetes-sigs/external-dns/pull/122/files/74e2c3d3e237411e619aefc5aab694742001cdec#r109863370
if !provider.SupportedRecordType(aws.StringValue(r.Type)) {
continue
}
@ -889,8 +885,5 @@ func canonicalHostedZone(hostname string) string {
// cleanZoneID removes the "/hostedzone/" prefix
func cleanZoneID(id string) string {
if strings.HasPrefix(id, "/hostedzone/") {
id = strings.TrimPrefix(id, "/hostedzone/")
}
return id
return strings.TrimPrefix(id, "/hostedzone/")
}

View File

@ -64,12 +64,12 @@ var (
type AWSSDClient interface {
CreateService(input *sd.CreateServiceInput) (*sd.CreateServiceOutput, error)
DeregisterInstance(input *sd.DeregisterInstanceInput) (*sd.DeregisterInstanceOutput, error)
GetService(input *sd.GetServiceInput) (*sd.GetServiceOutput, error)
ListInstancesPages(input *sd.ListInstancesInput, fn func(*sd.ListInstancesOutput, bool) bool) error
ListNamespacesPages(input *sd.ListNamespacesInput, fn func(*sd.ListNamespacesOutput, bool) bool) error
ListServicesPages(input *sd.ListServicesInput, fn func(*sd.ListServicesOutput, bool) bool) error
RegisterInstance(input *sd.RegisterInstanceInput) (*sd.RegisterInstanceOutput, error)
UpdateService(input *sd.UpdateServiceInput) (*sd.UpdateServiceOutput, error)
DeleteService(input *sd.DeleteServiceInput) (*sd.DeleteServiceOutput, error)
}
// AWSSDProvider is an implementation of Provider for AWS Cloud Map.
@ -81,10 +81,14 @@ type AWSSDProvider struct {
namespaceFilter endpoint.DomainFilter
// filter namespace by type (private or public)
namespaceTypeFilter *sd.NamespaceFilter
// enables service without instances cleanup
cleanEmptyService bool
// filter services for removal
ownerID string
}
// NewAWSSDProvider initializes a new AWS Cloud Map based Provider.
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, assumeRole string, dryRun bool) (*AWSSDProvider, error) {
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, assumeRole string, dryRun, cleanEmptyService bool, ownerID string) (*AWSSDProvider, error) {
config := aws.NewConfig()
config = config.WithHTTPClient(
@ -113,9 +117,11 @@ func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string,
provider := &AWSSDProvider{
client: sd.New(sess),
dryRun: dryRun,
namespaceFilter: domainFilter,
namespaceTypeFilter: newSdNamespaceFilter(namespaceType),
dryRun: dryRun,
cleanEmptyService: cleanEmptyService,
ownerID: ownerID,
}
return provider, nil
@ -162,6 +168,12 @@ func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp
ep := p.instancesToEndpoint(ns, srv, instances)
endpoints = append(endpoints, ep)
}
if len(instances) == 0 {
err = p.DeleteService(srv)
if err != nil {
log.Warnf("Failed to delete service \"%s\", error: %s", aws.StringValue(srv.Name), err)
}
}
}
}
@ -285,9 +297,8 @@ func (p *AWSSDProvider) submitCreates(namespaces []*sd.NamespaceSummary, changes
}
// update local list of services
services[*srv.Name] = srv
} else if (ch.RecordTTL.IsConfigured() && *srv.DnsConfig.DnsRecords[0].TTL != int64(ch.RecordTTL)) ||
aws.StringValue(srv.Description) != ch.Labels[endpoint.AWSSDDescriptionLabel] {
// update service when TTL or Description differ
} else if ch.RecordTTL.IsConfigured() && *srv.DnsConfig.DnsRecords[0].TTL != int64(ch.RecordTTL) {
// update service when TTL differ
err = p.UpdateService(srv, ch)
if err != nil {
return err
@ -359,13 +370,10 @@ func (p *AWSSDProvider) ListNamespaces() ([]*sd.NamespaceSummary, error) {
// ListServicesByNamespaceID returns list of services in given namespace. Returns map[srv_name]*sd.Service
func (p *AWSSDProvider) ListServicesByNamespaceID(namespaceID *string) (map[string]*sd.Service, error) {
serviceIds := make([]*string, 0)
services := make([]*sd.ServiceSummary, 0)
f := func(resp *sd.ListServicesOutput, lastPage bool) bool {
for _, srv := range resp.Services {
serviceIds = append(serviceIds, srv.Id)
}
services = append(services, resp.Services...)
return true
}
@ -374,35 +382,31 @@ func (p *AWSSDProvider) ListServicesByNamespaceID(namespaceID *string) (map[stri
Name: aws.String(sd.ServiceFilterNameNamespaceId),
Values: []*string{namespaceID},
}},
MaxResults: aws.Int64(100),
}, f)
if err != nil {
return nil, err
}
// get detail of each listed service
services := make(map[string]*sd.Service)
for _, serviceID := range serviceIds {
service, err := p.GetServiceDetail(serviceID)
if err != nil {
return nil, err
servicesMap := make(map[string]*sd.Service)
for _, serviceSummary := range services {
service := &sd.Service{
Arn: serviceSummary.Arn,
CreateDate: serviceSummary.CreateDate,
Description: serviceSummary.Description,
DnsConfig: serviceSummary.DnsConfig,
HealthCheckConfig: serviceSummary.HealthCheckConfig,
HealthCheckCustomConfig: serviceSummary.HealthCheckCustomConfig,
Id: serviceSummary.Id,
InstanceCount: serviceSummary.InstanceCount,
Name: serviceSummary.Name,
NamespaceId: namespaceID,
Type: serviceSummary.Type,
}
services[aws.StringValue(service.Name)] = service
servicesMap[aws.StringValue(service.Name)] = service
}
return services, nil
}
// GetServiceDetail returns detail of given service
func (p *AWSSDProvider) GetServiceDetail(serviceID *string) (*sd.Service, error) {
output, err := p.client.GetService(&sd.GetServiceInput{
Id: serviceID,
})
if err != nil {
return nil, err
}
return output.Service, nil
return servicesMap, nil
}
// ListInstancesByServiceID returns list of instances registered in given service.
@ -491,6 +495,27 @@ func (p *AWSSDProvider) UpdateService(service *sd.Service, ep *endpoint.Endpoint
return nil
}
// DeleteService deletes empty Service from AWS API if its owner id match
func (p *AWSSDProvider) DeleteService(service *sd.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.Serialize(false)
if aws.StringValue(service.Description) == label[endpoint.AWSSDDescriptionLabel] {
log.Infof("Deleting service \"%s\"", *service.Name)
_, err := p.client.DeleteService(&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\"", aws.StringValue(service.Name), aws.StringValue(service.Description), label[endpoint.AWSSDDescriptionLabel])
}
return nil
}
// RegisterInstance creates a new instance in given service.
func (p *AWSSDProvider) RegisterInstance(service *sd.Service, ep *endpoint.Endpoint) error {
for _, target := range ep.Targets {
@ -578,11 +603,16 @@ func serviceToServiceSummary(service *sd.Service) *sd.ServiceSummary {
}
return &sd.ServiceSummary{
Name: service.Name,
Id: service.Id,
Arn: service.Arn,
Description: service.Description,
InstanceCount: service.InstanceCount,
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,
}
}

View File

@ -181,12 +181,27 @@ func (s *AWSSDClientStub) UpdateService(input *sd.UpdateServiceInput) (*sd.Updat
return &sd.UpdateServiceOutput{}, nil
}
func newTestAWSSDProvider(api AWSSDClient, domainFilter endpoint.DomainFilter, namespaceTypeFilter string) *AWSSDProvider {
func (s *AWSSDClientStub) DeleteService(input *sd.DeleteServiceInput) (*sd.DeleteServiceOutput, error) {
out, err := s.GetService(&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),
dryRun: false,
cleanEmptyService: true,
ownerID: ownerID,
}
}
@ -288,7 +303,7 @@ func TestAWSSDProvider_Records(t *testing.T) {
instances: instances,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
endpoints, _ := provider.Records(context.Background())
@ -316,7 +331,7 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) {
{DNSName: "service3.private.com", Targets: endpoint.Targets{"cname.target.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100},
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
ctx := context.Background()
@ -376,7 +391,7 @@ func TestAWSSDProvider_ListNamespaces(t *testing.T) {
{"domain filter", endpoint.NewDomainFilter([]string{"public.com"}), "", []*sd.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
{"non-existing domain", endpoint.NewDomainFilter([]string{"xxx.com"}), "", []*sd.NamespaceSummary{}},
} {
provider := newTestAWSSDProvider(api, tc.domainFilter, tc.namespaceTypeFilter)
provider := newTestAWSSDProvider(api, tc.domainFilter, tc.namespaceTypeFilter, "")
result, err := provider.ListNamespaces()
require.NoError(t, err)
@ -413,18 +428,21 @@ func TestAWSSDProvider_ListServicesByNamespace(t *testing.T) {
services := map[string]map[string]*sd.Service{
"private": {
"srv1": {
Id: aws.String("srv1"),
Name: aws.String("service1"),
Id: aws.String("srv1"),
Name: aws.String("service1"),
NamespaceId: aws.String("private"),
},
"srv2": {
Id: aws.String("srv2"),
Name: aws.String("service2"),
Id: aws.String("srv2"),
Name: aws.String("service2"),
NamespaceId: aws.String("private"),
},
},
"public": {
"srv3": {
Id: aws.String("srv3"),
Name: aws.String("service3"),
Id: aws.String("srv3"),
Name: aws.String("service3"),
NamespaceId: aws.String("public"),
},
},
}
@ -439,14 +457,11 @@ func TestAWSSDProvider_ListServicesByNamespace(t *testing.T) {
}{
{map[string]*sd.Service{"service1": services["private"]["srv1"], "service2": services["private"]["srv2"]}},
} {
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
result, err := provider.ListServicesByNamespaceID(namespaces["private"].Id)
require.NoError(t, err)
if !reflect.DeepEqual(result, tc.expectedServices) {
t.Errorf("AWSSDProvider.ListServicesByNamespaceID() error = %v, wantErr %v", result, tc.expectedServices)
}
assert.Equal(t, tc.expectedServices, result)
}
}
@ -495,7 +510,7 @@ func TestAWSSDProvider_ListInstancesByService(t *testing.T) {
instances: instances,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
result, err := provider.ListInstancesByServiceID(services["private"]["srv1"].Id)
require.NoError(t, err)
@ -532,7 +547,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) {
expectedServices := make(map[string]*sd.Service)
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
// A type
provider.CreateService(aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{
@ -636,7 +651,7 @@ func TestAWSSDProvider_UpdateService(t *testing.T) {
services: services,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
// update service with different TTL
provider.UpdateService(services["private"]["srv1"], &endpoint.Endpoint{
@ -647,6 +662,56 @@ func TestAWSSDProvider_UpdateService(t *testing.T) {
assert.Equal(t, int64(100), *api.services["private"]["srv1"].DnsConfig.DnsRecords[0].TTL)
}
func TestAWSSDProvider_DeleteService(t *testing.T) {
namespaces := map[string]*sd.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
},
}
services := map[string]map[string]*sd.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"),
},
"srv2": {
Id: aws.String("srv2"),
Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"),
Name: aws.String("service2"),
NamespaceId: aws.String("private"),
},
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: services,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "owner-id")
// delete fist service
err := provider.DeleteService(services["private"]["srv1"])
assert.NoError(t, err)
assert.Len(t, api.services["private"], 1)
expectedServices := map[string]*sd.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"),
},
}
assert.Equal(t, expectedServices, api.services["private"])
}
func TestAWSSDProvider_RegisterInstance(t *testing.T) {
namespaces := map[string]*sd.Namespace{
"private": {
@ -703,7 +768,7 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) {
instances: make(map[string]map[string]*sd.Instance),
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
expectedInstances := make(map[string]*sd.Instance)
@ -820,7 +885,7 @@ func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
instances: instances,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
provider.DeregisterInstance(services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))

View File

@ -216,7 +216,7 @@ func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneID
},
}
mockZoneListResultPage := privatedns.NewPrivateZoneListResultPage(pageIterator.getNextPage)
mockZoneListResultPage := privatedns.NewPrivateZoneListResultPage(privatedns.PrivateZoneListResult{}, pageIterator.getNextPage)
mockZoneClientIterator := privatedns.NewPrivateZoneListResultIterator(mockZoneListResultPage)
zonesClient := mockPrivateZonesClient{
mockZonesClientIterator: &mockZoneClientIterator,
@ -230,7 +230,8 @@ func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneID
},
},
}
mockRecordSetListResultPage := privatedns.NewRecordSetListResultPage(resultPageIterator.getNextPage)
mockRecordSetListResultPage := privatedns.NewRecordSetListResultPage(privatedns.RecordSetListResult{}, resultPageIterator.getNextPage)
mockRecordSetListIterator := privatedns.NewRecordSetListResultIterator(mockRecordSetListResultPage)
recordSetsClient := mockPrivateRecordSetsClient{
mockRecordSetListIterator: &mockRecordSetListIterator,
@ -370,7 +371,7 @@ func testAzurePrivateDNSApplyChangesInternal(t *testing.T, dryRun bool, client P
zlr,
}
mockZoneListResultPage := privatedns.NewPrivateZoneListResultPage(func(ctxParam context.Context, zlrParam privatedns.PrivateZoneListResult) (privatedns.PrivateZoneListResult, error) {
mockZoneListResultPage := privatedns.NewPrivateZoneListResultPage(privatedns.PrivateZoneListResult{}, func(ctxParam context.Context, zlrParam privatedns.PrivateZoneListResult) (privatedns.PrivateZoneListResult, error) {
if len(results) > 0 {
result := results[0]
results = nil

View File

@ -215,7 +215,7 @@ func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter e
},
}
mockZoneListResultPage := dns.NewZoneListResultPage(pageIterator.getNextPage)
mockZoneListResultPage := dns.NewZoneListResultPage(dns.ZoneListResult{}, pageIterator.getNextPage)
mockZoneClientIterator := dns.NewZoneListResultIterator(mockZoneListResultPage)
zonesClient := mockZonesClient{
mockZonesClientIterator: &mockZoneClientIterator,
@ -229,7 +229,8 @@ func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter e
},
},
}
mockRecordSetListResultPage := dns.NewRecordSetListResultPage(resultPageIterator.getNextPage)
mockRecordSetListResultPage := dns.NewRecordSetListResultPage(dns.RecordSetListResult{}, resultPageIterator.getNextPage)
mockRecordSetListIterator := dns.NewRecordSetListResultIterator(mockRecordSetListResultPage)
recordSetsClient := mockRecordSetsClient{
mockRecordSetListIterator: &mockRecordSetListIterator,
@ -377,7 +378,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC
zlr,
}
mockZoneListResultPage := dns.NewZoneListResultPage(func(ctxParam context.Context, zlrParam dns.ZoneListResult) (dns.ZoneListResult, error) {
mockZoneListResultPage := dns.NewZoneListResultPage(dns.ZoneListResult{}, func(ctxParam context.Context, zlrParam dns.ZoneListResult) (dns.ZoneListResult, error) {
if len(results) > 0 {
result := results[0]
results = nil
@ -512,7 +513,7 @@ func testAzureApplyChangesInternalZoneName(t *testing.T, dryRun bool, client Rec
zlr,
}
mockZoneListResultPage := dns.NewZoneListResultPage(func(ctxParam context.Context, zlrParam dns.ZoneListResult) (dns.ZoneListResult, error) {
mockZoneListResultPage := dns.NewZoneListResultPage(dns.ZoneListResult{}, func(ctxParam context.Context, zlrParam dns.ZoneListResult) (dns.ZoneListResult, error) {
if len(results) > 0 {
result := results[0]
results = nil

View File

@ -44,6 +44,8 @@ type bluecatConfig struct {
GatewayUsername string `json:"gatewayUsername,omitempty"`
GatewayPassword string `json:"gatewayPassword,omitempty"`
DNSConfiguration string `json:"dnsConfiguration"`
DNSServerName string `json:"dnsServerName"`
DNSDeployType string `json:"dnsDeployType"`
View string `json:"dnsView"`
RootZone string `json:"rootZone"`
SkipTLSVerify bool `json:"skipTLSVerify"`
@ -57,6 +59,8 @@ type BluecatProvider struct {
dryRun bool
RootZone string
DNSConfiguration string
DNSServerName string
DNSDeployType string
View string
gatewayClient GatewayClient
}
@ -76,6 +80,7 @@ type GatewayClient interface {
getTXTRecord(name string, record *BluecatTXTRecord) error
createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (res interface{}, err error)
deleteTXTRecord(name string, zone string) error
serverFullDeploy() error
}
// GatewayClientConfig defines new client on bluecat gateway
@ -86,6 +91,7 @@ type GatewayClientConfig struct {
DNSConfiguration string
View string
RootZone string
DNSServerName string
SkipTLSVerify bool
}
@ -144,26 +150,48 @@ type bluecatCreateTXTRecordRequest struct {
Text string `json:"txt"`
}
type bluecatServerFullDeployRequest struct {
ServerName string `json:"server_name"`
}
// NewBluecatProvider creates a new Bluecat provider.
//
// Returns a pointer to the provider or an error if a provider could not be created.
func NewBluecatProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (*BluecatProvider, error) {
contents, err := ioutil.ReadFile(configFile)
func NewBluecatProvider(configFile, dnsConfiguration, dnsServerName, dnsDeployType, dnsView, gatewayHost, rootZone string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun, skipTLSVerify bool) (*BluecatProvider, error) {
cfg := bluecatConfig{}
contents, err := os.ReadFile(configFile)
if err != nil {
return nil, errors.Wrapf(err, "failed to read Bluecat config file %v", configFile)
if errors.Is(err, os.ErrNotExist) {
cfg = bluecatConfig{
GatewayHost: gatewayHost,
DNSConfiguration: dnsConfiguration,
DNSServerName: dnsServerName,
DNSDeployType: dnsDeployType,
View: dnsView,
RootZone: rootZone,
SkipTLSVerify: skipTLSVerify,
GatewayUsername: "",
GatewayPassword: "",
}
} else {
return nil, errors.Wrapf(err, "failed to read Bluecat config file %v", configFile)
}
} else {
err = json.Unmarshal(contents, &cfg)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse Bluecat JSON config file %v", configFile)
}
}
cfg := bluecatConfig{}
err = json.Unmarshal(contents, &cfg)
if err != nil {
return nil, errors.Wrapf(err, "failed to read Bluecat config file %v", configFile)
if !isValidDNSDeployType(cfg.DNSDeployType) {
return nil, errors.Errorf("%v is not a valid deployment type", cfg.DNSDeployType)
}
token, cookie, err := getBluecatGatewayToken(cfg)
if err != nil {
return nil, errors.Wrap(err, "failed to get API token from Bluecat Gateway")
}
gatewayClient := NewGatewayClient(cookie, token, cfg.GatewayHost, cfg.DNSConfiguration, cfg.View, cfg.RootZone, cfg.SkipTLSVerify)
gatewayClient := NewGatewayClient(cookie, token, cfg.GatewayHost, cfg.DNSConfiguration, cfg.View, cfg.RootZone, cfg.DNSServerName, cfg.SkipTLSVerify)
provider := &BluecatProvider{
domainFilter: domainFilter,
@ -171,6 +199,8 @@ func NewBluecatProvider(configFile string, domainFilter endpoint.DomainFilter, z
dryRun: dryRun,
gatewayClient: gatewayClient,
DNSConfiguration: cfg.DNSConfiguration,
DNSServerName: cfg.DNSServerName,
DNSDeployType: cfg.DNSDeployType,
View: cfg.View,
RootZone: cfg.RootZone,
}
@ -178,7 +208,9 @@ func NewBluecatProvider(configFile string, domainFilter endpoint.DomainFilter, z
}
// NewGatewayClient creates and returns a new Bluecat gateway client
func NewGatewayClient(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone string, skipTLSVerify bool) GatewayClientConfig {
func NewGatewayClient(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone, dnsServerName string, skipTLSVerify bool) GatewayClientConfig {
// TODO: do not handle defaulting here
//
// Right now the Bluecat gateway doesn't seem to have a way to get the root zone from the API. If the user
// doesn't provide one via the config file we'll assume it's 'com'
if rootZone == "" {
@ -189,6 +221,7 @@ func NewGatewayClient(cookie http.Cookie, token, gatewayHost, dnsConfiguration,
Token: token,
Host: gatewayHost,
DNSConfiguration: dnsConfiguration,
DNSServerName: dnsServerName,
View: view,
RootZone: rootZone,
SkipTLSVerify: skipTLSVerify,
@ -278,8 +311,6 @@ func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.En
}
endpoints = append(endpoints, ep)
}
// TODO: add bluecat deploy API call here
}
log.Debugf("fetched %d records from Bluecat", len(endpoints))
@ -302,6 +333,20 @@ func (p *BluecatProvider) ApplyChanges(ctx context.Context, changes *plan.Change
p.deleteRecords(deleted)
p.createRecords(created)
if p.DNSServerName != "" {
switch p.DNSDeployType {
case "full-deploy":
err := p.gatewayClient.serverFullDeploy()
if err != nil {
return err
}
case "no-deploy":
log.Debug("Not executing deploy because DNSDeployType is set to 'no-deploy'")
}
} else {
log.Debug("Not executing deploy because server name was not provided")
}
return nil
}
@ -922,6 +967,41 @@ func (c GatewayClientConfig) deleteTXTRecord(name string, zone string) error {
return nil
}
func (c GatewayClientConfig) serverFullDeploy() error {
log.Infof("Executing full deploy on server %s", c.DNSServerName)
httpClient := newHTTPClient(c.SkipTLSVerify)
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/server/full_deploy/"
requestBody := bluecatServerFullDeployRequest{
ServerName: c.DNSServerName,
}
body, err := json.Marshal(requestBody)
if err != nil {
return errors.Wrap(err, "could not marshal body for server full deploy")
}
request, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return errors.Wrap(err, "error building http request")
}
request.Header.Add("Content-Type", "application/json")
response, err := httpClient.Do(request)
if err != nil {
return errors.Wrap(err, "error executing full deploy")
}
if response.StatusCode != http.StatusCreated {
responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return errors.Wrap(err, "failed to read full deploy response body")
}
return errors.Errorf("got HTTP response code %v, detailed message: %v", response.StatusCode, string(responseBody))
}
return nil
}
//buildHTTPRequest builds a standard http Request and adds authentication headers required by Bluecat Gateway
func (c GatewayClientConfig) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, url, body)
@ -947,6 +1027,17 @@ func splitProperties(props string) map[string]string {
return propMap
}
// isValidDNSDeployType validates the deployment type provided by a users configuration is supported by the Bluecat Provider.
func isValidDNSDeployType(deployType string) bool {
validDNSDeployTypes := []string{"no-deploy", "full-deploy"}
for _, t := range validDNSDeployTypes {
if t == deployType {
return true
}
}
return false
}
//expandZone takes an absolute domain name such as 'example.com' and returns a zone hierarchy used by Bluecat Gateway,
//such as '/zones/com/zones/example/zones/'
func expandZone(zone string) string {

View File

@ -109,6 +109,9 @@ func (g mockGatewayClient) deleteTXTRecord(name string, zone string) error {
*g.mockBluecatTXTs = nil
return nil
}
func (g mockGatewayClient) serverFullDeploy() error {
return nil
}
func (g mockGatewayClient) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) {
request, _ := http.NewRequest("GET", fmt.Sprintf("%s/users", "http://some.com/api/v1"), nil)
@ -374,11 +377,12 @@ func TestBluecatNewGatewayClient(t *testing.T) {
testToken := "exampleToken"
testgateWayHost := "exampleHost"
testDNSConfiguration := "exampleDNSConfiguration"
testDNSServer := "exampleServer"
testView := "testView"
testZone := "example.com"
testVerify := true
client := NewGatewayClient(testCookie, testToken, testgateWayHost, testDNSConfiguration, testView, testZone, testVerify)
client := NewGatewayClient(testCookie, testToken, testgateWayHost, testDNSConfiguration, testView, testZone, testDNSServer, testVerify)
if client.Cookie.Value != testCookie.Value || client.Cookie.Name != testCookie.Name || client.Token != testToken || client.Host != testgateWayHost || client.DNSConfiguration != testDNSConfiguration || client.View != testView || client.RootZone != testZone || client.SkipTLSVerify != testVerify {
t.Fatal("Client values dont match")
@ -475,6 +479,21 @@ func TestBluecatRecordset(t *testing.T) {
assert.Equal(t, cnameActual.res, cnameExpected.res)
}
func TestValidDeployTypes(t *testing.T) {
validTypes := []string{"no-deploy", "full-deploy"}
invalidTypes := []string{"anything-else"}
for _, i := range validTypes {
if !isValidDNSDeployType(i) {
t.Fatalf("%s should be a valid deploy type", i)
}
}
for _, i := range invalidTypes {
if isValidDNSDeployType(i) {
t.Fatalf("%s should be a invalid deploy type", i)
}
}
}
func validateEndpoints(t *testing.T, actual, expected []*endpoint.Endpoint) {
assert.True(t, testutils.SameEndpoints(actual, expected), "actual and expected endpoints don't match. %s:%s", actual, expected)
}

View File

@ -141,6 +141,10 @@ func (p *HetznerProvider) submitChanges(ctx context.Context, changes []*HetznerC
}
}
logMessage := "Changing record"
if p.DryRun {
logMessage = "Would change record"
}
log.WithFields(log.Fields{
"id": change.ResourceRecordSet.ID,
"record": change.ResourceRecordSet.Name,
@ -150,7 +154,10 @@ func (p *HetznerProvider) submitChanges(ctx context.Context, changes []*HetznerC
"action": change.Action,
"zone": change.ZoneName,
"zone_id": change.ZoneID,
}).Info("Changing record")
}).Info(logMessage)
if p.DryRun {
continue
}
switch change.Action {
case hetznerCreate:

View File

@ -42,31 +42,33 @@ const (
// InfobloxConfig clarifies the method signature
type InfobloxConfig struct {
DomainFilter endpoint.DomainFilter
ZoneIDFilter provider.ZoneIDFilter
Host string
Port int
Username string
Password string
Version string
SSLVerify bool
DryRun bool
View string
MaxResults int
FQDNRexEx string
CreatePTR bool
DomainFilter endpoint.DomainFilter
ZoneIDFilter provider.ZoneIDFilter
Host string
Port int
Username string
Password string
Version string
SSLVerify bool
DryRun bool
View string
MaxResults int
FQDNRexEx string
CreatePTR bool
CacheDuration int
}
// InfobloxProvider implements the DNS provider for Infoblox.
type InfobloxProvider struct {
provider.BaseProvider
client ibclient.IBConnector
domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
view string
dryRun bool
fqdnRegEx string
createPTR bool
client ibclient.IBConnector
domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
view string
dryRun bool
fqdnRegEx string
createPTR bool
cacheDuration int
}
type infobloxRecordSet struct {
@ -146,13 +148,14 @@ func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, erro
}
provider := &InfobloxProvider{
client: client,
domainFilter: infobloxConfig.DomainFilter,
zoneIDFilter: infobloxConfig.ZoneIDFilter,
dryRun: infobloxConfig.DryRun,
view: infobloxConfig.View,
fqdnRegEx: infobloxConfig.FQDNRexEx,
createPTR: infobloxConfig.CreatePTR,
client: client,
domainFilter: infobloxConfig.DomainFilter,
zoneIDFilter: infobloxConfig.ZoneIDFilter,
dryRun: infobloxConfig.DryRun,
view: infobloxConfig.View,
fqdnRegEx: infobloxConfig.FQDNRexEx,
createPTR: infobloxConfig.CreatePTR,
cacheDuration: infobloxConfig.CacheDuration,
}
return provider, nil
@ -216,6 +219,8 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E
}
for _, res := range resH {
for _, ip := range res.Ipv4Addrs {
logrus.Debugf("Record='%s' A(H):'%s'", res.Name, ip.Ipv4Addr)
// host record is an abstraction in infoblox that combines A and PTR records
// for any host record we already should have a PTR record in infoblox, so mark it as created
newEndpoint := endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, ip.Ipv4Addr)
@ -238,6 +243,7 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E
return nil, fmt.Errorf("could not fetch CNAME records from zone '%s': %s", zone.Fqdn, err)
}
for _, res := range resC {
logrus.Debugf("Record='%s' CNAME:'%s'", res.Name, res.Canonical)
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeCNAME, res.Canonical))
}
@ -281,6 +287,8 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E
if _, err := strconv.Unquote(res.Text); err != nil {
res.Text = strconv.Quote(res.Text)
}
logrus.Debugf("Record='%s' TXT:'%s'", res.Name, res.Text)
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeTXT, res.Text))
}
}
@ -320,6 +328,11 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E
}
func (p *InfobloxProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
// Update user specified TTL (0 == disabled)
for i := range endpoints {
endpoints[i].RecordTTL = endpoint.TTL(p.cacheDuration)
}
if !p.createPTR {
return endpoints
}
@ -559,26 +572,27 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool, targ
func (p *InfobloxProvider) createRecords(created infobloxChangeMap) {
for zone, endpoints := range created {
for _, ep := range endpoints {
if p.dryRun {
for targetIndex := range ep.Targets {
if p.dryRun {
logrus.Infof(
"Would create %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
)
continue
}
logrus.Infof(
"Would create %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
"Creating %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
ep.Targets,
ep.Targets[targetIndex],
zone,
)
continue
}
logrus.Infof(
"Creating %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
)
for targetIndex := range ep.Targets {
recordSet, err := p.recordSet(ep, false, targetIndex)
if err != nil {
logrus.Errorf(
@ -611,50 +625,66 @@ func (p *InfobloxProvider) deleteRecords(deleted infobloxChangeMap) {
// Delete records first
for zone, endpoints := range deleted {
for _, ep := range endpoints {
if p.dryRun {
logrus.Infof("Would delete %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
} else {
logrus.Infof("Deleting %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
for targetIndex := range ep.Targets {
recordSet, err := p.recordSet(ep, true, targetIndex)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
continue
}
switch ep.RecordType {
case endpoint.RecordTypeA:
for _, record := range *recordSet.res.(*[]ibclient.RecordA) {
_, err = p.client.DeleteObject(record.Ref)
}
case endpoint.RecordTypePTR:
for _, record := range *recordSet.res.(*[]ibclient.RecordPTR) {
_, err = p.client.DeleteObject(record.Ref)
}
case endpoint.RecordTypeCNAME:
for _, record := range *recordSet.res.(*[]ibclient.RecordCNAME) {
_, err = p.client.DeleteObject(record.Ref)
}
case endpoint.RecordTypeTXT:
for _, record := range *recordSet.res.(*[]ibclient.RecordTXT) {
for targetIndex := range ep.Targets {
recordSet, err := p.recordSet(ep, true, targetIndex)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
continue
}
switch ep.RecordType {
case endpoint.RecordTypeA:
for _, record := range *recordSet.res.(*[]ibclient.RecordA) {
if p.dryRun {
logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "A", record.Name, record.Ipv4Addr, record.Zone)
} else {
logrus.Debugf("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "A", record.Name, record.Ipv4Addr, record.Zone)
_, err = p.client.DeleteObject(record.Ref)
}
}
if err != nil {
logrus.Errorf(
"Failed to delete %s record named '%s' for Infoblox DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
zone,
err,
)
case endpoint.RecordTypePTR:
for _, record := range *recordSet.res.(*[]ibclient.RecordPTR) {
if p.dryRun {
logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "PTR", record.PtrdName, record.Ipv4Addr, record.Zone)
} else {
logrus.Debugf("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "PTR", record.PtrdName, record.Ipv4Addr, record.Zone)
_, err = p.client.DeleteObject(record.Ref)
}
}
case endpoint.RecordTypeCNAME:
for _, record := range *recordSet.res.(*[]ibclient.RecordCNAME) {
if p.dryRun {
logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "CNAME", record.Name, record.Canonical, record.Zone)
} else {
logrus.Debugf("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "CNAME", record.Name, record.Canonical, record.Zone)
_, err = p.client.DeleteObject(record.Ref)
}
}
case endpoint.RecordTypeTXT:
for _, record := range *recordSet.res.(*[]ibclient.RecordTXT) {
if p.dryRun {
logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "TXT", record.Name, record.Text, record.Zone)
} else {
logrus.Debugf("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "TXT", record.Name, record.Text, record.Zone)
_, err = p.client.DeleteObject(record.Ref)
}
}
}
if err != nil {
logrus.Errorf(
"Failed to delete %s record named '%s' to '%s' for Infoblox DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
}
}
}

242
provider/safedns/safedns.go Normal file
View File

@ -0,0 +1,242 @@
/*
Copyright 2021 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 safedns
import (
"context"
"fmt"
"os"
log "github.com/sirupsen/logrus"
ukfClient "github.com/ukfast/sdk-go/pkg/client"
ukfConnection "github.com/ukfast/sdk-go/pkg/connection"
"github.com/ukfast/sdk-go/pkg/service/safedns"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
// SafeDNS is an interface that is a subset of the SafeDNS service API that are actually used.
// Signatures must match exactly.
type SafeDNS interface {
CreateZoneRecord(zoneName string, req safedns.CreateRecordRequest) (int, error)
DeleteZoneRecord(zoneName string, recordID int) error
GetZone(zoneName string) (safedns.Zone, error)
GetZoneRecord(zoneName string, recordID int) (safedns.Record, error)
GetZoneRecords(zoneName string, parameters ukfConnection.APIRequestParameters) ([]safedns.Record, error)
GetZones(parameters ukfConnection.APIRequestParameters) ([]safedns.Zone, error)
PatchZoneRecord(zoneName string, recordID int, patch safedns.PatchRecordRequest) (int, error)
UpdateZoneRecord(zoneName string, record safedns.Record) (int, error)
}
// SafeDNSProvider implements the DNS provider spec for UKFast SafeDNS.
type SafeDNSProvider struct {
provider.BaseProvider
Client SafeDNS
// Only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter
DryRun bool
APIRequestParams ukfConnection.APIRequestParameters
}
// ZoneRecord is a datatype to simplify management of a record in a zone.
type ZoneRecord struct {
ID int
Name string
Type safedns.RecordType
TTL safedns.RecordTTL
Zone string
Content string
}
func NewSafeDNSProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*SafeDNSProvider, error) {
token, ok := os.LookupEnv("SAFEDNS_TOKEN")
if !ok {
return nil, fmt.Errorf("no SAFEDNS_TOKEN found in environment")
}
ukfAPIConnection := ukfConnection.NewAPIKeyCredentialsAPIConnection(token)
ukfClient := ukfClient.NewClient(ukfAPIConnection)
safeDNS := ukfClient.SafeDNSService()
provider := &SafeDNSProvider{
Client: safeDNS,
domainFilter: domainFilter,
DryRun: dryRun,
APIRequestParams: *ukfConnection.NewAPIRequestParameters(),
}
return provider, nil
}
// Zones returns the list of hosted zones in the SafeDNS account
func (p *SafeDNSProvider) Zones(ctx context.Context) ([]safedns.Zone, error) {
var zones []safedns.Zone
allZones, err := p.Client.GetZones(p.APIRequestParams)
if err != nil {
return nil, err
}
// Check each found zone to see whether they match the domain filter provided. If they do, append it to the array of
// zones defined above. If not, continue to the next item in the loop.
for _, zone := range allZones {
if p.domainFilter.Match(zone.Name) {
zones = append(zones, zone)
} else {
continue
}
}
return zones, nil
}
func (p *SafeDNSProvider) ZoneRecords(ctx context.Context) ([]ZoneRecord, error) {
zones, err := p.Zones(ctx)
if err != nil {
return nil, err
}
var zoneRecords []ZoneRecord
for _, zone := range zones {
// For each zone in the zonelist, get all records of an ExternalDNS supported type.
records, err := p.Client.GetZoneRecords(zone.Name, p.APIRequestParams)
if err != nil {
return nil, err
}
for _, r := range records {
zoneRecord := ZoneRecord{
ID: r.ID,
Name: r.Name,
Type: r.Type,
TTL: r.TTL,
Zone: zone.Name,
Content: r.Content,
}
zoneRecords = append(zoneRecords, zoneRecord)
}
}
return zoneRecords, nil
}
// Records returns a list of Endpoint resources created from all records in supported zones.
func (p *SafeDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
zoneRecords, err := p.ZoneRecords(ctx)
if err != nil {
return nil, err
}
for _, r := range zoneRecords {
if provider.SupportedRecordType(string(r.Type)) {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, string(r.Type), endpoint.TTL(r.TTL), r.Content))
}
}
return endpoints, nil
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *SafeDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
// Identify the zone name for each record
zoneNameIDMapper := provider.ZoneIDName{}
zones, err := p.Zones(ctx)
if err != nil {
return err
}
for _, zone := range zones {
zoneNameIDMapper.Add(zone.Name, zone.Name)
}
zoneRecords, err := p.ZoneRecords(ctx)
if err != nil {
return err
}
for _, endpoint := range changes.Create {
_, ZoneName := zoneNameIDMapper.FindZone(endpoint.DNSName)
for _, target := range endpoint.Targets {
request := safedns.CreateRecordRequest{
Name: endpoint.DNSName,
Type: endpoint.RecordType,
Content: target,
}
log.WithFields(log.Fields{
"zoneID": ZoneName,
"dnsName": endpoint.DNSName,
"recordType": endpoint.RecordType,
"Value": target,
}).Info("Creating record")
_, err := p.Client.CreateZoneRecord(ZoneName, request)
if err != nil {
return err
}
}
}
for _, endpoint := range changes.UpdateNew {
// Currently iterates over each zoneRecord in ZoneRecords for each Endpoint
// in UpdateNew; the same will go for Delete. As it's double-iteration,
// that's O(n^2), which isn't great. No performance issues have been noted
// thus far.
var zoneRecord ZoneRecord
for _, target := range endpoint.Targets {
for _, zr := range zoneRecords {
if zr.Name == endpoint.DNSName && zr.Content == target {
zoneRecord = zr
break
}
}
newTTL := safedns.RecordTTL(int(endpoint.RecordTTL))
newRecord := safedns.PatchRecordRequest{
Name: endpoint.DNSName,
Content: target,
TTL: &newTTL,
Type: endpoint.RecordType,
}
log.WithFields(log.Fields{
"zoneID": zoneRecord.Zone,
"dnsName": newRecord.Name,
"recordType": newRecord.Type,
"Value": newRecord.Content,
"Priority": newRecord.Priority,
}).Info("Patching record")
_, err = p.Client.PatchZoneRecord(zoneRecord.Zone, zoneRecord.ID, newRecord)
if err != nil {
return err
}
}
}
for _, endpoint := range changes.Delete {
// As above, currently iterates in O(n^2). May be a good start for optimisations.
var zoneRecord ZoneRecord
for _, zr := range zoneRecords {
if zr.Name == endpoint.DNSName && string(zr.Type) == endpoint.RecordType {
zoneRecord = zr
break
}
}
log.WithFields(log.Fields{
"zoneID": zoneRecord.Zone,
"dnsName": zoneRecord.Name,
"recordType": zoneRecord.Type,
}).Info("Deleting record")
err := p.Client.DeleteZoneRecord(zoneRecord.Zone, zoneRecord.ID)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,366 @@
/*
Copyright 2021 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 safedns
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
ukfConnection "github.com/ukfast/sdk-go/pkg/connection"
"github.com/ukfast/sdk-go/pkg/service/safedns"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
)
// Create an implementation of the SafeDNS interface for Mocking
type MockSafeDNSService struct {
mock.Mock
}
func (m *MockSafeDNSService) CreateZoneRecord(zoneName string, req safedns.CreateRecordRequest) (int, error) {
args := m.Called(zoneName, req)
return args.Int(0), args.Error(1)
}
func (m *MockSafeDNSService) DeleteZoneRecord(zoneName string, recordID int) error {
args := m.Called(zoneName, recordID)
return args.Error(0)
}
func (m *MockSafeDNSService) GetZone(zoneName string) (safedns.Zone, error) {
args := m.Called(zoneName)
return args.Get(0).(safedns.Zone), args.Error(1)
}
func (m *MockSafeDNSService) GetZoneRecord(zoneName string, recordID int) (safedns.Record, error) {
args := m.Called(zoneName, recordID)
return args.Get(0).(safedns.Record), args.Error(1)
}
func (m *MockSafeDNSService) GetZoneRecords(zoneName string, parameters ukfConnection.APIRequestParameters) ([]safedns.Record, error) {
args := m.Called(zoneName, parameters)
return args.Get(0).([]safedns.Record), args.Error(1)
}
func (m *MockSafeDNSService) GetZones(parameters ukfConnection.APIRequestParameters) ([]safedns.Zone, error) {
args := m.Called(parameters)
return args.Get(0).([]safedns.Zone), args.Error(1)
}
func (m *MockSafeDNSService) PatchZoneRecord(zoneName string, recordID int, patch safedns.PatchRecordRequest) (int, error) {
args := m.Called(zoneName, recordID, patch)
return args.Int(0), args.Error(1)
}
func (m *MockSafeDNSService) UpdateZoneRecord(zoneName string, record safedns.Record) (int, error) {
args := m.Called(zoneName, record)
return args.Int(0), args.Error(1)
}
// Utility functions
func createZones() []safedns.Zone {
return []safedns.Zone{
{Name: "foo.com", Description: "Foo dot com"},
{Name: "bar.io", Description: ""},
{Name: "baz.org", Description: "Org"},
}
}
func createFooRecords() []safedns.Record {
return []safedns.Record{
{
ID: 11,
Type: safedns.RecordTypeA,
Name: "foo.com",
Content: "targetFoo",
TTL: safedns.RecordTTL(3600),
},
{
ID: 12,
Type: safedns.RecordTypeTXT,
Name: "foo.com",
Content: "text",
TTL: safedns.RecordTTL(3600),
},
{
ID: 13,
Type: safedns.RecordTypeCAA,
Name: "foo.com",
Content: "",
TTL: safedns.RecordTTL(3600),
},
}
}
func createBarRecords() []safedns.Record {
return []safedns.Record{}
}
func createBazRecords() []safedns.Record {
return []safedns.Record{
{
ID: 31,
Type: safedns.RecordTypeA,
Name: "baz.org",
Content: "targetBaz",
TTL: safedns.RecordTTL(3600),
},
{
ID: 32,
Type: safedns.RecordTypeTXT,
Name: "baz.org",
Content: "text",
TTL: safedns.RecordTTL(3600),
},
{
ID: 33,
Type: safedns.RecordTypeA,
Name: "api.baz.org",
Content: "targetBazAPI",
TTL: safedns.RecordTTL(3600),
},
{
ID: 34,
Type: safedns.RecordTypeTXT,
Name: "api.baz.org",
Content: "text",
TTL: safedns.RecordTTL(3600),
},
}
}
// Actual tests
func TestNewSafeDNSProvider(t *testing.T) {
_ = os.Setenv("SAFEDNS_TOKEN", "DUMMYVALUE")
_, err := NewSafeDNSProvider(endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true)
require.NoError(t, err)
_ = os.Unsetenv("SAFEDNS_TOKEN")
_, err = NewSafeDNSProvider(endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true)
require.Error(t, err)
}
func TestRecords(t *testing.T) {
mockSafeDNSService := MockSafeDNSService{}
provider := &SafeDNSProvider{
Client: &mockSafeDNSService,
domainFilter: endpoint.NewDomainFilter([]string{}),
DryRun: false,
}
mockSafeDNSService.On(
"GetZones",
mock.Anything,
).Return(createZones(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"foo.com",
mock.Anything,
).Return(createFooRecords(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"bar.io",
mock.Anything,
).Return(createBarRecords(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"baz.org",
mock.Anything,
).Return(createBazRecords(), nil).Once()
actual, err := provider.Records(context.Background())
require.NoError(t, err)
expected := []*endpoint.Endpoint{
{
DNSName: "foo.com",
Targets: []string{"targetFoo"},
RecordType: "A",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "foo.com",
Targets: []string{"text"},
RecordType: "TXT",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "baz.org",
Targets: []string{"targetBaz"},
RecordType: "A",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "baz.org",
Targets: []string{"text"},
RecordType: "TXT",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "api.baz.org",
Targets: []string{"targetBazAPI"},
RecordType: "A",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "api.baz.org",
Targets: []string{"text"},
RecordType: "TXT",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
}
mockSafeDNSService.AssertExpectations(t)
assert.Equal(t, expected, actual)
}
func TestSafeDNSApplyChanges(t *testing.T) {
mockSafeDNSService := MockSafeDNSService{}
provider := &SafeDNSProvider{
Client: &mockSafeDNSService,
domainFilter: endpoint.NewDomainFilter([]string{}),
DryRun: false,
}
// Dummy data
mockSafeDNSService.On(
"GetZones",
mock.Anything,
).Return(createZones(), nil).Once()
mockSafeDNSService.On(
"GetZones",
mock.Anything,
).Return(createZones(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"foo.com",
mock.Anything,
).Return(createFooRecords(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"bar.io",
mock.Anything,
).Return(createBarRecords(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"baz.org",
mock.Anything,
).Return(createBazRecords(), nil).Once()
// Apply actions
mockSafeDNSService.On(
"DeleteZoneRecord",
"baz.org",
33,
).Return(nil).Once()
mockSafeDNSService.On(
"DeleteZoneRecord",
"baz.org",
34,
).Return(nil).Once()
TTL300 := safedns.RecordTTL(300)
mockSafeDNSService.On(
"PatchZoneRecord",
"foo.com",
11,
safedns.PatchRecordRequest{
Type: "A",
Name: "foo.com",
Content: "targetFoo",
TTL: &TTL300,
},
).Return(123, nil).Once()
mockSafeDNSService.On(
"CreateZoneRecord",
"bar.io",
safedns.CreateRecordRequest{
Type: "A",
Name: "create.bar.io",
Content: "targetBar",
},
).Return(246, nil).Once()
mockSafeDNSService.On(
"CreateZoneRecord",
"bar.io",
safedns.CreateRecordRequest{
Type: "A",
Name: "bar.io",
Content: "targetBar",
},
).Return(369, nil).Once()
err := provider.ApplyChanges(context.Background(), &plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "create.bar.io",
RecordType: "A",
Targets: []string{"targetBar"},
RecordTTL: 3600,
},
{
DNSName: "bar.io",
RecordType: "A",
Targets: []string{"targetBar"},
RecordTTL: 3600,
},
},
Delete: []*endpoint.Endpoint{
{
DNSName: "api.baz.org",
RecordType: "A",
},
{
DNSName: "api.baz.org",
RecordType: "TXT",
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "foo.com",
RecordType: "A",
RecordTTL: 300,
Targets: []string{"targetFoo"},
},
},
UpdateOld: []*endpoint.Endpoint{},
})
require.NoError(t, err)
mockSafeDNSService.AssertExpectations(t)
}

View File

@ -30,7 +30,6 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
@ -64,6 +63,7 @@ type ambassadorHostSource struct {
// NewAmbassadorHostSource creates a new ambassadorHostSource with the given config.
func NewAmbassadorHostSource(
ctx context.Context,
dynamicKubeClient dynamic.Interface,
kubeClient kubernetes.Interface,
namespace string) (Source, error) {
@ -82,8 +82,7 @@ func NewAmbassadorHostSource(
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil {
return nil, err

View File

@ -28,7 +28,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
@ -53,6 +52,7 @@ type httpProxySource struct {
// NewContourHTTPProxySource creates a new contourHTTPProxySource with the given config.
func NewContourHTTPProxySource(
ctx context.Context,
dynamicKubeClient dynamic.Interface,
namespace string,
annotationFilter string,
@ -78,8 +78,7 @@ func NewContourHTTPProxySource(
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
// wait for the local cache to be populated.
if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil {

View File

@ -88,6 +88,7 @@ func (suite *HTTPProxySuite) SetupTest() {
var err error
suite.source, err = NewContourHTTPProxySource(
context.TODO(),
fakeDynamicClient,
"default",
"",
@ -184,6 +185,7 @@ func TestNewContourHTTPProxySource(t *testing.T) {
fakeDynamicClient, _ := newDynamicKubernetesClient()
_, err := NewContourHTTPProxySource(
context.TODO(),
fakeDynamicClient,
"",
ti.annotationFilter,
@ -1033,6 +1035,7 @@ func testHTTPProxyEndpoints(t *testing.T) {
}
httpProxySource, err := NewContourHTTPProxySource(
context.TODO(),
fakeDynamicClient,
ti.targetNamespace,
ti.annotationFilter,
@ -1059,6 +1062,7 @@ func newTestHTTPProxySource() (*httpProxySource, error) {
fakeDynamicClient, _ := newDynamicKubernetesClient()
src, err := NewContourHTTPProxySource(
context.TODO(),
fakeDynamicClient,
"default",
"",

View File

@ -28,7 +28,6 @@ import (
networkv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/util/wait"
kubeinformers "k8s.io/client-go/informers"
netinformers "k8s.io/client-go/informers/networking/v1"
"k8s.io/client-go/kubernetes"
@ -67,7 +66,7 @@ type ingressSource struct {
}
// NewIngressSource creates a new ingressSource with the given config.
func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector, ingressClassNames []string) (Source, error) {
func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector, ingressClassNames []string) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate)
if err != nil {
return nil, err
@ -101,8 +100,7 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
// wait for the local cache to be populated.
if err := waitForCacheSync(context.Background(), informerFactory); err != nil {

View File

@ -56,6 +56,7 @@ func (suite *IngressSuite) SetupTest() {
suite.NoError(err, "should succeed")
suite.sc, err = NewIngressSource(
context.TODO(),
fakeClient,
"",
"",
@ -151,6 +152,7 @@ func TestNewIngressSource(t *testing.T) {
t.Parallel()
_, err := NewIngressSource(
context.TODO(),
fake.NewSimpleClientset(),
"",
ti.annotationFilter,
@ -1291,6 +1293,7 @@ func testIngressEndpoints(t *testing.T) {
}
source, _ := NewIngressSource(
context.TODO(),
fakeClient,
ti.targetNamespace,
ti.annotationFilter,

View File

@ -30,7 +30,6 @@ import (
networkingv1alpha3informer "istio.io/client-go/pkg/informers/externalversions/networking/v1alpha3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
@ -56,6 +55,7 @@ type gatewaySource struct {
// NewIstioGatewaySource creates a new gatewaySource with the given config.
func NewIstioGatewaySource(
ctx context.Context,
kubeClient kubernetes.Interface,
istioClient istioclient.Interface,
namespace string,
@ -93,9 +93,8 @@ func NewIstioGatewaySource(
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
istioInformerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
istioInformerFactory.Start(ctx.Done())
// wait for the local cache to be populated.
if err := waitForCacheSync(context.Background(), informerFactory); err != nil {

View File

@ -69,6 +69,7 @@ func (suite *GatewaySuite) SetupTest() {
}
suite.source, err = NewIstioGatewaySource(
context.TODO(),
fakeKubernetesClient,
fakeIstioClient,
"",
@ -142,6 +143,7 @@ func TestNewIstioGatewaySource(t *testing.T) {
t.Parallel()
_, err := NewIstioGatewaySource(
context.TODO(),
fake.NewSimpleClientset(),
istiofake.NewSimpleClientset(),
"",
@ -1165,6 +1167,7 @@ func testGatewayEndpoints(t *testing.T) {
}
gatewaySource, err := NewIstioGatewaySource(
context.TODO(),
fakeKubernetesClient,
fakeIstioClient,
ti.targetNamespace,
@ -1201,6 +1204,7 @@ func newTestGatewaySource(loadBalancerList []fakeIngressGatewayService) (*gatewa
}
src, err := NewIstioGatewaySource(
context.TODO(),
fakeKubernetesClient,
fakeIstioClient,
"",

View File

@ -31,7 +31,6 @@ import (
networkingv1alpha3informer "istio.io/client-go/pkg/informers/externalversions/networking/v1alpha3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
@ -60,6 +59,7 @@ type virtualServiceSource struct {
// NewIstioVirtualServiceSource creates a new virtualServiceSource with the given config.
func NewIstioVirtualServiceSource(
ctx context.Context,
kubeClient kubernetes.Interface,
istioClient istioclient.Interface,
namespace string,
@ -97,9 +97,8 @@ func NewIstioVirtualServiceSource(
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
istioInformerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
istioInformerFactory.Start(ctx.Done())
// wait for the local cache to be populated.
if err := waitForCacheSync(context.Background(), informerFactory); err != nil {

View File

@ -89,6 +89,7 @@ func (suite *VirtualServiceSuite) SetupTest() {
suite.NoError(err, "should succeed")
suite.source, err = NewIstioVirtualServiceSource(
context.TODO(),
fakeKubernetesClient,
fakeIstioClient,
"",
@ -165,6 +166,7 @@ func TestNewIstioVirtualServiceSource(t *testing.T) {
t.Parallel()
_, err := NewIstioVirtualServiceSource(
context.TODO(),
fake.NewSimpleClientset(),
istiofake.NewSimpleClientset(),
"",
@ -1482,6 +1484,7 @@ func testVirtualServiceEndpoints(t *testing.T) {
}
virtualServiceSource, err := NewIstioVirtualServiceSource(
context.TODO(),
fakeKubernetesClient,
fakeIstioClient,
ti.targetNamespace,
@ -1557,6 +1560,7 @@ func newTestVirtualServiceSource(loadBalancerList []fakeIngressGatewayService, g
}
src, err := NewIstioVirtualServiceSource(
context.TODO(),
fakeKubernetesClient,
fakeIstioClient,
"",

View File

@ -29,7 +29,6 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
@ -57,7 +56,7 @@ type kongTCPIngressSource struct {
}
// NewKongTCPIngressSource creates a new kongTCPIngressSource with the given config.
func NewKongTCPIngressSource(dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace string, annotationFilter string) (Source, error) {
func NewKongTCPIngressSource(ctx context.Context, dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace string, annotationFilter string) (Source, error) {
var err error
// Use shared informer to listen for add/update/delete of Host in the specified namespace.
@ -73,8 +72,7 @@ func NewKongTCPIngressSource(dynamicKubeClient dynamic.Interface, kubeClient kub
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
// wait for the local cache to be populated.
if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil {

View File

@ -241,7 +241,7 @@ func TestKongTCPIngressEndpoints(t *testing.T) {
_, err = fakeDynamicClient.Resource(kongGroupdVersionResource).Namespace(defaultKongNamespace).Create(context.Background(), &tcpi, metav1.CreateOptions{})
assert.NoError(t, err)
source, err := NewKongTCPIngressSource(fakeDynamicClient, fakeKubernetesClient, defaultKongNamespace, "kubernetes.io/ingress.class=kong")
source, err := NewKongTCPIngressSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultKongNamespace, "kubernetes.io/ingress.class=kong")
assert.NoError(t, err)
assert.NotNil(t, source)

View File

@ -25,7 +25,6 @@ import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
@ -42,7 +41,7 @@ type nodeSource struct {
}
// NewNodeSource creates a new nodeSource with the given config.
func NewNodeSource(kubeClient kubernetes.Interface, annotationFilter, fqdnTemplate string) (Source, error) {
func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotationFilter, fqdnTemplate string) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate)
if err != nil {
return nil, err
@ -62,8 +61,7 @@ func NewNodeSource(kubeClient kubernetes.Interface, annotationFilter, fqdnTempla
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
// wait for the local cache to be populated.
if err := waitForCacheSync(context.Background(), informerFactory); err != nil {

View File

@ -71,6 +71,7 @@ func testNodeSourceNewNodeSource(t *testing.T) {
t.Parallel()
_, err := NewNodeSource(
context.TODO(),
fake.NewSimpleClientset(),
ti.annotationFilter,
ti.fqdnTemplate,
@ -353,6 +354,7 @@ func testNodeSourceEndpoints(t *testing.T) {
// Create our object under test and get the endpoints.
client, err := NewNodeSource(
context.TODO(),
kubernetes,
tc.annotationFilter,
tc.fqdnTemplate,

View File

@ -29,7 +29,6 @@ import (
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint"
@ -54,6 +53,7 @@ type ocpRouteSource struct {
// NewOcpRouteSource creates a new ocpRouteSource with the given config.
func NewOcpRouteSource(
ctx context.Context,
ocpClient versioned.Interface,
namespace string,
annotationFilter string,
@ -81,8 +81,7 @@ func NewOcpRouteSource(
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
// wait for the local cache to be populated.
if err := waitForCacheSync(context.Background(), informerFactory); err != nil {

View File

@ -43,6 +43,7 @@ func (suite *OCPRouteSuite) SetupTest() {
var err error
suite.sc, err = NewOcpRouteSource(
context.TODO(),
fakeClient,
"",
"",
@ -141,6 +142,7 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) {
t.Parallel()
_, err := NewOcpRouteSource(
context.TODO(),
fake.NewSimpleClientset(),
"",
ti.annotationFilter,
@ -439,6 +441,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
require.NoError(t, err)
source, err := NewOcpRouteSource(
context.TODO(),
fakeClient,
"",
"",

View File

@ -24,7 +24,6 @@ import (
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
@ -40,7 +39,7 @@ type podSource struct {
}
// NewPodSource creates a new podSource with the given config.
func NewPodSource(kubeClient kubernetes.Interface, namespace string, compatibility string) (Source, error) {
func NewPodSource(ctx context.Context, kubeClient kubernetes.Interface, namespace string, compatibility string) (Source, error) {
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace))
podInformer := informerFactory.Core().V1().Pods()
nodeInformer := informerFactory.Core().V1().Nodes()
@ -58,7 +57,7 @@ func NewPodSource(kubeClient kubernetes.Interface, namespace string, compatibili
},
)
informerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
// wait for the local cache to be populated.
if err := waitForCacheSync(context.Background(), informerFactory); err != nil {

View File

@ -412,7 +412,7 @@ func TestPodSource(t *testing.T) {
}
}
client, err := NewPodSource(kubernetes, tc.targetNamespace, tc.compatibility)
client, err := NewPodSource(context.TODO(), kubernetes, tc.targetNamespace, tc.compatibility)
require.NoError(t, err)
endpoints, err := client.Endpoints(ctx)

View File

@ -27,7 +27,6 @@ import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
@ -67,7 +66,7 @@ type serviceSource struct {
}
// NewServiceSource creates a new serviceSource with the given config.
func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool, labelSelector labels.Selector) (Source, error) {
func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool, labelSelector labels.Selector) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate)
if err != nil {
return nil, err
@ -107,8 +106,7 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
informerFactory.Start(ctx.Done())
// wait for the local cache to be populated.
if err := waitForCacheSync(context.Background(), informerFactory); err != nil {

View File

@ -65,6 +65,7 @@ func (suite *ServiceSuite) SetupTest() {
suite.NoError(err, "should successfully create service")
suite.sc, err = NewServiceSource(
context.TODO(),
fakeClient,
"",
"",
@ -144,6 +145,7 @@ func testServiceSourceNewServiceSource(t *testing.T) {
t.Parallel()
_, err := NewServiceSource(
context.TODO(),
fake.NewSimpleClientset(),
"",
ti.annotationFilter,
@ -1039,6 +1041,7 @@ func testServiceSourceEndpoints(t *testing.T) {
// Create our object under test and get the endpoints.
client, err := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
tc.annotationFilter,
@ -1227,6 +1230,7 @@ func testMultipleServicesEndpoints(t *testing.T) {
// Create our object under test and get the endpoints.
client, err := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
tc.annotationFilter,
@ -1391,6 +1395,7 @@ func TestClusterIpServices(t *testing.T) {
}
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
tc.annotationFilter,
@ -1960,6 +1965,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
tc.annotationFilter,
@ -2295,6 +2301,7 @@ func TestHeadlessServices(t *testing.T) {
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
"",
@ -2651,6 +2658,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
"",
@ -2762,6 +2770,7 @@ func TestExternalServices(t *testing.T) {
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
"",
@ -2815,6 +2824,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
require.NoError(b, err)
client, err := NewServiceSource(
context.TODO(),
kubernetes,
v1.NamespaceAll,
"",

View File

@ -17,6 +17,7 @@ limitations under the License.
package source
import (
"context"
"net/http"
"os"
"strings"
@ -159,10 +160,10 @@ func (p *SingletonClientGenerator) OpenShiftClient() (openshift.Interface, error
}
// ByNames returns multiple Sources given multiple names.
func ByNames(p ClientGenerator, names []string, cfg *Config) ([]Source, error) {
func ByNames(ctx context.Context, p ClientGenerator, names []string, cfg *Config) ([]Source, error) {
sources := []Source{}
for _, name := range names {
source, err := BuildWithConfig(name, p, cfg)
source, err := BuildWithConfig(ctx, name, p, cfg)
if err != nil {
return nil, err
}
@ -173,32 +174,32 @@ func ByNames(p ClientGenerator, names []string, cfg *Config) ([]Source, error) {
}
// BuildWithConfig allows to generate a Source implementation from the shared config
func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, error) {
func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg *Config) (Source, error) {
switch source {
case "node":
client, err := p.KubeClient()
if err != nil {
return nil, err
}
return NewNodeSource(client, cfg.AnnotationFilter, cfg.FQDNTemplate)
return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate)
case "service":
client, err := p.KubeClient()
if err != nil {
return nil, err
}
return NewServiceSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter)
return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter)
case "ingress":
client, err := p.KubeClient()
if err != nil {
return nil, err
}
return NewIngressSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames)
return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames)
case "pod":
client, err := p.KubeClient()
if err != nil {
return nil, err
}
return NewPodSource(client, cfg.Namespace, cfg.Compatibility)
return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility)
case "istio-gateway":
kubernetesClient, err := p.KubeClient()
if err != nil {
@ -208,7 +209,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil {
return nil, err
}
return NewIstioGatewaySource(kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
return NewIstioGatewaySource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
case "istio-virtualservice":
kubernetesClient, err := p.KubeClient()
if err != nil {
@ -218,7 +219,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil {
return nil, err
}
return NewIstioVirtualServiceSource(kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
return NewIstioVirtualServiceSource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
case "cloudfoundry":
cfClient, err := p.CloudFoundryClient(cfg.CFAPIEndpoint, cfg.CFUsername, cfg.CFPassword)
if err != nil {
@ -234,13 +235,13 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil {
return nil, err
}
return NewAmbassadorHostSource(dynamicClient, kubernetesClient, cfg.Namespace)
return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace)
case "contour-httpproxy":
dynamicClient, err := p.DynamicKubernetesClient()
if err != nil {
return nil, err
}
return NewContourHTTPProxySource(dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
return NewContourHTTPProxySource(ctx, dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
case "gloo-proxy":
kubernetesClient, err := p.KubeClient()
if err != nil {
@ -256,7 +257,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil {
return nil, err
}
return NewOcpRouteSource(ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.OCPRouterName)
return NewOcpRouteSource(ctx, ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.OCPRouterName)
case "fake":
return NewFakeSource(cfg.FQDNTemplate)
case "connector":
@ -291,7 +292,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil {
return nil, err
}
return NewKongTCPIngressSource(dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter)
return NewKongTCPIngressSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter)
}
return nil, ErrSourceNotFound
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package source
import (
"context"
"errors"
"testing"
@ -115,7 +116,7 @@ func (suite *ByNamesTestSuite) TestAllInitialized() {
}: "TCPIngressesList",
}), nil)
sources, err := ByNames(mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-httpproxy", "kong-tcpingress", "fake"}, minimalConfig)
sources, err := ByNames(context.TODO(), mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-httpproxy", "kong-tcpingress", "fake"}, minimalConfig)
suite.NoError(err, "should not generate errors")
suite.Len(sources, 6, "should generate all six sources")
}
@ -124,7 +125,7 @@ func (suite *ByNamesTestSuite) TestOnlyFake() {
mockClientGenerator := new(MockClientGenerator)
mockClientGenerator.On("KubeClient").Return(fakeKube.NewSimpleClientset(), nil)
sources, err := ByNames(mockClientGenerator, []string{"fake"}, minimalConfig)
sources, err := ByNames(context.TODO(), mockClientGenerator, []string{"fake"}, minimalConfig)
suite.NoError(err, "should not generate errors")
suite.Len(sources, 1, "should generate fake source")
suite.Nil(mockClientGenerator.kubeClient, "client should not be created")
@ -134,7 +135,7 @@ func (suite *ByNamesTestSuite) TestSourceNotFound() {
mockClientGenerator := new(MockClientGenerator)
mockClientGenerator.On("KubeClient").Return(fakeKube.NewSimpleClientset(), nil)
sources, err := ByNames(mockClientGenerator, []string{"foo"}, minimalConfig)
sources, err := ByNames(context.TODO(), mockClientGenerator, []string{"foo"}, minimalConfig)
suite.Equal(err, ErrSourceNotFound, "should return source not found")
suite.Len(sources, 0, "should not returns any source")
}
@ -143,16 +144,16 @@ func (suite *ByNamesTestSuite) TestKubeClientFails() {
mockClientGenerator := new(MockClientGenerator)
mockClientGenerator.On("KubeClient").Return(nil, errors.New("foo"))
_, err := ByNames(mockClientGenerator, []string{"service"}, minimalConfig)
_, err := ByNames(context.TODO(), mockClientGenerator, []string{"service"}, minimalConfig)
suite.Error(err, "should return an error if kubernetes client cannot be created")
_, err = ByNames(mockClientGenerator, []string{"ingress"}, minimalConfig)
_, err = ByNames(context.TODO(), mockClientGenerator, []string{"ingress"}, minimalConfig)
suite.Error(err, "should return an error if kubernetes client cannot be created")
_, err = ByNames(mockClientGenerator, []string{"istio-gateway"}, minimalConfig)
_, err = ByNames(context.TODO(), mockClientGenerator, []string{"istio-gateway"}, minimalConfig)
suite.Error(err, "should return an error if kubernetes client cannot be created")
_, err = ByNames(mockClientGenerator, []string{"kong-tcpingress"}, minimalConfig)
_, err = ByNames(context.TODO(), mockClientGenerator, []string{"kong-tcpingress"}, minimalConfig)
suite.Error(err, "should return an error if kubernetes client cannot be created")
}
@ -162,10 +163,10 @@ func (suite *ByNamesTestSuite) TestIstioClientFails() {
mockClientGenerator.On("IstioClient").Return(nil, errors.New("foo"))
mockClientGenerator.On("DynamicKubernetesClient").Return(nil, errors.New("foo"))
_, err := ByNames(mockClientGenerator, []string{"istio-gateway"}, minimalConfig)
_, err := ByNames(context.TODO(), mockClientGenerator, []string{"istio-gateway"}, minimalConfig)
suite.Error(err, "should return an error if istio client cannot be created")
_, err = ByNames(mockClientGenerator, []string{"contour-httpproxy"}, minimalConfig)
_, err = ByNames(context.TODO(), mockClientGenerator, []string{"contour-httpproxy"}, minimalConfig)
suite.Error(err, "should return an error if contour client cannot be created")
}