diff --git a/.golangci.yml b/.golangci.yml index 3c9495ef9..15a75a89b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,23 +1,70 @@ -run: - concurrency: 4 - - modules-download-mode: readonly - linters-settings: + exhaustive: + default-signifies-exhaustive: false + goimports: + local-prefixes: github.com/kubernetes-sigs/external-dns golint: - min-confidence: 0.9 - - gocyclo: - min-complexity: 15 + min-confidence: 0.9 + maligned: + suggest-new: true + misspell: + locale: US linters: + # please, do not use `enable-all`: it's deprecated and will be removed soon. + # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint disable-all: true enable: + - deadcode + - depguard + - dogsled + - gofmt + - goimports + - golint + - goprintffuncname + - gosimple - govet - ineffassign - - golint - - goimports - - misspell - - unconvert - - megacheck - interfacer + - misspell + - rowserrcheck + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unused + - varcheck + - whitespace + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - deadcode + - depguard + - dogsled + - gofmt + - goimports + - golint + - goprintffuncname + - gosimple + - govet + - ineffassign + - interfacer + - misspell + - nolintlint + - rowserrcheck + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unused + - varcheck + - whitespace + +run: + skip-files: + - endpoint/zz_generated.deepcopy.go diff --git a/.travis.yml b/.travis.yml index 5368e0435..3b82c7976 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,23 +6,35 @@ os: language: go go: -- "1.13.x" +- "1.14.x" - tip matrix: allow_failures: - go: tip -env: -- GOLANGCI_RELEASE="v1.23.1" - -before_install: -- GO111MODULE=off go get github.com/mattn/goveralls -- GO111MODULE=off go get github.com/lawrencewoodman/roveralls -- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_RELEASE} +cache: + directories: + - $GOPATH/pkg/mod script: - make test -- make lint -- travis_wait 20 roveralls -- goveralls -coverprofile=roveralls.coverprofile -service=travis-ci + +jobs: + include: + - name: "Linting" + go: "1.14.x" + env: + - GOLANGCI_RELEASE="v1.26.0" + before_install: + - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_RELEASE} + script: + - make lint + - name: "Coverage" + go: "1.14.x" + before_install: + - GO111MODULE=off go get github.com/mattn/goveralls + - GO111MODULE=off go get github.com/lawrencewoodman/roveralls + script: + - travis_wait 20 roveralls + - goveralls -coverprofile=roveralls.coverprofile -service=travis-ci diff --git a/CHANGELOG.md b/CHANGELOG.md index 05804f49f..d2479e44e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +## v0.7.2 - 2020-06-03 + +- Update blogpost in README (#1610) @vanhumbeecka +- Support for AWS Route53 in China (#1603) @greenu +- Update Govcloud provider hosted zones (#1592) @clhuang +- Fix issue with too large DNS messages (#1590) @dmayle +- use the latest linode go version (#1587) @tariq1890 +- use istio client-go and clean up k8s deps (#1584) @tariq1890 +- Add owners for cloudflare and coredns providers (#1582) @Raffo +- remove some code duplication in gateway source (#1575) @tariq1890 +- update Contour IngressRoute deps (#1569) @stevesloka +- Make tests faster (#1568) @sheerun +- Fix scheduling of reconciliation (#1567) @sheerun +- fix minor typos in istio gateway source docs (#1566) @tariq1890 +- Provider structure refactor (#1565) @Raffo +- Fix typo in ttl.md (#1564) @rtnpro +- Fix goreportcard warnings (#1561) @squat +- Use consistent headless service name in example (#1559) @lowkeyliesmyth +- Update go versions to 1.14.x that were missed in commit 99cebfcf from PR #1476 (#1554) @stealthybox +- Remove duplicate selector from DigitalOcean manifest (#1553) @ggordan +- Upgrade DNSimple client and add support for contexts (#1551) @weppos +- Upgrade github.com/miekg/dns to v1.1.25 (#1545) @davidcollom +- Fix updates in CloudFlare provider (#1542) @sheerun +- update readme for latest version (#1539) @elsesiy +- Improve Cloudflare tests in preparation to fix other issues (#1537) @sheerun +- Allow for custom property comparators (#1536) @sheerun +- fix typo (#1535) @tmatias +- Bump github.com/pkg/errors from 0.8.1 to 0.9.1 (#1531) @njuettner +- Bump github.com/digitalocean/godo from 1.19.0 to 1.34.0 (#1530) @njuettner +- Bump github.com/prometheus/client_golang from 1.0.0 to 1.5.1 (#1529) @njuettner +- Bump github.com/akamai/AkamaiOPEN-edgegrid-golang from 0.9.10 to 0.9.11 (#1528) @njuettner +- Fix RFC2316 Windows Documentation (#1516) @scottd018 +- remove dependency on kubernetes/kubernetes (#1513) @tariq1890 +- update akamai openapi dependency (#1511) @tariq1890 +- Vultr Provider (#1509) @ddymko +- Add AWS region ap-east-1(HK) (#1497) @lovemai073 +- Fix: file coredns.go is not `goimports`-ed (#1496) @njuettner +- Allow ZoneIDFilter for Cloudflare (#1494) @james-callahan +- update etcd dependency to latest version (#1485) @tariq1890 +- Support for openshift routes (#1484) @jgrumboe +- add --txt-suffix feature (#1483) @jgrumboe +- update to go 1.14 (#1476) @jochen42 +- Multiple A records support for the same FQDN (#1475) @ytsarev +- Implement annotation filter for CRD source (#1399) @ytsarev + ## v0.7.1 - 2020-04-01 - Prometheus metric: timestamp of last successful sync with the DNS provider (#1480) @njuettner diff --git a/Dockerfile.mini b/Dockerfile.mini index 1fa4ac370..8ce138567 100644 --- a/Dockerfile.mini +++ b/Dockerfile.mini @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.13 as builder +FROM golang:1.14 as builder WORKDIR /sigs.k8s.io/external-dns diff --git a/Makefile b/Makefile index 47fa74ae4..c927ee71a 100644 --- a/Makefile +++ b/Makefile @@ -31,14 +31,14 @@ cover-html: cover # Run all the linters lint: - golangci-lint run --timeout=5m ./... + golangci-lint run --timeout=15m ./... # The verify target runs tasks similar to the CI tasks, but without code coverage .PHONY: verify test test: - go test -v -race $(shell go list ./... | grep -v /vendor/) + go test -race ./... # The build targets allow to build the binary and docker image .PHONY: build build.docker build.mini diff --git a/README.md b/README.md index 9aa2553e8..12fb20411 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ In a broader sense, ExternalDNS allows you to control DNS records dynamically vi The [FAQ](docs/faq.md) contains additional information and addresses several questions about key concepts of ExternalDNS. -To see ExternalDNS in action, have a look at this [video](https://www.youtube.com/watch?v=9HQ2XgL9YVI) or read this [blogpost](https://medium.com/wearetheledger/deploying-test-environments-with-azure-devops-eks-and-externaldns-67abe647e4e). +To see ExternalDNS in action, have a look at this [video](https://www.youtube.com/watch?v=9HQ2XgL9YVI) or read this [blogpost](https://codemine.be/posts/20190125-devops-eks-externaldns/). ## The Latest Release: v0.7 diff --git a/controller/controller.go b/controller/controller.go index cc4feefb7..2f2c7181b 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + "sync" "time" "github.com/prometheus/client_golang/prometheus" @@ -112,6 +113,10 @@ type Controller struct { Interval time.Duration // The DomainFilter defines which DNS records to keep or exclude DomainFilter endpoint.DomainFilter + // The nextRunAt used for throttling and batching reconciliation + nextRunAt time.Time + // The nextRunAtMux is for atomic updating of nextRunAt + nextRunAtMux sync.Mutex } // RunOnce runs a single iteration of a reconciliation loop. @@ -135,10 +140,11 @@ func (c *Controller) RunOnce(ctx context.Context) error { sourceEndpointsTotal.Set(float64(len(endpoints))) plan := &plan.Plan{ - Policies: []plan.Policy{c.Policy}, - Current: records, - Desired: endpoints, - DomainFilter: c.DomainFilter, + Policies: []plan.Policy{c.Policy}, + Current: records, + Desired: endpoints, + DomainFilter: c.DomainFilter, + PropertyComparator: c.Registry.PropertyValuesEqual, } plan = plan.Calculate() @@ -154,18 +160,39 @@ func (c *Controller) RunOnce(ctx context.Context) error { return nil } -// Run runs RunOnce in a loop with a delay until stopChan receives a value. -func (c *Controller) Run(ctx context.Context, stopChan <-chan struct{}) { - ticker := time.NewTicker(c.Interval) +// MinInterval is used as window for batching events +const MinInterval = 5 * time.Second + +// RunOnceThrottled makes sure execution happens at most once per interval. +func (c *Controller) ScheduleRunOnce(now time.Time) { + c.nextRunAtMux.Lock() + defer c.nextRunAtMux.Unlock() + c.nextRunAt = now.Add(MinInterval) +} + +func (c *Controller) ShouldRunOnce(now time.Time) bool { + c.nextRunAtMux.Lock() + defer c.nextRunAtMux.Unlock() + if now.Before(c.nextRunAt) { + return false + } + c.nextRunAt = now.Add(c.Interval) + return true +} + +// Run runs RunOnce in a loop with a delay until context is canceled +func (c *Controller) Run(ctx context.Context) { + ticker := time.NewTicker(time.Second) defer ticker.Stop() for { - err := c.RunOnce(ctx) - if err != nil { - log.Error(err) + if c.ShouldRunOnce(time.Now()) { + if err := c.RunOnce(ctx); err != nil { + log.Error(err) + } } select { case <-ticker.C: - case <-stopChan: + case <-ctx.Done(): log.Info("Terminating main controller loop") return } diff --git a/controller/controller_test.go b/controller/controller_test.go index 5a7cb7425..22c2b4b15 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -35,6 +35,7 @@ import ( // mockProvider returns mock endpoints and validates changes. type mockProvider struct { + provider.BaseProvider RecordsStore []*endpoint.Endpoint ExpectChanges *plan.Changes } @@ -153,43 +154,41 @@ func TestRunOnce(t *testing.T) { source.AssertExpectations(t) } -// TestSourceEventHandler tests that the Controller can use a Source's registered handler as a callback. -func TestSourceEventHandler(t *testing.T) { - source := new(testutils.MockSource) +func TestShouldRunOnce(t *testing.T) { + ctrl := &Controller{Interval: 10 * time.Minute} - handlerCh := make(chan bool) - timeoutCh := make(chan bool, 1) - stopChan := make(chan struct{}, 1) + now := time.Now() - ctrl := &Controller{ - Source: source, - Registry: nil, - Policy: &plan.SyncPolicy{}, - } + // First run of Run loop should execute RunOnce + assert.True(t, ctrl.ShouldRunOnce(now)) - // Define and register a simple handler that sends a message to a channel to show it was called. - handler := func() error { - handlerCh <- true - return nil - } - // Example of preventing handler from being called more than once every 5 seconds. - ctrl.Source.AddEventHandler(handler, stopChan, 5*time.Second) + // Second run should not + assert.False(t, ctrl.ShouldRunOnce(now)) - // Send timeout message after 10 seconds to fail test if handler is not called. - go func() { - time.Sleep(10 * time.Second) - timeoutCh <- true - }() + now = now.Add(10 * time.Second) + // Changes happen in ingresses or services + ctrl.ScheduleRunOnce(now) + ctrl.ScheduleRunOnce(now) - // Wait until we either receive a message from handlerCh or timeoutCh channel after 10 seconds. - select { - case msg := <-handlerCh: - assert.True(t, msg) - case <-timeoutCh: - assert.Fail(t, "timed out waiting for event handler to be called") - } + // Because we batch changes, ShouldRunOnce returns False at first + assert.False(t, ctrl.ShouldRunOnce(now)) + assert.False(t, ctrl.ShouldRunOnce(now.Add(100*time.Microsecond))) - close(stopChan) - close(handlerCh) - close(timeoutCh) + // But after MinInterval we should run reconciliation + now = now.Add(MinInterval) + assert.True(t, ctrl.ShouldRunOnce(now)) + + // But just one time + assert.False(t, ctrl.ShouldRunOnce(now)) + + // We should wait maximum possible time after last reconciliation started + now = now.Add(10*time.Minute - time.Second) + assert.False(t, ctrl.ShouldRunOnce(now)) + + // After exactly Interval it's OK again to reconcile + now = now.Add(time.Second) + assert.True(t, ctrl.ShouldRunOnce(now)) + + // But not two times + assert.False(t, ctrl.ShouldRunOnce(now)) } diff --git a/docs/ttl.md b/docs/ttl.md index cfab65baf..c6447007d 100644 --- a/docs/ttl.md +++ b/docs/ttl.md @@ -51,7 +51,7 @@ PRs welcome! Notes ===== -When the `external-dns.alpha.kubernetes.io/ttl` annotation is not provided, the TTL will default to 0 seconds and `enpoint.TTL.isConfigured()` will be false. +When the `external-dns.alpha.kubernetes.io/ttl` annotation is not provided, the TTL will default to 0 seconds and `endpoint.TTL.isConfigured()` will be false. ### AWS Provider The AWS Provider overrides the value to 300s when the TTL is 0. diff --git a/docs/tutorials/digitalocean.md b/docs/tutorials/digitalocean.md index c23fe9878..2a316a390 100644 --- a/docs/tutorials/digitalocean.md +++ b/docs/tutorials/digitalocean.md @@ -36,9 +36,6 @@ spec: app: external-dns strategy: type: Recreate - selector: - matchLabels: - app: external-dns template: metadata: labels: @@ -102,9 +99,6 @@ spec: app: external-dns strategy: type: Recreate - selector: - matchLabels: - app: external-dns template: metadata: labels: @@ -195,3 +189,13 @@ Now that we have verified that ExternalDNS will automatically manage DigitalOcea $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml ``` + +## Advanced Usage + +### API Page Size + +If you have a large number of domains and/or records within a domain, you may encounter API +rate limiting because of the number of API calls that external-dns must make to the DigitalOcean API to retrieve +the current DNS configuration during every reconciliation loop. If this is the case, use the +`--digitalocean-api-page-size` option to increase the size of the pages used when querying the DigitalOcean API. +(Note: external-dns uses a default of 50.) diff --git a/docs/tutorials/hostport.md b/docs/tutorials/hostport.md index 7568b36b4..bba509530 100644 --- a/docs/tutorials/hostport.md +++ b/docs/tutorials/hostport.md @@ -9,7 +9,7 @@ The main use cases that inspired this feature is the necessity for fixed address We will go through a small example of deploying a simple Kafka with use of a headless service. -### Exernal DNS +### External DNS A simple deploy could look like this: ### Manifest (for clusters without RBAC enabled) @@ -17,7 +17,7 @@ A simple deploy could look like this: apiVersion: apps/v1 kind: Deployment metadata: - name: exeternal-dns + name: external-dns spec: strategy: type: Recreate @@ -81,7 +81,7 @@ subjects: apiVersion: apps/v1 kind: Deployment metadata: - name: exeternal-dns + name: external-dns spec: strategy: type: Recreate @@ -111,7 +111,7 @@ spec: ### Kafka Stateful Set -First lets deploy a Kafka Stateful set, a simple example(a lot of stuff is missing) with a headless service called `kafka-hsvc` +First lets deploy a Kafka Stateful set, a simple example(a lot of stuff is missing) with a headless service called `ksvc` ```yaml apiVersion: apps/v1beta1 @@ -155,7 +155,7 @@ spec: requests: storage: 500Gi ``` -Very important here, is to set the `hostport`(only works if the PodSecurityPolicy allows it)! and in case your app requires an actual hostname inside the container, unlike Kafka, which can advertise on another address, you have to set the hostname yourself. +Very important here, is to set the `hostPort`(only works if the PodSecurityPolicy allows it)! and in case your app requires an actual hostname inside the container, unlike Kafka, which can advertise on another address, you have to set the hostname yourself. ### Headless Service diff --git a/docs/tutorials/istio.md b/docs/tutorials/istio.md index d397709af..4f9495f9b 100644 --- a/docs/tutorials/istio.md +++ b/docs/tutorials/istio.md @@ -6,7 +6,7 @@ It is meant to supplement the other provider-specific setup tutorials. * Manifest (for clusters without RBAC enabled) * Manifest (for clusters with RBAC enabled) -* Update existing Existing DNS Deployment +* Update existing ExternalDNS Deployment ### Manifest (for clusters without RBAC enabled) @@ -48,7 +48,7 @@ kind: ServiceAccount metadata: name: external-dns --- -apiVersion: rbac.authorization.k8s.io/v1beta1 +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns @@ -66,7 +66,7 @@ rules: resources: ["gateways"] verbs: ["get","watch","list"] --- -apiVersion: rbac.authorization.k8s.io/v1beta1 +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer @@ -110,7 +110,7 @@ spec: - --txt-owner-id=my-identifier ``` -### Update existing Existing DNS Deployment +### Update existing ExternalDNS Deployment * For clusters with running `external-dns`, you can just update the deployment. * With access to the `kube-system` namespace, update the existing `external-dns` deployment. @@ -130,7 +130,7 @@ kubectl patch clusterrole external-dns --type='json' \ -p='[{"op": "add", "path": "/rules/4", "value": { "apiGroups": [ "networking.istio.io"], "resources": ["gateways"],"verbs": ["get", "watch", "list" ]} }]' ``` -### Verify External DNS works (Gateway example) +### Verify ExternalDNS works (Gateway example) Follow the [Istio ingress traffic tutorial](https://istio.io/docs/tasks/traffic-management/ingress/) to deploy a sample service that will be exposed outside of the service mesh. @@ -217,7 +217,7 @@ transfer-encoding: chunked **Note:** The `-H` flag in the original Istio tutorial is no longer necessary in the `curl` commands. -### Debug External-DNS +### Debug ExternalDNS * Look for the deployment pod to see the status @@ -239,8 +239,8 @@ At this point, you can `create` or `update` any `Istio Gateway` object with `hos ```console time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.com A" -time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.comm TXT" -time="2020-01-17T06:08:08Z" level=info msg="2 record(s) in zone example.comm. were successfully updated" +time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.com TXT" +time="2020-01-17T06:08:08Z" level=info msg="2 record(s) in zone example.com. were successfully updated" time="2020-01-17T06:09:08Z" level=info msg="All records are already up to date, there are no changes for the matching hosted zones" ``` diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 3c0187935..cc3183d73 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -159,6 +159,7 @@ func NewEndpointWithTTL(dnsName, recordType string, ttl TTL, targets ...string) } } +// WithSetIdentifier applies the given set identifier to the endpoint. func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint { e.SetIdentifier = setIdentifier return e diff --git a/go.mod b/go.mod index d6f616d89..27a1e361c 100644 --- a/go.mod +++ b/go.mod @@ -3,46 +3,46 @@ module sigs.k8s.io/external-dns go 1.14 require ( - cloud.google.com/go v0.44.3 + cloud.google.com/go v0.50.0 github.com/Azure/azure-sdk-for-go v36.0.0+incompatible - github.com/Azure/go-autorest/autorest v0.9.0 - github.com/Azure/go-autorest/autorest/adal v0.6.0 + github.com/Azure/go-autorest/autorest v0.9.4 + github.com/Azure/go-autorest/autorest/adal v0.8.3 github.com/Azure/go-autorest/autorest/azure/auth v0.0.0-00010101000000-000000000000 github.com/Azure/go-autorest/autorest/to v0.3.0 github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11 github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect github.com/alecthomas/colour v0.1.0 // indirect github.com/alecthomas/kingpin v2.2.5+incompatible - github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect + github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f - github.com/aws/aws-sdk-go v1.27.4 + github.com/aws/aws-sdk-go v1.31.4 github.com/cloudflare/cloudflare-go v0.10.1 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 - github.com/denverdino/aliyungo v0.0.0-20180815121905-69560d9530f5 + github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba github.com/digitalocean/godo v1.34.0 - github.com/dnaeon/go-vcr v1.0.1 // indirect - github.com/dnsimple/dnsimple-go v0.14.0 + github.com/dnsimple/dnsimple-go v0.60.0 github.com/exoscale/egoscale v0.18.1 github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 - github.com/go-resty/resty v1.8.0 // indirect github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b // indirect github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f github.com/gophercloud/gophercloud v0.1.0 github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/heptio/contour v0.15.0 + github.com/gorilla/mux v1.7.4 // indirect github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65 github.com/linki/instrumented_http v0.2.0 - github.com/linode/linodego v0.3.0 - github.com/mattn/go-isatty v0.0.11 // indirect + github.com/linode/linodego v0.15.0 + github.com/maxatome/go-testdeep v1.4.0 github.com/miekg/dns v1.1.25 github.com/nesv/go-dynect v0.6.0 github.com/nic-at/rc0go v1.1.0 - github.com/openshift/api v0.0.0-20190322043348-8741ff068a47 - github.com/openshift/client-go v3.9.0+incompatible + github.com/openshift/api v0.0.0-20200302134843-001335d6cc34 + github.com/openshift/client-go v0.0.0-20200116145930-eb24d03d8420 github.com/oracle/oci-go-sdk v1.8.0 github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.0.0 + github.com/projectcontour/contour v1.4.0 + github.com/prometheus/client_golang v1.1.0 github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0 github.com/sergi/go-diff v1.1.0 // indirect github.com/sirupsen/logrus v1.6.0 @@ -50,22 +50,27 @@ require ( github.com/smartystreets/gunit v1.1.1 // indirect github.com/stretchr/testify v1.4.0 github.com/terra-farm/udnssdk v1.3.5 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + github.com/sirupsen/logrus v1.4.2 + github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect + github.com/smartystreets/gunit v1.3.4 // indirect + github.com/stretchr/testify v1.5.1 github.com/transip/gotransip v5.8.2+incompatible github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92 github.com/vultr/govultr v0.3.2 go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875 - golang.org/x/net v0.0.0-20190923162816-aa69164e4478 + golang.org/x/net v0.0.0-20200202094626-16171245cfb2 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 - google.golang.org/api v0.9.0 + google.golang.org/api v0.15.0 gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1 - gopkg.in/yaml.v2 v2.2.5 - istio.io/api v0.0.0-20190820204432-483f2547d882 - istio.io/istio v0.0.0-20190322063008-2b1331886076 - k8s.io/api v0.0.0-20190620084959-7cf5895f2711 - k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719 - k8s.io/client-go v10.0.0+incompatible - k8s.io/klog v0.3.1 + gopkg.in/yaml.v2 v2.2.8 + istio.io/api v0.0.0-20200324230725-4b064f75ad8f + istio.io/client-go v0.0.0-20200324231043-96a582576da1 + k8s.io/api v0.17.5 + k8s.io/apimachinery v0.17.5 + k8s.io/client-go v0.17.5 + k8s.io/klog v1.0.0 ) replace ( @@ -74,11 +79,5 @@ replace ( github.com/Azure/go-autorest/autorest/adal => github.com/Azure/go-autorest/autorest/adal v0.6.0 github.com/Azure/go-autorest/autorest/azure/auth => github.com/Azure/go-autorest/autorest/azure/auth v0.3.0 github.com/golang/glog => github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d - istio.io/api => istio.io/api v0.0.0-20190820204432-483f2547d882 - istio.io/istio => istio.io/istio v0.0.0-20190911205955-c2bd59595ce6 - k8s.io/api => k8s.io/api v0.0.0-20190817221950-ebce17126a01 - k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190919022157-e8460a76b3ad - k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190817221809-bf4de9df677c - k8s.io/client-go => k8s.io/client-go v0.0.0-20190817222206-ee6c071a42cf k8s.io/klog => github.com/mikkeloscar/knolog v0.0.0-20190326191552-80742771eb6b ) diff --git a/go.sum b/go.sum index 1fed7d8db..dd55699d7 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,22 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.3 h1:0sMegbmn/8uTwpNkB0q9cLEpZ2W5a6kl+wtBQgPWBJQ= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0 h1:0E3eE8MX426vUOs7aHfI7aN1BrIzzzf4ccKCSfSjGmc= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/logging v1.0.0/go.mod h1:V1cc3ogwobYzQq5f2R7DS/GvRIrI4FKj01Gs5glwAls= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk= code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI= -contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= -contrib.go.opencensus.io/exporter/prometheus v0.1.0/go.mod h1:cGFniUXGZlKRjzOyuZJ6mgB+PgBcCIa79kEKR8YCW+A= -contrib.go.opencensus.io/exporter/stackdriver v0.6.0/go.mod h1:QeFzMJDAw8TXt5+aRaSuE8l5BwaMIOIlaVkBOPRuMuw= -contrib.go.opencensus.io/exporter/zipkin v0.1.1/go.mod h1:GMvdSl3eJ2gapOaLKzTKE3qDgUkJ86k9k3yY2eqwkzc= -fortio.org/fortio v1.3.0/go.mod h1:Go0fRqoPJ1xy5JOWcS23jyF58byVZxFyEePYsGmCR0k= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v36.0.0+incompatible h1:XIaBmA4pgKqQ7jInQPaNJQ4pOHrdJjw9gYXhbyiChaU= github.com/Azure/azure-sdk-for-go v36.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-autorest v13.0.1+incompatible h1:wRg6hB3T3dp7qjj5v3NmVsdU9IyXodW+SQnN9xlpGEA= -github.com/Azure/go-autorest v13.0.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.1 h1:JB7Mqhna/7J8gZfVHjxDSTLSD6ciz2YgSMb/4qLXTtY= github.com/Azure/go-autorest/autorest v0.9.1/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest/adal v0.6.0 h1:UCTq22yE3RPgbU/8u4scfnnzuCW6pwQ9n+uBtV78ouo= @@ -42,23 +39,16 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= -github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/sprig v0.0.0-20190301161902-9f8fceff796f/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig v2.14.1+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/SAP/go-hdb v0.14.1/go.mod h1:7fdQLVC2lER3urZLjZCm0AuMQfApof92n3aylBPEkMo= -github.com/SermoDigital/jose v0.9.1/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/ahmetb/gen-crd-api-reference-docs v0.1.5 h1:OU+AFpBEhyclrQGx4I6zpCx5WvXiKqvFeeOASOmhKCY= +github.com/ahmetb/gen-crd-api-reference-docs v0.1.5/go.mod h1:P/XzJ+c2+khJKNKABcm2biRwk2QAuwbLf8DlXuaL7WM= github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11 h1:QGjNHMwoPYxE5NpOAc8kpd2KTY293/oFk5BWdjkza+k= github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11/go.mod h1:L+HB2uBoDgi3+r1pJEJcbGwyyHhd2QXaGsKLbDwtm8Q= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= @@ -67,28 +57,23 @@ github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrD github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kingpin v2.2.5+incompatible h1:umWl1NNd72+ZvRti3T9C0SYean2hPZ7ZhxU8bsgc9BQ= github.com/alecthomas/kingpin v2.2.5+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= -github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 h1:GDQdwm/gAcJcLAKQQZGOJ4knlw+7rfEQQcmwTbt4p5E= -github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c h1:MVVbswUlqicyj8P/JljoocA7AyCo62gzD0O7jfvrhtE= +github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis v0.0.0-20180201100744-9d52b1fc8da9/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f h1:hinXH9rcBjRoIih5tl4f1BCbNjOmPJ2UnZwcYDhEHR0= github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA= -github.com/antlr/antlr4 v0.0.0-20190223165740-dade65a895c2/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y= -github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/appscode/jsonpatch v0.0.0-20190108182946-7c0e3b262f30/go.mod h1:4AJxUpXUhv4N+ziTvIcWWXgeorXpxPZOfk9HdEVr96M= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.13.24/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= -github.com/aws/aws-sdk-go v1.27.4 h1:pfzAQZn2B4OFFSG9YHGwfCANZICW6LGSRToP6fsWotI= -github.com/aws/aws-sdk-go v1.27.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.31.4 h1:YZ0uEYIWeanGuAomElHmRWMAbXVqrQixxgf2vtIjO6M= +github.com/aws/aws-sdk-go v1.31.4/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -102,43 +87,44 @@ github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.10.1 h1:d2CL6F9k2O0Ux0w27LgogJ5UOzZRj6a/hDPFqPP68d8= github.com/cloudflare/cloudflare-go v0.10.1/go.mod h1:C0Y6eWnTJPMK2ceuOxx2pjh78UUHihcXeTTHb8r7QjU= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14= +github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533 h1:8wZizuKuZVu5COB7EsBYxBQz8nRcXXn5d4Gt91eJLvU= +github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-oidc v0.0.0-20180117170138-065b426bd416/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 h1:3jFq2xL4ZajGK4aZY8jz+DAF0FHjI51BXjjSwCzS1Dk= -github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/prometheus-operator v0.29.0/go.mod h1:SO+r5yZUacDFPKHfPoUjI3hMsH+ZUdiuNNhuSq3WoSg= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea h1:n2Ltr3SrfQlf/9nOna1DoGKxLx3qTSI8Ttl6Xrqp6mw= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U= -github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= -github.com/dchest/siphash v1.1.0/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= -github.com/denisenkom/go-mssqldb v0.0.0-20190423183735-731ef375ac02/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= -github.com/denverdino/aliyungo v0.0.0-20180815121905-69560d9530f5 h1:YjnQWGUNtqeKqndapy9V1BzlfMwc/dBJf2MU9dmuXSQ= -github.com/denverdino/aliyungo v0.0.0-20180815121905-69560d9530f5/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/digitalocean/godo v1.34.0 h1:OXJhLLJS2VTB5SziTyCq8valKVZ0uBHCFQsDW3/HF78= @@ -147,118 +133,136 @@ github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TR github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/dnsimple/dnsimple-go v0.14.0 h1:JGYtVVA/uHc91q0LjDWqR1oVj6EGu9Kn0lMRxjH/w30= -github.com/dnsimple/dnsimple-go v0.14.0/go.mod h1:0FYu4qVNv/UcfZPNwa9zi68IkggJu3TIwM54D7rhmI4= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v0.0.0-20180612054059-a9fbbdc8dd87/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/dropbox/godropbox v0.0.0-20190501155911-5749d3b71cbe/go.mod h1:glr97hP/JuXb+WMYCizc4PIFuzw1lCR97mwbe1VVXhQ= -github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= -github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= -github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74/go.mod h1:UqXY1lYT/ERa4OEAywUqdok1T4RCRdArkhic1Opuavo= +github.com/dnsimple/dnsimple-go v0.60.0 h1:N+q+ML1CZGf+5r4udu9Opy7WJNtOaFT9aM86Af9gLhk= +github.com/dnsimple/dnsimple-go v0.60.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= -github.com/elazarl/goproxy v0.0.0-20190630181448-f1e96bc0f4c5/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/elazarl/goproxy/ext v0.0.0-20190630181448-f1e96bc0f4c5/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= -github.com/emicklei/go-restful v2.9.3+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.8.2/go.mod h1:EWRTAFN6uuDZIa6KOuUfrOMJ7ySgXZ44rVKiTWjKe34= -github.com/envoyproxy/go-control-plane v0.8.7-0.20190821215049-f062b07a671a h1:SaBXBWjRmig9yVB49C6TcbDtbZTbhuFLod7YiGjuFxQ= -github.com/envoyproxy/go-control-plane v0.8.7-0.20190821215049-f062b07a671a/go.mod h1:XB9+ce7x+IrsjgIVnRnql0O61gj/np0/bGDfhJI3sCU= -github.com/envoyproxy/protoc-gen-validate v0.0.0-20190405222122-d6164de49109/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0 h1:67WMNTvGrl7V1dWdKCeTwxDr7nio9clKoTlLhwIPnT4= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.5 h1:lRJIqDD8yjV1YyPRqecMdytjDLs2fTXq363aCib5xPU= +github.com/envoyproxy/go-control-plane v0.9.5/go.mod h1:OXl5to++W0ctG+EHWTFUjiypVxC/Y4VLc/KFU+al13s= github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.0.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exoscale/egoscale v0.18.1 h1:1FNZVk8jHUx0AvWhOZxLEDNlacTU0chMXUUNkm9EZaI= github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= -github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= -github.com/facebookgo/stackerr v0.0.0-20150612192056-c2fcf88613f4/go.mod h1:SBHk9aNQtiw4R4bEuzHjVmZikkUKCnO1v3lPQ21HZGk= -github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 h1:jmwW6QWvUO2OPe22YfgFvBaaZlSr8Rlrac5lZvG6IdM= github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99/go.mod h1:4mP9w9+vYGw2jUx2+2v03IA+phyQQjNRR4AL3uxlNrs= -github.com/fluent/fluent-logger-golang v1.3.0/go.mod h1:2/HCT/jTy78yGyeNGQLGQsjF3zzzAuy6Xlk6FCMV5eU= -github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-ini/ini v1.33.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/zapr v0.1.1/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= +github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.19.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.19.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-redis/redis v6.10.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-resty/resty v1.8.0 h1:vbNCxbHOWCototzwxf3L63PQCKx6xgT6v8SHfoqkp6U= -github.com/go-resty/resty v1.8.0/go.mod h1:n37daLLGIHq2FFYHxg+FYQiwA95FpfNI+A9uxoIYGRk= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +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-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.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gocql/gocql v0.0.0-20190423091413-b99afaf3b163/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= +github.com/gobuffalo/flect v0.1.5 h1:xpKq9ap8MbYfhuPCF0dBH854Gp9CxZjr/IocxELFflo= +github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48 h1:X+zN6RZXsvnrSJaAIQhZezPfAfvsqihKKR8oiLHid34= -github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f h1:kSqKc8ouCLIBHqdj9a9xxhtxlZhNqbePClixA4HoM44= github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:YCHYtYb9c8Q7XgYVYjmJBPtFPKx5QvOcPxHZWjldabE= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/cel-go v0.2.0/go.mod h1:fTCVOuSN/Vn6d49zvRpr3fDAKFyfpLViE0gU+9Vtm7g= -github.com/google/cel-spec v0.2.0/go.mod h1:MjQm800JAGhOZXI7vatnVpmIaFTR6L8FHcKk+piiKpI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-github v15.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -268,119 +272,79 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= -github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/gophercloud/gophercloud v0.0.0-20190424031112-b9b92a825806/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= +github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= -github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= -github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJlb8Kqsd41CTE= -github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20171214222146-0e7658f8ee99/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hashicorp/consul v1.3.0/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= -github.com/hashicorp/go-hclog v0.9.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-memdb v1.0.1/go.mod h1:I6dKdmYhZqU0RJSheVEWgTNWdVQH5QvTgIUQ0t/t32M= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-plugin v1.0.0/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.6.4 h1:BbgctKO892xEyOXnGiaAwIoSq1QZ/SS4AhjoAh9DnfY= github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.1/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= -github.com/hashicorp/vault v0.10.0/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/heptio/contour v0.15.0 h1:JhyJMauwoYtqTzd23jStx7Bx7S3sHbqTWZs6+Ed+UPE= -github.com/heptio/contour v0.15.0/go.mod h1:y4LmuX+86v8mlRd1HVrb2u4t77jMjOQ3DnjfRCiwrfA= -github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= -github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65 h1:FP5rOFP4ifbtFIjFHJmwhFrsbDyONILK/FNntl/Pou8= github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI= -github.com/jefferai/jsonx v1.0.0/go.mod h1:OGmqmi2tTeI/PS+qQfBDToLHHJIy/RMp24fPo8vFvoQ= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jteeuwen/go-bindata v0.0.0-20180305030458-6025e8de665b/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/errors v0.0.0-20190207033735-e65537c515d7/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= -github.com/juju/loggo v0.0.0-20190212223446-d976af380377/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= -github.com/juju/testing v0.0.0-20190429233213-dfc56b8c09fc/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/keybase/go-crypto v0.0.0-20190416182011-b785b22cc757/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -390,159 +354,145 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d h1:JV46OtdhH2vVt8mJ1EWUE94k99vbN9fZs1WQ8kcEapU= github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d/go.mod h1:CHQ3o5KBH1PIS2Fb1mRLTIWO5YzP9kSUB3KoCICwlvA= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/linki/instrumented_http v0.2.0 h1:zLhcB3Q/McQQqml3qd5kzdZ0cGnL3vquPFIW2338f5Y= github.com/linki/instrumented_http v0.2.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk= -github.com/linode/linodego v0.3.0 h1:I83pEPg4owSy5pCPaKix7xkGbWIjPxmAoc/Yu5OYDDY= -github.com/linode/linodego v0.3.0/go.mod h1:ga11n3ivecUrPCHN0rANxKmfWBJVkOXfLMZinAbj2sY= -github.com/lyft/protoc-gen-star v0.4.10/go.mod h1:mE8fbna26u7aEA2QCVvvfBU/ZrPgocG1206xAFPcs94= +github.com/linode/linodego v0.15.0 h1:hIPphfUvQlheBEV2YbTQQ1KUPE5LPe0EDHvoySwuiu4= +github.com/linode/linodego v0.15.0/go.mod h1:vlzb2glsL9XrRYTRJ5JrgUoKZ5yfZBe11GYfEB68McY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mdempsky/unconvert v0.0.0-20190325185700-2f5dc3378ed3/go.mod h1:9+3Wp2ccIz73BJqVfc7n2+1A+mzvnEwtDTqEjeRngBQ= -github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= -github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/maxatome/go-testdeep v1.4.0 h1:vKQh3/lHKAMsxggya/fXB6fLbf70c7k6wlLveuS9sKE= +github.com/maxatome/go-testdeep v1.4.0/go.mod h1:011SgQ6efzZYAen6fDn4BqQ+lUR72ysdyKe7Dyogw70= github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg= github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/mikkeloscar/knolog v0.0.0-20190326191552-80742771eb6b h1:5f5B1kp+QerGOF91q1qVJcUWWvXsVEN3OKiyEzAAjIM= github.com/mikkeloscar/knolog v0.0.0-20190326191552-80742771eb6b/go.mod h1:PizLs/1ddmVrXpFgWOGNmTJ2YHSWUkpUXMYuUkTo3Go= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= -github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= -github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nesv/go-dynect v0.6.0 h1:Ow/DiSm4LAISwnFku/FITSQHnU6pBvhQMsUE5Gu6Oq4= github.com/nesv/go-dynect v0.6.0/go.mod h1:GHRBRKzTwjAMhosHJQq/KrZaFkXIFyJ5zRE7thGXXrs= github.com/nic-at/rc0go v1.1.0 h1:k6/Bru/npTjmCSFw65ulYRw/b3ycIS30t6/YM4r42V4= github.com/nic-at/rc0go v1.1.0/go.mod h1:KEa3H5fmDNXCaXSqOeAZxkKnG/8ggr1OHIG25Ve7fjU= -github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.4.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/open-policy-agent/opa v0.8.2/go.mod h1:rlfeSeHuZmMEpmrcGla42AjkOUjP4rGIpS96H12un3o= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/openshift/api v0.0.0-20190322043348-8741ff068a47 h1:PAlaAXvwmPxgh8gm0/eVmNMGLeJ1bURwyKvJVLnsr6s= -github.com/openshift/api v0.0.0-20190322043348-8741ff068a47/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= -github.com/openshift/client-go v3.9.0+incompatible h1:13k3Ok0B7TA2hA3bQW2aFqn6y04JaJWdk7ITTyg+Ek0= -github.com/openshift/client-go v3.9.0+incompatible/go.mod h1:6rzn+JTr7+WYS2E1TExP4gByoABxMznR6y2SnUIkmxk= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/operator-framework/operator-sdk v0.7.0/go.mod h1:iVyukRkam5JZa8AnjYf+/G3rk7JI1+M6GsU0sq0B9NA= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/openshift/api v0.0.0-20200116145750-0e2ff1e215dd/go.mod h1:fT6U/JfG8uZzemTRwZA2kBDJP5nWz7v05UHnty/D+pk= +github.com/openshift/api v0.0.0-20200302134843-001335d6cc34 h1:dqL8/YLrv/n4E5JBBkYPU/jzLQLLP5YZpsKAfh8CtNI= +github.com/openshift/api v0.0.0-20200302134843-001335d6cc34/go.mod h1:frTMT4l3rOMlXj3ClYgKxgkq24D7IKXb3Bl4vJEewJw= +github.com/openshift/build-machinery-go v0.0.0-20200211121458-5e3d6e570160/go.mod h1:1CkcsT3aVebzRBzVTSbiKSkJMsC/CASqxesfqEMfJEc= +github.com/openshift/client-go v0.0.0-20200116145930-eb24d03d8420 h1:+0HMnbsn4odRTirQB5ImG2w13yH6vj1MI2WD0+wPDaI= +github.com/openshift/client-go v0.0.0-20200116145930-eb24d03d8420/go.mod h1:4riOwdj99Hd/q+iAcJZfNCsQQQMwURnZV6RL4WHYS5w= github.com/oracle/oci-go-sdk v1.8.0 h1:4SO45bKV0I3/Mn1os3ANDZmV0eSE5z5CLdSUIkxtyzs= github.com/oracle/oci-go-sdk v1.8.0/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= -github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014 h1:37VE5TYj2m/FLA9SNr4z0+A0JefvTmR60Zwf8XSEV7c= github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.2.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/cachecontrol v0.0.0-20180306154005-525d0eb5f91d/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/projectcontour/contour v1.4.0 h1:VJPABZM92FN2rT8hvYaYr0Js5xgnuSTzrfcC9+RAHdc= +github.com/projectcontour/contour v1.4.0/go.mod h1:XBfsFUhiGCVFWNUF4g0O0eBWY+EI97iEEj6FB7584EE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/prom2json v1.1.0/go.mod h1:v7OY1795b9fEUZgq4UU2+15YjRv0LfpxKejIQCy3L7o= github.com/prometheus/prom2json v1.2.1/go.mod h1:yIcXOj/TLPdtZ12qRyhswPnu+02sfDoqatDjj0WGSvo= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/ryanuber/go-glob v0.0.0-20160226084822-572520ed46db/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0 h1:vOcHdR1nu7DO4BAx1rwzdHV7jQTzW3gqcBT5qxHSc6A= github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0/go.mod h1:FeplEtXXejBYC4NPAFTrs5L7KuK+5RL9bf5nB2vZe9o= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sethgrid/pester v0.0.0-20180227223404-ed9870dad317/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns= -github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= -github.com/signalfx/com_signalfx_metrics_protobuf v0.0.0-20170330202426-93e507b42f43/go.mod h1:muYA2clvwCdj7nzAJ5vJIXYpJsUumhAl4Uu1wUNpWzA= -github.com/signalfx/gohistogram v0.0.0-20160107210732-1ccfd2ff5083/go.mod h1:adPDS6s7WaajdFBV9mQ7i0dKfQ8xiDnF9ZNETVPpp7c= -github.com/signalfx/golib v1.1.6/go.mod h1:nWYefOwlUKWm/SpN/LgVSBnyH1T9NpT1ANlmgRIi1Cs= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= @@ -550,17 +500,14 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= -github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/gunit v1.1.1 h1:lr7g9LZl13zx9rAp5KcY78CjRdxLSHIzNJ6XMoHRbyo= -github.com/smartystreets/gunit v1.1.1/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= +github.com/smartystreets/gunit v1.3.4 h1:iHc8Rfhb/uCOc9a3KGuD3ut22L+hLIVaqR1o5fS6zC4= +github.com/smartystreets/gunit v1.3.4/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+zMHZXV9/bvak= +github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -568,14 +515,22 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= @@ -584,14 +539,12 @@ github.com/technosophos/moniker v0.0.0-20180509230615-a5dbd03a2245/go.mod h1:O1c github.com/terra-farm/udnssdk v1.3.5 h1:MNR3adfuuEK/l04+jzo8WW/0fnorY+nW515qb3vEr6I= github.com/terra-farm/udnssdk v1.3.5/go.mod h1:8RnM56yZTR7mYyUIvrDgXzdRaEyFIzqdEi7+um26Sv8= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/transip/gotransip v5.8.2+incompatible h1:aNJhw/w/3QBqFcHAIPz1ytoK5FexeMzbUCGrrhWr3H0= github.com/transip/gotransip v5.8.2+incompatible/go.mod h1:uacMoJVmrfOcscM4Bi5NVg708b7c6rz2oDTWqa7i2Ic= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/uber/jaeger-client-go v0.0.0-20190228190846-ecf2d03a9e80/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v2.0.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 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= @@ -605,61 +558,71 @@ github.com/vultr/govultr v0.3.2/go.mod h1:81RwK1wAmb08alkFDJiZmu9gdv+IO+UamzaF0+ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yashtewari/glob-intersection v0.0.0-20180206001645-7af743e8ec84/go.mod h1:HptNXiXVDcJjXe9SqMd0v2FsL9f8dz4GnXgltU6q/co= -github.com/yl2chen/cidranger v0.0.0-20180214081945-928b519e5268 h1:lkoOjizoHqOcEFsvYGE5c8Ykdijjnd0R3r1yDYHzLno= -github.com/yl2chen/cidranger v0.0.0-20180214081945-928b519e5268/go.mod h1:mq0zhomp/G6rRTb0dvHWXRHr/2+Qgeq5hMXfJ670+i4= -github.com/yuin/gopher-lua v0.0.0-20180316054350-84ea3a3c79b3/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875 h1:C7kWARE8r64ppRadl40yfNo6pag+G6ocvGU2xZ6yNes= go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -667,16 +630,27 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -689,38 +663,41 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190508220229-2d0786266e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII= +golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -728,63 +705,83 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 h1:xQwXv67TxFo9nC1GJFyab5eq/5B590r6RlnL/G8Sz7w= +golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190325161752-5a8dccf5b48a/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -golang.org/x/tools v0.0.0-20190802220118-1d1727260058/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -golang.org/x/tools v0.0.0-20190822000311-fc82fb2afd64/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190929041059-e7abfedfabcf h1:NvypsVlesF+lEDKVK5RNkww4fzArJXChZxNin79j05M= +golang.org/x/tools v0.0.0-20190929041059-e7abfedfabcf/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200115044656-831fdb1e1868/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190227213309-4f5b463f9597/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20190916214212-f660b8655731 h1:Phvl0+G5t5k/EUFUi0wPdUUeTL2HydMQUXHnunWgSb0= +google.golang.org/genproto v0.0.0-20190916214212-f660b8655731/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= @@ -792,31 +789,25 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/d4l3k/messagediff.v1 v1.2.1 h1:70AthpjunwzUiarMHyED52mj9UwtAnE89l1Gmrt3EU0= -gopkg.in/d4l3k/messagediff.v1 v1.2.1/go.mod h1:EUzikiKadqXWcD1AzJLagx0j/BeeWGtn++04Xniyg44= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/logfmt.v0 v0.3.0/go.mod h1:mRLMcMLrml5h2Ux/H+4zccFOlVCiRvOvndsolsJoU8Q= -gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1 h1:+fgY/3ngqdBW9oLQCMwL5g+QRkKFPJH05fx2/pipqRQ= gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw= -gopkg.in/ory-am/dockertest.v3 v3.3.4/go.mod h1:s9mmoLkaGeAh97qygnNj4xWkiN7e1SKekYC6CovU+ek= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/stack.v1 v1.7.0/go.mod h1:QtWz4C5wbvhA63ngux3942W/ppRxtyYjHvvhz02s7+M= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -824,47 +815,82 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 h1:XZx7nhd5GMaZpmDaEHFVafUZC7ya0fuo7cSJ3UCKYmM= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -istio.io/api v0.0.0-20190820204432-483f2547d882 h1:L0WC/5HTk8T5eGTg/ka9jGZgw7GMuWj9rm6DFF4owL8= -istio.io/api v0.0.0-20190820204432-483f2547d882/go.mod h1:42cBjnu/rTJcCaKi8nLdIvq0n71RcLrkgZ9IQSvDdSQ= -istio.io/gogo-genproto v0.0.0-20190614210408-e88dc8b0e4db/go.mod h1:eIDJ6jNk/IeJz6ODSksHl5Aiczy5JUq6vFhJWI5OtiI= -istio.io/gogo-genproto v0.0.0-20190731221249-06e20ada0df2/go.mod h1:IjvrbUlRbbw4JCpsgvgihcz9USUwEoNTL/uwMtyV5yk= -istio.io/gogo-genproto v0.0.0-20190819131816-7a8328e41c1a h1:QN+P7SPcjI4az1Lb40MdhJg/OkV03u5XS1DVIk8PS6E= -istio.io/gogo-genproto v0.0.0-20190819131816-7a8328e41c1a/go.mod h1:IjvrbUlRbbw4JCpsgvgihcz9USUwEoNTL/uwMtyV5yk= -istio.io/istio v0.0.0-20190911205955-c2bd59595ce6 h1:SICsYFkNnMRHPPxHYUGMqZp1lQXW8+nJKBSIZVIk16M= -istio.io/istio v0.0.0-20190911205955-c2bd59595ce6/go.mod h1:dn7qqBbC/pAcJnqNvB2jv7llDBbExQVbnPIVuPPScx8= -istio.io/operator v0.0.0-20190830172131-647a5416137b/go.mod h1:NW/+IXU4IeeVtWLS8S57Vq67H/GOy92Temo4eI5efLs= -istio.io/pkg v0.0.0-20190515193414-9332430ad747/go.mod h1:0EkPwmR0tESYjN4Ilq1D52nTBurXaQvny3r2VY4j4tw= -istio.io/pkg v0.0.0-20190731230704-fcbac27d69d5 h1:HcASpvj/fuuABkYH9YbsTGEOT75YHyWvvFnTe229zXs= -istio.io/pkg v0.0.0-20190731230704-fcbac27d69d5/go.mod h1:We4ZQuCbiiNfXge2GfOshBsyDXVwzFwP8703V5DcM14= -k8s.io/api v0.0.0-20190817221950-ebce17126a01 h1:AMUY6ojynTCBURCALg9KVOsrCmWjHF7h7UbuBod7FYc= -k8s.io/api v0.0.0-20190817221950-ebce17126a01/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= -k8s.io/apiextensions-apiserver v0.0.0-20190919022157-e8460a76b3ad h1:I3kcGO+4TazAR49NWgNiEGND6b4q5HVc7Q0K1IQxV9Q= -k8s.io/apiextensions-apiserver v0.0.0-20190919022157-e8460a76b3ad/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE= -k8s.io/apimachinery v0.0.0-20190817221809-bf4de9df677c h1:TlFvXut3q9hNMA2UdjrC6LLfANYrh4lnDzb0gDNSMSg= -k8s.io/apimachinery v0.0.0-20190817221809-bf4de9df677c/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= -k8s.io/apiserver v0.0.0-20181213151703-3ccfe8365421/go.mod h1:6bqaTSOSJavUIXUtfaR9Os9JtTCm8ZqH2SUl2S60C4w= -k8s.io/cli-runtime v0.0.0-20190221101700-11047e25a94a/go.mod h1:qWnH3/b8sp/l7EvlDh7ulDU3UWA4P4N1NFbEEP791tM= -k8s.io/client-go v0.0.0-20190817222206-ee6c071a42cf h1:ZCiWjWPoxHYlMo7N4ZPz7yqo2YuCPvBi3nNX11oBTMg= -k8s.io/client-go v0.0.0-20190817222206-ee6c071a42cf/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= -k8s.io/code-generator v0.0.0-20190311093542-50b561225d70/go.mod h1:MYiN+ZJZ9HkETbgVZdWw2AsuAi9PZ4V80cwfuf2axe8= -k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/helm v2.13.1+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= -k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30 h1:TRb4wNWoBVrH9plmkp2q86FIDppkbrEXdXlxU3a3BMI= -k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= -k8s.io/kubernetes v1.13.1/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= -k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +istio.io/api v0.0.0-20200324230230-11f0c7211ae4/go.mod h1:bcY3prusO/6vA6zGHz4PNG2v79clPyTw06Xx3fprJSQ= +istio.io/api v0.0.0-20200324230725-4b064f75ad8f h1:KIE2M1/XiG8YWNrk1Wkcp8cfWbZ0lDihUqtBCmlrMH0= +istio.io/api v0.0.0-20200324230725-4b064f75ad8f/go.mod h1:bcY3prusO/6vA6zGHz4PNG2v79clPyTw06Xx3fprJSQ= +istio.io/client-go v0.0.0-20200324231043-96a582576da1 h1:DTiU1Xcb38riKGTI6bL14lcCZgW2/HdqX8NXG2HByRQ= +istio.io/client-go v0.0.0-20200324231043-96a582576da1/go.mod h1:nSSQnALPGh+QfuiQ09DpSCcgXolWEhRpmIqwqqptckw= +istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a h1:w7zILua2dnYo9CxImhpNW4NE/8ZxEoc/wfBfHrhUhrE= +istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a/go.mod h1:OzpAts7jljZceG4Vqi5/zXy/pOg1b209T3jb7Nv5wIs= +k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= +k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= +k8s.io/api v0.17.1/go.mod h1:zxiAc5y8Ngn4fmhWUtSxuUlkfz1ixT7j9wESokELzOg= +k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc= +k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4= +k8s.io/api v0.17.5 h1:EkVieIbn1sC8YCDwckLKLpf+LoVofXYW72+LTZWo4aQ= +k8s.io/api v0.17.5/go.mod h1:0zV5/ungglgy2Rlm3QK8fbxkXVs+BSJWpJP/+8gUVLY= +k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= +k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= +k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.17.1/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4= +k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.17.5 h1:QAjfgeTtSGksdkgyaPrIb4lhU16FWMIzxKejYD5S0gc= +k8s.io/apimachinery v0.17.5/go.mod h1:ioIo1G/a+uONV7Tv+ZmCbMG1/a3kVw5YcDdncd8ugQ0= +k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= +k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= +k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= +k8s.io/client-go v0.17.1/go.mod h1:HZtHJSC/VuSHcETN9QA5QDZky1tXiYrkF/7t7vRpO1A= +k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc= +k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI= +k8s.io/client-go v0.17.5 h1:Sm/9AQ415xPAX42JLKbJZnreXFgD2rVfDUDwOTm0gzA= +k8s.io/client-go v0.17.5/go.mod h1:S8uZpBpjJJdEH/fEyxcqg7Rn0P5jH+ilkgBHjriSmNo= +k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269 h1:d8Fm55A+7HOczX58+x9x+nJnJ1Devt1aCrWVIPaw/Vg= +k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= +k8s.io/code-generator v0.17.1/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20191120174120-e74f70b9b27e h1:HqlU9dKk5YVs7R84jmq6U3Wo/XslpkxHpBv2iWHLtLc= +k8s.io/gengo v0.0.0-20191120174120-e74f70b9b27e/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf h1:EYm5AW/UUDbnmnI+gK0TJDVK9qPLhM+sRHYanNKw0EQ= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20200316234421-82d701f24f9d h1:jocF7XFucw2pEiv2wS7wk2FRFCjDFGV1oa4TMs0SAT0= +k8s.io/kube-openapi v0.0.0-20200316234421-82d701f24f9d/go.mod h1:F+5wygcW0wmRTnM3cOgIqGivxkwSWIWT5YdsDbeAOaU= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -sigs.k8s.io/controller-runtime v0.1.10/go.mod h1:HFAYoOh6XMV+jKF1UjFwrknPbowfyHEHHRdJMf2jMX8= -sigs.k8s.io/testing_frameworks v0.1.1/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U= +sigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns= +sigs.k8s.io/controller-tools v0.2.4 h1:la1h46EzElvWefWLqfsXrnsO3lZjpkI0asTpX6h8PLA= +sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA= +sigs.k8s.io/kustomize/kyaml v0.1.1 h1:nGUNYINljZNmlAS8uoobUv/wx/s3Pg8dNxYo+W7uYh0= +sigs.k8s.io/kustomize/kyaml v0.1.1/go.mod h1:/NdPPfrperSCGjm55cwEro1loBVtbtVIXSb7FguK6uk= +sigs.k8s.io/service-apis v0.0.0-20200213014236-51691dd89266/go.mod h1:s6Cwc0sg5w4xxZ/HEr88qPkQ4CITr80lnMpYZUzL9VY= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ= +sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= +sigs.k8s.io/structured-merge-diff/v2 v2.0.1/go.mod h1:Wb7vfKAodbKgf6tn1Kl0VvGj7mRH6DGaRcixXEJXTsE= +sigs.k8s.io/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 000000000..7a05f36e2 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,4 @@ +package config + +// FastPoll used for fast testing +var FastPoll = false diff --git a/internal/testutils/endpoint.go b/internal/testutils/endpoint.go index b1507f588..62e5c5465 100644 --- a/internal/testutils/endpoint.go +++ b/internal/testutils/endpoint.go @@ -39,7 +39,6 @@ func (b byAllFields) Less(i, j int) bool { return b[i].RecordType <= b[j].RecordType } return b[i].Targets.String() <= b[j].Targets.String() - } return false } diff --git a/internal/testutils/init.go b/internal/testutils/init.go new file mode 100644 index 000000000..63456d199 --- /dev/null +++ b/internal/testutils/init.go @@ -0,0 +1,23 @@ +package testutils + +import ( + "io/ioutil" + "os" + + "log" + + "github.com/sirupsen/logrus" + "sigs.k8s.io/external-dns/internal/config" +) + +func init() { + config.FastPoll = true + if os.Getenv("DEBUG") == "" { + logrus.SetOutput(ioutil.Discard) + log.SetOutput(ioutil.Discard) + } else { + if level, err := logrus.ParseLevel(os.Getenv("DEBUG")); err == nil { + logrus.SetLevel(level) + } + } +} diff --git a/internal/testutils/mock_source.go b/internal/testutils/mock_source.go index e1d62980a..d6acb9349 100644 --- a/internal/testutils/mock_source.go +++ b/internal/testutils/mock_source.go @@ -17,6 +17,7 @@ limitations under the License. package testutils import ( + "context" "time" "github.com/stretchr/testify/mock" @@ -40,21 +41,18 @@ func (m *MockSource) Endpoints() ([]*endpoint.Endpoint, error) { return endpoints.([]*endpoint.Endpoint), args.Error(1) } -// AddEventHandler adds an event handler function that's called when sources that support such a thing have changed. -func (m *MockSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { - // Execute callback handler no more than once per minInterval, until a message on stopChan is received. +// AddEventHandler adds an event handler that should be triggered if something in source changes +func (m *MockSource) AddEventHandler(ctx context.Context, handler func()) { go func() { - var lastCallbackTime time.Time + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { select { - case <-stopChan: + case <-ctx.Done(): return - default: - now := time.Now() - if now.After(lastCallbackTime.Add(minInterval)) { - handler() - lastCallbackTime = time.Now() - } + case <-ticker.C: + handler() } } }() diff --git a/main.go b/main.go index 0cff80cda..8ef845a74 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,32 @@ import ( log "github.com/sirupsen/logrus" _ "k8s.io/client-go/plugin/pkg/client/auth" + "sigs.k8s.io/external-dns/provider/akamai" + "sigs.k8s.io/external-dns/provider/alibabacloud" + "sigs.k8s.io/external-dns/provider/aws" + "sigs.k8s.io/external-dns/provider/awssd" + "sigs.k8s.io/external-dns/provider/azure" + "sigs.k8s.io/external-dns/provider/cloudflare" + "sigs.k8s.io/external-dns/provider/coredns" + "sigs.k8s.io/external-dns/provider/designate" + "sigs.k8s.io/external-dns/provider/digitalocean" + "sigs.k8s.io/external-dns/provider/dnsimple" + "sigs.k8s.io/external-dns/provider/dyn" + "sigs.k8s.io/external-dns/provider/exoscale" + "sigs.k8s.io/external-dns/provider/google" + "sigs.k8s.io/external-dns/provider/infoblox" + "sigs.k8s.io/external-dns/provider/inmemory" + "sigs.k8s.io/external-dns/provider/linode" + "sigs.k8s.io/external-dns/provider/ns1" + "sigs.k8s.io/external-dns/provider/oci" + "sigs.k8s.io/external-dns/provider/ovh" + "sigs.k8s.io/external-dns/provider/pdns" + "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/transip" + "sigs.k8s.io/external-dns/provider/vinyldns" + "sigs.k8s.io/external-dns/provider/vultr" "sigs.k8s.io/external-dns/controller" "sigs.k8s.io/external-dns/endpoint" @@ -63,12 +89,10 @@ func main() { } log.SetLevel(ll) - ctx := context.Background() - - stopChan := make(chan struct{}, 1) + ctx, cancel := context.WithCancel(context.Background()) go serveMetrics(cfg.MetricsAddress) - go handleSigterm(stopChan) + go handleSigterm(cancel) // Create a source.Config from the flags passed by the user. sourceCfg := &source.Config{ @@ -123,8 +147,8 @@ func main() { var p provider.Provider switch cfg.Provider { case "akamai": - p = provider.NewAkamaiProvider( - provider.AkamaiConfig{ + p = akamai.NewAkamaiProvider( + akamai.AkamaiConfig{ DomainFilter: domainFilter, ZoneIDFilter: zoneIDFilter, ServiceConsumerDomain: cfg.AkamaiServiceConsumerDomain, @@ -135,10 +159,10 @@ func main() { }, ) case "alibabacloud": - p, err = provider.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun) + p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun) case "aws": - p, err = provider.NewAWSProvider( - provider.AWSConfig{ + p, err = aws.NewAWSProvider( + aws.AWSConfig{ DomainFilter: domainFilter, ZoneIDFilter: zoneIDFilter, ZoneTypeFilter: zoneTypeFilter, @@ -158,36 +182,34 @@ func main() { log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry) cfg.Registry = "aws-sd" } - p, err = provider.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun) + p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun) case "azure-dns", "azure": - p, err = provider.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) + p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) case "azure-private-dns": - p, err = provider.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun) + p, err = azure.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun) case "vinyldns": - p, err = provider.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun) + p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun) case "vultr": p, err = provider.NewVultrProvider(domainFilter, cfg.DryRun) - case "ultradns": p, err = provider.NewUltraDNSProvider(domainFilter, cfg.DryRun ) - case "cloudflare": - p, err = provider.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun) + p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun) case "rcodezero": - p, err = provider.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt) + p, err = rcode0.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt) case "google": - p, err = provider.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.DryRun) + p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.DryRun) case "digitalocean": - p, err = provider.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun) + p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize) case "ovh": - p, err = provider.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.DryRun) + p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.DryRun) case "linode": - p, err = provider.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version) + p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version) case "dnsimple": - p, err = provider.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun) + p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun) case "infoblox": - p, err = provider.NewInfobloxProvider( - provider.InfobloxConfig{ + p, err = infoblox.NewInfobloxProvider( + infoblox.InfobloxConfig{ DomainFilter: domainFilter, ZoneIDFilter: zoneIDFilter, Host: cfg.InfobloxGridHost, @@ -202,8 +224,8 @@ func main() { }, ) case "dyn": - p, err = provider.NewDynProvider( - provider.DynConfig{ + p, err = dyn.NewDynProvider( + dyn.DynConfig{ DomainFilter: domainFilter, ZoneIDFilter: zoneIDFilter, DryRun: cfg.DryRun, @@ -215,29 +237,29 @@ func main() { }, ) case "coredns", "skydns": - p, err = provider.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun) + p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun) case "rdns": - p, err = provider.NewRDNSProvider( - provider.RDNSConfig{ + p, err = rdns.NewRDNSProvider( + rdns.RDNSConfig{ DomainFilter: domainFilter, DryRun: cfg.DryRun, }, ) case "exoscale": - p, err = provider.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, cfg.DryRun, provider.ExoscaleWithDomain(domainFilter), provider.ExoscaleWithLogging()), nil + p, err = exoscale.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, cfg.DryRun, exoscale.ExoscaleWithDomain(domainFilter), exoscale.ExoscaleWithLogging()), nil case "inmemory": - p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil + p, err = inmemory.NewInMemoryProvider(inmemory.InMemoryInitZones(cfg.InMemoryZones), inmemory.InMemoryWithDomain(domainFilter), inmemory.InMemoryWithLogging()), nil case "designate": - p, err = provider.NewDesignateProvider(domainFilter, cfg.DryRun) + p, err = designate.NewDesignateProvider(domainFilter, cfg.DryRun) case "pdns": - p, err = provider.NewPDNSProvider( + p, err = pdns.NewPDNSProvider( ctx, - provider.PDNSConfig{ + pdns.PDNSConfig{ DomainFilter: domainFilter, DryRun: cfg.DryRun, Server: cfg.PDNSServer, APIKey: cfg.PDNSAPIKey, - TLSConfig: provider.TLSConfig{ + TLSConfig: pdns.TLSConfig{ TLSEnabled: cfg.PDNSTLSEnabled, CAFilePath: cfg.TLSCA, ClientCertFilePath: cfg.TLSClientCert, @@ -246,16 +268,16 @@ func main() { }, ) case "oci": - var config *provider.OCIConfig - config, err = provider.LoadOCIConfig(cfg.OCIConfigFile) + var config *oci.OCIConfig + config, err = oci.LoadOCIConfig(cfg.OCIConfigFile) if err == nil { - p, err = provider.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun) + p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun) } case "rfc2136": - p, err = provider.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, nil) + p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, nil) case "ns1": - p, err = provider.NewNS1Provider( - provider.NS1Config{ + p, err = ns1.NewNS1Provider( + ns1.NS1Config{ DomainFilter: domainFilter, ZoneIDFilter: zoneIDFilter, NS1Endpoint: cfg.NS1Endpoint, @@ -264,7 +286,7 @@ func main() { }, ) case "transip": - p, err = provider.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun) + p, err = transip.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun) default: log.Fatalf("unknown dns provider: %s", cfg.Provider) } @@ -277,9 +299,9 @@ func main() { case "noop": r, err = registry.NewNoopRegistry(p) case "txt": - r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTOwnerID, cfg.TXTCacheInterval) + r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval) case "aws-sd": - r, err = registry.NewAWSSDRegistry(p.(*provider.AWSSDProvider), cfg.TXTOwnerID) + r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID) default: log.Fatalf("unknown registry: %s", cfg.Registry) } @@ -301,13 +323,6 @@ func main() { DomainFilter: domainFilter, } - if cfg.UpdateEvents { - // Add RunOnce as the handler function that will be called when ingress/service sources have changed. - // Note that k8s Informers will perform an initial list operation, which results in the handler - // function initially being called for every Service/Ingress that exists limted by minInterval. - ctrl.Source.AddEventHandler(func() error { return ctrl.RunOnce(ctx) }, stopChan, 1*time.Minute) - } - if cfg.Once { err := ctrl.RunOnce(ctx) if err != nil { @@ -316,15 +331,24 @@ func main() { os.Exit(0) } - ctrl.Run(ctx, stopChan) + + if cfg.UpdateEvents { + // Add RunOnce as the handler function that will be called when ingress/service sources have changed. + // Note that k8s Informers will perform an initial list operation, which results in the handler + // function initially being called for every Service/Ingress that exists + ctrl.Source.AddEventHandler(ctx, func() { ctrl.ScheduleRunOnce(time.Now()) }) + } + + ctrl.ScheduleRunOnce(time.Now()) + ctrl.Run(ctx) } -func handleSigterm(stopChan chan struct{}) { +func handleSigterm(cancel func()) { signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGTERM) <-signals log.Info("Received SIGTERM. Terminating...") - close(stopChan) + cancel() } func serveMetrics(address string) { diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index a79794d06..7182e53d4 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -109,6 +109,7 @@ type Config struct { Registry string TXTOwnerID string TXTPrefix string + TXTSuffix string Interval time.Duration Once bool DryRun bool @@ -139,6 +140,7 @@ type Config struct { NS1IgnoreSSL bool TransIPAccountName string TransIPPrivateKeyFile string + DigitalOceanAPIPageSize int } var defaultConfig = &Config{ @@ -205,6 +207,7 @@ var defaultConfig = &Config{ Registry: "txt", TXTOwnerID: "default", TXTPrefix: "", + TXTSuffix: "", TXTCacheInterval: 0, Interval: time.Minute, Once: false, @@ -235,6 +238,7 @@ var defaultConfig = &Config{ NS1IgnoreSSL: false, TransIPAccountName: "", TransIPPrivateKeyFile: "", + DigitalOceanAPIPageSize: 50, } // NewConfig returns new Config object @@ -361,6 +365,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("pdns-tls-enabled", "When using the PowerDNS/PDNS provider, specify whether to use TLS (default: false, requires --tls-ca, optionally specify --tls-client-cert and --tls-client-cert-key)").Default(strconv.FormatBool(defaultConfig.PDNSTLSEnabled)).BoolVar(&cfg.PDNSTLSEnabled) app.Flag("ns1-endpoint", "When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/)").Default(defaultConfig.NS1Endpoint).StringVar(&cfg.NS1Endpoint) app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL) + app.Flag("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIPageSize)).IntVar(&cfg.DigitalOceanAPIPageSize) // Flags related to TLS communication app.Flag("tls-ca", "When using TLS communication, the path to the certificate authority to verify server communications (optionally specify --tls-client-cert for two-way TLS)").Default(defaultConfig.TLSCA).StringVar(&cfg.TLSCA) @@ -392,7 +397,8 @@ func (cfg *Config) ParseFlags(args []string) error { // Flags related to the registry app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "aws-sd") app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID) - app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional)").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix) + app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix) + app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix) // Flags related to the main control loop app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 2257f410f..929f5f1cc 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -98,6 +98,7 @@ var ( RcodezeroTXTEncrypt: false, TransIPAccountName: "", TransIPPrivateKeyFile: "", + DigitalOceanAPIPageSize: 50, } overriddenConfig = &Config{ @@ -177,6 +178,7 @@ var ( NS1IgnoreSSL: true, TransIPAccountName: "transip", TransIPPrivateKeyFile: "/path/to/transip.key", + DigitalOceanAPIPageSize: 100, } ) @@ -280,6 +282,7 @@ func TestParseFlags(t *testing.T) { "--ns1-ignoressl", "--transip-account=transip", "--transip-keyfile=/path/to/transip.key", + "--digitalocean-api-page-size=100", }, envVars: map[string]string{}, expected: overriddenConfig, @@ -364,6 +367,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_NS1_IGNORESSL": "1", "EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip", "EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key", + "EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100", }, expected: overriddenConfig, }, diff --git a/pkg/apis/externaldns/validation/validation.go b/pkg/apis/externaldns/validation/validation.go index 292c8d869..58371ed6a 100644 --- a/pkg/apis/externaldns/validation/validation.go +++ b/pkg/apis/externaldns/validation/validation.go @@ -91,5 +91,10 @@ func ValidateConfig(cfg *externaldns.Config) error { if cfg.IgnoreHostnameAnnotation && cfg.FQDNTemplate == "" { return errors.New("FQDN Template must be set if ignoring annotations") } + + if len(cfg.TXTPrefix) > 0 && len(cfg.TXTSuffix) > 0 { + return errors.New("txt-prefix and txt-suffix are mutual exclusive") + } + return nil } diff --git a/pkg/k8sutils/async/bounded_frequency_runner.go b/pkg/k8sutils/async/bounded_frequency_runner.go deleted file mode 100644 index 8619dac15..000000000 --- a/pkg/k8sutils/async/bounded_frequency_runner.go +++ /dev/null @@ -1,238 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package async - -import ( - "fmt" - "sync" - "time" - - "k8s.io/client-go/util/flowcontrol" - "k8s.io/klog" -) - -// BoundedFrequencyRunner manages runs of a user-provided function. -// See NewBoundedFrequencyRunner for examples. -type BoundedFrequencyRunner struct { - name string // the name of this instance - minInterval time.Duration // the min time between runs, modulo bursts - maxInterval time.Duration // the max time between runs - - run chan struct{} // try an async run - - mu sync.Mutex // guards runs of fn and all mutations - fn func() // function to run - lastRun time.Time // time of last run - timer timer // timer for deferred runs - limiter rateLimiter // rate limiter for on-demand runs -} - -// designed so that flowcontrol.RateLimiter satisfies -type rateLimiter interface { - TryAccept() bool - Stop() -} - -type nullLimiter struct{} - -func (nullLimiter) TryAccept() bool { - return true -} - -func (nullLimiter) Stop() {} - -var _ rateLimiter = nullLimiter{} - -// for testing -type timer interface { - // C returns the timer's selectable channel. - C() <-chan time.Time - - // See time.Timer.Reset. - Reset(d time.Duration) bool - - // See time.Timer.Stop. - Stop() bool - - // See time.Now. - Now() time.Time - - // See time.Since. - Since(t time.Time) time.Duration - - // See time.Sleep. - Sleep(d time.Duration) -} - -// implement our timer in terms of std time.Timer. -type realTimer struct { - *time.Timer -} - -func (rt realTimer) C() <-chan time.Time { - return rt.Timer.C -} - -func (rt realTimer) Now() time.Time { - return time.Now() -} - -func (rt realTimer) Since(t time.Time) time.Duration { - return time.Since(t) -} - -func (rt realTimer) Sleep(d time.Duration) { - time.Sleep(d) -} - -var _ timer = realTimer{} - -// NewBoundedFrequencyRunner creates a new BoundedFrequencyRunner instance, -// which will manage runs of the specified function. -// -// All runs will be async to the caller of BoundedFrequencyRunner.Run, but -// multiple runs are serialized. If the function needs to hold locks, it must -// take them internally. -// -// Runs of the function will have at least minInterval between them (from -// completion to next start), except that up to bursts may be allowed. Burst -// runs are "accumulated" over time, one per minInterval up to burstRuns total. -// This can be used, for example, to mitigate the impact of expensive operations -// being called in response to user-initiated operations. Run requests that -// would violate the minInterval are coallesced and run at the next opportunity. -// -// The function will be run at least once per maxInterval. For example, this can -// force periodic refreshes of state in the absence of anyone calling Run. -// -// Examples: -// -// NewBoundedFrequencyRunner("name", fn, time.Second, 5*time.Second, 1) -// - fn will have at least 1 second between runs -// - fn will have no more than 5 seconds between runs -// -// NewBoundedFrequencyRunner("name", fn, 3*time.Second, 10*time.Second, 3) -// - fn will have at least 3 seconds between runs, with up to 3 burst runs -// - fn will have no more than 10 seconds between runs -// -// The maxInterval must be greater than or equal to the minInterval, If the -// caller passes a maxInterval less than minInterval, this function will panic. -func NewBoundedFrequencyRunner(name string, fn func(), minInterval, maxInterval time.Duration, burstRuns int) *BoundedFrequencyRunner { - timer := realTimer{Timer: time.NewTimer(0)} // will tick immediately - <-timer.C() // consume the first tick - return construct(name, fn, minInterval, maxInterval, burstRuns, timer) -} - -// Make an instance with dependencies injected. -func construct(name string, fn func(), minInterval, maxInterval time.Duration, burstRuns int, timer timer) *BoundedFrequencyRunner { - if maxInterval < minInterval { - panic(fmt.Sprintf("%s: maxInterval (%v) must be >= minInterval (%v)", name, minInterval, maxInterval)) - } - if timer == nil { - panic(fmt.Sprintf("%s: timer must be non-nil", name)) - } - - bfr := &BoundedFrequencyRunner{ - name: name, - fn: fn, - minInterval: minInterval, - maxInterval: maxInterval, - run: make(chan struct{}, 1), - timer: timer, - } - if minInterval == 0 { - bfr.limiter = nullLimiter{} - } else { - // allow burst updates in short succession - qps := float32(time.Second) / float32(minInterval) - bfr.limiter = flowcontrol.NewTokenBucketRateLimiterWithClock(qps, burstRuns, timer) - } - return bfr -} - -// Loop handles the periodic timer and run requests. This is expected to be -// called as a goroutine. -func (bfr *BoundedFrequencyRunner) Loop(stop <-chan struct{}) { - klog.V(3).Infof("%s Loop running", bfr.name) - bfr.timer.Reset(bfr.maxInterval) - for { - select { - case <-stop: - bfr.stop() - klog.V(3).Infof("%s Loop stopping", bfr.name) - return - case <-bfr.timer.C(): - bfr.tryRun() - case <-bfr.run: - bfr.tryRun() - } - } -} - -// Run the function as soon as possible. If this is called while Loop is not -// running, the call may be deferred indefinitely. -// If there is already a queued request to call the underlying function, it -// may be dropped - it is just guaranteed that we will try calling the -// underlying function as soon as possible starting from now. -func (bfr *BoundedFrequencyRunner) Run() { - // If it takes a lot of time to run the underlying function, noone is really - // processing elements from channel. So to avoid blocking here on the - // putting element to it, we simply skip it if there is already an element - // in it. - select { - case bfr.run <- struct{}{}: - default: - } -} - -// assumes the lock is not held -func (bfr *BoundedFrequencyRunner) stop() { - bfr.mu.Lock() - defer bfr.mu.Unlock() - bfr.limiter.Stop() - bfr.timer.Stop() -} - -// assumes the lock is not held -func (bfr *BoundedFrequencyRunner) tryRun() { - bfr.mu.Lock() - defer bfr.mu.Unlock() - - if bfr.limiter.TryAccept() { - // We're allowed to run the function right now. - bfr.fn() - bfr.lastRun = bfr.timer.Now() - bfr.timer.Stop() - bfr.timer.Reset(bfr.maxInterval) - klog.V(3).Infof("%s: ran, next possible in %v, periodic in %v", bfr.name, bfr.minInterval, bfr.maxInterval) - return - } - - // It can't run right now, figure out when it can run next. - - elapsed := bfr.timer.Since(bfr.lastRun) // how long since last run - nextPossible := bfr.minInterval - elapsed // time to next possible run - nextScheduled := bfr.maxInterval - elapsed // time to next periodic run - klog.V(4).Infof("%s: %v since last run, possible in %v, scheduled in %v", bfr.name, elapsed, nextPossible, nextScheduled) - - if nextPossible < nextScheduled { - // Set the timer for ASAP, but don't drain here. Assuming Loop is running, - // it might get a delivery in the mean time, but that is OK. - bfr.timer.Stop() - bfr.timer.Reset(nextPossible) - klog.V(3).Infof("%s: throttled, scheduling run in %v", bfr.name, nextPossible) - } -} diff --git a/plan/plan.go b/plan/plan.go index 675eecf5f..5c0aaf4c3 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -18,11 +18,15 @@ package plan import ( "fmt" + "strconv" "strings" "sigs.k8s.io/external-dns/endpoint" ) +// PropertyComparator is used in Plan for comparing the previous and current custom annotations. +type PropertyComparator func(name string, previous string, current string) bool + // Plan can convert a list of desired and current records to a series of create, // update and delete actions. type Plan struct { @@ -37,6 +41,8 @@ type Plan struct { Changes *Changes // DomainFilter matches DNS names DomainFilter endpoint.DomainFilter + // Property comparator compares custom properties of providers + PropertyComparator PropertyComparator } // Changes holds lists of actions to be executed by dns providers @@ -135,7 +141,7 @@ func (p *Plan) Calculate() *Plan { if row.current != nil && len(row.candidates) > 0 { //dns name is taken update := t.resolver.ResolveUpdate(row.current, row.candidates) // compare "update" to "current" to figure out if actual update is required - if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || shouldUpdateProviderSpecific(update, row.current) { + if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || p.shouldUpdateProviderSpecific(update, row.current) { inheritOwner(row.current, update) changes.UpdateNew = append(changes.UpdateNew, update) changes.UpdateOld = append(changes.UpdateOld, row.current) @@ -178,45 +184,40 @@ func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool { return desired.RecordTTL != current.RecordTTL } -func shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool { - if current.ProviderSpecific == nil && len(desired.ProviderSpecific) == 0 { - return false - } - for _, c := range current.ProviderSpecific { - // don't consider target health when detecting changes - // see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954 - if c.Name == "aws/evaluate-target-health" { - continue - } +func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool { + desiredProperties := map[string]endpoint.ProviderSpecificProperty{} - found := false + if desired.ProviderSpecific != nil { for _, d := range desired.ProviderSpecific { - if d.Name == c.Name { - if d.Value != c.Value { - // provider-specific attribute updated + desiredProperties[d.Name] = d + } + } + if current.ProviderSpecific != nil { + for _, c := range current.ProviderSpecific { + // don't consider target health when detecting changes + // see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954 + if c.Name == "aws/evaluate-target-health" { + continue + } + + if d, ok := desiredProperties[c.Name]; ok { + if p.PropertyComparator != nil { + if !p.PropertyComparator(c.Name, c.Value, d.Value) { + return true + } + } else if c.Value != d.Value { + return true + } + } else { + if p.PropertyComparator != nil { + if !p.PropertyComparator(c.Name, c.Value, "") { + return true + } + } else if c.Value != "" { return true } - found = true - break } } - if !found { - // provider-specific attribute deleted - return true - } - } - for _, d := range desired.ProviderSpecific { - found := false - for _, c := range current.ProviderSpecific { - if d.Name == c.Name { - found = true - break - } - } - if !found { - // provider-specific attribute added - return true - } } return false @@ -260,3 +261,28 @@ func normalizeDNSName(dnsName string) string { } return s } + +// CompareBoolean is an implementation of PropertyComparator for comparing boolean-line values +// For example external-dns.alpha.kubernetes.io/cloudflare-proxied: "true" +// If value doesn't parse as boolean, the defaultValue is used +func CompareBoolean(defaultValue bool, name, current, previous string) bool { + var err error + + v1, v2 := defaultValue, defaultValue + + if previous != "" { + v1, err = strconv.ParseBool(previous) + if err != nil { + v1 = defaultValue + } + } + + if current != "" { + v2, err = strconv.ParseBool(current) + if err != nil { + v2 = defaultValue + } + } + + return v1 == v2 +} diff --git a/plan/plan_test.go b/plan/plan_test.go index fafcb2ce8..1a3416196 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -38,6 +38,7 @@ type PlanTestSuite struct { bar127AWithTTL *endpoint.Endpoint bar127AWithProviderSpecificTrue *endpoint.Endpoint bar127AWithProviderSpecificFalse *endpoint.Endpoint + bar127AWithProviderSpecificUnset *endpoint.Endpoint bar192A *endpoint.Endpoint multiple1 *endpoint.Endpoint multiple2 *endpoint.Endpoint @@ -138,6 +139,15 @@ func (suite *PlanTestSuite) SetupTest() { }, }, } + suite.bar127AWithProviderSpecificUnset = &endpoint.Endpoint{ + DNSName: "bar", + Targets: endpoint.Targets{"127.0.0.1"}, + RecordType: "A", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/bar-127", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + } suite.bar192A = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"192.168.0.1"}, @@ -291,6 +301,54 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() { validateEntries(suite.T(), changes.Delete, expectedDelete) } +func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefaultFalse() { + current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse} + desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset} + expectedCreate := []*endpoint.Endpoint{} + expectedUpdateOld := []*endpoint.Endpoint{} + expectedUpdateNew := []*endpoint.Endpoint{} + expectedDelete := []*endpoint.Endpoint{} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + PropertyComparator: func(name, previous, current string) bool { + return CompareBoolean(false, name, previous, current) + }, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) + validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) + validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) + validateEntries(suite.T(), changes.Delete, expectedDelete) +} + +func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefualtTrue() { + current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue} + desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset} + expectedCreate := []*endpoint.Endpoint{} + expectedUpdateOld := []*endpoint.Endpoint{} + expectedUpdateNew := []*endpoint.Endpoint{} + expectedDelete := []*endpoint.Endpoint{} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + PropertyComparator: func(name, previous, current string) bool { + return CompareBoolean(true, name, previous, current) + }, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) + validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) + validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) + validateEntries(suite.T(), changes.Delete, expectedDelete) +} + func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() { current := []*endpoint.Endpoint{suite.fooV1Cname} desired := []*endpoint.Endpoint{suite.fooV2Cname} diff --git a/provider/akamai.go b/provider/akamai/akamai.go similarity index 94% rename from provider/akamai.go rename to provider/akamai/akamai.go index fe5e0aab1..d90a53d49 100644 --- a/provider/akamai.go +++ b/provider/akamai/akamai.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package akamai import ( "bytes" @@ -30,6 +30,7 @@ import ( log "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) type akamaiClient interface { @@ -50,7 +51,7 @@ func (*akamaiOpenClient) Do(config edgegrid.Config, req *http.Request) (*http.Re // AkamaiConfig clarifies the method signature type AkamaiConfig struct { DomainFilter endpoint.DomainFilter - ZoneIDFilter ZoneIDFilter + ZoneIDFilter provider.ZoneIDFilter ServiceConsumerDomain string ClientToken string ClientSecret string @@ -60,8 +61,9 @@ type AkamaiConfig struct { // AkamaiProvider implements the DNS provider for Akamai. type AkamaiProvider struct { + provider.BaseProvider domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter config edgegrid.Config dryRun bool client akamaiClient @@ -226,7 +228,7 @@ func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoin // ApplyChanges applies a given set of changes in a given zone. func (p *AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} zones, err := p.fetchZones() if err != nil { log.Warnf("No zones to fetch endpoints from!") @@ -289,9 +291,8 @@ func (p *AkamaiProvider) newAkamaiRecord(dnsName, recordType string, targets ... } } -func (p *AkamaiProvider) createRecords(zoneNameIDMapper zoneIDName, endpoints []*endpoint.Endpoint) (created []*endpoint.Endpoint, failed []*endpoint.Endpoint) { +func (p *AkamaiProvider) createRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (created []*endpoint.Endpoint, failed []*endpoint.Endpoint) { for _, endpoint := range endpoints { - if !p.domainFilter.Match(endpoint.DNSName) { log.Debugf("Skipping creation at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType) continue @@ -320,9 +321,8 @@ func (p *AkamaiProvider) createRecords(zoneNameIDMapper zoneIDName, endpoints [] return created, failed } -func (p *AkamaiProvider) deleteRecords(zoneNameIDMapper zoneIDName, endpoints []*endpoint.Endpoint) (deleted []*endpoint.Endpoint, failed []*endpoint.Endpoint) { +func (p *AkamaiProvider) deleteRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (deleted []*endpoint.Endpoint, failed []*endpoint.Endpoint) { for _, endpoint := range endpoints { - if !p.domainFilter.Match(endpoint.DNSName) { log.Debugf("Skipping deletion at Akamai of endpoint: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType) continue @@ -349,9 +349,8 @@ func (p *AkamaiProvider) deleteRecords(zoneNameIDMapper zoneIDName, endpoints [] return deleted, failed } -func (p *AkamaiProvider) updateNewRecords(zoneNameIDMapper zoneIDName, endpoints []*endpoint.Endpoint) (updated []*endpoint.Endpoint, failed []*endpoint.Endpoint) { +func (p *AkamaiProvider) updateNewRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (updated []*endpoint.Endpoint, failed []*endpoint.Endpoint) { for _, endpoint := range endpoints { - if !p.domainFilter.Match(endpoint.DNSName) { log.Debugf("Skipping update at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType) continue diff --git a/provider/akamai_test.go b/provider/akamai/akamai_test.go similarity index 93% rename from provider/akamai_test.go rename to provider/akamai/akamai_test.go index 525c283bf..7d0bd7a5e 100644 --- a/provider/akamai_test.go +++ b/provider/akamai/akamai_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package akamai import ( "bytes" @@ -32,6 +32,7 @@ import ( "github.com/stretchr/testify/mock" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) type mockAkamaiClient struct { @@ -92,7 +93,7 @@ func TestRequestError(t *testing.T) { func TestFetchZonesZoneIDFilter(t *testing.T) { config := AkamaiConfig{ - ZoneIDFilter: NewZoneIDFilter([]string{"Test"}), + ZoneIDFilter: provider.NewZoneIDFilter([]string{"Test"}), } client := &mockAkamaiClient{} @@ -109,7 +110,7 @@ func TestFetchZonesZoneIDFilter(t *testing.T) { func TestFetchZonesEmpty(t *testing.T) { config := AkamaiConfig{ DomainFilter: endpoint.NewDomainFilter([]string{"Nonexistent"}), - ZoneIDFilter: NewZoneIDFilter([]string{"Nonexistent"}), + ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}), } client := &mockAkamaiClient{} @@ -171,7 +172,7 @@ func TestAkamaiRecords(t *testing.T) { func TestAkamaiRecordsEmpty(t *testing.T) { config := AkamaiConfig{ - ZoneIDFilter: NewZoneIDFilter([]string{"Nonexistent"}), + ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}), } client := &mockAkamaiClient{} @@ -185,7 +186,7 @@ func TestAkamaiRecordsEmpty(t *testing.T) { func TestAkamaiRecordsFilters(t *testing.T) { config := AkamaiConfig{ DomainFilter: endpoint.NewDomainFilter([]string{"www.exclude.me"}), - ZoneIDFilter: NewZoneIDFilter([]string{"Exclude-Me"}), + ZoneIDFilter: provider.NewZoneIDFilter([]string{"Exclude-Me"}), } client := &mockAkamaiClient{} @@ -208,7 +209,7 @@ func TestCreateRecords(t *testing.T) { c := NewAkamaiProvider(config) c.client = client - zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"} endpoints := make([]*endpoint.Endpoint, 0) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) @@ -228,7 +229,7 @@ func TestCreateRecordsDomainFilter(t *testing.T) { c := NewAkamaiProvider(config) c.client = client - zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"} endpoints := make([]*endpoint.Endpoint, 0) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) @@ -247,7 +248,7 @@ func TestDeleteRecords(t *testing.T) { c := NewAkamaiProvider(config) c.client = client - zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"} endpoints := make([]*endpoint.Endpoint, 0) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) @@ -267,7 +268,7 @@ func TestDeleteRecordsDomainFilter(t *testing.T) { c := NewAkamaiProvider(config) c.client = client - zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"} endpoints := make([]*endpoint.Endpoint, 0) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) @@ -286,7 +287,7 @@ func TestUpdateRecords(t *testing.T) { c := NewAkamaiProvider(config) c.client = client - zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"} endpoints := make([]*endpoint.Endpoint, 0) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) @@ -306,7 +307,7 @@ func TestUpdateRecordsDomainFilter(t *testing.T) { c := NewAkamaiProvider(config) c.client = client - zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"} endpoints := make([]*endpoint.Endpoint, 0) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) diff --git a/provider/alibaba_cloud.go b/provider/alibabacloud/alibaba_cloud.go similarity index 96% rename from provider/alibaba_cloud.go rename to provider/alibabacloud/alibaba_cloud.go index eb78bc987..1dd411ca9 100644 --- a/provider/alibaba_cloud.go +++ b/provider/alibabacloud/alibaba_cloud.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package alibabacloud import ( "context" @@ -33,6 +33,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -66,8 +67,9 @@ type AlibabaCloudPrivateZoneAPI interface { // AlibabaCloudProvider implements the DNS provider for Alibaba Cloud. type AlibabaCloudProvider struct { + provider.BaseProvider domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter // Private Zone only + zoneIDFilter provider.ZoneIDFilter // Private Zone only MaxChangeCount int EvaluateTargetHealth bool AssumeRole string @@ -93,22 +95,22 @@ type alibabaCloudConfig struct { // NewAlibabaCloudProvider creates a new Alibaba Cloud provider. // // Returns the provider or an error if a provider could not be created. -func NewAlibabaCloudProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFileter ZoneIDFilter, zoneType string, dryRun bool) (*AlibabaCloudProvider, error) { +func NewAlibabaCloudProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFileter provider.ZoneIDFilter, zoneType string, dryRun bool) (*AlibabaCloudProvider, error) { cfg := alibabaCloudConfig{} if configFile != "" { contents, err := ioutil.ReadFile(configFile) if err != nil { - return nil, fmt.Errorf("Failed to read Alibaba Cloud config file '%s': %v", configFile, err) + return nil, fmt.Errorf("failed to read Alibaba Cloud config file '%s': %v", configFile, err) } err = yaml.Unmarshal(contents, &cfg) if err != nil { - return nil, fmt.Errorf("Failed to parse Alibaba Cloud config file '%s': %v", configFile, err) + return nil, fmt.Errorf("failed to parse Alibaba Cloud config file '%s': %v", configFile, err) } } else { var tmpError error cfg, tmpError = getCloudConfigFromStsToken() if tmpError != nil { - return nil, fmt.Errorf("Failed to getCloudConfigFromStsToken: %v", tmpError) + return nil, fmt.Errorf("failed to getCloudConfigFromStsToken: %v", tmpError) } } @@ -180,19 +182,19 @@ func getCloudConfigFromStsToken() (alibabaCloudConfig, error) { roleName := "" var err error if roleName, err = m.RoleName(); err != nil { - return cfg, fmt.Errorf("Failed to get role name from Metadata Service: %v", err) + return cfg, fmt.Errorf("failed to get role name from Metadata Service: %v", err) } vpcID, err := m.VpcID() if err != nil { - return cfg, fmt.Errorf("Failed to get VPC ID from Metadata Service: %v", err) + return cfg, fmt.Errorf("failed to get VPC ID from Metadata Service: %v", err) } regionID, err := m.Region() if err != nil { - return cfg, fmt.Errorf("Failed to get Region ID from Metadata Service: %v", err) + return cfg, fmt.Errorf("failed to get Region ID from Metadata Service: %v", err) } role, err := m.RamRoleToken(roleName) if err != nil { - return cfg, fmt.Errorf("Failed to get STS Token from Metadata Service: %v", err) + return cfg, fmt.Errorf("failed to get STS Token from Metadata Service: %v", err) } cfg.RegionID = regionID cfg.RoleName = roleName @@ -315,7 +317,6 @@ func (p *AlibabaCloudProvider) getDNSName(rr, domain string) string { // // Returns the current records or an error if the operation failed. func (p *AlibabaCloudProvider) recordsForDNS() (endpoints []*endpoint.Endpoint, _ error) { - records, err := p.records() if err != nil { return nil, err @@ -344,7 +345,6 @@ func (p *AlibabaCloudProvider) recordsForDNS() (endpoints []*endpoint.Endpoint, } func getNextPageNumber(pageNumber, pageSize, totalCount int) int { - if pageNumber*pageSize >= totalCount { return 0 } @@ -363,18 +363,13 @@ func (p *AlibabaCloudProvider) getRecordKeyByEndpoint(endpoint *endpoint.Endpoin } func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) (endpointMap map[string][]alidns.Record) { - endpointMap = make(map[string][]alidns.Record) - for _, record := range records { - key := p.getRecordKey(record) recordList := endpointMap[key] endpointMap[key] = append(recordList, record) - } - return endpointMap } @@ -449,18 +444,15 @@ func (p *AlibabaCloudProvider) getDomainRecords(domainName string) ([]alidns.Rec } for _, record := range response.DomainRecords.Record { - domainName := record.DomainName recordType := record.Type if !p.domainFilter.Match(domainName) { continue } - - if !supportedRecordType(recordType) { + if !provider.SupportedRecordType(recordType) { continue } - //TODO filter Locked record results = append(results, record) } @@ -622,7 +614,6 @@ func (p *AlibabaCloudProvider) equals(record alidns.Record, endpoint *endpoint.E } func (p *AlibabaCloudProvider) updateRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint) error { - for _, endpoint := range endpoints { key := p.getRecordKeyByEndpoint(endpoint) records := recordMap[key] @@ -667,7 +658,6 @@ func (p *AlibabaCloudProvider) updateRecords(recordMap map[string][]alidns.Recor } func (p *AlibabaCloudProvider) splitDNSName(endpoint *endpoint.Endpoint) (rr string, domain string) { - name := strings.TrimSuffix(endpoint.DNSName, ".") found := false @@ -727,7 +717,6 @@ func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool { } func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) { - var zones []pvtz.Zone request := pvtz.CreateDescribeZonesRequest() @@ -782,7 +771,6 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone } for _, zone := range zones { - request := pvtz.CreateDescribeZoneRecordsRequest() request.ZoneId = zone.ZoneId request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) @@ -799,10 +787,9 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone } for _, record := range response.Records.Record { - recordType := record.Type - if !supportedRecordType(recordType) { + if !provider.SupportedRecordType(recordType) { continue } @@ -829,7 +816,6 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone } func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) (endpointMap map[string][]pvtz.Record) { - endpointMap = make(map[string][]pvtz.Record) for _, record := range zone.records { @@ -845,7 +831,6 @@ func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) // // Returns the current records or an error if the operation failed. func (p *AlibabaCloudProvider) privateZoneRecords() (endpoints []*endpoint.Endpoint, _ error) { - zones, err := p.getPrivateZones() if err != nil { return nil, err @@ -879,7 +864,7 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibaba rr, domain := p.splitDNSName(endpoint) zone := zones[domain] if zone == nil { - err := fmt.Errorf("Failed to find private zone '%s'", domain) + err := fmt.Errorf("failed to find private zone '%s'", domain) log.Errorf("Failed to create %s record named '%s' to '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, target, err) return err } @@ -925,7 +910,6 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecords(zones map[string]*alibab } func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int) error { - if p.dryRun { log.Infof("Dry run: Delete record id '%d' in Alibaba Cloud Private Zone", recordID) } @@ -949,7 +933,7 @@ func (p *AlibabaCloudProvider) deletePrivateZoneRecords(zones map[string]*alibab zone := zones[domain] if zone == nil { - err := fmt.Errorf("Failed to find private zone '%s'", domain) + err := fmt.Errorf("failed to find private zone '%s'", domain) log.Errorf("Failed to delete %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err) continue } @@ -1033,12 +1017,11 @@ func (p *AlibabaCloudProvider) equalsPrivateZone(record pvtz.Record, endpoint *e } func (p *AlibabaCloudProvider) updatePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error { - for _, endpoint := range endpoints { rr, domain := p.splitDNSName(endpoint) zone := zones[domain] if zone == nil { - err := fmt.Errorf("Failed to find private zone '%s'", domain) + err := fmt.Errorf("failed to find private zone '%s'", domain) log.Errorf("Failed to update %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err) continue } diff --git a/provider/alibaba_cloud_test.go b/provider/alibabacloud/alibaba_cloud_test.go similarity index 99% rename from provider/alibaba_cloud_test.go rename to provider/alibabacloud/alibaba_cloud_test.go index 6ed419ead..387867850 100644 --- a/provider/alibaba_cloud_test.go +++ b/provider/alibabacloud/alibaba_cloud_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package alibabacloud import ( "context" diff --git a/provider/aws.go b/provider/aws/aws.go similarity index 95% rename from provider/aws.go rename to provider/aws/aws.go index 50e983040..f94609b29 100644 --- a/provider/aws.go +++ b/provider/aws/aws.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package aws import ( "context" @@ -34,6 +34,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -51,8 +52,7 @@ const ( ) var ( - // see: https://docs.aws.amazon.com/general/latest/gr/rande.html#elb_region - // and: https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/using-govcloud-endpoints.html + // see: https://docs.aws.amazon.com/general/latest/gr/elb.html canonicalHostedZones = map[string]string{ // Application Load Balancers and Classic Load Balancers "us-east-2.elb.amazonaws.com": "Z3AADJGX6KTTL2", @@ -73,9 +73,10 @@ var ( "eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4", "eu-north-1.elb.amazonaws.com": "Z23TAZ6LKFMNIO", "sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU", - "cn-north-1.elb.amazonaws.com.cn": "Z3BX2TMKNYI13Y", - "cn-northwest-1.elb.amazonaws.com.cn": "Z3BX2TMKNYI13Y", - "us-gov-west-1.amazonaws.com": "Z1K6XKP9SAGWDV", + "cn-north-1.elb.amazonaws.com.cn": "Z1GDH35T77C1KE", + "cn-northwest-1.elb.amazonaws.com.cn": "ZM7IZAIOVVDZF", + "us-gov-west-1.elb.amazonaws.com": "Z33AYJ8TM3BH4J", + "us-gov-east-1.elb.amazonaws.com": "Z166TLBEWOO7G0", "me-south-1.elb.amazonaws.com": "ZS929ML54UICD", // Network Load Balancers "elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP", @@ -97,6 +98,8 @@ var ( "elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU", "elb.cn-north-1.amazonaws.com.cn": "Z3QFB96KMJ7ED6", "elb.cn-northwest-1.amazonaws.com.cn": "ZQEIKTCZ8352D", + "elb.us-gov-west-1.amazonaws.com": "ZMG1MZ2THAWF1", + "elb.us-gov-east-1.amazonaws.com": "Z1ZSMQQ6Q24QQ8", "elb.me-south-1.amazonaws.com": "Z3QSRYVP46NYYV", } ) @@ -113,6 +116,7 @@ type Route53API interface { // AWSProvider is an implementation of Provider for AWS Route53. type AWSProvider struct { + provider.BaseProvider client Route53API dryRun bool batchChangeSize int @@ -121,20 +125,20 @@ type AWSProvider struct { // only consider hosted zones managing domains ending in this suffix domainFilter endpoint.DomainFilter // filter hosted zones by id - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter // filter hosted zones by type (e.g. private or public) - zoneTypeFilter ZoneTypeFilter + zoneTypeFilter provider.ZoneTypeFilter // filter hosted zones by tags - zoneTagFilter ZoneTagFilter + zoneTagFilter provider.ZoneTagFilter preferCNAME bool } // AWSConfig contains configuration to create a new AWS provider. type AWSConfig struct { DomainFilter endpoint.DomainFilter - ZoneIDFilter ZoneIDFilter - ZoneTypeFilter ZoneTypeFilter - ZoneTagFilter ZoneTagFilter + ZoneIDFilter provider.ZoneIDFilter + ZoneTypeFilter provider.ZoneTypeFilter + ZoneTagFilter provider.ZoneTagFilter BatchChangeSize int BatchChangeInterval time.Duration EvaluateTargetHealth bool @@ -266,7 +270,7 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos // TODO(linki, ownership): Remove once ownership system is in place. // See: https://github.com/kubernetes-sigs/external-dns/pull/122/files/74e2c3d3e237411e619aefc5aab694742001cdec#r109863370 - if !supportedRecordType(aws.StringValue(r.Type)) { + if !provider.SupportedRecordType(aws.StringValue(r.Type)) { continue } @@ -377,7 +381,7 @@ func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) e return err } - records, ok := ctx.Value(RecordsContextKey).([]*endpoint.Endpoint) + records, ok := ctx.Value(provider.RecordsContextKey).([]*endpoint.Endpoint) if !ok { var err error records, err = p.records(ctx, zones) @@ -449,7 +453,7 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes []*route53.Chan } if len(failedZones) > 0 { - return fmt.Errorf("Failed to submit all changes for the following zones: %v", failedZones) + return fmt.Errorf("failed to submit all changes for the following zones: %v", failedZones) } return nil @@ -588,7 +592,8 @@ func (p *AWSProvider) tagsForZone(ctx context.Context, zoneID string) (map[strin func batchChangeSet(cs []*route53.Change, batchSize int) [][]*route53.Change { if len(cs) <= batchSize { - return [][]*route53.Change{cs} + res := sortChangesByActionNameType(cs) + return [][]*route53.Change{res} } batchChanges := make([][]*route53.Change, 0) @@ -635,10 +640,10 @@ func batchChangeSet(cs []*route53.Change, batchSize int) [][]*route53.Change { func sortChangesByActionNameType(cs []*route53.Change) []*route53.Change { sort.SliceStable(cs, func(i, j int) bool { - if *cs[i].Action < *cs[j].Action { + if *cs[i].Action > *cs[j].Action { return true } - if *cs[i].Action > *cs[j].Action { + if *cs[i].Action < *cs[j].Action { return false } if *cs[i].ResourceRecordSet.Name < *cs[j].ResourceRecordSet.Name { @@ -662,7 +667,7 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet []*route53.Ch } for _, c := range changeSet { - hostname := ensureTrailingDot(aws.StringValue(c.ResourceRecordSet.Name)) + hostname := provider.EnsureTrailingDot(aws.StringValue(c.ResourceRecordSet.Name)) zones := suitableZones(hostname, zones) if len(zones) == 0 { @@ -733,7 +738,6 @@ func isAWSAlias(ep *endpoint.Endpoint, addrs []*endpoint.Endpoint) string { if hostedZone := canonicalHostedZone(addr.Targets[0]); hostedZone != "" { return hostedZone } - } } } diff --git a/provider/aws_test.go b/provider/aws/aws_test.go similarity index 93% rename from provider/aws_test.go rename to provider/aws/aws_test.go index 199154de4..aaa32741f 100644 --- a/provider/aws_test.go +++ b/provider/aws/aws_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package aws import ( "context" @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -178,17 +179,17 @@ func (r *Route53APIStub) ChangeResourceRecordSetsWithContext(ctx context.Context } } - change.ResourceRecordSet.Name = aws.String(wildcardEscape(ensureTrailingDot(aws.StringValue(change.ResourceRecordSet.Name)))) + change.ResourceRecordSet.Name = aws.String(wildcardEscape(provider.EnsureTrailingDot(aws.StringValue(change.ResourceRecordSet.Name)))) if change.ResourceRecordSet.AliasTarget != nil { - change.ResourceRecordSet.AliasTarget.DNSName = aws.String(wildcardEscape(ensureTrailingDot(aws.StringValue(change.ResourceRecordSet.AliasTarget.DNSName)))) + change.ResourceRecordSet.AliasTarget.DNSName = aws.String(wildcardEscape(provider.EnsureTrailingDot(aws.StringValue(change.ResourceRecordSet.AliasTarget.DNSName)))) } - setId := "" + setID := "" if change.ResourceRecordSet.SetIdentifier != nil { - setId = aws.StringValue(change.ResourceRecordSet.SetIdentifier) + setID = aws.StringValue(change.ResourceRecordSet.SetIdentifier) } - key := aws.StringValue(change.ResourceRecordSet.Name) + "::" + aws.StringValue(change.ResourceRecordSet.Type) + "::" + setId + key := aws.StringValue(change.ResourceRecordSet.Name) + "::" + aws.StringValue(change.ResourceRecordSet.Type) + "::" + setID switch aws.StringValue(change.Action) { case route53.ChangeActionCreate: if _, found := recordSets[key]; found { @@ -287,17 +288,17 @@ func TestAWSZones(t *testing.T) { for _, ti := range []struct { msg string - zoneIDFilter ZoneIDFilter - zoneTypeFilter ZoneTypeFilter - zoneTagFilter ZoneTagFilter + zoneIDFilter provider.ZoneIDFilter + zoneTypeFilter provider.ZoneTypeFilter + zoneTagFilter provider.ZoneTagFilter expectedZones map[string]*route53.HostedZone }{ - {"no filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), allZones}, - {"public filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("public"), NewZoneTagFilter([]string{}), publicZones}, - {"private filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("private"), NewZoneTagFilter([]string{}), privateZones}, - {"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), NewZoneTagFilter([]string{}), noZones}, - {"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), privateZones}, - {"tag filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{"zone=3"}), privateZones}, + {"no filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), allZones}, + {"public filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("public"), provider.NewZoneTagFilter([]string{}), publicZones}, + {"private filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("private"), provider.NewZoneTagFilter([]string{}), privateZones}, + {"unknown filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("unknown"), provider.NewZoneTagFilter([]string{}), noZones}, + {"zone id filter", provider.NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), privateZones}, + {"tag filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{"zone=3"}), privateZones}, } { provider, _ := newAWSProviderWithTagFilter(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) @@ -309,7 +310,7 @@ func TestAWSZones(t *testing.T) { } func TestAWSRecords(t *testing.T) { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, false, []*endpoint.Endpoint{ + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), @@ -351,7 +352,7 @@ func TestAWSRecords(t *testing.T) { func TestAWSCreateRecords(t *testing.T) { customTTL := endpoint.TTL(60) - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), @@ -376,7 +377,7 @@ func TestAWSCreateRecords(t *testing.T) { } func TestAWSUpdateRecords(t *testing.T) { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{ + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), @@ -419,7 +420,7 @@ func TestAWSDeleteRecords(t *testing.T) { endpoint.NewEndpointWithTTL("delete-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), } - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, false, originalEndpoints) + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, originalEndpoints) require.NoError(t, provider.DeleteRecords(context.Background(), originalEndpoints)) @@ -441,12 +442,12 @@ func TestAWSApplyChanges(t *testing.T) { ctx := context.Background() records, err := p.Records(ctx) require.NoError(t, err) - return context.WithValue(ctx, RecordsContextKey, records) + return context.WithValue(ctx, provider.RecordsContextKey, records) }, 0}, } for _, tt := range tests { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{ + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), @@ -538,7 +539,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) { endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"), } - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalEndpoints) + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalEndpoints) createRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), @@ -686,7 +687,7 @@ func TestAWSChangesByZones(t *testing.T) { } func TestAWSsubmitChanges(t *testing.T) { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) const subnets = 16 const hosts = defaultBatchChangeSize / subnets @@ -715,7 +716,7 @@ func TestAWSsubmitChanges(t *testing.T) { } func TestAWSsubmitChangesError(t *testing.T) { - provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure")) ctx := context.Background() @@ -851,7 +852,7 @@ func validateAWSChangeRecord(t *testing.T, record *route53.Change, expected *rou } func TestAWSCreateRecordsWithCNAME(t *testing.T) { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.example.org"}, RecordType: endpoint.RecordTypeCNAME}, @@ -881,7 +882,7 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { "false": false, "": false, } { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) // Test dualstack and ipv4 load balancer targets records := []*endpoint.Endpoint{ @@ -1023,8 +1024,8 @@ func TestAWSCanonicalHostedZone(t *testing.T) { {"foo.eu-west-2.elb.amazonaws.com", "ZHURV8PSTC4K8"}, {"foo.eu-west-3.elb.amazonaws.com", "Z3Q77PNBQS71R4"}, {"foo.sa-east-1.elb.amazonaws.com", "Z2P70J7HTTTPLU"}, - {"foo.cn-north-1.elb.amazonaws.com.cn", "Z3BX2TMKNYI13Y"}, - {"foo.cn-northwest-1.elb.amazonaws.com.cn", "Z3BX2TMKNYI13Y"}, + {"foo.cn-north-1.elb.amazonaws.com.cn", "Z1GDH35T77C1KE"}, + {"foo.cn-northwest-1.elb.amazonaws.com.cn", "ZM7IZAIOVVDZF"}, // Network Load Balancers {"foo.elb.us-east-2.amazonaws.com", "ZLMOA37VPKANP"}, {"foo.elb.us-east-1.amazonaws.com", "Z26RNL4JYFTOTI"}, @@ -1182,11 +1183,11 @@ func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) { require.NoError(t, err) } } -func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { - return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records) +func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { + return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, provider.NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records) } -func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, zoneTagFilter ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { +func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, zoneTagFilter provider.ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { client := NewRoute53APIStub() provider := &AWSProvider{ diff --git a/provider/aws_sd.go b/provider/awssd/aws_sd.go similarity index 99% rename from provider/aws_sd.go rename to provider/awssd/aws_sd.go index 149d8e41e..34016a7e3 100644 --- a/provider/aws_sd.go +++ b/provider/awssd/aws_sd.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package awssd import ( "context" @@ -37,6 +37,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/pkg/apis/externaldns" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -73,6 +74,7 @@ type AWSSDClient interface { // AWSSDProvider is an implementation of Provider for AWS Cloud Map. type AWSSDProvider struct { + provider.BaseProvider client AWSSDClient dryRun bool // only consider namespaces ending in this suffix diff --git a/provider/aws_sd_test.go b/provider/awssd/aws_sd_test.go similarity index 99% rename from provider/aws_sd_test.go rename to provider/awssd/aws_sd_test.go index 9e85cddf5..0ac5db556 100644 --- a/provider/aws_sd_test.go +++ b/provider/awssd/aws_sd_test.go @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package awssd import ( "context" "errors" "math/rand" "reflect" + "strconv" "testing" "time" @@ -51,7 +52,7 @@ type AWSSDClientStub struct { func (s *AWSSDClientStub) CreateService(input *sd.CreateServiceInput) (*sd.CreateServiceOutput, error) { srv := &sd.Service{ - Id: aws.String(string(rand.Intn(10000))), + Id: aws.String(strconv.Itoa(rand.Intn(10000))), DnsConfig: input.DnsConfig, Name: input.Name, Description: input.Description, diff --git a/provider/azure.go b/provider/azure/azure.go similarity index 97% rename from provider/azure.go rename to provider/azure/azure.go index dc58a778a..d216f5dde 100644 --- a/provider/azure.go +++ b/provider/azure/azure.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package azure import ( "context" @@ -34,6 +34,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -66,8 +67,9 @@ type RecordSetsClient interface { // AzureProvider implements the DNS provider for Microsoft's Azure cloud platform. type AzureProvider struct { + provider.BaseProvider domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter dryRun bool resourceGroup string userAssignedIdentityClientID string @@ -78,7 +80,7 @@ type AzureProvider struct { // NewAzureProvider creates a new Azure provider. // // Returns the provider or an error if a provider could not be created. -func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) { +func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) { contents, err := ioutil.ReadFile(configFile) if err != nil { return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) @@ -199,7 +201,7 @@ func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp return true } recordType := strings.TrimPrefix(*recordSet.Type, "Microsoft.Network/dnszones/") - if !supportedRecordType(recordType) { + if !provider.SupportedRecordType(recordType) { return true } name := formatAzureDNSName(*recordSet.Name, *zone.Name) @@ -300,7 +302,7 @@ func (p *AzureProvider) mapChanges(zones []dns.Zone, changes *plan.Changes) (azu ignored := map[string]bool{} deleted := azureChangeMap{} updated := azureChangeMap{} - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} for _, z := range zones { if z.Name != nil { zoneNameIDMapper.Add(*z.Name, *z.Name) diff --git a/provider/azure_private_dns.go b/provider/azure/azure_private_dns.go similarity index 97% rename from provider/azure_private_dns.go rename to provider/azure/azure_private_dns.go index 61588bee8..d07279d81 100644 --- a/provider/azure_private_dns.go +++ b/provider/azure/azure_private_dns.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package azure import ( "context" @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) // PrivateZonesClient is an interface of privatedns.PrivateZoneClient that can be stubbed for testing. @@ -45,8 +46,9 @@ type PrivateRecordSetsClient interface { // AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service type AzurePrivateDNSProvider struct { + provider.BaseProvider domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter dryRun bool subscriptionID string resourceGroup string @@ -57,7 +59,7 @@ type AzurePrivateDNSProvider struct { // NewAzurePrivateDNSProvider creates a new Azure Private DNS provider. // // Returns the provider or an error if a provider could not be created. -func NewAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, resourceGroup string, subscriptionID string, dryRun bool) (*AzurePrivateDNSProvider, error) { +func NewAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, subscriptionID string, dryRun bool) (*AzurePrivateDNSProvider, error) { authorizer, err := auth.NewAuthorizerFromEnvironment() if err != nil { return nil, err @@ -208,7 +210,7 @@ func (p *AzurePrivateDNSProvider) mapChanges(zones []privatedns.PrivateZone, cha ignored := map[string]bool{} deleted := azurePrivateDNSChangeMap{} updated := azurePrivateDNSChangeMap{} - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} for _, z := range zones { if z.Name != nil { zoneNameIDMapper.Add(*z.Name, *z.Name) diff --git a/provider/azure_privatedns_test.go b/provider/azure/azure_privatedns_test.go similarity index 96% rename from provider/azure_privatedns_test.go rename to provider/azure/azure_privatedns_test.go index df2499080..c5396ef57 100644 --- a/provider/azure_privatedns_test.go +++ b/provider/azure/azure_privatedns_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package azure import ( "context" @@ -27,6 +27,11 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +const ( + recordTTL = 300 ) // mockPrivateZonesClient implements the methods of the Azure Private DNS Zones Client which are used in the Azure Private DNS Provider @@ -203,7 +208,7 @@ func (client *mockPrivateRecordSetsClient) CreateOrUpdate(ctx context.Context, r } // newMockedAzurePrivateDNSProvider creates an AzureProvider comprising the mocked clients for zones and recordsets -func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, resourceGroup string, zones *[]privatedns.PrivateZone, recordSets *[]privatedns.RecordSet) (*AzurePrivateDNSProvider, error) { +func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, zones *[]privatedns.PrivateZone, recordSets *[]privatedns.RecordSet) (*AzurePrivateDNSProvider, error) { // init zone-related parts of the mock-client pageIterator := mockPrivateZoneListResultPageIterator{ results: []privatedns.PrivateZoneListResult{ @@ -236,7 +241,7 @@ func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneID return newAzurePrivateDNSProvider(domainFilter, zoneIDFilter, dryRun, resourceGroup, &zonesClient, &recordSetsClient), nil } -func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, resourceGroup string, privateZonesClient PrivateZonesClient, privateRecordsClient PrivateRecordSetsClient) *AzurePrivateDNSProvider { +func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, privateZonesClient PrivateZonesClient, privateRecordsClient PrivateRecordSetsClient) *AzurePrivateDNSProvider { return &AzurePrivateDNSProvider{ domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, @@ -248,7 +253,7 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter } func TestAzurePrivateDNSRecord(t *testing.T) { - provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s", + provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", &[]privatedns.PrivateZone{ createMockPrivateZone("example.com", "/privateDnsZones/example.com"), }, @@ -284,7 +289,7 @@ func TestAzurePrivateDNSRecord(t *testing.T) { } func TestAzurePrivateDNSMultiRecord(t *testing.T) { - provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s", + provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", &[]privatedns.PrivateZone{ createMockPrivateZone("example.com", "/privateDnsZones/example.com"), }, @@ -383,7 +388,7 @@ func testAzurePrivateDNSApplyChangesInternal(t *testing.T, dryRun bool, client P provider := newAzurePrivateDNSProvider( endpoint.NewDomainFilter([]string{""}), - NewZoneIDFilter([]string{""}), + provider.NewZoneIDFilter([]string{""}), dryRun, "group", &zonesClient, diff --git a/provider/azure_test.go b/provider/azure/azure_test.go similarity index 96% rename from provider/azure_test.go rename to provider/azure/azure_test.go index 45de8244e..30a21f2d6 100644 --- a/provider/azure_test.go +++ b/provider/azure/azure_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package azure import ( "context" @@ -30,6 +30,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) // mockZonesClient implements the methods of the Azure DNS Zones Client which are used in the Azure Provider @@ -206,7 +207,7 @@ func (client *mockRecordSetsClient) CreateOrUpdate(ctx context.Context, resource } // newMockedAzureProvider creates an AzureProvider comprising the mocked clients for zones and recordsets -func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zones *[]dns.Zone, recordSets *[]dns.RecordSet) (*AzureProvider, error) { +func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zones *[]dns.Zone, recordSets *[]dns.RecordSet) (*AzureProvider, error) { // init zone-related parts of the mock-client pageIterator := mockZoneListResultPageIterator{ results: []dns.ZoneListResult{ @@ -239,7 +240,7 @@ func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter Zon return newAzureProvider(domainFilter, zoneIDFilter, dryRun, resourceGroup, userAssignedIdentityClientID, &zonesClient, &recordSetsClient), nil } -func newAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zonesClient ZonesClient, recordsClient RecordSetsClient) *AzureProvider { +func newAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zonesClient ZonesClient, recordsClient RecordSetsClient) *AzureProvider { return &AzureProvider{ domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, @@ -256,7 +257,7 @@ func validateAzureEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expect } func TestAzureRecord(t *testing.T) { - provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s", "", + provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "", &[]dns.Zone{ createMockZone("example.com", "/dnszones/example.com"), }, @@ -293,7 +294,7 @@ func TestAzureRecord(t *testing.T) { } func TestAzureMultiRecord(t *testing.T) { - provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s", "", + provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "", &[]dns.Zone{ createMockZone("example.com", "/dnszones/example.com"), }, @@ -393,7 +394,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC provider := newAzureProvider( endpoint.NewDomainFilter([]string{""}), - NewZoneIDFilter([]string{""}), + provider.NewZoneIDFilter([]string{""}), dryRun, "group", "", diff --git a/provider/cloudflare/OWNERS b/provider/cloudflare/OWNERS new file mode 100644 index 000000000..89e3f6640 --- /dev/null +++ b/provider/cloudflare/OWNERS @@ -0,0 +1,2 @@ +approvers: + - sheerun diff --git a/provider/cloudflare.go b/provider/cloudflare/cloudflare.go similarity index 67% rename from provider/cloudflare.go rename to provider/cloudflare/cloudflare.go index 5fbf19351..d78764acd 100644 --- a/provider/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -14,13 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package cloudflare import ( "context" "fmt" "os" - "sort" "strconv" "strings" @@ -29,6 +28,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/source" ) @@ -58,6 +58,7 @@ type cloudFlareDNS interface { ZoneIDByName(zoneName string) (string, error) ListZones(zoneID ...string) ([]cloudflare.Zone, error) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) + ZoneDetails(zoneID string) (cloudflare.Zone, error) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) DeleteDNSRecord(zoneID, recordID string) error @@ -98,12 +99,17 @@ func (z zoneService) ListZonesContext(ctx context.Context, opts ...cloudflare.Re return z.service.ListZonesContext(ctx, opts...) } +func (z zoneService) ZoneDetails(zoneID string) (cloudflare.Zone, error) { + return z.service.ZoneDetails(zoneID) +} + // CloudFlareProvider is an implementation of Provider for CloudFlare DNS. type CloudFlareProvider struct { + provider.BaseProvider Client cloudFlareDNS // only consider hosted zones managing domains ending in this suffix domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter proxiedByDefault bool DryRun bool PaginationOptions cloudflare.PaginationOptions @@ -111,12 +117,12 @@ type CloudFlareProvider struct { // cloudFlareChange differentiates between ChangActions type cloudFlareChange struct { - Action string - ResourceRecordSet []cloudflare.DNSRecord + Action string + ResourceRecord cloudflare.DNSRecord } // NewCloudFlareProvider initializes a new CloudFlare DNS based Provider. -func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, zonesPerPage int, proxiedByDefault bool, dryRun bool) (*CloudFlareProvider, error) { +func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zonesPerPage int, proxiedByDefault bool, dryRun bool) (*CloudFlareProvider, error) { // initialize via chosen auth method and returns new API object var ( config *cloudflare.API @@ -150,6 +156,27 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro result := []cloudflare.Zone{} p.PaginationOptions.Page = 1 + // if there is a zoneIDfilter configured + // && if the filter isnt just a blank string (used in tests) + if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" { + log.Debugln("zoneIDFilter configured. only looking up zone IDs defined") + for _, zoneID := range p.zoneIDFilter.ZoneIDs { + log.Debugf("looking up zone %s", zoneID) + detailResponse, err := p.Client.ZoneDetails(zoneID) + if err != nil { + log.Errorf("zone %s lookup failed, %v", zoneID, err) + continue + } + log.WithFields(log.Fields{ + "zoneName": detailResponse.Name, + "zoneID": detailResponse.ID, + }).Debugln("adding zone for consideration") + result = append(result, detailResponse) + } + return result, nil + } + + log.Debugln("no zoneIDFilter configured, looking at all zones") for { zonesResponse, err := p.Client.ListZonesContext(ctx, cloudflare.WithPagination(p.PaginationOptions)) if err != nil { @@ -158,10 +185,7 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro for _, zone := range zonesResponse.Result { if !p.domainFilter.Match(zone.Name) { - continue - } - - if !p.zoneIDFilter.Match(zone.ID) { + log.Debugf("zone %s not in domain filter", zone.Name) continue } result = append(result, zone) @@ -199,15 +223,47 @@ func (p *CloudFlareProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, // ApplyChanges applies a given set of changes in a given zone. func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - proxiedByDefault := p.proxiedByDefault + cloudflareChanges := []*cloudFlareChange{} - combinedChanges := make([]*cloudFlareChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) + for _, endpoint := range changes.Create { + for _, target := range endpoint.Targets { + cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, endpoint, target)) + } + } - combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareCreate, changes.Create, proxiedByDefault)...) - combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareUpdate, changes.UpdateNew, proxiedByDefault)...) - combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareDelete, changes.Delete, proxiedByDefault)...) + for i, desired := range changes.UpdateNew { + current := changes.UpdateOld[i] - return p.submitChanges(ctx, combinedChanges) + add, remove, leave := provider.Difference(current.Targets, desired.Targets) + + for _, a := range add { + cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, desired, a)) + } + + for _, a := range leave { + cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareUpdate, desired, a)) + } + + for _, a := range remove { + cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, current, a)) + } + } + + for _, endpoint := range changes.Delete { + for _, target := range endpoint.Targets { + cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, endpoint, target)) + } + } + + return p.submitChanges(ctx, cloudflareChanges) +} + +func (p *CloudFlareProvider) PropertyValuesEqual(name string, previous string, current string) bool { + if name == source.CloudflareProxiedKey { + return plan.CompareBoolean(p.proxiedByDefault, name, previous, current) + } + + return p.BaseProvider.PropertyValuesEqual(name, previous, current) } // submitChanges takes a zone and a collection of Changes and sends them as a single transaction. @@ -231,12 +287,11 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud } for _, change := range changes { logFields := log.Fields{ - "record": change.ResourceRecordSet[0].Name, - "type": change.ResourceRecordSet[0].Type, - "ttl": change.ResourceRecordSet[0].TTL, - "targets": len(change.ResourceRecordSet), - "action": change.Action, - "zone": zoneID, + "record": change.ResourceRecord.Name, + "type": change.ResourceRecord.Type, + "ttl": change.ResourceRecord.TTL, + "action": change.Action, + "zone": zoneID, } log.WithFields(logFields).Info("Changing record.") @@ -245,24 +300,30 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud continue } - recordIDs := p.getRecordIDs(records, change.ResourceRecordSet[0]) - - // to simplify bookkeeping for multiple records, an update is executed as delete+create - if change.Action == cloudFlareDelete || change.Action == cloudFlareUpdate { - for _, recordID := range recordIDs { - err := p.Client.DeleteDNSRecord(zoneID, recordID) - if err != nil { - log.WithFields(logFields).Errorf("failed to delete record: %v", err) - } + if change.Action == cloudFlareUpdate { + recordID := p.getRecordID(records, change.ResourceRecord) + if recordID == "" { + log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord) + continue } - } - - if change.Action == cloudFlareCreate || change.Action == cloudFlareUpdate { - for _, record := range change.ResourceRecordSet { - _, err := p.Client.CreateDNSRecord(zoneID, record) - if err != nil { - log.WithFields(logFields).Errorf("failed to create record: %v", err) - } + err := p.Client.UpdateDNSRecord(zoneID, recordID, change.ResourceRecord) + if err != nil { + log.WithFields(logFields).Errorf("failed to delete record: %v", err) + } + } else if change.Action == cloudFlareDelete { + recordID := p.getRecordID(records, change.ResourceRecord) + if recordID == "" { + log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord) + continue + } + err := p.Client.DeleteDNSRecord(zoneID, recordID) + if err != nil { + log.WithFields(logFields).Errorf("failed to delete record: %v", err) + } + } else if change.Action == cloudFlareCreate { + _, err := p.Client.CreateDNSRecord(zoneID, change.ResourceRecord) + if err != nil { + log.WithFields(logFields).Errorf("failed to create record: %v", err) } } } @@ -273,7 +334,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud // changesByZone separates a multi-zone change into a single change per zone. func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange { changes := make(map[string][]*cloudFlareChange) - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} for _, z := range zones { zoneNameIDMapper.Add(z.ID, z.Name) @@ -281,9 +342,9 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet [] } for _, c := range changeSet { - zoneID, _ := zoneNameIDMapper.FindZone(c.ResourceRecordSet[0].Name) + zoneID, _ := zoneNameIDMapper.FindZone(c.ResourceRecord.Name) if zoneID == "" { - log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSet[0].Name) + log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecord.Name) continue } changes[zoneID] = append(changes[zoneID], c) @@ -292,51 +353,36 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet [] return changes } -func (p *CloudFlareProvider) getRecordIDs(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) []string { - recordIDs := make([]string, 0) +func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) string { for _, zoneRecord := range records { - if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type { - recordIDs = append(recordIDs, zoneRecord.ID) + if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type && zoneRecord.Content == record.Content { + return zoneRecord.ID } } - sort.Strings(recordIDs) - return recordIDs + return "" } -// newCloudFlareChanges returns a collection of Changes based on the given records and action. -func newCloudFlareChanges(action string, endpoints []*endpoint.Endpoint, proxiedByDefault bool) []*cloudFlareChange { - changes := make([]*cloudFlareChange, 0, len(endpoints)) - - for _, endpoint := range endpoints { - changes = append(changes, newCloudFlareChange(action, endpoint, proxiedByDefault)) - } - - return changes -} - -func newCloudFlareChange(action string, endpoint *endpoint.Endpoint, proxiedByDefault bool) *cloudFlareChange { +func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoint.Endpoint, target string) *cloudFlareChange { ttl := defaultCloudFlareRecordTTL - proxied := shouldBeProxied(endpoint, proxiedByDefault) + proxied := shouldBeProxied(endpoint, p.proxiedByDefault) if endpoint.RecordTTL.IsConfigured() { ttl = int(endpoint.RecordTTL) } - resourceRecordSet := make([]cloudflare.DNSRecord, len(endpoint.Targets)) + if len(endpoint.Targets) > 1 { + log.Errorf("Updates should have just one target") + } - for i := range endpoint.Targets { - resourceRecordSet[i] = cloudflare.DNSRecord{ + return &cloudFlareChange{ + Action: action, + ResourceRecord: cloudflare.DNSRecord{ Name: endpoint.DNSName, TTL: ttl, Proxied: proxied, Type: endpoint.RecordType, - Content: endpoint.Targets[i], - } - } - - return &cloudFlareChange{ - Action: action, - ResourceRecordSet: resourceRecordSet, + Content: target, + }, } } @@ -368,7 +414,7 @@ func groupByNameAndType(records []cloudflare.DNSRecord) []*endpoint.Endpoint { groups := map[string][]cloudflare.DNSRecord{} for _, r := range records { - if !supportedRecordType(r.Type) { + if !provider.SupportedRecordType(r.Type) { continue } diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go new file mode 100644 index 000000000..395f2b2e2 --- /dev/null +++ b/provider/cloudflare/cloudflare_test.go @@ -0,0 +1,1135 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudflare + +import ( + "context" + "errors" + "os" + "testing" + + cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/stretchr/testify/assert" + + "github.com/maxatome/go-testdeep/td" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +type MockAction struct { + Name string + ZoneId string + RecordId string + RecordData cloudflare.DNSRecord +} + +type mockCloudFlareClient struct { + User cloudflare.User + Zones map[string]string + Records map[string]map[string]cloudflare.DNSRecord + Actions []MockAction + listZonesError error + dnsRecordsError error +} + +var ExampleDomain = []cloudflare.DNSRecord{ + { + ID: "1234567890", + ZoneID: "001", + Name: "foobar.bar.com", + Type: endpoint.RecordTypeA, + TTL: 120, + Content: "1.2.3.4", + Proxied: false, + }, + { + ID: "2345678901", + ZoneID: "001", + Name: "foobar.bar.com", + Type: endpoint.RecordTypeA, + TTL: 120, + Content: "3.4.5.6", + Proxied: false, + }, + { + ID: "1231231233", + ZoneID: "002", + Name: "bar.foo.com", + Type: endpoint.RecordTypeA, + TTL: 1, + Content: "2.3.4.5", + Proxied: false, + }, +} + +func NewMockCloudFlareClient() *mockCloudFlareClient { + return &mockCloudFlareClient{ + User: cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, + Zones: map[string]string{ + "001": "bar.com", + "002": "foo.com", + }, + Records: map[string]map[string]cloudflare.DNSRecord{ + "001": {}, + "002": {}, + }, + } +} + +func NewMockCloudFlareClientWithRecords(records map[string][]cloudflare.DNSRecord) *mockCloudFlareClient { + m := NewMockCloudFlareClient() + + for zoneID, zoneRecords := range records { + if zone, ok := m.Records[zoneID]; ok { + for _, record := range zoneRecords { + zone[record.ID] = record + } + } + } + + return m +} + +func (m *mockCloudFlareClient) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + m.Actions = append(m.Actions, MockAction{ + Name: "Create", + ZoneId: zoneID, + RecordId: rr.ID, + RecordData: rr, + }) + if zone, ok := m.Records[zoneID]; ok { + zone[rr.ID] = rr + } + return nil, nil +} + +func (m *mockCloudFlareClient) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + if m.dnsRecordsError != nil { + return nil, m.dnsRecordsError + } + result := []cloudflare.DNSRecord{} + if zone, ok := m.Records[zoneID]; ok { + for _, record := range zone { + result = append(result, record) + } + return result, nil + } + return result, nil +} + +func (m *mockCloudFlareClient) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + m.Actions = append(m.Actions, MockAction{ + Name: "Update", + ZoneId: zoneID, + RecordId: recordID, + RecordData: rr, + }) + if zone, ok := m.Records[zoneID]; ok { + if _, ok := zone[recordID]; ok { + zone[recordID] = rr + } + } + return nil +} + +func (m *mockCloudFlareClient) DeleteDNSRecord(zoneID, recordID string) error { + m.Actions = append(m.Actions, MockAction{ + Name: "Delete", + ZoneId: zoneID, + RecordId: recordID, + }) + if zone, ok := m.Records[zoneID]; ok { + if _, ok := zone[recordID]; ok { + delete(zone, recordID) + return nil + } + } + return nil +} + +func (m *mockCloudFlareClient) UserDetails() (cloudflare.User, error) { + return m.User, nil +} + +func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) { + for id, name := range m.Zones { + if name == zoneName { + return id, nil + } + } + + return "", errors.New("Unknown zone: " + zoneName) +} + +func (m *mockCloudFlareClient) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + if m.listZonesError != nil { + return nil, m.listZonesError + } + + result := []cloudflare.Zone{} + + for zoneID, zoneName := range m.Zones { + result = append(result, cloudflare.Zone{ + ID: zoneID, + Name: zoneName, + }) + } + + return result, nil +} + +func (m *mockCloudFlareClient) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) { + if m.listZonesError != nil { + return cloudflare.ZonesResponse{}, m.listZonesError + } + + result := []cloudflare.Zone{} + + for zoneId, zoneName := range m.Zones { + result = append(result, cloudflare.Zone{ + ID: zoneId, + Name: zoneName, + }) + } + + return cloudflare.ZonesResponse{ + Result: result, + ResultInfo: cloudflare.ResultInfo{ + Page: 1, + TotalPages: 1, + }, + }, nil +} + +func (m *mockCloudFlareClient) ZoneDetails(zoneID string) (cloudflare.Zone, error) { + for id, zoneName := range m.Zones { + if zoneID == id { + return cloudflare.Zone{ + ID: zoneID, + Name: zoneName, + }, nil + } + } + + return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID) +} + +func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, args ...interface{}) { + t.Helper() + + var client *mockCloudFlareClient + + if provider.Client == nil { + client = NewMockCloudFlareClient() + provider.Client = client + } else { + client = provider.Client.(*mockCloudFlareClient) + } + + ctx := context.Background() + + records, err := provider.Records(ctx) + + if err != nil { + t.Fatalf("cannot fetch records, %s", err) + } + + plan := &plan.Plan{ + Current: records, + Desired: endpoints, + DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}), + } + + changes := plan.Calculate().Changes + + // Records other than A and CNAME are not supported by planner, just create them + for _, endpoint := range endpoints { + if endpoint.RecordType != "A" && endpoint.RecordType != "CNAME" { + changes.Create = append(changes.Create, endpoint) + } + } + + err = provider.ApplyChanges(context.Background(), changes) + + if err != nil { + t.Fatalf("cannot apply changes, %s", err) + } + + td.Cmp(t, client.Actions, actions, args...) +} + +func TestCloudflareA(t *testing.T) { + endpoints := []*endpoint.Endpoint{ + { + RecordType: "A", + DNSName: "bar.com", + Targets: endpoint.Targets{"127.0.0.1", "127.0.0.2"}, + }, + } + + AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: "A", + Name: "bar.com", + Content: "127.0.0.1", + TTL: 1, + Proxied: false, + }, + }, + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: "A", + Name: "bar.com", + Content: "127.0.0.2", + TTL: 1, + Proxied: false, + }, + }, + }) +} + +func TestCloudflareCname(t *testing.T) { + endpoints := []*endpoint.Endpoint{ + { + RecordType: "CNAME", + DNSName: "cname.bar.com", + Targets: endpoint.Targets{"google.com", "facebook.com"}, + }, + } + + AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: "CNAME", + Name: "cname.bar.com", + Content: "google.com", + TTL: 1, + Proxied: false, + }, + }, + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: "CNAME", + Name: "cname.bar.com", + Content: "facebook.com", + TTL: 1, + Proxied: false, + }, + }, + }) +} + +func TestCloudflareCustomTTL(t *testing.T) { + endpoints := []*endpoint.Endpoint{ + { + RecordType: "A", + DNSName: "ttl.bar.com", + Targets: endpoint.Targets{"127.0.0.1"}, + RecordTTL: 120, + }, + } + + AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: "A", + Name: "ttl.bar.com", + Content: "127.0.0.1", + TTL: 120, + Proxied: false, + }, + }, + }) +} + +func TestCloudflareProxiedDefault(t *testing.T) { + endpoints := []*endpoint.Endpoint{ + { + RecordType: "A", + DNSName: "bar.com", + Targets: endpoint.Targets{"127.0.0.1"}, + }, + } + + AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: "A", + Name: "bar.com", + Content: "127.0.0.1", + TTL: 1, + Proxied: true, + }, + }, + }) +} + +func TestCloudflareProxiedOverrideTrue(t *testing.T) { + endpoints := []*endpoint.Endpoint{ + { + RecordType: "A", + DNSName: "bar.com", + Targets: endpoint.Targets{"127.0.0.1"}, + ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "true", + }, + }, + }, + } + + AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: "A", + Name: "bar.com", + Content: "127.0.0.1", + TTL: 1, + Proxied: true, + }, + }, + }) +} + +func TestCloudflareProxiedOverrideFalse(t *testing.T) { + endpoints := []*endpoint.Endpoint{ + { + RecordType: "A", + DNSName: "bar.com", + Targets: endpoint.Targets{"127.0.0.1"}, + ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + } + + AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: "A", + Name: "bar.com", + Content: "127.0.0.1", + TTL: 1, + Proxied: false, + }, + }, + }) +} + +func TestCloudflareProxiedOverrideIllegal(t *testing.T) { + endpoints := []*endpoint.Endpoint{ + { + RecordType: "A", + DNSName: "bar.com", + Targets: endpoint.Targets{"127.0.0.1"}, + ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "asfasdfa", + }, + }, + }, + } + + AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{ + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: "A", + Name: "bar.com", + Content: "127.0.0.1", + TTL: 1, + Proxied: true, + }, + }, + }) +} + +func TestCloudflareSetProxied(t *testing.T) { + var testCases = []struct { + recordType string + domain string + proxiable bool + }{ + {"A", "bar.com", true}, + {"CNAME", "bar.com", true}, + {"TXT", "bar.com", false}, + {"MX", "bar.com", false}, + {"NS", "bar.com", false}, + {"SPF", "bar.com", false}, + {"SRV", "bar.com", false}, + {"A", "*.bar.com", false}, + } + + for _, testCase := range testCases { + endpoints := []*endpoint.Endpoint{ + { + RecordType: testCase.recordType, + DNSName: testCase.domain, + Targets: endpoint.Targets{"127.0.0.1"}, + ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "true", + }, + }, + }, + } + + AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{ + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Type: testCase.recordType, + Name: testCase.domain, + Content: "127.0.0.1", + TTL: 1, + Proxied: testCase.proxiable, + }, + }, + }, testCase.recordType+" record on "+testCase.domain) + } +} + +func TestCloudflareZones(t *testing.T) { + provider := &CloudFlareProvider{ + Client: NewMockCloudFlareClient(), + domainFilter: endpoint.NewDomainFilter([]string{"bar.com"}), + zoneIDFilter: provider.NewZoneIDFilter([]string{""}), + } + + zones, err := provider.Zones(context.Background()) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, 1, len(zones)) + assert.Equal(t, "bar.com", zones[0].Name) +} + +func TestCloudFlareZonesWithIDFilter(t *testing.T) { + client := NewMockCloudFlareClient() + client.listZonesError = errors.New("shouldn't need to list zones when ZoneIDFilter in use") + provider := &CloudFlareProvider{ + Client: client, + domainFilter: endpoint.NewDomainFilter([]string{"bar.com", "foo.com"}), + zoneIDFilter: provider.NewZoneIDFilter([]string{"001"}), + } + + zones, err := provider.Zones(context.Background()) + if err != nil { + t.Fatal(err) + } + + // foo.com should *not* be returned as it doesn't match ZoneID filter + assert.Equal(t, 1, len(zones)) + assert.Equal(t, "bar.com", zones[0].Name) +} + +func TestCloudflareRecords(t *testing.T) { + client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{ + "001": ExampleDomain, + }) + + provider := &CloudFlareProvider{ + Client: client, + } + ctx := context.Background() + + records, err := provider.Records(ctx) + if err != nil { + t.Errorf("should not fail, %s", err) + } + + assert.Equal(t, 2, len(records)) + client.dnsRecordsError = errors.New("failed to list dns records") + _, err = provider.Records(ctx) + if err == nil { + t.Errorf("expected to fail") + } + client.dnsRecordsError = nil + client.listZonesError = errors.New("failed to list zones") + _, err = provider.Records(ctx) + if err == nil { + t.Errorf("expected to fail") + } +} + +func TestCloudflareProvider(t *testing.T) { + _ = os.Setenv("CF_API_TOKEN", "abc123def") + _, err := NewCloudFlareProvider( + endpoint.NewDomainFilter([]string{"bar.com"}), + provider.NewZoneIDFilter([]string{""}), + 25, + false, + true) + if err != nil { + t.Errorf("should not fail, %s", err) + } + _ = os.Unsetenv("CF_API_TOKEN") + _ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx") + _ = os.Setenv("CF_API_EMAIL", "test@test.com") + _, err = NewCloudFlareProvider( + endpoint.NewDomainFilter([]string{"bar.com"}), + provider.NewZoneIDFilter([]string{""}), + 1, + false, + true) + if err != nil { + t.Errorf("should not fail, %s", err) + } + _ = os.Unsetenv("CF_API_KEY") + _ = os.Unsetenv("CF_API_EMAIL") + _, err = NewCloudFlareProvider( + endpoint.NewDomainFilter([]string{"bar.com"}), + provider.NewZoneIDFilter([]string{""}), + 50, + false, + true) + if err == nil { + t.Errorf("expected to fail") + } +} + +func TestCloudflareApplyChanges(t *testing.T) { + changes := &plan.Changes{} + client := NewMockCloudFlareClient() + provider := &CloudFlareProvider{ + Client: client, + } + changes.Create = []*endpoint.Endpoint{{ + DNSName: "new.bar.com", + Targets: endpoint.Targets{"target"}, + }, { + DNSName: "new.ext-dns-test.unrelated.to", + Targets: endpoint.Targets{"target"}, + }} + changes.Delete = []*endpoint.Endpoint{{ + DNSName: "foobar.bar.com", + Targets: endpoint.Targets{"target"}, + }} + changes.UpdateOld = []*endpoint.Endpoint{{ + DNSName: "foobar.bar.com", + Targets: endpoint.Targets{"target-old"}, + }} + changes.UpdateNew = []*endpoint.Endpoint{{ + DNSName: "foobar.bar.com", + Targets: endpoint.Targets{"target-new"}, + }} + err := provider.ApplyChanges(context.Background(), changes) + + if err != nil { + t.Errorf("should not fail, %s", err) + } + + td.Cmp(t, client.Actions, []MockAction{ + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Name: "new.bar.com", + Content: "target", + TTL: 1, + }, + }, + { + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Name: "foobar.bar.com", + Content: "target-new", + TTL: 1, + }, + }, + }) + + // empty changes + changes.Create = []*endpoint.Endpoint{} + changes.Delete = []*endpoint.Endpoint{} + changes.UpdateOld = []*endpoint.Endpoint{} + changes.UpdateNew = []*endpoint.Endpoint{} + + err = provider.ApplyChanges(context.Background(), changes) + if err != nil { + t.Errorf("should not fail, %s", err) + } +} + +func TestCloudflareGetRecordID(t *testing.T) { + p := &CloudFlareProvider{} + records := []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeCNAME, + Content: "foobar", + ID: "1", + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + ID: "2", + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "1.2.3.4", + ID: "2", + }, + } + + assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{ + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "foobar", + })) + + assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{ + Name: "foo.com", + Type: endpoint.RecordTypeCNAME, + Content: "fizfuz", + })) + + assert.Equal(t, "1", p.getRecordID(records, cloudflare.DNSRecord{ + Name: "foo.com", + Type: endpoint.RecordTypeCNAME, + Content: "foobar", + })) + assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{ + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "2.3.4.5", + })) + assert.Equal(t, "2", p.getRecordID(records, cloudflare.DNSRecord{ + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "1.2.3.4", + })) +} + +func TestCloudflareGroupByNameAndType(t *testing.T) { + testCases := []struct { + Name string + Records []cloudflare.DNSRecord + ExpectedEndpoints []*endpoint.Endpoint + }{ + { + Name: "empty", + Records: []cloudflare.DNSRecord{}, + ExpectedEndpoints: []*endpoint.Endpoint{}, + }, + { + Name: "single record - single target", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + { + Name: "single record - multiple targets", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + { + Name: "multiple record - multiple targets", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + { + DNSName: "bar.de", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + { + Name: "multiple record - mixed single/multiple targets", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + { + DNSName: "bar.de", + Targets: endpoint.Targets{"10.10.10.1"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + { + Name: "unsupported record type", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: "NOT SUPPORTED", + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + assert.ElementsMatch(t, groupByNameAndType(tc.Records), tc.ExpectedEndpoints) + } +} + +func TestProviderPropertiesIdempotency(t *testing.T) { + testCases := []struct { + ProviderProxiedByDefault bool + RecordsAreProxied bool + ShouldBeUpdated bool + }{ + { + ProviderProxiedByDefault: false, + RecordsAreProxied: false, + ShouldBeUpdated: false, + }, + { + ProviderProxiedByDefault: true, + RecordsAreProxied: true, + ShouldBeUpdated: false, + }, + { + ProviderProxiedByDefault: true, + RecordsAreProxied: false, + ShouldBeUpdated: true, + }, + { + ProviderProxiedByDefault: false, + RecordsAreProxied: true, + ShouldBeUpdated: true, + }, + } + + for _, test := range testCases { + client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{ + "001": { + { + ID: "1234567890", + ZoneID: "001", + Name: "foobar.bar.com", + Type: endpoint.RecordTypeA, + TTL: 120, + Content: "1.2.3.4", + Proxied: test.RecordsAreProxied, + }, + }, + }) + + provider := &CloudFlareProvider{ + Client: client, + proxiedByDefault: test.ProviderProxiedByDefault, + } + ctx := context.Background() + + current, err := provider.Records(ctx) + if err != nil { + t.Errorf("should not fail, %s", err) + } + assert.Equal(t, 1, len(current)) + + desired := []*endpoint.Endpoint{} + for _, c := range current { + // Copy all except ProviderSpecific fields + desired = append(desired, &endpoint.Endpoint{ + DNSName: c.DNSName, + Targets: c.Targets, + RecordType: c.RecordType, + SetIdentifier: c.SetIdentifier, + RecordTTL: c.RecordTTL, + Labels: c.Labels, + }) + } + + plan := plan.Plan{ + Current: current, + Desired: desired, + PropertyComparator: provider.PropertyValuesEqual, + } + + plan = *plan.Calculate() + assert.NotNil(t, plan.Changes, "should have plan") + if plan.Changes == nil { + return + } + assert.Equal(t, 0, len(plan.Changes.Create), "should not have creates") + assert.Equal(t, 0, len(plan.Changes.Delete), "should not have deletes") + + if test.ShouldBeUpdated { + assert.Equal(t, 1, len(plan.Changes.UpdateNew), "should not have new updates") + assert.Equal(t, 1, len(plan.Changes.UpdateOld), "should not have old updates") + } else { + assert.Equal(t, 0, len(plan.Changes.UpdateNew), "should not have new updates") + assert.Equal(t, 0, len(plan.Changes.UpdateOld), "should not have old updates") + } + } +} + +func TestCloudflareComplexUpdate(t *testing.T) { + client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{ + "001": ExampleDomain, + }) + + provider := &CloudFlareProvider{ + Client: client, + } + ctx := context.Background() + + records, err := provider.Records(ctx) + + if err != nil { + t.Errorf("should not fail, %s", err) + } + + plan := &plan.Plan{ + Current: records, + Desired: []*endpoint.Endpoint{ + { + DNSName: "foobar.bar.com", + Targets: endpoint.Targets{"1.2.3.4", "2.3.4.5"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "true", + }, + }, + }, + }, + DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}), + } + + planned := plan.Calculate() + + err = provider.ApplyChanges(context.Background(), planned.Changes) + + if err != nil { + t.Errorf("should not fail, %s", err) + } + + td.CmpDeeply(t, client.Actions, []MockAction{ + MockAction{ + Name: "Create", + ZoneId: "001", + RecordData: cloudflare.DNSRecord{ + Name: "foobar.bar.com", + Type: "A", + Content: "2.3.4.5", + TTL: 1, + Proxied: true, + }, + }, + MockAction{ + Name: "Update", + ZoneId: "001", + RecordId: "1234567890", + RecordData: cloudflare.DNSRecord{ + Name: "foobar.bar.com", + Type: "A", + Content: "1.2.3.4", + TTL: 1, + Proxied: true, + }, + }, + MockAction{ + Name: "Delete", + ZoneId: "001", + RecordId: "2345678901", + }, + }) +} diff --git a/provider/cloudflare_test.go b/provider/cloudflare_test.go deleted file mode 100644 index 87daddb29..000000000 --- a/provider/cloudflare_test.go +++ /dev/null @@ -1,607 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package provider - -import ( - "context" - "fmt" - "os" - "testing" - - cloudflare "github.com/cloudflare/cloudflare-go" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" -) - -type mockCloudFlareClient struct{} - -func (m *mockCloudFlareClient) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { - return nil, nil -} - -func (m *mockCloudFlareClient) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { - if zoneID == "1234567890" { - return []cloudflare.DNSRecord{ - {ID: "1234567890", Name: "foobar.ext-dns-test.zalando.to.", Type: endpoint.RecordTypeA, TTL: 120}, - {ID: "1231231233", Name: "foo.bar.com", TTL: 1}}, - nil - } - return nil, nil -} - -func (m *mockCloudFlareClient) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { - return nil -} - -func (m *mockCloudFlareClient) DeleteDNSRecord(zoneID, recordID string) error { - return nil -} - -func (m *mockCloudFlareClient) UserDetails() (cloudflare.User, error) { - return cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, nil -} - -func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) { - return "1234567890", nil -} - -func (m *mockCloudFlareClient) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { - return []cloudflare.Zone{{ID: "1234567890", Name: "ext-dns-test.zalando.to."}, {ID: "1234567891", Name: "foo.com."}}, nil -} - -func (m *mockCloudFlareClient) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) { - return cloudflare.ZonesResponse{ - Result: []cloudflare.Zone{ - {ID: "1234567890", Name: "ext-dns-test.zalando.to."}, - {ID: "1234567891", Name: "foo.com."}}, - ResultInfo: cloudflare.ResultInfo{ - Page: 1, - TotalPages: 1, - }, - }, nil -} - -type mockCloudFlareDNSRecordsFail struct{} - -func (m *mockCloudFlareDNSRecordsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { - return nil, nil -} - -func (m *mockCloudFlareDNSRecordsFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { - return []cloudflare.DNSRecord{}, fmt.Errorf("can not get records from zone") -} -func (m *mockCloudFlareDNSRecordsFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { - return nil -} - -func (m *mockCloudFlareDNSRecordsFail) DeleteDNSRecord(zoneID, recordID string) error { - return nil -} - -func (m *mockCloudFlareDNSRecordsFail) UserDetails() (cloudflare.User, error) { - return cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, nil -} - -func (m *mockCloudFlareDNSRecordsFail) ZoneIDByName(zoneName string) (string, error) { - return "", nil -} - -func (m *mockCloudFlareDNSRecordsFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { - return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil -} - -func (m *mockCloudFlareDNSRecordsFail) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) { - return cloudflare.ZonesResponse{ - Result: []cloudflare.Zone{ - {ID: "1234567890", Name: "ext-dns-test.zalando.to."}, - {ID: "1234567891", Name: "foo.com."}}, - ResultInfo: cloudflare.ResultInfo{ - TotalPages: 1, - }, - }, nil -} - -type mockCloudFlareListZonesFail struct{} - -func (m *mockCloudFlareListZonesFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { - return nil, nil -} - -func (m *mockCloudFlareListZonesFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { - return []cloudflare.DNSRecord{}, nil -} - -func (m *mockCloudFlareListZonesFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { - return nil -} - -func (m *mockCloudFlareListZonesFail) DeleteDNSRecord(zoneID, recordID string) error { - return nil -} - -func (m *mockCloudFlareListZonesFail) UserDetails() (cloudflare.User, error) { - return cloudflare.User{}, nil -} - -func (m *mockCloudFlareListZonesFail) ZoneIDByName(zoneName string) (string, error) { - return "1234567890", nil -} - -func (m *mockCloudFlareListZonesFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { - return []cloudflare.Zone{{}}, fmt.Errorf("no zones available") -} - -func (m *mockCloudFlareListZonesFail) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) { - return cloudflare.ZonesResponse{}, fmt.Errorf("no zones available") -} - -func TestNewCloudFlareChanges(t *testing.T) { - expect := []struct { - Name string - TTL int - }{ - { - "CustomRecordTTL", - 120, - }, - { - "DefaultRecordTTL", - 1, - }, - } - endpoints := []*endpoint.Endpoint{ - {DNSName: "new", Targets: endpoint.Targets{"target"}, RecordTTL: 120}, - {DNSName: "new2", Targets: endpoint.Targets{"target2"}}, - } - changes := newCloudFlareChanges(cloudFlareCreate, endpoints, true) - for i, change := range changes { - assert.Equal( - t, - change.ResourceRecordSet[0].TTL, - expect[i].TTL, - expect[i].Name) - } -} - -func TestNewCloudFlareChangeNoProxied(t *testing.T) { - change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Targets: endpoint.Targets{"target"}}, false) - assert.False(t, change.ResourceRecordSet[0].Proxied) -} - -func TestNewCloudFlareProxiedAnnotationTrue(t *testing.T) { - change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Targets: endpoint.Targets{"target"}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "true", - }, - }}, false) - assert.True(t, change.ResourceRecordSet[0].Proxied) -} - -func TestNewCloudFlareProxiedAnnotationFalse(t *testing.T) { - change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Targets: endpoint.Targets{"target"}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, - }}, true) - assert.False(t, change.ResourceRecordSet[0].Proxied) -} - -func TestNewCloudFlareProxiedAnnotationIllegalValue(t *testing.T) { - change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Targets: endpoint.Targets{"target"}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "asdaslkjndaslkdjals", - }, - }}, false) - assert.False(t, change.ResourceRecordSet[0].Proxied) -} - -func TestNewCloudFlareChangeProxiable(t *testing.T) { - var cloudFlareTypes = []struct { - recordType string - proxiable bool - }{ - {"A", true}, - {"CNAME", true}, - {"LOC", false}, - {"MX", false}, - {"NS", false}, - {"SPF", false}, - {"TXT", false}, - {"SRV", false}, - } - - for _, cloudFlareType := range cloudFlareTypes { - change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: cloudFlareType.recordType, Targets: endpoint.Targets{"target"}}, true) - - if cloudFlareType.proxiable { - assert.True(t, change.ResourceRecordSet[0].Proxied) - } else { - assert.False(t, change.ResourceRecordSet[0].Proxied) - } - } - - change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "*.foo", RecordType: "A", Targets: endpoint.Targets{"target"}}, true) - assert.False(t, change.ResourceRecordSet[0].Proxied) -} - -func TestCloudFlareZones(t *testing.T) { - provider := &CloudFlareProvider{ - Client: &mockCloudFlareClient{}, - domainFilter: endpoint.NewDomainFilter([]string{"zalando.to."}), - zoneIDFilter: NewZoneIDFilter([]string{""}), - } - - zones, err := provider.Zones(context.Background()) - if err != nil { - t.Fatal(err) - } - - validateCloudFlareZones(t, zones, []cloudflare.Zone{ - {Name: "ext-dns-test.zalando.to."}, - }) -} - -func TestRecords(t *testing.T) { - provider := &CloudFlareProvider{ - Client: &mockCloudFlareClient{}, - } - ctx := context.Background() - - records, err := provider.Records(ctx) - if err != nil { - t.Errorf("should not fail, %s", err) - } - - assert.Equal(t, 1, len(records)) - provider.Client = &mockCloudFlareDNSRecordsFail{} - _, err = provider.Records(ctx) - if err == nil { - t.Errorf("expected to fail") - } - provider.Client = &mockCloudFlareListZonesFail{} - _, err = provider.Records(ctx) - if err == nil { - t.Errorf("expected to fail") - } -} - -func TestNewCloudFlareProvider(t *testing.T) { - _ = os.Setenv("CF_API_TOKEN", "abc123def") - _, err := NewCloudFlareProvider( - endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), - NewZoneIDFilter([]string{""}), - 25, - false, - true) - if err != nil { - t.Errorf("should not fail, %s", err) - } - _ = os.Unsetenv("CF_API_TOKEN") - _ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx") - _ = os.Setenv("CF_API_EMAIL", "test@test.com") - _, err = NewCloudFlareProvider( - endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), - NewZoneIDFilter([]string{""}), - 1, - false, - true) - if err != nil { - t.Errorf("should not fail, %s", err) - } - _ = os.Unsetenv("CF_API_KEY") - _ = os.Unsetenv("CF_API_EMAIL") - _, err = NewCloudFlareProvider( - endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), - NewZoneIDFilter([]string{""}), - 50, - false, - true) - if err == nil { - t.Errorf("expected to fail") - } -} - -func TestApplyChanges(t *testing.T) { - changes := &plan.Changes{} - provider := &CloudFlareProvider{ - Client: &mockCloudFlareClient{}, - } - changes.Create = []*endpoint.Endpoint{{DNSName: "new.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target"}}, {DNSName: "new.ext-dns-test.unrelated.to.", Targets: endpoint.Targets{"target"}}} - changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target"}}} - changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target-old"}}} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target-new"}}} - err := provider.ApplyChanges(context.Background(), changes) - if err != nil { - t.Errorf("should not fail, %s", err) - } - - // empty changes - changes.Create = []*endpoint.Endpoint{} - changes.Delete = []*endpoint.Endpoint{} - changes.UpdateOld = []*endpoint.Endpoint{} - changes.UpdateNew = []*endpoint.Endpoint{} - - err = provider.ApplyChanges(context.Background(), changes) - if err != nil { - t.Errorf("should not fail, %s", err) - } -} - -func TestCloudFlareGetRecordID(t *testing.T) { - p := &CloudFlareProvider{} - records := []cloudflare.DNSRecord{ - { - Name: "foo.com", - Type: endpoint.RecordTypeCNAME, - ID: "1", - }, - { - Name: "bar.de", - Type: endpoint.RecordTypeA, - ID: "2", - }, - } - - assert.Len(t, p.getRecordIDs(records, cloudflare.DNSRecord{ - Name: "foo.com", - Type: endpoint.RecordTypeA, - }), 0) - assert.Len(t, p.getRecordIDs(records, cloudflare.DNSRecord{ - Name: "bar.de", - Type: endpoint.RecordTypeA, - }), 1) - assert.Equal(t, "2", p.getRecordIDs(records, cloudflare.DNSRecord{ - Name: "bar.de", - Type: endpoint.RecordTypeA, - })[0]) -} - -func validateCloudFlareZones(t *testing.T, zones []cloudflare.Zone, expected []cloudflare.Zone) { - require.Len(t, zones, len(expected)) - - for i, zone := range zones { - assert.Equal(t, expected[i].Name, zone.Name) - } -} - -func TestGroupByNameAndType(t *testing.T) { - testCases := []struct { - Name string - Records []cloudflare.DNSRecord - ExpectedEndpoints []*endpoint.Endpoint - }{ - { - Name: "empty", - Records: []cloudflare.DNSRecord{}, - ExpectedEndpoints: []*endpoint.Endpoint{}, - }, - { - Name: "single record - single target", - Records: []cloudflare.DNSRecord{ - { - Name: "foo.com", - Type: endpoint.RecordTypeA, - Content: "10.10.10.1", - TTL: defaultCloudFlareRecordTTL, - }, - }, - ExpectedEndpoints: []*endpoint.Endpoint{ - { - DNSName: "foo.com", - Targets: endpoint.Targets{"10.10.10.1"}, - RecordType: endpoint.RecordTypeA, - RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), - Labels: endpoint.Labels{}, - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, - }, - }, - }, - }, - { - Name: "single record - multiple targets", - Records: []cloudflare.DNSRecord{ - { - Name: "foo.com", - Type: endpoint.RecordTypeA, - Content: "10.10.10.1", - TTL: defaultCloudFlareRecordTTL, - }, - { - Name: "foo.com", - Type: endpoint.RecordTypeA, - Content: "10.10.10.2", - TTL: defaultCloudFlareRecordTTL, - }, - }, - ExpectedEndpoints: []*endpoint.Endpoint{ - { - DNSName: "foo.com", - Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, - RecordType: endpoint.RecordTypeA, - RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), - Labels: endpoint.Labels{}, - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, - }, - }, - }, - }, - { - Name: "multiple record - multiple targets", - Records: []cloudflare.DNSRecord{ - { - Name: "foo.com", - Type: endpoint.RecordTypeA, - Content: "10.10.10.1", - TTL: defaultCloudFlareRecordTTL, - }, - { - Name: "foo.com", - Type: endpoint.RecordTypeA, - Content: "10.10.10.2", - TTL: defaultCloudFlareRecordTTL, - }, - { - Name: "bar.de", - Type: endpoint.RecordTypeA, - Content: "10.10.10.1", - TTL: defaultCloudFlareRecordTTL, - }, - { - Name: "bar.de", - Type: endpoint.RecordTypeA, - Content: "10.10.10.2", - TTL: defaultCloudFlareRecordTTL, - }, - }, - ExpectedEndpoints: []*endpoint.Endpoint{ - { - DNSName: "foo.com", - Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, - RecordType: endpoint.RecordTypeA, - RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), - Labels: endpoint.Labels{}, - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, - }, - }, - { - DNSName: "bar.de", - Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, - RecordType: endpoint.RecordTypeA, - RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), - Labels: endpoint.Labels{}, - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, - }, - }, - }, - }, - { - Name: "multiple record - mixed single/multiple targets", - Records: []cloudflare.DNSRecord{ - { - Name: "foo.com", - Type: endpoint.RecordTypeA, - Content: "10.10.10.1", - TTL: defaultCloudFlareRecordTTL, - }, - { - Name: "foo.com", - Type: endpoint.RecordTypeA, - Content: "10.10.10.2", - TTL: defaultCloudFlareRecordTTL, - }, - { - Name: "bar.de", - Type: endpoint.RecordTypeA, - Content: "10.10.10.1", - TTL: defaultCloudFlareRecordTTL, - }, - }, - ExpectedEndpoints: []*endpoint.Endpoint{ - { - DNSName: "foo.com", - Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, - RecordType: endpoint.RecordTypeA, - RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), - Labels: endpoint.Labels{}, - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, - }, - }, - { - DNSName: "bar.de", - Targets: endpoint.Targets{"10.10.10.1"}, - RecordType: endpoint.RecordTypeA, - RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), - Labels: endpoint.Labels{}, - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, - }, - }, - }, - }, - { - Name: "unsupported record type", - Records: []cloudflare.DNSRecord{ - { - Name: "foo.com", - Type: endpoint.RecordTypeA, - Content: "10.10.10.1", - TTL: defaultCloudFlareRecordTTL, - }, - { - Name: "foo.com", - Type: endpoint.RecordTypeA, - Content: "10.10.10.2", - TTL: defaultCloudFlareRecordTTL, - }, - { - Name: "bar.de", - Type: "NOT SUPPORTED", - Content: "10.10.10.1", - TTL: defaultCloudFlareRecordTTL, - }, - }, - ExpectedEndpoints: []*endpoint.Endpoint{ - { - DNSName: "foo.com", - Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, - RecordType: endpoint.RecordTypeA, - RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), - Labels: endpoint.Labels{}, - ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, - }, - }, - }, - }, - } - - for _, tc := range testCases { - assert.ElementsMatch(t, groupByNameAndType(tc.Records), tc.ExpectedEndpoints) - } -} diff --git a/provider/coredns/OWNERS b/provider/coredns/OWNERS new file mode 100644 index 000000000..4734d4973 --- /dev/null +++ b/provider/coredns/OWNERS @@ -0,0 +1,2 @@ +approvers: + - ytsarev diff --git a/provider/coredns.go b/provider/coredns/coredns.go similarity index 98% rename from provider/coredns.go rename to provider/coredns/coredns.go index fc4dc9188..8c23f5a6b 100644 --- a/provider/coredns.go +++ b/provider/coredns/coredns.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package coredns import ( "context" @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) func init() { @@ -56,6 +57,7 @@ type coreDNSClient interface { } type coreDNSProvider struct { + provider.BaseProvider dryRun bool coreDNSPrefix string domainFilter endpoint.DomainFilter @@ -84,7 +86,7 @@ type Service struct { // answer. Group string `json:"group,omitempty"` - // Etcd key where we found this service and ignored from json un-/marshalling + // Etcd key where we found this service and ignored from json un-/marshaling Key string `json:"-"` } @@ -244,7 +246,7 @@ func newETCDClient() (coreDNSClient, error) { } // NewCoreDNSProvider is a CoreDNS provider constructor -func NewCoreDNSProvider(domainFilter endpoint.DomainFilter, prefix string, dryRun bool) (Provider, error) { +func NewCoreDNSProvider(domainFilter endpoint.DomainFilter, prefix string, dryRun bool) (provider.Provider, error) { client, err := newETCDClient() if err != nil { return nil, err @@ -395,7 +397,6 @@ func (p coreDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes } } } - } index := 0 for _, ep := range group { diff --git a/provider/coredns_test.go b/provider/coredns/coredns_test.go similarity index 99% rename from provider/coredns_test.go rename to provider/coredns/coredns_test.go index 380563e49..84f124ac5 100644 --- a/provider/coredns_test.go +++ b/provider/coredns/coredns_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package coredns import ( "context" diff --git a/provider/designate.go b/provider/designate/designate.go similarity index 99% rename from provider/designate.go rename to provider/designate/designate.go index 5d878dfb3..68160f0b8 100644 --- a/provider/designate.go +++ b/provider/designate/designate.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package designate import ( "context" @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/pkg/tlsutils" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -226,6 +227,7 @@ func (c designateClient) DeleteRecordSet(zoneID, recordSetID string) error { // designate provider type type designateProvider struct { + provider.BaseProvider client designateClientInterface // only consider hosted zones managing domains ending in this suffix @@ -234,7 +236,7 @@ type designateProvider struct { } // NewDesignateProvider is a factory function for OpenStack designate providers -func NewDesignateProvider(domainFilter endpoint.DomainFilter, dryRun bool) (Provider, error) { +func NewDesignateProvider(domainFilter endpoint.DomainFilter, dryRun bool) (provider.Provider, error) { client, err := newDesignateClient() if err != nil { return nil, err diff --git a/provider/designate_test.go b/provider/designate/designate_test.go similarity index 99% rename from provider/designate_test.go rename to provider/designate/designate_test.go index a37225c2f..290ee295d 100644 --- a/provider/designate_test.go +++ b/provider/designate/designate_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package designate import ( "context" @@ -34,6 +34,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) var lastGeneratedDesignateID int32 @@ -130,7 +131,7 @@ func (c fakeDesignateClient) DeleteRecordSet(zoneID, recordSetID string) error { return nil } -func (c fakeDesignateClient) ToProvider() Provider { +func (c fakeDesignateClient) ToProvider() provider.Provider { return &designateProvider{client: c} } diff --git a/provider/digital_ocean.go b/provider/digitalocean/digital_ocean.go similarity index 94% rename from provider/digital_ocean.go rename to provider/digitalocean/digital_ocean.go index 1426baee3..30af3eed2 100644 --- a/provider/digital_ocean.go +++ b/provider/digitalocean/digital_ocean.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package digitalocean import ( "context" @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -44,10 +45,13 @@ const ( // DigitalOceanProvider is an implementation of Provider for Digital Ocean's DNS. type DigitalOceanProvider struct { + provider.BaseProvider Client godo.DomainsService // only consider hosted zones managing domains ending in this suffix domainFilter endpoint.DomainFilter - DryRun bool + // page size when querying paginated APIs + apiPageSize int + DryRun bool } // DigitalOceanChange differentiates between ChangActions @@ -57,10 +61,10 @@ type DigitalOceanChange struct { } // NewDigitalOceanProvider initializes a new DigitalOcean DNS based Provider. -func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*DigitalOceanProvider, error) { +func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool, apiPageSize int) (*DigitalOceanProvider, error) { token, ok := os.LookupEnv("DO_TOKEN") if !ok { - return nil, fmt.Errorf("No token found") + return nil, fmt.Errorf("no token found") } oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ AccessToken: token, @@ -70,6 +74,7 @@ func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFi provider := &DigitalOceanProvider{ Client: client.Domains, domainFilter: domainFilter, + apiPageSize: apiPageSize, DryRun: dryRun, } return provider, nil @@ -107,7 +112,7 @@ func (p *DigitalOceanProvider) Records(ctx context.Context) ([]*endpoint.Endpoin } for _, r := range records { - if supportedRecordType(r.Type) { + if provider.SupportedRecordType(r.Type) { name := r.Name + "." + zone.Name // root name is identified by @ and should be @@ -126,7 +131,7 @@ func (p *DigitalOceanProvider) Records(ctx context.Context) ([]*endpoint.Endpoin func (p *DigitalOceanProvider) fetchRecords(ctx context.Context, zoneName string) ([]godo.DomainRecord, error) { allRecords := []godo.DomainRecord{} - listOptions := &godo.ListOptions{} + listOptions := &godo.ListOptions{PerPage: p.apiPageSize} for { records, resp, err := p.Client.Records(ctx, zoneName, listOptions) if err != nil { @@ -151,7 +156,7 @@ func (p *DigitalOceanProvider) fetchRecords(ctx context.Context, zoneName string func (p *DigitalOceanProvider) fetchZones(ctx context.Context) ([]godo.Domain, error) { allZones := []godo.Domain{} - listOptions := &godo.ListOptions{} + listOptions := &godo.ListOptions{PerPage: p.apiPageSize} for { zones, resp, err := p.Client.List(ctx, listOptions) if err != nil { @@ -314,7 +319,7 @@ func (p *DigitalOceanProvider) getRecordID(records []godo.DomainRecord, record g // digitalOceanchangesByZone separates a multi-zone change into a single change per zone. func digitalOceanChangesByZone(zones []godo.Domain, changeSet []*DigitalOceanChange) map[string][]*DigitalOceanChange { changes := make(map[string][]*DigitalOceanChange) - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} for _, z := range zones { zoneNameIDMapper.Add(z.Name, z.Name) changes[z.Name] = []*DigitalOceanChange{} diff --git a/provider/digital_ocean_test.go b/provider/digitalocean/digital_ocean_test.go similarity index 98% rename from provider/digital_ocean_test.go rename to provider/digitalocean/digital_ocean_test.go index 2d4740ffb..421b46fe3 100644 --- a/provider/digital_ocean_test.go +++ b/provider/digitalocean/digital_ocean_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package digitalocean import ( "context" @@ -187,12 +187,12 @@ func TestDigitalOceanApplyChanges(t *testing.T) { func TestNewDigitalOceanProvider(t *testing.T) { _ = os.Setenv("DO_TOKEN", "xxxxxxxxxxxxxxxxx") - _, err := NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true) + _, err := NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50) if err != nil { t.Errorf("should not fail, %s", err) } _ = os.Unsetenv("DO_TOKEN") - _, err = NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true) + _, err = NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50) if err == nil { t.Errorf("expected to fail") } diff --git a/provider/dnsimple.go b/provider/dnsimple/dnsimple.go similarity index 55% rename from provider/dnsimple.go rename to provider/dnsimple/dnsimple.go index 77ef974e9..8e357d1ab 100644 --- a/provider/dnsimple.go +++ b/provider/dnsimple/dnsimple.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package dnsimple import ( "context" @@ -25,70 +25,64 @@ import ( "github.com/dnsimple/dnsimple-go/dnsimple" log "github.com/sirupsen/logrus" + "golang.org/x/oauth2" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/pkg/apis/externaldns" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const dnsimpleRecordTTL = 3600 // Default TTL of 1 hour if not set (DNSimple's default) -type identityService struct { +type dnsimpleIdentityService struct { service *dnsimple.IdentityService } -func (i identityService) Whoami() (*dnsimple.WhoamiResponse, error) { - return i.service.Whoami() +func (i dnsimpleIdentityService) Whoami(ctx context.Context) (*dnsimple.WhoamiResponse, error) { + return i.service.Whoami(ctx) } -// Returns the account ID given dnsimple credentials -func (p *dnsimpleProvider) GetAccountID(credentials dnsimple.Credentials, client dnsimple.Client) (accountID string, err error) { - // get DNSimple client accountID - whoamiResponse, err := client.Identity.Whoami() - if err != nil { - return "", err - } - return strconv.Itoa(whoamiResponse.Data.Account.ID), nil -} - -// dnsimpleZoneServiceInterface is an interface that contains all necessary zone services from dnsimple +// dnsimpleZoneServiceInterface is an interface that contains all necessary zone services from DNSimple type dnsimpleZoneServiceInterface interface { - ListZones(accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) - ListRecords(accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) - CreateRecord(accountID string, zoneID string, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) - DeleteRecord(accountID string, zoneID string, recordID int) (*dnsimple.ZoneRecordResponse, error) - UpdateRecord(accountID string, zoneID string, recordID int, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) + ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) + ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) + CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) + DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error) + UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) } type dnsimpleZoneService struct { service *dnsimple.ZonesService } -func (z dnsimpleZoneService) ListZones(accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) { - return z.service.ListZones(accountID, options) +func (z dnsimpleZoneService) ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) { + return z.service.ListZones(ctx, accountID, options) } -func (z dnsimpleZoneService) ListRecords(accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) { - return z.service.ListRecords(accountID, zoneID, options) +func (z dnsimpleZoneService) ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) { + return z.service.ListRecords(ctx, accountID, zoneID, options) } -func (z dnsimpleZoneService) CreateRecord(accountID string, zoneID string, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) { - return z.service.CreateRecord(accountID, zoneID, recordAttributes) +func (z dnsimpleZoneService) CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) { + return z.service.CreateRecord(ctx, accountID, zoneID, recordAttributes) } -func (z dnsimpleZoneService) DeleteRecord(accountID string, zoneID string, recordID int) (*dnsimple.ZoneRecordResponse, error) { - return z.service.DeleteRecord(accountID, zoneID, recordID) +func (z dnsimpleZoneService) DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error) { + return z.service.DeleteRecord(ctx, accountID, zoneID, recordID) } -func (z dnsimpleZoneService) UpdateRecord(accountID string, zoneID string, recordID int, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) { - return z.service.UpdateRecord(accountID, zoneID, recordID, recordAttributes) +func (z dnsimpleZoneService) UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) { + return z.service.UpdateRecord(ctx, accountID, zoneID, recordID, recordAttributes) } type dnsimpleProvider struct { + provider.BaseProvider client dnsimpleZoneServiceInterface - identity identityService + identity dnsimpleIdentityService accountID string domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter dryRun bool } @@ -104,35 +98,52 @@ const ( ) // NewDnsimpleProvider initializes a new Dnsimple based provider -func NewDnsimpleProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) (Provider, error) { +func NewDnsimpleProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (provider.Provider, error) { oauthToken := os.Getenv("DNSIMPLE_OAUTH") if len(oauthToken) == 0 { - return nil, fmt.Errorf("No dnsimple oauth token provided") + return nil, fmt.Errorf("no dnsimple oauth token provided") } - client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(oauthToken)) + + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: oauthToken}) + tc := oauth2.NewClient(context.Background(), ts) + + client := dnsimple.NewClient(tc) + client.SetUserAgent(fmt.Sprintf("Kubernetes ExternalDNS/%s", externaldns.Version)) + provider := &dnsimpleProvider{ client: dnsimpleZoneService{service: client.Zones}, - identity: identityService{service: client.Identity}, + identity: dnsimpleIdentityService{service: client.Identity}, domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, dryRun: dryRun, } - whoamiResponse, err := provider.identity.service.Whoami() + + whoamiResponse, err := provider.identity.Whoami(context.Background()) if err != nil { return nil, err } - provider.accountID = strconv.Itoa(whoamiResponse.Data.Account.ID) + provider.accountID = int64ToString(whoamiResponse.Data.Account.ID) return provider, nil } +// GetAccountID returns the account ID given DNSimple credentials. +func (p *dnsimpleProvider) GetAccountID(ctx context.Context) (accountID string, err error) { + // get DNSimple client accountID + whoamiResponse, err := p.identity.Whoami(ctx) + if err != nil { + return "", err + } + return int64ToString(whoamiResponse.Data.Account.ID), nil +} + // Returns a list of filtered Zones -func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) { +func (p *dnsimpleProvider) Zones(ctx context.Context) (map[string]dnsimple.Zone, error) { zones := make(map[string]dnsimple.Zone) page := 1 listOptions := &dnsimple.ZoneListOptions{} for { - listOptions.Page = page - zonesResponse, err := p.client.ListZones(p.accountID, listOptions) + listOptions.Page = &page + zonesResponse, err := p.client.ListZones(ctx, p.accountID, listOptions) if err != nil { return nil, err } @@ -141,11 +152,11 @@ func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) { continue } - if !p.zoneIDFilter.Match(strconv.Itoa(zone.ID)) { + if !p.zoneIDFilter.Match(int64ToString(zone.ID)) { continue } - zones[strconv.Itoa(zone.ID)] = zone + zones[int64ToString(zone.ID)] = zone } page++ @@ -158,7 +169,7 @@ func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) { // Records returns a list of endpoints in a given zone func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { - zones, err := p.Zones() + zones, err := p.Zones(ctx) if err != nil { return nil, err } @@ -166,8 +177,8 @@ func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.E page := 1 listOptions := &dnsimple.ZoneRecordListOptions{} for { - listOptions.Page = page - records, err := p.client.ListRecords(p.accountID, zone.Name, listOptions) + listOptions.Page = &page + records, err := p.client.ListRecords(ctx, p.accountID, zone.Name, listOptions) if err != nil { return nil, err } @@ -224,12 +235,12 @@ func newDnsimpleChanges(action string, endpoints []*endpoint.Endpoint) []*dnsimp } // submitChanges takes a zone and a collection of changes and makes all changes from the collection -func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error { +func (p *dnsimpleProvider) submitChanges(ctx context.Context, changes []*dnsimpleChange) error { if len(changes) == 0 { log.Infof("All records are already up to date") return nil } - zones, err := p.Zones() + zones, err := p.Zones(ctx) if err != nil { return err } @@ -248,28 +259,35 @@ func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error { change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, fmt.Sprintf(".%s", zone.Name)) } + recordAttributes := dnsimple.ZoneRecordAttributes{ + Name: &change.ResourceRecordSet.Name, + Type: change.ResourceRecordSet.Type, + Content: change.ResourceRecordSet.Content, + TTL: change.ResourceRecordSet.TTL, + } + if !p.dryRun { switch change.Action { case dnsimpleCreate: - _, err := p.client.CreateRecord(p.accountID, zone.Name, change.ResourceRecordSet) + _, err := p.client.CreateRecord(ctx, p.accountID, zone.Name, recordAttributes) if err != nil { return err } case dnsimpleDelete: - recordID, err := p.GetRecordID(zone.Name, change.ResourceRecordSet.Name) + recordID, err := p.GetRecordID(ctx, zone.Name, *recordAttributes.Name) if err != nil { return err } - _, err = p.client.DeleteRecord(p.accountID, zone.Name, recordID) + _, err = p.client.DeleteRecord(ctx, p.accountID, zone.Name, recordID) if err != nil { return err } case dnsimpleUpdate: - recordID, err := p.GetRecordID(zone.Name, change.ResourceRecordSet.Name) + recordID, err := p.GetRecordID(ctx, zone.Name, *recordAttributes.Name) if err != nil { return err } - _, err = p.client.UpdateRecord(p.accountID, zone.Name, recordID, change.ResourceRecordSet) + _, err = p.client.UpdateRecord(ctx, p.accountID, zone.Name, recordID, recordAttributes) if err != nil { return err } @@ -279,13 +297,13 @@ func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error { return nil } -// Returns the record ID for a given record name and zone -func (p *dnsimpleProvider) GetRecordID(zone string, recordName string) (recordID int, err error) { +// GetRecordID returns the record ID for a given record name and zone. +func (p *dnsimpleProvider) GetRecordID(ctx context.Context, zone string, recordName string) (recordID int64, err error) { page := 1 - listOptions := &dnsimple.ZoneRecordListOptions{Name: recordName} + listOptions := &dnsimple.ZoneRecordListOptions{Name: &recordName} for { - listOptions.Page = page - records, err := p.client.ListRecords(p.accountID, zone, listOptions) + listOptions.Page = &page + records, err := p.client.ListRecords(ctx, p.accountID, zone, listOptions) if err != nil { return 0, err } @@ -301,7 +319,7 @@ func (p *dnsimpleProvider) GetRecordID(zone string, recordName string) (recordID break } } - return 0, fmt.Errorf("No record id found") + return 0, fmt.Errorf("no record id found") } // dnsimpleSuitableZone returns the most suitable zone for a given hostname and a set of zones. @@ -319,18 +337,18 @@ func dnsimpleSuitableZone(hostname string, zones map[string]dnsimple.Zone) *dnsi } // CreateRecords creates records for a given slice of endpoints -func (p *dnsimpleProvider) CreateRecords(endpoints []*endpoint.Endpoint) error { - return p.submitChanges(newDnsimpleChanges(dnsimpleCreate, endpoints)) +func (p *dnsimpleProvider) CreateRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error { + return p.submitChanges(ctx, newDnsimpleChanges(dnsimpleCreate, endpoints)) } // DeleteRecords deletes records for a given slice of endpoints -func (p *dnsimpleProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error { - return p.submitChanges(newDnsimpleChanges(dnsimpleDelete, endpoints)) +func (p *dnsimpleProvider) DeleteRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error { + return p.submitChanges(ctx, newDnsimpleChanges(dnsimpleDelete, endpoints)) } // UpdateRecords updates records for a given slice of endpoints -func (p *dnsimpleProvider) UpdateRecords(endpoints []*endpoint.Endpoint) error { - return p.submitChanges(newDnsimpleChanges(dnsimpleUpdate, endpoints)) +func (p *dnsimpleProvider) UpdateRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error { + return p.submitChanges(ctx, newDnsimpleChanges(dnsimpleUpdate, endpoints)) } // ApplyChanges applies a given set of changes @@ -341,5 +359,9 @@ func (p *dnsimpleProvider) ApplyChanges(ctx context.Context, changes *plan.Chang combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleUpdate, changes.UpdateNew)...) combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleDelete, changes.Delete)...) - return p.submitChanges(combinedChanges) + return p.submitChanges(ctx, combinedChanges) +} + +func int64ToString(i int64) string { + return strconv.FormatInt(i, 10) } diff --git a/provider/dnsimple_test.go b/provider/dnsimple/dnsimple_test.go similarity index 63% rename from provider/dnsimple_test.go rename to provider/dnsimple/dnsimple_test.go index ec87f4ba1..ed3032edd 100644 --- a/provider/dnsimple_test.go +++ b/provider/dnsimple/dnsimple_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package dnsimple import ( "context" @@ -22,8 +22,6 @@ import ( "os" "testing" - "strconv" - "github.com/dnsimple/dnsimple-go/dnsimple" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -31,6 +29,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) var mockProvider dnsimpleProvider @@ -102,15 +101,17 @@ func TestDnsimpleServices(t *testing.T) { } // Setup mock services + // Note: AnythingOfType doesn't work with interfaces https://github.com/stretchr/testify/issues/519 mockDNS := &mockDnsimpleZoneServiceInterface{} - mockDNS.On("ListZones", "1", &dnsimple.ZoneListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimpleListZonesResponse, nil) - mockDNS.On("ListZones", "2", &dnsimple.ZoneListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(nil, fmt.Errorf("Account ID not found")) - mockDNS.On("ListRecords", "1", "example.com", &dnsimple.ZoneRecordListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimpleListRecordsResponse, nil) - mockDNS.On("ListRecords", "1", "example-beta.com", &dnsimple.ZoneRecordListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimple.ZoneRecordsResponse{Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}}}, nil) + mockDNS.On("ListZones", mock.AnythingOfType("*context.emptyCtx"), "1", &dnsimple.ZoneListOptions{ListOptions: dnsimple.ListOptions{Page: dnsimple.Int(1)}}).Return(&dnsimpleListZonesResponse, nil) + mockDNS.On("ListZones", mock.AnythingOfType("*context.emptyCtx"), "2", &dnsimple.ZoneListOptions{ListOptions: dnsimple.ListOptions{Page: dnsimple.Int(1)}}).Return(nil, fmt.Errorf("Account ID not found")) + mockDNS.On("ListRecords", mock.AnythingOfType("*context.emptyCtx"), "1", "example.com", &dnsimple.ZoneRecordListOptions{ListOptions: dnsimple.ListOptions{Page: dnsimple.Int(1)}}).Return(&dnsimpleListRecordsResponse, nil) + mockDNS.On("ListRecords", mock.AnythingOfType("*context.emptyCtx"), "1", "example-beta.com", &dnsimple.ZoneRecordListOptions{ListOptions: dnsimple.ListOptions{Page: dnsimple.Int(1)}}).Return(&dnsimple.ZoneRecordsResponse{Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}}}, nil) for _, record := range records { - simpleRecord := dnsimple.ZoneRecord{ - Name: record.Name, + recordName := record.Name + simpleRecord := dnsimple.ZoneRecordAttributes{ + Name: &recordName, Type: record.Type, Content: record.Content, TTL: record.TTL, @@ -121,10 +122,10 @@ func TestDnsimpleServices(t *testing.T) { Data: []dnsimple.ZoneRecord{record}, } - mockDNS.On("ListRecords", "1", record.ZoneID, &dnsimple.ZoneRecordListOptions{Name: record.Name, ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimpleRecordResponse, nil) - mockDNS.On("CreateRecord", "1", record.ZoneID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil) - mockDNS.On("DeleteRecord", "1", record.ZoneID, record.ID).Return(&dnsimple.ZoneRecordResponse{}, nil) - mockDNS.On("UpdateRecord", "1", record.ZoneID, record.ID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil) + mockDNS.On("ListRecords", mock.AnythingOfType("*context.emptyCtx"), "1", record.ZoneID, &dnsimple.ZoneRecordListOptions{Name: &recordName, ListOptions: dnsimple.ListOptions{Page: dnsimple.Int(1)}}).Return(&dnsimpleRecordResponse, nil) + mockDNS.On("CreateRecord", mock.AnythingOfType("*context.emptyCtx"), "1", record.ZoneID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil) + mockDNS.On("DeleteRecord", mock.AnythingOfType("*context.emptyCtx"), "1", record.ZoneID, record.ID).Return(&dnsimple.ZoneRecordResponse{}, nil) + mockDNS.On("UpdateRecord", mock.AnythingOfType("*context.emptyCtx"), "1", record.ZoneID, record.ID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil) } mockProvider = dnsimpleProvider{client: mockDNS} @@ -139,13 +140,14 @@ func TestDnsimpleServices(t *testing.T) { } func testDnsimpleProviderZones(t *testing.T) { + ctx := context.Background() mockProvider.accountID = "1" - result, err := mockProvider.Zones() + result, err := mockProvider.Zones(ctx) assert.Nil(t, err) validateDnsimpleZones(t, result, dnsimpleListZonesResponse.Data) mockProvider.accountID = "2" - _, err = mockProvider.Zones() + _, err = mockProvider.Zones(ctx) assert.NotNil(t, err) } @@ -160,13 +162,16 @@ func testDnsimpleProviderRecords(t *testing.T) { _, err = mockProvider.Records(ctx) assert.NotNil(t, err) } + func testDnsimpleProviderApplyChanges(t *testing.T) { changes := &plan.Changes{} changes.Create = []*endpoint.Endpoint{ {DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}, {DNSName: "custom-ttl.example.com", RecordTTL: 60, Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}, } - changes.Delete = []*endpoint.Endpoint{{DNSName: "example-beta.example.com", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: endpoint.RecordTypeA}} + changes.Delete = []*endpoint.Endpoint{ + {DNSName: "example-beta.example.com", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: endpoint.RecordTypeA}, + } changes.UpdateNew = []*endpoint.Endpoint{ {DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}, {DNSName: "example.com", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: endpoint.RecordTypeA}, @@ -193,8 +198,9 @@ func testDnsimpleProviderApplyChangesSkipsUnknown(t *testing.T) { } func testDnsimpleSuitableZone(t *testing.T) { + ctx := context.Background() mockProvider.accountID = "1" - zones, err := mockProvider.Zones() + zones, err := mockProvider.Zones(ctx) assert.Nil(t, err) zone := dnsimpleSuitableZone("example-beta.example.com", zones) @@ -203,7 +209,7 @@ func testDnsimpleSuitableZone(t *testing.T) { func TestNewDnsimpleProvider(t *testing.T) { os.Setenv("DNSIMPLE_OAUTH", "xxxxxxxxxxxxxxxxxxxxxxxxxx") - _, err := NewDnsimpleProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true) + _, err := NewDnsimpleProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true) if err == nil { t.Errorf("Expected to fail new provider on bad token") } @@ -214,21 +220,24 @@ func TestNewDnsimpleProvider(t *testing.T) { } func testDnsimpleGetRecordID(t *testing.T) { - mockProvider.accountID = "1" - result, err := mockProvider.GetRecordID("example.com", "example") - assert.Nil(t, err) - assert.Equal(t, 2, result) + var result int64 + var err error - result, err = mockProvider.GetRecordID("example.com", "example-beta") + mockProvider.accountID = "1" + result, err = mockProvider.GetRecordID(context.Background(), "example.com", "example") assert.Nil(t, err) - assert.Equal(t, 1, result) + assert.Equal(t, int64(2), result) + + result, err = mockProvider.GetRecordID(context.Background(), "example.com", "example-beta") + assert.Nil(t, err) + assert.Equal(t, int64(1), result) } func validateDnsimpleZones(t *testing.T, zones map[string]dnsimple.Zone, expected []dnsimple.Zone) { require.Len(t, zones, len(expected)) for _, e := range expected { - assert.Equal(t, zones[strconv.Itoa(e.ID)].Name, e.Name) + assert.Equal(t, zones[int64ToString(e.ID)].Name, e.Name) } } @@ -236,8 +245,8 @@ type mockDnsimpleZoneServiceInterface struct { mock.Mock } -func (_m *mockDnsimpleZoneServiceInterface) CreateRecord(accountID string, zoneID string, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) { - args := _m.Called(accountID, zoneID, recordAttributes) +func (_m *mockDnsimpleZoneServiceInterface) CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) { + args := _m.Called(ctx, accountID, zoneID, recordAttributes) var r0 *dnsimple.ZoneRecordResponse if args.Get(0) != nil { @@ -247,8 +256,8 @@ func (_m *mockDnsimpleZoneServiceInterface) CreateRecord(accountID string, zoneI return r0, args.Error(1) } -func (_m *mockDnsimpleZoneServiceInterface) DeleteRecord(accountID string, zoneID string, recordID int) (*dnsimple.ZoneRecordResponse, error) { - args := _m.Called(accountID, zoneID, recordID) +func (_m *mockDnsimpleZoneServiceInterface) DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error) { + args := _m.Called(ctx, accountID, zoneID, recordID) var r0 *dnsimple.ZoneRecordResponse if args.Get(0) != nil { @@ -258,8 +267,8 @@ func (_m *mockDnsimpleZoneServiceInterface) DeleteRecord(accountID string, zoneI return r0, args.Error(1) } -func (_m *mockDnsimpleZoneServiceInterface) ListRecords(accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) { - args := _m.Called(accountID, zoneID, options) +func (_m *mockDnsimpleZoneServiceInterface) ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) { + args := _m.Called(ctx, accountID, zoneID, options) var r0 *dnsimple.ZoneRecordsResponse if args.Get(0) != nil { @@ -269,8 +278,8 @@ func (_m *mockDnsimpleZoneServiceInterface) ListRecords(accountID string, zoneID return r0, args.Error(1) } -func (_m *mockDnsimpleZoneServiceInterface) ListZones(accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) { - args := _m.Called(accountID, options) +func (_m *mockDnsimpleZoneServiceInterface) ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) { + args := _m.Called(ctx, accountID, options) var r0 *dnsimple.ZonesResponse if args.Get(0) != nil { @@ -280,8 +289,8 @@ func (_m *mockDnsimpleZoneServiceInterface) ListZones(accountID string, options return r0, args.Error(1) } -func (_m *mockDnsimpleZoneServiceInterface) UpdateRecord(accountID string, zoneID string, recordID int, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) { - args := _m.Called(accountID, zoneID, recordID, recordAttributes) +func (_m *mockDnsimpleZoneServiceInterface) UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) { + args := _m.Called(ctx, accountID, zoneID, recordID, recordAttributes) var r0 *dnsimple.ZoneRecordResponse if args.Get(0) != nil { diff --git a/provider/dyn.go b/provider/dyn/dyn.go similarity index 97% rename from provider/dyn.go rename to provider/dyn/dyn.go index 5829623de..1bc19fc3f 100644 --- a/provider/dyn.go +++ b/provider/dyn/dyn.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package dyn import ( "context" @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -57,7 +58,7 @@ func unixNow() int64 { // DynConfig hold connection parameters to dyn.com and internal state type DynConfig struct { DomainFilter endpoint.DomainFilter - ZoneIDFilter ZoneIDFilter + ZoneIDFilter provider.ZoneIDFilter DryRun bool CustomerName string Username string @@ -103,6 +104,7 @@ func (snap *ZoneSnapshot) StoreRecordsForSerial(zone string, serial int, records // DynProvider is the actual interface impl. type dynProviderState struct { + provider.BaseProvider DynConfig LastLoginErrorTime int64 @@ -141,7 +143,7 @@ type ZonePublishResponse struct { } // NewDynProvider initializes a new Dyn Provider. -func NewDynProvider(config DynConfig) (Provider, error) { +func NewDynProvider(config DynConfig) (provider.Provider, error) { return &dynProviderState{ DynConfig: config, ZoneSnapshot: &ZoneSnapshot{ @@ -156,7 +158,6 @@ func NewDynProvider(config DynConfig) (Provider, error) { func filterAndFixLinks(links []string, filter endpoint.DomainFilter) []string { var result []string for _, link := range links { - // link looks like /REST/CNAMERecord/acme.com/exchange.acme.com/349386875 // strip /REST/ @@ -290,7 +291,6 @@ func (d *dynProviderState) allRecordsToEndpoints(records *dynectsoap.GetAllRecor } return result - } func errorOrValue(err error, value interface{}) interface{} { @@ -392,7 +392,6 @@ func (d *dynProviderState) fetchAllRecordsInZone(zone string) (*dynectsoap.GetAl } return &records, nil - } // buildLinkToRecord build a resource link. The symmetry of the dyn API is used to save @@ -404,7 +403,7 @@ func (d *dynProviderState) buildLinkToRecord(ep *endpoint.Endpoint) string { return "" } var matchingZone = "" - for _, zone := range d.ZoneIDFilter.zoneIDs { + for _, zone := range d.ZoneIDFilter.ZoneIDs { if strings.HasSuffix(ep.DNSName, zone) { matchingZone = zone break @@ -460,7 +459,7 @@ func (d *dynProviderState) login() (*dynect.Client, error) { // the zones we are allowed to touch. Currently only exact matches are considered, not all // zones with the given suffix func (d *dynProviderState) zones(client *dynect.Client) []string { - return d.ZoneIDFilter.zoneIDs + return d.ZoneIDFilter.ZoneIDs } func (d *dynProviderState) buildRecordRequest(ep *endpoint.Endpoint) (string, *dynect.RecordRequest) { @@ -568,7 +567,7 @@ func (d *dynProviderState) commit(client *dynect.Client) error { case 1: return errs[0] default: - return fmt.Errorf("Multiple errors committing: %+v", errs) + return fmt.Errorf("multiple errors committing: %+v", errs) } } @@ -679,7 +678,7 @@ func (d *dynProviderState) ApplyChanges(ctx context.Context, changes *plan.Chang case 1: return errs[0] default: - return fmt.Errorf("Multiple errors committing: %+v", errs) + return fmt.Errorf("multiple errors committing: %+v", errs) } if needsCommit { diff --git a/provider/dyn_test.go b/provider/dyn/dyn_test.go similarity index 98% rename from provider/dyn_test.go rename to provider/dyn/dyn_test.go index 766a275cd..296052178 100644 --- a/provider/dyn_test.go +++ b/provider/dyn/dyn_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package dyn import ( "errors" @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/assert" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/provider" ) func TestDynMerge_NoUpdateOnTTL0Changes(t *testing.T) { @@ -187,7 +188,7 @@ func TestDyn_endpointToRecord(t *testing.T) { func TestDyn_buildLinkToRecord(t *testing.T) { provider := &dynProviderState{ DynConfig: DynConfig{ - ZoneIDFilter: NewZoneIDFilter([]string{"example.com"}), + ZoneIDFilter: provider.NewZoneIDFilter([]string{"example.com"}), DomainFilter: endpoint.NewDomainFilter([]string{"the-target.example.com"}), }, } diff --git a/provider/exoscale.go b/provider/exoscale/exoscale.go similarity index 88% rename from provider/exoscale.go rename to provider/exoscale/exoscale.go index 636ded342..1c3c89f5a 100644 --- a/provider/exoscale.go +++ b/provider/exoscale/exoscale.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package exoscale import ( "context" @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) // EgoscaleClientI for replaceable implementation @@ -38,6 +39,7 @@ type EgoscaleClientI interface { // ExoscaleProvider initialized as dns provider with no records type ExoscaleProvider struct { + provider.BaseProvider domain endpoint.DomainFilter client EgoscaleClientI filter *zoneFilter @@ -257,3 +259,38 @@ func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[int64 } return matchZoneID, name } + +func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint { + findMatch := func(template *endpoint.Endpoint) *endpoint.Endpoint { + for _, new := range updateNew { + if template.DNSName == new.DNSName && + template.RecordType == new.RecordType { + return new + } + } + return nil + } + + var result []*endpoint.Endpoint + for _, old := range updateOld { + matchingNew := findMatch(old) + if matchingNew == nil { + // no match, shouldn't happen + continue + } + + if !matchingNew.Targets.Same(old.Targets) { + // new target: always update, TTL will be overwritten too if necessary + result = append(result, matchingNew) + continue + } + + if matchingNew.RecordTTL != 0 && matchingNew.RecordTTL != old.RecordTTL { + // same target, but new non-zero TTL set in k8s, must update + // probably would happen only if there is a bug in the code calling the provider + result = append(result, matchingNew) + } + } + + return result +} diff --git a/provider/exoscale_test.go b/provider/exoscale/exoscale_test.go similarity index 60% rename from provider/exoscale_test.go rename to provider/exoscale/exoscale_test.go index 7a33bd9a1..d82d0d7cd 100644 --- a/provider/exoscale_test.go +++ b/provider/exoscale/exoscale_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package exoscale import ( "context" @@ -189,3 +189,144 @@ func TestExoscaleApplyChanges(t *testing.T) { assert.Equal(t, "foo.com", updateExoscale[0].name) assert.Equal(t, int64(1), updateExoscale[0].updateDNSRecord.ID) } + +func TestExoscaleMerge_NoUpdateOnTTL0Changes(t *testing.T) { + updateOld := []*endpoint.Endpoint{ + { + DNSName: "name1", + Targets: endpoint.Targets{"target1"}, + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "name2", + Targets: endpoint.Targets{"target2"}, + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeA, + }, + } + + updateNew := []*endpoint.Endpoint{ + { + DNSName: "name1", + Targets: endpoint.Targets{"target1"}, + RecordTTL: endpoint.TTL(0), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Targets: endpoint.Targets{"target2"}, + RecordTTL: endpoint.TTL(0), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + assert.Equal(t, 0, len(merge(updateOld, updateNew))) +} + +func TestExoscaleMerge_UpdateOnTTLChanges(t *testing.T) { + updateOld := []*endpoint.Endpoint{ + { + DNSName: "name1", + Targets: endpoint.Targets{"target1"}, + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Targets: endpoint.Targets{"target2"}, + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + updateNew := []*endpoint.Endpoint{ + { + DNSName: "name1", + Targets: endpoint.Targets{"target1"}, + RecordTTL: endpoint.TTL(77), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Targets: endpoint.Targets{"target2"}, + RecordTTL: endpoint.TTL(10), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + merged := merge(updateOld, updateNew) + assert.Equal(t, 2, len(merged)) + assert.Equal(t, "name1", merged[0].DNSName) +} + +func TestExoscaleMerge_AlwaysUpdateTarget(t *testing.T) { + updateOld := []*endpoint.Endpoint{ + { + DNSName: "name1", + Targets: endpoint.Targets{"target1"}, + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Targets: endpoint.Targets{"target2"}, + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + updateNew := []*endpoint.Endpoint{ + { + DNSName: "name1", + Targets: endpoint.Targets{"target1-changed"}, + RecordTTL: endpoint.TTL(0), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Targets: endpoint.Targets{"target2"}, + RecordTTL: endpoint.TTL(0), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + merged := merge(updateOld, updateNew) + assert.Equal(t, 1, len(merged)) + assert.Equal(t, "target1-changed", merged[0].Targets[0]) +} + +func TestExoscaleMerge_NoUpdateIfTTLUnchanged(t *testing.T) { + updateOld := []*endpoint.Endpoint{ + { + DNSName: "name1", + Targets: endpoint.Targets{"target1"}, + RecordTTL: endpoint.TTL(55), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Targets: endpoint.Targets{"target2"}, + RecordTTL: endpoint.TTL(55), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + updateNew := []*endpoint.Endpoint{ + { + DNSName: "name1", + Targets: endpoint.Targets{"target1"}, + RecordTTL: endpoint.TTL(55), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Targets: endpoint.Targets{"target2"}, + RecordTTL: endpoint.TTL(55), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + merged := merge(updateOld, updateNew) + assert.Equal(t, 0, len(merged)) +} diff --git a/provider/google.go b/provider/google/google.go similarity index 95% rename from provider/google.go rename to provider/google/google.go index 59245bc8f..ef744dbe5 100644 --- a/provider/google.go +++ b/provider/google/google.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package google import ( "context" @@ -33,6 +33,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -98,6 +99,7 @@ func (c changesService) Create(project string, managedZone string, change *dns.C // GoogleProvider is an implementation of Provider for Google CloudDNS. type GoogleProvider struct { + provider.BaseProvider // The Google project to work in project string // Enabled dry-run will print any modifying actions rather than execute them. @@ -109,7 +111,7 @@ type GoogleProvider struct { // only consider hosted zones managing domains ending in this suffix domainFilter endpoint.DomainFilter // only consider hosted zones ending with this zone id - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter // A client for managing resource record sets resourceRecordSetsClient resourceRecordSetsClientInterface // A client for managing hosted zones @@ -121,7 +123,7 @@ type GoogleProvider struct { } // NewGoogleProvider initializes a new Google CloudDNS based Provider. -func NewGoogleProvider(ctx context.Context, project string, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, batchChangeSize int, batchChangeInterval time.Duration, dryRun bool) (*GoogleProvider, error) { +func NewGoogleProvider(ctx context.Context, project string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, batchChangeSize int, batchChangeInterval time.Duration, dryRun bool) (*GoogleProvider, error) { gcloud, err := google.DefaultClient(ctx, dns.NdevClouddnsReadwriteScope) if err != nil { return nil, err @@ -209,7 +211,7 @@ func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.End f := func(resp *dns.ResourceRecordSetsListResponse) error { for _, r := range resp.Rrsets { - if !supportedRecordType(r.Type) { + if !provider.SupportedRecordType(r.Type) { continue } endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), r.Rrdatas...)) @@ -398,7 +400,7 @@ func batchChange(change *dns.Change, batchSize int) []*dns.Change { // separateChange separates a multi-zone change into a single change per zone. func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[string]*dns.Change { changes := make(map[string]*dns.Change) - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} for _, z := range zones { zoneNameIDMapper[z.Name] = z.DnsName changes[z.Name] = &dns.Change{ @@ -407,7 +409,7 @@ func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[s } } for _, a := range change.Additions { - if zoneName, _ := zoneNameIDMapper.FindZone(ensureTrailingDot(a.Name)); zoneName != "" { + if zoneName, _ := zoneNameIDMapper.FindZone(provider.EnsureTrailingDot(a.Name)); zoneName != "" { changes[zoneName].Additions = append(changes[zoneName].Additions, a) } else { log.Warnf("No matching zone for record addition: %s %s %s %d", a.Name, a.Type, a.Rrdatas, a.Ttl) @@ -415,7 +417,7 @@ func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[s } for _, d := range change.Deletions { - if zoneName, _ := zoneNameIDMapper.FindZone(ensureTrailingDot(d.Name)); zoneName != "" { + if zoneName, _ := zoneNameIDMapper.FindZone(provider.EnsureTrailingDot(d.Name)); zoneName != "" { changes[zoneName].Deletions = append(changes[zoneName].Deletions, d) } else { log.Warnf("No matching zone for record deletion: %s %s %s %d", d.Name, d.Type, d.Rrdatas, d.Ttl) @@ -440,7 +442,7 @@ func newRecord(ep *endpoint.Endpoint) *dns.ResourceRecordSet { targets := make([]string, len(ep.Targets)) copy(targets, []string(ep.Targets)) if ep.RecordType == endpoint.RecordTypeCNAME { - targets[0] = ensureTrailingDot(targets[0]) + targets[0] = provider.EnsureTrailingDot(targets[0]) } // no annotation results in a Ttl of 0, default to 300 for backwards-compatibility @@ -450,7 +452,7 @@ func newRecord(ep *endpoint.Endpoint) *dns.ResourceRecordSet { } return &dns.ResourceRecordSet{ - Name: ensureTrailingDot(ep.DNSName), + Name: provider.EnsureTrailingDot(ep.DNSName), Rrdatas: targets, Ttl: ttl, Type: ep.RecordType, diff --git a/provider/google_test.go b/provider/google/google_test.go similarity index 94% rename from provider/google_test.go rename to provider/google/google_test.go index 3da1fe772..7f780359e 100644 --- a/provider/google_test.go +++ b/provider/google/google_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package google import ( "fmt" @@ -30,7 +30,9 @@ import ( "google.golang.org/api/googleapi" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) var ( @@ -192,7 +194,7 @@ func hasTrailingDot(target string) bool { } func TestGoogleZonesIDFilter(t *testing.T) { - provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), NewZoneIDFilter([]string{"10002"}), false, []*endpoint.Endpoint{}) + provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), provider.NewZoneIDFilter([]string{"10002"}), false, []*endpoint.Endpoint{}) zones, err := provider.Zones(context.Background()) require.NoError(t, err) @@ -203,7 +205,7 @@ func TestGoogleZonesIDFilter(t *testing.T) { } func TestGoogleZonesNameFilter(t *testing.T) { - provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), NewZoneIDFilter([]string{"internal-2"}), false, []*endpoint.Endpoint{}) + provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), provider.NewZoneIDFilter([]string{"internal-2"}), false, []*endpoint.Endpoint{}) zones, err := provider.Zones(context.Background()) require.NoError(t, err) @@ -214,7 +216,7 @@ func TestGoogleZonesNameFilter(t *testing.T) { } func TestGoogleZones(t *testing.T) { - provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) + provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) zones, err := provider.Zones(context.Background()) require.NoError(t, err) @@ -233,7 +235,7 @@ func TestGoogleRecords(t *testing.T) { endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(3), "foo.elb.amazonaws.com"), } - provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, originalEndpoints) + provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, originalEndpoints) records, err := provider.Records(context.Background()) require.NoError(t, err) @@ -261,7 +263,7 @@ func TestGoogleRecordsFilter(t *testing.T) { "zone-0.ext-dns-test-2.gcp.zalan.do.", // there exists a third zone "zone-3" that we want to exclude from being managed. }), - NewZoneIDFilter([]string{""}), + provider.NewZoneIDFilter([]string{""}), false, originalEndpoints, ) @@ -286,7 +288,7 @@ func TestGoogleRecordsFilter(t *testing.T) { } func TestGoogleCreateRecords(t *testing.T) { - provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) + provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), @@ -312,7 +314,7 @@ func TestGoogleUpdateRecords(t *testing.T) { endpoint.NewEndpointWithTTL("update-test-ttl.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(15), "8.8.4.4"), endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "foo.elb.amazonaws.com"), } - provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, currentRecords) + provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, currentRecords) updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), endpoint.NewEndpointWithTTL("update-test-ttl.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(25), "4.3.2.1"), @@ -338,7 +340,7 @@ func TestGoogleDeleteRecords(t *testing.T) { endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "baz.elb.amazonaws.com"), } - provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, originalEndpoints) + provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, originalEndpoints) require.NoError(t, provider.DeleteRecords(originalEndpoints)) @@ -359,7 +361,7 @@ func TestGoogleApplyChanges(t *testing.T) { "zone-0.ext-dns-test-2.gcp.zalan.do.", // there exists a third zone "zone-3" that we want to exclude from being managed. }), - NewZoneIDFilter([]string{""}), + provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"), @@ -433,7 +435,7 @@ func TestGoogleApplyChangesDryRun(t *testing.T) { endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "qux.elb.amazonaws.com"), } - provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), true, originalEndpoints) + provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), true, originalEndpoints) createRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), @@ -475,12 +477,12 @@ func TestGoogleApplyChangesDryRun(t *testing.T) { } func TestGoogleApplyChangesEmpty(t *testing.T) { - provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) + provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) assert.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{})) } func TestNewFilteredRecords(t *testing.T) { - provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) + provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) records := provider.newFilteredRecords([]*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 1, "8.8.4.4"), @@ -670,7 +672,7 @@ func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected assert.Equal(t, expected.Type, record.Type) } -func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { +func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { provider := &GoogleProvider{ project: "zalando-external-dns-test", dryRun: false, @@ -705,7 +707,7 @@ func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilt } -func newGoogleProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { +func newGoogleProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { provider := &GoogleProvider{ project: "zalando-external-dns-test", dryRun: false, @@ -792,3 +794,7 @@ func clearGoogleRecords(t *testing.T, provider *GoogleProvider, zone string) { require.NoError(t, err) } } + +func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { + assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %s:%s", endpoints, expected) +} diff --git a/provider/infoblox.go b/provider/infoblox/infoblox.go similarity index 98% rename from provider/infoblox.go rename to provider/infoblox/infoblox.go index 73277685c..49c95a210 100644 --- a/provider/infoblox.go +++ b/provider/infoblox/infoblox.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package infoblox import ( "context" @@ -29,12 +29,13 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) // InfobloxConfig clarifies the method signature type InfobloxConfig struct { DomainFilter endpoint.DomainFilter - ZoneIDFilter ZoneIDFilter + ZoneIDFilter provider.ZoneIDFilter Host string Port int Username string @@ -48,9 +49,10 @@ type InfobloxConfig struct { // InfobloxProvider implements the DNS provider for Infoblox. type InfobloxProvider struct { + provider.BaseProvider client ibclient.IBConnector domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter view string dryRun bool } diff --git a/provider/infoblox_test.go b/provider/infoblox/infoblox_test.go similarity index 96% rename from provider/infoblox_test.go rename to provider/infoblox/infoblox_test.go index bf0f91c4a..fa9cb0c28 100644 --- a/provider/infoblox_test.go +++ b/provider/infoblox/infoblox_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package infoblox import ( "context" @@ -28,7 +28,9 @@ import ( "github.com/stretchr/testify/assert" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) type mockIBConnector struct { @@ -329,7 +331,7 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject return nil } -func newInfobloxProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, client ibclient.IBConnector) *InfobloxProvider { +func newInfobloxProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, client ibclient.IBConnector) *InfobloxProvider { return &InfobloxProvider{ client: client, domainFilter: domainFilter, @@ -354,7 +356,7 @@ func TestInfobloxRecords(t *testing.T) { }, } - provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, &client) + provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, &client) actual, err := provider.Records(context.Background()) if err != nil { @@ -428,7 +430,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient provider := newInfobloxProvider( endpoint.NewDomainFilter([]string{""}), - NewZoneIDFilter([]string{""}), + provider.NewZoneIDFilter([]string{""}), dryRun, client, ) @@ -486,7 +488,7 @@ func TestInfobloxZones(t *testing.T) { mockInfobloxObjects: &[]ibclient.IBObject{}, } - provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, &client) + provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, &client) zones, _ := provider.zones() var emptyZoneAuth *ibclient.ZoneAuth assert.Equal(t, provider.findZone(zones, "example.com").Fqdn, "example.com") @@ -521,3 +523,7 @@ func TestMaxResultsRequestBuilder(t *testing.T) { assert.True(t, req.URL.Query().Get("_max_results") == "") } + +func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { + assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %s:%s", endpoints, expected) +} diff --git a/provider/inmemory.go b/provider/inmemory/inmemory.go similarity index 99% rename from provider/inmemory.go rename to provider/inmemory/inmemory.go index e3c7ad964..0abb1c955 100644 --- a/provider/inmemory.go +++ b/provider/inmemory/inmemory.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package inmemory import ( "context" @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) var ( @@ -43,6 +44,7 @@ var ( // InMemoryProvider - dns provider only used for testing purposes // initialized as dns provider with no records type InMemoryProvider struct { + provider.BaseProvider domain endpoint.DomainFilter client *inMemoryClient filter *filter diff --git a/provider/inmemory_test.go b/provider/inmemory/inmemory_test.go similarity index 99% rename from provider/inmemory_test.go rename to provider/inmemory/inmemory_test.go index bb13cb354..9c2b842e7 100644 --- a/provider/inmemory_test.go +++ b/provider/inmemory/inmemory_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package inmemory import ( "context" @@ -26,10 +26,11 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) var ( - _ Provider = &InMemoryProvider{} + _ provider.Provider = &InMemoryProvider{} ) func TestInMemoryProvider(t *testing.T) { diff --git a/provider/linode.go b/provider/linode/linode.go similarity index 87% rename from provider/linode.go rename to provider/linode/linode.go index b93acc344..3ecd2e55f 100644 --- a/provider/linode.go +++ b/provider/linode/linode.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package linode import ( "context" @@ -30,12 +30,13 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) // LinodeDomainClient interface to ease testing type LinodeDomainClient interface { - ListDomainRecords(ctx context.Context, domainID int, opts *linodego.ListOptions) ([]*linodego.DomainRecord, error) - ListDomains(ctx context.Context, opts *linodego.ListOptions) ([]*linodego.Domain, error) + ListDomainRecords(ctx context.Context, domainID int, opts *linodego.ListOptions) ([]linodego.DomainRecord, error) + ListDomains(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Domain, error) CreateDomainRecord(ctx context.Context, domainID int, domainrecord linodego.DomainRecordCreateOptions) (*linodego.DomainRecord, error) DeleteDomainRecord(ctx context.Context, domainID int, id int) error UpdateDomainRecord(ctx context.Context, domainID int, id int, domainrecord linodego.DomainRecordUpdateOptions) (*linodego.DomainRecord, error) @@ -43,6 +44,7 @@ type LinodeDomainClient interface { // LinodeProvider is an implementation of Provider for Digital Ocean's DNS. type LinodeProvider struct { + provider.BaseProvider Client LinodeDomainClient domainFilter endpoint.DomainFilter DryRun bool @@ -50,28 +52,28 @@ type LinodeProvider struct { // LinodeChanges All API calls calculated from the plan type LinodeChanges struct { - Creates []*LinodeChangeCreate - Deletes []*LinodeChangeDelete - Updates []*LinodeChangeUpdate + Creates []LinodeChangeCreate + Deletes []LinodeChangeDelete + Updates []LinodeChangeUpdate } // LinodeChangeCreate Linode Domain Record Creates type LinodeChangeCreate struct { - Domain *linodego.Domain + Domain linodego.Domain Options linodego.DomainRecordCreateOptions } // LinodeChangeUpdate Linode Domain Record Updates type LinodeChangeUpdate struct { - Domain *linodego.Domain - DomainRecord *linodego.DomainRecord + Domain linodego.Domain + DomainRecord linodego.DomainRecord Options linodego.DomainRecordUpdateOptions } // LinodeChangeDelete Linode Domain Record Deletes type LinodeChangeDelete struct { - Domain *linodego.Domain - DomainRecord *linodego.DomainRecord + Domain linodego.Domain + DomainRecord linodego.DomainRecord } // NewLinodeProvider initializes a new Linode DNS based Provider. @@ -101,7 +103,7 @@ func NewLinodeProvider(domainFilter endpoint.DomainFilter, dryRun bool, appVersi } // Zones returns the list of hosted zones. -func (p *LinodeProvider) Zones(ctx context.Context) ([]*linodego.Domain, error) { +func (p *LinodeProvider) Zones(ctx context.Context) ([]linodego.Domain, error) { zones, err := p.fetchZones(ctx) if err != nil { return nil, err @@ -126,7 +128,7 @@ func (p *LinodeProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err } for _, r := range records { - if supportedRecordType(string(r.Type)) { + if provider.SupportedRecordType(string(r.Type)) { name := fmt.Sprintf("%s.%s", r.Name, zone.Domain) // root name is identified by the empty string and should be @@ -143,7 +145,7 @@ func (p *LinodeProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err return endpoints, nil } -func (p *LinodeProvider) fetchRecords(ctx context.Context, domainID int) ([]*linodego.DomainRecord, error) { +func (p *LinodeProvider) fetchRecords(ctx context.Context, domainID int) ([]linodego.DomainRecord, error) { records, err := p.Client.ListDomainRecords(ctx, domainID, nil) if err != nil { return nil, err @@ -152,8 +154,8 @@ func (p *LinodeProvider) fetchRecords(ctx context.Context, domainID int) ([]*lin return records, nil } -func (p *LinodeProvider) fetchZones(ctx context.Context) ([]*linodego.Domain, error) { - var zones []*linodego.Domain +func (p *LinodeProvider) fetchZones(ctx context.Context) ([]linodego.Domain, error) { + var zones []linodego.Domain allZones, err := p.Client.ListDomains(ctx, linodego.NewListOptions(0, "")) @@ -257,7 +259,7 @@ func getPriority() *int { // ApplyChanges applies a given set of changes in a given zone. func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - recordsByZoneID := make(map[string][]*linodego.DomainRecord) + recordsByZoneID := make(map[string][]linodego.DomainRecord) zones, err := p.fetchZones(ctx) @@ -265,9 +267,9 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes return err } - zonesByID := make(map[string]*linodego.Domain) + zonesByID := make(map[string]linodego.Domain) - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} for _, z := range zones { zoneNameIDMapper.Add(strconv.Itoa(z.ID), z.Domain) @@ -289,9 +291,9 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes updatesByZone := endpointsByZone(zoneNameIDMapper, changes.UpdateNew) deletesByZone := endpointsByZone(zoneNameIDMapper, changes.Delete) - var linodeCreates []*LinodeChangeCreate - var linodeUpdates []*LinodeChangeUpdate - var linodeDeletes []*LinodeChangeDelete + var linodeCreates []LinodeChangeCreate + var linodeUpdates []LinodeChangeUpdate + var linodeDeletes []LinodeChangeDelete // Generate Creates for zoneID, creates := range createsByZone { @@ -326,7 +328,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes } for _, target := range ep.Targets { - linodeCreates = append(linodeCreates, &LinodeChangeCreate{ + linodeCreates = append(linodeCreates, LinodeChangeCreate{ Domain: zone, Options: linodego.DomainRecordCreateOptions{ Target: target, @@ -374,7 +376,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes return err } - matchedRecordsByTarget := make(map[string]*linodego.DomainRecord) + matchedRecordsByTarget := make(map[string]linodego.DomainRecord) for _, record := range matchedRecords { matchedRecordsByTarget[record.Target] = record @@ -390,7 +392,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes "target": target, }).Warn("Updating Existing Target") - linodeUpdates = append(linodeUpdates, &LinodeChangeUpdate{ + linodeUpdates = append(linodeUpdates, LinodeChangeUpdate{ Domain: zone, DomainRecord: record, Options: linodego.DomainRecordUpdateOptions{ @@ -415,7 +417,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes "target": target, }).Warn("Creating New Target") - linodeCreates = append(linodeCreates, &LinodeChangeCreate{ + linodeCreates = append(linodeCreates, LinodeChangeCreate{ Domain: zone, Options: linodego.DomainRecordCreateOptions{ Target: target, @@ -440,12 +442,11 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes "target": record.Target, }).Warn("Deleting Target") - linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{ + linodeDeletes = append(linodeDeletes, LinodeChangeDelete{ Domain: zone, DomainRecord: record, }) } - } } @@ -476,7 +477,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes } for _, record := range matchedRecords { - linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{ + linodeDeletes = append(linodeDeletes, LinodeChangeDelete{ Domain: zone, DomainRecord: record, }) @@ -491,8 +492,8 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes }) } -func endpointsByZone(zoneNameIDMapper zoneIDName, endpoints []*endpoint.Endpoint) map[string][]*endpoint.Endpoint { - endpointsByZone := make(map[string][]*endpoint.Endpoint) +func endpointsByZone(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]endpoint.Endpoint { + endpointsByZone := make(map[string][]endpoint.Endpoint) for _, ep := range endpoints { zoneID, _ := zoneNameIDMapper.FindZone(ep.DNSName) @@ -500,7 +501,7 @@ func endpointsByZone(zoneNameIDMapper zoneIDName, endpoints []*endpoint.Endpoint log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", ep.DNSName) continue } - endpointsByZone[zoneID] = append(endpointsByZone[zoneID], ep) + endpointsByZone[zoneID] = append(endpointsByZone[zoneID], *ep) } return endpointsByZone @@ -523,7 +524,7 @@ func convertRecordType(recordType string) (linodego.DomainRecordType, error) { } } -func getStrippedRecordName(zone *linodego.Domain, ep *endpoint.Endpoint) string { +func getStrippedRecordName(zone linodego.Domain, ep endpoint.Endpoint) string { // Handle root if ep.DNSName == zone.Domain { return "" @@ -532,8 +533,8 @@ func getStrippedRecordName(zone *linodego.Domain, ep *endpoint.Endpoint) string return strings.TrimSuffix(ep.DNSName, "."+zone.Domain) } -func getRecordID(records []*linodego.DomainRecord, zone *linodego.Domain, ep *endpoint.Endpoint) []*linodego.DomainRecord { - var matchedRecords []*linodego.DomainRecord +func getRecordID(records []linodego.DomainRecord, zone linodego.Domain, ep endpoint.Endpoint) []linodego.DomainRecord { + var matchedRecords []linodego.DomainRecord for _, record := range records { if record.Name == getStrippedRecordName(zone, ep) && string(record.Type) == ep.RecordType { diff --git a/provider/linode_test.go b/provider/linode/linode_test.go similarity index 88% rename from provider/linode_test.go rename to provider/linode/linode_test.go index 82e21e127..b55e3762d 100644 --- a/provider/linode_test.go +++ b/provider/linode/linode_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package linode import ( "context" @@ -34,14 +34,14 @@ type MockDomainClient struct { mock.Mock } -func (m *MockDomainClient) ListDomainRecords(ctx context.Context, domainID int, opts *linodego.ListOptions) ([]*linodego.DomainRecord, error) { +func (m *MockDomainClient) ListDomainRecords(ctx context.Context, domainID int, opts *linodego.ListOptions) ([]linodego.DomainRecord, error) { args := m.Called(ctx, domainID, opts) - return args.Get(0).([]*linodego.DomainRecord), args.Error(1) + return args.Get(0).([]linodego.DomainRecord), args.Error(1) } -func (m *MockDomainClient) ListDomains(ctx context.Context, opts *linodego.ListOptions) ([]*linodego.Domain, error) { +func (m *MockDomainClient) ListDomains(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Domain, error) { args := m.Called(ctx, opts) - return args.Get(0).([]*linodego.Domain), args.Error(1) + return args.Get(0).([]linodego.Domain), args.Error(1) } func (m *MockDomainClient) CreateDomainRecord(ctx context.Context, domainID int, opts linodego.DomainRecordCreateOptions) (*linodego.DomainRecord, error) { args := m.Called(ctx, domainID, opts) @@ -56,16 +56,16 @@ func (m *MockDomainClient) UpdateDomainRecord(ctx context.Context, domainID int, return args.Get(0).(*linodego.DomainRecord), args.Error(1) } -func createZones() []*linodego.Domain { - return []*linodego.Domain{ +func createZones() []linodego.Domain { + return []linodego.Domain{ {ID: 1, Domain: "foo.com"}, {ID: 2, Domain: "bar.io"}, {ID: 3, Domain: "baz.com"}, } } -func createFooRecords() []*linodego.DomainRecord { - return []*linodego.DomainRecord{{ +func createFooRecords() []linodego.DomainRecord { + return []linodego.DomainRecord{{ ID: 11, Type: linodego.RecordTypeA, Name: "", @@ -83,12 +83,12 @@ func createFooRecords() []*linodego.DomainRecord { }} } -func createBarRecords() []*linodego.DomainRecord { - return []*linodego.DomainRecord{} +func createBarRecords() []linodego.DomainRecord { + return []linodego.DomainRecord{} } -func createBazRecords() []*linodego.DomainRecord { - return []*linodego.DomainRecord{{ +func createBazRecords() []linodego.DomainRecord { + return []linodego.DomainRecord{{ ID: 31, Type: linodego.RecordTypeA, Name: "", @@ -147,15 +147,15 @@ func TestNewLinodeProvider(t *testing.T) { } func TestLinodeStripRecordName(t *testing.T) { - assert.Equal(t, "api", getStrippedRecordName(&linodego.Domain{ + assert.Equal(t, "api", getStrippedRecordName(linodego.Domain{ Domain: "example.com", - }, &endpoint.Endpoint{ + }, endpoint.Endpoint{ DNSName: "api.example.com", })) - assert.Equal(t, "", getStrippedRecordName(&linodego.Domain{ + assert.Equal(t, "", getStrippedRecordName(linodego.Domain{ Domain: "example.com", - }, &endpoint.Endpoint{ + }, endpoint.Endpoint{ DNSName: "example.com", })) } @@ -198,7 +198,7 @@ func TestLinodeFetchZonesWithFilter(t *testing.T) { mock.Anything, ).Return(createZones(), nil).Once() - expected := []*linodego.Domain{ + expected := []linodego.Domain{ {ID: 1, Domain: "foo.com"}, {ID: 3, Domain: "baz.com"}, } @@ -210,15 +210,15 @@ func TestLinodeFetchZonesWithFilter(t *testing.T) { } func TestLinodeGetStrippedRecordName(t *testing.T) { - assert.Equal(t, "", getStrippedRecordName(&linodego.Domain{ + assert.Equal(t, "", getStrippedRecordName(linodego.Domain{ Domain: "foo.com", - }, &endpoint.Endpoint{ + }, endpoint.Endpoint{ DNSName: "foo.com", })) - assert.Equal(t, "api", getStrippedRecordName(&linodego.Domain{ + assert.Equal(t, "api", getStrippedRecordName(linodego.Domain{ Domain: "foo.com", - }, &endpoint.Endpoint{ + }, endpoint.Endpoint{ DNSName: "api.foo.com", })) } @@ -398,14 +398,14 @@ func TestLinodeApplyChangesTargetAdded(t *testing.T) { "ListDomains", mock.Anything, mock.Anything, - ).Return([]*linodego.Domain{{Domain: "example.com", ID: 1}}, nil).Once() + ).Return([]linodego.Domain{{Domain: "example.com", ID: 1}}, nil).Once() mockDomainClient.On( "ListDomainRecords", mock.Anything, 1, mock.Anything, - ).Return([]*linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}}, nil).Once() + ).Return([]linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}}, nil).Once() // Apply Actions mockDomainClient.On( @@ -457,14 +457,14 @@ func TestLinodeApplyChangesTargetRemoved(t *testing.T) { "ListDomains", mock.Anything, mock.Anything, - ).Return([]*linodego.Domain{{Domain: "example.com", ID: 1}}, nil).Once() + ).Return([]linodego.Domain{{Domain: "example.com", ID: 1}}, nil).Once() mockDomainClient.On( "ListDomainRecords", mock.Anything, 1, mock.Anything, - ).Return([]*linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}, {ID: 12, Type: "A", Name: "", Target: "targetB"}}, nil).Once() + ).Return([]linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}, {ID: 12, Type: "A", Name: "", Target: "targetB"}}, nil).Once() // Apply Actions mockDomainClient.On( @@ -513,14 +513,14 @@ func TestLinodeApplyChangesNoChanges(t *testing.T) { "ListDomains", mock.Anything, mock.Anything, - ).Return([]*linodego.Domain{{Domain: "example.com", ID: 1}}, nil).Once() + ).Return([]linodego.Domain{{Domain: "example.com", ID: 1}}, nil).Once() mockDomainClient.On( "ListDomainRecords", mock.Anything, 1, mock.Anything, - ).Return([]*linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}}, nil).Once() + ).Return([]linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}}, nil).Once() err := provider.ApplyChanges(context.Background(), &plan.Changes{}) require.NoError(t, err) diff --git a/provider/ns1.go b/provider/ns1/ns1.go similarity index 97% rename from provider/ns1.go rename to provider/ns1/ns1.go index 3d72efa9a..53e3d43b5 100644 --- a/provider/ns1.go +++ b/provider/ns1/ns1.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package ns1 import ( "context" @@ -30,6 +30,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -85,7 +86,7 @@ func (n NS1DomainService) ListZones() ([]*dns.Zone, *http.Response, error) { // NS1Config passes cli args to the NS1Provider type NS1Config struct { DomainFilter endpoint.DomainFilter - ZoneIDFilter ZoneIDFilter + ZoneIDFilter provider.ZoneIDFilter NS1Endpoint string NS1IgnoreSSL bool DryRun bool @@ -93,9 +94,10 @@ type NS1Config struct { // NS1Provider is the NS1 provider type NS1Provider struct { + provider.BaseProvider client NS1DomainClient domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter dryRun bool } @@ -150,7 +152,6 @@ func (p *NS1Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) var endpoints []*endpoint.Endpoint for _, zone := range zones { - // TODO handle Header Codes zoneData, _, err := p.client.GetZone(zone.String()) if err != nil { @@ -158,7 +159,7 @@ func (p *NS1Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) } for _, record := range zoneData.Records { - if supportedRecordType(record.Type) { + if provider.SupportedRecordType(record.Type) { endpoints = append(endpoints, endpoint.NewEndpointWithTTL( record.Domain, record.Type, @@ -299,7 +300,7 @@ func newNS1Changes(action string, endpoints []*endpoint.Endpoint) []*ns1Change { // ns1ChangesByZone separates a multi-zone change into a single change per zone. func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change { changes := make(map[string][]*ns1Change) - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} for _, z := range zones { zoneNameIDMapper.Add(z.Zone, z.Zone) changes[z.Zone] = []*ns1Change{} diff --git a/provider/ns1_test.go b/provider/ns1/ns1_test.go similarity index 97% rename from provider/ns1_test.go rename to provider/ns1/ns1_test.go index 6758aea71..f4309ec50 100644 --- a/provider/ns1_test.go +++ b/provider/ns1/ns1_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package ns1 import ( "context" @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) type MockNS1DomainClient struct { @@ -130,7 +131,7 @@ func TestNS1Records(t *testing.T) { provider := &NS1Provider{ client: &MockNS1DomainClient{}, domainFilter: endpoint.NewDomainFilter([]string{"foo.com."}), - zoneIDFilter: NewZoneIDFilter([]string{""}), + zoneIDFilter: provider.NewZoneIDFilter([]string{""}), } ctx := context.Background() @@ -151,7 +152,7 @@ func TestNewNS1Provider(t *testing.T) { _ = os.Setenv("NS1_APIKEY", "xxxxxxxxxxxxxxxxx") testNS1Config := NS1Config{ DomainFilter: endpoint.NewDomainFilter([]string{"foo.com."}), - ZoneIDFilter: NewZoneIDFilter([]string{""}), + ZoneIDFilter: provider.NewZoneIDFilter([]string{""}), DryRun: false, } _, err := NewNS1Provider(testNS1Config) @@ -166,7 +167,7 @@ func TestNS1Zones(t *testing.T) { provider := &NS1Provider{ client: &MockNS1DomainClient{}, domainFilter: endpoint.NewDomainFilter([]string{"foo.com."}), - zoneIDFilter: NewZoneIDFilter([]string{""}), + zoneIDFilter: provider.NewZoneIDFilter([]string{""}), } zones, err := provider.zonesFiltered() diff --git a/provider/oci.go b/provider/oci/oci.go similarity index 94% rename from provider/oci.go rename to provider/oci/oci.go index ba2c3ae5e..759caebf1 100644 --- a/provider/oci.go +++ b/provider/oci/oci.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package oci import ( "context" @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ociRecordTTL = 300 @@ -52,11 +53,12 @@ type OCIConfig struct { // OCIProvider is an implementation of Provider for Oracle Cloud Infrastructure // (OCI) DNS. type OCIProvider struct { + provider.BaseProvider client ociDNSClient cfg OCIConfig domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter dryRun bool } @@ -82,8 +84,8 @@ func LoadOCIConfig(path string) (*OCIConfig, error) { return &cfg, nil } -// NewOCIProvider initialises a new OCI DNS based Provider. -func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) (*OCIProvider, error) { +// NewOCIProvider initializes a new OCI DNS based Provider. +func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (*OCIProvider, error) { var client ociDNSClient client, err := dns.NewDnsClientWithConfigurationProvider(common.NewRawConfigurationProvider( cfg.Auth.TenancyID, @@ -94,7 +96,7 @@ func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFil &cfg.Auth.Passphrase, )) if err != nil { - return nil, errors.Wrap(err, "initialising OCI DNS API client") + return nil, errors.Wrap(err, "initializing OCI DNS API client") } return &OCIProvider{ @@ -177,7 +179,7 @@ func (p *OCIProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) } for _, record := range resp.Items { - if !supportedRecordType(*record.Rtype) { + if !provider.SupportedRecordType(*record.Rtype) { continue } endpoints = append(endpoints, @@ -252,7 +254,7 @@ func newRecordOperation(ep *endpoint.Endpoint, opType dns.RecordOperationOperati targets := make([]string, len(ep.Targets)) copy(targets, []string(ep.Targets)) if ep.RecordType == endpoint.RecordTypeCNAME { - targets[0] = ensureTrailingDot(targets[0]) + targets[0] = provider.EnsureTrailingDot(targets[0]) } rdata := strings.Join(targets, " ") @@ -274,7 +276,7 @@ func newRecordOperation(ep *endpoint.Endpoint, opType dns.RecordOperationOperati func operationsByZone(zones map[string]dns.ZoneSummary, ops []dns.RecordOperation) map[string][]dns.RecordOperation { changes := make(map[string][]dns.RecordOperation) - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} for _, z := range zones { zoneNameIDMapper.Add(*z.Id, *z.Name) changes[*z.Id] = []dns.RecordOperation{} diff --git a/provider/oci_test.go b/provider/oci/oci_test.go similarity index 97% rename from provider/oci_test.go rename to provider/oci/oci_test.go index decaa7469..5f890a4b5 100644 --- a/provider/oci_test.go +++ b/provider/oci/oci_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package oci import ( "context" @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) type mockOCIDNSClient struct{} @@ -100,7 +101,7 @@ func (c *mockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.Pat } // newOCIProvider creates an OCI provider with API calls mocked out. -func newOCIProvider(client ociDNSClient, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) *OCIProvider { +func newOCIProvider(client ociDNSClient, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) *OCIProvider { return &OCIProvider{ client: client, cfg: OCIConfig{ @@ -176,7 +177,7 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K `, }, }, - err: errors.New("initialising OCI DNS API client: can not create client, bad configuration: PEM data was not found in buffer"), + err: errors.New("initializing OCI DNS API client: can not create client, bad configuration: PEM data was not found in buffer"), }, } for name, tc := range testCases { @@ -184,7 +185,7 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K _, err := NewOCIProvider( tc.config, endpoint.NewDomainFilter([]string{"com"}), - NewZoneIDFilter([]string{""}), + provider.NewZoneIDFilter([]string{""}), false, ) if err == nil { @@ -200,13 +201,13 @@ func TestOCIZones(t *testing.T) { testCases := []struct { name string domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter expected map[string]dns.ZoneSummary }{ { name: "DomainFilter_com", domainFilter: endpoint.NewDomainFilter([]string{"com"}), - zoneIDFilter: NewZoneIDFilter([]string{""}), + zoneIDFilter: provider.NewZoneIDFilter([]string{""}), expected: map[string]dns.ZoneSummary{ "foo.com": { Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"), @@ -220,7 +221,7 @@ func TestOCIZones(t *testing.T) { }, { name: "DomainFilter_foo.com", domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}), - zoneIDFilter: NewZoneIDFilter([]string{""}), + zoneIDFilter: provider.NewZoneIDFilter([]string{""}), expected: map[string]dns.ZoneSummary{ "foo.com": { Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"), @@ -230,7 +231,7 @@ func TestOCIZones(t *testing.T) { }, { name: "ZoneIDFilter_ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959", domainFilter: endpoint.NewDomainFilter([]string{""}), - zoneIDFilter: NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"}), + zoneIDFilter: provider.NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"}), expected: map[string]dns.ZoneSummary{ "foo.com": { Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"), @@ -253,13 +254,13 @@ func TestOCIRecords(t *testing.T) { testCases := []struct { name string domainFilter endpoint.DomainFilter - zoneIDFilter ZoneIDFilter + zoneIDFilter provider.ZoneIDFilter expected []*endpoint.Endpoint }{ { name: "unfiltered", domainFilter: endpoint.NewDomainFilter([]string{""}), - zoneIDFilter: NewZoneIDFilter([]string{""}), + zoneIDFilter: provider.NewZoneIDFilter([]string{""}), expected: []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"), endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"), @@ -269,7 +270,7 @@ func TestOCIRecords(t *testing.T) { }, { name: "DomainFilter_foo.com", domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}), - zoneIDFilter: NewZoneIDFilter([]string{""}), + zoneIDFilter: provider.NewZoneIDFilter([]string{""}), expected: []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"), endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"), @@ -278,7 +279,7 @@ func TestOCIRecords(t *testing.T) { }, { name: "ZoneIDFilter_ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404", domainFilter: endpoint.NewDomainFilter([]string{""}), - zoneIDFilter: NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"}), + zoneIDFilter: provider.NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"}), expected: []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("foo.bar.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"), }, @@ -826,7 +827,7 @@ func TestOCIApplyChanges(t *testing.T) { provider := newOCIProvider( client, endpoint.NewDomainFilter([]string{""}), - NewZoneIDFilter([]string{""}), + provider.NewZoneIDFilter([]string{""}), tc.dryRun, ) diff --git a/provider/ovh.go b/provider/ovh/ovh.go similarity index 98% rename from provider/ovh.go rename to provider/ovh/ovh.go index a53231969..a32260e1b 100644 --- a/provider/ovh.go +++ b/provider/ovh/ovh.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package ovh import ( "context" @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -45,6 +46,8 @@ var ( // OVHProvider is an implementation of Provider for OVH DNS. type OVHProvider struct { + provider.BaseProvider + client ovhClient domainFilter endpoint.DomainFilter @@ -94,7 +97,6 @@ func NewOVHProvider(ctx context.Context, domainFilter endpoint.DomainFilter, end // Records returns the list of records in all relevant zones. func (p *OVHProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - _, records, err := p.zonesRecords(ctx) if err != nil { return nil, err @@ -237,7 +239,7 @@ func (p *OVHProvider) record(zone *string, id uint64, records chan<- ovhRecord) if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record/%d", *zone, id), &record); err != nil { return err } - if supportedRecordType(record.FieldType) { + if provider.SupportedRecordType(record.FieldType) { log.Debugf("OVH: Record %d for %s is %+v", id, *zone, record) records <- record } @@ -278,7 +280,7 @@ func ovhGroupByNameAndType(records []ovhRecord) []*endpoint.Endpoint { } func newOvhChange(action int, endpoints []*endpoint.Endpoint, zones []string, records []ovhRecord) []ovhChange { - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} ovhChanges := make([]ovhChange, 0, countTargets(endpoints)) for _, zone := range zones { zoneNameIDMapper.Add(zone, zone) diff --git a/provider/ovh_test.go b/provider/ovh/ovh_test.go similarity index 91% rename from provider/ovh_test.go rename to provider/ovh/ovh_test.go index 2f2b793c0..59b336941 100644 --- a/provider/ovh_test.go +++ b/provider/ovh/ovh_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package ovh import ( "context" @@ -142,9 +142,9 @@ func TestOvhRecords(t *testing.T) { sort.Strings(endoint.Targets) } assert.ElementsMatch(endpoints, []*endpoint.Endpoint{ - &endpoint.Endpoint{DNSName: "example.org", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42"}}, - &endpoint.Endpoint{DNSName: "www.example.org", RecordType: "CNAME", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"example.org"}}, - &endpoint.Endpoint{DNSName: "ovh.example.net", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42", "203.0.113.43"}}, + {DNSName: "example.org", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42"}}, + {DNSName: "www.example.org", RecordType: "CNAME", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"example.org"}}, + {DNSName: "ovh.example.net", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42", "203.0.113.43"}}, }) client.AssertExpectations(t) @@ -283,10 +283,10 @@ func TestOvhCountTargets(t *testing.T) { endpoints [][]*endpoint.Endpoint count int }{ - {[][]*endpoint.Endpoint{[]*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 1}, - {[][]*endpoint.Endpoint{[]*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}, {DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 2}, - {[][]*endpoint.Endpoint{[]*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target", "target"}}}}, 3}, - {[][]*endpoint.Endpoint{[]*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}, []*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}}, 4}, + {[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 1}, + {[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}, {DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 2}, + {[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target", "target"}}}}, 3}, + {[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}, {{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}}, 4}, } for _, test := range cases { count := countTargets(test.endpoints...) diff --git a/provider/pdns.go b/provider/pdns/pdns.go similarity index 97% rename from provider/pdns.go rename to provider/pdns/pdns.go index fc1eff564..8ecd9d823 100644 --- a/provider/pdns.go +++ b/provider/pdns/pdns.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package pdns import ( "bytes" @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/pkg/tlsutils" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) type pdnsChangeType string @@ -116,7 +117,6 @@ func (tlsConfig *TLSConfig) setHTTPClient(pdnsClientConfig *pgo.Configuration) e // Function for debug printing func stringifyHTTPResponseBody(r *http.Response) (body string) { - if r == nil { return "" } @@ -125,7 +125,6 @@ func stringifyHTTPResponseBody(r *http.Response) (body string) { buf.ReadFrom(r.Body) body = buf.String() return body - } // PDNSAPIProvider : Interface used and extended by the PDNSAPIClient struct as @@ -161,7 +160,6 @@ func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err log.Errorf("Unable to fetch zones. %v", err) return zones, resp, err - } // PartitionZones : Method returns a slice of zones that adhere to the domain filter and a slice of ones that does not adhere to the filter @@ -190,14 +188,12 @@ func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Respo log.Debugf("Retrying ListZone() ... %d", i) time.Sleep(retryAfterTime * (1 << uint(i))) continue - } return zone, resp, err } log.Errorf("Unable to list zone. %v", err) return zone, resp, err - } // PatchZone : Method used to update the contents of a particular zone from PowerDNS @@ -210,7 +206,6 @@ func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *htt log.Debugf("Retrying PatchZone() ... %d", i) time.Sleep(retryAfterTime * (1 << uint(i))) continue - } return resp, err } @@ -221,16 +216,16 @@ func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *htt // PDNSProvider is an implementation of the Provider interface for PowerDNS type PDNSProvider struct { + provider.BaseProvider client PDNSAPIProvider } // NewPDNSProvider initializes a new PowerDNS based Provider. func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, error) { - // Do some input validation if config.APIKey == "" { - return nil, errors.New("Missing API Key for PDNS. Specify using --pdns-api-key=") + return nil, errors.New("missing API Key for PDNS. Specify using --pdns-api-key=") } // We do not support dry running, exit safely instead of surprising the user @@ -257,7 +252,6 @@ func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, err domainFilter: config.DomainFilter, }, } - return provider, nil } @@ -270,13 +264,11 @@ func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpo endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rr.Type_, endpoint.TTL(rr.Ttl), record.Content)) } } - return endpoints, nil } // ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) { - zonelist = []pgo.Zone{} endpoints := make([]*endpoint.Endpoint, len(eps)) copy(endpoints, eps) @@ -310,15 +302,15 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet zone.Rrsets = []pgo.RrSet{} for i := 0; i < len(endpoints); { ep := endpoints[i] - dnsname := ensureTrailingDot(ep.DNSName) + dnsname := provider.EnsureTrailingDot(ep.DNSName) if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) { // The assumption here is that there will only ever be one target // per (ep.DNSName, ep.RecordType) tuple, which holds true for // external-dns v5.0.0-alpha onwards records := []pgo.Record{} for _, t := range ep.Targets { - if "CNAME" == ep.RecordType { - t = ensureTrailingDot(t) + if ep.RecordType == "CNAME" { + t = provider.EnsureTrailingDot(t) } records = append(records, pgo.Record{Content: t}) @@ -333,7 +325,7 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet // DELETEs explicitly forbid a TTL, therefore only PATCHes need the TTL if changetype == PdnsReplace { if int64(ep.RecordTTL) > int64(math.MaxInt32) { - return nil, errors.New("Value of record TTL overflows, limited to int32") + return nil, errors.New("value of record TTL overflows, limited to int32") } if ep.RecordTTL == 0 { // No TTL was specified for the record, we use the default @@ -351,13 +343,10 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet // If we didn't pop anything, we move to the next item in the list i++ } - } - if len(zone.Rrsets) > 0 { zonelist = append(zonelist, zone) } - } // residualZones is unsorted by name length like its counterpart @@ -365,7 +354,7 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet for _, zone := range residualZones { for i := 0; i < len(endpoints); { ep := endpoints[i] - dnsname := ensureTrailingDot(ep.DNSName) + dnsname := provider.EnsureTrailingDot(ep.DNSName) if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) { // "pop" endpoint if it's matched to a residual zone... essentially a no-op log.Debugf("Ignoring Endpoint because it was matched to a zone that was not specified within Domain Filter(s): %s", dnsname) @@ -375,7 +364,6 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet } } } - // If we still have some endpoints left, it means we couldn't find a matching zone (filtered or residual) for them // We warn instead of hard fail here because we don't want a misconfig to cause everything to go down if len(endpoints) > 0 { @@ -400,20 +388,17 @@ func (p *PDNSProvider) mutateRecords(endpoints []*endpoint.Endpoint, changetype } else { log.Debugf("Struct for PatchZone:\n%s", string(jso)) } - resp, err := p.client.PatchZone(zone.Id, zone) if err != nil { log.Debugf("PDNS API response: %s", stringifyHTTPResponseBody(resp)) return err } - } return nil } // Records returns all DNS records controlled by the configured PDNS server (for all zones) func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { - zones, _, err := p.client.ListZones() if err != nil { return nil, err @@ -443,7 +428,6 @@ func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpo // ApplyChanges takes a list of changes (endpoints) and updates the PDNS server // by sending the correct HTTP PATCH requests to a matching zone func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - startTime := time.Now() // Create @@ -491,7 +475,6 @@ func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) return err } } - log.Debugf("Changes pushed out to PowerDNS in %s\n", time.Since(startTime)) return nil } diff --git a/provider/pdns_test.go b/provider/pdns/pdns_test.go similarity index 98% rename from provider/pdns_test.go rename to provider/pdns/pdns_test.go index 6248cad06..cfd0b099b 100644 --- a/provider/pdns_test.go +++ b/provider/pdns/pdns_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package pdns import ( "context" @@ -735,7 +735,7 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreateTLS() { DomainFilter: endpoint.NewDomainFilter([]string{""}), TLSConfig: TLSConfig{ TLSEnabled: true, - CAFilePath: "../internal/testresources/ca.pem", + CAFilePath: "../../internal/testresources/ca.pem", }, }) assert.Nil(suite.T(), err, "Enabled TLS Config with --tls-ca should raise no error") @@ -748,8 +748,8 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreateTLS() { DomainFilter: endpoint.NewDomainFilter([]string{""}), TLSConfig: TLSConfig{ TLSEnabled: true, - CAFilePath: "../internal/testresources/ca.pem", - ClientCertFilePath: "../internal/testresources/client-cert.pem", + CAFilePath: "../../internal/testresources/ca.pem", + ClientCertFilePath: "../../internal/testresources/client-cert.pem", }, }) assert.Error(suite.T(), err, "Enabled TLS Config with --tls-client-cert only should raise an error") @@ -762,8 +762,8 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreateTLS() { DomainFilter: endpoint.NewDomainFilter([]string{""}), TLSConfig: TLSConfig{ TLSEnabled: true, - CAFilePath: "../internal/testresources/ca.pem", - ClientCertKeyFilePath: "../internal/testresources/client-cert-key.pem", + CAFilePath: "../../internal/testresources/ca.pem", + ClientCertKeyFilePath: "../../internal/testresources/client-cert-key.pem", }, }) assert.Error(suite.T(), err, "Enabled TLS Config with --tls-client-cert-key only should raise an error") @@ -776,9 +776,9 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreateTLS() { DomainFilter: endpoint.NewDomainFilter([]string{""}), TLSConfig: TLSConfig{ TLSEnabled: true, - CAFilePath: "../internal/testresources/ca.pem", - ClientCertFilePath: "../internal/testresources/client-cert.pem", - ClientCertKeyFilePath: "../internal/testresources/client-cert-key.pem", + CAFilePath: "../../internal/testresources/ca.pem", + ClientCertFilePath: "../../internal/testresources/client-cert.pem", + ClientCertKeyFilePath: "../../internal/testresources/client-cert-key.pem", }, }) assert.Nil(suite.T(), err, "Enabled TLS Config with all flags should raise no error") diff --git a/provider/provider.go b/provider/provider.go index fcd12018c..f2b8cd5e6 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -29,6 +29,14 @@ import ( type Provider interface { Records(ctx context.Context) ([]*endpoint.Endpoint, error) ApplyChanges(ctx context.Context, changes *plan.Changes) error + PropertyValuesEqual(name string, previous string, current string) bool +} + +type BaseProvider struct { +} + +func (b BaseProvider) PropertyValuesEqual(name, previous, current string) bool { + return previous == current } type contextKey struct { @@ -42,11 +50,34 @@ func (k *contextKey) String() string { return "provider context value " + k.name // type []*endpoint.Endpoint. var RecordsContextKey = &contextKey{"records"} -// ensureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already. -func ensureTrailingDot(hostname string) string { +// EnsureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already. +func EnsureTrailingDot(hostname string) string { if net.ParseIP(hostname) != nil { return hostname } return strings.TrimSuffix(hostname, ".") + "." } + +// Difference tells which entries need to be respectively +// added, removed, or left untouched for "current" to be transformed to "desired" +func Difference(current, desired []string) ([]string, []string, []string) { + add, remove, leave := []string{}, []string{}, []string{} + index := make(map[string]struct{}, len(current)) + for _, x := range current { + index[x] = struct{}{} + } + for _, x := range desired { + if _, found := index[x]; found { + leave = append(leave, x) + delete(index, x) + } else { + add = append(add, x) + delete(index, x) + } + } + for x := range index { + remove = append(remove, x) + } + return add, remove, leave +} diff --git a/provider/provider_test.go b/provider/provider_test.go index 08fc2989d..0e24ca4f0 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -17,9 +17,19 @@ limitations under the License. package provider import ( + "io/ioutil" + "os" "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" ) +func TestMain(m *testing.M) { + log.SetOutput(ioutil.Discard) + os.Exit(m.Run()) +} + func TestEnsureTrailingDot(t *testing.T) { for _, tc := range []struct { input, expected string @@ -28,10 +38,28 @@ func TestEnsureTrailingDot(t *testing.T) { {"example.org.", "example.org."}, {"8.8.8.8", "8.8.8.8"}, } { - output := ensureTrailingDot(tc.input) + output := EnsureTrailingDot(tc.input) if output != tc.expected { t.Errorf("expected %s, got %s", tc.expected, output) } } } + +func TestDifference(t *testing.T) { + current := []string{"foo", "bar"} + desired := []string{"bar", "baz"} + add, remove, leave := Difference(current, desired) + assert.Equal(t, add, []string{"baz"}) + assert.Equal(t, remove, []string{"foo"}) + assert.Equal(t, leave, []string{"bar"}) +} + +func TestBaseProviderPropertyEquality(t *testing.T) { + p := BaseProvider{} + assert.True(t, p.PropertyValuesEqual("some.property", "", ""), "Both properties not present") + assert.False(t, p.PropertyValuesEqual("some.property", "", "Foo"), "First property missing") + assert.False(t, p.PropertyValuesEqual("some.property", "Foo", ""), "Second property missing") + assert.True(t, p.PropertyValuesEqual("some.property", "Foo", "Foo"), "Properties the same") + assert.False(t, p.PropertyValuesEqual("some.property", "Foo", "Bar"), "Attributes differ") +} diff --git a/provider/rcode0.go b/provider/rcode0/rcode0.go similarity index 98% rename from provider/rcode0.go rename to provider/rcode0/rcode0.go index 0577be000..571857c21 100644 --- a/provider/rcode0.go +++ b/provider/rcode0/rcode0.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package rcode0 import ( "context" @@ -28,10 +28,12 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) // RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS. type RcodeZeroProvider struct { + provider.BaseProvider Client *rc0.Client DomainFilter endpoint.DomainFilter @@ -44,7 +46,6 @@ type RcodeZeroProvider struct { // // Returns the provider or an error if a provider could not be created. func NewRcodeZeroProvider(domainFilter endpoint.DomainFilter, dryRun bool, txtEnc bool) (*RcodeZeroProvider, error) { - client, err := rc0.NewClient(os.Getenv("RC0_API_KEY")) if err != nil { @@ -76,7 +77,6 @@ func NewRcodeZeroProvider(domainFilter endpoint.DomainFilter, dryRun bool, txtEn // Zones returns filtered zones if filter is set func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) { - var result []*rc0.Zone zones, err := p.fetchZones() @@ -97,7 +97,6 @@ func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) { // // Decrypts TXT records if TXT-Encrypt flag is set and key is provided func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - zones, err := p.Zones() if err != nil { return nil, err @@ -106,7 +105,6 @@ func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, var endpoints []*endpoint.Endpoint for _, zone := range zones { - rrset, err := p.fetchRecords(zone.Domain) if err != nil { @@ -114,25 +112,19 @@ func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, } for _, r := range rrset { - - if supportedRecordType(r.Type) { - + if provider.SupportedRecordType(r.Type) { if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(r.Type, "TXT") { p.Client.RRSet.DecryptTXT(p.Key, r) } - if len(r.Records) > 1 { - for _, _r := range r.Records { if !_r.Disabled { endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), _r.Content)) } } - } else if !r.Records[0].Disabled { endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Records[0].Content)) } - } } } @@ -142,7 +134,6 @@ func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, // ApplyChanges applies a given set of changes in a given zone. func (p *RcodeZeroProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - combinedChanges := make([]*rc0.RRSetChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeADD, changes.Create)...) @@ -154,9 +145,8 @@ func (p *RcodeZeroProvider) ApplyChanges(ctx context.Context, changes *plan.Chan // Helper function func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map[string][]*rc0.RRSetChange { - changes := make(map[string][]*rc0.RRSetChange) - zoneNameIDMapper := zoneIDName{} + zoneNameIDMapper := provider.ZoneIDName{} for _, z := range zones { zoneNameIDMapper.Add(z.Domain, z.Domain) changes[z.Domain] = []*rc0.RRSetChange{} @@ -176,7 +166,6 @@ func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map // Helper function func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) { - var allRecords []*rc0.RRType listOptions := rc0.NewListOptions() @@ -202,7 +191,6 @@ func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) // Helper function func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) { - var allZones []*rc0.Zone listOptions := rc0.NewListOptions() @@ -228,7 +216,6 @@ func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) { // // Changes are submitted by change type. func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error { - if len(changes) == 0 { return nil } @@ -240,11 +227,8 @@ func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error { // separate into per-zone change sets to be passed to the API. changesByZone := rcodezeroChangesByZone(zones, changes) - for zoneName, changes := range changesByZone { - for _, change := range changes { - logFields := log.Fields{ "record": change.Name, "content": change.Records[0].Content, @@ -306,7 +290,6 @@ func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error { // NewRcodezeroChanges returns a RcodeZero specific array with rrset change objects. func (p *RcodeZeroProvider) NewRcodezeroChanges(action string, endpoints []*endpoint.Endpoint) []*rc0.RRSetChange { - changes := make([]*rc0.RRSetChange, 0, len(endpoints)) for _, _endpoint := range endpoints { @@ -318,7 +301,6 @@ func (p *RcodeZeroProvider) NewRcodezeroChanges(action string, endpoints []*endp // NewRcodezeroChange returns a RcodeZero specific rrset change object. func (p *RcodeZeroProvider) NewRcodezeroChange(action string, endpoint *endpoint.Endpoint) *rc0.RRSetChange { - change := &rc0.RRSetChange{ Type: endpoint.RecordType, ChangeType: action, diff --git a/provider/rcode0_test.go b/provider/rcode0/rcode0_test.go similarity index 99% rename from provider/rcode0_test.go rename to provider/rcode0/rcode0_test.go index dda94e2f7..f380ec8e3 100644 --- a/provider/rcode0_test.go +++ b/provider/rcode0/rcode0_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package rcode0 import ( "context" diff --git a/provider/rdns.go b/provider/rdns/rdns.go similarity index 99% rename from provider/rdns.go rename to provider/rdns/rdns.go index 6bbda693e..b64325bb7 100644 --- a/provider/rdns.go +++ b/provider/rdns/rdns.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package rdns import ( "context" @@ -35,9 +35,11 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( + etcdTimeout = 5 * time.Second rdnsMaxHosts = 10 rdnsOriginalLabel = "originalText" rdnsPrefix = "/rdnsv3" @@ -65,6 +67,7 @@ type RDNSConfig struct { // RDNSProvider is an implementation of Provider for Rancher DNS(RDNS). type RDNSProvider struct { + provider.BaseProvider client RDNSClient dryRun bool domainFilter endpoint.DomainFilter diff --git a/provider/rdns_test.go b/provider/rdns/rdns_test.go similarity index 99% rename from provider/rdns_test.go rename to provider/rdns/rdns_test.go index de66c21c3..450834313 100644 --- a/provider/rdns_test.go +++ b/provider/rdns/rdns_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package rdns import ( "context" diff --git a/provider/recordfilter.go b/provider/recordfilter.go index 8b5c31178..487595a64 100644 --- a/provider/recordfilter.go +++ b/provider/recordfilter.go @@ -16,9 +16,9 @@ limitations under the License. package provider -// supportedRecordType returns true only for supported record types. +// SupportedRecordType returns true only for supported record types. // Currently A, CNAME, SRV, and TXT record types are supported. -func supportedRecordType(recordType string) bool { +func SupportedRecordType(recordType string) bool { switch recordType { case "A", "CNAME", "SRV", "TXT": return true diff --git a/provider/recordfilter_test.go b/provider/recordfilter_test.go index e4e1ebbb9..fc8835819 100644 --- a/provider/recordfilter_test.go +++ b/provider/recordfilter_test.go @@ -41,7 +41,7 @@ func TestRecordTypeFilter(t *testing.T) { }, } for _, r := range records { - got := supportedRecordType(r.rtype) + got := SupportedRecordType(r.rtype) if r.expect != got { t.Errorf("wrong record type %s: expect %v, but got %v", r.rtype, r.expect, got) } diff --git a/provider/rfc2136.go b/provider/rfc2136/rfc2136.go similarity index 93% rename from provider/rfc2136.go rename to provider/rfc2136/rfc2136.go index f5262ab4f..8874b9821 100644 --- a/provider/rfc2136.go +++ b/provider/rfc2136/rfc2136.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package rfc2136 import ( "context" @@ -30,10 +30,17 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +const ( + // maximum size of a UDP transport message in DNS protocol + udpMaxMsgSize = 512 ) // rfc2136 provider type type rfc2136Provider struct { + provider.BaseProvider nameserver string zoneName string tsigKeyName string @@ -65,7 +72,7 @@ type rfc2136Actions interface { } // NewRfc2136Provider is a factory function for OpenStack rfc2136 providers -func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter endpoint.DomainFilter, dryRun bool, minTTL time.Duration, actions rfc2136Actions) (Provider, error) { +func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter endpoint.DomainFilter, dryRun bool, minTTL time.Duration, actions rfc2136Actions) (provider.Provider, error) { secretAlgChecked, ok := tsigAlgs[secretAlg] if !ok && !insecure { return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg) @@ -206,7 +213,6 @@ func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes m.SetUpdate(r.zoneName) for _, ep := range changes.Create { - if !r.domainFilter.Match(ep.DNSName) { log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName) continue @@ -214,17 +220,15 @@ func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes r.AddRecord(m, ep) } - for _, ep := range changes.UpdateNew { - + for i, ep := range changes.UpdateNew { if !r.domainFilter.Match(ep.DNSName) { log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName) continue } - r.UpdateRecord(m, ep) + r.UpdateRecord(m, changes.UpdateOld[i], ep) } for _, ep := range changes.Delete { - if !r.domainFilter.Match(ep.DNSName) { log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName) continue @@ -244,13 +248,13 @@ func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes return nil } -func (r rfc2136Provider) UpdateRecord(m *dns.Msg, ep *endpoint.Endpoint) error { - err := r.RemoveRecord(m, ep) +func (r rfc2136Provider) UpdateRecord(m *dns.Msg, oldEp *endpoint.Endpoint, newEp *endpoint.Endpoint) error { + err := r.RemoveRecord(m, oldEp) if err != nil { return err } - return r.AddRecord(m, ep) + return r.AddRecord(m, newEp) } func (r rfc2136Provider) AddRecord(m *dns.Msg, ep *endpoint.Endpoint) error { @@ -308,6 +312,10 @@ func (r rfc2136Provider) SendMessage(msg *dns.Msg) error { msg.SetTsig(r.tsigKeyName, r.tsigSecretAlg, 300, time.Now().Unix()) } + if msg.Len() > udpMaxMsgSize { + c.Net = "tcp" + } + resp, _, err := c.Exchange(msg, r.nameserver) if err != nil { log.Infof("error in dns.Client.Exchange: %s", err) diff --git a/provider/rfc2136_test.go b/provider/rfc2136/rfc2136_test.go similarity index 73% rename from provider/rfc2136_test.go rename to provider/rfc2136/rfc2136_test.go index ad932ca75..163414158 100644 --- a/provider/rfc2136_test.go +++ b/provider/rfc2136/rfc2136_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package rfc2136 import ( "context" @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) type rfc2136Stub struct { @@ -93,7 +94,7 @@ func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelo return outChan, nil } -func createRfc2136StubProvider(stub *rfc2136Stub) (Provider, error) { +func createRfc2136StubProvider(stub *rfc2136Stub) (provider.Provider, error) { return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, stub) } @@ -246,3 +247,90 @@ func TestRfc2136ApplyChangesWithDifferentTTLs(t *testing.T) { assert.True(t, strings.Contains(createRecords[2], "300")) } + +func TestRfc2136ApplyChangesWithUpdate(t *testing.T) { + stub := newStub() + + provider, err := createRfc2136StubProvider(stub) + assert.NoError(t, err) + + p := &plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "v1.foo.com", + RecordType: "A", + Targets: []string{"1.2.3.4"}, + RecordTTL: endpoint.TTL(400), + }, + { + DNSName: "v1.foobar.com", + RecordType: "TXT", + Targets: []string{"boom"}, + }, + }, + } + + err = provider.ApplyChanges(context.Background(), p) + assert.NoError(t, err) + + p = &plan.Changes{ + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "v1.foo.com", + RecordType: "A", + Targets: []string{"1.2.3.4"}, + RecordTTL: endpoint.TTL(400), + }, + { + DNSName: "v1.foobar.com", + RecordType: "TXT", + Targets: []string{"boom"}, + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "v1.foo.com", + RecordType: "A", + Targets: []string{"1.2.3.5"}, + RecordTTL: endpoint.TTL(400), + }, + { + DNSName: "v1.foobar.com", + RecordType: "TXT", + Targets: []string{"kablui"}, + }, + }, + } + + err = provider.ApplyChanges(context.Background(), p) + assert.NoError(t, err) + + assert.Equal(t, 4, len(stub.createMsgs)) + assert.Equal(t, 2, len(stub.updateMsgs)) + + assert.True(t, strings.Contains(stub.createMsgs[0].String(), "v1.foo.com")) + assert.True(t, strings.Contains(stub.createMsgs[0].String(), "1.2.3.4")) + assert.True(t, strings.Contains(stub.createMsgs[2].String(), "v1.foo.com")) + assert.True(t, strings.Contains(stub.createMsgs[2].String(), "1.2.3.5")) + + assert.True(t, strings.Contains(stub.updateMsgs[0].String(), "v1.foo.com")) + assert.True(t, strings.Contains(stub.updateMsgs[0].String(), "1.2.3.4")) + + assert.True(t, strings.Contains(stub.createMsgs[1].String(), "v1.foobar.com")) + assert.True(t, strings.Contains(stub.createMsgs[1].String(), "boom")) + assert.True(t, strings.Contains(stub.createMsgs[3].String(), "v1.foobar.com")) + assert.True(t, strings.Contains(stub.createMsgs[3].String(), "kablui")) + + assert.True(t, strings.Contains(stub.updateMsgs[1].String(), "v1.foobar.com")) + assert.True(t, strings.Contains(stub.updateMsgs[1].String(), "boom")) + +} + +func contains(arr []*endpoint.Endpoint, name string) bool { + for _, a := range arr { + if a.DNSName == name { + return true + } + } + return false +} diff --git a/provider/transip.go b/provider/transip/transip.go similarity index 92% rename from provider/transip.go rename to provider/transip/transip.go index b95d90468..47d2e1441 100644 --- a/provider/transip.go +++ b/provider/transip/transip.go @@ -1,4 +1,20 @@ -package provider +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transip import ( "context" @@ -12,6 +28,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -22,6 +39,7 @@ const ( // TransIPProvider is an implementation of Provider for TransIP. type TransIPProvider struct { + provider.BaseProvider client gotransip.SOAPClient domainFilter endpoint.DomainFilter dryRun bool @@ -72,7 +90,7 @@ func (p *TransIPProvider) ApplyChanges(ctx context.Context, changes *plan.Change return err } - zoneNameMapper := zoneIDName{} + zoneNameMapper := provider.ZoneIDName{} zonesByName := make(map[string]transip.Domain) updatedZones := make(map[string]bool) for _, zone := range zones { @@ -232,7 +250,7 @@ func (p *TransIPProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, er // go over all zones and their DNS entries and create endpoints for them for _, zone := range zones { for _, r := range zone.DNSEntries { - if !supportedRecordType(string(r.Type)) { + if !provider.SupportedRecordType(string(r.Type)) { continue } @@ -360,7 +378,7 @@ func (p *TransIPProvider) addEndpointToEntries(ep *endpoint.Endpoint, zone trans // zoneForZoneName returns the zone mapped to given name or error if zone could // not be found -func (p *TransIPProvider) zoneForZoneName(name string, m zoneIDName, z map[string]transip.Domain) (transip.Domain, error) { +func (p *TransIPProvider) zoneForZoneName(name string, m provider.ZoneIDName, z map[string]transip.Domain) (transip.Domain, error) { _, zoneName := m.FindZone(name) if zoneName == "" { return transip.Domain{}, fmt.Errorf("could not find zoneName for %s", name) diff --git a/provider/transip_test.go b/provider/transip/transip_test.go similarity index 90% rename from provider/transip_test.go rename to provider/transip/transip_test.go index 960c57b6c..76e090b78 100644 --- a/provider/transip_test.go +++ b/provider/transip/transip_test.go @@ -1,4 +1,20 @@ -package provider +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package transip import ( "testing" diff --git a/provider/vinyldns.go b/provider/vinyldns/vinyldns.go similarity index 96% rename from provider/vinyldns.go rename to provider/vinyldns/vinyldns.go index 20a4e2f4f..e0cbaef90 100644 --- a/provider/vinyldns.go +++ b/provider/vinyldns/vinyldns.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package vinyldns import ( "context" @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -47,8 +48,9 @@ type vinyldnsZoneInterface interface { } type vinyldnsProvider struct { + provider.BaseProvider client vinyldnsZoneInterface - zoneFilter ZoneIDFilter + zoneFilter provider.ZoneIDFilter domainFilter endpoint.DomainFilter dryRun bool } @@ -59,7 +61,7 @@ type vinyldnsChange struct { } // NewVinylDNSProvider provides support for VinylDNS records -func NewVinylDNSProvider(domainFilter endpoint.DomainFilter, zoneFilter ZoneIDFilter, dryRun bool) (Provider, error) { +func NewVinylDNSProvider(domainFilter endpoint.DomainFilter, zoneFilter provider.ZoneIDFilter, dryRun bool) (provider.Provider, error) { _, ok := os.LookupEnv("VINYLDNS_ACCESS_KEY") if !ok { return nil, fmt.Errorf("no vinyldns access key found") @@ -97,7 +99,7 @@ func (p *vinyldnsProvider) Records(ctx context.Context) (endpoints []*endpoint.E } for _, r := range records { - if supportedRecordType(r.Type) { + if provider.SupportedRecordType(r.Type) { recordsCount := len(r.Records) log.Debugf(fmt.Sprintf("%s.%s.%d.%s", r.Name, r.Type, recordsCount, zone.Name)) @@ -204,7 +206,7 @@ func (p *vinyldnsProvider) findRecordSetID(zoneID string, recordSetName string) } } - return "", fmt.Errorf("Record not found") + return "", fmt.Errorf("record not found") } func (p *vinyldnsProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { diff --git a/provider/vinyldns_test.go b/provider/vinyldns/vinyldns_test.go similarity index 91% rename from provider/vinyldns_test.go rename to provider/vinyldns/vinyldns_test.go index 4a42ee3b6..5388425f2 100644 --- a/provider/vinyldns_test.go +++ b/provider/vinyldns/vinyldns_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package vinyldns import ( "context" @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) type mockVinyldnsZoneInterface struct { @@ -97,12 +98,12 @@ func testVinylDNSProviderRecords(t *testing.T) { assert.Nil(t, err) assert.Equal(t, len(vinylDNSRecords), len(result)) - mockVinylDNSProvider.zoneFilter = NewZoneIDFilter([]string{"0"}) + mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"0"}) result, err = mockVinylDNSProvider.Records(ctx) assert.Nil(t, err) assert.Equal(t, len(vinylDNSRecords), len(result)) - mockVinylDNSProvider.zoneFilter = NewZoneIDFilter([]string{"1"}) + mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"1"}) result, err = mockVinylDNSProvider.Records(ctx) assert.Nil(t, err) assert.Equal(t, 0, len(result)) @@ -118,7 +119,7 @@ func testVinylDNSProviderApplyChanges(t *testing.T) { } changes.Delete = []*endpoint.Endpoint{{DNSName: "example.com", Targets: endpoint.Targets{"vinyldns.com"}, RecordType: endpoint.RecordTypeCNAME}} - mockVinylDNSProvider.zoneFilter = NewZoneIDFilter([]string{"1"}) + mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"1"}) err := mockVinylDNSProvider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("Failed to apply changes: %v", err) @@ -126,7 +127,7 @@ func testVinylDNSProviderApplyChanges(t *testing.T) { } func testVinylDNSSuitableZone(t *testing.T) { - mockVinylDNSProvider.zoneFilter = NewZoneIDFilter([]string{"0"}) + mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"0"}) zone := vinyldnsSuitableZone("example.com", vinylDNSZones) assert.Equal(t, zone.Name, "example.com.") @@ -134,11 +135,11 @@ func testVinylDNSSuitableZone(t *testing.T) { func TestNewVinylDNSProvider(t *testing.T) { os.Setenv("VINYLDNS_ACCESS_KEY", "xxxxxxxxxxxxxxxxxxxxxxxxxx") - _, err := NewVinylDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{"0"}), true) + _, err := NewVinylDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{"0"}), true) assert.Nil(t, err) os.Unsetenv("VINYLDNS_ACCESS_KEY") - _, err = NewVinylDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{"0"}), true) + _, err = NewVinylDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{"0"}), true) assert.NotNil(t, err) if err == nil { t.Errorf("Expected to fail new provider on empty token") @@ -146,7 +147,7 @@ func TestNewVinylDNSProvider(t *testing.T) { } func testVinylDNSFindRecordSetID(t *testing.T) { - mockVinylDNSProvider.zoneFilter = NewZoneIDFilter([]string{"0"}) + mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"0"}) result, err := mockVinylDNSProvider.findRecordSetID("0", "example.com.") assert.Nil(t, err) assert.Equal(t, "", result) diff --git a/provider/vultr.go b/provider/vultr/vultr.go similarity index 94% rename from provider/vultr.go rename to provider/vultr/vultr.go index 869ace7c3..5de422421 100644 --- a/provider/vultr.go +++ b/provider/vultr/vultr.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package vultr import ( "context" @@ -27,6 +27,7 @@ import ( "github.com/vultr/govultr" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) const ( @@ -36,13 +37,16 @@ const ( vultrTTL = 3600 ) +// VultrProvider is an implementation of Provider for Vultr DNS. type VultrProvider struct { + provider.BaseProvider client govultr.Client domainFilter endpoint.DomainFilter DryRun bool } +// VultrChanges differentiates between ChangActions. type VultrChanges struct { Action string @@ -78,6 +82,7 @@ func (p *VultrProvider) Zones(ctx context.Context) ([]govultr.DNSDomain, error) return zones, nil } +// Records returns the list of records. func (p *VultrProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { zones, err := p.Zones(ctx) if err != nil { @@ -93,7 +98,7 @@ func (p *VultrProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, erro } for _, r := range records { - if supportedRecordType(r.Type) { + if provider.SupportedRecordType(r.Type) { name := fmt.Sprintf("%s.%s", r.Name, zone.Domain) // root name is identified by the empty string and should be @@ -151,7 +156,6 @@ func (p *VultrProvider) submitChanges(ctx context.Context, changes []*VultrChang for zoneName, changes := range zoneChanges { for _, change := range changes { - log.WithFields(log.Fields{ "record": change.ResourceRecordSet.Name, "type": change.ResourceRecordSet.Type, @@ -197,10 +201,10 @@ func (p *VultrProvider) submitChanges(ctx context.Context, changes []*VultrChang } } } - return nil } +// ApplyChanges applies a given set of changes in a given zone. func (p *VultrProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { combinedChanges := make([]*VultrChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) @@ -215,7 +219,6 @@ func newVultrChanges(action string, endpoints []*endpoint.Endpoint) []*VultrChan changes := make([]*VultrChanges, 0, len(endpoints)) ttl := vultrTTL for _, e := range endpoints { - if e.RecordTTL.IsConfigured() { ttl = int(e.RecordTTL) } @@ -236,7 +239,7 @@ func newVultrChanges(action string, endpoints []*endpoint.Endpoint) []*VultrChan func seperateChangesByZone(zones []govultr.DNSDomain, changes []*VultrChanges) map[string][]*VultrChanges { change := make(map[string][]*VultrChanges) - zoneNameID := zoneIDName{} + zoneNameID := provider.ZoneIDName{} for _, z := range zones { zoneNameID.Add(z.Domain, z.Domain) @@ -250,7 +253,6 @@ func seperateChangesByZone(zones []govultr.DNSDomain, changes []*VultrChanges) m continue } change[zone] = append(change[zone], c) - } return change } diff --git a/provider/vultr_test.go b/provider/vultr/vultr_test.go similarity index 99% rename from provider/vultr_test.go rename to provider/vultr/vultr_test.go index 73bd0e02a..6487657b6 100644 --- a/provider/vultr_test.go +++ b/provider/vultr/vultr_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package provider +package vultr import ( "context" diff --git a/provider/zone_id_filter.go b/provider/zone_id_filter.go index 066d622f6..e15581f27 100644 --- a/provider/zone_id_filter.go +++ b/provider/zone_id_filter.go @@ -20,7 +20,7 @@ import "strings" // ZoneIDFilter holds a list of zone ids to filter by type ZoneIDFilter struct { - zoneIDs []string + ZoneIDs []string } // NewZoneIDFilter returns a new ZoneIDFilter given a list of zone ids @@ -31,11 +31,11 @@ func NewZoneIDFilter(zoneIDs []string) ZoneIDFilter { // Match checks whether a zone matches one of the provided zone ids func (f ZoneIDFilter) Match(zoneID string) bool { // An empty filter includes all zones. - if len(f.zoneIDs) == 0 { + if len(f.ZoneIDs) == 0 { return true } - for _, id := range f.zoneIDs { + for _, id := range f.ZoneIDs { if strings.HasSuffix(zoneID, id) { return true } diff --git a/provider/zonefinder.go b/provider/zonefinder.go index 6eed20423..18396000b 100644 --- a/provider/zonefinder.go +++ b/provider/zonefinder.go @@ -18,13 +18,13 @@ package provider import "strings" -type zoneIDName map[string]string +type ZoneIDName map[string]string -func (z zoneIDName) Add(zoneID, zoneName string) { +func (z ZoneIDName) Add(zoneID, zoneName string) { z[zoneID] = zoneName } -func (z zoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) { +func (z ZoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) { for zoneID, zoneName := range z { if hostname == zoneName || strings.HasSuffix(hostname, "."+zoneName) { if suitableZoneName == "" || len(zoneName) > len(suitableZoneName) { diff --git a/provider/zonefinder_test.go b/provider/zonefinder_test.go index 58923add4..592ef65fb 100644 --- a/provider/zonefinder_test.go +++ b/provider/zonefinder_test.go @@ -23,12 +23,12 @@ import ( ) func TestZoneIDName(t *testing.T) { - z := zoneIDName{} + z := ZoneIDName{} z.Add("123456", "foo.bar") z.Add("123456", "qux.baz") z.Add("654321", "foo.qux.baz") - assert.Equal(t, zoneIDName{ + assert.Equal(t, ZoneIDName{ "123456": "qux.baz", "654321": "foo.qux.baz", }, z) diff --git a/registry/aws_sd_registry.go b/registry/aws_sd_registry.go index f9a0f0d65..2e4823f71 100644 --- a/registry/aws_sd_registry.go +++ b/registry/aws_sd_registry.go @@ -87,3 +87,7 @@ func (sdr *AWSSDRegistry) updateLabels(endpoints []*endpoint.Endpoint) { ep.Labels[endpoint.AWSSDDescriptionLabel] = ep.Labels.Serialize(false) } } + +func (sdr *AWSSDRegistry) PropertyValuesEqual(name string, previous string, current string) bool { + return sdr.provider.PropertyValuesEqual(name, previous, current) +} diff --git a/registry/aws_sd_registry_test.go b/registry/aws_sd_registry_test.go index 7aca319d7..94054ce51 100644 --- a/registry/aws_sd_registry_test.go +++ b/registry/aws_sd_registry_test.go @@ -26,9 +26,11 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" ) type inMemoryProvider struct { + provider.BaseProvider endpoints []*endpoint.Endpoint onApplyChanges func(changes *plan.Changes) } diff --git a/registry/noop.go b/registry/noop.go index 4b91fbaf5..10e54adb2 100644 --- a/registry/noop.go +++ b/registry/noop.go @@ -45,3 +45,8 @@ func (im *NoopRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, erro func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { return im.provider.ApplyChanges(ctx, changes) } + +// PropertyValuesEqual compares two property values for equality +func (im *NoopRegistry) PropertyValuesEqual(attribute string, previous string, current string) bool { + return im.provider.PropertyValuesEqual(attribute, previous, current) +} diff --git a/registry/noop_test.go b/registry/noop_test.go index 7e7598807..70d7dac58 100644 --- a/registry/noop_test.go +++ b/registry/noop_test.go @@ -26,7 +26,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" + "sigs.k8s.io/external-dns/provider/inmemory" ) var _ Registry = &NoopRegistry{} @@ -38,7 +38,7 @@ func TestNoopRegistry(t *testing.T) { } func testNoopInit(t *testing.T) { - p := provider.NewInMemoryProvider() + p := inmemory.NewInMemoryProvider() r, err := NewNoopRegistry(p) require.NoError(t, err) assert.Equal(t, p, r.provider) @@ -46,9 +46,9 @@ func testNoopInit(t *testing.T) { func testNoopRecords(t *testing.T) { ctx := context.Background() - p := provider.NewInMemoryProvider() + p := inmemory.NewInMemoryProvider() p.CreateZone("org") - providerRecords := []*endpoint.Endpoint{ + inmemoryRecords := []*endpoint.Endpoint{ { DNSName: "example.org", Targets: endpoint.Targets{"example-lb.com"}, @@ -56,21 +56,21 @@ func testNoopRecords(t *testing.T) { }, } p.ApplyChanges(ctx, &plan.Changes{ - Create: providerRecords, + Create: inmemoryRecords, }) r, _ := NewNoopRegistry(p) eps, err := r.Records(ctx) require.NoError(t, err) - assert.True(t, testutils.SameEndpoints(eps, providerRecords)) + assert.True(t, testutils.SameEndpoints(eps, inmemoryRecords)) } func testNoopApplyChanges(t *testing.T) { // do some prep - p := provider.NewInMemoryProvider() + p := inmemory.NewInMemoryProvider() p.CreateZone("org") - providerRecords := []*endpoint.Endpoint{ + inmemoryRecords := []*endpoint.Endpoint{ { DNSName: "example.org", Targets: endpoint.Targets{"old-lb.com"}, @@ -92,7 +92,7 @@ func testNoopApplyChanges(t *testing.T) { ctx := context.Background() p.ApplyChanges(ctx, &plan.Changes{ - Create: providerRecords, + Create: inmemoryRecords, }) // wrong changes @@ -106,7 +106,7 @@ func testNoopApplyChanges(t *testing.T) { }, }, }) - assert.EqualError(t, err, provider.ErrRecordAlreadyExists.Error()) + assert.EqualError(t, err, inmemory.ErrRecordAlreadyExists.Error()) //correct changes require.NoError(t, r.ApplyChanges(ctx, &plan.Changes{ diff --git a/registry/registry.go b/registry/registry.go index 746e7fdd3..1155411d6 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -32,6 +32,7 @@ import ( type Registry interface { Records(ctx context.Context) ([]*endpoint.Endpoint, error) ApplyChanges(ctx context.Context, changes *plan.Changes) error + PropertyValuesEqual(attribute string, previous string, current string) bool } //TODO(ideahitme): consider moving this to Plan diff --git a/registry/txt.go b/registry/txt.go index 2d99b0660..b4cb97a79 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -43,12 +43,16 @@ type TXTRegistry struct { } // NewTXTRegistry returns new TXTRegistry object -func NewTXTRegistry(provider provider.Provider, txtPrefix, ownerID string, cacheInterval time.Duration) (*TXTRegistry, error) { +func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration) (*TXTRegistry, error) { if ownerID == "" { return nil, errors.New("owner id cannot be empty") } - mapper := newPrefixNameMapper(txtPrefix) + if len(txtPrefix) > 0 && len(txtSuffix) > 0 { + return nil, errors.New("txt-prefix and txt-suffix are mutual exclusive") + } + + mapper := newaffixNameMapper(txtPrefix, txtSuffix) return &TXTRegistry{ provider: provider, @@ -187,6 +191,11 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) return im.provider.ApplyChanges(ctx, filteredChanges) } +// PropertyValuesEqual compares two attribute values for equality +func (im *TXTRegistry) PropertyValuesEqual(name string, previous string, current string) bool { + return im.provider.PropertyValuesEqual(name, previous, current) +} + /** TXT registry specific private methods */ @@ -201,26 +210,35 @@ type nameMapper interface { toTXTName(string) string } -type prefixNameMapper struct { +type affixNameMapper struct { prefix string + suffix string } -var _ nameMapper = prefixNameMapper{} +var _ nameMapper = affixNameMapper{} -func newPrefixNameMapper(prefix string) prefixNameMapper { - return prefixNameMapper{prefix: strings.ToLower(prefix)} +func newaffixNameMapper(prefix string, suffix string) affixNameMapper { + return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix)} } -func (pr prefixNameMapper) toEndpointName(txtDNSName string) string { +func (pr affixNameMapper) toEndpointName(txtDNSName string) string { lowerDNSName := strings.ToLower(txtDNSName) - if strings.HasPrefix(lowerDNSName, pr.prefix) { + if strings.HasPrefix(lowerDNSName, pr.prefix) && len(pr.suffix) == 0 { return strings.TrimPrefix(lowerDNSName, pr.prefix) } + + if len(pr.suffix) > 0 { + DNSName := strings.SplitN(lowerDNSName, ".", 2) + if strings.HasSuffix(DNSName[0], pr.suffix) { + return strings.TrimSuffix(DNSName[0], pr.suffix) + "." + DNSName[1] + } + } return "" } -func (pr prefixNameMapper) toTXTName(endpointDNSName string) string { - return pr.prefix + endpointDNSName +func (pr affixNameMapper) toTXTName(endpointDNSName string) string { + DNSName := strings.SplitN(endpointDNSName, ".", 2) + return pr.prefix + DNSName[0] + pr.suffix + "." + DNSName[1] } func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) { diff --git a/registry/txt_test.go b/registry/txt_test.go index 64b31f0f0..5157dd560 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" + "sigs.k8s.io/external-dns/provider/inmemory" ) const ( @@ -42,33 +43,44 @@ func TestTXTRegistry(t *testing.T) { } func testTXTRegistryNew(t *testing.T) { - p := provider.NewInMemoryProvider() - _, err := NewTXTRegistry(p, "txt", "", time.Hour) + p := inmemory.NewInMemoryProvider() + _, err := NewTXTRegistry(p, "txt", "", "", time.Hour) require.Error(t, err) - r, err := NewTXTRegistry(p, "txt", "owner", time.Hour) + _, err = NewTXTRegistry(p, "", "txt", "", time.Hour) + require.Error(t, err) + + r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour) + require.NoError(t, err) + assert.Equal(t, p, r.provider) + + r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour) require.NoError(t, err) - _, ok := r.mapper.(prefixNameMapper) + _, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour) + require.Error(t, err) + + _, ok := r.mapper.(affixNameMapper) require.True(t, ok) assert.Equal(t, "owner", r.ownerID) assert.Equal(t, p, r.provider) - r, err = NewTXTRegistry(p, "", "owner", time.Hour) + r, err = NewTXTRegistry(p, "", "", "owner", time.Hour) require.NoError(t, err) - _, ok = r.mapper.(prefixNameMapper) + _, ok = r.mapper.(affixNameMapper) assert.True(t, ok) } func testTXTRegistryRecords(t *testing.T) { t.Run("With prefix", testTXTRegistryRecordsPrefixed) + t.Run("With suffix", testTXTRegistryRecordsSuffixed) t.Run("No prefix", testTXTRegistryRecordsNoPrefix) } func testTXTRegistryRecordsPrefixed(t *testing.T) { ctx := context.Background() - p := provider.NewInMemoryProvider() + p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -159,20 +171,125 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour) + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour) records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) // Ensure prefix is case-insensitive - r, _ = NewTXTRegistry(p, "TxT.", "owner", time.Hour) + r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour) + records, _ = r.Records(ctx) + + assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) +} + +func testTXTRegistryRecordsSuffixed(t *testing.T) { + ctx := context.Background() + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + p.ApplyChanges(ctx, &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwnerAndLabels("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"foo": "somefoo"}), + newEndpointWithOwnerAndLabels("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"bar": "somebar"}), + newEndpointWithOwner("bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("bar-txt.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), + newEndpointWithOwnerAndLabels("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"tar": "sometar"}), + newEndpointWithOwner("tar-TxT.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), // case-insensitive TXT prefix + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"), + newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), + newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"), + newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), + }, + }) + expectedRecords := []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + "foo": "somefoo", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + "bar": "somebar", + }, + }, + { + DNSName: "bar-txt.test-zone.example.org", + Targets: endpoint.Targets{"baz.test-zone.example.org"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "qux.test-zone.example.org", + Targets: endpoint.Targets{"random"}, + RecordType: endpoint.RecordTypeTXT, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "tar.test-zone.example.org", + Targets: endpoint.Targets{"tar.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner-2", + "tar": "sometar", + }, + }, + { + DNSName: "foobar.test-zone.example.org", + Targets: endpoint.Targets{"foobar.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "multiple.test-zone.example.org", + Targets: endpoint.Targets{"lb1.loadbalancer.com"}, + SetIdentifier: "test-set-1", + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "multiple.test-zone.example.org", + Targets: endpoint.Targets{"lb2.loadbalancer.com"}, + SetIdentifier: "test-set-2", + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + } + + r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour) + records, _ := r.Records(ctx) + + assert.True(t, testutils.SameEndpoints(records, expectedRecords)) + + // Ensure prefix is case-insensitive + r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) } func testTXTRegistryRecordsNoPrefix(t *testing.T) { - p := provider.NewInMemoryProvider() + p := inmemory.NewInMemoryProvider() ctx := context.Background() p.CreateZone(testZone) p.ApplyChanges(ctx, &plan.Changes{ @@ -240,7 +357,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "", "owner", time.Hour) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour) records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) @@ -248,11 +365,12 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { func testTXTRegistryApplyChanges(t *testing.T) { t.Run("With Prefix", testTXTRegistryApplyChangesWithPrefix) + t.Run("With Suffix", testTXTRegistryApplyChangesWithSuffix) t.Run("No prefix", testTXTRegistryApplyChangesNoPrefix) } func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { - p := provider.NewInMemoryProvider() + p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctxEndpoints := []*endpoint.Endpoint{} ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) @@ -276,7 +394,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), }, }) - r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour) + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -342,8 +460,99 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { require.NoError(t, err) } +func testTXTRegistryApplyChangesWithSuffix(t *testing.T) { + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + ctxEndpoints := []*endpoint.Endpoint{} + ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) + } + p.ApplyChanges(ctx, &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("bar-txt.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), + newEndpointWithOwner("foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"), + newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), + newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"), + newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), + }, + }) + r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour) + + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"), + newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), + }, + Delete: []*endpoint.Endpoint{ + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), + newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"), + }, + UpdateNew: []*endpoint.Endpoint{ + newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"), + newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"), + }, + UpdateOld: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), + newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"), + }, + } + expected := &plan.Changes{ + Create: []*endpoint.Endpoint{ + newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"), + newEndpointWithOwner("new-record-1-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "owner", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"), + newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-3"), + }, + Delete: []*endpoint.Endpoint{ + newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), + newEndpointWithOwner("foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"), + newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), + }, + UpdateNew: []*endpoint.Endpoint{ + newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"), + newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"), + newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), + }, + UpdateOld: []*endpoint.Endpoint{ + newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"), + newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"), + newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), + }, + } + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + mExpected := map[string][]*endpoint.Endpoint{ + "Create": expected.Create, + "UpdateNew": expected.UpdateNew, + "UpdateOld": expected.UpdateOld, + "Delete": expected.Delete, + } + mGot := map[string][]*endpoint.Endpoint{ + "Create": got.Create, + "UpdateNew": got.UpdateNew, + "UpdateOld": got.UpdateOld, + "Delete": got.Delete, + } + assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) + assert.Equal(t, nil, ctx.Value(provider.RecordsContextKey)) + } + err := r.ApplyChanges(ctx, changes) + require.NoError(t, err) +} + func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { - p := provider.NewInMemoryProvider() + p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) ctxEndpoints := []*endpoint.Endpoint{} ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) @@ -363,7 +572,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) - r, _ := NewTXTRegistry(p, "", "owner", time.Hour) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -485,10 +694,8 @@ func newEndpointWithOwner(dnsName, target, recordType, ownerID string) *endpoint func newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID string, labels endpoint.Labels) *endpoint.Endpoint { e := endpoint.NewEndpoint(dnsName, recordType, target) e.Labels[endpoint.OwnerLabelKey] = ownerID - if labels != nil { - for k, v := range labels { - e.Labels[k] = v - } + for k, v := range labels { + e.Labels[k] = v } return e } diff --git a/source/cloudfoundry.go b/source/cloudfoundry.go index 8157e841b..ffa79df9a 100644 --- a/source/cloudfoundry.go +++ b/source/cloudfoundry.go @@ -17,8 +17,8 @@ limitations under the License. package source import ( + "context" "net/url" - "time" cfclient "github.com/cloudfoundry-community/go-cfclient" @@ -36,7 +36,7 @@ func NewCloudFoundrySource(cfClient *cfclient.Client) (Source, error) { }, nil } -func (rs *cloudfoundrySource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (rs *cloudfoundrySource) AddEventHandler(ctx context.Context, handler func()) { } // Endpoints returns endpoint objects diff --git a/source/connector.go b/source/connector.go index 13cbe2e94..0bd9479ee 100644 --- a/source/connector.go +++ b/source/connector.go @@ -17,6 +17,7 @@ limitations under the License. package source import ( + "context" "encoding/gob" "net" "time" @@ -65,5 +66,5 @@ func (cs *connectorSource) Endpoints() ([]*endpoint.Endpoint, error) { return endpoints, nil } -func (cs *connectorSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (cs *connectorSource) AddEventHandler(ctx context.Context, handler func()) { } diff --git a/source/crd.go b/source/crd.go index d2ed5b0b2..cbd8828e4 100644 --- a/source/crd.go +++ b/source/crd.go @@ -17,10 +17,10 @@ limitations under the License. package source import ( + "context" "fmt" "os" "strings" - "time" log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -92,7 +92,7 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, kube config.ContentConfig.GroupVersion = &groupVersion config.APIPath = "/apis" - config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)} + config.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)} crdClient, err := rest.UnversionedRESTClientFor(config) if err != nil { @@ -112,7 +112,7 @@ func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFi }, nil } -func (cs *crdSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (cs *crdSource) AddEventHandler(ctx context.Context, handler func()) { } // Endpoints returns endpoint objects. diff --git a/source/crd_test.go b/source/crd_test.go index 02874f034..031903190 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -78,7 +78,7 @@ func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, ki }, } - codecFactory := serializer.DirectCodecFactory{ + codecFactory := serializer.WithoutConversionCodecFactory{ CodecFactory: serializer.NewCodecFactory(scheme), } diff --git a/source/dedup_source.go b/source/dedup_source.go index bf8c25887..bfe5f8519 100644 --- a/source/dedup_source.go +++ b/source/dedup_source.go @@ -17,7 +17,7 @@ limitations under the License. package source import ( - "time" + "context" log "github.com/sirupsen/logrus" @@ -59,6 +59,6 @@ func (ms *dedupSource) Endpoints() ([]*endpoint.Endpoint, error) { return result, nil } -func (ms *dedupSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { - ms.source.AddEventHandler(handler, stopChan, minInterval) +func (ms *dedupSource) AddEventHandler(ctx context.Context, handler func()) { + ms.source.AddEventHandler(ctx, handler) } diff --git a/source/empty.go b/source/empty.go index 415c8705b..f731ae364 100644 --- a/source/empty.go +++ b/source/empty.go @@ -17,7 +17,7 @@ limitations under the License. package source import ( - "time" + "context" "sigs.k8s.io/external-dns/endpoint" ) @@ -25,7 +25,7 @@ import ( // emptySource is a Source that returns no endpoints. type emptySource struct{} -func (e *emptySource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (e *emptySource) AddEventHandler(ctx context.Context, handler func()) { } // Endpoints collects endpoints of all nested Sources and returns them in a single slice. diff --git a/source/fake.go b/source/fake.go index 85fb779c1..54a59b604 100644 --- a/source/fake.go +++ b/source/fake.go @@ -21,6 +21,7 @@ Note: currently only supports IP targets (A records), not hostname targets package source import ( + "context" "fmt" "math/rand" "net" @@ -54,7 +55,7 @@ func NewFakeSource(fqdnTemplate string) (Source, error) { }, nil } -func (sc *fakeSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (sc *fakeSource) AddEventHandler(ctx context.Context, handler func()) { } // Endpoints returns endpoint objects. diff --git a/source/gateway.go b/source/gateway.go index 7e77cc2ec..1ef00d9e5 100644 --- a/source/gateway.go +++ b/source/gateway.go @@ -18,6 +18,7 @@ package source import ( "bytes" + "context" "fmt" "sort" "strings" @@ -25,8 +26,8 @@ import ( "time" log "github.com/sirupsen/logrus" - istionetworking "istio.io/api/networking/v1alpha3" - istiomodel "istio.io/istio/pilot/pkg/model" + networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + istioclient "istio.io/client-go/pkg/clientset/versioned" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/wait" @@ -43,7 +44,7 @@ import ( // Use targetAnnotationKey to explicitly set Endpoint. type gatewaySource struct { kubeClient kubernetes.Interface - istioClient istiomodel.ConfigStore + istioClient istioclient.Interface namespace string annotationFilter string fqdnTemplate *template.Template @@ -55,11 +56,11 @@ type gatewaySource struct { // NewIstioGatewaySource creates a new gatewaySource with the given config. func NewIstioGatewaySource( kubeClient kubernetes.Interface, - istioClient istiomodel.ConfigStore, + istioClient istioclient.Interface, namespace string, annotationFilter string, fqdnTemplate string, - combineFqdnAnnotation bool, + combineFQDNAnnotation bool, ignoreHostnameAnnotation bool, ) (Source, error) { var ( @@ -94,7 +95,7 @@ func NewIstioGatewaySource( informerFactory.Start(wait.NeverStop) // wait for the local cache to be populated. - err = wait.Poll(time.Second, 60*time.Second, func() (bool, error) { + err = poll(time.Second, 60*time.Second, func() (bool, error) { return serviceInformer.Informer().HasSynced(), nil }) if err != nil { @@ -107,7 +108,7 @@ func NewIstioGatewaySource( namespace: namespace, annotationFilter: annotationFilter, fqdnTemplate: tmpl, - combineFQDNAnnotation: combineFqdnAnnotation, + combineFQDNAnnotation: combineFQDNAnnotation, ignoreHostnameAnnotation: ignoreHostnameAnnotation, serviceInformer: serviceInformer, }, nil @@ -116,53 +117,59 @@ func NewIstioGatewaySource( // Endpoints returns endpoint objects for each host-target combination that should be processed. // Retrieves all gateway resources in the source's namespace(s). func (sc *gatewaySource) Endpoints() ([]*endpoint.Endpoint, error) { - configs, err := sc.istioClient.List(istiomodel.Gateway.Type, sc.namespace) + gwList, err := sc.istioClient.NetworkingV1alpha3().Gateways(sc.namespace).List(metav1.ListOptions{}) if err != nil { return nil, err } - configs, err = sc.filterByAnnotations(configs) + gateways := gwList.Items + gateways, err = sc.filterByAnnotations(gateways) if err != nil { return nil, err } - endpoints := []*endpoint.Endpoint{} + var endpoints []*endpoint.Endpoint - for _, config := range configs { + for _, gateway := range gateways { // Check controller annotation to see if we are responsible. - controller, ok := config.Annotations[controllerAnnotationKey] + controller, ok := gateway.Annotations[controllerAnnotationKey] if ok && controller != controllerAnnotationValue { log.Debugf("Skipping gateway %s/%s because controller value does not match, found: %s, required: %s", - config.Namespace, config.Name, controller, controllerAnnotationValue) + gateway.Namespace, gateway.Name, controller, controllerAnnotationValue) continue } - gwEndpoints, err := sc.endpointsFromGatewayConfig(config) + gwHostnames, err := sc.hostNamesFromGateway(gateway) if err != nil { return nil, err } // apply template if host is missing on gateway - if (sc.combineFQDNAnnotation || len(gwEndpoints) == 0) && sc.fqdnTemplate != nil { - iEndpoints, err := sc.endpointsFromTemplate(&config) + if (sc.combineFQDNAnnotation || len(gwHostnames) == 0) && sc.fqdnTemplate != nil { + iHostnames, err := sc.hostNamesFromTemplate(gateway) if err != nil { return nil, err } if sc.combineFQDNAnnotation { - gwEndpoints = append(gwEndpoints, iEndpoints...) + gwHostnames = append(gwHostnames, iHostnames...) } else { - gwEndpoints = iEndpoints + gwHostnames = iHostnames } } - if len(gwEndpoints) == 0 { - log.Debugf("No endpoints could be generated from gateway %s/%s", config.Namespace, config.Name) + if len(gwHostnames) == 0 { + log.Debugf("No hostnames could be generated from gateway %s/%s", gateway.Namespace, gateway.Name) continue } - log.Debugf("Endpoints generated from gateway: %s/%s: %v", config.Namespace, config.Name, gwEndpoints) - sc.setResourceLabel(config, gwEndpoints) + gwEndpoints, err := sc.endpointsFromGateway(gwHostnames, gateway) + if err != nil { + return nil, err + } + + log.Debugf("Endpoints generated from gateway: %s/%s: %v", gateway.Namespace, gateway.Name, gwEndpoints) + sc.setResourceLabel(gateway, gwEndpoints) endpoints = append(endpoints, gwEndpoints...) } @@ -173,47 +180,11 @@ func (sc *gatewaySource) Endpoints() ([]*endpoint.Endpoint, error) { return endpoints, nil } -func (sc *gatewaySource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) { } -func (sc *gatewaySource) endpointsFromTemplate(config *istiomodel.Config) ([]*endpoint.Endpoint, error) { - // Process the whole template string - var buf bytes.Buffer - err := sc.fqdnTemplate.Execute(&buf, config) - if err != nil { - return nil, fmt.Errorf("failed to apply template on istio config %s: %v", config, err) - } - - hostnames := buf.String() - - ttl, err := getTTLFromAnnotations(config.Annotations) - if err != nil { - log.Warn(err) - } - - targets := getTargetsFromTargetAnnotation(config.Annotations) - - if len(targets) == 0 { - targets, err = sc.targetsFromGatewayConfig(config) - if err != nil { - return nil, err - } - } - - providerSpecific, setIdentifier := getProviderSpecificAnnotations(config.Annotations) - - var endpoints []*endpoint.Endpoint - // splits the FQDN template and removes the trailing periods - hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",") - for _, hostname := range hostnameList { - hostname = strings.TrimSuffix(hostname, ".") - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) - } - return endpoints, nil -} - -// filterByAnnotations filters a list of configs by a given annotation selector. -func (sc *gatewaySource) filterByAnnotations(configs []istiomodel.Config) ([]istiomodel.Config, error) { +// filterByAnnotations2 filters a list of configs by a given annotation selector. +func (sc *gatewaySource) filterByAnnotations(gateways []networkingv1alpha3.Gateway) ([]networkingv1alpha3.Gateway, error) { labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) if err != nil { return nil, err @@ -225,33 +196,32 @@ func (sc *gatewaySource) filterByAnnotations(configs []istiomodel.Config) ([]ist // empty filter returns original list if selector.Empty() { - return configs, nil + return gateways, nil } - filteredList := []istiomodel.Config{} + var filteredList []networkingv1alpha3.Gateway - for _, config := range configs { + for _, gw := range gateways { // convert the annotations to an equivalent label selector - annotations := labels.Set(config.Annotations) + annotations := labels.Set(gw.Annotations) // include if the annotations match the selector if selector.Matches(annotations) { - filteredList = append(filteredList, config) + filteredList = append(filteredList, gw) } } return filteredList, nil } -func (sc *gatewaySource) setResourceLabel(config istiomodel.Config, endpoints []*endpoint.Endpoint) { +func (sc *gatewaySource) setResourceLabel(gateway networkingv1alpha3.Gateway, endpoints []*endpoint.Endpoint) { for _, ep := range endpoints { - ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("gateway/%s/%s", config.Namespace, config.Name) + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("gateway/%s/%s", gateway.Namespace, gateway.Name) } } -func (sc *gatewaySource) targetsFromGatewayConfig(config *istiomodel.Config) (targets endpoint.Targets, err error) { - gateway := config.Spec.(*istionetworking.Gateway) - labelSelector, err := metav1.ParseToLabelSelector(labels.Set(gateway.Selector).String()) +func (sc *gatewaySource) targetsFromGatewayConfig(gateway networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { + labelSelector, err := metav1.ParseToLabelSelector(labels.Set(gateway.Spec.Selector).String()) if err != nil { return nil, err } @@ -270,8 +240,7 @@ func (sc *gatewaySource) targetsFromGatewayConfig(config *istiomodel.Config) (ta for _, lb := range service.Status.LoadBalancer.Ingress { if lb.IP != "" { targets = append(targets, lb.IP) - } - if lb.Hostname != "" { + } else if lb.Hostname != "" { targets = append(targets, lb.Hostname) } } @@ -281,28 +250,41 @@ func (sc *gatewaySource) targetsFromGatewayConfig(config *istiomodel.Config) (ta } // endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object -func (sc *gatewaySource) endpointsFromGatewayConfig(config istiomodel.Config) ([]*endpoint.Endpoint, error) { +func (sc *gatewaySource) endpointsFromGateway(hostnames []string, gateway networkingv1alpha3.Gateway) ([]*endpoint.Endpoint, error) { var endpoints []*endpoint.Endpoint - ttl, err := getTTLFromAnnotations(config.Annotations) + annotations := gateway.Annotations + ttl, err := getTTLFromAnnotations(annotations) if err != nil { log.Warn(err) } - targets := getTargetsFromTargetAnnotation(config.Annotations) + targets := getTargetsFromTargetAnnotation(annotations) if len(targets) == 0 { - targets, err = sc.targetsFromGatewayConfig(&config) + targets, err = sc.targetsFromGatewayConfig(gateway) if err != nil { return nil, err } } - gateway := config.Spec.(*istionetworking.Gateway) + providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations) - providerSpecific, setIdentifier := getProviderSpecificAnnotations(config.Annotations) + // Skip endpoints if we do not want entries from annotations + if !sc.ignoreHostnameAnnotation { + hostnames = append(hostnames, getHostnamesFromAnnotations(annotations)...) + } - for _, server := range gateway.Servers { + for _, host := range hostnames { + endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...) + } + + return endpoints, nil +} + +func (sc *gatewaySource) hostNamesFromGateway(gateway networkingv1alpha3.Gateway) ([]string, error) { + var hostnames []string + for _, server := range gateway.Spec.Servers { for _, host := range server.Hosts { if host == "" { continue @@ -316,17 +298,20 @@ func (sc *gatewaySource) endpointsFromGatewayConfig(config istiomodel.Config) ([ host = parts[1] } - endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...) + hostnames = append(hostnames, host) } } - - // Skip endpoints if we do not want entries from annotations - if !sc.ignoreHostnameAnnotation { - hostnameList := getHostnamesFromAnnotations(config.Annotations) - for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) - } - } - - return endpoints, nil + return hostnames, nil +} + +func (sc *gatewaySource) hostNamesFromTemplate(gateway networkingv1alpha3.Gateway) ([]string, error) { + // Process the whole template string + var buf bytes.Buffer + err := sc.fqdnTemplate.Execute(&buf, gateway) + if err != nil { + return nil, fmt.Errorf("failed to apply template on istio gateway %v: %v", gateway, err) + } + + hostnames := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",") + return hostnames, nil } diff --git a/source/gateway_test.go b/source/gateway_test.go index a1b6d5a62..9324fcc05 100644 --- a/source/gateway_test.go +++ b/source/gateway_test.go @@ -17,16 +17,16 @@ limitations under the License. package source import ( - "strconv" - "sync" "testing" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - istionetworking "istio.io/api/networking/v1alpha3" - istiomodel "istio.io/istio/pilot/pkg/model" + networkingv1alpha3api "istio.io/api/networking/v1alpha3" + networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + istioclient "istio.io/client-go/pkg/clientset/versioned" + istiofake "istio.io/client-go/pkg/clientset/versioned/fake" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" @@ -37,13 +37,10 @@ import ( // This is a compile-time validation that gatewaySource is a Source. var _ Source = &gatewaySource{} -var gatewayType = istiomodel.Gateway.Type - type GatewaySuite struct { suite.Suite source Source lbServices []*v1.Service - config istiomodel.Config } func (suite *GatewaySuite) SetupTest() { @@ -81,13 +78,6 @@ func (suite *GatewaySuite) SetupTest() { false, ) suite.NoError(err, "should initialize gateway source") - - suite.config = (fakeGatewayConfig{ - name: "foo-gateway-with-targets", - namespace: "default", - dnsnames: [][]string{{"foo"}}, - }).Config() - _, err = fakeIstioClient.Create(suite.config) suite.NoError(err, "should succeed") } @@ -316,9 +306,12 @@ func testEndpointsFromGatewayConfig(t *testing.T) { }, } { t.Run(ti.title, func(t *testing.T) { + gatewayCfg := ti.config.Config() if source, err := newTestGatewaySource(ti.lbServices); err != nil { require.NoError(t, err) - } else if endpoints, err := source.endpointsFromGatewayConfig(ti.config.Config()); err != nil { + } else if hostnames, err := source.hostNamesFromGateway(gatewayCfg); err != nil { + require.NoError(t, err) + } else if endpoints, err := source.endpointsFromGateway(hostnames, gatewayCfg); err != nil { require.NoError(t, err) } else { validateEndpoints(t, endpoints, ti.expected) @@ -328,7 +321,6 @@ func testEndpointsFromGatewayConfig(t *testing.T) { } func testGatewayEndpoints(t *testing.T) { - namespace := "testing" for _, ti := range []struct { title string targetNamespace string @@ -357,12 +349,12 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", dnsnames: [][]string{{"example.org"}}, }, { name: "fake2", - namespace: namespace, + namespace: "", dnsnames: [][]string{{"new.org"}}, }, }, @@ -397,12 +389,12 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: "testing1", + namespace: "", dnsnames: [][]string{{"example.org"}}, }, { name: "fake2", - namespace: "testing2", + namespace: "", dnsnames: [][]string{{"new.org"}}, }, }, @@ -441,11 +433,6 @@ func testGatewayEndpoints(t *testing.T) { namespace: "testing1", dnsnames: [][]string{{"example.org"}}, }, - { - name: "fake2", - namespace: "testing2", - dnsnames: [][]string{{"new.org"}}, - }, }, expected: []*endpoint.Endpoint{ { @@ -470,7 +457,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ "kubernetes.io/gateway.class": "nginx", }, @@ -496,7 +483,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ "kubernetes.io/gateway.class": "tectonic", }, @@ -517,7 +504,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ "kubernetes.io/gateway.class": "alb", }, @@ -539,7 +526,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ "kubernetes.io/gateway.class": "nginx", }, @@ -565,7 +552,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ "kubernetes.io/gateway.class": "alb", }, @@ -585,7 +572,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ controllerAnnotationKey: controllerAnnotationValue, }, @@ -610,7 +597,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ controllerAnnotationKey: "some-other-tool", }, @@ -631,7 +618,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ controllerAnnotationKey: controllerAnnotationValue, }, @@ -661,7 +648,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ controllerAnnotationKey: "other-controller", }, @@ -682,7 +669,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{}, dnsnames: [][]string{}, }, @@ -712,13 +699,13 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{}, dnsnames: [][]string{}, }, { name: "fake2", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "gateway-target.com", }, @@ -766,7 +753,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "gateway-target.com", }, @@ -774,7 +761,7 @@ func testGatewayEndpoints(t *testing.T) { }, { name: "fake2", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "gateway-target.com", }, @@ -782,7 +769,7 @@ func testGatewayEndpoints(t *testing.T) { }, { name: "fake3", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "1.2.3.4", }, @@ -818,7 +805,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ hostnameAnnotationKey: "dns-through-hostname.com", }, @@ -849,7 +836,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ hostnameAnnotationKey: "dns-through-hostname.com, another-dns-through-hostname.com", }, @@ -885,7 +872,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ hostnameAnnotationKey: "dns-through-hostname.com", targetAnnotationKey: "gateway-target.com", @@ -917,7 +904,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "gateway-target.com", ttlAnnotationKey: "6", @@ -926,7 +913,7 @@ func testGatewayEndpoints(t *testing.T) { }, { name: "fake2", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "gateway-target.com", ttlAnnotationKey: "1", @@ -935,7 +922,7 @@ func testGatewayEndpoints(t *testing.T) { }, { name: "fake3", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "gateway-target.com", ttlAnnotationKey: "10s", @@ -973,7 +960,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "gateway-target.com", }, @@ -981,7 +968,7 @@ func testGatewayEndpoints(t *testing.T) { }, { name: "fake2", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "gateway-target.com", }, @@ -989,7 +976,7 @@ func testGatewayEndpoints(t *testing.T) { }, { name: "fake3", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "1.2.3.4", }, @@ -1027,7 +1014,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ targetAnnotationKey: "", }, @@ -1049,7 +1036,7 @@ func testGatewayEndpoints(t *testing.T) { configItems: []fakeGatewayConfig{ { name: "fake1", - namespace: namespace, + namespace: "", annotations: map[string]string{ hostnameAnnotationKey: "ignore.me", }, @@ -1057,7 +1044,7 @@ func testGatewayEndpoints(t *testing.T) { }, { name: "fake2", - namespace: namespace, + namespace: "", annotations: map[string]string{ hostnameAnnotationKey: "ignore.me.too", }, @@ -1086,10 +1073,6 @@ func testGatewayEndpoints(t *testing.T) { }, } { t.Run(ti.title, func(t *testing.T) { - configs := make([]istiomodel.Config, 0) - for _, item := range ti.configItems { - configs = append(configs, item.Config()) - } fakeKubernetesClient := fake.NewSimpleClientset() @@ -1100,8 +1083,9 @@ func testGatewayEndpoints(t *testing.T) { } fakeIstioClient := NewFakeConfigStore() - for _, config := range configs { - _, err := fakeIstioClient.Create(config) + for _, config := range ti.configItems { + gatewayCfg := config.Config() + _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(ti.targetNamespace).Create(&gatewayCfg) require.NoError(t, err) } @@ -1204,137 +1188,31 @@ type fakeGatewayConfig struct { selector map[string]string } -func (c fakeGatewayConfig) Config() istiomodel.Config { - gw := &istionetworking.Gateway{ - Servers: []*istionetworking.Server{}, +func (c fakeGatewayConfig) Config() networkingv1alpha3.Gateway { + gw := networkingv1alpha3.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.name, + Namespace: c.namespace, + Annotations: c.annotations, + }, + Spec: networkingv1alpha3api.Gateway{ + Servers: nil, + Selector: c.selector, + }, } + var servers []*networkingv1alpha3api.Server for _, dnsnames := range c.dnsnames { - gw.Servers = append(gw.Servers, &istionetworking.Server{ + servers = append(servers, &networkingv1alpha3api.Server{ Hosts: dnsnames, }) } - gw.Selector = c.selector + gw.Spec.Servers = servers - config := istiomodel.Config{ - ConfigMeta: istiomodel.ConfigMeta{ - Namespace: c.namespace, - Name: c.name, - Type: gatewayType, - Annotations: c.annotations, - }, - Spec: gw, - } - - return config + return gw } -type fakeConfigStore struct { - descriptor istiomodel.ConfigDescriptor - configs []*istiomodel.Config - sync.RWMutex -} - -func NewFakeConfigStore() istiomodel.ConfigStore { - return &fakeConfigStore{ - descriptor: istiomodel.ConfigDescriptor{ - istiomodel.Gateway, - }, - configs: make([]*istiomodel.Config, 0), - } -} - -func (f *fakeConfigStore) ConfigDescriptor() istiomodel.ConfigDescriptor { - return f.descriptor -} - -func (f *fakeConfigStore) Get(typ, name, namespace string) (config *istiomodel.Config) { - f.RLock() - defer f.RUnlock() - - if cfg, _ := f.get(typ, name, namespace); cfg != nil { - config = cfg - } - - return -} - -func (f *fakeConfigStore) get(typ, name, namespace string) (*istiomodel.Config, int) { - for idx, cfg := range f.configs { - if cfg.Type == typ && cfg.Name == name && cfg.Namespace == namespace { - return cfg, idx - } - } - - return nil, -1 -} - -func (f *fakeConfigStore) List(typ, namespace string) (configs []istiomodel.Config, err error) { - f.RLock() - defer f.RUnlock() - - if namespace == "" { - for _, cfg := range f.configs { - configs = append(configs, *cfg) - } - } else { - for _, cfg := range f.configs { - if cfg.Type == typ && cfg.Namespace == namespace { - configs = append(configs, *cfg) - } - } - } - - return -} - -func (f *fakeConfigStore) Create(config istiomodel.Config) (revision string, err error) { - f.Lock() - defer f.Unlock() - - if cfg, _ := f.get(config.Type, config.Name, config.Namespace); cfg != nil { - err = errors.New("config already exists") - } else { - revision = "0" - cfg := &config - cfg.ResourceVersion = revision - f.configs = append(f.configs, cfg) - } - - return -} - -func (f *fakeConfigStore) Update(config istiomodel.Config) (newRevision string, err error) { - f.Lock() - defer f.Unlock() - - if oldCfg, idx := f.get(config.Type, config.Name, config.Namespace); oldCfg == nil { - err = errors.New("config does not exist") - } else if oldRevision, e := strconv.Atoi(oldCfg.ResourceVersion); e != nil { - err = e - } else { - newRevision = strconv.Itoa(oldRevision + 1) - cfg := &config - cfg.ResourceVersion = newRevision - f.configs[idx] = cfg - } - - return -} - -func (f *fakeConfigStore) Delete(typ, name, namespace string) error { - f.Lock() - defer f.Unlock() - - _, idx := f.get(typ, name, namespace) - if idx < 0 { - return errors.New("config does not exist") - } - - copy(f.configs[idx:], f.configs[idx+1:]) - f.configs[len(f.configs)-1] = nil - f.configs = f.configs[:len(f.configs)-1] - - return nil +func NewFakeConfigStore() istioclient.Interface { + return istiofake.NewSimpleClientset() } diff --git a/source/ingress.go b/source/ingress.go index 5d7c55685..01972ec41 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -18,6 +18,7 @@ package source import ( "bytes" + "context" "fmt" "sort" "strings" @@ -34,7 +35,6 @@ import ( "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/pkg/k8sutils/async" ) const ( @@ -56,7 +56,6 @@ type ingressSource struct { combineFQDNAnnotation bool ignoreHostnameAnnotation bool ingressInformer extinformers.IngressInformer - runner *async.BoundedFrequencyRunner } // NewIngressSource creates a new ingressSource with the given config. @@ -91,7 +90,7 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt informerFactory.Start(wait.NeverStop) // wait for the local cache to be populated. - err = wait.Poll(time.Second, 60*time.Second, func() (bool, error) { + err = poll(time.Second, 60*time.Second, func() (bool, error) { return ingressInformer.Informer().HasSynced(), nil }) if err != nil { @@ -298,30 +297,21 @@ func targetsFromIngressStatus(status v1beta1.IngressStatus) endpoint.Targets { return targets } -func (sc *ingressSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { - // Add custom resource event handler - log.Debug("Adding (bounded) event handler for ingress") +func (sc *ingressSource) AddEventHandler(ctx context.Context, handler func()) { + log.Debug("Adding event handler for ingress") - maxInterval := 24 * time.Hour // handler will be called if it has not run in 24 hours - burst := 2 // allow up to two handler burst calls - log.Debugf("Adding handler to BoundedFrequencyRunner with minInterval: %v, syncPeriod: %v, bursts: %d", - minInterval, maxInterval, burst) - sc.runner = async.NewBoundedFrequencyRunner("ingress-handler", func() { - _ = handler() - }, minInterval, maxInterval, burst) - go sc.runner.Loop(stopChan) - - // run the handler function as soon as the BoundedFrequencyRunner will allow when an update occurs + // Right now there is no way to remove event handler from informer, see: + // https://github.com/kubernetes/kubernetes/issues/79610 sc.ingressInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { - sc.runner.Run() + handler() }, UpdateFunc: func(old interface{}, new interface{}) { - sc.runner.Run() + handler() }, DeleteFunc: func(obj interface{}) { - sc.runner.Run() + handler() }, }, ) diff --git a/source/ingress_test.go b/source/ingress_test.go index 37a4efd4e..d20b3db74 100644 --- a/source/ingress_test.go +++ b/source/ingress_test.go @@ -26,7 +26,6 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -63,7 +62,7 @@ func (suite *IngressSuite) SetupTest() { hostnames: []string{"v1"}, annotations: map[string]string{ALBDualstackAnnotationKey: ALBDualstackAnnotationValue}, }).Ingress() - _, err = fakeClient.Extensions().Ingresses(suite.fooWithTargets.Namespace).Create(suite.fooWithTargets) + _, err = fakeClient.ExtensionsV1beta1().Ingresses(suite.fooWithTargets.Namespace).Create(suite.fooWithTargets) suite.NoError(err, "should succeed") } @@ -1001,7 +1000,7 @@ func testIngressEndpoints(t *testing.T) { } fakeClient := fake.NewSimpleClientset() - ingressSource, _ := NewIngressSource( + source, _ := NewIngressSource( fakeClient, ti.targetNamespace, ti.annotationFilter, @@ -1010,29 +1009,41 @@ func testIngressEndpoints(t *testing.T) { ti.ignoreHostnameAnnotation, ) for _, ingress := range ingresses { - _, err := fakeClient.Extensions().Ingresses(ingress.Namespace).Create(ingress) + _, err := fakeClient.ExtensionsV1beta1().Ingresses(ingress.Namespace).Create(ingress) require.NoError(t, err) } - var res []*endpoint.Endpoint - var err error + // Wait for the Ingress resources to be visible to the source. We check the + // source's informer cache to detect when this occurs. (This violates encapsulation + // but is okay as this is a test and we want to ensure the informer's cache updates.) + concreteIngressSource := source.(*ingressSource) + ingressLister := concreteIngressSource.ingressInformer.Lister() + err := poll(250*time.Millisecond, 6*time.Second, func() (bool, error) { + allIngressesPresent := true + for _, ingress := range ingresses { + // Skip ingresses that the source would also skip. + if ti.targetNamespace != "" && ti.targetNamespace != ingress.Namespace { + continue + } - // wait up to a few seconds for new resources to appear in informer cache. - err = wait.Poll(time.Second, 3*time.Second, func() (bool, error) { - res, err = ingressSource.Endpoints() - if err != nil { - // stop waiting if we get an error - return true, err + // Check for the presence of this ingress. + _, err := ingressLister.Ingresses(ingress.Namespace).Get(ingress.Name) + if err != nil { + allIngressesPresent = false + break + } } - return len(res) >= len(ti.expected), nil + return allIngressesPresent, nil }) + require.NoError(t, err) + // Informer cache has all of the ingresses. Retrieve and validate their endpoints. + res, err := source.Endpoints() if ti.expectError { - assert.Error(t, err) + require.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) } - validateEndpoints(t, res, ti.expected) }) } diff --git a/source/ingressroute.go b/source/ingressroute.go index 8a61bdb7f..412ab32da 100644 --- a/source/ingressroute.go +++ b/source/ingressroute.go @@ -18,46 +18,51 @@ package source import ( "bytes" + "context" "fmt" "sort" "strings" "text/template" "time" - contourapi "github.com/heptio/contour/apis/contour/v1beta1" - contour "github.com/heptio/contour/apis/generated/clientset/versioned" - contourinformers "github.com/heptio/contour/apis/generated/informers/externalversions" - extinformers "github.com/heptio/contour/apis/generated/informers/externalversions/contour/v1beta1" "github.com/pkg/errors" + contourapi "github.com/projectcontour/contour/apis/contour/v1beta1" log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" ) -// ingressRouteSource is an implementation of Source for Heptio Contour IngressRoute objects. +// ingressRouteSource is an implementation of Source for ProjectContour IngressRoute objects. // The IngressRoute implementation uses the spec.virtualHost.fqdn value for the hostname. // Use targetAnnotationKey to explicitly set Endpoint. type ingressRouteSource struct { + dynamicKubeClient dynamic.Interface kubeClient kubernetes.Interface - contourClient contour.Interface contourLoadBalancerService string namespace string annotationFilter string fqdnTemplate *template.Template combineFQDNAnnotation bool ignoreHostnameAnnotation bool - ingressRouteInformer extinformers.IngressRouteInformer + ingressRouteInformer informers.GenericInformer + unstructuredConverter *UnstructuredConverter } // NewContourIngressRouteSource creates a new contourIngressRouteSource with the given config. func NewContourIngressRouteSource( + dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, - contourClient contour.Interface, contourLoadBalancerService string, namespace string, annotationFilter string, @@ -84,12 +89,8 @@ func NewContourIngressRouteSource( // Use shared informer to listen for add/update/delete of ingressroutes in the specified namespace. // Set resync period to 0, to prevent processing when nothing has changed. - informerFactory := contourinformers.NewSharedInformerFactoryWithOptions( - contourClient, - 0, - contourinformers.WithNamespace(namespace), - ) - ingressRouteInformer := informerFactory.Contour().V1beta1().IngressRoutes() + informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil) + ingressRouteInformer := informerFactory.ForResource(contourapi.IngressRouteGVR) // Add default resource event handlers to properly initialize informer. ingressRouteInformer.Informer().AddEventHandler( @@ -103,16 +104,21 @@ func NewContourIngressRouteSource( informerFactory.Start(wait.NeverStop) // wait for the local cache to be populated. - err = wait.Poll(time.Second, 60*time.Second, func() (bool, error) { + err = poll(time.Second, 60*time.Second, func() (bool, error) { return ingressRouteInformer.Informer().HasSynced(), nil }) if err != nil { return nil, fmt.Errorf("failed to sync cache: %v", err) } + uc, err := NewUnstructuredConverter() + if err != nil { + return nil, fmt.Errorf("failed to setup Unstructured Converter: %v", err) + } + return &ingressRouteSource{ + dynamicKubeClient: dynamicKubeClient, kubeClient: kubeClient, - contourClient: contourClient, contourLoadBalancerService: contourLoadBalancerService, namespace: namespace, annotationFilter: annotationFilter, @@ -120,25 +126,42 @@ func NewContourIngressRouteSource( combineFQDNAnnotation: combineFqdnAnnotation, ignoreHostnameAnnotation: ignoreHostnameAnnotation, ingressRouteInformer: ingressRouteInformer, + unstructuredConverter: uc, }, nil } // Endpoints returns endpoint objects for each host-target combination that should be processed. // Retrieves all ingressroute resources in the source's namespace(s). func (sc *ingressRouteSource) Endpoints() ([]*endpoint.Endpoint, error) { - irs, err := sc.ingressRouteInformer.Lister().IngressRoutes(sc.namespace).List(labels.Everything()) + irs, err := sc.ingressRouteInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything()) if err != nil { return nil, err } - irs, err = sc.filterByAnnotations(irs) + // Convert to []*contourapi.IngressRoute + var ingressRoutes []*contourapi.IngressRoute + for _, ir := range irs { + unstrucuredIR, ok := ir.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert") + } + + irConverted := &contourapi.IngressRoute{} + err := sc.unstructuredConverter.scheme.Convert(unstrucuredIR, irConverted, nil) + if err != nil { + return nil, err + } + ingressRoutes = append(ingressRoutes, irConverted) + } + + ingressRoutes, err = sc.filterByAnnotations(ingressRoutes) if err != nil { return nil, err } endpoints := []*endpoint.Endpoint{} - for _, ir := range irs { + for _, ir := range ingressRoutes { // Check controller annotation to see if we are responsible. controller, ok := ir.Annotations[controllerAnnotationKey] if ok && controller != controllerAnnotationValue { @@ -333,5 +356,28 @@ func parseContourLoadBalancerService(service string) (namespace, name string, er return } -func (sc *ingressRouteSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (sc *ingressRouteSource) AddEventHandler(ctx context.Context, handler func()) { +} + +// UnstructuredConverter handles conversions between unstructured.Unstructured and Contour types +type UnstructuredConverter struct { + // scheme holds an initializer for converting Unstructured to a type + scheme *runtime.Scheme +} + +// NewUnstructuredConverter returns a new UnstructuredConverter initialized +func NewUnstructuredConverter() (*UnstructuredConverter, error) { + uc := &UnstructuredConverter{ + scheme: runtime.NewScheme(), + } + + // Setup converter to understand custom CRD types + contourapi.AddKnownTypes(uc.scheme) + + // Add the core types we need + if err := scheme.AddToScheme(uc.scheme); err != nil { + return nil, err + } + + return uc, nil } diff --git a/source/ingressroute_test.go b/source/ingressroute_test.go index b13f5b7f2..8dc524255 100644 --- a/source/ingressroute_test.go +++ b/source/ingressroute_test.go @@ -17,16 +17,20 @@ limitations under the License. package source import ( + "context" "testing" - contour "github.com/heptio/contour/apis/contour/v1beta1" - fakeContour "github.com/heptio/contour/apis/generated/clientset/versioned/fake" "github.com/pkg/errors" + contour "github.com/projectcontour/contour/apis/contour/v1beta1" + projectcontour "github.com/projectcontour/contour/apis/projectcontour/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + fakeDynamic "k8s.io/client-go/dynamic/fake" fakeKube "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -44,7 +48,7 @@ type IngressRouteSuite struct { func (suite *IngressRouteSuite) SetupTest() { fakeKubernetesClient := fakeKube.NewSimpleClientset() - fakeContourClient := fakeContour.NewSimpleClientset() + fakeDynamicClient, s := newDynamicKubernetesClient() var err error suite.loadBalancer = (fakeLoadBalancerService{ @@ -58,8 +62,8 @@ func (suite *IngressRouteSuite) SetupTest() { suite.NoError(err, "should succeed") suite.source, err = NewContourIngressRouteSource( + fakeDynamicClient, fakeKubernetesClient, - fakeContourClient, "heptio-contour/contour", "default", "", @@ -74,7 +78,14 @@ func (suite *IngressRouteSuite) SetupTest() { namespace: "default", host: "example.com", }).IngressRoute() - _, err = fakeContourClient.ContourV1beta1().IngressRoutes(suite.ingressRoute.Namespace).Create(suite.ingressRoute) + + // Convert to unstructured + unstructuredIngressRoute, err := convertToUnstructured(suite.ingressRoute, s) + if err != nil { + suite.Error(err) + } + + _, err = fakeDynamicClient.Resource(contour.IngressRouteGVR).Namespace(suite.ingressRoute.Namespace).Create(unstructuredIngressRoute, metav1.CreateOptions{}) suite.NoError(err, "should succeed") } @@ -85,6 +96,20 @@ func (suite *IngressRouteSuite) TestResourceLabelIsSet() { } } +func newDynamicKubernetesClient() (*fakeDynamic.FakeDynamicClient, *runtime.Scheme) { + s := runtime.NewScheme() + contour.AddKnownTypes(s) + return fakeDynamic.NewSimpleDynamicClient(s), s +} + +func convertToUnstructured(ir *contour.IngressRoute, s *runtime.Scheme) (*unstructured.Unstructured, error) { + unstructuredIngressRoute := &unstructured.Unstructured{} + if err := s.Convert(ir, unstructuredIngressRoute, context.TODO()); err != nil { + return nil, err + } + return unstructuredIngressRoute, nil +} + func TestIngressRoute(t *testing.T) { suite.Run(t, new(IngressRouteSuite)) t.Run("endpointsFromIngressRoute", testEndpointsFromIngressRoute) @@ -131,9 +156,11 @@ func TestNewContourIngressRouteSource(t *testing.T) { }, } { t.Run(ti.title, func(t *testing.T) { + fakeDynamicClient, _ := newDynamicKubernetesClient() + _, err := NewContourIngressRouteSource( + fakeDynamicClient, fakeKube.NewSimpleClientset(), - fakeContour.NewSimpleClientset(), "heptio-contour/contour", "", ti.annotationFilter, @@ -984,15 +1011,17 @@ func testIngressRouteEndpoints(t *testing.T) { require.NoError(t, err) } - fakeContourClient := fakeContour.NewSimpleClientset() + fakeDynamicClient, scheme := newDynamicKubernetesClient() for _, ingressRoute := range ingressRoutes { - _, err := fakeContourClient.ContourV1beta1().IngressRoutes(ingressRoute.Namespace).Create(ingressRoute) + converted, err := convertToUnstructured(ingressRoute, scheme) + require.NoError(t, err) + _, err = fakeDynamicClient.Resource(contour.IngressRouteGVR).Namespace(ingressRoute.Namespace).Create(converted, metav1.CreateOptions{}) require.NoError(t, err) } ingressRouteSource, err := NewContourIngressRouteSource( + fakeDynamicClient, fakeKubernetesClient, - fakeContourClient, lbService.Namespace+"/"+lbService.Name, ti.targetNamespace, ti.annotationFilter, @@ -1017,7 +1046,7 @@ func testIngressRouteEndpoints(t *testing.T) { // ingressroute specific helper functions func newTestIngressRouteSource(loadBalancer fakeLoadBalancerService) (*ingressRouteSource, error) { fakeKubernetesClient := fakeKube.NewSimpleClientset() - fakeContourClient := fakeContour.NewSimpleClientset() + fakeDynamicClient, _ := newDynamicKubernetesClient() lbService := loadBalancer.Service() _, err := fakeKubernetesClient.CoreV1().Services(lbService.Namespace).Create(lbService) @@ -1026,8 +1055,8 @@ func newTestIngressRouteSource(loadBalancer fakeLoadBalancerService) (*ingressRo } src, err := NewContourIngressRouteSource( + fakeDynamicClient, fakeKubernetesClient, - fakeContourClient, lbService.Namespace+"/"+lbService.Name, "default", "", @@ -1104,7 +1133,7 @@ func (ir fakeIngressRoute) IngressRoute() *contour.IngressRoute { spec = contour.IngressRouteSpec{} } else { spec = contour.IngressRouteSpec{ - VirtualHost: &contour.VirtualHost{ + VirtualHost: &projectcontour.VirtualHost{ Fqdn: ir.host, }, } @@ -1117,7 +1146,7 @@ func (ir fakeIngressRoute) IngressRoute() *contour.IngressRoute { Annotations: ir.annotations, }, Spec: spec, - Status: contour.Status{ + Status: projectcontour.Status{ CurrentStatus: status, }, } diff --git a/source/multi_source.go b/source/multi_source.go index 1f255ceb3..494acd16b 100644 --- a/source/multi_source.go +++ b/source/multi_source.go @@ -17,7 +17,7 @@ limitations under the License. package source import ( - "time" + "context" "sigs.k8s.io/external-dns/endpoint" ) @@ -43,9 +43,9 @@ func (ms *multiSource) Endpoints() ([]*endpoint.Endpoint, error) { return result, nil } -func (ms *multiSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (ms *multiSource) AddEventHandler(ctx context.Context, handler func()) { for _, s := range ms.children { - s.AddEventHandler(handler, stopChan, minInterval) + s.AddEventHandler(ctx, handler) } } diff --git a/source/node.go b/source/node.go index 2ab323b76..d54ff890b 100644 --- a/source/node.go +++ b/source/node.go @@ -18,6 +18,7 @@ package source import ( "bytes" + "context" "fmt" "strings" "text/template" @@ -77,7 +78,7 @@ func NewNodeSource(kubeClient kubernetes.Interface, annotationFilter, fqdnTempla informerFactory.Start(wait.NeverStop) // wait for the local cache to be populated. - err = wait.Poll(time.Second, 60*time.Second, func() (bool, error) { + err = poll(time.Second, 60*time.Second, func() (bool, error) { return nodeInformer.Informer().HasSynced(), nil }) if err != nil { @@ -167,7 +168,7 @@ func (ns *nodeSource) Endpoints() ([]*endpoint.Endpoint, error) { return endpointsSlice, nil } -func (ns *nodeSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (ns *nodeSource) AddEventHandler(ctx context.Context, handler func()) { } // nodeAddress returns node's externalIP and if that's not found, node's internalIP diff --git a/source/ocproute.go b/source/ocproute.go index 528cb8fce..0f2ed30cf 100644 --- a/source/ocproute.go +++ b/source/ocproute.go @@ -18,6 +18,7 @@ package source import ( "bytes" + "context" "fmt" "sort" "strings" @@ -90,7 +91,7 @@ func NewOcpRouteSource( informerFactory.Start(wait.NeverStop) // wait for the local cache to be populated. - err = wait.Poll(time.Second, 60*time.Second, func() (bool, error) { + err = poll(time.Second, 60*time.Second, func() (bool, error) { return routeInformer.Informer().HasSynced(), nil }) if err != nil { @@ -109,7 +110,7 @@ func NewOcpRouteSource( } // TODO add a meaningful EventHandler -func (ors *ocpRouteSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +func (ors *ocpRouteSource) AddEventHandler(ctx context.Context, handler func()) { } // Endpoints returns endpoint objects for each host-target combination that should be processed. diff --git a/source/routegroup.go b/source/routegroup.go index 3a91e02a1..a9dbe97cf 100644 --- a/source/routegroup.go +++ b/source/routegroup.go @@ -18,6 +18,7 @@ package source import ( "bytes" + "context" "crypto/tls" "crypto/x509" "encoding/json" @@ -37,7 +38,8 @@ import ( ) const ( - defaultIdleConnTimeout = 30 * time.Second + defaultIdleConnTimeout = 30 * time.Second + // DefaultRoutegroupVersion is the default version for route groups. DefaultRoutegroupVersion = "zalando.org/v1" routeGroupListResource = "/apis/%s/routegroups" routeGroupNamespacedResource = "/apis/%s/namespaces/%s/routegroups" @@ -238,7 +240,7 @@ func NewRouteGroupSource(timeout time.Duration, token, tokenPath, master, namesp } // AddEventHandler for routegroup is currently a no op, because we do not implement caching, yet. -func (sc *routeGroupSource) AddEventHandler(func() error, <-chan struct{}, time.Duration) {} +func (sc *routeGroupSource) AddEventHandler(ctx context.Context, handler func()) {} // Endpoints returns endpoint objects for each host-target combination that should be processed. // Retrieves all routeGroup resources on all namespaces. diff --git a/source/routegroup_test.go b/source/routegroup_test.go index c8bff1a93..69e78da59 100644 --- a/source/routegroup_test.go +++ b/source/routegroup_test.go @@ -709,7 +709,6 @@ func TestRouteGroupsEndpoints(t *testing.T) { }} { t.Run(tt.name, func(t *testing.T) { if tt.fqdnTemplate != "" { - println("fqdnTemplate is set") tmpl, err := parseTemplate(tt.fqdnTemplate) if err != nil { t.Fatalf("Failed to parse template: %v", err) diff --git a/source/service.go b/source/service.go index ee697022a..2c47dc752 100644 --- a/source/service.go +++ b/source/service.go @@ -18,6 +18,7 @@ package source import ( "bytes" + "context" "fmt" "sort" "strings" @@ -35,7 +36,6 @@ import ( "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/pkg/k8sutils/async" ) const ( @@ -65,7 +65,6 @@ type serviceSource struct { podInformer coreinformers.PodInformer nodeInformer coreinformers.NodeInformer serviceTypeFilter map[string]struct{} - runner *async.BoundedFrequencyRunner } // NewServiceSource creates a new serviceSource with the given config. @@ -121,8 +120,11 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt informerFactory.Start(wait.NeverStop) // wait for the local cache to be populated. - err = wait.Poll(time.Second, 60*time.Second, func() (bool, error) { - return serviceInformer.Informer().HasSynced(), nil + err = poll(time.Second, 60*time.Second, func() (bool, error) { + return serviceInformer.Informer().HasSynced() && + endpointsInformer.Informer().HasSynced() && + podInformer.Informer().HasSynced() && + nodeInformer.Informer().HasSynced(), nil }) if err != nil { return nil, fmt.Errorf("failed to sync cache: %v", err) @@ -593,30 +595,21 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets e return endpoints } -func (sc *serviceSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { - // Add custom resource event handler - log.Debug("Adding (bounded) event handler for service") +func (sc *serviceSource) AddEventHandler(ctx context.Context, handler func()) { + log.Debug("Adding event handler for service") - maxInterval := 24 * time.Hour // handler will be called if it has not run in 24 hours - burst := 2 // allow up to two handler burst calls - log.Debugf("Adding handler to BoundedFrequencyRunner with minInterval: %v, syncPeriod: %v, bursts: %d", - minInterval, maxInterval, burst) - sc.runner = async.NewBoundedFrequencyRunner("service-handler", func() { - _ = handler() - }, minInterval, maxInterval, burst) - go sc.runner.Loop(stopChan) - - // run the handler function as soon as the BoundedFrequencyRunner will allow when an update occurs + // Right now there is no way to remove event handler from informer, see: + // https://github.com/kubernetes/kubernetes/issues/79610 sc.serviceInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { - sc.runner.Run() + handler() }, UpdateFunc: func(old interface{}, new interface{}) { - sc.runner.Run() + handler() }, DeleteFunc: func(obj interface{}) { - sc.runner.Run() + handler() }, }, ) diff --git a/source/service_test.go b/source/service_test.go index 3cdf63a12..c5e0dd838 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -26,7 +26,6 @@ import ( "github.com/stretchr/testify/suite" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -1118,7 +1117,7 @@ func testServiceSourceEndpoints(t *testing.T) { var res []*endpoint.Endpoint // wait up to a few seconds for new resources to appear in informer cache. - err = wait.Poll(time.Second, 3*time.Second, func() (bool, error) { + err = poll(time.Second, 3*time.Second, func() (bool, error) { res, err = client.Endpoints() if err != nil { // stop waiting if we get an error @@ -1552,7 +1551,7 @@ func TestNodePortServices(t *testing.T) { // Create the nodes for _, node := range tc.nodes { - if _, err := kubernetes.Core().Nodes().Create(node); err != nil { + if _, err := kubernetes.CoreV1().Nodes().Create(node); err != nil { t.Fatal(err) } } diff --git a/source/source.go b/source/source.go index 458872739..d8394ebfb 100644 --- a/source/source.go +++ b/source/source.go @@ -17,6 +17,7 @@ limitations under the License. package source import ( + "context" "fmt" "math" "net" @@ -26,7 +27,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/internal/config" ) const ( @@ -60,9 +63,8 @@ const ( // Source defines the interface Endpoint sources should implement. type Source interface { Endpoints() ([]*endpoint.Endpoint, error) - // AddEventHandler adds an event handler function that's called when (supported) sources have changed. - // The handler should not be called more than than once per time.Duration and not again after stop channel is closed. - AddEventHandler(func() error, <-chan struct{}, time.Duration) + // AddEventHandler adds an event handler that should be triggered if something in source changes + AddEventHandler(context.Context, func()) } func getTTLFromAnnotations(annotations map[string]string) (endpoint.TTL, error) { @@ -224,3 +226,24 @@ func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]stri annotations := labels.Set(srcAnnotations) return selector.Matches(annotations) } + +func poll(interval time.Duration, timeout time.Duration, condition wait.ConditionFunc) error { + if config.FastPoll { + time.Sleep(5 * time.Millisecond) + + ok, err := condition() + + if err != nil { + return err + } + + if ok { + return nil + } + + interval = 50 * time.Millisecond + timeout = 10 * time.Second + } + + return wait.Poll(interval, timeout, condition) +} diff --git a/source/source_test.go b/source/source_test.go index 04c3b4612..dbf6c90c7 100644 --- a/source/source_test.go +++ b/source/source_test.go @@ -17,12 +17,15 @@ limitations under the License. package source import ( + "context" "fmt" "testing" + "time" "github.com/stretchr/testify/assert" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/internal/testutils" ) func TestGetTTLFromAnnotations(t *testing.T) { @@ -105,3 +108,35 @@ func TestSuitableType(t *testing.T) { } } } + +// TestSourceEventHandler that AddEventHandler calls provided handler +func TestSourceEventHandler(t *testing.T) { + source := new(testutils.MockSource) + + handlerCh := make(chan bool) + + ctx, cancel := context.WithCancel(context.Background()) + + // Define and register a simple handler that sends a message to a channel to show it was called. + handler := func() { + handlerCh <- true + } + // Example of preventing handler from being called more than once every 5 seconds. + source.AddEventHandler(ctx, handler) + + // Send timeout message after 10 seconds to fail test if handler is not called. + go func() { + time.Sleep(10 * time.Second) + cancel() + }() + + // Wait until we either receive a message from handlerCh or timeoutCh channel after 10 seconds. + select { + case msg := <-handlerCh: + assert.True(t, msg) + case <-ctx.Done(): + assert.Fail(t, "timed out waiting for event handler to be called") + } + + close(handlerCh) +} diff --git a/source/store.go b/source/store.go index 5ad9f97fd..070ef9725 100644 --- a/source/store.go +++ b/source/store.go @@ -17,7 +17,6 @@ limitations under the License. package source import ( - "errors" "net/http" "os" "strings" @@ -25,12 +24,12 @@ import ( "time" "github.com/cloudfoundry-community/go-cfclient" - contour "github.com/heptio/contour/apis/generated/clientset/versioned" "github.com/linki/instrumented_http" openshift "github.com/openshift/client-go/route/clientset/versioned" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" - istiocontroller "istio.io/istio/pilot/pkg/config/kube/crd/controller" - istiomodel "istio.io/istio/pilot/pkg/model" + istioclient "istio.io/client-go/pkg/clientset/versioned" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -68,9 +67,9 @@ type Config struct { // ClientGenerator provides clients type ClientGenerator interface { KubeClient() (kubernetes.Interface, error) - IstioClient() (istiomodel.ConfigStore, error) + IstioClient() (istioclient.Interface, error) CloudFoundryClient(cfAPPEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error) - ContourClient() (contour.Interface, error) + DynamicKubernetesClient() (dynamic.Interface, error) OpenShiftClient() (openshift.Interface, error) } @@ -81,9 +80,9 @@ type SingletonClientGenerator struct { KubeMaster string RequestTimeout time.Duration kubeClient kubernetes.Interface - istioClient istiomodel.ConfigStore + istioClient *istioclient.Clientset cfClient *cfclient.Client - contourClient contour.Interface + contourClient dynamic.Interface openshiftClient openshift.Interface kubeOnce sync.Once istioOnce sync.Once @@ -101,11 +100,11 @@ func (p *SingletonClientGenerator) KubeClient() (kubernetes.Interface, error) { return p.kubeClient, err } -// IstioClient generates an istio client if it was not created before -func (p *SingletonClientGenerator) IstioClient() (istiomodel.ConfigStore, error) { +// IstioClient generates an istio go client if it was not created before +func (p *SingletonClientGenerator) IstioClient() (istioclient.Interface, error) { var err error p.istioOnce.Do(func() { - p.istioClient, err = NewIstioClient(p.KubeConfig) + p.istioClient, err = NewIstioClient(p.KubeConfig, p.KubeMaster) }) return p.istioClient, err } @@ -134,11 +133,11 @@ func NewCFClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*c return client, nil } -// ContourClient generates a contour client if it was not created before -func (p *SingletonClientGenerator) ContourClient() (contour.Interface, error) { +// DynamicKubernetesClient generates a contour client if it was not created before +func (p *SingletonClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) { var err error p.contourOnce.Do(func() { - p.contourClient, err = NewContourClient(p.KubeConfig, p.KubeMaster, p.RequestTimeout) + p.contourClient, err = NewDynamicKubernetesClient(p.KubeConfig, p.KubeMaster, p.RequestTimeout) }) return p.contourClient, err } @@ -208,11 +207,11 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err if err != nil { return nil, err } - contourClient, err := p.ContourClient() + dynamicClient, err := p.DynamicKubernetesClient() if err != nil { return nil, err } - return NewContourIngressRouteSource(kubernetesClient, contourClient, cfg.ContourLoadBalancerService, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return NewContourIngressRouteSource(dynamicClient, kubernetesClient, cfg.ContourLoadBalancerService, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) case "openshift-route": ocpClient, err := p.OpenShiftClient() if err != nil { @@ -316,32 +315,30 @@ func NewKubeClient(kubeConfig, kubeMaster string, requestTimeout time.Duration) // wrappers) to the client's config at this level. Furthermore, the Istio client // constructor does not expose the ability to override the Kubernetes master, // so the Master config attribute has no effect. -func NewIstioClient(kubeConfig string) (*istiocontroller.Client, error) { +func NewIstioClient(kubeConfig string, kubeMaster string) (*istioclient.Clientset, error) { if kubeConfig == "" { if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil { kubeConfig = clientcmd.RecommendedHomeFile } } - client, err := istiocontroller.NewClient( - kubeConfig, - "", - istiomodel.ConfigDescriptor{istiomodel.Gateway}, - "", - ) + restCfg, err := clientcmd.BuildConfigFromFlags(kubeMaster, kubeConfig) if err != nil { return nil, err } - log.Info("Created Istio client") + ic, err := istioclient.NewForConfig(restCfg) + if err != nil { + return nil, errors.Wrap(err, "Failed to create istio client") + } - return client, nil + return ic, nil } -// NewContourClient returns a new Contour client object. It takes a Config and +// NewDynamicKubernetesClient returns a new Dynamic Kubernetes client object. It takes a Config and // uses KubeMaster and KubeConfig attributes to connect to the cluster. If // KubeConfig isn't provided it defaults to using the recommended default. -func NewContourClient(kubeConfig, kubeMaster string, requestTimeout time.Duration) (*contour.Clientset, error) { +func NewDynamicKubernetesClient(kubeConfig, kubeMaster string, requestTimeout time.Duration) (dynamic.Interface, error) { if kubeConfig == "" { if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil { kubeConfig = clientcmd.RecommendedHomeFile @@ -364,12 +361,12 @@ func NewContourClient(kubeConfig, kubeMaster string, requestTimeout time.Duratio config.Timeout = requestTimeout - client, err := contour.NewForConfig(config) + client, err := dynamic.NewForConfig(config) if err != nil { return nil, err } - log.Infof("Created Contour client %s", config.Host) + log.Infof("Created Dynamic Kubernetes client %s", config.Host) return client, nil } diff --git a/source/store_test.go b/source/store_test.go index f9acffa5d..15652ddcd 100644 --- a/source/store_test.go +++ b/source/store_test.go @@ -21,23 +21,22 @@ import ( "testing" cfclient "github.com/cloudfoundry-community/go-cfclient" - contour "github.com/heptio/contour/apis/generated/clientset/versioned" - fakeContour "github.com/heptio/contour/apis/generated/clientset/versioned/fake" openshift "github.com/openshift/client-go/route/clientset/versioned" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" - istiomodel "istio.io/istio/pilot/pkg/model" + istioclient "istio.io/client-go/pkg/clientset/versioned" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/fake" + fakeKube "k8s.io/client-go/kubernetes/fake" ) type MockClientGenerator struct { mock.Mock - kubeClient kubernetes.Interface - istioClient istiomodel.ConfigStore - cloudFoundryClient *cfclient.Client - contourClient contour.Interface - openshiftClient openshift.Interface + kubeClient kubernetes.Interface + istioClient istioclient.Interface + cloudFoundryClient *cfclient.Client + dynamicKubernetesClient dynamic.Interface + openshiftClient openshift.Interface } func (m *MockClientGenerator) KubeClient() (kubernetes.Interface, error) { @@ -49,10 +48,10 @@ func (m *MockClientGenerator) KubeClient() (kubernetes.Interface, error) { return nil, args.Error(1) } -func (m *MockClientGenerator) IstioClient() (istiomodel.ConfigStore, error) { +func (m *MockClientGenerator) IstioClient() (istioclient.Interface, error) { args := m.Called() if args.Error(1) == nil { - m.istioClient = args.Get(0).(istiomodel.ConfigStore) + m.istioClient = args.Get(0).(istioclient.Interface) return m.istioClient, nil } return nil, args.Error(1) @@ -67,11 +66,11 @@ func (m *MockClientGenerator) CloudFoundryClient(cfAPIEndpoint string, cfUsernam return nil, args.Error(1) } -func (m *MockClientGenerator) ContourClient() (contour.Interface, error) { +func (m *MockClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) { args := m.Called() if args.Error(1) == nil { - m.contourClient = args.Get(0).(contour.Interface) - return m.contourClient, nil + m.dynamicKubernetesClient = args.Get(0).(dynamic.Interface) + return m.dynamicKubernetesClient, nil } return nil, args.Error(1) } @@ -90,10 +89,12 @@ type ByNamesTestSuite struct { } func (suite *ByNamesTestSuite) TestAllInitialized() { + fakeDynamic, _ := newDynamicKubernetesClient() + mockClientGenerator := new(MockClientGenerator) - mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil) + mockClientGenerator.On("KubeClient").Return(fakeKube.NewSimpleClientset(), nil) mockClientGenerator.On("IstioClient").Return(NewFakeConfigStore(), nil) - mockClientGenerator.On("ContourClient").Return(fakeContour.NewSimpleClientset(), nil) + mockClientGenerator.On("DynamicKubernetesClient").Return(fakeDynamic, nil) sources, err := ByNames(mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-ingressroute", "fake"}, minimalConfig) suite.NoError(err, "should not generate errors") @@ -102,7 +103,7 @@ func (suite *ByNamesTestSuite) TestAllInitialized() { func (suite *ByNamesTestSuite) TestOnlyFake() { mockClientGenerator := new(MockClientGenerator) - mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil) + mockClientGenerator.On("KubeClient").Return(fakeKube.NewSimpleClientset(), nil) sources, err := ByNames(mockClientGenerator, []string{"fake"}, minimalConfig) suite.NoError(err, "should not generate errors") @@ -112,7 +113,7 @@ func (suite *ByNamesTestSuite) TestOnlyFake() { func (suite *ByNamesTestSuite) TestSourceNotFound() { mockClientGenerator := new(MockClientGenerator) - mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil) + mockClientGenerator.On("KubeClient").Return(fakeKube.NewSimpleClientset(), nil) sources, err := ByNames(mockClientGenerator, []string{"foo"}, minimalConfig) suite.Equal(err, ErrSourceNotFound, "should return source not found") @@ -138,9 +139,9 @@ func (suite *ByNamesTestSuite) TestKubeClientFails() { func (suite *ByNamesTestSuite) TestIstioClientFails() { mockClientGenerator := new(MockClientGenerator) - mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil) + mockClientGenerator.On("KubeClient").Return(fakeKube.NewSimpleClientset(), nil) mockClientGenerator.On("IstioClient").Return(nil, errors.New("foo")) - mockClientGenerator.On("ContourClient").Return(nil, errors.New("foo")) + mockClientGenerator.On("DynamicKubernetesClient").Return(nil, errors.New("foo")) _, err := ByNames(mockClientGenerator, []string{"istio-gateway"}, minimalConfig) suite.Error(err, "should return an error if istio client cannot be created")