mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
Add Gandi provider
This commit is contained in:
parent
497aba2cfb
commit
b5f7570c35
@ -51,6 +51,7 @@ ExternalDNS' current release is `v0.7`. This version allows you to keep selected
|
||||
* [Scaleway](https://www.scaleway.com)
|
||||
* [Akamai Edge DNS](https://learn.akamai.com/en-us/products/cloud_security/edge_dns.html)
|
||||
* [GoDaddy](https://www.godaddy.com)
|
||||
* [Gandi](https://www.gandi.net)
|
||||
|
||||
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.
|
||||
|
||||
@ -107,6 +108,7 @@ The following table clarifies the current status of the providers according to t
|
||||
| Vultr | Alpha | |
|
||||
| UltraDNS | Alpha | |
|
||||
| GoDaddy | Alpha | |
|
||||
| Gandi | Alpha | @packi |
|
||||
|
||||
## Running ExternalDNS:
|
||||
|
||||
@ -160,6 +162,7 @@ The following tutorials are provided:
|
||||
* [Vultr](docs/tutorials/vultr.md)
|
||||
* [UltraDNS](docs/tutorials/ultradns.md)
|
||||
* [GoDaddy](docs/tutorials/godaddy.md)
|
||||
* [Gandi](docs/tutorials/gandi.md)
|
||||
|
||||
### Running Locally
|
||||
|
||||
|
191
docs/tutorials/gandi.md
Normal file
191
docs/tutorials/gandi.md
Normal file
@ -0,0 +1,191 @@
|
||||
# Setting up ExternalDNS for Services on Gandi
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Gandi.
|
||||
|
||||
Make sure to use **>=0.7.7** version of ExternalDNS for this tutorial.
|
||||
|
||||
## Creating a Gandi DNS zone (domain)
|
||||
|
||||
Create a new DNS zone where you want to create your records in. Let's use `example.com` as an example here. Make sure the zone uses
|
||||
|
||||
## Creating Gandi API Key
|
||||
|
||||
Generate an API key on [your account](https://account.gandi.net) (click on "Security").
|
||||
|
||||
The environment variable `GANDI_KEY` will be needed to run ExternalDNS with Gandi.
|
||||
|
||||
## 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: k8s.gcr.io/external-dns/external-dns:v0.7.7
|
||||
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=gandi
|
||||
env:
|
||||
- name: GANDI_KEY
|
||||
value: "YOUR_GANDI_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","networking.k8s.io"]
|
||||
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: k8s.gcr.io/external-dns/external-dns:v0.7.7
|
||||
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=gandi
|
||||
env:
|
||||
- name: GANDI_KEY
|
||||
value: "YOUR_GANDI_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 Gandi Domain. Make sure that your Domain is configured to use Live-DNS.
|
||||
|
||||
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 Gandi DNS records.
|
||||
|
||||
## Verifying Gandi DNS records
|
||||
|
||||
Check your [Gandi Dashboard](https://admin.gandi.net/domain) to view the records for your Gandi 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 Gandi DNS records, we can delete the tutorial's example:
|
||||
|
||||
```
|
||||
$ kubectl delete service -f nginx.yaml
|
||||
$ kubectl delete service -f externaldns.yaml
|
||||
```
|
||||
|
||||
# Additional options
|
||||
|
||||
If you're using organizations to separate your domains, you can pass the organization's ID in an environment variable called `GANDI_SHARING_ID` to get access to it.
|
1
go.mod
1
go.mod
@ -26,6 +26,7 @@ require (
|
||||
github.com/exoscale/egoscale v0.18.1
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
|
||||
github.com/go-gandi/go-gandi v0.0.0-20200921091836-0d8a64b9cc09
|
||||
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f
|
||||
github.com/google/go-cmp v0.4.1
|
||||
github.com/gophercloud/gophercloud v0.1.0
|
||||
|
3
go.sum
3
go.sum
@ -84,6 +84,7 @@ github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrD
|
||||
github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kingpin v2.2.5+incompatible h1:umWl1NNd72+ZvRti3T9C0SYean2hPZ7ZhxU8bsgc9BQ=
|
||||
github.com/alecthomas/kingpin v2.2.5+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/kong v0.2.2/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
||||
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c h1:MVVbswUlqicyj8P/JljoocA7AyCo62gzD0O7jfvrhtE=
|
||||
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@ -266,6 +267,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-gandi/go-gandi v0.0.0-20200921091836-0d8a64b9cc09 h1:w+iZczt5J4LJa13RX5uguKI866vIEMOESgXr4XcwrwA=
|
||||
github.com/go-gandi/go-gandi v0.0.0-20200921091836-0d8a64b9cc09/go.mod h1:Vv36tv/GTi8FNAFIQ88+9GPHm4CAihAuJu7rfqRJ9aY=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
|
3
main.go
3
main.go
@ -47,6 +47,7 @@ import (
|
||||
"sigs.k8s.io/external-dns/provider/dnsimple"
|
||||
"sigs.k8s.io/external-dns/provider/dyn"
|
||||
"sigs.k8s.io/external-dns/provider/exoscale"
|
||||
"sigs.k8s.io/external-dns/provider/gandi"
|
||||
"sigs.k8s.io/external-dns/provider/godaddy"
|
||||
"sigs.k8s.io/external-dns/provider/google"
|
||||
"sigs.k8s.io/external-dns/provider/hetzner"
|
||||
@ -305,6 +306,8 @@ func main() {
|
||||
p, err = scaleway.NewScalewayProvider(ctx, domainFilter, cfg.DryRun)
|
||||
case "godaddy":
|
||||
p, err = godaddy.NewGoDaddyProvider(ctx, domainFilter, cfg.GoDaddyTTL, cfg.GoDaddyAPIKey, cfg.GoDaddySecretKey, cfg.GoDaddyOTE, cfg.DryRun)
|
||||
case "gandi":
|
||||
p, err = gandi.NewGandiProvider(ctx, domainFilter, cfg.DryRun)
|
||||
default:
|
||||
log.Fatalf("unknown dns provider: %s", cfg.Provider)
|
||||
}
|
||||
|
@ -362,7 +362,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("managed-record-types", "Comma separated list of record types to manage (default: A, CNAME) (supported records: CNAME, A, NS").Default("A", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes)
|
||||
|
||||
// Flags related to providers
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, 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", "scaleway", "vultr", "ultradns", "godaddy", "bluecat")
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi)").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", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi")
|
||||
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-name-filter", "Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneNameFilter)
|
||||
|
120
provider/gandi/client.go
Normal file
120
provider/gandi/client.go
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package gandi
|
||||
|
||||
import (
|
||||
"github.com/go-gandi/go-gandi/domain"
|
||||
"github.com/go-gandi/go-gandi/livedns"
|
||||
)
|
||||
|
||||
type DomainClientAdapter interface {
|
||||
ListDomains() (domains []domain.ListResponse, err error)
|
||||
}
|
||||
|
||||
type DomainClient struct {
|
||||
Client *domain.Domain
|
||||
}
|
||||
|
||||
func (p *DomainClient) ListDomains() (domains []domain.ListResponse, err error) {
|
||||
return p.Client.ListDomains()
|
||||
}
|
||||
|
||||
func NewDomainClient(client *domain.Domain) DomainClientAdapter {
|
||||
return &DomainClient{client}
|
||||
}
|
||||
|
||||
// StandardResponse copied from go-gandi/internal/gandi.go
|
||||
type StandardResponse struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
Object string `json:"object,omitempty"`
|
||||
Cause string `json:"cause,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Errors []StandardError `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// StandardError copied from go-gandi/internal/gandi.go
|
||||
type StandardError struct {
|
||||
Location string `json:"location"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type LiveDNSClientAdapter interface {
|
||||
GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error)
|
||||
CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response StandardResponse, err error)
|
||||
DeleteDomainRecord(fqdn, name, recordtype string) (err error)
|
||||
UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response StandardResponse, err error)
|
||||
}
|
||||
|
||||
type LiveDNSClient struct {
|
||||
Client *livedns.LiveDNS
|
||||
}
|
||||
|
||||
func NewLiveDNSClient(client *livedns.LiveDNS) LiveDNSClientAdapter {
|
||||
return &LiveDNSClient{client}
|
||||
}
|
||||
|
||||
func (p *LiveDNSClient) GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error) {
|
||||
return p.Client.GetDomainRecords(fqdn)
|
||||
}
|
||||
|
||||
func (p *LiveDNSClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response StandardResponse, err error) {
|
||||
res, err := p.Client.CreateDomainRecord(fqdn, name, recordtype, ttl, values)
|
||||
if err != nil {
|
||||
return StandardResponse{}, err
|
||||
}
|
||||
|
||||
// response needs to be copied as the Standard* structs are internal
|
||||
var errors []StandardError
|
||||
for _, e := range res.Errors {
|
||||
errors = append(errors, StandardError(e))
|
||||
}
|
||||
return StandardResponse{
|
||||
Code: res.Code,
|
||||
Message: res.Message,
|
||||
UUID: res.UUID,
|
||||
Object: res.Object,
|
||||
Cause: res.Cause,
|
||||
Status: res.Status,
|
||||
Errors: errors,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (p *LiveDNSClient) DeleteDomainRecord(fqdn, name, recordtype string) (err error) {
|
||||
return p.Client.DeleteDomainRecord(fqdn, name, recordtype)
|
||||
}
|
||||
|
||||
func (p *LiveDNSClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response StandardResponse, err error) {
|
||||
res, err := p.Client.UpdateDomainRecordByNameAndType(fqdn, name, recordtype, ttl, values)
|
||||
if err != nil {
|
||||
return StandardResponse{}, err
|
||||
}
|
||||
|
||||
// response needs to be copied as the Standard* structs are internal
|
||||
var errors []StandardError
|
||||
for _, e := range res.Errors {
|
||||
errors = append(errors, StandardError(e))
|
||||
}
|
||||
return StandardResponse{
|
||||
Code: res.Code,
|
||||
Message: res.Message,
|
||||
UUID: res.UUID,
|
||||
Object: res.Object,
|
||||
Cause: res.Cause,
|
||||
Status: res.Status,
|
||||
Errors: errors,
|
||||
}, err
|
||||
}
|
268
provider/gandi/gandi.go
Normal file
268
provider/gandi/gandi.go
Normal file
@ -0,0 +1,268 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package gandi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gandi/go-gandi"
|
||||
"github.com/go-gandi/go-gandi/livedns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
gandiCreate = "CREATE"
|
||||
gandiDelete = "DELETE"
|
||||
gandiUpdate = "UPDATE"
|
||||
gandiTTL = 600
|
||||
gandiLiveDNSProvider = "livedns"
|
||||
)
|
||||
|
||||
type GandiChanges struct {
|
||||
Action string
|
||||
ZoneName string
|
||||
Record livedns.DomainRecord
|
||||
}
|
||||
|
||||
type GandiProvider struct {
|
||||
provider.BaseProvider
|
||||
LiveDNSClient LiveDNSClientAdapter
|
||||
DomainClient DomainClientAdapter
|
||||
domainFilter endpoint.DomainFilter
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
func NewGandiProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*GandiProvider, error) {
|
||||
key, ok := os.LookupEnv("GANDI_KEY")
|
||||
if !ok {
|
||||
return nil, errors.New("no environment variable GANDI_KEY provided")
|
||||
}
|
||||
sharingID, _ := os.LookupEnv("GANDI_SHARING_ID")
|
||||
|
||||
g := gandi.Config{
|
||||
SharingID: sharingID,
|
||||
Debug: false,
|
||||
// dry-run doesn't work but it won't hurt passing the flag
|
||||
DryRun: dryRun,
|
||||
}
|
||||
|
||||
liveDNSClient := gandi.NewLiveDNSClient(key, g)
|
||||
domainClient := gandi.NewDomainClient(key, g)
|
||||
|
||||
gandiProvider := &GandiProvider{
|
||||
LiveDNSClient: NewLiveDNSClient(liveDNSClient),
|
||||
DomainClient: NewDomainClient(domainClient),
|
||||
domainFilter: domainFilter,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
return gandiProvider, nil
|
||||
}
|
||||
|
||||
func (p *GandiProvider) Zones() (zones []string, err error) {
|
||||
availableDomains, err := p.DomainClient.ListDomains()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zones = []string{}
|
||||
for _, domain := range availableDomains {
|
||||
if !p.domainFilter.Match(domain.FQDN) {
|
||||
log.Debugf("Excluding domain %s by domain-filter", domain.FQDN)
|
||||
continue
|
||||
}
|
||||
|
||||
if domain.NameServer.Current != gandiLiveDNSProvider {
|
||||
log.Debugf("Excluding domain %s, not configured for livedns", domain.FQDN)
|
||||
continue
|
||||
}
|
||||
|
||||
zones = append(zones, domain.FQDN)
|
||||
}
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (p *GandiProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
liveDNSZones, err := p.Zones()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
for _, zone := range liveDNSZones {
|
||||
records, err := p.LiveDNSClient.GetDomainRecords(zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
if provider.SupportedRecordType(r.RrsetType) {
|
||||
name := r.RrsetName + "." + zone
|
||||
|
||||
if r.RrsetName == "@" {
|
||||
name = zone
|
||||
}
|
||||
|
||||
if len(r.RrsetValues) > 1 {
|
||||
return nil, fmt.Errorf("can't handle multiple values for rrset %s", name)
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(name, r.RrsetType, r.RrsetValues[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (p *GandiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
combinedChanges := make([]*GandiChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
|
||||
|
||||
combinedChanges = append(combinedChanges, p.newGandiChanges(gandiCreate, changes.Create)...)
|
||||
combinedChanges = append(combinedChanges, p.newGandiChanges(gandiUpdate, changes.UpdateNew)...)
|
||||
combinedChanges = append(combinedChanges, p.newGandiChanges(gandiDelete, changes.Delete)...)
|
||||
|
||||
return p.submitChanges(ctx, combinedChanges)
|
||||
}
|
||||
|
||||
func (p *GandiProvider) submitChanges(ctx context.Context, changes []*GandiChanges) error {
|
||||
if len(changes) == 0 {
|
||||
log.Infof("All records are already up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
liveDNSDomains, err := p.Zones()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zoneChanges := p.groupAndFilterByZone(liveDNSDomains, changes)
|
||||
|
||||
for _, changes := range zoneChanges {
|
||||
for _, change := range changes {
|
||||
// Prepare record name
|
||||
recordName := strings.TrimSuffix(change.Record.RrsetName, "."+change.ZoneName)
|
||||
if recordName == change.ZoneName {
|
||||
recordName = "@"
|
||||
}
|
||||
if change.Record.RrsetType == endpoint.RecordTypeCNAME && !strings.HasSuffix(change.Record.RrsetValues[0], ".") {
|
||||
change.Record.RrsetValues[0] += "."
|
||||
}
|
||||
change.Record.RrsetName = recordName
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"record": change.Record.RrsetName,
|
||||
"type": change.Record.RrsetType,
|
||||
"value": change.Record.RrsetValues[0],
|
||||
"ttl": change.Record.RrsetTTL,
|
||||
"action": change.Action,
|
||||
"zone": change.ZoneName,
|
||||
}).Info("Changing record")
|
||||
|
||||
if !p.DryRun {
|
||||
switch change.Action {
|
||||
case gandiCreate:
|
||||
answer, err := p.LiveDNSClient.CreateDomainRecord(
|
||||
change.ZoneName,
|
||||
change.Record.RrsetName,
|
||||
change.Record.RrsetType,
|
||||
change.Record.RrsetTTL,
|
||||
change.Record.RrsetValues,
|
||||
)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"Code": answer.Code,
|
||||
"Message": answer.Message,
|
||||
"Cause": answer.Cause,
|
||||
"Errors": answer.Errors,
|
||||
}).Warning("Create problem")
|
||||
return err
|
||||
}
|
||||
case gandiDelete:
|
||||
err := p.LiveDNSClient.DeleteDomainRecord(change.ZoneName, change.Record.RrsetName, change.Record.RrsetType)
|
||||
if err != nil {
|
||||
log.Warning("Delete problem")
|
||||
return err
|
||||
}
|
||||
case gandiUpdate:
|
||||
answer, err := p.LiveDNSClient.UpdateDomainRecordByNameAndType(
|
||||
change.ZoneName,
|
||||
change.Record.RrsetName,
|
||||
change.Record.RrsetType,
|
||||
change.Record.RrsetTTL,
|
||||
change.Record.RrsetValues,
|
||||
)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"Code": answer.Code,
|
||||
"Message": answer.Message,
|
||||
"Cause": answer.Cause,
|
||||
"Errors": answer.Errors,
|
||||
}).Warning("Update problem")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GandiProvider) newGandiChanges(action string, endpoints []*endpoint.Endpoint) []*GandiChanges {
|
||||
changes := make([]*GandiChanges, 0, len(endpoints))
|
||||
ttl := gandiTTL
|
||||
for _, e := range endpoints {
|
||||
if e.RecordTTL.IsConfigured() {
|
||||
ttl = int(e.RecordTTL)
|
||||
}
|
||||
change := &GandiChanges{
|
||||
Action: action,
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: e.RecordType,
|
||||
RrsetName: e.DNSName,
|
||||
RrsetValues: e.Targets,
|
||||
RrsetTTL: ttl,
|
||||
},
|
||||
}
|
||||
changes = append(changes, change)
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func (p *GandiProvider) groupAndFilterByZone(zones []string, changes []*GandiChanges) map[string][]*GandiChanges {
|
||||
change := make(map[string][]*GandiChanges)
|
||||
zoneNameID := provider.ZoneIDName{}
|
||||
|
||||
for _, z := range zones {
|
||||
zoneNameID.Add(z, z)
|
||||
change[z] = []*GandiChanges{}
|
||||
}
|
||||
|
||||
for _, c := range changes {
|
||||
zoneID, zoneName := zoneNameID.FindZone(c.Record.RrsetName)
|
||||
if zoneName == "" {
|
||||
log.Debugf("Skipping record %s because no hosted domain matching record DNS Name was detected", c.Record.RrsetName)
|
||||
continue
|
||||
}
|
||||
c.ZoneName = zoneName
|
||||
change[zoneID] = append(change[zoneID], c)
|
||||
}
|
||||
return change
|
||||
}
|
755
provider/gandi/gandi_test.go
Normal file
755
provider/gandi/gandi_test.go
Normal file
@ -0,0 +1,755 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package gandi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/go-gandi/go-gandi/domain"
|
||||
"github.com/go-gandi/go-gandi/livedns"
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
"strings"
|
||||
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
type MockAction struct {
|
||||
Name string
|
||||
FQDN string
|
||||
Record livedns.DomainRecord
|
||||
}
|
||||
|
||||
type mockGandiClient struct {
|
||||
Actions []MockAction
|
||||
FunctionToFail string
|
||||
RecordsToReturn []livedns.DomainRecord
|
||||
}
|
||||
|
||||
func mockGandiClientNew() *mockGandiClient {
|
||||
return &mockGandiClient{
|
||||
RecordsToReturn: testRecords(),
|
||||
}
|
||||
}
|
||||
|
||||
func mockGandiClientNewWithRecords(recordsToReturn []livedns.DomainRecord) *mockGandiClient {
|
||||
return &mockGandiClient{
|
||||
RecordsToReturn: recordsToReturn,
|
||||
}
|
||||
}
|
||||
|
||||
func mockGandiClientNewWithFailure(functionToFail string) *mockGandiClient {
|
||||
return &mockGandiClient{
|
||||
FunctionToFail: functionToFail,
|
||||
RecordsToReturn: testRecords(),
|
||||
}
|
||||
}
|
||||
|
||||
func testRecords() []livedns.DomainRecord {
|
||||
return []livedns.DomainRecord{
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "@",
|
||||
RrsetHref: "https://api.gandi.net/v5/domain/domains/example.com/records/%40/A",
|
||||
RrsetValues: []string{"192.168.0.1"},
|
||||
},
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "www",
|
||||
RrsetHref: "https://api.gandi.net/v5/domain/domains/example.com/records/www/CNAME",
|
||||
RrsetValues: []string{"lb.example.com"},
|
||||
},
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "test",
|
||||
RrsetHref: "https://api.gandi.net/v5/domain/domains/example.com/records/test/A",
|
||||
RrsetValues: []string{"192.168.0.2"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Mock all methods
|
||||
|
||||
func (m *mockGandiClient) GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "GetDomainRecords",
|
||||
FQDN: fqdn,
|
||||
})
|
||||
if m.FunctionToFail == "GetDomainRecords" {
|
||||
return nil, fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return m.RecordsToReturn, err
|
||||
}
|
||||
|
||||
func (m *mockGandiClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response StandardResponse, err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: fqdn,
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: recordtype,
|
||||
RrsetTTL: ttl,
|
||||
RrsetName: name,
|
||||
RrsetValues: values,
|
||||
},
|
||||
})
|
||||
if m.FunctionToFail == "CreateDomainRecord" {
|
||||
return StandardResponse{}, fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return StandardResponse{}, nil
|
||||
}
|
||||
|
||||
func (m *mockGandiClient) DeleteDomainRecord(fqdn, name, recordtype string) (err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "DeleteDomainRecord",
|
||||
FQDN: fqdn,
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: recordtype,
|
||||
RrsetName: name,
|
||||
},
|
||||
})
|
||||
|
||||
if m.FunctionToFail == "DeleteDomainRecord" {
|
||||
return fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockGandiClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response StandardResponse, err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: fqdn,
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: recordtype,
|
||||
RrsetTTL: ttl,
|
||||
RrsetName: name,
|
||||
RrsetValues: values,
|
||||
},
|
||||
})
|
||||
|
||||
if m.FunctionToFail == "UpdateDomainRecordByNameAndType" {
|
||||
return StandardResponse{}, fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return StandardResponse{}, nil
|
||||
}
|
||||
|
||||
func (m *mockGandiClient) ListDomains() (domains []domain.ListResponse, err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "ListDomains",
|
||||
})
|
||||
|
||||
if m.FunctionToFail == "ListDomains" {
|
||||
return []domain.ListResponse{}, fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return []domain.ListResponse{
|
||||
{
|
||||
FQDN: "example.com",
|
||||
FQDNUnicode: "example.com",
|
||||
Href: "https://api.gandi.net/v5/domain/domains/example.com",
|
||||
ID: "b3e9c271-1c29-4441-97d9-bc021a7ac7c3",
|
||||
NameServer: &domain.NameServerConfig{
|
||||
Current: gandiLiveDNSProvider,
|
||||
},
|
||||
TLD: "com",
|
||||
},
|
||||
{
|
||||
FQDN: "example.net",
|
||||
FQDNUnicode: "example.net",
|
||||
Href: "https://api.gandi.net/v5/domain/domains/example.net",
|
||||
ID: "dc78c1d8-6143-4edb-93bc-3a20d8bc3570",
|
||||
NameServer: &domain.NameServerConfig{
|
||||
Current: "other",
|
||||
},
|
||||
TLD: "net",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
func TestNewGandiProvider(t *testing.T) {
|
||||
_ = os.Setenv("GANDI_KEY", "myGandiKey")
|
||||
provider, err := NewGandiProvider(context.Background(), endpoint.NewDomainFilter([]string{"example.com"}), true)
|
||||
if err != nil {
|
||||
t.Errorf("failed : %s", err)
|
||||
}
|
||||
assert.Equal(t, true, provider.DryRun)
|
||||
|
||||
_ = os.Setenv("GANDI_SHARING_ID", "aSharingId")
|
||||
provider, err = NewGandiProvider(context.Background(), endpoint.NewDomainFilter([]string{"example.com"}), false)
|
||||
if err != nil {
|
||||
t.Errorf("failed : %s", err)
|
||||
}
|
||||
assert.Equal(t, false, provider.DryRun)
|
||||
|
||||
_ = os.Unsetenv("GANDI_KEY")
|
||||
_, err = NewGandiProvider(context.Background(), endpoint.NewDomainFilter([]string{"example.com"}), true)
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGandiProvider_TestData(t *testing.T) {
|
||||
mockedClient := mockGandiClientNew()
|
||||
|
||||
// Check test zone data is ok
|
||||
expectedZonesAnswer := []domain.ListResponse{
|
||||
{
|
||||
FQDN: "example.com",
|
||||
FQDNUnicode: "example.com",
|
||||
Href: "https://api.gandi.net/v5/domain/domains/example.com",
|
||||
ID: "b3e9c271-1c29-4441-97d9-bc021a7ac7c3",
|
||||
NameServer: &domain.NameServerConfig{
|
||||
Current: gandiLiveDNSProvider,
|
||||
},
|
||||
TLD: "com",
|
||||
},
|
||||
{
|
||||
FQDN: "example.net",
|
||||
FQDNUnicode: "example.net",
|
||||
Href: "https://api.gandi.net/v5/domain/domains/example.net",
|
||||
ID: "dc78c1d8-6143-4edb-93bc-3a20d8bc3570",
|
||||
NameServer: &domain.NameServerConfig{
|
||||
Current: "other",
|
||||
},
|
||||
TLD: "net",
|
||||
},
|
||||
}
|
||||
|
||||
testingZonesAnswer, err := mockedClient.ListDomains()
|
||||
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 := []livedns.DomainRecord{
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "@",
|
||||
RrsetHref: "https://api.gandi.net/v5/domain/domains/example.com/records/%40/A",
|
||||
RrsetValues: []string{"192.168.0.1"},
|
||||
},
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "www",
|
||||
RrsetHref: "https://api.gandi.net/v5/domain/domains/example.com/records/www/CNAME",
|
||||
RrsetValues: []string{"lb.example.com"},
|
||||
},
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "test",
|
||||
RrsetHref: "https://api.gandi.net/v5/domain/domains/example.com/records/test/A",
|
||||
RrsetValues: []string{"192.168.0.2"},
|
||||
},
|
||||
}
|
||||
|
||||
testingRecordsAnswer, err := mockedClient.GetDomainRecords("example.com")
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedRecordsAnswer, testingRecordsAnswer) {
|
||||
t.Errorf("should be equal, %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGandiProvider_Records(t *testing.T) {
|
||||
mockedClient := mockGandiClientNew()
|
||||
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
expectedActions := []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "GetDomainRecords",
|
||||
FQDN: "example.com",
|
||||
},
|
||||
}
|
||||
|
||||
endpoints, err := mockedProvider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
assert.Equal(t, 3, len(endpoints))
|
||||
fmt.Printf("%+v\n", endpoints[0].DNSName)
|
||||
assert.Equal(t, "example.com", endpoints[0].DNSName)
|
||||
assert.Equal(t, endpoint.RecordTypeCNAME, endpoints[0].RecordType)
|
||||
td.Cmp(t, expectedActions, mockedClient.Actions)
|
||||
}
|
||||
|
||||
func TestGandiProvider_RecordsAppliesDomainFilter(t *testing.T) {
|
||||
|
||||
mockedClient := mockGandiClientNew()
|
||||
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
domainFilter: endpoint.NewDomainFilterWithExclusions([]string{}, []string{"example.com"}),
|
||||
}
|
||||
|
||||
expectedActions := []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
}
|
||||
|
||||
endpoints, err := mockedProvider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
assert.Equal(t, 0, len(endpoints))
|
||||
td.Cmp(t, expectedActions, mockedClient.Actions)
|
||||
}
|
||||
|
||||
func TestGandiProvider_RecordsErrorOnMultipleValues(t *testing.T) {
|
||||
|
||||
mockedClient := mockGandiClientNewWithRecords([]livedns.DomainRecord{
|
||||
{
|
||||
RrsetValues: []string{"foo", "bar"},
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
})
|
||||
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
expectedActions := []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "GetDomainRecords",
|
||||
FQDN: "example.com",
|
||||
},
|
||||
}
|
||||
|
||||
endpoints, err := mockedProvider.Records(context.Background())
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail")
|
||||
}
|
||||
assert.Equal(t, 0, len(endpoints))
|
||||
assert.True(t, strings.HasPrefix(err.Error(), "can't handle multiple values for rrset"))
|
||||
td.Cmp(t, expectedActions, mockedClient.Actions)
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesEmpty(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
if mockedClient.Actions != nil {
|
||||
t.Error("expected no changes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChanges(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{{DNSName: "test2.example.com", Targets: endpoint.Targets{"target"}, RecordType: "A", RecordTTL: 666}}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test3.example.com", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 777}}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "test4.example.com", Targets: endpoint.Targets{"target-other"}, RecordType: "A"}}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "test2",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "test3",
|
||||
RrsetValues: []string{"target-new"},
|
||||
RrsetTTL: 777,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "DeleteDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "test4",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesSkipsNonManaged(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{{DNSName: "example.net", Targets: endpoint.Targets{"target"}}}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.example.net", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 777}}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "test2.example.net", Targets: endpoint.Targets{"target"}, RecordType: "A"}}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesCreateUpdateCname(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "test-cname.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "CNAME"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test-cname2.example.com", Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 777}}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetName: "test-cname",
|
||||
RrsetValues: []string{"target."},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetName: "test-cname2",
|
||||
RrsetValues: []string{"target-new."},
|
||||
RrsetTTL: 777,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesCreateEmpty(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "@",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesRespectsDryRun(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesErrorListDomains(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNewWithFailure("ListDomains")
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err == nil {
|
||||
t.Error("should have failed")
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesErrorCreate(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNewWithFailure("CreateDomainRecord")
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err == nil {
|
||||
t.Error("should have failed")
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "foo",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesErrorUpdate(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNewWithFailure("UpdateDomainRecordByNameAndType")
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err == nil {
|
||||
t.Error("should have failed")
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "foo",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "bar",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesErrorDelete(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNewWithFailure("DeleteDomainRecord")
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err == nil {
|
||||
t.Error("should have failed")
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "foo",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "bar",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "DeleteDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "baz",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user