mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-05 17:16:59 +02:00
Merge pull request #4719 from mloiseleur/chore/unmaintained-providers
chore: remove unmaintained providers
This commit is contained in:
commit
bf70e3f0ac
18
README.md
18
README.md
@ -36,13 +36,10 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz
|
||||
* [AWS Route 53](https://aws.amazon.com/route53/)
|
||||
* [AWS Cloud Map](https://docs.aws.amazon.com/cloud-map/)
|
||||
* [AzureDNS](https://azure.microsoft.com/en-us/services/dns)
|
||||
* [BlueCat](https://bluecatnetworks.com)
|
||||
* [Civo](https://www.civo.com)
|
||||
* [CloudFlare](https://www.cloudflare.com/dns)
|
||||
* [RcodeZero](https://www.rcodezero.at/)
|
||||
* [DigitalOcean](https://www.digitalocean.com/products/networking)
|
||||
* [DNSimple](https://dnsimple.com/)
|
||||
* [Dyn](https://dyn.com/dns/)
|
||||
* [OpenStack Designate](https://docs.openstack.org/designate/latest/)
|
||||
* [PowerDNS](https://www.powerdns.com/)
|
||||
* [CoreDNS](https://coredns.io/)
|
||||
@ -52,14 +49,11 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz
|
||||
* [RFC2136](https://tools.ietf.org/html/rfc2136)
|
||||
* [NS1](https://ns1.com/)
|
||||
* [TransIP](https://www.transip.eu/domain-name/)
|
||||
* [VinylDNS](https://www.vinyldns.io)
|
||||
* [Vultr](https://www.vultr.com)
|
||||
* [OVH](https://www.ovh.com)
|
||||
* [Scaleway](https://www.scaleway.com)
|
||||
* [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)
|
||||
* [ANS Group SafeDNS](https://portal.ans.co.uk/safedns/)
|
||||
* [IBM Cloud DNS](https://www.ibm.com/cloud/dns)
|
||||
* [TencentCloud PrivateDNS](https://cloud.tencent.com/product/privatedns)
|
||||
* [TencentCloud DNSPod](https://cloud.tencent.com/product/cns)
|
||||
@ -116,13 +110,10 @@ The following table clarifies the current status of the providers according to t
|
||||
| AWS Cloud Map | Beta | |
|
||||
| Akamai Edge DNS | Beta | |
|
||||
| AzureDNS | Stable | |
|
||||
| BlueCat | Alpha | @seanmalloy @vinny-sabatini |
|
||||
| Civo | Alpha | @alejandrojnm |
|
||||
| CloudFlare | Beta | |
|
||||
| RcodeZero | Alpha | |
|
||||
| DigitalOcean | Alpha | |
|
||||
| DNSimple | Alpha | |
|
||||
| Dyn | Alpha | |
|
||||
| OpenStack Designate | Alpha | |
|
||||
| PowerDNS | Alpha | |
|
||||
| CoreDNS | Alpha | |
|
||||
@ -132,15 +123,12 @@ The following table clarifies the current status of the providers according to t
|
||||
| RFC2136 | Alpha | |
|
||||
| NS1 | Alpha | |
|
||||
| TransIP | Alpha | |
|
||||
| VinylDNS | Alpha | |
|
||||
| RancherDNS | Alpha | |
|
||||
| OVH | Alpha | |
|
||||
| Scaleway DNS | Alpha | @Sh4d1 |
|
||||
| Vultr | Alpha | |
|
||||
| UltraDNS | Alpha | |
|
||||
| GoDaddy | Alpha | |
|
||||
| Gandi | Alpha | @packi |
|
||||
| SafeDNS | Alpha | @assureddt |
|
||||
| IBMCloud | Alpha | @hughhuangzh |
|
||||
| TencentCloud | Alpha | @Hyzhou |
|
||||
| Plural | Alpha | @michaeljguarino |
|
||||
@ -179,11 +167,9 @@ The following tutorials are provided:
|
||||
* [Azure Private DNS](docs/tutorials/azure-private-dns.md)
|
||||
* [Civo](docs/tutorials/civo.md)
|
||||
* [Cloudflare](docs/tutorials/cloudflare.md)
|
||||
* [BlueCat](docs/tutorials/bluecat.md)
|
||||
* [CoreDNS](docs/tutorials/coredns.md)
|
||||
* [DigitalOcean](docs/tutorials/digitalocean.md)
|
||||
* [DNSimple](docs/tutorials/dnsimple.md)
|
||||
* [Dyn](docs/tutorials/dyn.md)
|
||||
* [Exoscale](docs/tutorials/exoscale.md)
|
||||
* [ExternalName Services](docs/tutorials/externalname.md)
|
||||
* Google Kubernetes Engine
|
||||
@ -200,18 +186,14 @@ The following tutorials are provided:
|
||||
* [OpenStack Designate](docs/tutorials/designate.md)
|
||||
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
|
||||
* [PowerDNS](docs/tutorials/pdns.md)
|
||||
* [RcodeZero](docs/tutorials/rcodezero.md)
|
||||
* [RancherDNS (RDNS)](docs/tutorials/rdns.md)
|
||||
* [RFC2136](docs/tutorials/rfc2136.md)
|
||||
* [TransIP](docs/tutorials/transip.md)
|
||||
* [VinylDNS](docs/tutorials/vinyldns.md)
|
||||
* [OVH](docs/tutorials/ovh.md)
|
||||
* [Scaleway](docs/tutorials/scaleway.md)
|
||||
* [Vultr](docs/tutorials/vultr.md)
|
||||
* [UltraDNS](docs/tutorials/ultradns.md)
|
||||
* [GoDaddy](docs/tutorials/godaddy.md)
|
||||
* [Gandi](docs/tutorials/gandi.md)
|
||||
* [SafeDNS](docs/tutorials/ANS_Group_SafeDNS.md)
|
||||
* [IBM Cloud](docs/tutorials/ibmcloud.md)
|
||||
* [Nodes as source](docs/tutorials/nodes.md)
|
||||
* [TencentCloud](docs/tutorials/tencentcloud.md)
|
||||
|
@ -52,7 +52,6 @@ For set up for a specific provider using the Helm chart, see the following links
|
||||
- [godaddy](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/godaddy.md#using-helm)
|
||||
- [ns1](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/ns1.md#using-helm)
|
||||
- [plural](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/plural.md#using-helm)
|
||||
- [vultr](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/vultr.md#using-helm)
|
||||
|
||||
## Namespaced Scoped Installation
|
||||
|
||||
|
@ -47,7 +47,6 @@ For set up for a specific provider using the Helm chart, see the following links
|
||||
- [godaddy](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/godaddy.md#using-helm)
|
||||
- [ns1](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/ns1.md#using-helm)
|
||||
- [plural](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/plural.md#using-helm)
|
||||
- [vultr](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/vultr.md#using-helm)
|
||||
|
||||
## Namespaced Scoped Installation
|
||||
|
||||
|
@ -45,7 +45,6 @@ Providers
|
||||
- [x] Linode
|
||||
- [x] TransIP
|
||||
- [x] RFC2136
|
||||
- [x] Vultr
|
||||
- [x] UltraDNS
|
||||
|
||||
PRs welcome!
|
||||
@ -86,8 +85,5 @@ The Linode Provider default TTL is used when the TTL is 0. The default is 24 hou
|
||||
### TransIP Provider
|
||||
The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s.
|
||||
|
||||
### Vultr Provider
|
||||
The Vultr provider minimal TTL is used when the TTL is 0. The default is 1 hour.
|
||||
|
||||
### UltraDNS
|
||||
The UltraDNS provider minimal TTL is used when the TTL is not provided. The default TTL is account level default TTL, if defined, otherwise 24 hours.
|
||||
|
@ -1,210 +0,0 @@
|
||||
# Setting up ExternalDNS for Services on ANS Group'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:
|
||||
|
||||
[ANS Group'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 ANS Group 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 ANS Group 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: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
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: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
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
|
||||
```
|
@ -1,152 +0,0 @@
|
||||
# Setting up external-dns for BlueCat
|
||||
|
||||
The first external-dns release with with BlueCat provider support is v0.8.0.
|
||||
|
||||
## Prerequisites
|
||||
Install the BlueCat Gateway product and deploy the [community gateway workflows](https://github.com/bluecatlabs/gateway-workflows).
|
||||
|
||||
## Configuration Options
|
||||
|
||||
There are two ways to pass configuration options to the Bluecat Provider JSON configuration file and command line flags. Currently if a valid configuration file is used all
|
||||
BlueCat provider configurations will be taken from the configuration file. If a configuraiton file is not provided or cannot be read then all BlueCat provider configurations will
|
||||
be taken from the command line flags. In the future an enhancement will be made to merge configuration options from the configuration file and command line flags if both are provided.
|
||||
|
||||
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: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
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
|
||||
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 |
|
||||
| ----------------- | ------------------ |
|
||||
| gatewayHost | Yes |
|
||||
| gatewayUsername | No |
|
||||
| gatewayPassword | No |
|
||||
| dnsConfiguration | Yes |
|
||||
| dnsView | Yes |
|
||||
| rootZone | Yes |
|
||||
| dnsServerName | No |
|
||||
| dnsDeployType | No |
|
||||
| skipTLSVerify | No (default false) |
|
||||
|
||||
#### Deploy
|
||||
Setup configuration file as k8s `Secret`.
|
||||
```
|
||||
cat << EOF > ~/bluecat.json
|
||||
{
|
||||
"gatewayHost": "https://bluecatgw.example.com",
|
||||
"gatewayUsername": "user",
|
||||
"gatewayPassword": "pass",
|
||||
"dnsConfiguration": "Example",
|
||||
"dnsView": "Internal",
|
||||
"rootZone": "example.com",
|
||||
"skipTLSVerify": false
|
||||
}
|
||||
EOF
|
||||
kubectl create secret generic bluecatconfig --from-file ~/bluecat.json -n bluecat-example
|
||||
```
|
||||
|
||||
Setup up namespace, deployment, and service account:
|
||||
```
|
||||
kubectl create namespace 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
|
||||
volumes:
|
||||
- name: bluecatconfig
|
||||
secret:
|
||||
secretName: bluecatconfig
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
volumeMounts:
|
||||
- name: bluecatconfig
|
||||
mountPath: "/etc/external-dns/"
|
||||
readOnly: true
|
||||
args:
|
||||
- --log-level=debug
|
||||
- --source=service
|
||||
- --provider=bluecat
|
||||
- --txt-owner-id=bluecat-example
|
||||
- --bluecat-config-file=/etc/external-dns/bluecat.json
|
||||
EOF
|
||||
kubectl apply -f ~/bluecat.yml -n bluecat-example
|
||||
```
|
@ -1,149 +0,0 @@
|
||||
# Setting up ExternalDNS for Dyn
|
||||
|
||||
## Creating a Dyn Configuration Secret
|
||||
|
||||
For ExternalDNS to access the Dyn API, create a Kubernetes secret.
|
||||
|
||||
To create the secret:
|
||||
|
||||
```
|
||||
$ kubectl create secret generic external-dns \
|
||||
--from-literal=EXTERNAL_DNS_DYN_CUSTOMER_NAME=${DYN_CUSTOMER_NAME} \
|
||||
--from-literal=EXTERNAL_DNS_DYN_USERNAME=${DYN_USERNAME} \
|
||||
--from-literal=EXTERNAL_DNS_DYN_PASSWORD=${DYN_PASSWORD}
|
||||
```
|
||||
|
||||
The credentials are the same ones created during account registration. As best practise, you are advised to
|
||||
create an API-only user that is entitled to only the zones intended to be changed by ExternalDNS
|
||||
|
||||
## Deploy ExternalDNS
|
||||
The rest of this tutorial assumes you own `example.com` domain and your DNS provider is Dyn. Change `example.com`
|
||||
with a domain/zone that you really own.
|
||||
|
||||
In case of the dyn provider, the flag `--zone-id-filter` is mandatory as it specifies which zones to scan for records. Without it
|
||||
|
||||
|
||||
Create a deployment file called `externaldns.yaml` with the following contents:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
args:
|
||||
- --source=ingress
|
||||
- --txt-prefix=_d
|
||||
- --namespace=example
|
||||
- --zone-id-filter=example.com
|
||||
- --domain-filter=example.com
|
||||
- --provider=dyn
|
||||
env:
|
||||
- name: EXTERNAL_DNS_DYN_CUSTOMER_NAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_DYN_CUSTOMER_NAME
|
||||
- name: EXTERNAL_DNS_DYN_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_DYN_USERNAME
|
||||
- name: EXTERNAL_DNS_DYN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_DYN_PASSWORD
|
||||
EOF
|
||||
```
|
||||
|
||||
As we'll be creating an Ingress resource, you need `--txt-prefix=_d` as a CNAME cannot coexist with a TXT record. You can change the prefix to
|
||||
any valid start of a FQDN.
|
||||
|
||||
Create the deployment for ExternalDNS:
|
||||
|
||||
```
|
||||
$ kubectl create -f externaldns.yaml
|
||||
```
|
||||
|
||||
## Running a locally build version
|
||||
If you just want to test ExternalDNS in dry-run mode locally without doing the above deployment you can also do it.
|
||||
Make sure your kubectl is configured correctly . Assuming you have the sources, build and run it like so:
|
||||
|
||||
```bash
|
||||
make
|
||||
# output skipped
|
||||
|
||||
./build/external-dns \
|
||||
--provider=dyn \
|
||||
--dyn-customer-name=${DYN_CUSTOMER_NAME} \
|
||||
--dyn-username=${DYN_USERNAME} \
|
||||
--dyn-password=${DYN_PASSWORD} \
|
||||
--domain-filter=example.com \
|
||||
--zone-id-filter=example.com \
|
||||
--namespace=example \
|
||||
--log-level=debug \
|
||||
--txt-prefix=_ \
|
||||
--dry-run=true
|
||||
INFO[0000] running in dry-run mode. No changes to DNS records will be made.
|
||||
INFO[0000] Connected to cluster at https://some-k8s-cluster.example.com
|
||||
INFO[0001] Zones: [example.com]
|
||||
# output skipped
|
||||
```
|
||||
|
||||
Having `--dry-run=true` and `--log-level=debug` is a great way to see _exactly_ what DynamicDNS is doing or is about to do.
|
||||
|
||||
## Deploying an Ingress Resource
|
||||
|
||||
Create a file called 'test-ingress.yaml' with the following contents:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: test-ingress
|
||||
namespace: example
|
||||
spec:
|
||||
rules:
|
||||
- host: test-ingress.example.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: my-awesome-service
|
||||
port:
|
||||
number: 8080
|
||||
pathType: Prefix
|
||||
```
|
||||
|
||||
As the DNS name `test-ingress.example.com` matches the filter, external-dns will create two records:
|
||||
a CNAME for test-ingress.example.com and TXT for _dtest-ingress.example.com.
|
||||
|
||||
Create the Ingress:
|
||||
|
||||
```
|
||||
$ kubectl create -f test-ingress.yaml
|
||||
```
|
||||
|
||||
By default external-dns scans for changes every minute so give it some time to catch up with the
|
||||
## Verifying Dyn DNS records
|
||||
|
||||
Login to the console at https://portal.dynect.net/login/ and verify records are created
|
||||
|
||||
## Clean up
|
||||
|
||||
Login to the console at https://portal.dynect.net/login/ and delete the records created. Alternatively, just delete the sample
|
||||
Ingress resources and external-dns will delete the records.
|
@ -1,206 +0,0 @@
|
||||
# Setting up ExternalDNS for Services on RcodeZero
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using [RcodeZero Anycast DNS](https://www.rcodezero.at). Make sure to use **>=0.5.0** version of ExternalDNS for this tutorial.
|
||||
|
||||
The following steps are required to use RcodeZero with ExternalDNS:
|
||||
|
||||
1. Sign up for an RcodeZero account (or use an existing account).
|
||||
2. Add your zone to the RcodeZero DNS
|
||||
3. Enable the RcodeZero API, and generate an API key.
|
||||
4. Deploy ExternalDNS to use the RcodeZero provider.
|
||||
5. Verify the setup bey deploying a test services (optional)
|
||||
|
||||
## Creating a RcodeZero DNS zone
|
||||
|
||||
Before records can be added to your domain name automatically, you need to add your domain name to the set of zones managed by RcodeZero. In order to add the zone, perform the following steps:
|
||||
|
||||
1. Log in to the RcodeZero Dashboard, and move to the [Add Zone](https://my.rcodezero.at/domain/create) page.
|
||||
2. Select "MASTER" as domain type, and add your domain name there. Use this domain name instead of "example.com" throughout the rest of this tutorial.
|
||||
|
||||
Note that "SECONDARY" domains cannot be managed by ExternalDNS, because this would not allow modification of records in the zone.
|
||||
|
||||
## Enable the API, and create Credentials
|
||||
|
||||
> The RcodeZero Anycast-Network is provisioned via web interface or REST-API.
|
||||
|
||||
Enable the RcodeZero API to generate an API key on [RcodeZero API](https://my.rcodezero.at/enableapi). The API key will be added to the environment variable 'RC0_API_KEY' via one of the Manifest templates (as described below).
|
||||
|
||||
## Deploy ExternalDNS
|
||||
|
||||
Connect your `kubectl` client to the cluster you want to test ExternalDNS with. Choose a Manifest from below, depending on whether or not you have RBAC enabled. Before applying it, modify the Manifest as follows:
|
||||
|
||||
- Replace "example.com" with the domain name you added to RcodeZero.
|
||||
- Replace YOUR_RCODEZERO_API_KEY with the API key created above.
|
||||
- Replace YOUR_ENCRYPTION_KEY_STRING with a string to encrypt the TXT records
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=rcodezero
|
||||
- --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var.
|
||||
env:
|
||||
- name: RC0_API_KEY
|
||||
value: "YOUR_RCODEZERO_API_KEY"
|
||||
- name: RC0_ENC_VAR
|
||||
value: "YOUR_ENCRYPTION_KEY_STRING"
|
||||
```
|
||||
|
||||
### 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: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=rcodezero
|
||||
- --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var.
|
||||
env:
|
||||
- name: RC0_API_KEY
|
||||
value: "YOUR_RCODEZERO_API_KEY"
|
||||
- name: RC0_ENC_VAR
|
||||
value: "YOUR_ENCRYPTION_KEY_STRING"
|
||||
```
|
||||
|
||||
## Deploying an Nginx Service
|
||||
|
||||
After you have deployed ExternalDNS with RcodeZero, you can deploy a simple service based on Nginx to test the setup. This is optional, though highly recommended before using ExternalDNS in production.
|
||||
|
||||
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: example.com
|
||||
external-dns.alpha.kubernetes.io/ttl: "120" #optional
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
Change the file as follows:
|
||||
|
||||
- Replace the annotation of the service; use the same hostname as the RcodeZero DNS zone created above. The annotation may also be a subdomain
|
||||
of the DNS zone (e.g. 'www.example.com').
|
||||
- Set the TTL annotation of the service. A valid TTL of 120 or above must be given. This annotation is optional, and defaults to "300" if no value is given.
|
||||
|
||||
These annotations will be used to determine what services should be registered with DNS. Removing these annotations will cause ExternalDNS to remove the corresponding DNS records.
|
||||
|
||||
Create the Deployment and Service:
|
||||
|
||||
```bash
|
||||
$ kubectl create -f nginx.yaml
|
||||
```
|
||||
|
||||
Depending on your cloud provider, it might take a while to create an external IP for the service. Once an external IP address is assigned to the service, ExternalDNS will notice the new address and synchronize the RcodeZero DNS records accordingly.
|
||||
|
||||
## Verifying RcodeZero DNS records
|
||||
|
||||
Check your [RcodeZero Configured Zones](https://my.rcodezero.at/domain) and select the respective zone name. The zone should now contain the external IP address of the service as an A record.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Once you have verified that ExternalDNS successfully manages RcodeZero DNS records for external services, you can delete the tutorial example as follows:
|
||||
|
||||
```bash
|
||||
$ kubectl delete -f nginx.yaml
|
||||
$ kubectl delete -f externaldns.yaml
|
||||
```
|
@ -1,190 +0,0 @@
|
||||
# Setting up ExternalDNS for VinylDNS
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using VinylDNS.
|
||||
|
||||
The environment vars `VINYLDNS_ACCESS_KEY`, `VINYLDNS_SECRET_KEY`, and `VINYLDNS_HOST` will be needed to run ExternalDNS with VinylDNS.
|
||||
|
||||
## Create a sample deployment and service for external-dns to use
|
||||
|
||||
Run an application and expose it via a Kubernetes Service:
|
||||
|
||||
```console
|
||||
$ kubectl run nginx --image=nginx --replicas=1 --port=80
|
||||
$ kubectl expose deployment nginx --port=80 --target-port=80 --type=LoadBalancer
|
||||
```
|
||||
|
||||
Annotate the Service with your desired external DNS name. Make sure to change `example.org` to your domain.
|
||||
|
||||
```console
|
||||
$ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/hostname=nginx.example.org."
|
||||
```
|
||||
|
||||
After the service is up and running, it should get an EXTERNAL-IP. At first this may showing as `<pending>`
|
||||
|
||||
```console
|
||||
$ kubectl get svc
|
||||
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
kubernetes 10.0.0.1 <none> 443/TCP 1h
|
||||
nginx 10.0.0.115 <pending> 80:30543/TCP 10s
|
||||
```
|
||||
|
||||
Once it's available
|
||||
|
||||
```console
|
||||
% kubectl get svc
|
||||
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
kubernetes 10.0.0.1 <none> 443/TCP 1h
|
||||
nginx 10.0.0.115 34.x.x.x 80:30543/TCP 2m
|
||||
```
|
||||
|
||||
## Deploy ExternalDNS to Kubernetes
|
||||
|
||||
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
|
||||
Then apply one of the following manifests file to deploy ExternalDNS.
|
||||
|
||||
**Note for examples below**
|
||||
|
||||
When using `registry=txt` option, make sure to also use the `txt-prefix` and `txt-owner-id` options as well. If you try to create a `TXT` record in VinylDNS without a prefix, it will try to create a `TXT` record with the same name as your actual DNS record and fail (creating a stranded record `external-dns` cannot manage).
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
args:
|
||||
- --provider=vinyldns
|
||||
- --source=service
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --registry=txt
|
||||
- --txt-owner-id=grizz
|
||||
- --txt-prefix=txt-
|
||||
env:
|
||||
- name: VINYLDNS_HOST
|
||||
value: "YOUR_VINYLDNS_HOST"
|
||||
- name: VINYLDNS_ACCESS_KEY
|
||||
value: "YOUR_VINYLDNS_ACCESS_KEY"
|
||||
- name: VINYLDNS_SECRET_KEY
|
||||
value: "YOUR_VINYLDNS_SECRET_KEY"
|
||||
```
|
||||
|
||||
### 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: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
args:
|
||||
- --provider=vinyldns
|
||||
- --source=service
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --registry=txt
|
||||
- --txt-owner-id=grizz
|
||||
- --txt-prefix=txt-
|
||||
env:
|
||||
env:
|
||||
- name: VINYLDNS_HOST
|
||||
value: "YOUR_VINYLDNS_HOST"
|
||||
- name: VINYLDNS_ACCESS_KEY
|
||||
value: "YOUR_VINYLDNS_ACCESS_KEY"
|
||||
- name: VINYLDNS_SECRET_KEY
|
||||
value: "YOUR_VINYLDNS_SECRET_KEYY
|
||||
```
|
||||
|
||||
## Running a locally built version pointed to the above nginx service
|
||||
Make sure your kubectl is configured correctly. Assuming you have the sources, build and run it like below.
|
||||
|
||||
The vinyl access details needs to exported to the environment before running.
|
||||
|
||||
```bash
|
||||
make
|
||||
# output skipped
|
||||
|
||||
export VINYLDNS_HOST=<fqdn of vinyl dns api>
|
||||
export VINYLDNS_ACCESS_KEY=<access key>
|
||||
export VINYLDNS_SECRET_KEY=<secret key>
|
||||
|
||||
./build/external-dns \
|
||||
--provider=vinyldns \
|
||||
--source=service \
|
||||
--domain-filter=elements.capsps.comcast.net. \
|
||||
--zone-id-filter=20e8bfd2-3a70-4e1b-8e11-c9c1948528d3 \
|
||||
--registry=txt \
|
||||
--txt-owner-id=grizz \
|
||||
--txt-prefix=txt- \
|
||||
--namespace=default \
|
||||
--once \
|
||||
--dry-run \
|
||||
--log-level debug
|
||||
|
||||
INFO[0000] running in dry-run mode. No changes to DNS records will be made.
|
||||
INFO[0000] Created Kubernetes client https://some-k8s-cluster.example.com
|
||||
INFO[0001] Zone: [nginx.example.org.]
|
||||
# output skipped
|
||||
```
|
||||
|
||||
Having `--dry-run=true` and `--log-level=debug` is a great way to see _exactly_ what VinylDNS is doing or is about to do.
|
@ -1,225 +0,0 @@
|
||||
# Setting up ExternalDNS for Services on Vultr
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Vultr DNS.
|
||||
|
||||
Make sure to use **>=0.6** version of ExternalDNS for this tutorial.
|
||||
|
||||
## Managing DNS with Vultr
|
||||
|
||||
If you want to read up on vultr DNS service you can read the following tutorial:
|
||||
[Introduction to Vultr DNS](https://www.vultr.com/docs/introduction-to-vultr-dns)
|
||||
|
||||
Create a new DNS Zone where you want to create your records in. For the examples we will be using `example.com`
|
||||
|
||||
## Creating Vultr Credentials
|
||||
|
||||
You will need to create a new API Key which can be found on the [Vultr Dashboard](https://my.vultr.com/settings/#settingsapi).
|
||||
|
||||
The environment variable `VULTR_API_KEY` will be needed to run ExternalDNS with Vultr.
|
||||
|
||||
## Deploy ExternalDNS
|
||||
|
||||
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
|
||||
|
||||
Begin by creating a Kubernetes secret to securely store your Akamai Edge DNS Access Tokens. This key will enable ExternalDNS to authenticate with Akamai Edge DNS:
|
||||
|
||||
```shell
|
||||
kubectl create secret generic VULTR_API_KEY --from-literal=VULTR_API_KEY=YOUR_VULTR_API_KEY
|
||||
```
|
||||
|
||||
Ensure to replace YOUR_VULTR_API_KEY, with your actual Vultr API key.
|
||||
|
||||
|
||||
Then apply one of the following manifests file to deploy ExternalDNS.
|
||||
|
||||
### Using Helm
|
||||
|
||||
reate a values.yaml file to configure ExternalDNS to use Akamai Edge DNS as the DNS provider. This file should include the necessary environment variables:
|
||||
|
||||
```shell
|
||||
provider:
|
||||
name: akamai
|
||||
env:
|
||||
- name: VULTR_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: VULTR_API_KEY
|
||||
key: VULTR_API_KEY
|
||||
```
|
||||
|
||||
Finally, install the ExternalDNS chart with Helm using the configuration specified in your values.yaml file:
|
||||
|
||||
```shell
|
||||
helm upgrade --install external-dns external-dns/external-dns --values values.yaml
|
||||
```
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=vultr
|
||||
env:
|
||||
- name: VULTR_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: VULTR_API_KEY
|
||||
key: VULTR_API_KEY
|
||||
```
|
||||
|
||||
### 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: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=vultr
|
||||
env:
|
||||
- name: VULTR_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: VULTR_API_KEY
|
||||
key: VULTR_API_KEY
|
||||
```
|
||||
|
||||
## Deploying a Nginx Service
|
||||
|
||||
Create a service file called 'nginx.yaml' with the following contents:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: my-app.example.com
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
Note the annotation on the service; use the same hostname as the Vultr DNS zone created above.
|
||||
|
||||
ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.
|
||||
|
||||
Create the deployment and service:
|
||||
|
||||
```console
|
||||
$ kubectl create -f nginx.yaml
|
||||
```
|
||||
|
||||
Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.
|
||||
|
||||
Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Vultr DNS records.
|
||||
|
||||
## Verifying Vultr DNS records
|
||||
|
||||
Check your [Vultr UI](https://my.vultr.com/dns/) to view the records for your Vultr DNS zone.
|
||||
|
||||
Click on the zone for the one created above if a different domain was used.
|
||||
|
||||
This should show the external IP address of the service as the A record for your domain.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Now that we have verified that ExternalDNS will automatically manage Vultr DNS records, we can delete the tutorial's example:
|
||||
|
||||
```
|
||||
$ kubectl delete service -f nginx.yaml
|
||||
$ kubectl delete service -f externaldns.yaml
|
||||
```
|
22
go.mod
22
go.mod
@ -16,7 +16,6 @@ require (
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.0
|
||||
github.com/ans-group/sdk-go v1.20.0
|
||||
github.com/aws/aws-sdk-go v1.55.5
|
||||
github.com/bodgit/tsig v1.2.2
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
@ -34,13 +33,10 @@ require (
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gophercloud/gophercloud v1.14.0
|
||||
github.com/hooklift/gowsdl v0.5.0
|
||||
github.com/linki/instrumented_http v0.3.0
|
||||
github.com/linode/linodego v1.39.0
|
||||
github.com/maxatome/go-testdeep v1.14.0
|
||||
github.com/miekg/dns v1.1.62
|
||||
github.com/nesv/go-dynect v0.6.0
|
||||
github.com/nic-at/rc0go v1.1.1
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/openshift/api v0.0.0-20230607130528-611114dca681
|
||||
github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3
|
||||
@ -59,8 +55,6 @@ require (
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.984
|
||||
github.com/transip/gotransip/v6 v6.25.0
|
||||
github.com/ultradns/ultradns-sdk-go v1.3.7
|
||||
github.com/vinyldns/go-vinyldns v0.9.16
|
||||
github.com/vultr/govultr/v2 v2.17.2
|
||||
go.etcd.io/etcd/api/v3 v3.5.15
|
||||
go.etcd.io/etcd/client/v3 v3.5.15
|
||||
go.uber.org/ratelimit v0.3.1
|
||||
@ -90,7 +84,6 @@ require (
|
||||
github.com/Masterminds/semver v1.4.2 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect
|
||||
github.com/ans-group/go-durationstring v1.2.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
@ -134,7 +127,6 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
@ -149,7 +141,6 @@ require (
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
@ -162,7 +153,6 @@ require (
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/peterhellberg/link v1.1.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
@ -170,21 +160,12 @@ require (
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/schollz/progressbar/v3 v3.8.6 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/smartystreets/gunit v1.3.4 // indirect
|
||||
github.com/sony/gobreaker v0.5.0 // indirect
|
||||
github.com/sosodev/duration v1.2.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.10.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.17.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/terra-farm/udnssdk v1.3.5 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.5.14 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@ -200,7 +181,6 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sys v0.23.0 // indirect
|
||||
golang.org/x/term v0.23.0 // indirect
|
||||
@ -211,10 +191,8 @@ require (
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/resty.v1 v1.12.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f // indirect
|
||||
|
299
go.sum
299
go.sum
@ -2,46 +2,12 @@ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxo
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/auth v0.8.1 h1:QZW9FjC5lZzN864p13YxvAtGUlQ+KgRL+8Sg45Z6vxo=
|
||||
cloud.google.com/go/auth v0.8.1/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk=
|
||||
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
@ -133,10 +99,6 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.63.0/go.mod h1:SOSDHfe1kX91v3W5QiBsWSL
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
|
||||
github.com/ans-group/go-durationstring v1.2.0 h1:UJIuQATkp0t1rBvZsHRwki33YHV9E+Ulro+3NbMB7MM=
|
||||
github.com/ans-group/go-durationstring v1.2.0/go.mod h1:QGF9Mdpq9058QXaut8r55QWu6lcHX6i/GvF1PZVkV6o=
|
||||
github.com/ans-group/sdk-go v1.20.0 h1:2bgVIO29WJR35VuDrVMbEL8msxc5z34/J5BlQT4wb/0=
|
||||
github.com/ans-group/sdk-go v1.20.0/go.mod h1:w4tX8raa9y3j7pug6TLcF8ZW1j9G05AmNoQLBloYxEY=
|
||||
github.com/aokoli/goutils v1.1.0/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
@ -185,9 +147,6 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/civo/civogo v0.3.73 h1:thkNnkziU+xh+MEOChIUwRZI1forN20+SSAPe/VFDME=
|
||||
github.com/civo/civogo v0.3.73/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
@ -198,7 +157,6 @@ github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
|
||||
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
|
||||
@ -260,8 +218,6 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
|
||||
github.com/digitalocean/godo v1.120.0 h1:t2DpzIitSnCDNQM7svSW4+cZd8E4Lv6+r8y33Kym0Xw=
|
||||
github.com/digitalocean/godo v1.120.0/go.mod h1:WQVH83OHUy6gC4gXpEVQKtxTd4L5oCp+5OialidkPLY=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY=
|
||||
github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8=
|
||||
github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
@ -296,8 +252,6 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.3.0-java.0.20200609174644-bd816e4522c1/go.mod h1:bjmEhrMDubXDd0uKxnWwRmgSsiEv2CkJliIHnj6ETm8=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
@ -318,8 +272,6 @@ github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99/go.mod h1:4mP9w
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
@ -340,8 +292,6 @@ github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJ
|
||||
github.com/go-gandi/go-gandi v0.7.0 h1:gsP33dUspsN1M+ZW9HEgHchK9HiaSkYnltO73RHhSZA=
|
||||
github.com/go-gandi/go-gandi v0.7.0/go.mod h1:9NoYyfWCjFosClPiWjkbbRK5UViaZ4ctpT8/pKSSFlw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@ -437,8 +387,6 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
|
||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
|
||||
github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
|
||||
@ -475,26 +423,16 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
@ -521,9 +459,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@ -539,18 +475,7 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM=
|
||||
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
@ -565,13 +490,11 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gookit/color v1.2.3/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/gophercloud/gophercloud v1.14.0 h1:Bt9zQDhPrbd4qX7EILGmy+i7GP35cc+AAL2+wIJpUE8=
|
||||
@ -585,7 +508,6 @@ github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
@ -636,20 +558,14 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hooklift/gowsdl v0.5.0 h1:DE8RevqhGPLchumV/V7OwbCzfJ8lcozFg1uWC/ESCBQ=
|
||||
github.com/hooklift/gowsdl v0.5.0/go.mod h1:9kRc402w9Ci/Mek5a1DNgTmU14yPY8fMumxNVvxhis4=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
@ -695,7 +611,6 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
@ -716,7 +631,6 @@ github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQ
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@ -761,8 +675,6 @@ github.com/lyft/protoc-gen-star v0.4.10/go.mod h1:mE8fbna26u7aEA2QCVvvfBU/ZrPgoc
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@ -843,10 +755,6 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/nesv/go-dynect v0.6.0 h1:Ow/DiSm4LAISwnFku/FITSQHnU6pBvhQMsUE5Gu6Oq4=
|
||||
github.com/nesv/go-dynect v0.6.0/go.mod h1:GHRBRKzTwjAMhosHJQq/KrZaFkXIFyJ5zRE7thGXXrs=
|
||||
github.com/nic-at/rc0go v1.1.1 h1:bf2gTwYecJEh7qmnOEuarXKueZn4A8N08U1Uop3K8+s=
|
||||
github.com/nic-at/rc0go v1.1.1/go.mod h1:KEa3H5fmDNXCaXSqOeAZxkKnG/8ggr1OHIG25Ve7fjU=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
@ -917,8 +825,6 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/peterhellberg/link v1.1.0 h1:s2+RH8EGuI/mI4QwrWGSYQCRz7uNgip9BaM04HKu5kc=
|
||||
@ -936,7 +842,6 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pluralsh/gqlclient v1.12.2 h1:BrEFAASktf4quFw57CIaLAd+NZUTLhG08fe6tnhBQN4=
|
||||
github.com/pluralsh/gqlclient v1.12.2/go.mod h1:OEjN9L63x8m3A3eQBv5kVkFgiY9fp2aZ0cgOF0uII58=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@ -1001,10 +906,6 @@ github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
|
||||
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.29 h1:BkTk4gynLjguayxrYxZoMZjBnAOh7ntQvUkOFmkMqPU=
|
||||
@ -1028,32 +929,22 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
||||
github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q=
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||
github.com/smartystreets/gunit v1.3.4 h1:iHc8Rfhb/uCOc9a3KGuD3ut22L+hLIVaqR1o5fS6zC4=
|
||||
github.com/smartystreets/gunit v1.3.4/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+zMHZXV9/bvak=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
|
||||
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us=
|
||||
github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
|
||||
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
@ -1067,8 +958,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
|
||||
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
@ -1091,8 +980,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.984 h1:QLSx+ibsV68NXKgzofPuo1gxFwYSWk2++rvxZxNjbVo=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.984/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
@ -1128,10 +1015,6 @@ github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+
|
||||
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||
github.com/vektah/gqlparser/v2 v2.5.14 h1:dzLq75BJe03jjQm6n56PdH1oweB8ana42wj7E4jRy70=
|
||||
github.com/vektah/gqlparser/v2 v2.5.14/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
|
||||
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
|
||||
github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
@ -1145,9 +1028,7 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
@ -1174,9 +1055,6 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
@ -1215,7 +1093,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -1226,7 +1103,6 @@ golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@ -1242,16 +1118,7 @@ golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
@ -1259,22 +1126,14 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@ -1297,37 +1156,20 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
@ -1346,12 +1188,6 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
|
||||
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -1360,10 +1196,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -1388,52 +1222,28 @@ golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -1461,12 +1271,10 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
@ -1495,63 +1303,26 @@ golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
@ -1569,72 +1340,22 @@ gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZ
|
||||
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.192.0 h1:PljqpNAfZaaSpS+TnANfnNAXKdzHM/B9bKhwRlo7JP0=
|
||||
google.golang.org/api v0.192.0/go.mod h1:9VcphjvAxPKLmSxVSzPlSRXy/5ARMEw5bf58WoVXafQ=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f h1:b1Ln/PG8orm0SsBbHZWke8dDp2lrCD4jSmfglFpTZbk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
|
||||
@ -1645,22 +1366,14 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
@ -1671,7 +1384,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
@ -1694,10 +1406,6 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWM
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
@ -1710,7 +1418,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.12.0 h1:cqdqQoTx17JmTusfxh5m3e2b36jfUzFAZedv89pFX18=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.12.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
|
||||
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
@ -1736,11 +1443,8 @@ helm.sh/helm/v3 v3.2.4/go.mod h1:ZaXz/vzktgwjyGGFbUWtIQkscfE7WYoRGP2szqAFHR0=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
istio.io/api v1.23.0 h1:yqv3lNW6XSYS5XkbEkxsmFROXIQznp4lFWqj7xKEqCA=
|
||||
istio.io/api v1.23.0/go.mod h1:QPSTGXuIQdnZFEm3myf9NZ5uBMwCdJWUvfj9ZZ+2oBM=
|
||||
istio.io/client-go v1.23.0 h1://xojbifr84q29WE3eMx74p36hD4lvcejX1KxE3iJvY=
|
||||
@ -1798,11 +1502,8 @@ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
|
||||
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0=
|
||||
sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gEORz0efEja7A=
|
||||
sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw=
|
||||
|
29
main.go
29
main.go
@ -47,14 +47,12 @@ import (
|
||||
"sigs.k8s.io/external-dns/provider/aws"
|
||||
"sigs.k8s.io/external-dns/provider/awssd"
|
||||
"sigs.k8s.io/external-dns/provider/azure"
|
||||
"sigs.k8s.io/external-dns/provider/bluecat"
|
||||
"sigs.k8s.io/external-dns/provider/civo"
|
||||
"sigs.k8s.io/external-dns/provider/cloudflare"
|
||||
"sigs.k8s.io/external-dns/provider/coredns"
|
||||
"sigs.k8s.io/external-dns/provider/designate"
|
||||
"sigs.k8s.io/external-dns/provider/digitalocean"
|
||||
"sigs.k8s.io/external-dns/provider/dnsimple"
|
||||
"sigs.k8s.io/external-dns/provider/dyn"
|
||||
"sigs.k8s.io/external-dns/provider/exoscale"
|
||||
"sigs.k8s.io/external-dns/provider/gandi"
|
||||
"sigs.k8s.io/external-dns/provider/godaddy"
|
||||
@ -68,16 +66,12 @@ import (
|
||||
"sigs.k8s.io/external-dns/provider/pdns"
|
||||
"sigs.k8s.io/external-dns/provider/pihole"
|
||||
"sigs.k8s.io/external-dns/provider/plural"
|
||||
"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/tencentcloud"
|
||||
"sigs.k8s.io/external-dns/provider/transip"
|
||||
"sigs.k8s.io/external-dns/provider/ultradns"
|
||||
"sigs.k8s.io/external-dns/provider/vinyldns"
|
||||
"sigs.k8s.io/external-dns/provider/vultr"
|
||||
"sigs.k8s.io/external-dns/provider/webhook"
|
||||
webhookapi "sigs.k8s.io/external-dns/provider/webhook/api"
|
||||
"sigs.k8s.io/external-dns/registry"
|
||||
@ -246,20 +240,12 @@ func main() {
|
||||
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.DryRun)
|
||||
case "azure-private-dns":
|
||||
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.DryRun)
|
||||
case "bluecat":
|
||||
p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, cfg.BluecatDNSConfiguration, cfg.BluecatDNSServerName, cfg.BluecatDNSDeployType, cfg.BluecatDNSView, cfg.BluecatGatewayHost, cfg.BluecatRootZone, cfg.TXTPrefix, cfg.TXTSuffix, domainFilter, zoneIDFilter, cfg.DryRun, cfg.BluecatSkipTLSVerify)
|
||||
case "vinyldns":
|
||||
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
|
||||
case "vultr":
|
||||
p, err = vultr.NewVultrProvider(ctx, domainFilter, cfg.DryRun)
|
||||
case "ultradns":
|
||||
p, err = ultradns.NewUltraDNSProvider(domainFilter, cfg.DryRun)
|
||||
case "civo":
|
||||
p, err = civo.NewCivoProvider(domainFilter, cfg.DryRun)
|
||||
case "cloudflare":
|
||||
p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareProxied, cfg.DryRun, cfg.CloudflareDNSRecordsPerPage)
|
||||
case "rcodezero":
|
||||
p, err = rcode0.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt)
|
||||
case "google":
|
||||
p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun)
|
||||
case "digitalocean":
|
||||
@ -270,19 +256,6 @@ func main() {
|
||||
p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
|
||||
case "dnsimple":
|
||||
p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
|
||||
case "dyn":
|
||||
p, err = dyn.NewDynProvider(
|
||||
dyn.DynConfig{
|
||||
DomainFilter: domainFilter,
|
||||
ZoneIDFilter: zoneIDFilter,
|
||||
DryRun: cfg.DryRun,
|
||||
CustomerName: cfg.DynCustomerName,
|
||||
Username: cfg.DynUsername,
|
||||
Password: cfg.DynPassword,
|
||||
MinTTLSeconds: cfg.DynMinTTLSeconds,
|
||||
AppVersion: externaldns.Version,
|
||||
},
|
||||
)
|
||||
case "coredns", "skydns":
|
||||
p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
|
||||
case "rdns":
|
||||
@ -381,8 +354,6 @@ func main() {
|
||||
)
|
||||
case "ibmcloud":
|
||||
p, err = ibmcloud.NewIBMCloudProvider(cfg.IBMCloudConfigFile, domainFilter, zoneIDFilter, endpointsSource, cfg.IBMCloudProxied, cfg.DryRun)
|
||||
case "safedns":
|
||||
p, err = safedns.NewSafeDNSProvider(domainFilter, cfg.DryRun)
|
||||
case "plural":
|
||||
p, err = plural.NewPluralProvider(cfg.PluralCluster, cfg.PluralProvider)
|
||||
case "tencentcloud":
|
||||
|
@ -104,28 +104,15 @@ type Config struct {
|
||||
AzureSubscriptionID string
|
||||
AzureUserAssignedIdentityClientID string
|
||||
AzureActiveDirectoryAuthorityHost string
|
||||
BluecatDNSConfiguration string
|
||||
BluecatConfigFile string
|
||||
BluecatDNSView string
|
||||
BluecatGatewayHost string
|
||||
BluecatRootZone string
|
||||
BluecatDNSServerName string
|
||||
BluecatDNSDeployType string
|
||||
BluecatSkipTLSVerify bool
|
||||
CloudflareProxied bool
|
||||
CloudflareDNSRecordsPerPage int
|
||||
CoreDNSPrefix string
|
||||
RcodezeroTXTEncrypt bool
|
||||
AkamaiServiceConsumerDomain string
|
||||
AkamaiClientToken string
|
||||
AkamaiClientSecret string
|
||||
AkamaiAccessToken string
|
||||
AkamaiEdgercPath string
|
||||
AkamaiEdgercSection string
|
||||
DynCustomerName string
|
||||
DynUsername string
|
||||
DynPassword string `secure:"yes"`
|
||||
DynMinTTLSeconds int
|
||||
OCIConfigFile string
|
||||
OCICompartmentOCID string
|
||||
OCIAuthInstancePrincipal bool
|
||||
@ -272,12 +259,9 @@ var defaultConfig = &Config{
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
AzureSubscriptionID: "",
|
||||
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
|
||||
BluecatDNSDeployType: "no-deploy",
|
||||
CloudflareProxied: false,
|
||||
CloudflareDNSRecordsPerPage: 100,
|
||||
CoreDNSPrefix: "/skydns/",
|
||||
RcodezeroTXTEncrypt: false,
|
||||
AkamaiServiceConsumerDomain: "",
|
||||
AkamaiClientToken: "",
|
||||
AkamaiClientSecret: "",
|
||||
@ -456,7 +440,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew)
|
||||
|
||||
// Flags related to providers
|
||||
providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "bluecat", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "dyn", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rcodezero", "rdns", "rfc2136", "safedns", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "vinyldns", "vultr", "webhook"}
|
||||
providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rdns", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"}
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...)
|
||||
app.Flag("provider-cache-time", "The time to cache the DNS provider record list requests.").Default(defaultConfig.ProviderCacheTime.String()).DurationVar(&cfg.ProviderCacheTime)
|
||||
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
|
||||
@ -493,16 +477,6 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("tencent-cloud-config-file", "When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud)").Default(defaultConfig.TencentCloudConfigFile).StringVar(&cfg.TencentCloudConfigFile)
|
||||
app.Flag("tencent-cloud-zone-type", "When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private)").Default(defaultConfig.TencentCloudZoneType).EnumVar(&cfg.TencentCloudZoneType, "", "public", "private")
|
||||
|
||||
// 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-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage)
|
||||
app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)
|
||||
@ -512,16 +486,11 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
|
||||
app.Flag("akamai-edgerc-path", "When using the Akamai provider, specify the .edgerc file path. Path must be reachable form invocation environment. (required when --provider=akamai and *-token, secret serviceconsumerdomain not specified)").Default(defaultConfig.AkamaiEdgercPath).StringVar(&cfg.AkamaiEdgercPath)
|
||||
app.Flag("akamai-edgerc-section", "When using the Akamai provider, specify the .edgerc file path (Optional when edgerc-path is specified)").Default(defaultConfig.AkamaiEdgercSection).StringVar(&cfg.AkamaiEdgercSection)
|
||||
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)
|
||||
app.Flag("dyn-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.DynMinTTLSeconds)
|
||||
app.Flag("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile)
|
||||
app.Flag("oci-compartment-ocid", "When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records. Required when using OCI IAM instance principal authentication.").StringVar(&cfg.OCICompartmentOCID)
|
||||
app.Flag("oci-zone-scope", "When using OCI provider, filter for zones with this scope (optional, options: GLOBAL, PRIVATE). Defaults to GLOBAL, setting to empty value will target both.").Default(defaultConfig.OCIZoneScope).EnumVar(&cfg.OCIZoneScope, "", "GLOBAL", "PRIVATE")
|
||||
app.Flag("oci-auth-instance-principal", "When using the OCI provider, specify whether OCI IAM instance principal authentication should be used (instead of key-based auth via the OCI config file).").Default(strconv.FormatBool(defaultConfig.OCIAuthInstancePrincipal)).BoolVar(&cfg.OCIAuthInstancePrincipal)
|
||||
app.Flag("oci-zones-cache-duration", "When using the OCI provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.OCIZoneCacheDuration.String()).DurationVar(&cfg.OCIZoneCacheDuration)
|
||||
app.Flag("rcodezero-txt-encrypt", "When using the Rcodezero provider with txt registry option, set if TXT rrs are encrypted (default: false)").Default(strconv.FormatBool(defaultConfig.RcodezeroTXTEncrypt)).BoolVar(&cfg.RcodezeroTXTEncrypt)
|
||||
app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones)
|
||||
app.Flag("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)").Default(defaultConfig.OVHEndpoint).StringVar(&cfg.OVHEndpoint)
|
||||
app.Flag("ovh-api-rate-limit", "When using the OVH provider, specify the API request rate limit, X operations by seconds (default: 20)").Default(strconv.Itoa(defaultConfig.OVHApiRateLimit)).IntVar(&cfg.OVHApiRateLimit)
|
||||
|
@ -72,14 +72,6 @@ var (
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
AzureSubscriptionID: "",
|
||||
BluecatDNSConfiguration: "",
|
||||
BluecatDNSServerName: "",
|
||||
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
|
||||
BluecatDNSView: "",
|
||||
BluecatGatewayHost: "",
|
||||
BluecatRootZone: "",
|
||||
BluecatDNSDeployType: defaultConfig.BluecatDNSDeployType,
|
||||
BluecatSkipTLSVerify: false,
|
||||
CloudflareProxied: false,
|
||||
CloudflareDNSRecordsPerPage: 100,
|
||||
CoreDNSPrefix: "/skydns/",
|
||||
@ -117,7 +109,6 @@ var (
|
||||
ExoscaleAPISecret: "",
|
||||
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
|
||||
CRDSourceKind: "DNSEndpoint",
|
||||
RcodezeroTXTEncrypt: false,
|
||||
TransIPAccountName: "",
|
||||
TransIPPrivateKeyFile: "",
|
||||
DigitalOceanAPIPageSize: 50,
|
||||
@ -179,14 +170,6 @@ var (
|
||||
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,
|
||||
CloudflareDNSRecordsPerPage: 5000,
|
||||
CoreDNSPrefix: "/coredns/",
|
||||
@ -228,7 +211,6 @@ var (
|
||||
ExoscaleAPISecret: "2",
|
||||
CRDSourceAPIVersion: "test.k8s.io/v1alpha1",
|
||||
CRDSourceKind: "Endpoint",
|
||||
RcodezeroTXTEncrypt: true,
|
||||
NS1Endpoint: "https://api.example.com/v1",
|
||||
NS1IgnoreSSL: true,
|
||||
TransIPAccountName: "transip",
|
||||
@ -289,14 +271,6 @@ 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-dns-records-per-page=5000",
|
||||
"--coredns-prefix=/coredns/",
|
||||
@ -370,7 +344,6 @@ func TestParseFlags(t *testing.T) {
|
||||
"--exoscale-apisecret=2",
|
||||
"--crd-source-apiversion=test.k8s.io/v1alpha1",
|
||||
"--crd-source-kind=Endpoint",
|
||||
"--rcodezero-txt-encrypt",
|
||||
"--ns1-endpoint=https://api.example.com/v1",
|
||||
"--ns1-ignoressl",
|
||||
"--transip-account=transip",
|
||||
@ -414,14 +387,6 @@ 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_DNS_RECORDS_PER_PAGE": "5000",
|
||||
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
|
||||
@ -488,7 +453,6 @@ func TestParseFlags(t *testing.T) {
|
||||
"EXTERNAL_DNS_EXOSCALE_APISECRET": "2",
|
||||
"EXTERNAL_DNS_CRD_SOURCE_APIVERSION": "test.k8s.io/v1alpha1",
|
||||
"EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint",
|
||||
"EXTERNAL_DNS_RCODEZERO_TXT_ENCRYPT": "1",
|
||||
"EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1",
|
||||
"EXTERNAL_DNS_NS1_IGNORESSL": "1",
|
||||
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
|
||||
@ -536,14 +500,12 @@ func restoreEnv(t *testing.T, originalEnv map[string]string) {
|
||||
|
||||
func TestPasswordsNotLogged(t *testing.T) {
|
||||
cfg := Config{
|
||||
DynPassword: "dyn-pass",
|
||||
PDNSAPIKey: "pdns-api-key",
|
||||
RFC2136TSIGSecret: "tsig-secret",
|
||||
}
|
||||
|
||||
s := cfg.String()
|
||||
|
||||
assert.False(t, strings.Contains(s, "dyn-pass"))
|
||||
assert.False(t, strings.Contains(s, "pdns-api-key"))
|
||||
assert.False(t, strings.Contains(s, "tsig-secret"))
|
||||
}
|
||||
|
@ -61,19 +61,6 @@ func ValidateConfig(cfg *externaldns.Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Provider == "dyn" {
|
||||
if cfg.DynUsername == "" {
|
||||
return errors.New("no Dyn username specified")
|
||||
}
|
||||
if cfg.DynCustomerName == "" {
|
||||
return errors.New("no Dyn customer name specified")
|
||||
}
|
||||
|
||||
if cfg.DynMinTTLSeconds < 0 {
|
||||
return errors.New("TTL specified for Dyn is negative")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Provider == "rfc2136" {
|
||||
if cfg.RFC2136MinTTL < 0 {
|
||||
return errors.New("TTL specified for rfc2136 is negative")
|
||||
|
@ -64,59 +64,6 @@ func newValidConfig(t *testing.T) *externaldns.Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func addRequiredFieldsForDyn(cfg *externaldns.Config) {
|
||||
cfg.LogFormat = "json"
|
||||
cfg.Sources = []string{"ingress"}
|
||||
cfg.Provider = "dyn"
|
||||
}
|
||||
|
||||
func TestValidateBadDynConfig(t *testing.T) {
|
||||
badConfigs := []*externaldns.Config{
|
||||
{},
|
||||
{
|
||||
// only username
|
||||
DynUsername: "test",
|
||||
},
|
||||
{
|
||||
// only customer name
|
||||
DynCustomerName: "test",
|
||||
},
|
||||
{
|
||||
// negative timeout
|
||||
DynUsername: "test",
|
||||
DynCustomerName: "test",
|
||||
DynMinTTLSeconds: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cfg := range badConfigs {
|
||||
addRequiredFieldsForDyn(cfg)
|
||||
err := ValidateConfig(cfg)
|
||||
assert.NotNil(t, err, "Configuration %+v should NOT have passed validation", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGoodDynConfig(t *testing.T) {
|
||||
goodConfigs := []*externaldns.Config{
|
||||
{
|
||||
DynUsername: "test",
|
||||
DynCustomerName: "test",
|
||||
DynMinTTLSeconds: 600,
|
||||
},
|
||||
{
|
||||
DynUsername: "test",
|
||||
DynCustomerName: "test",
|
||||
DynMinTTLSeconds: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cfg := range goodConfigs {
|
||||
addRequiredFieldsForDyn(cfg)
|
||||
err := ValidateConfig(cfg)
|
||||
assert.Nil(t, err, "Configuration should be valid, got this error instead", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBadIgnoreHostnameAnnotationsConfig(t *testing.T) {
|
||||
cfg := externaldns.NewConfig()
|
||||
cfg.IgnoreHostnameAnnotation = true
|
||||
|
@ -1,6 +0,0 @@
|
||||
approvers:
|
||||
- seanmalloy
|
||||
- vinny-sabatini
|
||||
reviewers:
|
||||
- seanmalloy
|
||||
- vinny-sabatini
|
@ -1,511 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Ensure we have proper error handling/logging for API calls to Bluecat. getBluecatGatewayToken has a good example of this
|
||||
// TODO: Remove studdering
|
||||
// TODO: Make API calls more consistent (eg error handling on HTTP response codes)
|
||||
// TODO: zone-id-filter does not seem to work with our provider
|
||||
|
||||
package bluecat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
api "sigs.k8s.io/external-dns/provider/bluecat/gateway"
|
||||
)
|
||||
|
||||
// BluecatProvider implements the DNS provider for Bluecat DNS
|
||||
type BluecatProvider struct {
|
||||
provider.BaseProvider
|
||||
domainFilter endpoint.DomainFilter
|
||||
zoneIDFilter provider.ZoneIDFilter
|
||||
dryRun bool
|
||||
RootZone string
|
||||
DNSConfiguration string
|
||||
DNSServerName string
|
||||
DNSDeployType string
|
||||
View string
|
||||
gatewayClient api.GatewayClient
|
||||
TxtPrefix string
|
||||
TxtSuffix string
|
||||
}
|
||||
|
||||
type bluecatRecordSet struct {
|
||||
obj interface{}
|
||||
res interface{}
|
||||
}
|
||||
|
||||
// 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, dnsConfiguration, dnsServerName, dnsDeployType, dnsView, gatewayHost, rootZone, txtPrefix, txtSuffix string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun, skipTLSVerify bool) (*BluecatProvider, error) {
|
||||
cfg := api.BluecatConfig{}
|
||||
contents, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
cfg = api.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)
|
||||
}
|
||||
}
|
||||
|
||||
if !api.IsValidDNSDeployType(cfg.DNSDeployType) {
|
||||
return nil, errors.Errorf("%v is not a valid deployment type", cfg.DNSDeployType)
|
||||
}
|
||||
|
||||
token, cookie, err := api.GetBluecatGatewayToken(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get API token from Bluecat Gateway")
|
||||
}
|
||||
gatewayClient := api.NewGatewayClientConfig(cookie, token, cfg.GatewayHost, cfg.DNSConfiguration, cfg.View, cfg.RootZone, cfg.DNSServerName, cfg.SkipTLSVerify)
|
||||
|
||||
provider := &BluecatProvider{
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: dryRun,
|
||||
gatewayClient: gatewayClient,
|
||||
DNSConfiguration: cfg.DNSConfiguration,
|
||||
DNSServerName: cfg.DNSServerName,
|
||||
DNSDeployType: cfg.DNSDeployType,
|
||||
View: cfg.View,
|
||||
RootZone: cfg.RootZone,
|
||||
TxtPrefix: txtPrefix,
|
||||
TxtSuffix: txtSuffix,
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// Records fetches Host, CNAME, and TXT records from bluecat gateway
|
||||
func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
|
||||
zones, err := p.zones()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not fetch zones")
|
||||
}
|
||||
|
||||
// Parsing Text records first, so we can get the owner from them.
|
||||
for _, zone := range zones {
|
||||
log.Debugf("fetching records from zone '%s'", zone)
|
||||
|
||||
var resT []api.BluecatTXTRecord
|
||||
err = p.gatewayClient.GetTXTRecords(zone, &resT)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not fetch TXT records for zone: %v", zone)
|
||||
}
|
||||
for _, rec := range resT {
|
||||
tempEndpoint := endpoint.NewEndpoint(rec.Name, endpoint.RecordTypeTXT, rec.Properties)
|
||||
tempEndpoint.Labels[endpoint.OwnerLabelKey], err = extractOwnerfromTXTRecord(rec.Properties)
|
||||
if err != nil {
|
||||
log.Debugf("External DNS Owner %s", err)
|
||||
}
|
||||
endpoints = append(endpoints, tempEndpoint)
|
||||
}
|
||||
|
||||
var resH []api.BluecatHostRecord
|
||||
err = p.gatewayClient.GetHostRecords(zone, &resH)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not fetch host records for zone: %v", zone)
|
||||
}
|
||||
var ep *endpoint.Endpoint
|
||||
for _, rec := range resH {
|
||||
propMap := api.SplitProperties(rec.Properties)
|
||||
ips := strings.Split(propMap["addresses"], ",")
|
||||
for _, ip := range ips {
|
||||
if _, ok := propMap["ttl"]; ok {
|
||||
ttl, err := strconv.Atoi(propMap["ttl"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse ttl '%d' as int for host record %v", ttl, rec.Name)
|
||||
}
|
||||
ep = endpoint.NewEndpointWithTTL(propMap["absoluteName"], endpoint.RecordTypeA, endpoint.TTL(ttl), ip)
|
||||
} else {
|
||||
ep = endpoint.NewEndpoint(propMap["absoluteName"], endpoint.RecordTypeA, ip)
|
||||
}
|
||||
for _, txtRec := range resT {
|
||||
if strings.Compare(p.TxtPrefix+rec.Name+p.TxtSuffix, txtRec.Name) == 0 {
|
||||
ep.Labels[endpoint.OwnerLabelKey], err = extractOwnerfromTXTRecord(txtRec.Properties)
|
||||
if err != nil {
|
||||
log.Debugf("External DNS Owner %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
}
|
||||
|
||||
var resC []api.BluecatCNAMERecord
|
||||
err = p.gatewayClient.GetCNAMERecords(zone, &resC)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not fetch CNAME records for zone: %v", zone)
|
||||
}
|
||||
|
||||
for _, rec := range resC {
|
||||
propMap := api.SplitProperties(rec.Properties)
|
||||
if _, ok := propMap["ttl"]; ok {
|
||||
ttl, err := strconv.Atoi(propMap["ttl"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse ttl '%d' as int for CNAME record %v", ttl, rec.Name)
|
||||
}
|
||||
ep = endpoint.NewEndpointWithTTL(propMap["absoluteName"], endpoint.RecordTypeCNAME, endpoint.TTL(ttl), propMap["linkedRecordName"])
|
||||
} else {
|
||||
ep = endpoint.NewEndpoint(propMap["absoluteName"], endpoint.RecordTypeCNAME, propMap["linkedRecordName"])
|
||||
}
|
||||
for _, txtRec := range resT {
|
||||
if strings.Compare(p.TxtPrefix+rec.Name+p.TxtSuffix, txtRec.Name) == 0 {
|
||||
ep.Labels[endpoint.OwnerLabelKey], err = extractOwnerfromTXTRecord(txtRec.Properties)
|
||||
if err != nil {
|
||||
log.Debugf("External DNS Owner %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("fetched %d records from Bluecat", len(endpoints))
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// ApplyChanges updates necessary zones and replaces old records with new ones
|
||||
//
|
||||
// Returns nil upon success and err is there is an error
|
||||
func (p *BluecatProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
zones, err := p.zones()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("zones is: %+v\n", zones)
|
||||
log.Infof("changes: %+v\n", changes)
|
||||
created, deleted := p.mapChanges(zones, changes)
|
||||
log.Infof("created: %+v\n", created)
|
||||
log.Infof("deleted: %+v\n", deleted)
|
||||
p.deleteRecords(deleted)
|
||||
p.createRecords(created)
|
||||
|
||||
if p.DNSServerName != "" {
|
||||
if p.dryRun {
|
||||
log.Debug("Not executing deploy because this is running in dry-run mode")
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
type bluecatChangeMap map[string][]*endpoint.Endpoint
|
||||
|
||||
func (p *BluecatProvider) mapChanges(zones []string, changes *plan.Changes) (bluecatChangeMap, bluecatChangeMap) {
|
||||
created := bluecatChangeMap{}
|
||||
deleted := bluecatChangeMap{}
|
||||
|
||||
mapChange := func(changeMap bluecatChangeMap, change *endpoint.Endpoint) {
|
||||
zone := p.findZone(zones, change.DNSName)
|
||||
if zone == "" {
|
||||
log.Debugf("ignoring changes to '%s' because a suitable Bluecat DNS zone was not found", change.DNSName)
|
||||
return
|
||||
}
|
||||
changeMap[zone] = append(changeMap[zone], change)
|
||||
}
|
||||
|
||||
for _, change := range changes.Delete {
|
||||
mapChange(deleted, change)
|
||||
}
|
||||
for _, change := range changes.UpdateOld {
|
||||
mapChange(deleted, change)
|
||||
}
|
||||
for _, change := range changes.Create {
|
||||
mapChange(created, change)
|
||||
}
|
||||
for _, change := range changes.UpdateNew {
|
||||
mapChange(created, change)
|
||||
}
|
||||
|
||||
return created, deleted
|
||||
}
|
||||
|
||||
// findZone finds the most specific matching zone for a given record 'name' from a list of all zones
|
||||
func (p *BluecatProvider) findZone(zones []string, name string) string {
|
||||
var result string
|
||||
|
||||
for _, zone := range zones {
|
||||
if strings.HasSuffix(name, "."+zone) {
|
||||
if result == "" || len(zone) > len(result) {
|
||||
result = zone
|
||||
}
|
||||
} else if strings.EqualFold(name, zone) {
|
||||
if result == "" || len(zone) > len(result) {
|
||||
result = zone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *BluecatProvider) zones() ([]string, error) {
|
||||
log.Debugf("retrieving Bluecat zones for configuration: %s, view: %s", p.DNSConfiguration, p.View)
|
||||
var zones []string
|
||||
|
||||
zonelist, err := p.gatewayClient.GetBluecatZones(p.RootZone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, zone := range zonelist {
|
||||
if !p.domainFilter.Match(zone.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: match to absoluteName(string) not Id(int)
|
||||
if !p.zoneIDFilter.Match(strconv.Itoa(zone.ID)) {
|
||||
continue
|
||||
}
|
||||
|
||||
zoneProps := api.SplitProperties(zone.Properties)
|
||||
|
||||
zones = append(zones, zoneProps["absoluteName"])
|
||||
}
|
||||
log.Debugf("found %d zones", len(zones))
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (p *BluecatProvider) createRecords(created bluecatChangeMap) {
|
||||
for zone, endpoints := range created {
|
||||
for _, ep := range endpoints {
|
||||
if p.dryRun {
|
||||
log.Infof("would create %s record named '%s' to '%s' for Bluecat DNS zone '%s'.",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("creating %s record named '%s' to '%s' for Bluecat DNS zone '%s'.",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
)
|
||||
|
||||
recordSet, err := p.recordSet(ep, false)
|
||||
if err != nil {
|
||||
log.Errorf(
|
||||
"Failed to retrieve %s record named '%s' to '%s' for Bluecat DNS zone '%s': %v",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
var response interface{}
|
||||
switch ep.RecordType {
|
||||
case endpoint.RecordTypeA:
|
||||
err = p.gatewayClient.CreateHostRecord(zone, recordSet.obj.(*api.BluecatCreateHostRecordRequest))
|
||||
case endpoint.RecordTypeCNAME:
|
||||
err = p.gatewayClient.CreateCNAMERecord(zone, recordSet.obj.(*api.BluecatCreateCNAMERecordRequest))
|
||||
case endpoint.RecordTypeTXT:
|
||||
err = p.gatewayClient.CreateTXTRecord(zone, recordSet.obj.(*api.BluecatCreateTXTRecordRequest))
|
||||
}
|
||||
log.Debugf("Response from create: %v", response)
|
||||
if err != nil {
|
||||
log.Errorf(
|
||||
"Failed to create %s record named '%s' to '%s' for Bluecat DNS zone '%s': %v",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BluecatProvider) deleteRecords(deleted bluecatChangeMap) {
|
||||
// run deletions first
|
||||
for zone, endpoints := range deleted {
|
||||
for _, ep := range endpoints {
|
||||
if p.dryRun {
|
||||
log.Infof("would delete %s record named '%s' for Bluecat DNS zone '%s'.",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
zone,
|
||||
)
|
||||
continue
|
||||
} else {
|
||||
log.Infof("deleting %s record named '%s' for Bluecat DNS zone '%s'.",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
zone,
|
||||
)
|
||||
|
||||
recordSet, err := p.recordSet(ep, true)
|
||||
if err != nil {
|
||||
log.Errorf(
|
||||
"Failed to retrieve %s record named '%s' to '%s' for Bluecat DNS zone '%s': %v",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
switch ep.RecordType {
|
||||
case endpoint.RecordTypeA:
|
||||
for _, record := range *recordSet.res.(*[]api.BluecatHostRecord) {
|
||||
err = p.gatewayClient.DeleteHostRecord(record.Name, zone)
|
||||
}
|
||||
case endpoint.RecordTypeCNAME:
|
||||
for _, record := range *recordSet.res.(*[]api.BluecatCNAMERecord) {
|
||||
err = p.gatewayClient.DeleteCNAMERecord(record.Name, zone)
|
||||
}
|
||||
case endpoint.RecordTypeTXT:
|
||||
for _, record := range *recordSet.res.(*[]api.BluecatTXTRecord) {
|
||||
err = p.gatewayClient.DeleteTXTRecord(record.Name, zone)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Failed to delete %s record named '%s' for Bluecat DNS zone '%s': %v",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
zone,
|
||||
err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BluecatProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (bluecatRecordSet, error) {
|
||||
recordSet := bluecatRecordSet{}
|
||||
switch ep.RecordType {
|
||||
case endpoint.RecordTypeA:
|
||||
var res []api.BluecatHostRecord
|
||||
obj := api.BluecatCreateHostRecordRequest{
|
||||
AbsoluteName: ep.DNSName,
|
||||
IP4Address: ep.Targets[0],
|
||||
TTL: int(ep.RecordTTL),
|
||||
Properties: "",
|
||||
}
|
||||
if getObject {
|
||||
var record api.BluecatHostRecord
|
||||
err := p.gatewayClient.GetHostRecord(ep.DNSName, &record)
|
||||
if err != nil {
|
||||
return bluecatRecordSet{}, err
|
||||
}
|
||||
res = append(res, record)
|
||||
}
|
||||
recordSet = bluecatRecordSet{
|
||||
obj: &obj,
|
||||
res: &res,
|
||||
}
|
||||
case endpoint.RecordTypeCNAME:
|
||||
var res []api.BluecatCNAMERecord
|
||||
obj := api.BluecatCreateCNAMERecordRequest{
|
||||
AbsoluteName: ep.DNSName,
|
||||
LinkedRecord: ep.Targets[0],
|
||||
TTL: int(ep.RecordTTL),
|
||||
Properties: "",
|
||||
}
|
||||
if getObject {
|
||||
var record api.BluecatCNAMERecord
|
||||
err := p.gatewayClient.GetCNAMERecord(ep.DNSName, &record)
|
||||
if err != nil {
|
||||
return bluecatRecordSet{}, err
|
||||
}
|
||||
res = append(res, record)
|
||||
}
|
||||
recordSet = bluecatRecordSet{
|
||||
obj: &obj,
|
||||
res: &res,
|
||||
}
|
||||
case endpoint.RecordTypeTXT:
|
||||
var res []api.BluecatTXTRecord
|
||||
// TODO: Allow setting TTL
|
||||
// This is not implemented in the Bluecat Gateway
|
||||
obj := api.BluecatCreateTXTRecordRequest{
|
||||
AbsoluteName: ep.DNSName,
|
||||
Text: ep.Targets[0],
|
||||
}
|
||||
if getObject {
|
||||
var record api.BluecatTXTRecord
|
||||
err := p.gatewayClient.GetTXTRecord(ep.DNSName, &record)
|
||||
if err != nil {
|
||||
return bluecatRecordSet{}, err
|
||||
}
|
||||
res = append(res, record)
|
||||
}
|
||||
recordSet = bluecatRecordSet{
|
||||
obj: &obj,
|
||||
res: &res,
|
||||
}
|
||||
}
|
||||
return recordSet, nil
|
||||
}
|
||||
|
||||
// extractOwnerFromTXTRecord takes a single text property string and returns the owner after parsing the owner string.
|
||||
func extractOwnerfromTXTRecord(propString string) (string, error) {
|
||||
if len(propString) == 0 {
|
||||
return "", errors.Errorf("External-DNS Owner not found")
|
||||
}
|
||||
re := regexp.MustCompile(`external-dns/owner=[^,]+`)
|
||||
match := re.FindStringSubmatch(propString)
|
||||
if len(match) == 0 {
|
||||
return "", errors.Errorf("External-DNS Owner not found, %s", propString)
|
||||
}
|
||||
return strings.Split(match[0], "=")[1], nil
|
||||
}
|
@ -1,465 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bluecat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/internal/testutils"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
api "sigs.k8s.io/external-dns/provider/bluecat/gateway"
|
||||
)
|
||||
|
||||
type mockGatewayClient struct {
|
||||
mockBluecatZones *[]api.BluecatZone
|
||||
mockBluecatHosts *[]api.BluecatHostRecord
|
||||
mockBluecatCNAMEs *[]api.BluecatCNAMERecord
|
||||
mockBluecatTXTs *[]api.BluecatTXTRecord
|
||||
}
|
||||
|
||||
type Changes struct {
|
||||
// Records that need to be created
|
||||
Create []*endpoint.Endpoint
|
||||
// Records that need to be updated (current data)
|
||||
UpdateOld []*endpoint.Endpoint
|
||||
// Records that need to be updated (desired data)
|
||||
UpdateNew []*endpoint.Endpoint
|
||||
// Records that need to be deleted
|
||||
Delete []*endpoint.Endpoint
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) GetBluecatZones(zoneName string) ([]api.BluecatZone, error) {
|
||||
return *g.mockBluecatZones, nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) GetHostRecords(zone string, records *[]api.BluecatHostRecord) error {
|
||||
*records = *g.mockBluecatHosts
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) GetCNAMERecords(zone string, records *[]api.BluecatCNAMERecord) error {
|
||||
*records = *g.mockBluecatCNAMEs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) GetHostRecord(name string, record *api.BluecatHostRecord) error {
|
||||
for _, currentRecord := range *g.mockBluecatHosts {
|
||||
if currentRecord.Name == strings.Split(name, ".")[0] {
|
||||
*record = currentRecord
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) GetCNAMERecord(name string, record *api.BluecatCNAMERecord) error {
|
||||
for _, currentRecord := range *g.mockBluecatCNAMEs {
|
||||
if currentRecord.Name == strings.Split(name, ".")[0] {
|
||||
*record = currentRecord
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) CreateHostRecord(zone string, req *api.BluecatCreateHostRecordRequest) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) CreateCNAMERecord(zone string, req *api.BluecatCreateCNAMERecordRequest) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) DeleteHostRecord(name string, zone string) (err error) {
|
||||
*g.mockBluecatHosts = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) DeleteCNAMERecord(name string, zone string) (err error) {
|
||||
*g.mockBluecatCNAMEs = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) GetTXTRecords(zone string, records *[]api.BluecatTXTRecord) error {
|
||||
*records = *g.mockBluecatTXTs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) GetTXTRecord(name string, record *api.BluecatTXTRecord) error {
|
||||
for _, currentRecord := range *g.mockBluecatTXTs {
|
||||
if currentRecord.Name == name {
|
||||
*record = currentRecord
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) CreateTXTRecord(zone string, req *api.BluecatCreateTXTRecordRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) DeleteTXTRecord(name string, zone string) error {
|
||||
*g.mockBluecatTXTs = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) ServerFullDeploy() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createMockBluecatZone(fqdn string) api.BluecatZone {
|
||||
props := "absoluteName=" + fqdn
|
||||
return api.BluecatZone{
|
||||
Properties: props,
|
||||
Name: fqdn,
|
||||
ID: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func createMockBluecatHostRecord(fqdn, target string, ttl int) api.BluecatHostRecord {
|
||||
props := "absoluteName=" + fqdn + "|addresses=" + target + "|ttl=" + fmt.Sprint(ttl) + "|"
|
||||
nameParts := strings.Split(fqdn, ".")
|
||||
return api.BluecatHostRecord{
|
||||
Name: nameParts[0],
|
||||
Properties: props,
|
||||
ID: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func createMockBluecatCNAME(alias, target string, ttl int) api.BluecatCNAMERecord {
|
||||
props := "absoluteName=" + alias + "|linkedRecordName=" + target + "|ttl=" + fmt.Sprint(ttl) + "|"
|
||||
nameParts := strings.Split(alias, ".")
|
||||
return api.BluecatCNAMERecord{
|
||||
Name: nameParts[0],
|
||||
Properties: props,
|
||||
}
|
||||
}
|
||||
|
||||
func createMockBluecatTXT(fqdn, txt string) api.BluecatTXTRecord {
|
||||
return api.BluecatTXTRecord{
|
||||
Name: fqdn,
|
||||
Properties: txt,
|
||||
}
|
||||
}
|
||||
|
||||
func newBluecatProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, client mockGatewayClient) *BluecatProvider {
|
||||
return &BluecatProvider{
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: dryRun,
|
||||
gatewayClient: client,
|
||||
}
|
||||
}
|
||||
|
||||
type bluecatTestData []struct {
|
||||
TestDescription string
|
||||
Endpoints []*endpoint.Endpoint
|
||||
}
|
||||
|
||||
var tests = bluecatTestData{
|
||||
{
|
||||
"first test case", // TODO: better test description
|
||||
[]*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"123.123.123.122"},
|
||||
RecordTTL: endpoint.TTL(30),
|
||||
},
|
||||
{
|
||||
DNSName: "nginx.example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"123.123.123.123"},
|
||||
RecordTTL: endpoint.TTL(30),
|
||||
},
|
||||
{
|
||||
DNSName: "whitespace.example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"123.123.123.124"},
|
||||
RecordTTL: endpoint.TTL(30),
|
||||
},
|
||||
{
|
||||
DNSName: "hack.example.com",
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
Targets: endpoint.Targets{"bluecatnetworks.com"},
|
||||
RecordTTL: endpoint.TTL(30),
|
||||
},
|
||||
{
|
||||
DNSName: "wack.example.com",
|
||||
RecordType: endpoint.RecordTypeTXT,
|
||||
Targets: endpoint.Targets{"hello"},
|
||||
Labels: endpoint.Labels{"owner": ""},
|
||||
},
|
||||
{
|
||||
DNSName: "sack.example.com",
|
||||
RecordType: endpoint.RecordTypeTXT,
|
||||
Targets: endpoint.Targets{""},
|
||||
Labels: endpoint.Labels{"owner": ""},
|
||||
},
|
||||
{
|
||||
DNSName: "kdb.example.com",
|
||||
RecordType: endpoint.RecordTypeTXT,
|
||||
Targets: endpoint.Targets{"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"},
|
||||
Labels: endpoint.Labels{"owner": "default"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestBluecatRecords(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]api.BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatTXTs: &[]api.BluecatTXTRecord{
|
||||
createMockBluecatTXT("kdb.example.com", "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"),
|
||||
createMockBluecatTXT("wack.example.com", "hello"),
|
||||
createMockBluecatTXT("sack.example.com", ""),
|
||||
},
|
||||
mockBluecatHosts: &[]api.BluecatHostRecord{
|
||||
createMockBluecatHostRecord("example.com", "123.123.123.122", 30),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30),
|
||||
},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
for _, ti := range tests {
|
||||
actual, err := provider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateEndpoints(t, actual, ti.Endpoints)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBluecatApplyChangesCreate(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]api.BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatHosts: &[]api.BluecatHostRecord{},
|
||||
mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{},
|
||||
mockBluecatTXTs: &[]api.BluecatTXTRecord{},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
for _, ti := range tests {
|
||||
err := provider.ApplyChanges(context.Background(), &plan.Changes{Create: ti.Endpoints})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual, err := provider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateEndpoints(t, actual, []*endpoint.Endpoint{})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBluecatApplyChangesDelete(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]api.BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatHosts: &[]api.BluecatHostRecord{
|
||||
createMockBluecatHostRecord("example.com", "123.123.123.122", 30),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30),
|
||||
},
|
||||
mockBluecatTXTs: &[]api.BluecatTXTRecord{
|
||||
createMockBluecatTXT("kdb.example.com", "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"),
|
||||
createMockBluecatTXT("wack.example.com", "hello"),
|
||||
createMockBluecatTXT("sack.example.com", ""),
|
||||
},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
for _, ti := range tests {
|
||||
err := provider.ApplyChanges(context.Background(), &plan.Changes{Delete: ti.Endpoints})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual, err := provider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateEndpoints(t, actual, []*endpoint.Endpoint{})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBluecatApplyChangesDeleteWithOwner(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]api.BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatHosts: &[]api.BluecatHostRecord{
|
||||
createMockBluecatHostRecord("example.com", "123.123.123.122", 30),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30),
|
||||
},
|
||||
mockBluecatTXTs: &[]api.BluecatTXTRecord{
|
||||
createMockBluecatTXT("kdb.example.com", "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"),
|
||||
createMockBluecatTXT("wack.example.com", "hello"),
|
||||
createMockBluecatTXT("sack.example.com", ""),
|
||||
},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
for _, ti := range tests {
|
||||
for _, ep := range ti.Endpoints {
|
||||
if strings.Contains(ep.Targets.String(), "external-dns") {
|
||||
owner, err := extractOwnerfromTXTRecord(ep.Targets.String())
|
||||
if err != nil {
|
||||
t.Logf("%v", err)
|
||||
} else {
|
||||
t.Logf("Owner %s", owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
err := provider.ApplyChanges(context.Background(), &plan.Changes{Delete: ti.Endpoints})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual, err := provider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateEndpoints(t, actual, []*endpoint.Endpoint{})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: ensure findZone method is tested
|
||||
// TODO: ensure zones method is tested
|
||||
// TODO: ensure createRecords method is tested
|
||||
// TODO: ensure deleteRecords method is tested
|
||||
// TODO: ensure recordSet method is tested
|
||||
|
||||
// TODO: Figure out why recordSet.res is not being set properly
|
||||
func TestBluecatRecordset(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]api.BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatHosts: &[]api.BluecatHostRecord{
|
||||
createMockBluecatHostRecord("example.com", "123.123.123.122", 30),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30),
|
||||
},
|
||||
mockBluecatTXTs: &[]api.BluecatTXTRecord{
|
||||
createMockBluecatTXT("abc.example.com", "hello"),
|
||||
},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
// Test txt records for recordSet function
|
||||
testTxtEndpoint := endpoint.NewEndpoint("abc.example.com", endpoint.RecordTypeTXT, "hello")
|
||||
txtObj := api.BluecatCreateTXTRecordRequest{
|
||||
AbsoluteName: testTxtEndpoint.DNSName,
|
||||
Text: testTxtEndpoint.Targets[0],
|
||||
}
|
||||
txtRecords := []api.BluecatTXTRecord{
|
||||
createMockBluecatTXT("abc.example.com", "hello"),
|
||||
}
|
||||
expected := bluecatRecordSet{
|
||||
obj: &txtObj,
|
||||
res: &txtRecords,
|
||||
}
|
||||
actual, err := provider.recordSet(testTxtEndpoint, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, actual.obj, expected.obj)
|
||||
assert.Equal(t, actual.res, expected.res)
|
||||
|
||||
// Test a records for recordSet function
|
||||
testHostEndpoint := endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124")
|
||||
hostObj := api.BluecatCreateHostRecordRequest{
|
||||
AbsoluteName: testHostEndpoint.DNSName,
|
||||
IP4Address: testHostEndpoint.Targets[0],
|
||||
}
|
||||
hostRecords := []api.BluecatHostRecord{
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30),
|
||||
}
|
||||
hostExpected := bluecatRecordSet{
|
||||
obj: &hostObj,
|
||||
res: &hostRecords,
|
||||
}
|
||||
hostActual, err := provider.recordSet(testHostEndpoint, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, hostActual.obj, hostExpected.obj)
|
||||
assert.Equal(t, hostActual.res, hostExpected.res)
|
||||
|
||||
// Test CName records for recordSet function
|
||||
testCnameEndpoint := endpoint.NewEndpoint("hack.example.com", endpoint.RecordTypeCNAME, "bluecatnetworks.com")
|
||||
cnameObj := api.BluecatCreateCNAMERecordRequest{
|
||||
AbsoluteName: testCnameEndpoint.DNSName,
|
||||
LinkedRecord: testCnameEndpoint.Targets[0],
|
||||
}
|
||||
cnameRecords := []api.BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30),
|
||||
}
|
||||
cnameExpected := bluecatRecordSet{
|
||||
obj: &cnameObj,
|
||||
res: &cnameRecords,
|
||||
}
|
||||
cnameActual, err := provider.recordSet(testCnameEndpoint, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, cnameActual.obj, cnameExpected.obj)
|
||||
assert.Equal(t, cnameActual.res, cnameExpected.res)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
@ -1,583 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
// TODO: add logging
|
||||
// TODO: add timeouts
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// TODO: Ensure DNS Deploy Type Defaults to no-deploy instead of ""
|
||||
type BluecatConfig struct {
|
||||
GatewayHost string `json:"gatewayHost"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type GatewayClient interface {
|
||||
GetBluecatZones(zoneName string) ([]BluecatZone, error)
|
||||
GetHostRecords(zone string, records *[]BluecatHostRecord) error
|
||||
GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error
|
||||
GetHostRecord(name string, record *BluecatHostRecord) error
|
||||
GetCNAMERecord(name string, record *BluecatCNAMERecord) error
|
||||
CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) error
|
||||
CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) error
|
||||
DeleteHostRecord(name string, zone string) (err error)
|
||||
DeleteCNAMERecord(name string, zone string) (err error)
|
||||
GetTXTRecords(zone string, records *[]BluecatTXTRecord) error
|
||||
GetTXTRecord(name string, record *BluecatTXTRecord) error
|
||||
CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) error
|
||||
DeleteTXTRecord(name string, zone string) error
|
||||
ServerFullDeploy() error
|
||||
}
|
||||
|
||||
// GatewayClientConfig defines the configuration for a Bluecat Gateway Client
|
||||
type GatewayClientConfig struct {
|
||||
Cookie http.Cookie
|
||||
Token string
|
||||
Host string
|
||||
DNSConfiguration string
|
||||
View string
|
||||
RootZone string
|
||||
DNSServerName string
|
||||
SkipTLSVerify bool
|
||||
}
|
||||
|
||||
// BluecatZone defines a zone to hold records
|
||||
type BluecatZone struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Properties string `json:"properties"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// BluecatHostRecord defines dns Host record
|
||||
type BluecatHostRecord struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Properties string `json:"properties"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// BluecatCNAMERecord defines dns CNAME record
|
||||
type BluecatCNAMERecord struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Properties string `json:"properties"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// BluecatTXTRecord defines dns TXT record
|
||||
type BluecatTXTRecord struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Properties string `json:"properties"`
|
||||
}
|
||||
|
||||
type BluecatCreateHostRecordRequest struct {
|
||||
AbsoluteName string `json:"absolute_name"`
|
||||
IP4Address string `json:"ip4_address"`
|
||||
TTL int `json:"ttl"`
|
||||
Properties string `json:"properties"`
|
||||
}
|
||||
|
||||
type BluecatCreateCNAMERecordRequest struct {
|
||||
AbsoluteName string `json:"absolute_name"`
|
||||
LinkedRecord string `json:"linked_record"`
|
||||
TTL int `json:"ttl"`
|
||||
Properties string `json:"properties"`
|
||||
}
|
||||
|
||||
type BluecatCreateTXTRecordRequest struct {
|
||||
AbsoluteName string `json:"absolute_name"`
|
||||
Text string `json:"txt"`
|
||||
}
|
||||
|
||||
type BluecatServerFullDeployRequest struct {
|
||||
ServerName string `json:"server_name"`
|
||||
}
|
||||
|
||||
// NewGatewayClient creates and returns a new Bluecat gateway client
|
||||
func NewGatewayClientConfig(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 == "" {
|
||||
rootZone = "com"
|
||||
}
|
||||
return GatewayClientConfig{
|
||||
Cookie: cookie,
|
||||
Token: token,
|
||||
Host: gatewayHost,
|
||||
DNSConfiguration: dnsConfiguration,
|
||||
DNSServerName: dnsServerName,
|
||||
View: view,
|
||||
RootZone: rootZone,
|
||||
SkipTLSVerify: skipTLSVerify,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBluecatGatewayToken retrieves a Bluecat Gateway API token.
|
||||
func GetBluecatGatewayToken(cfg BluecatConfig) (string, http.Cookie, error) {
|
||||
var username string
|
||||
if cfg.GatewayUsername != "" {
|
||||
username = cfg.GatewayUsername
|
||||
}
|
||||
if v, ok := os.LookupEnv("BLUECAT_USERNAME"); ok {
|
||||
username = v
|
||||
}
|
||||
|
||||
var password string
|
||||
if cfg.GatewayPassword != "" {
|
||||
password = cfg.GatewayPassword
|
||||
}
|
||||
if v, ok := os.LookupEnv("BLUECAT_PASSWORD"); ok {
|
||||
password = v
|
||||
}
|
||||
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
})
|
||||
if err != nil {
|
||||
return "", http.Cookie{}, errors.Wrap(err, "could not unmarshal credentials for bluecat gateway config")
|
||||
}
|
||||
url := cfg.GatewayHost + "/rest_login"
|
||||
|
||||
response, err := executeHTTPRequest(cfg.SkipTLSVerify, http.MethodPost, url, "", bytes.NewBuffer(body), http.Cookie{})
|
||||
if err != nil {
|
||||
return "", http.Cookie{}, errors.Wrap(err, "error obtaining API token from bluecat gateway")
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", http.Cookie{}, errors.Wrap(err, "failed to read login response from bluecat gateway")
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", http.Cookie{}, errors.Errorf("got HTTP response code %v, detailed message: %v", response.StatusCode, string(responseBody))
|
||||
}
|
||||
|
||||
jsonResponse := map[string]string{}
|
||||
err = json.Unmarshal(responseBody, &jsonResponse)
|
||||
if err != nil {
|
||||
return "", http.Cookie{}, errors.Wrap(err, "error unmarshaling json response (auth) from bluecat gateway")
|
||||
}
|
||||
|
||||
// Example response: {"access_token": "BAMAuthToken: abc123"}
|
||||
// We only care about the actual token string - i.e. abc123
|
||||
// The gateway also creates a cookie as part of the response. This seems to be the actual auth mechanism, at least
|
||||
// for now.
|
||||
return strings.Split(jsonResponse["access_token"], " ")[1], *response.Cookies()[0], nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) GetBluecatZones(zoneName string) ([]BluecatZone, error) {
|
||||
zonePath := expandZone(zoneName)
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error requesting zones from gateway: %v, %v", url, zoneName)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, errors.Errorf("received http %v requesting zones from gateway in zone %v", response.StatusCode, zoneName)
|
||||
}
|
||||
|
||||
zones := []BluecatZone{}
|
||||
json.NewDecoder(response.Body).Decode(&zones)
|
||||
|
||||
// Bluecat Gateway only returns subzones one level deeper than the provided zone
|
||||
// so this recursion is needed to traverse subzones until none are returned
|
||||
for _, zone := range zones {
|
||||
zoneProps := SplitProperties(zone.Properties)
|
||||
subZones, err := c.GetBluecatZones(zoneProps["absoluteName"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error retrieving subzones from gateway: %v", zoneName)
|
||||
}
|
||||
zones = append(zones, subZones...)
|
||||
}
|
||||
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) GetHostRecords(zone string, records *[]BluecatHostRecord) error {
|
||||
zonePath := expandZone(zone)
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/"
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error requesting host records from gateway in zone %v", zone)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("received http %v requesting host records from gateway in zone %v", response.StatusCode, zone)
|
||||
}
|
||||
|
||||
json.NewDecoder(response.Body).Decode(records)
|
||||
log.Debugf("Get Host Records Response: %v", records)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error {
|
||||
zonePath := expandZone(zone)
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/"
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving cname records from gateway in zone %v", zone)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("received http %v requesting cname records from gateway in zone %v", response.StatusCode, zone)
|
||||
}
|
||||
|
||||
json.NewDecoder(response.Body).Decode(records)
|
||||
log.Debugf("Get CName Records Response: %v", records)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) GetTXTRecords(zone string, records *[]BluecatTXTRecord) error {
|
||||
zonePath := expandZone(zone)
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/"
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving txt records from gateway in zone %v", zone)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("received http %v requesting txt records from gateway in zone %v", response.StatusCode, zone)
|
||||
}
|
||||
|
||||
log.Debugf("Get Txt Records response: %v", response)
|
||||
json.NewDecoder(response.Body).Decode(records)
|
||||
log.Debugf("Get TXT Records Body: %v", records)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) GetHostRecord(name string, record *BluecatHostRecord) error {
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"host_records/" + name + "/"
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving host record %v from gateway", name)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("received http %v while retrieving host record %v from gateway", response.StatusCode, name)
|
||||
}
|
||||
|
||||
json.NewDecoder(response.Body).Decode(record)
|
||||
log.Debugf("Get Host Record Response: %v", record)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) GetCNAMERecord(name string, record *BluecatCNAMERecord) error {
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"cname_records/" + name + "/"
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving cname record %v from gateway", name)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("received http %v while retrieving cname record %v from gateway", response.StatusCode, name)
|
||||
}
|
||||
|
||||
json.NewDecoder(response.Body).Decode(record)
|
||||
log.Debugf("Get CName Record Response: %v", record)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) GetTXTRecord(name string, record *BluecatTXTRecord) error {
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"text_records/" + name + "/"
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving record %v from gateway", name)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("received http %v while retrieving txt record %v from gateway", response.StatusCode, name)
|
||||
}
|
||||
|
||||
json.NewDecoder(response.Body).Decode(record)
|
||||
log.Debugf("Get TXT Record Response: %v", record)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) error {
|
||||
zonePath := expandZone(zone)
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/"
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not marshal body for create host record")
|
||||
}
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error creating host record %v in gateway", req.AbsoluteName)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
return errors.Errorf("received http %v while creating host record %v in gateway", response.StatusCode, req.AbsoluteName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) error {
|
||||
zonePath := expandZone(zone)
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/"
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not marshal body for create cname record")
|
||||
}
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error creating cname record %v in gateway", req.AbsoluteName)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
return errors.Errorf("received http %v while creating cname record %v to alias %v in gateway", response.StatusCode, req.AbsoluteName, req.LinkedRecord)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) error {
|
||||
zonePath := expandZone(zone)
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/"
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not marshal body for create txt record")
|
||||
}
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error creating txt record %v in gateway", req.AbsoluteName)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
return errors.Errorf("received http %v while creating txt record %v in gateway", response.StatusCode, req.AbsoluteName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) DeleteHostRecord(name string, zone string) (err error) {
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"host_records/" + name + "." + zone + "/"
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error deleting host record %v from gateway", name)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
return errors.Errorf("received http %v while deleting host record %v from gateway", response.StatusCode, name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) DeleteCNAMERecord(name string, zone string) (err error) {
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"cname_records/" + name + "." + zone + "/"
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error deleting cname record %v from gateway", name)
|
||||
}
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
return errors.Errorf("received http %v while deleting cname record %v from gateway", response.StatusCode, name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) DeleteTXTRecord(name string, zone string) error {
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"text_records/" + name + "." + zone + "/"
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error deleting txt record %v from gateway", name)
|
||||
}
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
return errors.Errorf("received http %v while deleting txt record %v from gateway", response.StatusCode, name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) ServerFullDeploy() error {
|
||||
log.Infof("Executing full deploy on server %s", c.DNSServerName)
|
||||
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")
|
||||
}
|
||||
|
||||
response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error executing full deploy")
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
responseBody, err := io.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
|
||||
}
|
||||
|
||||
// SplitProperties is a helper function to break a '|' separated string into key/value pairs
|
||||
// i.e. "foo=bar|baz=mop"
|
||||
func SplitProperties(props string) map[string]string {
|
||||
propMap := make(map[string]string)
|
||||
// remove trailing | character before we split
|
||||
props = strings.TrimSuffix(props, "|")
|
||||
|
||||
splits := strings.Split(props, "|")
|
||||
for _, pair := range splits {
|
||||
items := strings.Split(pair, "=")
|
||||
propMap[items[0]] = items[1]
|
||||
}
|
||||
|
||||
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 {
|
||||
ze := "zones/"
|
||||
parts := strings.Split(zone, ".")
|
||||
if len(parts) > 1 {
|
||||
last := len(parts) - 1
|
||||
for i := range parts {
|
||||
ze = ze + parts[last-i] + "/zones/"
|
||||
}
|
||||
} else {
|
||||
ze = ze + zone + "/zones/"
|
||||
}
|
||||
return ze
|
||||
}
|
||||
|
||||
func executeHTTPRequest(skipTLSVerify bool, method, url, token string, body io.Reader, cookie http.Cookie) (*http.Response, error) {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: skipTLSVerify,
|
||||
},
|
||||
},
|
||||
}
|
||||
request, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if request.Method == http.MethodPost {
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
}
|
||||
request.Header.Add("Accept", "application/json")
|
||||
|
||||
if token != "" {
|
||||
request.Header.Add("Authorization", "Basic "+token)
|
||||
}
|
||||
request.AddCookie(&cookie)
|
||||
|
||||
return httpClient.Do(request)
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestBluecatNewGatewayClient(t *testing.T) {
|
||||
testCookie := http.Cookie{Name: "testCookie", Value: "exampleCookie"}
|
||||
testToken := "exampleToken"
|
||||
testgateWayHost := "exampleHost"
|
||||
testDNSConfiguration := "exampleDNSConfiguration"
|
||||
testDNSServer := "exampleServer"
|
||||
testView := "testView"
|
||||
testZone := "example.com"
|
||||
testVerify := true
|
||||
|
||||
client := NewGatewayClientConfig(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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBluecatExpandZones(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
"with subdomain": {input: "example.com", want: "zones/com/zones/example/zones/"},
|
||||
"only top level domain": {input: "com", want: "zones/com/zones/"},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := expandZone(tc.input)
|
||||
diff := cmp.Diff(tc.want, got)
|
||||
if diff != "" {
|
||||
t.Fatalf(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBluecatValidDeployTypes(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add error checking in case "properties" are not properly formatted
|
||||
// Example test case... "invalid": {input: "abcde", want: map[string]string{}, err: InvalidProperty},
|
||||
func TestBluecatSplitProperties(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input string
|
||||
want map[string]string
|
||||
}{
|
||||
"simple": {input: "ab=cd|ef=gh", want: map[string]string{"ab": "cd", "ef": "gh"}},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := SplitProperties(tc.input)
|
||||
diff := cmp.Diff(tc.want, got)
|
||||
if diff != "" {
|
||||
t.Fatalf(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTXTRecord(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
req := BluecatCreateTXTRecordRequest{}
|
||||
requestBodyBytes, _ := io.ReadAll(r.Body)
|
||||
err := json.Unmarshal(requestBodyBytes, &req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal body for server full deploy")
|
||||
}
|
||||
if req.AbsoluteName == "alreadyexists.test.com" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tests := map[string]struct {
|
||||
config GatewayClientConfig
|
||||
zone string
|
||||
record BluecatCreateTXTRecordRequest
|
||||
expectError bool
|
||||
}{
|
||||
"simple-success": {GatewayClientConfig{Host: server.URL}, "test.com", BluecatCreateTXTRecordRequest{AbsoluteName: "my.test.com", Text: "here is my text"}, false},
|
||||
"simple-failure": {GatewayClientConfig{Host: server.URL}, "test.com", BluecatCreateTXTRecordRequest{AbsoluteName: "alreadyexists.test.com", Text: "here is my text"}, true},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := tc.config.CreateTXTRecord(tc.zone, &tc.record)
|
||||
if got != nil && !tc.expectError {
|
||||
t.Fatalf("expected error %v, received error %v", tc.expectError, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTXTRecord(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.RequestURI, "doesnotexist") {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tests := map[string]struct {
|
||||
config GatewayClientConfig
|
||||
name string
|
||||
expectError bool
|
||||
}{
|
||||
"simple-success": {GatewayClientConfig{Host: server.URL}, "mytxtrecord", false},
|
||||
"simple-failure": {GatewayClientConfig{Host: server.URL}, "doesnotexist", true},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
record := BluecatTXTRecord{}
|
||||
got := tc.config.GetTXTRecord(tc.name, &record)
|
||||
if got != nil && !tc.expectError {
|
||||
t.Fatalf("expected error %v, received error %v", tc.expectError, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteTXTRecord(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.RequestURI, "doesnotexist") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tests := map[string]struct {
|
||||
config GatewayClientConfig
|
||||
name string
|
||||
zone string
|
||||
expectError bool
|
||||
}{
|
||||
"simple-success": {GatewayClientConfig{Host: server.URL}, "todelete", "test.com", false},
|
||||
"simple-failure": {GatewayClientConfig{Host: server.URL}, "doesnotexist", "test.com", true},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := tc.config.DeleteTXTRecord(tc.name, tc.zone)
|
||||
if got != nil && !tc.expectError {
|
||||
t.Fatalf("expected error %v, received error %v", tc.expectError, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerFullDeploy(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
req := BluecatServerFullDeployRequest{}
|
||||
requestBodyBytes, _ := io.ReadAll(r.Body)
|
||||
err := json.Unmarshal(requestBodyBytes, &req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal body for server full deploy")
|
||||
}
|
||||
if req.ServerName == "serverdoesnotexist" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tests := map[string]struct {
|
||||
config GatewayClientConfig
|
||||
expectError bool
|
||||
}{
|
||||
"simple-success": {GatewayClientConfig{Host: server.URL, DNSServerName: "myserver"}, false},
|
||||
"simple-failure": {GatewayClientConfig{Host: server.URL, DNSServerName: "serverdoesnotexist"}, true},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := tc.config.ServerFullDeploy()
|
||||
if got != nil && !tc.expectError {
|
||||
t.Fatalf("expected error %v, received error %v", tc.expectError, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,702 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 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 dyn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/nesv/go-dynect/dynect"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
dynsoap "sigs.k8s.io/external-dns/provider/dyn/soap"
|
||||
)
|
||||
|
||||
const (
|
||||
// 10 minutes default timeout if not configured using flags
|
||||
dynDefaultTTL = 600
|
||||
|
||||
// when rate limit is hit retry up to 5 times after sleep 1m between retries
|
||||
dynMaxRetriesOnErrRateLimited = 5
|
||||
|
||||
// two consecutive bad logins happen at least this many seconds apart
|
||||
// While it is easy to get the username right, misconfiguring the password
|
||||
// can get account blocked. Exit(1) is not a good solution
|
||||
// as k8s will restart the pod and another login attempt will be made
|
||||
badLoginMinIntervalSeconds = 30 * 60
|
||||
|
||||
// this prefix must be stripped from resource links before feeding them to dynect.Client.Do()
|
||||
restAPIPrefix = "/REST/"
|
||||
)
|
||||
|
||||
func unixNow() int64 {
|
||||
return time.Now().Unix()
|
||||
}
|
||||
|
||||
// DynConfig hold connection parameters to dyn.com and internal state
|
||||
type DynConfig struct {
|
||||
DomainFilter endpoint.DomainFilter
|
||||
ZoneIDFilter provider.ZoneIDFilter
|
||||
DryRun bool
|
||||
CustomerName string
|
||||
Username string
|
||||
Password string
|
||||
MinTTLSeconds int
|
||||
AppVersion string
|
||||
DynVersion string
|
||||
}
|
||||
|
||||
// ZoneSnapshot stores a single recordset for a zone for a single serial
|
||||
type ZoneSnapshot struct {
|
||||
serials map[string]int
|
||||
endpoints map[string][]*endpoint.Endpoint
|
||||
}
|
||||
|
||||
// GetRecordsForSerial retrieves from memory the last known recordset for the (zone, serial) tuple
|
||||
func (snap *ZoneSnapshot) GetRecordsForSerial(zone string, serial int) []*endpoint.Endpoint {
|
||||
lastSerial, ok := snap.serials[zone]
|
||||
if !ok {
|
||||
// no mapping
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastSerial != serial {
|
||||
// outdated mapping
|
||||
return nil
|
||||
}
|
||||
|
||||
endpoints, ok := snap.endpoints[zone]
|
||||
if !ok {
|
||||
// probably a bug
|
||||
return nil
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// StoreRecordsForSerial associates a result set with a (zone, serial)
|
||||
func (snap *ZoneSnapshot) StoreRecordsForSerial(zone string, serial int, records []*endpoint.Endpoint) {
|
||||
snap.serials[zone] = serial
|
||||
snap.endpoints[zone] = records
|
||||
}
|
||||
|
||||
// DynProvider is the actual interface impl.
|
||||
type dynProviderState struct {
|
||||
provider.BaseProvider
|
||||
DynConfig
|
||||
LastLoginErrorTime int64
|
||||
|
||||
ZoneSnapshot *ZoneSnapshot
|
||||
}
|
||||
|
||||
// ZoneChange is missing from dynect: https://help.dyn.com/get-zone-changeset-api/
|
||||
type ZoneChange struct {
|
||||
ID int `json:"id"`
|
||||
UserID int `json:"user_id"`
|
||||
Zone string `json:"zone"`
|
||||
FQDN string `json:"FQDN"`
|
||||
Serial int `json:"serial"`
|
||||
TTL int `json:"ttl"`
|
||||
Type string `json:"rdata_type"`
|
||||
RData dynect.DataBlock `json:"rdata"`
|
||||
}
|
||||
|
||||
// ZoneChangesResponse is missing from dynect: https://help.dyn.com/get-zone-changeset-api/
|
||||
type ZoneChangesResponse struct {
|
||||
dynect.ResponseBlock
|
||||
Data []ZoneChange `json:"data"`
|
||||
}
|
||||
|
||||
// ZonePublishRequest is missing from dynect but the notes field is a nice place to let
|
||||
// external-dns report some internal info during commit
|
||||
type ZonePublishRequest struct {
|
||||
Publish bool `json:"publish"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// ZonePublishResponse holds the status after publish
|
||||
type ZonePublishResponse struct {
|
||||
dynect.ResponseBlock
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// NewDynProvider initializes a new Dyn Provider.
|
||||
func NewDynProvider(config DynConfig) (provider.Provider, error) {
|
||||
return &dynProviderState{
|
||||
DynConfig: config,
|
||||
ZoneSnapshot: &ZoneSnapshot{
|
||||
endpoints: map[string][]*endpoint.Endpoint{},
|
||||
serials: map[string]int{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// filterAndFixLinks removes from `links` all the records we don't care about
|
||||
// and strops the /REST/ prefix
|
||||
func filterAndFixLinks(links []string, filter endpoint.DomainFilter) []string {
|
||||
var result []string
|
||||
for _, link := range links {
|
||||
// link looks like /REST/CNAMERecord/acme.com/exchange.acme.com/349386875
|
||||
|
||||
// strip /REST/
|
||||
link = strings.TrimPrefix(link, restAPIPrefix)
|
||||
|
||||
// simply ignore all record types we don't care about
|
||||
if !strings.HasPrefix(link, endpoint.RecordTypeA) &&
|
||||
!strings.HasPrefix(link, endpoint.RecordTypeCNAME) &&
|
||||
!strings.HasPrefix(link, endpoint.RecordTypeTXT) {
|
||||
continue
|
||||
}
|
||||
|
||||
// strip ID suffix
|
||||
domain := link[0:strings.LastIndexByte(link, '/')]
|
||||
// strip zone prefix
|
||||
domain = domain[strings.LastIndexByte(domain, '/')+1:]
|
||||
if filter.Match(domain) {
|
||||
result = append(result, link)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func fixMissingTTL(ttl endpoint.TTL, minTTLSeconds int) string {
|
||||
i := dynDefaultTTL
|
||||
if ttl.IsConfigured() {
|
||||
if int(ttl) < minTTLSeconds {
|
||||
i = minTTLSeconds
|
||||
} else {
|
||||
i = int(ttl)
|
||||
}
|
||||
}
|
||||
|
||||
return strconv.Itoa(i)
|
||||
}
|
||||
|
||||
// merge produces a single list of records that can be used as a replacement.
|
||||
// Dyn allows to replace all records with a single call
|
||||
// Invariant: the result contains only elements from the updateNew parameter
|
||||
func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint {
|
||||
findMatch := func(template *endpoint.Endpoint) *endpoint.Endpoint {
|
||||
for _, new := range updateNew {
|
||||
if template.DNSName == new.DNSName &&
|
||||
template.RecordType == new.RecordType {
|
||||
return new
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []*endpoint.Endpoint
|
||||
for _, old := range updateOld {
|
||||
matchingNew := findMatch(old)
|
||||
if matchingNew == nil {
|
||||
// no match, shouldn't happen
|
||||
continue
|
||||
}
|
||||
|
||||
if !matchingNew.Targets.Same(old.Targets) {
|
||||
// new target: always update, TTL will be overwritten too if necessary
|
||||
result = append(result, matchingNew)
|
||||
continue
|
||||
}
|
||||
|
||||
if matchingNew.RecordTTL != 0 && matchingNew.RecordTTL != old.RecordTTL {
|
||||
// same target, but new non-zero TTL set in k8s, must update
|
||||
// probably would happen only if there is a bug in the code calling the provider
|
||||
result = append(result, matchingNew)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func apiRetryLoop(f func() error) error {
|
||||
var err error
|
||||
for i := 0; i < dynMaxRetriesOnErrRateLimited; i++ {
|
||||
err = f()
|
||||
if err == nil || err != dynect.ErrRateLimited {
|
||||
// success or not retryable error
|
||||
return err
|
||||
}
|
||||
|
||||
// https://help.dyn.com/managed-dns-api-rate-limit/
|
||||
log.Debugf("Rate limit has been hit, sleeping for 1m (%d/%d)", i, dynMaxRetriesOnErrRateLimited)
|
||||
time.Sleep(1 * time.Minute)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *dynProviderState) allRecordsToEndpoints(records *dynsoap.GetAllRecordsResponseType) []*endpoint.Endpoint {
|
||||
result := []*endpoint.Endpoint{}
|
||||
// Convert each record to an endpoint
|
||||
|
||||
// Process A Records
|
||||
for _, rec := range records.Data.A_records {
|
||||
ep := &endpoint.Endpoint{
|
||||
DNSName: rec.Fqdn,
|
||||
RecordTTL: endpoint.TTL(rec.Ttl),
|
||||
RecordType: rec.Record_type,
|
||||
Targets: endpoint.Targets{rec.Rdata.Address},
|
||||
}
|
||||
log.Debugf("A record: %v", *ep)
|
||||
result = append(result, ep)
|
||||
}
|
||||
|
||||
// Process CNAME Records
|
||||
for _, rec := range records.Data.Cname_records {
|
||||
ep := &endpoint.Endpoint{
|
||||
DNSName: rec.Fqdn,
|
||||
RecordTTL: endpoint.TTL(rec.Ttl),
|
||||
RecordType: rec.Record_type,
|
||||
Targets: endpoint.Targets{strings.TrimSuffix(rec.Rdata.Cname, ".")},
|
||||
}
|
||||
log.Debugf("CNAME record: %v", *ep)
|
||||
result = append(result, ep)
|
||||
}
|
||||
|
||||
// Process TXT Records
|
||||
for _, rec := range records.Data.Txt_records {
|
||||
ep := &endpoint.Endpoint{
|
||||
DNSName: rec.Fqdn,
|
||||
RecordTTL: endpoint.TTL(rec.Ttl),
|
||||
RecordType: rec.Record_type,
|
||||
Targets: endpoint.Targets{rec.Rdata.Txtdata},
|
||||
}
|
||||
log.Debugf("TXT record: %v", *ep)
|
||||
result = append(result, ep)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func errorOrValue(err error, value interface{}) interface{} {
|
||||
if err == nil {
|
||||
return value
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// endpointToRecord puts the Target of an Endpoint in the correct field of DataBlock.
|
||||
// See DataBlock comments for more info
|
||||
func endpointToRecord(ep *endpoint.Endpoint) *dynect.DataBlock {
|
||||
result := dynect.DataBlock{}
|
||||
|
||||
if ep.RecordType == endpoint.RecordTypeA {
|
||||
result.Address = ep.Targets[0]
|
||||
} else if ep.RecordType == endpoint.RecordTypeCNAME {
|
||||
result.CName = ep.Targets[0]
|
||||
} else if ep.RecordType == endpoint.RecordTypeTXT {
|
||||
result.TxtData = ep.Targets[0]
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func (d *dynProviderState) fetchZoneSerial(client *dynect.Client, zone string) (int, error) {
|
||||
var resp dynect.ZoneResponse
|
||||
|
||||
err := client.Do("GET", fmt.Sprintf("Zone/%s", zone), nil, &resp)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return resp.Data.Serial, nil
|
||||
}
|
||||
|
||||
// Use SOAP to fetch all records with a single call
|
||||
func (d *dynProviderState) fetchAllRecordsInZone(zone string) (*dynsoap.GetAllRecordsResponseType, error) {
|
||||
var err error
|
||||
|
||||
service := dynsoap.NewDynectClient("https://api2.dynect.net/SOAP/")
|
||||
|
||||
sessionRequest := dynsoap.SessionLoginRequestType{
|
||||
Customer_name: d.CustomerName,
|
||||
User_name: d.Username,
|
||||
Password: d.Password,
|
||||
Fault_incompat: 0,
|
||||
}
|
||||
|
||||
var resp *dynsoap.SessionLoginResponseType
|
||||
|
||||
err = apiRetryLoop(func() error {
|
||||
resp, err = service.SessionLogin(&sessionRequest)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := resp.Data.Token
|
||||
|
||||
logoutRequest := &dynsoap.SessionLogoutRequestType{
|
||||
Token: token,
|
||||
Fault_incompat: 0,
|
||||
}
|
||||
|
||||
defer service.SessionLogout(logoutRequest)
|
||||
|
||||
req := dynsoap.GetAllRecordsRequestType{
|
||||
Token: token,
|
||||
Zone: zone,
|
||||
Fault_incompat: 0,
|
||||
}
|
||||
|
||||
records := &dynsoap.GetAllRecordsResponseType{}
|
||||
|
||||
err = apiRetryLoop(func() error {
|
||||
records, err = service.GetAllRecords(&req)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Got all Records, status is %s", records.Status)
|
||||
|
||||
if strings.ToLower(records.Status) == "incomplete" {
|
||||
jobRequest := dynsoap.GetJobRequestType{
|
||||
Token: token,
|
||||
Job_id: records.Job_id,
|
||||
Fault_incompat: 0,
|
||||
}
|
||||
|
||||
jobResults := dynsoap.GetJobResponseType{}
|
||||
err = apiRetryLoop(func() error {
|
||||
jobResults, err := service.GetJob(&jobRequest)
|
||||
if strings.ToLower(jobResults.Status) == "incomplete" {
|
||||
return fmt.Errorf("job is incomplete")
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return jobResults.Data.(*dynsoap.GetAllRecordsResponseType), nil
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// buildLinkToRecord build a resource link. The symmetry of the dyn API is used to save
|
||||
// switch-case boilerplate.
|
||||
// Empty response means the endpoint is not mappable to a records link: either because the fqdn
|
||||
// is not matched by the domainFilter or it is in the wrong zone
|
||||
func (d *dynProviderState) buildLinkToRecord(ep *endpoint.Endpoint) string {
|
||||
if ep == nil {
|
||||
return ""
|
||||
}
|
||||
matchingZone := ""
|
||||
for _, zone := range d.ZoneIDFilter.ZoneIDs {
|
||||
if strings.HasSuffix(ep.DNSName, zone) {
|
||||
matchingZone = zone
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchingZone == "" {
|
||||
// no matching zone, ignore
|
||||
return ""
|
||||
}
|
||||
|
||||
if !d.DomainFilter.Match(ep.DNSName) {
|
||||
// no matching domain, ignore
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%sRecord/%s/%s/", ep.RecordType, matchingZone, ep.DNSName)
|
||||
}
|
||||
|
||||
// create a dynect client and performs login. You need to clean it up.
|
||||
// This method also stores the DynAPI version.
|
||||
// Don't user the dynect.Client.Login()
|
||||
func (d *dynProviderState) login() (*dynect.Client, error) {
|
||||
if d.LastLoginErrorTime != 0 {
|
||||
secondsSinceLastError := unixNow() - d.LastLoginErrorTime
|
||||
if secondsSinceLastError < badLoginMinIntervalSeconds {
|
||||
return nil, fmt.Errorf("will not attempt an API call as the last login failure occurred just %ds ago", secondsSinceLastError)
|
||||
}
|
||||
}
|
||||
client := dynect.NewClient(d.CustomerName)
|
||||
|
||||
req := dynect.LoginBlock{
|
||||
Username: d.Username,
|
||||
Password: d.Password,
|
||||
CustomerName: d.CustomerName,
|
||||
}
|
||||
|
||||
var resp dynect.LoginResponse
|
||||
|
||||
err := client.Do("POST", "Session", req, &resp)
|
||||
if err != nil {
|
||||
d.LastLoginErrorTime = unixNow()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.LastLoginErrorTime = 0
|
||||
client.Token = resp.Data.Token
|
||||
|
||||
// this is the only change from the original
|
||||
d.DynVersion = resp.Data.Version
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// the zones we are allowed to touch. Currently only exact matches are considered, not all
|
||||
// zones with the given suffix
|
||||
func (d *dynProviderState) zones(client *dynect.Client) []string {
|
||||
return d.ZoneIDFilter.ZoneIDs
|
||||
}
|
||||
|
||||
func (d *dynProviderState) buildRecordRequest(ep *endpoint.Endpoint) (string, *dynect.RecordRequest) {
|
||||
link := d.buildLinkToRecord(ep)
|
||||
if link == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
record := dynect.RecordRequest{
|
||||
TTL: fixMissingTTL(ep.RecordTTL, d.MinTTLSeconds),
|
||||
RData: *endpointToRecord(ep),
|
||||
}
|
||||
return link, &record
|
||||
}
|
||||
|
||||
// deleteRecord deletes all existing records (CNAME, TXT, A) for the given Endpoint.DNSName with 1 API call
|
||||
func (d *dynProviderState) deleteRecord(client *dynect.Client, ep *endpoint.Endpoint) error {
|
||||
link := d.buildLinkToRecord(ep)
|
||||
if link == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := dynect.RecordResponse{}
|
||||
|
||||
err := apiRetryLoop(func() error {
|
||||
return client.Do("DELETE", link, nil, &response)
|
||||
})
|
||||
|
||||
log.Debugf("Deleting record %s: %+v,", link, errorOrValue(err, &response))
|
||||
return err
|
||||
}
|
||||
|
||||
// replaceRecord replaces all existing records pf the given type for the Endpoint.DNSName with 1 API call
|
||||
func (d *dynProviderState) replaceRecord(client *dynect.Client, ep *endpoint.Endpoint) error {
|
||||
link, record := d.buildRecordRequest(ep)
|
||||
if link == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := dynect.RecordResponse{}
|
||||
err := apiRetryLoop(func() error {
|
||||
return client.Do("PUT", link, record, &response)
|
||||
})
|
||||
|
||||
log.Debugf("Replacing record %s: %+v,", link, errorOrValue(err, &response))
|
||||
return err
|
||||
}
|
||||
|
||||
// createRecord creates a single record with 1 API call
|
||||
func (d *dynProviderState) createRecord(client *dynect.Client, ep *endpoint.Endpoint) error {
|
||||
link, record := d.buildRecordRequest(ep)
|
||||
if link == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := dynect.RecordResponse{}
|
||||
err := apiRetryLoop(func() error {
|
||||
return client.Do("POST", link, record, &response)
|
||||
})
|
||||
|
||||
log.Debugf("Creating record %s: %+v,", link, errorOrValue(err, &response))
|
||||
return err
|
||||
}
|
||||
|
||||
// commit commits all pending changes. It will always attempt to commit, if there are no
|
||||
func (d *dynProviderState) commit(client *dynect.Client) error {
|
||||
errs := []error{}
|
||||
|
||||
for _, zone := range d.zones(client) {
|
||||
// extra call if in debug mode to fetch pending changes
|
||||
if log.GetLevel() >= log.DebugLevel {
|
||||
response := ZoneChangesResponse{}
|
||||
err := client.Do("GET", fmt.Sprintf("ZoneChanges/%s/", zone), nil, &response)
|
||||
log.Debugf("Pending changes for zone %s: %+v", zone, errorOrValue(err, &response))
|
||||
}
|
||||
|
||||
h, err := os.Hostname()
|
||||
if err != nil {
|
||||
h = "unknown-host"
|
||||
}
|
||||
notes := fmt.Sprintf("Change by external-dns@%s, DynAPI@%s, %s on %s",
|
||||
d.AppVersion,
|
||||
d.DynVersion,
|
||||
time.Now().Format(time.RFC3339),
|
||||
h,
|
||||
)
|
||||
|
||||
zonePublish := ZonePublishRequest{
|
||||
Publish: true,
|
||||
Notes: notes,
|
||||
}
|
||||
|
||||
response := ZonePublishResponse{}
|
||||
|
||||
// always retry the commit: don't waste the good work so far
|
||||
err = apiRetryLoop(func() error {
|
||||
return client.Do("PUT", fmt.Sprintf("Zone/%s/", zone), &zonePublish, &response)
|
||||
})
|
||||
log.Infof("Committing changes for zone %s: %+v", zone, errorOrValue(err, &response))
|
||||
}
|
||||
|
||||
switch len(errs) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return errs[0]
|
||||
default:
|
||||
return fmt.Errorf("multiple errors committing: %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
// Records makes on average C + 2*Z requests (Z = number of zones): 1 login + 1 fetchAllRecords
|
||||
// A cache is used to avoid querying for every single record found. C is proportional to the number
|
||||
// of expired/changed records
|
||||
func (d *dynProviderState) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
client, err := d.login()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Logout()
|
||||
|
||||
log.Debugf("Using DynAPI@%s", d.DynVersion)
|
||||
|
||||
var result []*endpoint.Endpoint
|
||||
|
||||
zones := d.zones(client)
|
||||
log.Infof("Configured zones: %+v", zones)
|
||||
for _, zone := range zones {
|
||||
serial, err := d.fetchZoneSerial(client, zone)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "404 Not Found") {
|
||||
log.Infof("Ignore zone %s as it does not exist", zone)
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
relevantRecords := d.ZoneSnapshot.GetRecordsForSerial(zone, serial)
|
||||
if relevantRecords != nil {
|
||||
log.Infof("Using %d cached records for zone %s@%d", len(relevantRecords), zone, serial)
|
||||
result = append(result, relevantRecords...)
|
||||
continue
|
||||
}
|
||||
|
||||
// Fetch All Records
|
||||
records, err := d.fetchAllRecordsInZone(zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
relevantRecords = d.allRecordsToEndpoints(records)
|
||||
|
||||
log.Debugf("Relevant records %+v", relevantRecords)
|
||||
|
||||
d.ZoneSnapshot.StoreRecordsForSerial(zone, serial, relevantRecords)
|
||||
log.Infof("Stored %d records for %s@%d", len(relevantRecords), zone, serial)
|
||||
result = append(result, relevantRecords...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// this method does C + 2*Z requests: C=total number of changes, Z = number of
|
||||
// affected zones (1 login + 1 commit)
|
||||
func (d *dynProviderState) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
log.Debugf("Processing changes: %+v", changes)
|
||||
|
||||
if d.DryRun {
|
||||
log.Infof("Will NOT delete these records: %+v", changes.Delete)
|
||||
log.Infof("Will NOT create these records: %+v", changes.Create)
|
||||
log.Infof("Will NOT update these records: %+v", merge(changes.UpdateOld, changes.UpdateNew))
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := d.login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Logout()
|
||||
|
||||
var errs []error
|
||||
|
||||
needsCommit := false
|
||||
|
||||
for _, ep := range changes.Delete {
|
||||
err := d.deleteRecord(client, ep)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
needsCommit = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, ep := range changes.Create {
|
||||
err := d.createRecord(client, ep)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
needsCommit = true
|
||||
}
|
||||
}
|
||||
|
||||
updates := merge(changes.UpdateOld, changes.UpdateNew)
|
||||
log.Debugf("Updates after merging: %+v", updates)
|
||||
for _, ep := range updates {
|
||||
err := d.replaceRecord(client, ep)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
needsCommit = true
|
||||
}
|
||||
}
|
||||
|
||||
switch len(errs) {
|
||||
case 0:
|
||||
case 1:
|
||||
return errs[0]
|
||||
default:
|
||||
return fmt.Errorf("multiple errors committing: %+v", errs)
|
||||
}
|
||||
|
||||
if needsCommit {
|
||||
return d.commit(client)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,294 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 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 dyn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/nesv/go-dynect/dynect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
func TestDynMerge_NoUpdateOnTTL0Changes(t *testing.T) {
|
||||
updateOld := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name1",
|
||||
Targets: endpoint.Targets{"target1"},
|
||||
RecordTTL: endpoint.TTL(1),
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
},
|
||||
{
|
||||
DNSName: "name2",
|
||||
Targets: endpoint.Targets{"target2"},
|
||||
RecordTTL: endpoint.TTL(1),
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
},
|
||||
}
|
||||
|
||||
updateNew := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name1",
|
||||
Targets: endpoint.Targets{"target1"},
|
||||
RecordTTL: endpoint.TTL(0),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
{
|
||||
DNSName: "name2",
|
||||
Targets: endpoint.Targets{"target2"},
|
||||
RecordTTL: endpoint.TTL(0),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(merge(updateOld, updateNew)))
|
||||
}
|
||||
|
||||
func TestDynMerge_UpdateOnTTLChanges(t *testing.T) {
|
||||
updateOld := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name1",
|
||||
Targets: endpoint.Targets{"target1"},
|
||||
RecordTTL: endpoint.TTL(1),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
{
|
||||
DNSName: "name2",
|
||||
Targets: endpoint.Targets{"target2"},
|
||||
RecordTTL: endpoint.TTL(1),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
}
|
||||
|
||||
updateNew := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name1",
|
||||
Targets: endpoint.Targets{"target1"},
|
||||
RecordTTL: endpoint.TTL(77),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
{
|
||||
DNSName: "name2",
|
||||
Targets: endpoint.Targets{"target2"},
|
||||
RecordTTL: endpoint.TTL(10),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
}
|
||||
|
||||
merged := merge(updateOld, updateNew)
|
||||
assert.Equal(t, 2, len(merged))
|
||||
assert.Equal(t, "name1", merged[0].DNSName)
|
||||
}
|
||||
|
||||
func TestDynMerge_AlwaysUpdateTarget(t *testing.T) {
|
||||
updateOld := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name1",
|
||||
Targets: endpoint.Targets{"target1"},
|
||||
RecordTTL: endpoint.TTL(1),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
{
|
||||
DNSName: "name2",
|
||||
Targets: endpoint.Targets{"target2"},
|
||||
RecordTTL: endpoint.TTL(1),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
}
|
||||
|
||||
updateNew := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name1",
|
||||
Targets: endpoint.Targets{"target1-changed"},
|
||||
RecordTTL: endpoint.TTL(0),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
{
|
||||
DNSName: "name2",
|
||||
Targets: endpoint.Targets{"target2"},
|
||||
RecordTTL: endpoint.TTL(0),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
}
|
||||
|
||||
merged := merge(updateOld, updateNew)
|
||||
assert.Equal(t, 1, len(merged))
|
||||
assert.Equal(t, "target1-changed", merged[0].Targets[0])
|
||||
}
|
||||
|
||||
func TestDynMerge_NoUpdateIfTTLUnchanged(t *testing.T) {
|
||||
updateOld := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name1",
|
||||
Targets: endpoint.Targets{"target1"},
|
||||
RecordTTL: endpoint.TTL(55),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
{
|
||||
DNSName: "name2",
|
||||
Targets: endpoint.Targets{"target2"},
|
||||
RecordTTL: endpoint.TTL(55),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
}
|
||||
|
||||
updateNew := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name1",
|
||||
Targets: endpoint.Targets{"target1"},
|
||||
RecordTTL: endpoint.TTL(55),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
{
|
||||
DNSName: "name2",
|
||||
Targets: endpoint.Targets{"target2"},
|
||||
RecordTTL: endpoint.TTL(55),
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
}
|
||||
|
||||
merged := merge(updateOld, updateNew)
|
||||
assert.Equal(t, 0, len(merged))
|
||||
}
|
||||
|
||||
func TestDyn_endpointToRecord(t *testing.T) {
|
||||
tests := []struct {
|
||||
ep *endpoint.Endpoint
|
||||
extractor func(*dynect.DataBlock) string
|
||||
}{
|
||||
{endpoint.NewEndpoint("address", "A", "the-target"), func(b *dynect.DataBlock) string { return b.Address }},
|
||||
{endpoint.NewEndpoint("cname", "CNAME", "the-target"), func(b *dynect.DataBlock) string { return b.CName }},
|
||||
{endpoint.NewEndpoint("text", "TXT", "the-target"), func(b *dynect.DataBlock) string { return b.TxtData }},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
block := endpointToRecord(tc.ep)
|
||||
assert.Equal(t, "the-target", tc.extractor(block))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDyn_buildLinkToRecord(t *testing.T) {
|
||||
provider := &dynProviderState{
|
||||
DynConfig: DynConfig{
|
||||
ZoneIDFilter: provider.NewZoneIDFilter([]string{"example.com"}),
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{"the-target.example.com"}),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
ep *endpoint.Endpoint
|
||||
link string
|
||||
}{
|
||||
{endpoint.NewEndpoint("sub.the-target.example.com", "A", "address"), "ARecord/example.com/sub.the-target.example.com/"},
|
||||
{endpoint.NewEndpoint("the-target.example.com", "CNAME", "cname"), "CNAMERecord/example.com/the-target.example.com/"},
|
||||
{endpoint.NewEndpoint("the-target.example.com", "TXT", "text"), "TXTRecord/example.com/the-target.example.com/"},
|
||||
{endpoint.NewEndpoint("the-target.google.com", "TXT", "text"), ""},
|
||||
{endpoint.NewEndpoint("mail.example.com", "TXT", "text"), ""},
|
||||
{nil, ""},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
assert.Equal(t, tc.link, provider.buildLinkToRecord(tc.ep))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDyn_errorOrValue(t *testing.T) {
|
||||
e := errors.New("an error")
|
||||
val := "value"
|
||||
assert.Equal(t, e, errorOrValue(e, val))
|
||||
assert.Equal(t, val, errorOrValue(nil, val))
|
||||
}
|
||||
|
||||
func TestDyn_filterAndFixLinks(t *testing.T) {
|
||||
links := []string{
|
||||
"/REST/ARecord/example.com/the-target.example.com/",
|
||||
"/REST/ARecord/example.com/the-target.google.com/",
|
||||
"/REST/TXTRecord/example.com/the-target.example.com/",
|
||||
"/REST/TXTRecord/example.com/the-target.google.com/",
|
||||
"/REST/CNAMERecord/example.com/the-target.google.com/",
|
||||
"/REST/CNAMERecord/example.com/the-target.example.com/",
|
||||
"/REST/NSRecord/example.com/the-target.google.com/",
|
||||
"/REST/NSRecord/example.com/the-target.example.com/",
|
||||
}
|
||||
filter := endpoint.NewDomainFilter([]string{"example.com"})
|
||||
result := filterAndFixLinks(links, filter)
|
||||
|
||||
// should skip non-example.com records and NS records too
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "ARecord/example.com/the-target.example.com/", result[0])
|
||||
assert.Equal(t, "TXTRecord/example.com/the-target.example.com/", result[1])
|
||||
assert.Equal(t, "CNAMERecord/example.com/the-target.example.com/", result[2])
|
||||
}
|
||||
|
||||
func TestDyn_fixMissingTTL(t *testing.T) {
|
||||
assert.Equal(t, fmt.Sprintf("%v", dynDefaultTTL), fixMissingTTL(endpoint.TTL(0), 0))
|
||||
|
||||
// nothing to fix
|
||||
assert.Equal(t, "111", fixMissingTTL(endpoint.TTL(111), 25))
|
||||
|
||||
// apply min TTL
|
||||
assert.Equal(t, "1992", fixMissingTTL(endpoint.TTL(111), 1992))
|
||||
}
|
||||
|
||||
func TestDyn_Snapshot(t *testing.T) {
|
||||
snap := ZoneSnapshot{
|
||||
serials: map[string]int{},
|
||||
endpoints: map[string][]*endpoint.Endpoint{},
|
||||
}
|
||||
|
||||
recs := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name",
|
||||
Targets: endpoint.Targets{"target"},
|
||||
RecordTTL: endpoint.TTL(10000),
|
||||
RecordType: "A",
|
||||
},
|
||||
}
|
||||
|
||||
snap.StoreRecordsForSerial("test", 12, recs)
|
||||
|
||||
cached := snap.GetRecordsForSerial("test", 12)
|
||||
assert.Equal(t, recs, cached)
|
||||
|
||||
cached = snap.GetRecordsForSerial("test", 999)
|
||||
assert.Nil(t, cached)
|
||||
|
||||
cached = snap.GetRecordsForSerial("sfas", 12)
|
||||
assert.Nil(t, cached)
|
||||
|
||||
recs2 := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "name",
|
||||
Targets: endpoint.Targets{"target2"},
|
||||
RecordTTL: endpoint.TTL(100),
|
||||
RecordType: "CNAME",
|
||||
},
|
||||
}
|
||||
|
||||
// update zone with different records and newer serial
|
||||
snap.StoreRecordsForSerial("test", 13, recs2)
|
||||
|
||||
cached = snap.GetRecordsForSerial("test", 13)
|
||||
assert.Equal(t, recs2, cached)
|
||||
|
||||
cached = snap.GetRecordsForSerial("test", 12)
|
||||
assert.Nil(t, cached)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package dynsoap
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hooklift/gowsdl/soap"
|
||||
)
|
||||
|
||||
// NewDynectClient returns a client with a configured http.Client
|
||||
// The default settings for the http.client are a timeout of
|
||||
// 10 seconds and reading proxy variables from http.ProxyFromEnvironment
|
||||
func NewDynectClient(url string) Dynect {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
soapClient := soap.NewClient(url, soap.WithHTTPClient(client))
|
||||
return NewDynect(soapClient)
|
||||
}
|
||||
|
||||
// NewCustomDynectClient returns a client without a configured http.Client
|
||||
func NewCustomDynectClient(url string, client http.Client) Dynect {
|
||||
soapClient := soap.NewClient(url, soap.WithHTTPClient(&client))
|
||||
return NewDynect(soapClient)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,313 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 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 rcode0
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
rc0 "github.com/nic-at/rc0go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
// RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS.
|
||||
type RcodeZeroProvider struct {
|
||||
provider.BaseProvider
|
||||
Client *rc0.Client
|
||||
|
||||
DomainFilter endpoint.DomainFilter
|
||||
DryRun bool
|
||||
TXTEncrypt bool
|
||||
Key []byte
|
||||
}
|
||||
|
||||
// NewRcodeZeroProvider creates a new RcodeZero Anycast DNS provider.
|
||||
//
|
||||
// Returns the provider or an error if a provider could not be created.
|
||||
func NewRcodeZeroProvider(domainFilter endpoint.DomainFilter, dryRun bool, txtEnc bool) (*RcodeZeroProvider, error) {
|
||||
client, err := rc0.NewClient(os.Getenv("RC0_API_KEY"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value := os.Getenv("RC0_BASE_URL")
|
||||
if len(value) != 0 {
|
||||
client.BaseURL, err = url.Parse(os.Getenv("RC0_BASE_URL"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize rcodezero provider: %v", err)
|
||||
}
|
||||
|
||||
provider := &RcodeZeroProvider{
|
||||
Client: client,
|
||||
DomainFilter: domainFilter,
|
||||
DryRun: dryRun,
|
||||
TXTEncrypt: txtEnc,
|
||||
}
|
||||
|
||||
if txtEnc {
|
||||
provider.Key = []byte(os.Getenv("RC0_ENC_KEY"))
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// Zones returns filtered zones if filter is set
|
||||
func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) {
|
||||
var result []*rc0.Zone
|
||||
|
||||
zones, err := p.fetchZones()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, zone := range zones {
|
||||
if p.DomainFilter.Match(zone.Domain) {
|
||||
result = append(result, zone)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Records returns resource records
|
||||
//
|
||||
// Decrypts TXT records if TXT-Encrypt flag is set and key is provided
|
||||
func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
zones, err := p.Zones()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
for _, zone := range zones {
|
||||
rrset, err := p.fetchRecords(zone.Domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range rrset {
|
||||
if provider.SupportedRecordType(r.Type) {
|
||||
if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(r.Type, "TXT") {
|
||||
p.Client.RRSet.DecryptTXT(p.Key, r)
|
||||
}
|
||||
if len(r.Records) > 1 {
|
||||
for _, _r := range r.Records {
|
||||
if !_r.Disabled {
|
||||
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), _r.Content))
|
||||
}
|
||||
}
|
||||
} else if !r.Records[0].Disabled {
|
||||
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Records[0].Content))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes in a given zone.
|
||||
func (p *RcodeZeroProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
combinedChanges := make([]*rc0.RRSetChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
|
||||
|
||||
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeADD, changes.Create)...)
|
||||
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeUPDATE, changes.UpdateNew)...)
|
||||
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeDELETE, changes.Delete)...)
|
||||
|
||||
return p.submitChanges(combinedChanges)
|
||||
}
|
||||
|
||||
// Helper function
|
||||
func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map[string][]*rc0.RRSetChange {
|
||||
changes := make(map[string][]*rc0.RRSetChange)
|
||||
zoneNameIDMapper := provider.ZoneIDName{}
|
||||
for _, z := range zones {
|
||||
zoneNameIDMapper.Add(z.Domain, z.Domain)
|
||||
changes[z.Domain] = []*rc0.RRSetChange{}
|
||||
}
|
||||
|
||||
for _, c := range changeSet {
|
||||
zone, _ := zoneNameIDMapper.FindZone(c.Name)
|
||||
if zone == "" {
|
||||
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.Name)
|
||||
continue
|
||||
}
|
||||
changes[zone] = append(changes[zone], c)
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
// Helper function
|
||||
func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) {
|
||||
var allRecords []*rc0.RRType
|
||||
|
||||
listOptions := rc0.NewListOptions()
|
||||
|
||||
for {
|
||||
records, page, err := p.Client.RRSet.List(zoneName, listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allRecords = append(allRecords, records...)
|
||||
|
||||
if page == nil || (page.CurrentPage == page.LastPage) {
|
||||
break
|
||||
}
|
||||
|
||||
listOptions.SetPageNumber(page.CurrentPage + 1)
|
||||
}
|
||||
|
||||
return allRecords, nil
|
||||
}
|
||||
|
||||
// Helper function
|
||||
func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) {
|
||||
var allZones []*rc0.Zone
|
||||
|
||||
listOptions := rc0.NewListOptions()
|
||||
|
||||
for {
|
||||
zones, page, err := p.Client.Zones.List(listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allZones = append(allZones, zones...)
|
||||
|
||||
if page == nil || page.IsLastPage() {
|
||||
break
|
||||
}
|
||||
|
||||
listOptions.SetPageNumber(page.CurrentPage + 1)
|
||||
}
|
||||
|
||||
return allZones, nil
|
||||
}
|
||||
|
||||
// Helper function to submit changes.
|
||||
//
|
||||
// Changes are submitted by change type.
|
||||
func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error {
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
zones, err := p.Zones()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// separate into per-zone change sets to be passed to the API.
|
||||
changesByZone := rcodezeroChangesByZone(zones, changes)
|
||||
for zoneName, changes := range changesByZone {
|
||||
for _, change := range changes {
|
||||
logFields := log.Fields{
|
||||
"record": change.Name,
|
||||
"content": change.Records[0].Content,
|
||||
"type": change.Type,
|
||||
"action": change.ChangeType,
|
||||
"zone": zoneName,
|
||||
}
|
||||
|
||||
log.WithFields(logFields).Info("Changing record.")
|
||||
|
||||
if p.DryRun {
|
||||
continue
|
||||
}
|
||||
|
||||
// to avoid accidentally adding extra dot if already present
|
||||
change.Name = strings.TrimSuffix(change.Name, ".") + "."
|
||||
|
||||
switch change.ChangeType {
|
||||
case rc0.ChangeTypeADD:
|
||||
sr, err := p.Client.RRSet.Create(zoneName, []*rc0.RRSetChange{change})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sr.HasError() {
|
||||
return fmt.Errorf("adding new RR resulted in an error: %v", sr.Message)
|
||||
}
|
||||
|
||||
case rc0.ChangeTypeUPDATE:
|
||||
sr, err := p.Client.RRSet.Edit(zoneName, []*rc0.RRSetChange{change})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sr.HasError() {
|
||||
return fmt.Errorf("updating existing RR resulted in an error: %v", sr.Message)
|
||||
}
|
||||
|
||||
case rc0.ChangeTypeDELETE:
|
||||
sr, err := p.Client.RRSet.Delete(zoneName, []*rc0.RRSetChange{change})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sr.HasError() {
|
||||
return fmt.Errorf("deleting existing RR resulted in an error: %v", sr.Message)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported changeType submitted: %v", change.ChangeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRcodezeroChanges returns a RcodeZero specific array with rrset change objects.
|
||||
func (p *RcodeZeroProvider) NewRcodezeroChanges(action string, endpoints []*endpoint.Endpoint) []*rc0.RRSetChange {
|
||||
changes := make([]*rc0.RRSetChange, 0, len(endpoints))
|
||||
|
||||
for _, _endpoint := range endpoints {
|
||||
changes = append(changes, p.NewRcodezeroChange(action, _endpoint))
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
// NewRcodezeroChange returns a RcodeZero specific rrset change object.
|
||||
func (p *RcodeZeroProvider) NewRcodezeroChange(action string, endpoint *endpoint.Endpoint) *rc0.RRSetChange {
|
||||
change := &rc0.RRSetChange{
|
||||
Type: endpoint.RecordType,
|
||||
ChangeType: action,
|
||||
Name: endpoint.DNSName,
|
||||
Records: []*rc0.Record{{
|
||||
Disabled: false,
|
||||
Content: endpoint.Targets[0],
|
||||
}},
|
||||
}
|
||||
|
||||
if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(endpoint.RecordType, "TXT") {
|
||||
p.Client.RRSet.EncryptTXT(p.Key, change)
|
||||
}
|
||||
|
||||
return change
|
||||
}
|
@ -1,394 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 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 rcode0
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
rc0 "github.com/nic-at/rc0go"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
const (
|
||||
testZoneOne = "testzone1.at"
|
||||
testZoneTwo = "testzone2.at"
|
||||
|
||||
rrsetChangesUnsupportedChangeType = 0
|
||||
)
|
||||
|
||||
type mockRcodeZeroClient rc0.Client
|
||||
|
||||
type mockZoneManagementService struct {
|
||||
TestNilZonesReturned bool
|
||||
TestErrorReturned bool
|
||||
}
|
||||
|
||||
type mockRRSetService struct {
|
||||
TestErrorReturned bool
|
||||
}
|
||||
|
||||
func (m *mockZoneManagementService) resetTestConditions() {
|
||||
m.TestNilZonesReturned = false
|
||||
m.TestErrorReturned = false
|
||||
}
|
||||
|
||||
func TestRcodeZeroProvider_Records(t *testing.T) {
|
||||
mockRRSetService := &mockRRSetService{}
|
||||
mockZoneManagementService := &mockZoneManagementService{}
|
||||
|
||||
provider := &RcodeZeroProvider{
|
||||
Client: (*rc0.Client)(&mockRcodeZeroClient{
|
||||
Zones: mockZoneManagementService,
|
||||
RRSet: mockRRSetService,
|
||||
}),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
endpoints, err := provider.Records(ctx) // should return 6 rrs
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
require.Equal(t, 10, len(endpoints))
|
||||
|
||||
mockRRSetService.TestErrorReturned = true
|
||||
|
||||
_, err = provider.Records(ctx)
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail, %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRcodeZeroProvider_ApplyChanges(t *testing.T) {
|
||||
mockRRSetService := &mockRRSetService{}
|
||||
mockZoneManagementService := &mockZoneManagementService{}
|
||||
|
||||
provider := &RcodeZeroProvider{
|
||||
Client: (*rc0.Client)(&mockRcodeZeroClient{
|
||||
Zones: mockZoneManagementService,
|
||||
RRSet: mockRRSetService,
|
||||
}),
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{testZoneOne}),
|
||||
}
|
||||
|
||||
changes := mockChanges()
|
||||
|
||||
err := provider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRcodeZeroProvider_NewRcodezeroChanges(t *testing.T) {
|
||||
provider := &RcodeZeroProvider{}
|
||||
|
||||
changes := mockChanges()
|
||||
|
||||
createChanges := provider.NewRcodezeroChanges(testZoneOne, changes.Create)
|
||||
require.Equal(t, 4, len(createChanges))
|
||||
|
||||
deleteChanges := provider.NewRcodezeroChanges(testZoneOne, changes.Delete)
|
||||
require.Equal(t, 1, len(deleteChanges))
|
||||
|
||||
updateOldChanges := provider.NewRcodezeroChanges(testZoneOne, changes.UpdateOld)
|
||||
require.Equal(t, 1, len(updateOldChanges))
|
||||
|
||||
updateNewChanges := provider.NewRcodezeroChanges(testZoneOne, changes.UpdateNew)
|
||||
require.Equal(t, 1, len(updateNewChanges))
|
||||
}
|
||||
|
||||
func TestRcodeZeroProvider_NewRcodezeroChange(t *testing.T) {
|
||||
_endpoint := &endpoint.Endpoint{
|
||||
RecordType: "A",
|
||||
DNSName: "app." + testZoneOne,
|
||||
RecordTTL: 300,
|
||||
Targets: endpoint.Targets{"target"},
|
||||
}
|
||||
|
||||
provider := &RcodeZeroProvider{}
|
||||
|
||||
rrsetChange := provider.NewRcodezeroChange(testZoneOne, _endpoint)
|
||||
|
||||
require.Equal(t, _endpoint.RecordType, rrsetChange.Type)
|
||||
require.Equal(t, _endpoint.DNSName, rrsetChange.Name)
|
||||
require.Equal(t, _endpoint.Targets[0], rrsetChange.Records[0].Content)
|
||||
// require.Equal(t, endpoint.RecordTTL, rrsetChange.TTL)
|
||||
}
|
||||
|
||||
func Test_submitChanges(t *testing.T) {
|
||||
mockRRSetService := &mockRRSetService{}
|
||||
mockZoneManagementService := &mockZoneManagementService{}
|
||||
|
||||
provider := &RcodeZeroProvider{
|
||||
Client: (*rc0.Client)(&mockRcodeZeroClient{
|
||||
Zones: mockZoneManagementService,
|
||||
RRSet: mockRRSetService,
|
||||
}),
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{testZoneOne}),
|
||||
}
|
||||
|
||||
changes := mockRRSetChanges(rrsetChangesUnsupportedChangeType)
|
||||
|
||||
err := provider.submitChanges(changes)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail, %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mockRRSetChanges(condition int) []*rc0.RRSetChange {
|
||||
switch condition {
|
||||
case rrsetChangesUnsupportedChangeType:
|
||||
return []*rc0.RRSetChange{
|
||||
{
|
||||
Name: testZoneOne,
|
||||
Type: "A",
|
||||
ChangeType: "UNSUPPORTED",
|
||||
Records: []*rc0.Record{{Content: "fail"}},
|
||||
},
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mockChanges() *plan.Changes {
|
||||
changes := &plan.Changes{}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "new.ext-dns-test." + testZoneOne, Targets: endpoint.Targets{"target"}, RecordType: "A"},
|
||||
{DNSName: "new.ext-dns-test-with-ttl." + testZoneOne, Targets: endpoint.Targets{"target"}, RecordType: "A", RecordTTL: 100},
|
||||
{DNSName: "new.ext-dns-test.unexpected.com", Targets: endpoint.Targets{"target"}, RecordType: "AAAA"},
|
||||
{DNSName: testZoneOne, Targets: endpoint.Targets{"target"}, RecordType: "CNAME"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test." + testZoneOne, Targets: endpoint.Targets{"target"}}}
|
||||
changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test." + testZoneOne, Targets: endpoint.Targets{"target-old"}}}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test." + testZoneOne, Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 100}}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
func TestRcodeZeroProvider_Zones(t *testing.T) {
|
||||
mockRRSetService := &mockRRSetService{}
|
||||
mockZoneManagementService := &mockZoneManagementService{}
|
||||
|
||||
provider := &RcodeZeroProvider{
|
||||
Client: (*rc0.Client)(&mockRcodeZeroClient{
|
||||
Zones: mockZoneManagementService,
|
||||
RRSet: mockRRSetService,
|
||||
}),
|
||||
}
|
||||
|
||||
mockZoneManagementService.TestNilZonesReturned = true
|
||||
|
||||
zones, err := provider.Zones()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.Equal(t, 0, len(zones))
|
||||
mockZoneManagementService.resetTestConditions()
|
||||
|
||||
mockZoneManagementService.TestErrorReturned = true
|
||||
|
||||
_, err = provider.Zones()
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail, %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRcodeZeroProvider(t *testing.T) {
|
||||
_ = os.Setenv("RC0_API_KEY", "123")
|
||||
p, err := NewRcodeZeroProvider(endpoint.NewDomainFilter([]string{"ext-dns-test." + testZoneOne + "."}), true, true)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
require.Equal(t, true, p.DryRun)
|
||||
require.Equal(t, true, p.TXTEncrypt)
|
||||
require.Equal(t, true, p.DomainFilter.IsConfigured())
|
||||
require.Equal(t, false, p.DomainFilter.Match("ext-dns-test."+testZoneTwo+".")) // filter is set, so it should match only provided domains
|
||||
|
||||
p, err = NewRcodeZeroProvider(endpoint.DomainFilter{}, false, false)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
require.Equal(t, false, p.DryRun)
|
||||
require.Equal(t, false, p.DomainFilter.IsConfigured())
|
||||
require.Equal(t, true, p.DomainFilter.Match("ext-dns-test."+testZoneOne+".")) // filter is not set, so it should match any
|
||||
|
||||
_ = os.Unsetenv("RC0_API_KEY")
|
||||
_, err = NewRcodeZeroProvider(endpoint.DomainFilter{}, false, false)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail")
|
||||
}
|
||||
}
|
||||
|
||||
/* mocking mockRRSetServiceInterface */
|
||||
|
||||
func (m *mockRRSetService) List(zone string, options *rc0.ListOptions) ([]*rc0.RRType, *rc0.Page, error) {
|
||||
if m.TestErrorReturned {
|
||||
return nil, nil, fmt.Errorf("operation RRSet.List failed")
|
||||
}
|
||||
|
||||
return mockRRSet(zone), nil, nil
|
||||
}
|
||||
|
||||
func mockRRSet(zone string) []*rc0.RRType {
|
||||
return []*rc0.RRType{
|
||||
{
|
||||
Name: "app." + zone + ".",
|
||||
Type: "TXT",
|
||||
TTL: 300,
|
||||
Records: []*rc0.Record{
|
||||
{
|
||||
Content: "\"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/default/app\"",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "app." + zone + ".",
|
||||
Type: "A",
|
||||
TTL: 300,
|
||||
Records: []*rc0.Record{
|
||||
{
|
||||
Content: "127.0.0.1",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "www." + zone + ".",
|
||||
Type: "A",
|
||||
TTL: 300,
|
||||
Records: []*rc0.Record{
|
||||
{
|
||||
Content: "127.0.0.1",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: zone + ".",
|
||||
Type: "SOA",
|
||||
TTL: 3600,
|
||||
Records: []*rc0.Record{
|
||||
{
|
||||
Content: "sec1.rcode0.net. rcodezero-soa.ipcom.at. 2019011616 10800 3600 604800 3600",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: zone + ".",
|
||||
Type: "NS",
|
||||
TTL: 3600,
|
||||
Records: []*rc0.Record{
|
||||
{
|
||||
Content: "sec2.rcode0.net.",
|
||||
Disabled: false,
|
||||
},
|
||||
{
|
||||
Content: "sec1.rcode0.net.",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockRRSetService) Create(zone string, rrsetCreate []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
|
||||
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
|
||||
}
|
||||
|
||||
func (m *mockRRSetService) Edit(zone string, rrsetEdit []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
|
||||
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
|
||||
}
|
||||
|
||||
func (m *mockRRSetService) Delete(zone string, rrsetDelete []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
|
||||
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
|
||||
}
|
||||
|
||||
func (m *mockRRSetService) SubmitChangeSet(zone string, changeSet []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
|
||||
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
|
||||
}
|
||||
|
||||
func (m *mockRRSetService) EncryptTXT(key []byte, rrType *rc0.RRSetChange) {}
|
||||
|
||||
func (m *mockRRSetService) DecryptTXT(key []byte, rrType *rc0.RRType) {}
|
||||
|
||||
/* mocking ZoneManagementServiceInterface */
|
||||
|
||||
func (m *mockZoneManagementService) List(options *rc0.ListOptions) ([]*rc0.Zone, *rc0.Page, error) {
|
||||
if m.TestNilZonesReturned {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
if m.TestErrorReturned {
|
||||
return nil, nil, fmt.Errorf("operation Zone.List failed")
|
||||
}
|
||||
|
||||
zones := []*rc0.Zone{
|
||||
{
|
||||
Domain: testZoneOne,
|
||||
Type: "SLAVE",
|
||||
// "dnssec": "yes", @todo: add this
|
||||
// "created": "2018-04-09T09:27:31Z", @todo: add this
|
||||
LastCheck: "",
|
||||
Serial: 20180411,
|
||||
Masters: []string{
|
||||
"193.0.2.2",
|
||||
"2001:db8::2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Domain: testZoneTwo,
|
||||
Type: "MASTER",
|
||||
// "dnssec": "no", @todo: add this
|
||||
// "created": "2019-01-15T13:20:10Z", @todo: add this
|
||||
LastCheck: "",
|
||||
Serial: 2019011616,
|
||||
Masters: []string{
|
||||
"",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return zones, nil, nil
|
||||
}
|
||||
|
||||
func (m *mockZoneManagementService) Get(zone string) (*rc0.Zone, error) { return nil, nil }
|
||||
func (m *mockZoneManagementService) Create(zoneCreate *rc0.ZoneCreate) (*rc0.StatusResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockZoneManagementService) Edit(zone string, zoneEdit *rc0.ZoneEdit) (*rc0.StatusResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockZoneManagementService) Delete(zone string) (*rc0.StatusResponse, error) { return nil, nil }
|
||||
func (m *mockZoneManagementService) Transfer(zone string) (*rc0.StatusResponse, error) {
|
||||
return nil, nil
|
||||
}
|
@ -1,242 +0,0 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
ansClient "github.com/ans-group/sdk-go/pkg/client"
|
||||
ansConnection "github.com/ans-group/sdk-go/pkg/connection"
|
||||
"github.com/ans-group/sdk-go/pkg/service/safedns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"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 ansConnection.APIRequestParameters) ([]safedns.Record, error)
|
||||
GetZones(parameters ansConnection.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 ansConnection.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 := ansConnection.NewAPIKeyCredentialsAPIConnection(token)
|
||||
ansClient := ansClient.NewClient(ukfAPIConnection)
|
||||
safeDNS := ansClient.SafeDNSService()
|
||||
|
||||
provider := &SafeDNSProvider{
|
||||
Client: safeDNS,
|
||||
domainFilter: domainFilter,
|
||||
DryRun: dryRun,
|
||||
APIRequestParams: *ansConnection.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
|
||||
}
|
@ -1,366 +0,0 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
ansConnection "github.com/ans-group/sdk-go/pkg/connection"
|
||||
"github.com/ans-group/sdk-go/pkg/service/safedns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
// 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 ansConnection.APIRequestParameters) ([]safedns.Record, error) {
|
||||
args := m.Called(zoneName, parameters)
|
||||
return args.Get(0).([]safedns.Record), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockSafeDNSService) GetZones(parameters ansConnection.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)
|
||||
}
|
@ -1,272 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package vinyldns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vinyldns/go-vinyldns/vinyldns"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
vinyldnsCreate = "CREATE"
|
||||
vinyldnsDelete = "DELETE"
|
||||
vinyldnsUpdate = "UPDATE"
|
||||
|
||||
vinyldnsRecordTTL = 300
|
||||
)
|
||||
|
||||
type vinyldnsZoneInterface interface {
|
||||
Zones() ([]vinyldns.Zone, error)
|
||||
RecordSets(id string) ([]vinyldns.RecordSet, error)
|
||||
RecordSet(zoneID, recordSetID string) (vinyldns.RecordSet, error)
|
||||
RecordSetCreate(rs *vinyldns.RecordSet) (*vinyldns.RecordSetUpdateResponse, error)
|
||||
RecordSetUpdate(rs *vinyldns.RecordSet) (*vinyldns.RecordSetUpdateResponse, error)
|
||||
RecordSetDelete(zoneID, recordSetID string) (*vinyldns.RecordSetUpdateResponse, error)
|
||||
}
|
||||
|
||||
type vinyldnsProvider struct {
|
||||
provider.BaseProvider
|
||||
client vinyldnsZoneInterface
|
||||
zoneFilter provider.ZoneIDFilter
|
||||
domainFilter endpoint.DomainFilter
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
type vinyldnsChange struct {
|
||||
Action string
|
||||
ResourceRecordSet vinyldns.RecordSet
|
||||
}
|
||||
|
||||
// NewVinylDNSProvider provides support for VinylDNS records
|
||||
func NewVinylDNSProvider(domainFilter endpoint.DomainFilter, zoneFilter provider.ZoneIDFilter, dryRun bool) (provider.Provider, error) {
|
||||
_, ok := os.LookupEnv("VINYLDNS_ACCESS_KEY")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no vinyldns access key found")
|
||||
}
|
||||
|
||||
client := vinyldns.NewClientFromEnv()
|
||||
|
||||
return &vinyldnsProvider{
|
||||
client: client,
|
||||
dryRun: dryRun,
|
||||
zoneFilter: zoneFilter,
|
||||
domainFilter: domainFilter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *vinyldnsProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
|
||||
zones, err := p.client.Zones()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, zone := range zones {
|
||||
if !p.zoneFilter.Match(zone.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !p.domainFilter.Match(zone.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("Zone: [%s:%s]", zone.ID, zone.Name)
|
||||
records, err := p.client.RecordSets(zone.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
if provider.SupportedRecordType(r.Type) {
|
||||
recordsCount := len(r.Records)
|
||||
log.Debugf("%s.%s.%d.%s", r.Name, r.Type, recordsCount, zone.Name)
|
||||
|
||||
// TODO: AAAA Records
|
||||
if len(r.Records) > 0 {
|
||||
targets := make([]string, len(r.Records))
|
||||
for idx, rr := range r.Records {
|
||||
switch r.Type {
|
||||
case "A":
|
||||
targets[idx] = rr.Address
|
||||
case "CNAME":
|
||||
targets[idx] = rr.CName
|
||||
case "TXT":
|
||||
targets[idx] = rr.Text
|
||||
}
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name+"."+zone.Name, r.Type, endpoint.TTL(r.TTL), targets...))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func vinyldnsSuitableZone(hostname string, zones []vinyldns.Zone) *vinyldns.Zone {
|
||||
var zone *vinyldns.Zone
|
||||
for _, z := range zones {
|
||||
log.Debugf("hostname: %s and zoneName: %s", hostname, z.Name)
|
||||
// Adding a . as vinyl appends it to each zone record
|
||||
if strings.HasSuffix(hostname+".", z.Name) {
|
||||
zone = &z
|
||||
break
|
||||
}
|
||||
}
|
||||
return zone
|
||||
}
|
||||
|
||||
func (p *vinyldnsProvider) submitChanges(changes []*vinyldnsChange) error {
|
||||
if len(changes) == 0 {
|
||||
log.Infof("All records are already up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
zones, err := p.client.Zones()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, change := range changes {
|
||||
zone := vinyldnsSuitableZone(change.ResourceRecordSet.Name, zones)
|
||||
if zone == nil {
|
||||
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", change.ResourceRecordSet.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name+".", "."+zone.Name)
|
||||
change.ResourceRecordSet.ZoneID = zone.ID
|
||||
log.Infof("Changing records: %s %v in zone: %s", change.Action, change.ResourceRecordSet, zone.Name)
|
||||
|
||||
if !p.dryRun {
|
||||
switch change.Action {
|
||||
case vinyldnsCreate:
|
||||
_, err := p.client.RecordSetCreate(&change.ResourceRecordSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case vinyldnsUpdate:
|
||||
recordID, err := p.findRecordSetID(zone.ID, change.ResourceRecordSet.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
change.ResourceRecordSet.ID = recordID
|
||||
_, err = p.client.RecordSetUpdate(&change.ResourceRecordSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case vinyldnsDelete:
|
||||
recordID, err := p.findRecordSetID(zone.ID, change.ResourceRecordSet.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = p.client.RecordSetDelete(zone.ID, recordID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *vinyldnsProvider) findRecordSetID(zoneID string, recordSetName string) (recordID string, err error) {
|
||||
records, err := p.client.RecordSets(zoneID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
if r.Name == recordSetName {
|
||||
return r.ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("record not found")
|
||||
}
|
||||
|
||||
func (p *vinyldnsProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
combinedChanges := make([]*vinyldnsChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
|
||||
|
||||
combinedChanges = append(combinedChanges, newVinylDNSChanges(vinyldnsCreate, changes.Create)...)
|
||||
combinedChanges = append(combinedChanges, newVinylDNSChanges(vinyldnsUpdate, changes.UpdateNew)...)
|
||||
combinedChanges = append(combinedChanges, newVinylDNSChanges(vinyldnsDelete, changes.Delete)...)
|
||||
|
||||
return p.submitChanges(combinedChanges)
|
||||
}
|
||||
|
||||
// newVinylDNSChanges returns a collection of Changes based on the given records and action.
|
||||
func newVinylDNSChanges(action string, endpoints []*endpoint.Endpoint) []*vinyldnsChange {
|
||||
changes := make([]*vinyldnsChange, 0, len(endpoints))
|
||||
|
||||
for _, e := range endpoints {
|
||||
changes = append(changes, newVinylDNSChange(action, e))
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
func newVinylDNSChange(action string, endpoint *endpoint.Endpoint) *vinyldnsChange {
|
||||
ttl := vinyldnsRecordTTL
|
||||
if endpoint.RecordTTL.IsConfigured() {
|
||||
ttl = int(endpoint.RecordTTL)
|
||||
}
|
||||
|
||||
records := []vinyldns.Record{}
|
||||
|
||||
// TODO: AAAA
|
||||
if endpoint.RecordType == "CNAME" {
|
||||
records = []vinyldns.Record{
|
||||
{
|
||||
CName: endpoint.Targets[0],
|
||||
},
|
||||
}
|
||||
} else if endpoint.RecordType == "TXT" {
|
||||
records = []vinyldns.Record{
|
||||
{
|
||||
Text: endpoint.Targets[0],
|
||||
},
|
||||
}
|
||||
} else if endpoint.RecordType == "A" {
|
||||
records = []vinyldns.Record{
|
||||
{
|
||||
Address: endpoint.Targets[0],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
change := &vinyldnsChange{
|
||||
Action: action,
|
||||
ResourceRecordSet: vinyldns.RecordSet{
|
||||
Name: endpoint.DNSName,
|
||||
Type: endpoint.RecordType,
|
||||
TTL: ttl,
|
||||
Records: records,
|
||||
},
|
||||
}
|
||||
return change
|
||||
}
|
@ -1,225 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package vinyldns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/vinyldns/go-vinyldns/vinyldns"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
type mockVinyldnsZoneInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
var mockVinylDNSProvider vinyldnsProvider
|
||||
|
||||
var (
|
||||
vinylDNSZones []vinyldns.Zone
|
||||
vinylDNSRecords []vinyldns.RecordSet
|
||||
vinylDNSRecordSetUpdateResponse *vinyldns.RecordSetUpdateResponse
|
||||
)
|
||||
|
||||
func TestVinylDNSServices(t *testing.T) {
|
||||
firstZone := vinyldns.Zone{
|
||||
ID: "0",
|
||||
Name: "example.com.",
|
||||
}
|
||||
secondZone := vinyldns.Zone{
|
||||
ID: "1",
|
||||
Name: "example-beta.com.",
|
||||
}
|
||||
vinylDNSZones = []vinyldns.Zone{firstZone, secondZone}
|
||||
|
||||
firstRecord := vinyldns.RecordSet{
|
||||
ZoneID: "0",
|
||||
Name: "example.com.",
|
||||
TTL: 300,
|
||||
Type: "CNAME",
|
||||
Records: []vinyldns.Record{
|
||||
{
|
||||
CName: "vinyldns.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
vinylDNSRecords = []vinyldns.RecordSet{firstRecord}
|
||||
|
||||
vinylDNSRecordSetUpdateResponse = &vinyldns.RecordSetUpdateResponse{
|
||||
Zone: firstZone,
|
||||
RecordSet: firstRecord,
|
||||
ChangeID: "123",
|
||||
Status: "Active",
|
||||
}
|
||||
|
||||
mockVinylDNS := &mockVinyldnsZoneInterface{}
|
||||
mockVinylDNS.On("Zones").Return(vinylDNSZones, nil)
|
||||
mockVinylDNS.On("RecordSets", "0").Return(vinylDNSRecords, nil)
|
||||
mockVinylDNS.On("RecordSets", "1").Return(nil, nil)
|
||||
mockVinylDNS.On("RecordSets", "2").Return(nil, fmt.Errorf("Record not found"))
|
||||
mockVinylDNS.On("RecordSetCreate", &firstRecord).Return(vinylDNSRecordSetUpdateResponse, nil)
|
||||
mockVinylDNS.On("RecordSetUpdate", &firstRecord).Return(vinylDNSRecordSetUpdateResponse, nil)
|
||||
mockVinylDNS.On("RecordSetDelete", "0", "").Return(nil, nil)
|
||||
|
||||
mockVinylDNSProvider = vinyldnsProvider{client: mockVinylDNS}
|
||||
|
||||
// Run tests on mock services
|
||||
t.Run("Records", testVinylDNSProviderRecords)
|
||||
t.Run("ApplyChanges", testVinylDNSProviderApplyChanges)
|
||||
t.Run("SuitableZone", testVinylDNSSuitableZone)
|
||||
t.Run("GetRecordID", testVinylDNSFindRecordSetID)
|
||||
}
|
||||
|
||||
func testVinylDNSProviderRecords(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mockVinylDNSProvider.domainFilter = endpoint.NewDomainFilter([]string{"example.com"})
|
||||
result, err := mockVinylDNSProvider.Records(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(vinylDNSRecords), len(result))
|
||||
|
||||
mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"0"})
|
||||
result, err = mockVinylDNSProvider.Records(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(vinylDNSRecords), len(result))
|
||||
|
||||
mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"1"})
|
||||
result, err = mockVinylDNSProvider.Records(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(result))
|
||||
}
|
||||
|
||||
func testVinylDNSProviderApplyChanges(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "example.com", Targets: endpoint.Targets{"vinyldns.com"}, RecordType: endpoint.RecordTypeCNAME},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "example.com", Targets: endpoint.Targets{"vinyldns.com"}, RecordType: endpoint.RecordTypeCNAME},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "example.com", Targets: endpoint.Targets{"vinyldns.com"}, RecordType: endpoint.RecordTypeCNAME}}
|
||||
|
||||
mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"1"})
|
||||
err := mockVinylDNSProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to apply changes: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testVinylDNSSuitableZone(t *testing.T) {
|
||||
mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"0"})
|
||||
|
||||
zone := vinyldnsSuitableZone("example.com", vinylDNSZones)
|
||||
assert.Equal(t, zone.Name, "example.com.")
|
||||
}
|
||||
|
||||
func TestNewVinylDNSProvider(t *testing.T) {
|
||||
os.Setenv("VINYLDNS_ACCESS_KEY", "xxxxxxxxxxxxxxxxxxxxxxxxxx")
|
||||
_, err := NewVinylDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{"0"}), true)
|
||||
assert.Nil(t, err)
|
||||
|
||||
os.Unsetenv("VINYLDNS_ACCESS_KEY")
|
||||
_, err = NewVinylDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{"0"}), true)
|
||||
assert.NotNil(t, err)
|
||||
if err == nil {
|
||||
t.Errorf("Expected to fail new provider on empty token")
|
||||
}
|
||||
}
|
||||
|
||||
func testVinylDNSFindRecordSetID(t *testing.T) {
|
||||
mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"0"})
|
||||
result, err := mockVinylDNSProvider.findRecordSetID("0", "example.com.")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
|
||||
_, err = mockVinylDNSProvider.findRecordSetID("2", "example-beta")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func (m *mockVinyldnsZoneInterface) Zones() ([]vinyldns.Zone, error) {
|
||||
args := m.Called()
|
||||
var r0 []vinyldns.Zone
|
||||
|
||||
if args.Get(0) != nil {
|
||||
r0 = args.Get(0).([]vinyldns.Zone)
|
||||
}
|
||||
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockVinyldnsZoneInterface) RecordSet(zoneID, recordSet string) (vinyldns.RecordSet, error) {
|
||||
args := m.Called(zoneID, recordSet)
|
||||
var r0 vinyldns.RecordSet
|
||||
|
||||
if args.Get(0) != nil {
|
||||
r0 = args.Get(0).(vinyldns.RecordSet)
|
||||
}
|
||||
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockVinyldnsZoneInterface) RecordSets(id string) ([]vinyldns.RecordSet, error) {
|
||||
args := m.Called(id)
|
||||
var r0 []vinyldns.RecordSet
|
||||
|
||||
if args.Get(0) != nil {
|
||||
r0 = args.Get(0).([]vinyldns.RecordSet)
|
||||
}
|
||||
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockVinyldnsZoneInterface) RecordSetCreate(rs *vinyldns.RecordSet) (*vinyldns.RecordSetUpdateResponse, error) {
|
||||
args := m.Called(rs)
|
||||
var r0 *vinyldns.RecordSetUpdateResponse
|
||||
|
||||
if args.Get(0) != nil {
|
||||
r0 = args.Get(0).(*vinyldns.RecordSetUpdateResponse)
|
||||
}
|
||||
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockVinyldnsZoneInterface) RecordSetUpdate(rs *vinyldns.RecordSet) (*vinyldns.RecordSetUpdateResponse, error) {
|
||||
args := m.Called(rs)
|
||||
var r0 *vinyldns.RecordSetUpdateResponse
|
||||
|
||||
if args.Get(0) != nil {
|
||||
r0 = args.Get(0).(*vinyldns.RecordSetUpdateResponse)
|
||||
}
|
||||
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockVinyldnsZoneInterface) RecordSetDelete(zoneID, recordSetID string) (*vinyldns.RecordSetUpdateResponse, error) {
|
||||
args := m.Called(zoneID, recordSetID)
|
||||
var r0 *vinyldns.RecordSetUpdateResponse
|
||||
|
||||
if args.Get(0) != nil {
|
||||
r0 = args.Get(0).(*vinyldns.RecordSetUpdateResponse)
|
||||
}
|
||||
|
||||
return r0, args.Error(1)
|
||||
}
|
@ -1,304 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package vultr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vultr/govultr/v2"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
vultrCreate = "CREATE"
|
||||
vultrDelete = "DELETE"
|
||||
vultrUpdate = "UPDATE"
|
||||
vultrTTL = 3600
|
||||
)
|
||||
|
||||
// VultrProvider is an implementation of Provider for Vultr DNS.
|
||||
type VultrProvider struct {
|
||||
provider.BaseProvider
|
||||
client govultr.Client
|
||||
|
||||
domainFilter endpoint.DomainFilter
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// VultrChanges differentiates between ChangActions.
|
||||
type VultrChanges struct {
|
||||
Action string
|
||||
|
||||
ResourceRecordSet *govultr.DomainRecordReq
|
||||
}
|
||||
|
||||
// NewVultrProvider initializes a new Vultr BNS based provider
|
||||
func NewVultrProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*VultrProvider, error) {
|
||||
apiKey, ok := os.LookupEnv("VULTR_API_KEY")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no token found")
|
||||
}
|
||||
|
||||
oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
|
||||
AccessToken: apiKey,
|
||||
}))
|
||||
client := govultr.NewClient(oauthClient)
|
||||
client.SetUserAgent(fmt.Sprintf("ExternalDNS/%s", client.UserAgent))
|
||||
|
||||
p := &VultrProvider{
|
||||
client: *client,
|
||||
domainFilter: domainFilter,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Zones returns list of hosted zones
|
||||
func (p *VultrProvider) Zones(ctx context.Context) ([]govultr.Domain, error) {
|
||||
zones, err := p.fetchZones(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
// Records returns the list of records.
|
||||
func (p *VultrProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
zones, err := p.Zones(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
for _, zone := range zones {
|
||||
records, err := p.fetchRecords(ctx, zone.Domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
if provider.SupportedRecordType(r.Type) {
|
||||
name := fmt.Sprintf("%s.%s", r.Name, zone.Domain)
|
||||
|
||||
// root name is identified by the empty string and should be
|
||||
// translated to zone name for the endpoint entry.
|
||||
if r.Name == "" {
|
||||
name = zone.Domain
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, r.Type, endpoint.TTL(r.TTL), r.Data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (p *VultrProvider) fetchRecords(ctx context.Context, domain string) ([]govultr.DomainRecord, error) {
|
||||
var allRecords []govultr.DomainRecord
|
||||
listOptions := &govultr.ListOptions{}
|
||||
|
||||
for {
|
||||
records, meta, err := p.client.DomainRecord.List(ctx, domain, listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allRecords = append(allRecords, records...)
|
||||
|
||||
if meta.Links.Next == "" {
|
||||
break
|
||||
} else {
|
||||
listOptions.Cursor = meta.Links.Next
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return allRecords, nil
|
||||
}
|
||||
|
||||
func (p *VultrProvider) fetchZones(ctx context.Context) ([]govultr.Domain, error) {
|
||||
var zones []govultr.Domain
|
||||
listOptions := &govultr.ListOptions{}
|
||||
|
||||
for {
|
||||
allZones, meta, err := p.client.Domain.List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, zone := range allZones {
|
||||
if p.domainFilter.Match(zone.Domain) {
|
||||
zones = append(zones, zone)
|
||||
}
|
||||
}
|
||||
|
||||
if meta.Links.Next == "" {
|
||||
break
|
||||
} else {
|
||||
listOptions.Cursor = meta.Links.Next
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (p *VultrProvider) submitChanges(ctx context.Context, changes []*VultrChanges) error {
|
||||
if len(changes) == 0 {
|
||||
log.Infof("All records are already up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
zones, err := p.Zones(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zoneChanges := separateChangesByZone(zones, changes)
|
||||
|
||||
for zoneName, changes := range zoneChanges {
|
||||
for _, change := range changes {
|
||||
log.WithFields(log.Fields{
|
||||
"record": change.ResourceRecordSet.Name,
|
||||
"type": change.ResourceRecordSet.Type,
|
||||
"ttl": change.ResourceRecordSet.TTL,
|
||||
"action": change.Action,
|
||||
"zone": zoneName,
|
||||
}).Info("Changing record.")
|
||||
|
||||
switch change.Action {
|
||||
case vultrCreate:
|
||||
if _, err := p.client.DomainRecord.Create(ctx, zoneName, change.ResourceRecordSet); err != nil {
|
||||
return err
|
||||
}
|
||||
case vultrDelete:
|
||||
id, err := p.getRecordID(ctx, zoneName, change.ResourceRecordSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.client.DomainRecord.Delete(ctx, zoneName, id); err != nil {
|
||||
return err
|
||||
}
|
||||
case vultrUpdate:
|
||||
id, err := p.getRecordID(ctx, zoneName, change.ResourceRecordSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.client.DomainRecord.Update(ctx, zoneName, id, change.ResourceRecordSet); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes in a given zone.
|
||||
func (p *VultrProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
combinedChanges := make([]*VultrChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
|
||||
|
||||
combinedChanges = append(combinedChanges, newVultrChanges(vultrCreate, changes.Create)...)
|
||||
combinedChanges = append(combinedChanges, newVultrChanges(vultrUpdate, changes.UpdateNew)...)
|
||||
combinedChanges = append(combinedChanges, newVultrChanges(vultrDelete, changes.Delete)...)
|
||||
|
||||
return p.submitChanges(ctx, combinedChanges)
|
||||
}
|
||||
|
||||
func newVultrChanges(action string, endpoints []*endpoint.Endpoint) []*VultrChanges {
|
||||
changes := make([]*VultrChanges, 0, len(endpoints))
|
||||
ttl := vultrTTL
|
||||
for _, e := range endpoints {
|
||||
if e.RecordTTL.IsConfigured() {
|
||||
ttl = int(e.RecordTTL)
|
||||
}
|
||||
|
||||
change := &VultrChanges{
|
||||
Action: action,
|
||||
ResourceRecordSet: &govultr.DomainRecordReq{
|
||||
Type: e.RecordType,
|
||||
Name: e.DNSName,
|
||||
Data: e.Targets[0],
|
||||
TTL: ttl,
|
||||
},
|
||||
}
|
||||
|
||||
changes = append(changes, change)
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func separateChangesByZone(zones []govultr.Domain, changes []*VultrChanges) map[string][]*VultrChanges {
|
||||
change := make(map[string][]*VultrChanges)
|
||||
zoneNameID := provider.ZoneIDName{}
|
||||
|
||||
for _, z := range zones {
|
||||
zoneNameID.Add(z.Domain, z.Domain)
|
||||
change[z.Domain] = []*VultrChanges{}
|
||||
}
|
||||
|
||||
for _, c := range changes {
|
||||
zone, _ := zoneNameID.FindZone(c.ResourceRecordSet.Name)
|
||||
if zone == "" {
|
||||
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSet.Name)
|
||||
continue
|
||||
}
|
||||
change[zone] = append(change[zone], c)
|
||||
}
|
||||
return change
|
||||
}
|
||||
|
||||
func (p *VultrProvider) getRecordID(ctx context.Context, zone string, record *govultr.DomainRecordReq) (recordID string, err error) {
|
||||
listOptions := &govultr.ListOptions{}
|
||||
for {
|
||||
records, meta, err := p.client.DomainRecord.List(ctx, zone, listOptions)
|
||||
if err != nil {
|
||||
return "0", err
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
strippedName := strings.TrimSuffix(record.Name, "."+zone)
|
||||
if record.Name == zone {
|
||||
strippedName = ""
|
||||
}
|
||||
|
||||
if r.Name == strippedName && r.Type == record.Type {
|
||||
return r.ID, nil
|
||||
}
|
||||
}
|
||||
if meta.Links.Next == "" {
|
||||
break
|
||||
} else {
|
||||
listOptions.Cursor = meta.Links.Next
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no record was found")
|
||||
}
|
@ -1,213 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package vultr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/vultr/govultr/v2"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
type mockVultrDomain struct {
|
||||
client *govultr.Client
|
||||
}
|
||||
|
||||
func (m mockVultrDomain) Create(ctx context.Context, domainReq *govultr.DomainReq) (*govultr.Domain, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mockVultrDomain) Get(ctx context.Context, domain string) (*govultr.Domain, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mockVultrDomain) Update(ctx context.Context, domain, dnsSec string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockVultrDomain) Delete(ctx context.Context, domain string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockVultrDomain) List(ctx context.Context, options *govultr.ListOptions) ([]govultr.Domain, *govultr.Meta, error) {
|
||||
return []govultr.Domain{{Domain: "test.com", DateCreated: "1234"}}, &govultr.Meta{
|
||||
Total: 1,
|
||||
Links: &govultr.Links{
|
||||
Next: "",
|
||||
Prev: "",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m mockVultrDomain) GetSoa(ctx context.Context, domain string) (*govultr.Soa, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mockVultrDomain) UpdateSoa(ctx context.Context, domain string, soaReq *govultr.Soa) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockVultrDomain) GetDNSSec(ctx context.Context, domain string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type mockVultrRecord struct {
|
||||
client *govultr.Client
|
||||
}
|
||||
|
||||
func (m mockVultrRecord) Create(ctx context.Context, domain string, domainRecordReq *govultr.DomainRecordReq) (*govultr.DomainRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mockVultrRecord) Get(ctx context.Context, domain, recordID string) (*govultr.DomainRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mockVultrRecord) Update(ctx context.Context, domain, recordID string, domainRecordReq *govultr.DomainRecordReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockVultrRecord) Delete(ctx context.Context, domain, recordID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockVultrRecord) List(ctx context.Context, domain string, options *govultr.ListOptions) ([]govultr.DomainRecord, *govultr.Meta, error) {
|
||||
return []govultr.DomainRecord{{ID: "123", Type: "A", Name: "test", Data: "192.168.1.1", TTL: 300}}, &govultr.Meta{
|
||||
Total: 1,
|
||||
Links: &govultr.Links{
|
||||
Next: "",
|
||||
Prev: "",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestNewVultrProvider(t *testing.T) {
|
||||
_ = os.Setenv("VULTR_API_KEY", "")
|
||||
_, err := NewVultrProvider(context.Background(), endpoint.NewDomainFilter([]string{"test.vultr.com"}), true)
|
||||
if err != nil {
|
||||
t.Errorf("failed : %s", err)
|
||||
}
|
||||
|
||||
_ = os.Unsetenv("VULTR_API_KEY")
|
||||
_, err = NewVultrProvider(context.Background(), endpoint.NewDomainFilter([]string{"test.vultr.com"}), true)
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVultrProvider_Zones(t *testing.T) {
|
||||
mocked := mockVultrDomain{nil}
|
||||
provider := &VultrProvider{
|
||||
client: govultr.Client{
|
||||
Domain: &mocked,
|
||||
},
|
||||
}
|
||||
|
||||
expected, _, err := provider.client.Domain.List(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
provider.Zones(context.Background())
|
||||
zones, err := provider.Zones(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, zones) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVultrProvider_Records(t *testing.T) {
|
||||
mocked := mockVultrRecord{nil}
|
||||
mockedDomain := mockVultrDomain{nil}
|
||||
|
||||
provider := &VultrProvider{
|
||||
client: govultr.Client{
|
||||
DomainRecord: &mocked,
|
||||
Domain: &mockedDomain,
|
||||
},
|
||||
}
|
||||
|
||||
expected, _, _ := provider.client.DomainRecord.List(context.Background(), "test.com", nil)
|
||||
records, err := provider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, v := range records {
|
||||
assert.Equal(t, strings.TrimSuffix(v.DNSName, ".test.com"), expected[0].Name)
|
||||
assert.Equal(t, v.RecordType, expected[0].Type)
|
||||
assert.Equal(t, int(v.RecordTTL), expected[0].TTL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVultrProvider_ApplyChanges(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mocked := mockVultrRecord{nil}
|
||||
mockedDomain := mockVultrDomain{nil}
|
||||
|
||||
provider := &VultrProvider{
|
||||
client: govultr.Client{
|
||||
DomainRecord: &mocked,
|
||||
Domain: &mockedDomain,
|
||||
},
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "test.com", Targets: endpoint.Targets{"target"}},
|
||||
{DNSName: "ttl.test.com", Targets: endpoint.Targets{"target"}, RecordTTL: 100},
|
||||
}
|
||||
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.test.com", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 100}}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "test.test.com", Targets: endpoint.Targets{"target"}, RecordType: "A"}}
|
||||
err := provider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVultrProvider_getRecordID(t *testing.T) {
|
||||
mocked := mockVultrRecord{nil}
|
||||
mockedDomain := mockVultrDomain{nil}
|
||||
|
||||
provider := &VultrProvider{
|
||||
client: govultr.Client{
|
||||
DomainRecord: &mocked,
|
||||
Domain: &mockedDomain,
|
||||
},
|
||||
}
|
||||
|
||||
record := &govultr.DomainRecordReq{
|
||||
Type: "A",
|
||||
Name: "test.test.com",
|
||||
}
|
||||
id, err := provider.getRecordID(context.Background(), "test.com", record)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, id, "123")
|
||||
}
|
Loading…
Reference in New Issue
Block a user