Merge pull request #2923 from pluralsh/plural-provider-rebase

feat: Add Plural DNS provider
This commit is contained in:
Kubernetes Prow Robot 2022-09-15 09:23:23 -07:00 committed by GitHub
commit a763843764
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 905 additions and 14 deletions

View File

@ -59,6 +59,7 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz
* [IBM Cloud DNS](https://www.ibm.com/cloud/dns)
* [TencentCloud PrivateDNS](https://cloud.tencent.com/product/privatedns)
* [TencentCloud DNSPod](https://cloud.tencent.com/product/cns)
* [Plural](https://www.plural.sh/)
From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API.
@ -118,6 +119,7 @@ The following table clarifies the current status of the providers according to t
| SafeDNS | Alpha | @assureddt |
| IBMCloud | Alpha | @hughhuangzh |
| TencentCloud | Alpha | @Hyzhou |
| Plural | Alpha | @michaeljguarino |
## Kubernetes version compatibility
@ -187,6 +189,7 @@ The following tutorials are provided:
* [IBM Cloud](docs/tutorials/ibmcloud.md)
* [Nodes as source](docs/tutorials/nodes.md)
* [TencentCloud](docs/tutorials/tencentcloud.md)
* [Plural](docs/tutorials/plural.md)
### Running Locally

197
docs/tutorials/plural.md Normal file
View File

@ -0,0 +1,197 @@
# Setting up ExternalDNS for Services on Plural
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Plural DNS.
Make sure to use **>=0.12.3** version of ExternalDNS for this tutorial.
## Creating Plural Credentials
A secret containing the a Plural access token is needed for this provider. You can get a token for your user [here](https://app.plural.sh/profile/tokens).
To create the secret you can run `kubectl create secret generic plural-env --from-literal=PLURAL_ACCESS_TOKEN=<replace-with-your-access-token>`.
## Deploy ExternalDNS
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply one of the following manifests file to deploy ExternalDNS.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
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=plural
- --plural-cluster=example-plural-cluster
- --plural-provider=aws # gcp, azure, equinix and kind are also possible
env:
- name: PLURAL_ACCESS_TOKEN
valueFrom:
secretKeyRef:
key: PLURAL_ACCESS_TOKEN
name: plural-env
- name: PLURAL_ENDPOINT # (optional) use an alternative endpoint for Plural; defaults to https://app.plural.sh
value: https://app.plural.sh
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
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=plural
- --plural-cluster=example-plural-cluster
- --plural-provider=aws # gcp, azure, equinix and kind are also possible
env:
- name: PLURAL_ACCESS_TOKEN
valueFrom:
secretKeyRef:
key: PLURAL_ACCESS_TOKEN
name: plural-env
- name: PLURAL_ENDPOINT # (optional) use an alternative endpoint for Plural; defaults to https://app.plural.sh
value: https://app.plural.sh
```
## 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: 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 Plural DNS zone created above. The annotation may also be a subdomain
of the DNS zone (e.g. 'www.example.com').
By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above.
This annotation is optional, if you won't set it, it will be 1 (automatic) which is 300.
ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation
will cause ExternalDNS to remove the corresponding DNS records.
Create the deployment and service:
```
$ 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 Plural DNS records.
## Verifying Plural DNS records
Check your [Plural domain overview](https://app.plural.sh/account/domains) to view the domains associated with your Plural account. There you can view the records for each domain.
The records 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 Plural DNS records, we can delete the tutorial's example:
```
$ kubectl delete -f nginx.yaml
$ kubectl delete -f externaldns.yaml

17
go.mod
View File

@ -60,7 +60,7 @@ require (
go.uber.org/ratelimit v0.2.0
golang.org/x/net v0.0.0-20220722155237-a158d28d115b
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
google.golang.org/api v0.93.0
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
gopkg.in/yaml.v2 v2.4.0
@ -153,13 +153,12 @@ require (
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612 // indirect
google.golang.org/grpc v1.48.0 // indirect
@ -181,9 +180,12 @@ require (
sigs.k8s.io/yaml v1.3.0 // indirect
)
require github.com/pluralsh/gqlclient v1.1.6
require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/Yamashou/gqlgenc v0.11.0 // indirect
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
@ -192,7 +194,12 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/schollz/progressbar/v3 v3.8.6 // indirect
github.com/vektah/gqlparser/v2 v2.5.0 // indirect
)
replace k8s.io/klog/v2 => github.com/Raffo/knolog v0.0.0-20211016155154-e4d5e0cc970a

32
go.sum
View File

@ -160,6 +160,8 @@ github.com/StackExchange/dnscontrol v0.2.8 h1:7jviqDH9cIqRSRpH0UxgmpT7a8CwEhs9mL
github.com/StackExchange/dnscontrol v0.2.8/go.mod h1:BH+5nX50JxHDdb3+AD/z/UfYMCc7iaqEkRtQ+NjcFGE=
github.com/Venafi/vcert/v4 v4.14.3/go.mod h1:IL+6LA8QRWZbmcMzIr/vRhf9Aa6XDM2cQO50caWevjA=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/Yamashou/gqlgenc v0.11.0 h1:y6I7CDrUdY4JBxfwss9168HTP5k/CdExLV5+YPG/3nY=
github.com/Yamashou/gqlgenc v0.11.0/go.mod h1:OeQhghEgvGWvRwzx9XjMeg3FUQOHnTo5/12iuJSJxLg=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/ahmetb/gen-crd-api-reference-docs v0.2.1-0.20201224172655-df869c1245d4/go.mod h1:TdjdkYhlOifCQWPs1UdTma97kQQMozf5h26hTuG70u8=
@ -190,6 +192,7 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.61.1742 h1:1pBiWcgrwB5LN52SXAn6rHN3dIB
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1742/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
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=
@ -912,6 +915,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
@ -998,6 +1002,8 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -1019,6 +1025,8 @@ github.com/miekg/dns v1.1.48 h1:Ucfr7IIVyMBz4lRE8qmGUuZ4Wt3/ZGu9hmcMT3Uu4tQ=
github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -1178,6 +1186,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pluralsh/gqlclient v1.1.6 h1:lj6KVfUET9DPyB95+T+hXtCGqqOm+TXOB3jYzuBX1Zc=
github.com/pluralsh/gqlclient v1.1.6/go.mod h1:qcE4KD7hBGl/JFCoXXy8zgUP0mWHJsAM6bhDAf592AE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@ -1236,6 +1246,8 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -1263,6 +1275,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f h1:WSnaD0/cvbKJgSTYbjAPf4RJXVvNNDAwVm+W8wEmnGE=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c=
github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
@ -1379,6 +1393,8 @@ github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/vektah/gqlparser/v2 v2.5.0 h1:GwEwy7AJsqPWrey0bHnn+3JLaHLZVT66wY/+O+Tf9SU=
github.com/vektah/gqlparser/v2 v2.5.0/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs=
github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556 h1:UbVjBjgJUYGD8MlobEdOR+yTeNqaNa2Gf1/nskVNCSE=
github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg=
@ -1524,6 +1540,7 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@ -1578,8 +1595,8 @@ 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.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1695,8 +1712,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
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-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180425194835-bb9c189858d9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1822,8 +1840,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -1942,8 +1960,8 @@ golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyj
golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -59,6 +59,7 @@ import (
"sigs.k8s.io/external-dns/provider/oci"
"sigs.k8s.io/external-dns/provider/ovh"
"sigs.k8s.io/external-dns/provider/pdns"
"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"
@ -335,7 +336,9 @@ func main() {
p, err = ibmcloud.NewIBMCloudProvider(cfg.IBMCloudConfigFile, domainFilter, zoneIDFilter, endpointsSource, cfg.IBMCloudProxied, cfg.DryRun)
case "safedns":
p, err = safedns.NewSafeDNSProvider(domainFilter, cfg.DryRun)
case "tencentcloud":
case "plural":
p, err = plural.NewPluralProvider(cfg.PluralCluster, cfg.PluralProvider)
case "tencentcloud":
p, err = tencentcloud.NewTencentCloudProvider(domainFilter, zoneIDFilter, cfg.TencentCloudConfigFile, cfg.TencentCloudZoneType, cfg.DryRun)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)

View File

@ -194,6 +194,8 @@ type Config struct {
IBMCloudConfigFile string
TencentCloudConfigFile string
TencentCloudZoneType string
PluralCluster string
PluralProvider string
}
var defaultConfig = &Config{
@ -331,6 +333,8 @@ var defaultConfig = &Config{
IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json",
TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json",
TencentCloudZoneType: "",
PluralCluster: "",
PluralProvider: "",
}
// NewConfig returns new Config object
@ -420,7 +424,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets)
// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, ibmcloud, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns, tencentcloud)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "ibmcloud", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi", "safedns", "tencentcloud")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, ibmcloud, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns, tencentcloud)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "ibmcloud", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi", "safedns", "tencentcloud", "plural")
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
app.Flag("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default(defaultConfig.RegexDomainFilter.String()).RegexpVar(&cfg.RegexDomainFilter)
@ -534,6 +538,10 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("transip-account", "When using the TransIP provider, specify the account name (required when --provider=transip)").Default(defaultConfig.TransIPAccountName).StringVar(&cfg.TransIPAccountName)
app.Flag("transip-keyfile", "When using the TransIP provider, specify the path to the private key file (required when --provider=transip)").Default(defaultConfig.TransIPPrivateKeyFile).StringVar(&cfg.TransIPPrivateKeyFile)
// Flags related to the Plural provider
app.Flag("plural-cluster", "When using the plural provider, specify the cluster name you're running with").Default(defaultConfig.PluralCluster).StringVar(&cfg.PluralCluster)
app.Flag("plural-provider", "When using the plural provider, specify the provider name you're running with").Default(defaultConfig.PluralProvider).StringVar(&cfg.PluralProvider)
// Flags related to policies
app.Flag("policy", "Modify how DNS records are synchronized between sources and providers (default: sync, options: sync, upsert-only, create-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only", "create-only")

130
provider/plural/client.go Normal file
View File

@ -0,0 +1,130 @@
package plural
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/pluralsh/gqlclient"
"github.com/pluralsh/gqlclient/pkg/utils"
)
type authedTransport struct {
key string
wrapped http.RoundTripper
}
func (t *authedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", "Bearer "+t.key)
return t.wrapped.RoundTrip(req)
}
type Client interface {
DnsRecords() ([]*DnsRecord, error)
CreateRecord(record *DnsRecord) (*DnsRecord, error)
DeleteRecord(name, ttype string) error
}
type Config struct {
Token string
Endpoint string
Cluster string
Provider string
}
type client struct {
ctx context.Context
pluralClient *gqlclient.Client
config *Config
}
type DnsRecord struct {
Type string
Name string
Records []string
}
func NewClient(conf *Config) (Client, error) {
base, err := conf.BaseUrl()
if err != nil {
return nil, err
}
httpClient := http.Client{
Transport: &authedTransport{
key: conf.Token,
wrapped: http.DefaultTransport,
},
}
endpoint := base + "/gql"
return &client{
ctx: context.Background(),
pluralClient: gqlclient.NewClient(&httpClient, endpoint),
config: conf,
}, nil
}
func (c *Config) BaseUrl() (string, error) {
host := "https://app.plural.sh"
if c.Endpoint != "" {
host = fmt.Sprintf("https://%s", c.Endpoint)
if _, err := url.Parse(host); err != nil {
return "", err
}
}
return host, nil
}
func (client *client) DnsRecords() ([]*DnsRecord, error) {
resp, err := client.pluralClient.GetDNSRecords(client.ctx, client.config.Cluster, gqlclient.Provider(strings.ToUpper(client.config.Provider)))
if err != nil {
return nil, err
}
records := make([]*DnsRecord, 0)
for _, edge := range resp.DNSRecords.Edges {
if edge.Node != nil {
record := &DnsRecord{
Type: string(edge.Node.Type),
Name: edge.Node.Name,
Records: utils.ConvertStringArrayPointer(edge.Node.Records),
}
records = append(records, record)
}
}
return records, nil
}
func (client *client) CreateRecord(record *DnsRecord) (*DnsRecord, error) {
provider := gqlclient.Provider(strings.ToUpper(client.config.Provider))
cluster := client.config.Cluster
attr := gqlclient.DNSRecordAttributes{
Name: record.Name,
Type: gqlclient.DNSRecordType(record.Type),
Records: []*string{},
}
for _, record := range record.Records {
attr.Records = append(attr.Records, &record)
}
resp, err := client.pluralClient.CreateDNSRecord(client.ctx, cluster, provider, attr)
if err != nil {
return nil, err
}
return &DnsRecord{
Type: string(resp.CreateDNSRecord.Type),
Name: resp.CreateDNSRecord.Name,
Records: utils.ConvertStringArrayPointer(resp.CreateDNSRecord.Records),
}, nil
}
func (client *client) DeleteRecord(name, ttype string) error {
if _, err := client.pluralClient.DeleteDNSRecord(client.ctx, name, gqlclient.DNSRecordType(ttype)); err != nil {
return err
}
return nil
}

144
provider/plural/plural.go Normal file
View File

@ -0,0 +1,144 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plural
import (
"context"
"fmt"
"os"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
const (
CreateAction = "c"
DeleteAction = "d"
)
type PluralProvider struct {
provider.BaseProvider
Client Client
}
type RecordChange struct {
Action string
Record *DnsRecord
}
func NewPluralProvider(cluster, provider string) (*PluralProvider, error) {
token := os.Getenv("PLURAL_ACCESS_TOKEN")
endpoint := os.Getenv("PLURAL_ENDPOINT")
if token == "" {
return nil, fmt.Errorf("No plural access token provided, you must set the PLURAL_ACCESS_TOKEN env var")
}
config := &Config{
Token: token,
Endpoint: endpoint,
Cluster: cluster,
Provider: provider,
}
client, err := NewClient(config)
if err != nil {
return nil, err
}
prov := &PluralProvider{
Client: client,
}
return prov, nil
}
func (p *PluralProvider) Records(_ context.Context) (endpoints []*endpoint.Endpoint, err error) {
records, err := p.Client.DnsRecords()
if err != nil {
return
}
endpoints = make([]*endpoint.Endpoint, len(records))
for i, record := range records {
endpoints[i] = endpoint.NewEndpoint(record.Name, record.Type, record.Records...)
}
return
}
func (p *PluralProvider) PropertyValuesEqual(name, previous, current string) bool {
return p.BaseProvider.PropertyValuesEqual(name, previous, current)
}
func (p *PluralProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return endpoints
}
func (p *PluralProvider) ApplyChanges(_ context.Context, diffs *plan.Changes) error {
var changes []*RecordChange
for _, endpoint := range diffs.Create {
changes = append(changes, makeChange(CreateAction, endpoint.Targets, endpoint))
}
for _, desired := range diffs.UpdateNew {
changes = append(changes, makeChange(CreateAction, desired.Targets, desired))
}
for _, deleted := range diffs.Delete {
changes = append(changes, makeChange(DeleteAction, []string{}, deleted))
}
return p.applyChanges(changes)
}
func makeChange(change string, target []string, endpoint *endpoint.Endpoint) *RecordChange {
return &RecordChange{
Action: change,
Record: &DnsRecord{
Name: endpoint.DNSName,
Type: endpoint.RecordType,
Records: target,
},
}
}
func (p *PluralProvider) applyChanges(changes []*RecordChange) error {
for _, change := range changes {
logFields := log.Fields{
"name": change.Record.Name,
"type": change.Record.Type,
"action": change.Action,
}
log.WithFields(logFields).Info("Changing record.")
if change.Action == CreateAction {
_, err := p.Client.CreateRecord(change.Record)
if err != nil {
return err
}
}
if change.Action == DeleteAction {
if err := p.Client.DeleteRecord(change.Record.Name, change.Record.Type); err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,381 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plural
import (
"context"
"testing"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/provider"
)
type ClientStub struct {
mockDnsRecords []*DnsRecord
}
// CreateRecord provides a mock function with given fields: record
func (c *ClientStub) CreateRecord(record *DnsRecord) (*DnsRecord, error) {
c.mockDnsRecords = append(c.mockDnsRecords, record)
return record, nil
}
// DeleteRecord provides a mock function with given fields: name, ttype
func (c *ClientStub) DeleteRecord(name string, ttype string) error {
newRecords := make([]*DnsRecord, 0)
for _, record := range c.mockDnsRecords {
if record.Name == name && record.Type == ttype {
continue
}
newRecords = append(newRecords, record)
}
c.mockDnsRecords = newRecords
return nil
}
// DnsRecords provides a mock function with given fields:
func (c *ClientStub) DnsRecords() ([]*DnsRecord, error) {
return c.mockDnsRecords, nil
}
func newPluralProvider(pluralDNSRecord []*DnsRecord) *PluralProvider {
if pluralDNSRecord == nil {
pluralDNSRecord = make([]*DnsRecord, 0)
}
return &PluralProvider{
BaseProvider: provider.BaseProvider{},
Client: &ClientStub{
mockDnsRecords: pluralDNSRecord,
},
}
}
func TestPluralRecords(t *testing.T) {
tests := []struct {
name string
expectedEndpoints []*endpoint.Endpoint
records []*DnsRecord
}{
{
name: "check records",
records: []*DnsRecord{
{
Type: endpoint.RecordTypeA,
Name: "example.com",
Records: []string{"123.123.123.122"},
},
{
Type: endpoint.RecordTypeA,
Name: "nginx.example.com",
Records: []string{"123.123.123.123"},
},
{
Type: endpoint.RecordTypeCNAME,
Name: "hack.example.com",
Records: []string{"bluecatnetworks.com"},
},
{
Type: endpoint.RecordTypeTXT,
Name: "kdb.example.com",
Records: []string{"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"},
},
},
expectedEndpoints: []*endpoint.Endpoint{
{
DNSName: "example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.122"},
},
{
DNSName: "nginx.example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.123"},
},
{
DNSName: "hack.example.com",
RecordType: endpoint.RecordTypeCNAME,
Targets: endpoint.Targets{"bluecatnetworks.com"},
},
{
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"},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
provider := newPluralProvider(test.records)
actual, err := provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
validateEndpoints(t, actual, test.expectedEndpoints)
})
}
}
func TestPluralApplyChangesCreate(t *testing.T) {
tests := []struct {
name string
expectedEndpoints []*endpoint.Endpoint
}{
{
name: "create new endpoints",
expectedEndpoints: []*endpoint.Endpoint{
{
DNSName: "example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.122"},
},
{
DNSName: "nginx.example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.123"},
},
{
DNSName: "hack.example.com",
RecordType: endpoint.RecordTypeCNAME,
Targets: endpoint.Targets{"bluecatnetworks.com"},
},
{
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"},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
provider := newPluralProvider(nil)
// no records
actual, err := provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
assert.Equal(t, len(actual), 0, "expected no entries")
err = provider.ApplyChanges(context.Background(), &plan.Changes{Create: test.expectedEndpoints})
if err != nil {
t.Fatal(err)
}
actual, err = provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
validateEndpoints(t, actual, test.expectedEndpoints)
})
}
}
func TestPluralApplyChangesDelete(t *testing.T) {
tests := []struct {
name string
records []*DnsRecord
deleteEndpoints []*endpoint.Endpoint
expectedEndpoints []*endpoint.Endpoint
}{
{
name: "delete not existing record",
records: []*DnsRecord{
{
Type: endpoint.RecordTypeA,
Name: "example.com",
Records: []string{"123.123.123.122"},
},
{
Type: endpoint.RecordTypeA,
Name: "nginx.example.com",
Records: []string{"123.123.123.123"},
},
{
Type: endpoint.RecordTypeCNAME,
Name: "hack.example.com",
Records: []string{"bluecatnetworks.com"},
},
{
Type: endpoint.RecordTypeTXT,
Name: "kdb.example.com",
Records: []string{"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"},
},
},
deleteEndpoints: []*endpoint.Endpoint{
{
DNSName: "fake.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"1.2.3.4"},
},
},
expectedEndpoints: []*endpoint.Endpoint{
{
DNSName: "example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.122"},
},
{
DNSName: "nginx.example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.123"},
},
{
DNSName: "hack.example.com",
RecordType: endpoint.RecordTypeCNAME,
Targets: endpoint.Targets{"bluecatnetworks.com"},
},
{
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"},
},
},
},
{
name: "delete one record",
records: []*DnsRecord{
{
Type: endpoint.RecordTypeA,
Name: "example.com",
Records: []string{"123.123.123.122"},
},
{
Type: endpoint.RecordTypeA,
Name: "nginx.example.com",
Records: []string{"123.123.123.123"},
},
{
Type: endpoint.RecordTypeCNAME,
Name: "hack.example.com",
Records: []string{"bluecatnetworks.com"},
},
{
Type: endpoint.RecordTypeTXT,
Name: "kdb.example.com",
Records: []string{"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"},
},
},
deleteEndpoints: []*endpoint.Endpoint{
{
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"},
},
},
expectedEndpoints: []*endpoint.Endpoint{
{
DNSName: "example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.122"},
},
{
DNSName: "nginx.example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.123"},
},
{
DNSName: "hack.example.com",
RecordType: endpoint.RecordTypeCNAME,
Targets: endpoint.Targets{"bluecatnetworks.com"},
},
},
},
{
name: "delete all records",
records: []*DnsRecord{
{
Type: endpoint.RecordTypeA,
Name: "example.com",
Records: []string{"123.123.123.122"},
},
{
Type: endpoint.RecordTypeA,
Name: "nginx.example.com",
Records: []string{"123.123.123.123"},
},
{
Type: endpoint.RecordTypeCNAME,
Name: "hack.example.com",
Records: []string{"bluecatnetworks.com"},
},
{
Type: endpoint.RecordTypeTXT,
Name: "kdb.example.com",
Records: []string{"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"},
},
},
deleteEndpoints: []*endpoint.Endpoint{
{
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"},
},
{
DNSName: "example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.122"},
},
{
DNSName: "nginx.example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.123"},
},
{
DNSName: "hack.example.com",
RecordType: endpoint.RecordTypeCNAME,
Targets: endpoint.Targets{"bluecatnetworks.com"},
},
},
expectedEndpoints: []*endpoint.Endpoint{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
provider := newPluralProvider(test.records)
err := provider.ApplyChanges(context.Background(), &plan.Changes{Delete: test.deleteEndpoints})
if err != nil {
t.Fatal(err)
}
actual, err := provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
validateEndpoints(t, actual, test.expectedEndpoints)
})
}
}
func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
assert.True(t, testutils.SameEndpoints(endpoints, expected), "expected and actual endpoints don't match. %s:%s", endpoints, expected)
}