mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
Add OVH Provider
- OVH Provider - Tests - Documentations
This commit is contained in:
parent
1e7c600279
commit
430e357d27
@ -44,6 +44,7 @@ ExternalDNS' current release is `v0.6`. This version allows you to keep selected
|
||||
* [NS1](https://ns1.com/)
|
||||
* [TransIP](https://www.transip.eu/domain-name/)
|
||||
* [VinylDNS](https://www.vinyldns.io)
|
||||
* [OVH](https://www.ovh.com)
|
||||
|
||||
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.
|
||||
|
||||
@ -93,6 +94,7 @@ The following table clarifies the current status of the providers according to t
|
||||
| VinylDNS | Alpha |
|
||||
| RancherDNS | Alpha |
|
||||
| Akamai FastDNS | Alpha |
|
||||
| OVH | Alpha |
|
||||
|
||||
## Running ExternalDNS:
|
||||
|
||||
@ -139,6 +141,7 @@ The following tutorials are provided:
|
||||
* [RFC2136](docs/tutorials/rfc2136.md)
|
||||
* [TransIP](docs/tutorials/transip.md)
|
||||
* [VinylDNS](docs/tutorials/vinyldns.md)
|
||||
* [OVH](docs/tutorials/ovh.md)
|
||||
|
||||
### Running Locally
|
||||
|
||||
@ -269,6 +272,7 @@ Here's a rough outline on what is to come (subject to change):
|
||||
### v0.6
|
||||
|
||||
- [ ] Ability to replace Kops' [DNS Controller](https://github.com/kubernetes/kops/tree/master/dns-controller) (This could also directly become `v1.0`)
|
||||
- [x] Support for OVH
|
||||
|
||||
### v1.0
|
||||
|
||||
|
251
docs/tutorials/ovh.md
Normal file
251
docs/tutorials/ovh.md
Normal file
@ -0,0 +1,251 @@
|
||||
# Setting up ExternalDNS for Services on OVH
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for use within a
|
||||
Kubernetes cluster using OVH DNS.
|
||||
|
||||
Make sure to use **>=0.6** version of ExternalDNS for this tutorial.
|
||||
|
||||
## Creating a zone with OVH DNS
|
||||
|
||||
If you are new to OVH, we recommend you first read the following
|
||||
instructions for creating a zone.
|
||||
|
||||
[Creating a zone using the OVH manager](https://docs.ovh.com/gb/en/domains/create_a_dns_zone_for_a_domain_which_is_not_registered_at_ovh/)
|
||||
|
||||
[Creating a zone using the OVH API](https://api.ovh.com/console/)
|
||||
|
||||
## Creating OVH Credentials
|
||||
|
||||
You first need to create an OVH application.
|
||||
|
||||
Using the [OVH documentation](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/#creation-of-your-application-keys) you will have your `Application key` and `Application secret`
|
||||
|
||||
And you will need a `Consumer key`, you can ask `External DNS` to generate that for you, using the `--ovh-generate-consumer` option.
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --provider=ovh
|
||||
- --ovh-generate-consumer
|
||||
env:
|
||||
- name: OVH_APPLICATION_KEY
|
||||
value: "YOUR_OVH_APPLICATION_KEY"
|
||||
- name: OVH_APPLICATION_SECRET
|
||||
value: "YOUR_OVH_APPLICATION_SECRET"
|
||||
```
|
||||
|
||||
In log, you will have two log lines :
|
||||
```
|
||||
INFO[0000] Generated consumer key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
INFO[0000] Please visit https://eu.api.ovh.com/auth/?credentialToken=YYYYYYYYYYYYYY to validate it
|
||||
```
|
||||
|
||||
Keep the `Consumer key` and go to the link to allow external dns to manage your OVH Dns zone.
|
||||
|
||||
You can also generate the key manually, here the access needed :
|
||||
- GET on `/domain/zone`
|
||||
- GET on `/domain/zone/*/record`
|
||||
- POST on `/domain/zone/*/record`
|
||||
- PUT on `/domain/zone/*/record`
|
||||
- DELETE on `/domain/zone/*/record`
|
||||
- GET on `/domain/zone/*/record/*`
|
||||
- POST on `/domain/zone/*/record/*`
|
||||
- PUT on `/domain/zone/*/record/*`
|
||||
- DELETE on `/domain/zone/*/record/*`
|
||||
- POST on `/domain/zone/*/refresh`
|
||||
|
||||
## Deploy ExternalDNS
|
||||
|
||||
Connect your `kubectl` client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.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=ovh
|
||||
env:
|
||||
- name: OVH_APPLICATION_KEY
|
||||
value: "YOUR_OVH_APPLICATION_KEY"
|
||||
- name: OVH_APPLICATION_SECRET
|
||||
value: "YOUR_OVH_APPLICATION_SECRET"
|
||||
- name: OVH_CONSUMER_KEY
|
||||
value: "YOUR_OVH_CONSUMER_KEY_AFTER_VALIDATED_LINK"
|
||||
```
|
||||
|
||||
### 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"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
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:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
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=ovh
|
||||
env:
|
||||
- name: OVH_APPLICATION_KEY
|
||||
value: "YOUR_OVH_APPLICATION_KEY"
|
||||
- name: OVH_APPLICATION_SECRET
|
||||
value: "YOUR_OVH_APPLICATION_SECRET"
|
||||
- name: OVH_CONSUMER_KEY
|
||||
value: "YOUR_OVH_CONSUMER_KEY_AFTER_VALIDATED_LINK"
|
||||
```
|
||||
|
||||
## 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
|
||||
external-dns.alpha.kubernetes.io/ttl: "120" #optional
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
**A note about annotations**
|
||||
|
||||
Verify that the annotation on the service uses the same hostname as the OVH DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. 'www.example.com').
|
||||
|
||||
The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10.
|
||||
|
||||
ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.
|
||||
|
||||
### Create the deployment and service
|
||||
|
||||
```
|
||||
$ kubectl create -f nginx.yaml
|
||||
```
|
||||
|
||||
Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the OVH DNS records.
|
||||
|
||||
## Verifying OVH DNS records
|
||||
|
||||
Use the OVH manager or API to verify that the A record for your domain shows the external IP address of the services.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial's example:
|
||||
|
||||
```
|
||||
$ kubectl delete -f nginx.yaml
|
||||
$ kubectl delete -f externaldns.yaml
|
||||
```
|
2
go.mod
2
go.mod
@ -27,6 +27,7 @@ require (
|
||||
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
|
||||
github.com/go-resty/resty v1.8.0 // indirect
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b // indirect
|
||||
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f
|
||||
github.com/gophercloud/gophercloud v0.1.0
|
||||
github.com/heptio/contour v0.15.0
|
||||
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65
|
||||
@ -37,6 +38,7 @@ require (
|
||||
github.com/nesv/go-dynect v0.6.0
|
||||
github.com/nic-at/rc0go v1.1.0
|
||||
github.com/oracle/oci-go-sdk v1.8.0
|
||||
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/prometheus/client_golang v0.9.3
|
||||
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0
|
||||
|
3
go.sum
3
go.sum
@ -445,6 +445,8 @@ github.com/operator-framework/operator-sdk v0.7.0/go.mod h1:iVyukRkam5JZa8AnjYf+
|
||||
github.com/oracle/oci-go-sdk v1.8.0 h1:4SO45bKV0I3/Mn1os3ANDZmV0eSE5z5CLdSUIkxtyzs=
|
||||
github.com/oracle/oci-go-sdk v1.8.0/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||
github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
||||
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014 h1:37VE5TYj2m/FLA9SNr4z0+A0JefvTmR60Zwf8XSEV7c=
|
||||
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ=
|
||||
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso=
|
||||
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
@ -740,6 +742,7 @@ gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdOD
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.44.0 h1:YRJzTUp0kSYWUVFF5XAbDFfyiqwsl0Vb9R8TVP5eRi0=
|
||||
gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/logfmt.v0 v0.3.0/go.mod h1:mRLMcMLrml5h2Ux/H+4zccFOlVCiRvOvndsolsJoU8Q=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
|
2
main.go
2
main.go
@ -172,6 +172,8 @@ func main() {
|
||||
p, err = provider.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.DryRun)
|
||||
case "digitalocean":
|
||||
p, err = provider.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun)
|
||||
case "ovh":
|
||||
p, err = provider.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.OVHGenerateConsumerKey, cfg.DryRun)
|
||||
case "linode":
|
||||
p, err = provider.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
|
||||
case "dnsimple":
|
||||
|
@ -98,6 +98,8 @@ type Config struct {
|
||||
DynMinTTLSeconds int
|
||||
OCIConfigFile string
|
||||
InMemoryZones []string
|
||||
OVHEndpoint string
|
||||
OVHGenerateConsumerKey bool
|
||||
PDNSServer string
|
||||
PDNSAPIKey string `secure:"yes"`
|
||||
PDNSTLSEnabled bool
|
||||
@ -193,6 +195,8 @@ var defaultConfig = &Config{
|
||||
InfobloxMaxResults: 0,
|
||||
OCIConfigFile: "/etc/kubernetes/oci.yaml",
|
||||
InMemoryZones: []string{},
|
||||
OVHEndpoint: "ovh-eu",
|
||||
OVHGenerateConsumerKey: false,
|
||||
PDNSServer: "http://localhost:8081",
|
||||
PDNSAPIKey: "",
|
||||
PDNSTLSEnabled: false,
|
||||
@ -310,7 +314,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, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns)").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", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns")
|
||||
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)").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")
|
||||
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)
|
||||
@ -353,6 +357,8 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile)
|
||||
app.Flag("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-generate-consumer", "When using the OVH provider, will generate a consumer and validation URL (default: false)").Default(strconv.FormatBool(defaultConfig.OVHGenerateConsumerKey)).BoolVar(&cfg.OVHGenerateConsumerKey)
|
||||
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)
|
||||
|
355
provider/ovh.go
Normal file
355
provider/ovh.go
Normal file
@ -0,0 +1,355 @@
|
||||
/*
|
||||
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 provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/sync/errgroup"
|
||||
"github.com/ovh/go-ovh/ovh"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
const (
|
||||
ovhDefaultTTL = 0
|
||||
)
|
||||
|
||||
const (
|
||||
ovhCreate = iota
|
||||
ovhDelete
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrRecordToMutateNotFound when ApplyChange has to update/delete and didn't found the record in the existing zone (Change with no record ID)
|
||||
ErrRecordToMutateNotFound = errors.New("record to mutate not found in current zone")
|
||||
// ErrNoDryRun No dry run support for the moment
|
||||
ErrNoDryRun = errors.New("dry run not supported")
|
||||
)
|
||||
|
||||
// OVHProvider is an implementation of Provider for OVH DNS.
|
||||
type OVHProvider struct {
|
||||
client ovhClient
|
||||
|
||||
domainFilter DomainFilter
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
type ovhClient interface {
|
||||
Post(string, interface{}, interface{}) error
|
||||
Get(string, interface{}) error
|
||||
Delete(string, interface{}) error
|
||||
}
|
||||
|
||||
type ovhRecordFields struct {
|
||||
FieldType string `json:"fieldType"`
|
||||
SubDomain string `json:"subDomain"`
|
||||
TTL int64 `json:"ttl"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
type ovhRecord struct {
|
||||
ovhRecordFields
|
||||
ID uint64 `json:"id"`
|
||||
Zone string `json:"zone"`
|
||||
}
|
||||
|
||||
type ovhChange struct {
|
||||
ovhRecord
|
||||
Action int
|
||||
}
|
||||
|
||||
// NewOVHProvider initializes a new OVH DNS based Provider.
|
||||
func NewOVHProvider(ctx context.Context, domainFilter DomainFilter, endpoint string, generateConsumerKey bool, dryRun bool) (*OVHProvider, error) {
|
||||
client, err := ovh.NewEndpointClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if generateConsumerKey {
|
||||
ckReq := client.NewCkRequest()
|
||||
ckReq.AddRules(ovh.ReadOnly, "/domain/zone")
|
||||
ckReq.AddRecursiveRules(ovh.ReadWrite, "/domain/zone/*/record")
|
||||
ckReq.AddRules([]string{"POST"}, "/domain/zone/*/refresh")
|
||||
response, err := ckReq.Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("Generated consumer key: %s\n", response.ConsumerKey)
|
||||
log.Infof("Please visit %s to validate it\n", response.ValidationURL)
|
||||
return nil, fmt.Errorf("You have to validated the consumer key")
|
||||
}
|
||||
// TODO: Add Dry Run support
|
||||
if dryRun {
|
||||
return nil, ErrNoDryRun
|
||||
}
|
||||
return &OVHProvider{
|
||||
client: client,
|
||||
domainFilter: domainFilter,
|
||||
DryRun: dryRun,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Records returns the list of records in all relevant zones.
|
||||
func (p *OVHProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
|
||||
_, records, err := p.zonesRecords(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoints = ovhGroupByNameAndType(records)
|
||||
log.Infof("OVH: %d endpoints have been found\n", len(endpoints))
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes in a given zone.
|
||||
func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
zones, records, err := p.zonesRecords(ctx)
|
||||
zonesChangeUniques := map[string]bool{}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allChanges := make([]ovhChange, 0, countTargets(changes.Create, changes.UpdateNew, changes.UpdateOld, changes.Delete))
|
||||
|
||||
allChanges = append(allChanges, newOvhChange(ovhCreate, changes.Create, zones, records)...)
|
||||
allChanges = append(allChanges, newOvhChange(ovhCreate, changes.UpdateNew, zones, records)...)
|
||||
|
||||
allChanges = append(allChanges, newOvhChange(ovhDelete, changes.UpdateOld, zones, records)...)
|
||||
allChanges = append(allChanges, newOvhChange(ovhDelete, changes.Delete, zones, records)...)
|
||||
|
||||
log.Infof("OVH: %d changes will be done\n", len(allChanges))
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
for _, change := range allChanges {
|
||||
change := change
|
||||
zonesChangeUniques[change.Zone] = true
|
||||
eg.Go(func() error { return p.change(change) })
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("OVH: %d zones will be refreshed\n", len(zonesChangeUniques))
|
||||
|
||||
eg, _ = errgroup.WithContext(ctx)
|
||||
for zone := range zonesChangeUniques {
|
||||
zone := zone
|
||||
eg.Go(func() error { return p.refresh(zone) })
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OVHProvider) refresh(zone string) error {
|
||||
log.Debugf("OVH: Refresh %s zone\n", zone)
|
||||
return p.client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
|
||||
}
|
||||
|
||||
func (p *OVHProvider) change(change ovhChange) error {
|
||||
var err error = nil
|
||||
switch change.Action {
|
||||
case ovhCreate:
|
||||
log.Debugf("OVH: Add an entry to %s\n", change.String())
|
||||
err = p.client.Post(fmt.Sprintf("/domain/zone/%s/record", change.Zone), change.ovhRecordFields, nil)
|
||||
case ovhDelete:
|
||||
if change.ID == 0 {
|
||||
err = ErrRecordToMutateNotFound
|
||||
break
|
||||
}
|
||||
log.Debugf("OVH: Delete an entry to %s\n", change.String())
|
||||
err = p.client.Delete(fmt.Sprintf("/domain/zone/%s/record/%d", change.Zone, change.ID), nil)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *OVHProvider) zonesRecords(ctx context.Context) ([]string, []ovhRecord, error) {
|
||||
var allRecords []ovhRecord
|
||||
zones, err := p.zones()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
chRecords := make(chan []ovhRecord, len(zones))
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, zone := range zones {
|
||||
zone := zone
|
||||
eg.Go(func() error { return p.records(&ctx, &zone, chRecords) })
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
close(chRecords)
|
||||
for records := range chRecords {
|
||||
allRecords = append(allRecords, records...)
|
||||
}
|
||||
return zones, allRecords, nil
|
||||
}
|
||||
|
||||
func (p *OVHProvider) zones() ([]string, error) {
|
||||
zones := []string{}
|
||||
filteredZones := []string{}
|
||||
|
||||
if err := p.client.Get("/domain/zone", &zones); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, zoneName := range zones {
|
||||
if p.domainFilter.Match(zoneName) {
|
||||
filteredZones = append(filteredZones, zoneName)
|
||||
}
|
||||
}
|
||||
log.Infof("OVH: %d zones found\n", len(filteredZones))
|
||||
return filteredZones, nil
|
||||
}
|
||||
|
||||
func (p *OVHProvider) records(ctx *context.Context, zone *string, records chan<- []ovhRecord) error {
|
||||
var recordsIds []uint64
|
||||
ovhRecords := make([]ovhRecord, len(recordsIds))
|
||||
eg, _ := errgroup.WithContext(*ctx)
|
||||
|
||||
log.Debugf("OVH: Getting records for %s\n", *zone)
|
||||
if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record", *zone), &recordsIds); err != nil {
|
||||
return err
|
||||
}
|
||||
chRecords := make(chan ovhRecord, len(recordsIds))
|
||||
for _, id := range recordsIds {
|
||||
id := id
|
||||
eg.Go(func() error { return p.record(zone, id, chRecords) })
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
close(chRecords)
|
||||
for record := range chRecords {
|
||||
ovhRecords = append(ovhRecords, record)
|
||||
}
|
||||
records <- ovhRecords
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OVHProvider) record(zone *string, id uint64, records chan<- ovhRecord) error {
|
||||
record := ovhRecord{}
|
||||
|
||||
log.Debugf("OVH: Getting record %d for %s\n", id, *zone)
|
||||
if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record/%d", *zone, id), &record); err != nil {
|
||||
return err
|
||||
}
|
||||
if supportedRecordType(record.FieldType) {
|
||||
log.Debugf("OVH: Record %d for %s is %+v\n", id, *zone, record)
|
||||
records <- record
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ovhGroupByNameAndType(records []ovhRecord) []*endpoint.Endpoint {
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
|
||||
// group supported records by name and type
|
||||
groups := map[string][]ovhRecord{}
|
||||
|
||||
for _, r := range records {
|
||||
groupBy := r.Zone + r.SubDomain + r.FieldType
|
||||
if _, ok := groups[groupBy]; !ok {
|
||||
groups[groupBy] = []ovhRecord{}
|
||||
}
|
||||
|
||||
groups[groupBy] = append(groups[groupBy], r)
|
||||
}
|
||||
|
||||
// create single endpoint with all the targets for each name/type
|
||||
for _, records := range groups {
|
||||
targets := []string{}
|
||||
for _, record := range records {
|
||||
targets = append(targets, record.Target)
|
||||
}
|
||||
endpoint := endpoint.NewEndpointWithTTL(
|
||||
strings.TrimPrefix(records[0].SubDomain+"."+records[0].Zone, "."),
|
||||
records[0].FieldType,
|
||||
endpoint.TTL(records[0].TTL),
|
||||
targets...,
|
||||
)
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func newOvhChange(action int, endpoints []*endpoint.Endpoint, zones []string, records []ovhRecord) []ovhChange {
|
||||
zoneNameIDMapper := zoneIDName{}
|
||||
ovhChanges := make([]ovhChange, 0, countTargets(endpoints))
|
||||
for _, zone := range zones {
|
||||
zoneNameIDMapper.Add(zone, zone)
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
zone, _ := zoneNameIDMapper.FindZone(endpoint.DNSName)
|
||||
if zone == "" {
|
||||
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", endpoint.DNSName)
|
||||
continue
|
||||
}
|
||||
for _, target := range endpoint.Targets {
|
||||
change := ovhChange{
|
||||
Action: action,
|
||||
ovhRecord: ovhRecord{
|
||||
Zone: zone,
|
||||
ovhRecordFields: ovhRecordFields{
|
||||
FieldType: endpoint.RecordType,
|
||||
SubDomain: strings.TrimSuffix(endpoint.DNSName, "."+zone),
|
||||
TTL: ovhDefaultTTL,
|
||||
Target: target,
|
||||
},
|
||||
},
|
||||
}
|
||||
if endpoint.RecordTTL.IsConfigured() {
|
||||
change.TTL = int64(endpoint.RecordTTL)
|
||||
}
|
||||
for _, record := range records {
|
||||
if record.Zone == change.Zone && record.SubDomain == change.SubDomain && record.FieldType == change.FieldType && record.Target == change.Target {
|
||||
change.ID = record.ID
|
||||
}
|
||||
}
|
||||
ovhChanges = append(ovhChanges, change)
|
||||
}
|
||||
}
|
||||
|
||||
return ovhChanges
|
||||
}
|
||||
|
||||
func countTargets(allEndpoints ...[]*endpoint.Endpoint) int {
|
||||
count := 0
|
||||
for _, endpoints := range allEndpoints {
|
||||
for _, endpoint := range endpoints {
|
||||
count += len(endpoint.Targets)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (c *ovhChange) String() string {
|
||||
if c.ID != 0 {
|
||||
return fmt.Sprintf("%s zone (ID : %d) : %s %d IN %s %s", c.Zone, c.ID, c.SubDomain, c.TTL, c.FieldType, c.Target)
|
||||
}
|
||||
return fmt.Sprintf("%s zone : %s %d IN %s %s", c.Zone, c.SubDomain, c.TTL, c.FieldType, c.Target)
|
||||
}
|
295
provider/ovh_test.go
Normal file
295
provider/ovh_test.go
Normal file
@ -0,0 +1,295 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/ovh/go-ovh/ovh"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
type mockOvhClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (c *mockOvhClient) Post(endpoint string, input interface{}, output interface{}) error {
|
||||
stub := c.Called(endpoint, input)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockOvhClient) Get(endpoint string, output interface{}) error {
|
||||
stub := c.Called(endpoint)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockOvhClient) Delete(endpoint string, output interface{}) error {
|
||||
stub := c.Called(endpoint)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func TestOvhZones(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{
|
||||
client: client,
|
||||
domainFilter: NewDomainFilter([]string{"com"}),
|
||||
}
|
||||
|
||||
// Basic zones
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.com", "example.net"}, nil).Once()
|
||||
domains, err := provider.zones()
|
||||
assert.NoError(err)
|
||||
assert.Contains(domains, "example.com")
|
||||
assert.NotContains(domains, "example.net")
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zones
|
||||
client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
||||
domains, err = provider.zones()
|
||||
assert.Error(err)
|
||||
assert.Nil(domains)
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestOvhZoneRecords(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
|
||||
// Basic zones records
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
||||
zones, records, err := provider.zonesRecords(context.TODO())
|
||||
assert.NoError(err)
|
||||
assert.ElementsMatch(zones, []string{"example.org"})
|
||||
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}})
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zones list
|
||||
client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
||||
zones, records, err = provider.zonesRecords(context.TODO())
|
||||
assert.Error(err)
|
||||
assert.Nil(zones)
|
||||
assert.Nil(records)
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zone records
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.org/record").Return(nil, ovh.ErrAPIDown).Once()
|
||||
zones, records, err = provider.zonesRecords(context.TODO())
|
||||
assert.Error(err)
|
||||
assert.Nil(zones)
|
||||
assert.Nil(records)
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zone record detail
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.org/record").Return([]uint64{42}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.org/record/42").Return(nil, ovh.ErrAPIDown).Once()
|
||||
zones, records, err = provider.zonesRecords(context.TODO())
|
||||
assert.Error(err)
|
||||
assert.Nil(zones)
|
||||
assert.Nil(records)
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestOvhRecords(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
|
||||
// Basic zones records
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.org", "example.net"}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "www", FieldType: "CNAME", TTL: 10, Target: "example.org."}}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.net/record").Return([]uint64{24, 42}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.net/record/24").Return(ovhRecord{ID: 24, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.43"}}, nil).Once()
|
||||
endpoints, err := provider.Records(context.TODO())
|
||||
assert.NoError(err)
|
||||
// Little fix for multi targets endpoint
|
||||
for _, endoint := range endpoints {
|
||||
sort.Strings(endoint.Targets)
|
||||
}
|
||||
assert.ElementsMatch(endpoints, []*endpoint.Endpoint{
|
||||
&endpoint.Endpoint{DNSName: "example.org", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42"}},
|
||||
&endpoint.Endpoint{DNSName: "www.example.org", RecordType: "CNAME", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"example.org"}},
|
||||
&endpoint.Endpoint{DNSName: "ovh.example.net", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42", "203.0.113.43"}},
|
||||
})
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error getting zone
|
||||
client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
||||
endpoints, err = provider.Records(context.TODO())
|
||||
assert.Error(err)
|
||||
assert.Nil(endpoints)
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestOvhRefresh(t *testing.T) {
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
|
||||
// Basic zone refresh
|
||||
client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
||||
provider.refresh("example.net")
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestOvhNewChange(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
endpoints := []*endpoint.Endpoint{
|
||||
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
|
||||
{DNSName: "test.example.org"},
|
||||
}
|
||||
|
||||
// Create change
|
||||
changes := newOvhChange(ovhCreate, endpoints, []string{"example.net"}, []ovhRecord{})
|
||||
assert.ElementsMatch(changes, []ovhChange{
|
||||
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}},
|
||||
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: ovhDefaultTTL, Target: "203.0.113.43"}}},
|
||||
})
|
||||
|
||||
// Delete change
|
||||
endpoints = []*endpoint.Endpoint{
|
||||
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.42"}},
|
||||
}
|
||||
records := []ovhRecord{
|
||||
{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", SubDomain: "ovh", Target: "203.0.113.42"}},
|
||||
}
|
||||
changes = newOvhChange(ovhDelete, endpoints, []string{"example.net"}, records)
|
||||
assert.ElementsMatch(changes, []ovhChange{
|
||||
{Action: ovhDelete, ovhRecord: ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: ovhDefaultTTL, Target: "203.0.113.42"}}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestOvhApplyChanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
changes := plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||
},
|
||||
Delete: []*endpoint.Endpoint{
|
||||
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
|
||||
},
|
||||
}
|
||||
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.net/record").Return([]uint64{42}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.43"}}, nil).Once()
|
||||
client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}).Return(nil, nil).Once()
|
||||
client.On("Delete", "/domain/zone/example.net/record/42").Return(nil, nil).Once()
|
||||
client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
||||
|
||||
// Basic changes
|
||||
assert.NoError(provider.ApplyChanges(context.TODO(), &changes))
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Getting zones failed
|
||||
client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
||||
assert.Error(provider.ApplyChanges(context.TODO(), &changes))
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Apply change failed
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.net/record").Return([]uint64{}, nil).Once()
|
||||
client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}).Return(nil, ovh.ErrAPIDown).Once()
|
||||
assert.Error(provider.ApplyChanges(context.TODO(), &plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||
},
|
||||
}))
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Refresh failed
|
||||
client.On("Get", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||
client.On("Get", "/domain/zone/example.net/record").Return([]uint64{}, nil).Once()
|
||||
client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}).Return(nil, nil).Once()
|
||||
client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, ovh.ErrAPIDown).Once()
|
||||
assert.Error(provider.ApplyChanges(context.TODO(), &plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||
},
|
||||
}))
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestOvhChange(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := new(mockOvhClient)
|
||||
provider := &OVHProvider{client: client}
|
||||
|
||||
// Record creation
|
||||
client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "ovh"}).Return(nil, nil).Once()
|
||||
assert.NoError(provider.change(ovhChange{
|
||||
Action: ovhCreate,
|
||||
ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh"}},
|
||||
}))
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Record deletion
|
||||
client.On("Delete", "/domain/zone/example.net/record/42").Return(nil, nil).Once()
|
||||
assert.NoError(provider.change(ovhChange{
|
||||
Action: ovhDelete,
|
||||
ovhRecord: ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh"}},
|
||||
}))
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Record deletion error
|
||||
assert.Error(provider.change(ovhChange{
|
||||
Action: ovhDelete,
|
||||
ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh"}},
|
||||
}))
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestOvhCountTargets(t *testing.T) {
|
||||
cases := []struct {
|
||||
endpoints [][]*endpoint.Endpoint
|
||||
count int
|
||||
}{
|
||||
{[][]*endpoint.Endpoint{[]*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 1},
|
||||
{[][]*endpoint.Endpoint{[]*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}, {DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 2},
|
||||
{[][]*endpoint.Endpoint{[]*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target", "target"}}}}, 3},
|
||||
{[][]*endpoint.Endpoint{[]*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}, []*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}}, 4},
|
||||
}
|
||||
for _, test := range cases {
|
||||
count := countTargets(test.endpoints...)
|
||||
if count != test.count {
|
||||
t.Errorf("Wrong targets counts (Should be %d, get %d)", test.count, count)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user