Merge branch 'master' into master

This commit is contained in:
Kushal Bhandari 2020-06-18 10:56:13 -07:00 committed by GitHub
commit 4809e19d4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 740 additions and 21 deletions

View File

@ -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
View 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
View File

@ -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
View File

@ -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=

View 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"]

View 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

View 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

View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns

View File

@ -0,0 +1,5 @@
resources:
- ./external-dns-deployment.yaml
- ./external-dns-serviceaccount.yaml
- ./external-dns-clusterrole.yaml
- ./external-dns-clusterrolebinding.yaml

View File

@ -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":

View File

@ -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)

View File

@ -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
View 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
}

View 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)
}
}

View File

@ -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
}

View File

@ -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()