From d3ef1cd39f2985ce4f502bc07527e54dcc48666f Mon Sep 17 00:00:00 2001 From: Rick Henry Date: Thu, 7 Oct 2021 15:35:15 +0100 Subject: [PATCH 1/8] Create package for SafeDNS --- main.go | 3 +++ pkg/apis/externaldns/types.go | 2 +- provider/safedns/safedns.go | 20 ++++++++++++++++++++ provider/safedns/safedns_test.go | 27 +++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 provider/safedns/safedns.go create mode 100644 provider/safedns/safedns_test.go diff --git a/main.go b/main.go index fb6364c66..861b66513 100644 --- a/main.go +++ b/main.go @@ -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" @@ -323,6 +324,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(ctx, domainFilter, cfg.DryRun) default: log.Fatalf("unknown dns provider: %s", cfg.Provider) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 2c8d5fce4..3504d6a01 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -385,7 +385,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) diff --git a/provider/safedns/safedns.go b/provider/safedns/safedns.go new file mode 100644 index 000000000..06c1ecec1 --- /dev/null +++ b/provider/safedns/safedns.go @@ -0,0 +1,20 @@ +/* +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 ( +) diff --git a/provider/safedns/safedns_test.go b/provider/safedns/safedns_test.go new file mode 100644 index 000000000..a05ce46cc --- /dev/null +++ b/provider/safedns/safedns_test.go @@ -0,0 +1,27 @@ +/* +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 ( + "net/http" + + "github.com/stretchr/testify/mock" +) + +type MockSafeDNSDomainClient struct { + mock.Mock +} From a106313f5590b47dd67a668f65800a7e7a22224b Mon Sep 17 00:00:00 2001 From: Rick Henry Date: Thu, 7 Oct 2021 17:47:16 +0100 Subject: [PATCH 2/8] First-pass implementation of SafeDNS provider --- go.mod | 1 + go.sum | 15 ++++++ main.go | 2 +- provider/safedns/safedns.go | 102 ++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c41477dbb..9b8a3adae 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e1c8538f9..64d568cf8 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -265,6 +266,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= @@ -426,6 +428,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= @@ -738,6 +744,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= @@ -1076,6 +1084,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= @@ -1686,6 +1698,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= diff --git a/main.go b/main.go index 861b66513..4d76d0b4e 100644 --- a/main.go +++ b/main.go @@ -325,7 +325,7 @@ func main() { case "gandi": p, err = gandi.NewGandiProvider(ctx, domainFilter, cfg.DryRun) case "safedns": - p, err = safedns.NewSafeDNSProvider(ctx, domainFilter, cfg.DryRun) + p, err = safedns.NewSafeDNSProvider(domainFilter, cfg.DryRun) default: log.Fatalf("unknown dns provider: %s", cfg.Provider) } diff --git a/provider/safedns/safedns.go b/provider/safedns/safedns.go index 06c1ecec1..3339b2019 100644 --- a/provider/safedns/safedns.go +++ b/provider/safedns/safedns.go @@ -17,4 +17,106 @@ limitations under the License. package safedns import ( + "context" + "fmt" + "os" + + ukf_client "github.com/ukfast/sdk-go/pkg/client" + ukf_connection "github.com/ukfast/sdk-go/pkg/connection" + "github.com/ukfast/sdk-go/pkg/service/safedns" + + "sigs.k8s.io/external-dns/provider" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" ) + +// The SafeDNS interface 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 ukf_connection.APIRequestParameters) ([]safedns.Record, error) + GetZones(parameters ukf_connection.APIRequestParameters) ([]safedns.Zone, error) + PatchZoneRecord(zoneName string, recordID int, patch safedns.PatchRecordRequest) (int, error) + UpdateZoneRecord(zoneName string, record safedns.Record) (int, error) +} + +type SafeDNSProvider struct { + provider.BaseProvider + Client SafeDNS + // Only consider hosted zones managing domains ending in this suffix + domainFilter endpoint.DomainFilter + DryRun bool + APIRequestParams ukf_connection.APIRequestParameters +} + +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 := ukf_connection.NewAPIKeyCredentialsAPIConnection(token) + ukfClient := ukf_client.NewClient(ukfAPIConnection) + safeDNS := ukfClient.SafeDNSService() + + provider := &SafeDNSProvider{ + Client: safeDNS, + domainFilter: domainFilter, + DryRun: dryRun, + APIRequestParams: *ukf_connection.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 +} + +// Records returns a list of Endpoint resources created from all records in supported zones. +func (p *SafeDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { + zones, err := p.Zones(ctx) + if err != nil { + return nil, err + } + + endpoints := []*endpoint.Endpoint{} + 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 { + 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 { + // TODO: Implement this + return nil +} From 7cac705c084c56bf04bce8ac5c0c7f336d1d596e Mon Sep 17 00:00:00 2001 From: Rick Henry Date: Thu, 7 Oct 2021 18:57:46 +0100 Subject: [PATCH 3/8] Support editing records --- provider/safedns/safedns.go | 131 ++++++++++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 7 deletions(-) diff --git a/provider/safedns/safedns.go b/provider/safedns/safedns.go index 3339b2019..bc8e8d51e 100644 --- a/provider/safedns/safedns.go +++ b/provider/safedns/safedns.go @@ -21,9 +21,10 @@ import ( "fmt" "os" + "github.com/ukfast/sdk-go/pkg/service/safedns" + log "github.com/sirupsen/logrus" ukf_client "github.com/ukfast/sdk-go/pkg/client" ukf_connection "github.com/ukfast/sdk-go/pkg/connection" - "github.com/ukfast/sdk-go/pkg/service/safedns" "sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/endpoint" @@ -52,6 +53,15 @@ type SafeDNSProvider struct { APIRequestParams ukf_connection.APIRequestParameters } +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 { @@ -92,14 +102,13 @@ func (p *SafeDNSProvider) Zones(ctx context.Context) ([]safedns.Zone, error) { return zones, 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) { +func (p *SafeDNSProvider) ZoneRecords(ctx context.Context) ([]ZoneRecord, error){ zones, err := p.Zones(ctx) if err != nil { return nil, err } - endpoints := []*endpoint.Endpoint{} + 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) @@ -107,9 +116,30 @@ func (p *SafeDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, er return nil, err } for _, r := range records { - if provider.SupportedRecordType(string(r.Type)) { - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, string(r.Type), endpoint.TTL(r.TTL), r.Content)) + 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 @@ -117,6 +147,93 @@ func (p *SafeDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, er // ApplyChanges applies a given set of changes in a given zone. func (p *SafeDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - // TODO: Implement this + // 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 { + // TODO: Find a more effient way of doing this. + // 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. + 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 { + // TODO: Find a more effient way of doing this. + 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 } From 7addd03bb825f7624c1696645e9313e8fdc45813 Mon Sep 17 00:00:00 2001 From: Rick Henry Date: Fri, 17 Dec 2021 14:11:16 +0000 Subject: [PATCH 4/8] Comment on types explaining what they do --- provider/safedns/safedns.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/provider/safedns/safedns.go b/provider/safedns/safedns.go index bc8e8d51e..f313b133a 100644 --- a/provider/safedns/safedns.go +++ b/provider/safedns/safedns.go @@ -31,7 +31,7 @@ import ( "sigs.k8s.io/external-dns/plan" ) -// The SafeDNS interface is a subset of the SafeDNS service API that are actually used. +// 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) @@ -44,6 +44,7 @@ type SafeDNS interface { UpdateZoneRecord(zoneName string, record safedns.Record) (int, error) } +// SafeDNSProvider implements the DNS provider spec for UKFast SafeDNS. type SafeDNSProvider struct { provider.BaseProvider Client SafeDNS @@ -53,6 +54,7 @@ type SafeDNSProvider struct { APIRequestParams ukf_connection.APIRequestParameters } +// ZoneRecord is a datatype to simplify management of a record in a zone. type ZoneRecord struct { ID int Name string From 733f6a834ca97e57f5624c0b4c4b37566a68dd1d Mon Sep 17 00:00:00 2001 From: Rick Henry Date: Fri, 8 Oct 2021 09:54:06 +0100 Subject: [PATCH 5/8] Document SafeDNS provider --- README.md | 3 + docs/tutorials/UKFast_SafeDNS.md | 210 +++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 docs/tutorials/UKFast_SafeDNS.md diff --git a/README.md b/README.md index a9b7d8cdb..d91b60cba 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/tutorials/UKFast_SafeDNS.md b/docs/tutorials/UKFast_SafeDNS.md new file mode 100644 index 000000000..742c1fdd6 --- /dev/null +++ b/docs/tutorials/UKFast_SafeDNS.md @@ -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 +``` From 023eb23ef9d2de09b00638ac58c65b5d2ef02a84 Mon Sep 17 00:00:00 2001 From: Rick Henry Date: Fri, 8 Oct 2021 10:38:21 +0100 Subject: [PATCH 6/8] Implement tests for SafeDNS provider --- provider/safedns/safedns_test.go | 344 ++++++++++++++++++++++++++++++- 1 file changed, 342 insertions(+), 2 deletions(-) diff --git a/provider/safedns/safedns_test.go b/provider/safedns/safedns_test.go index a05ce46cc..47fae0c78 100644 --- a/provider/safedns/safedns_test.go +++ b/provider/safedns/safedns_test.go @@ -17,11 +17,351 @@ limitations under the License. package safedns import ( - "net/http" + "context" + "os" + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/ukfast/sdk-go/pkg/service/safedns" + ukf_connection "github.com/ukfast/sdk-go/pkg/connection" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" ) -type MockSafeDNSDomainClient struct { +// 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 ukf_connection.APIRequestParameters) ([]safedns.Record, error) { + args := m.Called(zoneName, parameters) + return args.Get(0).([]safedns.Record), args.Error(1) +} + +func (m *MockSafeDNSService) GetZones(parameters ukf_connection.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) +} From 6fc68a82dbb8de1bbf453e968e1539baeb39352c Mon Sep 17 00:00:00 2001 From: Rick Henry Date: Tue, 7 Dec 2021 14:38:34 +0000 Subject: [PATCH 7/8] Reformat code to meet lint standards --- provider/safedns/safedns.go | 360 ++++++++++---------- provider/safedns/safedns_test.go | 553 +++++++++++++++---------------- 2 files changed, 456 insertions(+), 457 deletions(-) diff --git a/provider/safedns/safedns.go b/provider/safedns/safedns.go index f313b133a..947414591 100644 --- a/provider/safedns/safedns.go +++ b/provider/safedns/safedns.go @@ -17,225 +17,225 @@ limitations under the License. package safedns import ( - "context" - "fmt" - "os" + "context" + "fmt" + "os" - "github.com/ukfast/sdk-go/pkg/service/safedns" - log "github.com/sirupsen/logrus" - ukf_client "github.com/ukfast/sdk-go/pkg/client" - ukf_connection "github.com/ukfast/sdk-go/pkg/connection" + 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/provider" - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" + "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 ukf_connection.APIRequestParameters) ([]safedns.Record, error) - GetZones(parameters ukf_connection.APIRequestParameters) ([]safedns.Zone, error) - PatchZoneRecord(zoneName string, recordID int, patch safedns.PatchRecordRequest) (int, error) - UpdateZoneRecord(zoneName string, record safedns.Record) (int, error) + 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 ukf_connection.APIRequestParameters + 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 + 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") - } + token, ok := os.LookupEnv("SAFEDNS_TOKEN") + if !ok { + return nil, fmt.Errorf("no SAFEDNS_TOKEN found in environment") + } - ukfAPIConnection := ukf_connection.NewAPIKeyCredentialsAPIConnection(token) - ukfClient := ukf_client.NewClient(ukfAPIConnection) - safeDNS := ukfClient.SafeDNSService() + ukfAPIConnection := ukfConnection.NewAPIKeyCredentialsAPIConnection(token) + ukfClient := ukfClient.NewClient(ukfAPIConnection) + safeDNS := ukfClient.SafeDNSService() - provider := &SafeDNSProvider{ - Client: safeDNS, - domainFilter: domainFilter, - DryRun: dryRun, - APIRequestParams: *ukf_connection.NewAPIRequestParameters(), - } - return provider, nil + 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 + var zones []safedns.Zone - allZones, err := p.Client.GetZones(p.APIRequestParams) - if err != nil { - return nil, err - } + 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 + // 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 - } +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 + 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 + 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{} + // 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) - } + 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 - } + 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 { - // TODO: Find a more effient way of doing this. - // 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. - var zoneRecord ZoneRecord - for _, target := range endpoint.Targets { - for _, zr := range zoneRecords { - if zr.Name == endpoint.DNSName && zr.Content == target { - zoneRecord = zr - break - } - } + 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 { + // TODO: Find a more effient way of doing this. + // 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. + 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 { - // TODO: Find a more effient way of doing this. - 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 + 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 { + // TODO: Find a more effient way of doing this. + 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 } diff --git a/provider/safedns/safedns_test.go b/provider/safedns/safedns_test.go index 47fae0c78..3c6da6109 100644 --- a/provider/safedns/safedns_test.go +++ b/provider/safedns/safedns_test.go @@ -17,351 +17,350 @@ limitations under the License. package safedns import ( - "context" - "os" - "testing" + "context" + "os" + "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/ukfast/sdk-go/pkg/service/safedns" - ukf_connection "github.com/ukfast/sdk-go/pkg/connection" + "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" + "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 + 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) + 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) + 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) + 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) + args := m.Called(zoneName, recordID) + return args.Get(0).(safedns.Record), args.Error(1) } -func (m *MockSafeDNSService) GetZoneRecords(zoneName string, parameters ukf_connection.APIRequestParameters) ([]safedns.Record, error) { - args := m.Called(zoneName, parameters) - 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 ukf_connection.APIRequestParameters) ([]safedns.Zone, error) { - args := m.Called(parameters) - return args.Get(0).([]safedns.Zone), 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) + 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) + 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"}, - } + 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), - }, - } + 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{} + 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), - }, - } + 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.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) + _ = 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{} + mockSafeDNSService := MockSafeDNSService{} - provider := &SafeDNSProvider{ - Client: &mockSafeDNSService, - domainFilter: endpoint.NewDomainFilter([]string{}), - DryRun: false, - } + provider := &SafeDNSProvider{ + Client: &mockSafeDNSService, + domainFilter: endpoint.NewDomainFilter([]string{}), + DryRun: false, + } - 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", + "foo.com", + mock.Anything, + ).Return(createFooRecords(), nil).Once() - mockSafeDNSService.On( - "GetZoneRecords", - "bar.io", - mock.Anything, - ).Return(createBarRecords(), nil).Once() + mockSafeDNSService.On( + "GetZoneRecords", + "bar.io", + mock.Anything, + ).Return(createBarRecords(), nil).Once() - mockSafeDNSService.On( - "GetZoneRecords", - "baz.org", - mock.Anything, - ).Return(createBazRecords(), nil).Once() + mockSafeDNSService.On( + "GetZoneRecords", + "baz.org", + mock.Anything, + ).Return(createBazRecords(), nil).Once() - actual, err := provider.Records(context.Background()) - require.NoError(t, err) + 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(), - }, - } + 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) + mockSafeDNSService.AssertExpectations(t) + assert.Equal(t, expected, actual) } -func TestSafeDNSApplyChanges( t *testing.T) { - mockSafeDNSService := MockSafeDNSService{} +func TestSafeDNSApplyChanges(t *testing.T) { + mockSafeDNSService := MockSafeDNSService{} - provider := &SafeDNSProvider{ - Client: &mockSafeDNSService, - domainFilter: endpoint.NewDomainFilter([]string{}), - DryRun: false, - } + 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() + // 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", + "foo.com", + mock.Anything, + ).Return(createFooRecords(), nil).Once() - mockSafeDNSService.On( - "GetZoneRecords", - "bar.io", - mock.Anything, - ).Return(createBarRecords(), nil).Once() + mockSafeDNSService.On( + "GetZoneRecords", + "bar.io", + mock.Anything, + ).Return(createBarRecords(), nil).Once() - mockSafeDNSService.On( - "GetZoneRecords", - "baz.org", - mock.Anything, - ).Return(createBazRecords(), 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() + // 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() + 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: "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() + 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) + 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) + mockSafeDNSService.AssertExpectations(t) } From ab8817eb9c26286727323fb6bf605c14225184c7 Mon Sep 17 00:00:00 2001 From: Rick Henry Date: Mon, 20 Dec 2021 18:47:48 +0000 Subject: [PATCH 8/8] Reword comments to remove explicit TODO Keeping the comments in some degree is valuable, as it clarifies the time complexity of the operations done; in the event of a performance issue, this would likely be somewhere to look. However, remove the `TODO` string directly, as it is not an urgent look-at-me-now. --- provider/safedns/safedns.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/provider/safedns/safedns.go b/provider/safedns/safedns.go index 947414591..64173a078 100644 --- a/provider/safedns/safedns.go +++ b/provider/safedns/safedns.go @@ -186,9 +186,10 @@ func (p *SafeDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Change } } for _, endpoint := range changes.UpdateNew { - // TODO: Find a more effient way of doing this. - // 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. + // 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 { @@ -219,7 +220,7 @@ func (p *SafeDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Change } } for _, endpoint := range changes.Delete { - // TODO: Find a more effient way of doing this. + // 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 {