mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
Merge branch 'master' into master
This commit is contained in:
commit
4809e19d4e
@ -31,6 +31,7 @@ ExternalDNS' current release is `v0.7`. This version allows you to keep selected
|
||||
* [CloudFlare](https://www.cloudflare.com/dns)
|
||||
* [RcodeZero](https://www.rcodezero.at/)
|
||||
* [DigitalOcean](https://www.digitalocean.com/products/networking)
|
||||
* [Hetzner](https://hetzner.com/)
|
||||
* [DNSimple](https://dnsimple.com/)
|
||||
* [Infoblox](https://www.infoblox.com/products/dns/)
|
||||
* [Dyn](https://dyn.com/dns/)
|
||||
@ -79,6 +80,7 @@ The following table clarifies the current status of the providers according to t
|
||||
| CloudFlare | Beta | |
|
||||
| RcodeZero | Alpha | |
|
||||
| DigitalOcean | Alpha | |
|
||||
| Hetzner | Alpha | @21h |
|
||||
| DNSimple | Alpha | |
|
||||
| Infoblox | Alpha | @saileshgiri |
|
||||
| Dyn | Alpha | |
|
||||
@ -121,6 +123,7 @@ The following tutorials are provided:
|
||||
* [Cloudflare](docs/tutorials/cloudflare.md)
|
||||
* [CoreDNS](docs/tutorials/coredns.md)
|
||||
* [DigitalOcean](docs/tutorials/digitalocean.md)
|
||||
* [Hetzner](docs/tutorials/hetzner.md)
|
||||
* [DNSimple](docs/tutorials/dnsimple.md)
|
||||
* [Dyn](docs/tutorials/dyn.md)
|
||||
* [Exoscale](docs/tutorials/exoscale.md)
|
||||
|
191
docs/tutorials/hetzner.md
Normal file
191
docs/tutorials/hetzner.md
Normal file
@ -0,0 +1,191 @@
|
||||
# Setting up ExternalDNS for Services on Hetzner DNS
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Hetzner DNS.
|
||||
|
||||
Make sure to use **>=0.7.3** version of ExternalDNS for this tutorial.
|
||||
|
||||
## Creating a Hetzner DNS zone
|
||||
|
||||
If you want to learn about how to use Hetzner's DNS service read the following tutorial series:
|
||||
|
||||
[An Introduction to Managing DNS](https://wiki.hetzner.de/index.php/DNS_Overview), and [Add a new DNS zone](https://wiki.hetzner.de/index.php/Getting_started).
|
||||
|
||||
Create a new DNS zone where you want to create your records in. Let's use `example.com` as an example here.
|
||||
|
||||
## Creating Hetzner Credentials
|
||||
|
||||
Generate a new personal token by going to [the API settings](https://dns.hetzner.com/settings/api-token) or follow [Generating an API access token](https://wiki.hetzner.de/index.php/API_access_token) if you need more information. Give the token a name and choose read and write access. The token needs to be passed to ExternalDNS so make a note of it for later use.
|
||||
|
||||
The environment variable `HETZNER_TOKEN` will be needed to run ExternalDNS with Hetzner.
|
||||
|
||||
## 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:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: eu.gcr.io/k8s-artifacts-prod/external-dns/external-dns:v0.7.3
|
||||
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=hetzner
|
||||
env:
|
||||
- name: HETZNER_TOKEN
|
||||
value: "YOUR_HETZNER_DNS_API_KEY"
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled)
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services","endpoints","pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: external-dns
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: external-dns
|
||||
namespace: default
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
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=hetzner
|
||||
env:
|
||||
- name: HETZNER_TOKEN
|
||||
value: "YOUR_HETZNER_DNS_API_KEY"
|
||||
```
|
||||
|
||||
|
||||
## 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:
|
||||
replicas: 1
|
||||
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 Hetzner 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 Hetzner DNS records.
|
||||
|
||||
## Verifying Hetzner DNS records
|
||||
|
||||
Check your [Hetzner DNS UI](https://dns.hetzner.com/) to view the records for your Hetzner 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 Hetzner DNS records, we can delete the tutorial's example:
|
||||
|
||||
```
|
||||
$ kubectl delete service -f nginx.yaml
|
||||
$ kubectl delete service -f externaldns.yaml
|
||||
```
|
3
go.mod
3
go.mod
@ -4,6 +4,7 @@ go 1.14
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.50.0
|
||||
git.blindage.org/21h/hcloud-dns v0.0.0-20200525170043-def10a4a28e0
|
||||
github.com/Azure/azure-sdk-for-go v36.0.0+incompatible
|
||||
github.com/Azure/go-autorest/autorest v0.9.4
|
||||
github.com/Azure/go-autorest/autorest/adal v0.8.3
|
||||
@ -58,6 +59,7 @@ require (
|
||||
github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92
|
||||
github.com/vultr/govultr v0.3.2
|
||||
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875
|
||||
go.uber.org/ratelimit v0.1.0
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
google.golang.org/api v0.15.0
|
||||
@ -68,7 +70,6 @@ require (
|
||||
k8s.io/api v0.17.5
|
||||
k8s.io/apimachinery v0.17.5
|
||||
k8s.io/client-go v0.17.5
|
||||
k8s.io/klog v1.0.0
|
||||
)
|
||||
|
||||
replace (
|
||||
|
11
go.sum
11
go.sum
@ -15,6 +15,8 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
|
||||
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=
|
||||
git.blindage.org/21h/hcloud-dns v0.0.0-20200525170043-def10a4a28e0 h1:kdxglEveTcqIG5zEPdQ0Y5KctnIGR7zXsQCQakoTNxU=
|
||||
git.blindage.org/21h/hcloud-dns v0.0.0-20200525170043-def10a4a28e0/go.mod h1:n26Twiii5jhkMC+Ocz/s8R73cBBcXRIwyTqQ+6bOZGo=
|
||||
github.com/Azure/azure-sdk-for-go v36.0.0+incompatible h1:XIaBmA4pgKqQ7jInQPaNJQ4pOHrdJjw9gYXhbyiChaU=
|
||||
github.com/Azure/azure-sdk-for-go v36.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
@ -355,8 +357,6 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65 h1:FP5rOFP4ifbtFIjFHJmwhFrsbDyONILK/FNntl/Pou8=
|
||||
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
||||
@ -483,7 +483,6 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -625,6 +624,8 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/ratelimit v0.1.0 h1:U2AruXqeTb4Eh9sYQSTrMhH8Cb7M0Ian2ibBOnBcnAw=
|
||||
go.uber.org/ratelimit v0.1.0/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
|
||||
go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||
@ -839,7 +840,6 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
|
||||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
@ -847,7 +847,6 @@ google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -872,9 +871,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
17
kustomize/external-dns-clusterrole.yaml
Normal file
17
kustomize/external-dns-clusterrole.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
12
kustomize/external-dns-clusterrolebinding.yaml
Normal file
12
kustomize/external-dns-clusterrolebinding.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: external-dns
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: external-dns
|
||||
namespace: default
|
22
kustomize/external-dns-deployment.yaml
Normal file
22
kustomize/external-dns-deployment.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
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: us.gcr.io/k8s-artifacts-prod/external-dns/external-dns:v0.7.2
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --registry=txt
|
4
kustomize/external-dns-serviceaccount.yaml
Normal file
4
kustomize/external-dns-serviceaccount.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
5
kustomize/kustomization.yaml
Normal file
5
kustomize/kustomization.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
resources:
|
||||
- ./external-dns-deployment.yaml
|
||||
- ./external-dns-serviceaccount.yaml
|
||||
- ./external-dns-clusterrole.yaml
|
||||
- ./external-dns-clusterrolebinding.yaml
|
5
main.go
5
main.go
@ -47,6 +47,7 @@ import (
|
||||
"sigs.k8s.io/external-dns/provider/dyn"
|
||||
"sigs.k8s.io/external-dns/provider/exoscale"
|
||||
"sigs.k8s.io/external-dns/provider/google"
|
||||
"sigs.k8s.io/external-dns/provider/hetzner"
|
||||
"sigs.k8s.io/external-dns/provider/infoblox"
|
||||
"sigs.k8s.io/external-dns/provider/inmemory"
|
||||
"sigs.k8s.io/external-dns/provider/linode"
|
||||
@ -201,8 +202,10 @@ func main() {
|
||||
p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.DryRun)
|
||||
case "digitalocean":
|
||||
p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize)
|
||||
case "hetzner":
|
||||
p, err = hetzner.NewHetznerProvider(ctx, domainFilter, cfg.DryRun)
|
||||
case "ovh":
|
||||
p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.DryRun)
|
||||
p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.OVHApiRateLimit, cfg.DryRun)
|
||||
case "linode":
|
||||
p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
|
||||
case "dnsimple":
|
||||
|
@ -99,6 +99,7 @@ type Config struct {
|
||||
OCIConfigFile string
|
||||
InMemoryZones []string
|
||||
OVHEndpoint string
|
||||
OVHApiRateLimit int
|
||||
PDNSServer string
|
||||
PDNSAPIKey string `secure:"yes"`
|
||||
PDNSTLSEnabled bool
|
||||
@ -197,6 +198,7 @@ var defaultConfig = &Config{
|
||||
OCIConfigFile: "/etc/kubernetes/oci.yaml",
|
||||
InMemoryZones: []string{},
|
||||
OVHEndpoint: "ovh-eu",
|
||||
OVHApiRateLimit: 20,
|
||||
PDNSServer: "http://localhost:8081",
|
||||
PDNSAPIKey: "",
|
||||
PDNSTLSEnabled: false,
|
||||
@ -316,7 +318,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter)
|
||||
|
||||
// Flags related to providers
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, vultr, ultradns)").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", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "vultr", "ultradns")
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, vultr, ultradns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "vultr", "ultradns")
|
||||
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("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter)
|
||||
@ -360,6 +362,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
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)
|
||||
app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)
|
||||
app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)
|
||||
app.Flag("pdns-tls-enabled", "When using the PowerDNS/PDNS provider, specify whether to use TLS (default: false, requires --tls-ca, optionally specify --tls-client-cert and --tls-client-cert-key)").Default(strconv.FormatBool(defaultConfig.PDNSTLSEnabled)).BoolVar(&cfg.PDNSTLSEnabled)
|
||||
|
@ -75,6 +75,7 @@ var (
|
||||
OCIConfigFile: "/etc/kubernetes/oci.yaml",
|
||||
InMemoryZones: []string{""},
|
||||
OVHEndpoint: "ovh-eu",
|
||||
OVHApiRateLimit: 20,
|
||||
PDNSServer: "http://localhost:8081",
|
||||
PDNSAPIKey: "",
|
||||
Policy: "sync",
|
||||
@ -149,6 +150,7 @@ var (
|
||||
OCIConfigFile: "oci.yaml",
|
||||
InMemoryZones: []string{"example.org", "company.com"},
|
||||
OVHEndpoint: "ovh-ca",
|
||||
OVHApiRateLimit: 42,
|
||||
PDNSServer: "http://ns.example.com:8081",
|
||||
PDNSAPIKey: "some-secret-key",
|
||||
PDNSTLSEnabled: true,
|
||||
@ -237,6 +239,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"--inmemory-zone=example.org",
|
||||
"--inmemory-zone=company.com",
|
||||
"--ovh-endpoint=ovh-ca",
|
||||
"--ovh-api-rate-limit=42",
|
||||
"--pdns-server=http://ns.example.com:8081",
|
||||
"--pdns-api-key=some-secret-key",
|
||||
"--pdns-tls-enabled",
|
||||
@ -326,6 +329,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml",
|
||||
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
|
||||
"EXTERNAL_DNS_OVH_ENDPOINT": "ovh-ca",
|
||||
"EXTERNAL_DNS_OVH_API_RATE_LIMIT": "42",
|
||||
"EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com",
|
||||
"EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com",
|
||||
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
|
||||
|
214
provider/hetzner/hetzner.go
Normal file
214
provider/hetzner/hetzner.go
Normal file
@ -0,0 +1,214 @@
|
||||
/*
|
||||
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 hetzner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
hclouddns "git.blindage.org/21h/hcloud-dns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
hetznerCreate = "CREATE"
|
||||
hetznerDelete = "DELETE"
|
||||
hetznerUpdate = "UPDATE"
|
||||
hetznerTTL = 600
|
||||
)
|
||||
|
||||
type HetznerChanges struct {
|
||||
Action string
|
||||
ZoneID string
|
||||
ZoneName string
|
||||
ResourceRecordSet hclouddns.HCloudRecord
|
||||
}
|
||||
|
||||
type HetznerProvider struct {
|
||||
provider.BaseProvider
|
||||
Client hclouddns.HCloudClientAdapter
|
||||
domainFilter endpoint.DomainFilter
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
func NewHetznerProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*HetznerProvider, error) {
|
||||
token, ok := os.LookupEnv("HETZNER_TOKEN")
|
||||
if !ok {
|
||||
return nil, errors.New("no environment variable HETZNER_TOKEN provided")
|
||||
}
|
||||
|
||||
client := hclouddns.New(token)
|
||||
|
||||
provider := &HetznerProvider{
|
||||
Client: client,
|
||||
domainFilter: domainFilter,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
zones, err := p.Client.GetZones(hclouddns.HCloudGetZonesParams{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
for _, zone := range zones.Zones {
|
||||
records, err := p.Client.GetRecords(hclouddns.HCloudGetRecordsParams{ZoneID: zone.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range records.Records {
|
||||
if provider.SupportedRecordType(string(r.RecordType)) {
|
||||
name := r.Name + "." + zone.Name
|
||||
|
||||
if r.Name == "@" {
|
||||
name = zone.Name
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(name, string(r.RecordType), r.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
combinedChanges := make([]*HetznerChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
|
||||
|
||||
combinedChanges = append(combinedChanges, p.newHetznerChanges(hetznerCreate, changes.Create)...)
|
||||
combinedChanges = append(combinedChanges, p.newHetznerChanges(hetznerUpdate, changes.UpdateNew)...)
|
||||
combinedChanges = append(combinedChanges, p.newHetznerChanges(hetznerDelete, changes.Delete)...)
|
||||
|
||||
return p.submitChanges(ctx, combinedChanges)
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) submitChanges(ctx context.Context, changes []*HetznerChanges) error {
|
||||
if len(changes) == 0 {
|
||||
log.Infof("All records are already up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
zones, err := p.Client.GetZones(hclouddns.HCloudGetZonesParams{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zoneChanges := p.seperateChangesByZone(zones.Zones, changes)
|
||||
|
||||
for _, changes := range zoneChanges {
|
||||
for _, change := range changes {
|
||||
log.WithFields(log.Fields{
|
||||
"record": change.ResourceRecordSet.Name,
|
||||
"type": change.ResourceRecordSet.RecordType,
|
||||
"ttl": change.ResourceRecordSet.TTL,
|
||||
"action": change.Action,
|
||||
"zone": change.ZoneName,
|
||||
"zone_id": change.ZoneID,
|
||||
}).Info("Changing record.")
|
||||
|
||||
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, "."+change.ZoneName)
|
||||
if change.ResourceRecordSet.Name == change.ZoneName {
|
||||
change.ResourceRecordSet.Name = "@"
|
||||
}
|
||||
if change.ResourceRecordSet.RecordType == endpoint.RecordTypeCNAME {
|
||||
change.ResourceRecordSet.Value += "."
|
||||
}
|
||||
|
||||
switch change.Action {
|
||||
case hetznerCreate:
|
||||
record := hclouddns.HCloudRecord{
|
||||
RecordType: change.ResourceRecordSet.RecordType,
|
||||
ZoneID: change.ZoneID,
|
||||
Name: change.ResourceRecordSet.Name,
|
||||
Value: change.ResourceRecordSet.Value,
|
||||
TTL: change.ResourceRecordSet.TTL,
|
||||
}
|
||||
_, err := p.Client.CreateRecord(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case hetznerDelete:
|
||||
_, err := p.Client.DeleteRecord(change.ResourceRecordSet.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case hetznerUpdate:
|
||||
record := hclouddns.HCloudRecord{
|
||||
RecordType: change.ResourceRecordSet.RecordType,
|
||||
ZoneID: change.ZoneID,
|
||||
Name: change.ResourceRecordSet.Name,
|
||||
Value: change.ResourceRecordSet.Value,
|
||||
TTL: change.ResourceRecordSet.TTL,
|
||||
}
|
||||
_, err := p.Client.UpdateRecord(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) newHetznerChanges(action string, endpoints []*endpoint.Endpoint) []*HetznerChanges {
|
||||
changes := make([]*HetznerChanges, 0, len(endpoints))
|
||||
ttl := hetznerTTL
|
||||
for _, e := range endpoints {
|
||||
if e.RecordTTL.IsConfigured() {
|
||||
ttl = int(e.RecordTTL)
|
||||
}
|
||||
change := &HetznerChanges{
|
||||
Action: action,
|
||||
ResourceRecordSet: hclouddns.HCloudRecord{
|
||||
RecordType: hclouddns.RecordType(e.RecordType),
|
||||
Name: e.DNSName,
|
||||
Value: e.Targets[0],
|
||||
TTL: ttl,
|
||||
},
|
||||
}
|
||||
changes = append(changes, change)
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) seperateChangesByZone(zones []hclouddns.HCloudZone, changes []*HetznerChanges) map[string][]*HetznerChanges {
|
||||
change := make(map[string][]*HetznerChanges)
|
||||
zoneNameID := provider.ZoneIDName{}
|
||||
|
||||
for _, z := range zones {
|
||||
zoneNameID.Add(z.ID, z.Name)
|
||||
change[z.ID] = []*HetznerChanges{}
|
||||
}
|
||||
|
||||
for _, c := range changes {
|
||||
zoneID, zoneName := zoneNameID.FindZone(c.ResourceRecordSet.Name)
|
||||
if zoneName == "" {
|
||||
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSet.Name)
|
||||
continue
|
||||
}
|
||||
c.ZoneName = zoneName
|
||||
c.ZoneID = zoneID
|
||||
change[zoneID] = append(change[zoneID], c)
|
||||
}
|
||||
return change
|
||||
}
|
227
provider/hetzner/hetzner_test.go
Normal file
227
provider/hetzner/hetzner_test.go
Normal file
@ -0,0 +1,227 @@
|
||||
/*
|
||||
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 hetzner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
hclouddns "git.blindage.org/21h/hcloud-dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
type mockHCloudClientAdapter interface {
|
||||
GetZone(ID string) (hclouddns.HCloudAnswerGetZone, error)
|
||||
GetZones(params hclouddns.HCloudGetZonesParams) (hclouddns.HCloudAnswerGetZones, error)
|
||||
UpdateZone(zone hclouddns.HCloudZone) (hclouddns.HCloudAnswerGetZone, error)
|
||||
DeleteZone(ID string) (hclouddns.HCloudAnswerDeleteZone, error)
|
||||
CreateZone(zone hclouddns.HCloudZone) (hclouddns.HCloudAnswerGetZone, error)
|
||||
ImportZoneString(zoneID string, zonePlainText string) (hclouddns.HCloudAnswerGetZone, error)
|
||||
ExportZoneToString(zoneID string) (hclouddns.HCloudAnswerGetZonePlainText, error)
|
||||
ValidateZoneString(zonePlainText string) (hclouddns.HCloudAnswerZoneValidate, error)
|
||||
GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error)
|
||||
UpdateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error)
|
||||
DeleteRecord(ID string) (hclouddns.HCloudAnswerDeleteRecord, error)
|
||||
CreateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error)
|
||||
CreateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerCreateRecords, error)
|
||||
UpdateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerUpdateRecords, error)
|
||||
}
|
||||
|
||||
type mockHCloudClient struct {
|
||||
Token string `yaml:"token"`
|
||||
}
|
||||
|
||||
// New instance
|
||||
func mockHCloudNew(t string) mockHCloudClientAdapter {
|
||||
return &mockHCloudClient{
|
||||
Token: t,
|
||||
}
|
||||
}
|
||||
|
||||
// Mock all methods
|
||||
|
||||
func (m *mockHCloudClient) GetZone(ID string) (hclouddns.HCloudAnswerGetZone, error) {
|
||||
return hclouddns.HCloudAnswerGetZone{}, nil
|
||||
}
|
||||
|
||||
func (m *mockHCloudClient) GetZones(params hclouddns.HCloudGetZonesParams) (hclouddns.HCloudAnswerGetZones, error) {
|
||||
return hclouddns.HCloudAnswerGetZones{
|
||||
Zones: []hclouddns.HCloudZone{
|
||||
{
|
||||
ID: "HetznerZoneID",
|
||||
Name: "blindage.org",
|
||||
TTL: 666,
|
||||
RecordsCount: 1,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// zones
|
||||
func (m *mockHCloudClient) UpdateZone(zone hclouddns.HCloudZone) (hclouddns.HCloudAnswerGetZone, error) {
|
||||
return hclouddns.HCloudAnswerGetZone{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) DeleteZone(ID string) (hclouddns.HCloudAnswerDeleteZone, error) {
|
||||
return hclouddns.HCloudAnswerDeleteZone{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) CreateZone(zone hclouddns.HCloudZone) (hclouddns.HCloudAnswerGetZone, error) {
|
||||
return hclouddns.HCloudAnswerGetZone{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) ImportZoneString(zoneID string, zonePlainText string) (hclouddns.HCloudAnswerGetZone, error) {
|
||||
return hclouddns.HCloudAnswerGetZone{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) ExportZoneToString(zoneID string) (hclouddns.HCloudAnswerGetZonePlainText, error) {
|
||||
return hclouddns.HCloudAnswerGetZonePlainText{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) ValidateZoneString(zonePlainText string) (hclouddns.HCloudAnswerZoneValidate, error) {
|
||||
return hclouddns.HCloudAnswerZoneValidate{}, nil
|
||||
}
|
||||
|
||||
// records
|
||||
func (m *mockHCloudClient) GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error) {
|
||||
return hclouddns.HCloudAnswerGetRecords{
|
||||
Records: []hclouddns.HCloudRecord{
|
||||
{
|
||||
RecordType: hclouddns.RecordType("A"),
|
||||
ID: "ATypeRecordID",
|
||||
ZoneID: "HetznerZoneID",
|
||||
Name: "@",
|
||||
Value: "127.0.0.1",
|
||||
TTL: 666,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) UpdateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error) {
|
||||
return hclouddns.HCloudAnswerGetRecord{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) DeleteRecord(ID string) (hclouddns.HCloudAnswerDeleteRecord, error) {
|
||||
return hclouddns.HCloudAnswerDeleteRecord{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) CreateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error) {
|
||||
return hclouddns.HCloudAnswerGetRecord{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) CreateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerCreateRecords, error) {
|
||||
return hclouddns.HCloudAnswerCreateRecords{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) UpdateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerUpdateRecords, error) {
|
||||
return hclouddns.HCloudAnswerUpdateRecords{}, nil
|
||||
}
|
||||
|
||||
func TestNewHetznerProvider(t *testing.T) {
|
||||
_ = os.Setenv("HETZNER_TOKEN", "myHetznerToken")
|
||||
_, err := NewHetznerProvider(context.Background(), endpoint.NewDomainFilter([]string{"blindage.org"}), true)
|
||||
if err != nil {
|
||||
t.Errorf("failed : %s", err)
|
||||
}
|
||||
|
||||
_ = os.Unsetenv("HETZNER_TOKEN")
|
||||
_, err = NewHetznerProvider(context.Background(), endpoint.NewDomainFilter([]string{"blindage.org"}), true)
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHetznerProvider_TestData(t *testing.T) {
|
||||
|
||||
mockedClient := mockHCloudNew("myHetznerToken")
|
||||
|
||||
// Check test zone data is ok
|
||||
expectedZonesAnswer := hclouddns.HCloudAnswerGetZones{
|
||||
Zones: []hclouddns.HCloudZone{
|
||||
{
|
||||
ID: "HetznerZoneID",
|
||||
Name: "blindage.org",
|
||||
TTL: 666,
|
||||
RecordsCount: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testingZonesAnswer, err := mockedClient.GetZones(hclouddns.HCloudGetZonesParams{})
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedZonesAnswer, testingZonesAnswer) {
|
||||
t.Errorf("should be equal, %s", err)
|
||||
}
|
||||
|
||||
// Check test record data is ok
|
||||
expectedRecordsAnswer := hclouddns.HCloudAnswerGetRecords{
|
||||
Records: []hclouddns.HCloudRecord{
|
||||
{
|
||||
RecordType: hclouddns.RecordType("A"),
|
||||
ID: "ATypeRecordID",
|
||||
ZoneID: "HetznerZoneID",
|
||||
Name: "@",
|
||||
Value: "127.0.0.1",
|
||||
TTL: 666,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testingRecordsAnswer, err := mockedClient.GetRecords(hclouddns.HCloudGetRecordsParams{})
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedRecordsAnswer, testingRecordsAnswer) {
|
||||
t.Errorf("should be equal, %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHetznerProvider_Records(t *testing.T) {
|
||||
|
||||
mockedClient := mockHCloudNew("myHetznerToken")
|
||||
|
||||
mockedProvider := &HetznerProvider{
|
||||
Client: mockedClient,
|
||||
}
|
||||
|
||||
// Now check Records function of provider, if ZoneID equal "blindage.org" must be returned
|
||||
endpoints, err := mockedProvider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
fmt.Printf("%+v\n", endpoints[0].DNSName)
|
||||
assert.Equal(t, "blindage.org", endpoints[0].DNSName)
|
||||
}
|
||||
|
||||
func TestHetznerProvider_ApplyChanges(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockHCloudNew("myHetznerToken")
|
||||
mockedProvider := &HetznerProvider{
|
||||
Client: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "test.org", Targets: endpoint.Targets{"target"}},
|
||||
{DNSName: "test.test.org", Targets: endpoint.Targets{"target"}, RecordTTL: 666},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.test.org", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 777}}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "test.test.org", Targets: endpoint.Targets{"target"}, RecordType: "A"}}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
}
|
@ -29,6 +29,8 @@ import (
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
|
||||
"go.uber.org/ratelimit"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -50,6 +52,8 @@ type OVHProvider struct {
|
||||
|
||||
client ovhClient
|
||||
|
||||
apiRateLimiter ratelimit.Limiter
|
||||
|
||||
domainFilter endpoint.DomainFilter
|
||||
DryRun bool
|
||||
}
|
||||
@ -79,7 +83,7 @@ type ovhChange struct {
|
||||
}
|
||||
|
||||
// NewOVHProvider initializes a new OVH DNS based Provider.
|
||||
func NewOVHProvider(ctx context.Context, domainFilter endpoint.DomainFilter, endpoint string, dryRun bool) (*OVHProvider, error) {
|
||||
func NewOVHProvider(ctx context.Context, domainFilter endpoint.DomainFilter, endpoint string, apiRateLimit int, dryRun bool) (*OVHProvider, error) {
|
||||
client, err := ovh.NewEndpointClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -89,9 +93,10 @@ func NewOVHProvider(ctx context.Context, domainFilter endpoint.DomainFilter, end
|
||||
return nil, ErrNoDryRun
|
||||
}
|
||||
return &OVHProvider{
|
||||
client: client,
|
||||
domainFilter: domainFilter,
|
||||
DryRun: dryRun,
|
||||
client: client,
|
||||
domainFilter: domainFilter,
|
||||
apiRateLimiter: ratelimit.New(apiRateLimit),
|
||||
DryRun: dryRun,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -149,10 +154,14 @@ func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) e
|
||||
|
||||
func (p *OVHProvider) refresh(zone string) error {
|
||||
log.Debugf("OVH: Refresh %s zone", zone)
|
||||
|
||||
p.apiRateLimiter.Take()
|
||||
return p.client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
|
||||
}
|
||||
|
||||
func (p *OVHProvider) change(change ovhChange) error {
|
||||
p.apiRateLimiter.Take()
|
||||
|
||||
switch change.Action {
|
||||
case ovhCreate:
|
||||
log.Debugf("OVH: Add an entry to %s", change.String())
|
||||
@ -194,6 +203,7 @@ func (p *OVHProvider) zones() ([]string, error) {
|
||||
zones := []string{}
|
||||
filteredZones := []string{}
|
||||
|
||||
p.apiRateLimiter.Take()
|
||||
if err := p.client.Get("/domain/zone", &zones); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -213,6 +223,8 @@ func (p *OVHProvider) records(ctx *context.Context, zone *string, records chan<-
|
||||
eg, _ := errgroup.WithContext(*ctx)
|
||||
|
||||
log.Debugf("OVH: Getting records for %s", *zone)
|
||||
|
||||
p.apiRateLimiter.Take()
|
||||
if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record", *zone), &recordsIds); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -236,6 +248,8 @@ func (p *OVHProvider) record(zone *string, id uint64, records chan<- ovhRecord)
|
||||
record := ovhRecord{}
|
||||
|
||||
log.Debugf("OVH: Getting record %d for %s", id, *zone)
|
||||
|
||||
p.apiRateLimiter.Take()
|
||||
if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record/%d", *zone, id), &record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/ovh/go-ovh/ovh"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"go.uber.org/ratelimit"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
@ -58,8 +59,9 @@ func TestOvhZones(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{
|
||||
client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"com"}),
|
||||
client: client,
|
||||
apiRateLimiter: ratelimit.New(10),
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"com"}),
|
||||
}
|
||||
|
||||
// Basic zones
|
||||
@ -81,7 +83,7 @@ func TestOvhZones(t *testing.T) {
|
||||
func TestOvhZoneRecords(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10)}
|
||||
|
||||
// Basic zones records
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||
@ -125,7 +127,7 @@ func TestOvhZoneRecords(t *testing.T) {
|
||||
func TestOvhRecords(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10)}
|
||||
|
||||
// Basic zones records
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.org", "example.net"}, nil).Once()
|
||||
@ -158,7 +160,7 @@ func TestOvhRecords(t *testing.T) {
|
||||
|
||||
func TestOvhRefresh(t *testing.T) {
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10)}
|
||||
|
||||
// Basic zone refresh
|
||||
client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
||||
@ -199,7 +201,7 @@ func TestOvhNewChange(t *testing.T) {
|
||||
func TestOvhApplyChanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10)}
|
||||
changes := plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||
@ -252,7 +254,7 @@ func TestOvhApplyChanges(t *testing.T) {
|
||||
func TestOvhChange(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10)}
|
||||
|
||||
// Record creation
|
||||
client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "ovh"}).Return(nil, nil).Once()
|
||||
|
Loading…
Reference in New Issue
Block a user