Merge pull request #2359 from assureddt/rjh_safedns_provider

Create SafeDNS provider
This commit is contained in:
Kubernetes Prow Robot 2022-01-05 12:31:49 -08:00 committed by GitHub
commit c8ef36567c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 841 additions and 1 deletions

View File

@ -52,6 +52,7 @@ ExternalDNS' allows you to keep selected zones (via `--domain-filter`) synchroni
* [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)
* [UKFast SafeDNS](https://my.ukfast.co.uk/safedns/)
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.
@ -109,6 +110,7 @@ The following table clarifies the current status of the providers according to t
| UltraDNS | Alpha | |
| GoDaddy | Alpha | |
| Gandi | Alpha | @packi |
| SafeDNS | Alpha | @assureddt |
## Kubernetes version compatibility
@ -171,6 +173,7 @@ The following tutorials are provided:
* [UltraDNS](docs/tutorials/ultradns.md)
* [GoDaddy](docs/tutorials/godaddy.md)
* [Gandi](docs/tutorials/gandi.md)
* [SafeDNS](docs/tutorials/safedns.md)
### Running Locally

View File

@ -0,0 +1,210 @@
# Setting up ExternalDNS for Services on UKFast's SafeDNS
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using SafeDNS.
Make sure to use **>=0.11.0** version of ExternalDNS for this tutorial.
## Managing DNS with SafeDNS
If you want to learn about how to use the SafeDNS service read the following tutorials:
To learn more about the use of SafeDNS in general, see the following page:
[UKFast's SafeDNS documentation](https://docs.ukfast.co.uk/domains/safedns/index.html).
## Creating SafeDNS credentials
Generate a fresh API token for use with ExternalDNS, following the instructions
at the UKFast developer [Getting-Started](https://developers.ukfast.io/getting-started)
page. You will need to grant read/write access to the SafeDNS API. No access to
any other UKFast service is required.
The environment variable `SAFEDNS_TOKEN` must have a value of this token to run
ExternalDNS with SafeDNS integration.
## Deploy ExternalDNS
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply one of the following manifests file to deploy ExternalDNS.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
# You will need to check what the latest version is yourself:
# https://github.com/kubernetes-sigs/external-dns/releases
image: k8s.gcr.io/external-dns/external-dns:vX.Y.Z
args:
- --source=service # ingress is also possible
# (optional) limit to only example.com domains; change to match the
# zone created above.
- --domain-filter=example.com
- --provider=safedns
env:
- name: SAFEDNS_TOKEN
value: "SAFEDNSTOKENSAFEDNSTOKEN"
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.11.0
args:
- --source=service # ingress is also possible
# (optional) limit to only example.com domains; change to match the
# zone created above.
- --domain-filter=example.com
- --provider=safedns
env:
- name: SAFEDNS_TOKEN
value: "SAFEDNSTOKENSAFEDNSTOKEN"
```
## 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: my-app.example.com
spec:
selector:
app: nginx
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
```
Note the annotation on the service; use a hostname that matches the domain
filter specified above.
ExternalDNS uses this annotation to determine what services should be registered
with DNS. Removing the annotation will cause ExternalDNS to remove the
corresponding DNS records.
Create the deployment and service:
```console
$ kubectl create -f nginx.yaml
```
Depending where you run your service it can take a little while for your cloud
provider to create an external IP for the service.
Once the service has an external IP assigned, ExternalDNS will notice the new
service IP address and synchronize the SafeDNS records.
## Verifying SafeDNS records
Check your [SafeDNS UI](https://my.ukfast.co.uk/safedns/index.php) and select
the appropriate domain to view the records for your SafeDNS zone.
This should show the external IP address of the service as the A record for your
domain.
Alternatively, you can perform a DNS lookup for the hostname specified:
```console
$ dig +short my-app.example.com
an.ip.addr.ess
```
## Cleanup
Now that we have verified that ExternalDNS will automatically manage SafeDNS
records, we can delete the tutorial's example:
```
$ kubectl delete service -f nginx.yaml
$ kubectl delete service -f externaldns.yaml
```

1
go.mod
View File

@ -56,6 +56,7 @@ require (
github.com/stretchr/testify v1.7.0
github.com/terra-farm/udnssdk v1.3.5 // indirect
github.com/transip/gotransip/v6 v6.6.2
github.com/ukfast/sdk-go v1.4.23 // indirect
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556
github.com/vultr/govultr/v2 v2.9.0

15
go.sum
View File

@ -52,6 +52,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
git.blindage.org/21h/hcloud-dns v0.0.0-20200807003420-f768ffe03f8d h1:d6sdozgfqtgaOhjUn++lbo5siX3HELjcOUnbtrvVQi4=
git.blindage.org/21h/hcloud-dns v0.0.0-20200807003420-f768ffe03f8d/go.mod h1:n26Twiii5jhkMC+Ocz/s8R73cBBcXRIwyTqQ+6bOZGo=
git.lukeshu.com/go/libsystemd v0.5.3/go.mod h1:FfDoP0i92r4p5Vn4NCLxvjkd7rCOe6otPa4L6hZg9WM=
github.com/0x4c6565/genie v1.0.0/go.mod h1:fDOjW0hFamMWOIkh4irf2D/TZpXXWMFtpP8MfgK0N3c=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v46.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v46.4.0+incompatible h1:fCN6Pi+tEiEwFa8RSmtVlFHRXEZ+DJm9gfx/MKqYWw4=
@ -270,6 +271,7 @@ github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2
github.com/datawire/ambassador v1.6.0 h1:4KduhY/wqtv0jK8sMVQNtENHy9fmoXugsuFp/UrM0Ts=
github.com/datawire/ambassador v1.6.0/go.mod h1:mV5EhoG/NnHBsffmLnjrq+x4ZNkYDWFZXW9R+AueUiE=
github.com/datawire/pf v0.0.0-20180510150411-31a823f9495a/go.mod h1:H8uUmE8qqo7z9u30MYB9riLyRckPHOPBk9ZdCuH+dQQ=
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -431,6 +433,10 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY=
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@ -742,6 +748,8 @@ github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4F
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -1082,6 +1090,10 @@ github.com/transip/gotransip/v6 v6.6.2 h1:+d3QO5Cyfh9n/J5OZxz8roer4JQIdmYvHVHExO
github.com/transip/gotransip/v6 v6.6.2/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ukfast/go-durationstring v1.0.0 h1:kgPuA7XjLjgLDfkG8j0MpolxcZh/eMdiVoOIFD/uc5I=
github.com/ukfast/go-durationstring v1.0.0/go.mod h1:Ci81n51kfxlKUIaLY9cINIKRO94VTqV+iCGbOMTb0V8=
github.com/ukfast/sdk-go v1.4.23 h1:dLZmHW2jgV0QQ2TGGdbL2tYVdtQPcuUub7Rzh+6Cqic=
github.com/ukfast/sdk-go v1.4.23/go.mod h1:tspweEP77MHhVEYgEEieKAKGITFgwkYl1q5fLh4HZAo=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 h1:n7unetnX8WWTc0U85h/0+dJoLWLqoaJwowXB9RkBdxU=
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60/go.mod h1:43vmy6GEvRuVMpGEWfJ/JoEM6RIqUQI1/tb8JqZR1zI=
@ -1694,6 +1706,9 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.27.0 h1:wCg/0hk9RzcB0CYw8pYV6FiBYug1on0cpco9YZF8jqA=
gopkg.in/go-playground/validator.v9 v9.27.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=

View File

@ -62,6 +62,7 @@ import (
"sigs.k8s.io/external-dns/provider/rcode0"
"sigs.k8s.io/external-dns/provider/rdns"
"sigs.k8s.io/external-dns/provider/rfc2136"
"sigs.k8s.io/external-dns/provider/safedns"
"sigs.k8s.io/external-dns/provider/scaleway"
"sigs.k8s.io/external-dns/provider/transip"
"sigs.k8s.io/external-dns/provider/ultradns"
@ -324,6 +325,8 @@ func main() {
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)
case "safedns":
p, err = safedns.NewSafeDNSProvider(domainFilter, cfg.DryRun)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}

View File

@ -387,7 +387,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("default-targets", "Set globally default IP address that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets)
// 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, 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("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, safedns)").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", "safedns")
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
app.Flag("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default(defaultConfig.RegexDomainFilter.String()).RegexpVar(&cfg.RegexDomainFilter)

242
provider/safedns/safedns.go Normal file
View File

@ -0,0 +1,242 @@
/*
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 safedns
import (
"context"
"fmt"
"os"
log "github.com/sirupsen/logrus"
ukfClient "github.com/ukfast/sdk-go/pkg/client"
ukfConnection "github.com/ukfast/sdk-go/pkg/connection"
"github.com/ukfast/sdk-go/pkg/service/safedns"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
// SafeDNS is an interface that is a subset of the SafeDNS service API that are actually used.
// Signatures must match exactly.
type SafeDNS interface {
CreateZoneRecord(zoneName string, req safedns.CreateRecordRequest) (int, error)
DeleteZoneRecord(zoneName string, recordID int) error
GetZone(zoneName string) (safedns.Zone, error)
GetZoneRecord(zoneName string, recordID int) (safedns.Record, error)
GetZoneRecords(zoneName string, parameters ukfConnection.APIRequestParameters) ([]safedns.Record, error)
GetZones(parameters ukfConnection.APIRequestParameters) ([]safedns.Zone, error)
PatchZoneRecord(zoneName string, recordID int, patch safedns.PatchRecordRequest) (int, error)
UpdateZoneRecord(zoneName string, record safedns.Record) (int, error)
}
// SafeDNSProvider implements the DNS provider spec for UKFast SafeDNS.
type SafeDNSProvider struct {
provider.BaseProvider
Client SafeDNS
// Only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter
DryRun bool
APIRequestParams ukfConnection.APIRequestParameters
}
// ZoneRecord is a datatype to simplify management of a record in a zone.
type ZoneRecord struct {
ID int
Name string
Type safedns.RecordType
TTL safedns.RecordTTL
Zone string
Content string
}
func NewSafeDNSProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*SafeDNSProvider, error) {
token, ok := os.LookupEnv("SAFEDNS_TOKEN")
if !ok {
return nil, fmt.Errorf("no SAFEDNS_TOKEN found in environment")
}
ukfAPIConnection := ukfConnection.NewAPIKeyCredentialsAPIConnection(token)
ukfClient := ukfClient.NewClient(ukfAPIConnection)
safeDNS := ukfClient.SafeDNSService()
provider := &SafeDNSProvider{
Client: safeDNS,
domainFilter: domainFilter,
DryRun: dryRun,
APIRequestParams: *ukfConnection.NewAPIRequestParameters(),
}
return provider, nil
}
// Zones returns the list of hosted zones in the SafeDNS account
func (p *SafeDNSProvider) Zones(ctx context.Context) ([]safedns.Zone, error) {
var zones []safedns.Zone
allZones, err := p.Client.GetZones(p.APIRequestParams)
if err != nil {
return nil, err
}
// Check each found zone to see whether they match the domain filter provided. If they do, append it to the array of
// zones defined above. If not, continue to the next item in the loop.
for _, zone := range allZones {
if p.domainFilter.Match(zone.Name) {
zones = append(zones, zone)
} else {
continue
}
}
return zones, nil
}
func (p *SafeDNSProvider) ZoneRecords(ctx context.Context) ([]ZoneRecord, error) {
zones, err := p.Zones(ctx)
if err != nil {
return nil, err
}
var zoneRecords []ZoneRecord
for _, zone := range zones {
// For each zone in the zonelist, get all records of an ExternalDNS supported type.
records, err := p.Client.GetZoneRecords(zone.Name, p.APIRequestParams)
if err != nil {
return nil, err
}
for _, r := range records {
zoneRecord := ZoneRecord{
ID: r.ID,
Name: r.Name,
Type: r.Type,
TTL: r.TTL,
Zone: zone.Name,
Content: r.Content,
}
zoneRecords = append(zoneRecords, zoneRecord)
}
}
return zoneRecords, nil
}
// Records returns a list of Endpoint resources created from all records in supported zones.
func (p *SafeDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
zoneRecords, err := p.ZoneRecords(ctx)
if err != nil {
return nil, err
}
for _, r := range zoneRecords {
if provider.SupportedRecordType(string(r.Type)) {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, string(r.Type), endpoint.TTL(r.TTL), r.Content))
}
}
return endpoints, nil
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *SafeDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
// Identify the zone name for each record
zoneNameIDMapper := provider.ZoneIDName{}
zones, err := p.Zones(ctx)
if err != nil {
return err
}
for _, zone := range zones {
zoneNameIDMapper.Add(zone.Name, zone.Name)
}
zoneRecords, err := p.ZoneRecords(ctx)
if err != nil {
return err
}
for _, endpoint := range changes.Create {
_, ZoneName := zoneNameIDMapper.FindZone(endpoint.DNSName)
for _, target := range endpoint.Targets {
request := safedns.CreateRecordRequest{
Name: endpoint.DNSName,
Type: endpoint.RecordType,
Content: target,
}
log.WithFields(log.Fields{
"zoneID": ZoneName,
"dnsName": endpoint.DNSName,
"recordType": endpoint.RecordType,
"Value": target,
}).Info("Creating record")
_, err := p.Client.CreateZoneRecord(ZoneName, request)
if err != nil {
return err
}
}
}
for _, endpoint := range changes.UpdateNew {
// Currently iterates over each zoneRecord in ZoneRecords for each Endpoint
// in UpdateNew; the same will go for Delete. As it's double-iteration,
// that's O(n^2), which isn't great. No performance issues have been noted
// thus far.
var zoneRecord ZoneRecord
for _, target := range endpoint.Targets {
for _, zr := range zoneRecords {
if zr.Name == endpoint.DNSName && zr.Content == target {
zoneRecord = zr
break
}
}
newTTL := safedns.RecordTTL(int(endpoint.RecordTTL))
newRecord := safedns.PatchRecordRequest{
Name: endpoint.DNSName,
Content: target,
TTL: &newTTL,
Type: endpoint.RecordType,
}
log.WithFields(log.Fields{
"zoneID": zoneRecord.Zone,
"dnsName": newRecord.Name,
"recordType": newRecord.Type,
"Value": newRecord.Content,
"Priority": newRecord.Priority,
}).Info("Patching record")
_, err = p.Client.PatchZoneRecord(zoneRecord.Zone, zoneRecord.ID, newRecord)
if err != nil {
return err
}
}
}
for _, endpoint := range changes.Delete {
// As above, currently iterates in O(n^2). May be a good start for optimisations.
var zoneRecord ZoneRecord
for _, zr := range zoneRecords {
if zr.Name == endpoint.DNSName && string(zr.Type) == endpoint.RecordType {
zoneRecord = zr
break
}
}
log.WithFields(log.Fields{
"zoneID": zoneRecord.Zone,
"dnsName": zoneRecord.Name,
"recordType": zoneRecord.Type,
}).Info("Deleting record")
err := p.Client.DeleteZoneRecord(zoneRecord.Zone, zoneRecord.ID)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,366 @@
/*
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 safedns
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
ukfConnection "github.com/ukfast/sdk-go/pkg/connection"
"github.com/ukfast/sdk-go/pkg/service/safedns"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
)
// Create an implementation of the SafeDNS interface for Mocking
type MockSafeDNSService struct {
mock.Mock
}
func (m *MockSafeDNSService) CreateZoneRecord(zoneName string, req safedns.CreateRecordRequest) (int, error) {
args := m.Called(zoneName, req)
return args.Int(0), args.Error(1)
}
func (m *MockSafeDNSService) DeleteZoneRecord(zoneName string, recordID int) error {
args := m.Called(zoneName, recordID)
return args.Error(0)
}
func (m *MockSafeDNSService) GetZone(zoneName string) (safedns.Zone, error) {
args := m.Called(zoneName)
return args.Get(0).(safedns.Zone), args.Error(1)
}
func (m *MockSafeDNSService) GetZoneRecord(zoneName string, recordID int) (safedns.Record, error) {
args := m.Called(zoneName, recordID)
return args.Get(0).(safedns.Record), args.Error(1)
}
func (m *MockSafeDNSService) GetZoneRecords(zoneName string, parameters ukfConnection.APIRequestParameters) ([]safedns.Record, error) {
args := m.Called(zoneName, parameters)
return args.Get(0).([]safedns.Record), args.Error(1)
}
func (m *MockSafeDNSService) GetZones(parameters ukfConnection.APIRequestParameters) ([]safedns.Zone, error) {
args := m.Called(parameters)
return args.Get(0).([]safedns.Zone), args.Error(1)
}
func (m *MockSafeDNSService) PatchZoneRecord(zoneName string, recordID int, patch safedns.PatchRecordRequest) (int, error) {
args := m.Called(zoneName, recordID, patch)
return args.Int(0), args.Error(1)
}
func (m *MockSafeDNSService) UpdateZoneRecord(zoneName string, record safedns.Record) (int, error) {
args := m.Called(zoneName, record)
return args.Int(0), args.Error(1)
}
// Utility functions
func createZones() []safedns.Zone {
return []safedns.Zone{
{Name: "foo.com", Description: "Foo dot com"},
{Name: "bar.io", Description: ""},
{Name: "baz.org", Description: "Org"},
}
}
func createFooRecords() []safedns.Record {
return []safedns.Record{
{
ID: 11,
Type: safedns.RecordTypeA,
Name: "foo.com",
Content: "targetFoo",
TTL: safedns.RecordTTL(3600),
},
{
ID: 12,
Type: safedns.RecordTypeTXT,
Name: "foo.com",
Content: "text",
TTL: safedns.RecordTTL(3600),
},
{
ID: 13,
Type: safedns.RecordTypeCAA,
Name: "foo.com",
Content: "",
TTL: safedns.RecordTTL(3600),
},
}
}
func createBarRecords() []safedns.Record {
return []safedns.Record{}
}
func createBazRecords() []safedns.Record {
return []safedns.Record{
{
ID: 31,
Type: safedns.RecordTypeA,
Name: "baz.org",
Content: "targetBaz",
TTL: safedns.RecordTTL(3600),
},
{
ID: 32,
Type: safedns.RecordTypeTXT,
Name: "baz.org",
Content: "text",
TTL: safedns.RecordTTL(3600),
},
{
ID: 33,
Type: safedns.RecordTypeA,
Name: "api.baz.org",
Content: "targetBazAPI",
TTL: safedns.RecordTTL(3600),
},
{
ID: 34,
Type: safedns.RecordTypeTXT,
Name: "api.baz.org",
Content: "text",
TTL: safedns.RecordTTL(3600),
},
}
}
// Actual tests
func TestNewSafeDNSProvider(t *testing.T) {
_ = os.Setenv("SAFEDNS_TOKEN", "DUMMYVALUE")
_, err := NewSafeDNSProvider(endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true)
require.NoError(t, err)
_ = os.Unsetenv("SAFEDNS_TOKEN")
_, err = NewSafeDNSProvider(endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true)
require.Error(t, err)
}
func TestRecords(t *testing.T) {
mockSafeDNSService := MockSafeDNSService{}
provider := &SafeDNSProvider{
Client: &mockSafeDNSService,
domainFilter: endpoint.NewDomainFilter([]string{}),
DryRun: false,
}
mockSafeDNSService.On(
"GetZones",
mock.Anything,
).Return(createZones(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"foo.com",
mock.Anything,
).Return(createFooRecords(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"bar.io",
mock.Anything,
).Return(createBarRecords(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"baz.org",
mock.Anything,
).Return(createBazRecords(), nil).Once()
actual, err := provider.Records(context.Background())
require.NoError(t, err)
expected := []*endpoint.Endpoint{
{
DNSName: "foo.com",
Targets: []string{"targetFoo"},
RecordType: "A",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "foo.com",
Targets: []string{"text"},
RecordType: "TXT",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "baz.org",
Targets: []string{"targetBaz"},
RecordType: "A",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "baz.org",
Targets: []string{"text"},
RecordType: "TXT",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "api.baz.org",
Targets: []string{"targetBazAPI"},
RecordType: "A",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
{
DNSName: "api.baz.org",
Targets: []string{"text"},
RecordType: "TXT",
RecordTTL: 3600,
Labels: endpoint.NewLabels(),
},
}
mockSafeDNSService.AssertExpectations(t)
assert.Equal(t, expected, actual)
}
func TestSafeDNSApplyChanges(t *testing.T) {
mockSafeDNSService := MockSafeDNSService{}
provider := &SafeDNSProvider{
Client: &mockSafeDNSService,
domainFilter: endpoint.NewDomainFilter([]string{}),
DryRun: false,
}
// Dummy data
mockSafeDNSService.On(
"GetZones",
mock.Anything,
).Return(createZones(), nil).Once()
mockSafeDNSService.On(
"GetZones",
mock.Anything,
).Return(createZones(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"foo.com",
mock.Anything,
).Return(createFooRecords(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"bar.io",
mock.Anything,
).Return(createBarRecords(), nil).Once()
mockSafeDNSService.On(
"GetZoneRecords",
"baz.org",
mock.Anything,
).Return(createBazRecords(), nil).Once()
// Apply actions
mockSafeDNSService.On(
"DeleteZoneRecord",
"baz.org",
33,
).Return(nil).Once()
mockSafeDNSService.On(
"DeleteZoneRecord",
"baz.org",
34,
).Return(nil).Once()
TTL300 := safedns.RecordTTL(300)
mockSafeDNSService.On(
"PatchZoneRecord",
"foo.com",
11,
safedns.PatchRecordRequest{
Type: "A",
Name: "foo.com",
Content: "targetFoo",
TTL: &TTL300,
},
).Return(123, nil).Once()
mockSafeDNSService.On(
"CreateZoneRecord",
"bar.io",
safedns.CreateRecordRequest{
Type: "A",
Name: "create.bar.io",
Content: "targetBar",
},
).Return(246, nil).Once()
mockSafeDNSService.On(
"CreateZoneRecord",
"bar.io",
safedns.CreateRecordRequest{
Type: "A",
Name: "bar.io",
Content: "targetBar",
},
).Return(369, nil).Once()
err := provider.ApplyChanges(context.Background(), &plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "create.bar.io",
RecordType: "A",
Targets: []string{"targetBar"},
RecordTTL: 3600,
},
{
DNSName: "bar.io",
RecordType: "A",
Targets: []string{"targetBar"},
RecordTTL: 3600,
},
},
Delete: []*endpoint.Endpoint{
{
DNSName: "api.baz.org",
RecordType: "A",
},
{
DNSName: "api.baz.org",
RecordType: "TXT",
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "foo.com",
RecordType: "A",
RecordTTL: 300,
Targets: []string{"targetFoo"},
},
},
UpdateOld: []*endpoint.Endpoint{},
})
require.NoError(t, err)
mockSafeDNSService.AssertExpectations(t)
}