Merge branch 'master' into master

This commit is contained in:
Kushal Bhandari 2020-06-16 13:55:52 -07:00 committed by GitHub
commit 00da3a130f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
121 changed files with 3748 additions and 2464 deletions

View File

@ -1,23 +1,70 @@
run:
concurrency: 4
modules-download-mode: readonly
linters-settings: linters-settings:
exhaustive:
default-signifies-exhaustive: false
goimports:
local-prefixes: github.com/kubernetes-sigs/external-dns
golint: golint:
min-confidence: 0.9 min-confidence: 0.9
maligned:
gocyclo: suggest-new: true
min-complexity: 15 misspell:
locale: US
linters: 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 disable-all: true
enable: enable:
- deadcode
- depguard
- dogsled
- gofmt
- goimports
- golint
- goprintffuncname
- gosimple
- govet - govet
- ineffassign - ineffassign
- golint
- goimports
- misspell
- unconvert
- megacheck
- interfacer - 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

View File

@ -6,23 +6,35 @@ os:
language: go language: go
go: go:
- "1.13.x" - "1.14.x"
- tip - tip
matrix: matrix:
allow_failures: allow_failures:
- go: tip - go: tip
env: cache:
- GOLANGCI_RELEASE="v1.23.1" directories:
- $GOPATH/pkg/mod
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}
script: script:
- make test - make test
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 - 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 - travis_wait 20 roveralls
- goveralls -coverprofile=roveralls.coverprofile -service=travis-ci - goveralls -coverprofile=roveralls.coverprofile -service=travis-ci

View File

@ -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 ## v0.7.1 - 2020-04-01
- Prometheus metric: timestamp of last successful sync with the DNS provider (#1480) @njuettner - Prometheus metric: timestamp of last successful sync with the DNS provider (#1480) @njuettner

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
FROM golang:1.13 as builder FROM golang:1.14 as builder
WORKDIR /sigs.k8s.io/external-dns WORKDIR /sigs.k8s.io/external-dns

View File

@ -31,14 +31,14 @@ cover-html: cover
# Run all the linters # Run all the linters
lint: 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 # The verify target runs tasks similar to the CI tasks, but without code coverage
.PHONY: verify test .PHONY: verify test
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 # The build targets allow to build the binary and docker image
.PHONY: build build.docker build.mini .PHONY: build build.docker build.mini

View File

@ -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. 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 ## The Latest Release: v0.7

View File

@ -18,6 +18,7 @@ package controller
import ( import (
"context" "context"
"sync"
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -112,6 +113,10 @@ type Controller struct {
Interval time.Duration Interval time.Duration
// The DomainFilter defines which DNS records to keep or exclude // The DomainFilter defines which DNS records to keep or exclude
DomainFilter endpoint.DomainFilter 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. // RunOnce runs a single iteration of a reconciliation loop.
@ -139,6 +144,7 @@ func (c *Controller) RunOnce(ctx context.Context) error {
Current: records, Current: records,
Desired: endpoints, Desired: endpoints,
DomainFilter: c.DomainFilter, DomainFilter: c.DomainFilter,
PropertyComparator: c.Registry.PropertyValuesEqual,
} }
plan = plan.Calculate() plan = plan.Calculate()
@ -154,18 +160,39 @@ func (c *Controller) RunOnce(ctx context.Context) error {
return nil return nil
} }
// Run runs RunOnce in a loop with a delay until stopChan receives a value. // MinInterval is used as window for batching events
func (c *Controller) Run(ctx context.Context, stopChan <-chan struct{}) { const MinInterval = 5 * time.Second
ticker := time.NewTicker(c.Interval)
// 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() defer ticker.Stop()
for { for {
err := c.RunOnce(ctx) if c.ShouldRunOnce(time.Now()) {
if err != nil { if err := c.RunOnce(ctx); err != nil {
log.Error(err) log.Error(err)
} }
}
select { select {
case <-ticker.C: case <-ticker.C:
case <-stopChan: case <-ctx.Done():
log.Info("Terminating main controller loop") log.Info("Terminating main controller loop")
return return
} }

View File

@ -35,6 +35,7 @@ import (
// mockProvider returns mock endpoints and validates changes. // mockProvider returns mock endpoints and validates changes.
type mockProvider struct { type mockProvider struct {
provider.BaseProvider
RecordsStore []*endpoint.Endpoint RecordsStore []*endpoint.Endpoint
ExpectChanges *plan.Changes ExpectChanges *plan.Changes
} }
@ -153,43 +154,41 @@ func TestRunOnce(t *testing.T) {
source.AssertExpectations(t) source.AssertExpectations(t)
} }
// TestSourceEventHandler tests that the Controller can use a Source's registered handler as a callback. func TestShouldRunOnce(t *testing.T) {
func TestSourceEventHandler(t *testing.T) { ctrl := &Controller{Interval: 10 * time.Minute}
source := new(testutils.MockSource)
handlerCh := make(chan bool) now := time.Now()
timeoutCh := make(chan bool, 1)
stopChan := make(chan struct{}, 1)
ctrl := &Controller{ // First run of Run loop should execute RunOnce
Source: source, assert.True(t, ctrl.ShouldRunOnce(now))
Registry: nil,
Policy: &plan.SyncPolicy{}, // Second run should not
} assert.False(t, ctrl.ShouldRunOnce(now))
// Define and register a simple handler that sends a message to a channel to show it was called. now = now.Add(10 * time.Second)
handler := func() error { // Changes happen in ingresses or services
handlerCh <- true ctrl.ScheduleRunOnce(now)
return nil ctrl.ScheduleRunOnce(now)
}
// Example of preventing handler from being called more than once every 5 seconds. // Because we batch changes, ShouldRunOnce returns False at first
ctrl.Source.AddEventHandler(handler, stopChan, 5*time.Second) assert.False(t, ctrl.ShouldRunOnce(now))
assert.False(t, ctrl.ShouldRunOnce(now.Add(100*time.Microsecond)))
// Send timeout message after 10 seconds to fail test if handler is not called.
go func() { // But after MinInterval we should run reconciliation
time.Sleep(10 * time.Second) now = now.Add(MinInterval)
timeoutCh <- true assert.True(t, ctrl.ShouldRunOnce(now))
}()
// But just one time
// Wait until we either receive a message from handlerCh or timeoutCh channel after 10 seconds. assert.False(t, ctrl.ShouldRunOnce(now))
select {
case msg := <-handlerCh: // We should wait maximum possible time after last reconciliation started
assert.True(t, msg) now = now.Add(10*time.Minute - time.Second)
case <-timeoutCh: assert.False(t, ctrl.ShouldRunOnce(now))
assert.Fail(t, "timed out waiting for event handler to be called")
} // After exactly Interval it's OK again to reconcile
now = now.Add(time.Second)
close(stopChan) assert.True(t, ctrl.ShouldRunOnce(now))
close(handlerCh)
close(timeoutCh) // But not two times
assert.False(t, ctrl.ShouldRunOnce(now))
} }

View File

@ -51,7 +51,7 @@ PRs welcome!
Notes 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 ### AWS Provider
The AWS Provider overrides the value to 300s when the TTL is 0. The AWS Provider overrides the value to 300s when the TTL is 0.

View File

@ -36,9 +36,6 @@ spec:
app: external-dns app: external-dns
strategy: strategy:
type: Recreate type: Recreate
selector:
matchLabels:
app: external-dns
template: template:
metadata: metadata:
labels: labels:
@ -102,9 +99,6 @@ spec:
app: external-dns app: external-dns
strategy: strategy:
type: Recreate type: Recreate
selector:
matchLabels:
app: external-dns
template: template:
metadata: metadata:
labels: 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 nginx.yaml
$ kubectl delete service -f externaldns.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.)

View File

@ -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. 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: A simple deploy could look like this:
### Manifest (for clusters without RBAC enabled) ### Manifest (for clusters without RBAC enabled)
@ -17,7 +17,7 @@ A simple deploy could look like this:
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: exeternal-dns name: external-dns
spec: spec:
strategy: strategy:
type: Recreate type: Recreate
@ -81,7 +81,7 @@ subjects:
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: exeternal-dns name: external-dns
spec: spec:
strategy: strategy:
type: Recreate type: Recreate
@ -111,7 +111,7 @@ spec:
### Kafka Stateful Set ### 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 ```yaml
apiVersion: apps/v1beta1 apiVersion: apps/v1beta1
@ -155,7 +155,7 @@ spec:
requests: requests:
storage: 500Gi 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 ### Headless Service

View File

@ -6,7 +6,7 @@ It is meant to supplement the other provider-specific setup tutorials.
* Manifest (for clusters without RBAC enabled) * Manifest (for clusters without RBAC enabled)
* Manifest (for clusters with RBAC enabled) * Manifest (for clusters with RBAC enabled)
* Update existing Existing DNS Deployment * Update existing ExternalDNS Deployment
### Manifest (for clusters without RBAC enabled) ### Manifest (for clusters without RBAC enabled)
@ -48,7 +48,7 @@ kind: ServiceAccount
metadata: metadata:
name: external-dns name: external-dns
--- ---
apiVersion: rbac.authorization.k8s.io/v1beta1 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole
metadata: metadata:
name: external-dns name: external-dns
@ -66,7 +66,7 @@ rules:
resources: ["gateways"] resources: ["gateways"]
verbs: ["get","watch","list"] verbs: ["get","watch","list"]
--- ---
apiVersion: rbac.authorization.k8s.io/v1beta1 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
metadata: metadata:
name: external-dns-viewer name: external-dns-viewer
@ -110,7 +110,7 @@ spec:
- --txt-owner-id=my-identifier - --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. * 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. * With access to the `kube-system` namespace, update the existing `external-dns` deployment.
@ -217,7 +217,7 @@ transfer-encoding: chunked
**Note:** The `-H` flag in the original Istio tutorial is no longer necessary in the `curl` commands. **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 * 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 ```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.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="Desired change: CREATE httpbin.example.com 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="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" 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"
``` ```

View File

@ -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 { func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint {
e.SetIdentifier = setIdentifier e.SetIdentifier = setIdentifier
return e return e

57
go.mod
View File

@ -3,46 +3,46 @@ module sigs.k8s.io/external-dns
go 1.14 go 1.14
require ( 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/azure-sdk-for-go v36.0.0+incompatible
github.com/Azure/go-autorest/autorest v0.9.0 github.com/Azure/go-autorest/autorest v0.9.4
github.com/Azure/go-autorest/autorest/adal v0.6.0 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/azure/auth v0.0.0-00010101000000-000000000000
github.com/Azure/go-autorest/autorest/to v0.3.0 github.com/Azure/go-autorest/autorest/to v0.3.0
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11 github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
github.com/alecthomas/colour v0.1.0 // indirect github.com/alecthomas/colour v0.1.0 // indirect
github.com/alecthomas/kingpin v2.2.5+incompatible 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/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/cloudflare/cloudflare-go v0.10.1
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 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/digitalocean/godo v1.34.0
github.com/dnaeon/go-vcr v1.0.1 // indirect github.com/dnsimple/dnsimple-go v0.60.0
github.com/dnsimple/dnsimple-go v0.14.0
github.com/exoscale/egoscale v0.18.1 github.com/exoscale/egoscale v0.18.1
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 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/gobs/pretty v0.0.0-20180724170744-09732c25a95b // indirect
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f
github.com/gophercloud/gophercloud v0.1.0 github.com/gophercloud/gophercloud v0.1.0
github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/heptio/contour v0.15.0 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/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65
github.com/linki/instrumented_http v0.2.0 github.com/linki/instrumented_http v0.2.0
github.com/linode/linodego v0.3.0 github.com/linode/linodego v0.15.0
github.com/mattn/go-isatty v0.0.11 // indirect github.com/maxatome/go-testdeep v1.4.0
github.com/miekg/dns v1.1.25 github.com/miekg/dns v1.1.25
github.com/nesv/go-dynect v0.6.0 github.com/nesv/go-dynect v0.6.0
github.com/nic-at/rc0go v1.1.0 github.com/nic-at/rc0go v1.1.0
github.com/openshift/api v0.0.0-20190322043348-8741ff068a47 github.com/openshift/api v0.0.0-20200302134843-001335d6cc34
github.com/openshift/client-go v3.9.0+incompatible github.com/openshift/client-go v0.0.0-20200116145930-eb24d03d8420
github.com/oracle/oci-go-sdk v1.8.0 github.com/oracle/oci-go-sdk v1.8.0
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014 github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014
github.com/pkg/errors v0.9.1 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/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0
github.com/sergi/go-diff v1.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect
github.com/sirupsen/logrus v1.6.0 github.com/sirupsen/logrus v1.6.0
@ -50,22 +50,27 @@ require (
github.com/smartystreets/gunit v1.1.1 // indirect github.com/smartystreets/gunit v1.1.1 // indirect
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/terra-farm/udnssdk v1.3.5 // indirect 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/transip/gotransip v5.8.2+incompatible
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92 github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92
github.com/vultr/govultr v0.3.2 github.com/vultr/govultr v0.3.2
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875 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 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/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
gopkg.in/yaml.v2 v2.2.5 gopkg.in/yaml.v2 v2.2.8
istio.io/api v0.0.0-20190820204432-483f2547d882 istio.io/api v0.0.0-20200324230725-4b064f75ad8f
istio.io/istio v0.0.0-20190322063008-2b1331886076 istio.io/client-go v0.0.0-20200324231043-96a582576da1
k8s.io/api v0.0.0-20190620084959-7cf5895f2711 k8s.io/api v0.17.5
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719 k8s.io/apimachinery v0.17.5
k8s.io/client-go v10.0.0+incompatible k8s.io/client-go v0.17.5
k8s.io/klog v0.3.1 k8s.io/klog v1.0.0
) )
replace ( 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/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/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 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 k8s.io/klog => github.com/mikkeloscar/knolog v0.0.0-20190326191552-80742771eb6b
) )

680
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
package config
// FastPoll used for fast testing
var FastPoll = false

View File

@ -39,7 +39,6 @@ func (b byAllFields) Less(i, j int) bool {
return b[i].RecordType <= b[j].RecordType return b[i].RecordType <= b[j].RecordType
} }
return b[i].Targets.String() <= b[j].Targets.String() return b[i].Targets.String() <= b[j].Targets.String()
} }
return false return false
} }

View File

@ -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)
}
}
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package testutils package testutils
import ( import (
"context"
"time" "time"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
@ -40,21 +41,18 @@ func (m *MockSource) Endpoints() ([]*endpoint.Endpoint, error) {
return endpoints.([]*endpoint.Endpoint), args.Error(1) 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. // AddEventHandler adds an event handler that should be triggered if something in source changes
func (m *MockSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { func (m *MockSource) AddEventHandler(ctx context.Context, handler func()) {
// Execute callback handler no more than once per minInterval, until a message on stopChan is received.
go func() { go func() {
var lastCallbackTime time.Time ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for { for {
select { select {
case <-stopChan: case <-ctx.Done():
return return
default: case <-ticker.C:
now := time.Now()
if now.After(lastCallbackTime.Add(minInterval)) {
handler() handler()
lastCallbackTime = time.Now()
}
} }
} }
}() }()

132
main.go
View File

@ -28,6 +28,32 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "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/controller"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
@ -63,12 +89,10 @@ func main() {
} }
log.SetLevel(ll) log.SetLevel(ll)
ctx := context.Background() ctx, cancel := context.WithCancel(context.Background())
stopChan := make(chan struct{}, 1)
go serveMetrics(cfg.MetricsAddress) go serveMetrics(cfg.MetricsAddress)
go handleSigterm(stopChan) go handleSigterm(cancel)
// Create a source.Config from the flags passed by the user. // Create a source.Config from the flags passed by the user.
sourceCfg := &source.Config{ sourceCfg := &source.Config{
@ -123,8 +147,8 @@ func main() {
var p provider.Provider var p provider.Provider
switch cfg.Provider { switch cfg.Provider {
case "akamai": case "akamai":
p = provider.NewAkamaiProvider( p = akamai.NewAkamaiProvider(
provider.AkamaiConfig{ akamai.AkamaiConfig{
DomainFilter: domainFilter, DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter, ZoneIDFilter: zoneIDFilter,
ServiceConsumerDomain: cfg.AkamaiServiceConsumerDomain, ServiceConsumerDomain: cfg.AkamaiServiceConsumerDomain,
@ -135,10 +159,10 @@ func main() {
}, },
) )
case "alibabacloud": 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": case "aws":
p, err = provider.NewAWSProvider( p, err = aws.NewAWSProvider(
provider.AWSConfig{ aws.AWSConfig{
DomainFilter: domainFilter, DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter, ZoneIDFilter: zoneIDFilter,
ZoneTypeFilter: zoneTypeFilter, 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) log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
cfg.Registry = "aws-sd" 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": 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": 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": case "vinyldns":
p, err = provider.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun) p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "vultr": case "vultr":
p, err = provider.NewVultrProvider(domainFilter, cfg.DryRun) p, err = provider.NewVultrProvider(domainFilter, cfg.DryRun)
case "ultradns": case "ultradns":
p, err = provider.NewUltraDNSProvider(domainFilter, cfg.DryRun ) p, err = provider.NewUltraDNSProvider(domainFilter, cfg.DryRun )
case "cloudflare": 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": case "rcodezero":
p, err = provider.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt) p, err = rcode0.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt)
case "google": 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": case "digitalocean":
p, err = provider.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun) p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize)
case "ovh": case "ovh":
p, err = provider.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.DryRun) p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.DryRun)
case "linode": case "linode":
p, err = provider.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version) p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
case "dnsimple": case "dnsimple":
p, err = provider.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun) p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "infoblox": case "infoblox":
p, err = provider.NewInfobloxProvider( p, err = infoblox.NewInfobloxProvider(
provider.InfobloxConfig{ infoblox.InfobloxConfig{
DomainFilter: domainFilter, DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter, ZoneIDFilter: zoneIDFilter,
Host: cfg.InfobloxGridHost, Host: cfg.InfobloxGridHost,
@ -202,8 +224,8 @@ func main() {
}, },
) )
case "dyn": case "dyn":
p, err = provider.NewDynProvider( p, err = dyn.NewDynProvider(
provider.DynConfig{ dyn.DynConfig{
DomainFilter: domainFilter, DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter, ZoneIDFilter: zoneIDFilter,
DryRun: cfg.DryRun, DryRun: cfg.DryRun,
@ -215,29 +237,29 @@ func main() {
}, },
) )
case "coredns", "skydns": case "coredns", "skydns":
p, err = provider.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun) p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
case "rdns": case "rdns":
p, err = provider.NewRDNSProvider( p, err = rdns.NewRDNSProvider(
provider.RDNSConfig{ rdns.RDNSConfig{
DomainFilter: domainFilter, DomainFilter: domainFilter,
DryRun: cfg.DryRun, DryRun: cfg.DryRun,
}, },
) )
case "exoscale": 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": 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": case "designate":
p, err = provider.NewDesignateProvider(domainFilter, cfg.DryRun) p, err = designate.NewDesignateProvider(domainFilter, cfg.DryRun)
case "pdns": case "pdns":
p, err = provider.NewPDNSProvider( p, err = pdns.NewPDNSProvider(
ctx, ctx,
provider.PDNSConfig{ pdns.PDNSConfig{
DomainFilter: domainFilter, DomainFilter: domainFilter,
DryRun: cfg.DryRun, DryRun: cfg.DryRun,
Server: cfg.PDNSServer, Server: cfg.PDNSServer,
APIKey: cfg.PDNSAPIKey, APIKey: cfg.PDNSAPIKey,
TLSConfig: provider.TLSConfig{ TLSConfig: pdns.TLSConfig{
TLSEnabled: cfg.PDNSTLSEnabled, TLSEnabled: cfg.PDNSTLSEnabled,
CAFilePath: cfg.TLSCA, CAFilePath: cfg.TLSCA,
ClientCertFilePath: cfg.TLSClientCert, ClientCertFilePath: cfg.TLSClientCert,
@ -246,16 +268,16 @@ func main() {
}, },
) )
case "oci": case "oci":
var config *provider.OCIConfig var config *oci.OCIConfig
config, err = provider.LoadOCIConfig(cfg.OCIConfigFile) config, err = oci.LoadOCIConfig(cfg.OCIConfigFile)
if err == nil { if err == nil {
p, err = provider.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun) p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
} }
case "rfc2136": 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": case "ns1":
p, err = provider.NewNS1Provider( p, err = ns1.NewNS1Provider(
provider.NS1Config{ ns1.NS1Config{
DomainFilter: domainFilter, DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter, ZoneIDFilter: zoneIDFilter,
NS1Endpoint: cfg.NS1Endpoint, NS1Endpoint: cfg.NS1Endpoint,
@ -264,7 +286,7 @@ func main() {
}, },
) )
case "transip": 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: default:
log.Fatalf("unknown dns provider: %s", cfg.Provider) log.Fatalf("unknown dns provider: %s", cfg.Provider)
} }
@ -277,9 +299,9 @@ func main() {
case "noop": case "noop":
r, err = registry.NewNoopRegistry(p) r, err = registry.NewNoopRegistry(p)
case "txt": 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": case "aws-sd":
r, err = registry.NewAWSSDRegistry(p.(*provider.AWSSDProvider), cfg.TXTOwnerID) r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID)
default: default:
log.Fatalf("unknown registry: %s", cfg.Registry) log.Fatalf("unknown registry: %s", cfg.Registry)
} }
@ -301,13 +323,6 @@ func main() {
DomainFilter: domainFilter, 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 { if cfg.Once {
err := ctrl.RunOnce(ctx) err := ctrl.RunOnce(ctx)
if err != nil { if err != nil {
@ -316,15 +331,24 @@ func main() {
os.Exit(0) 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()) })
} }
func handleSigterm(stopChan chan struct{}) { ctrl.ScheduleRunOnce(time.Now())
ctrl.Run(ctx)
}
func handleSigterm(cancel func()) {
signals := make(chan os.Signal, 1) signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGTERM) signal.Notify(signals, syscall.SIGTERM)
<-signals <-signals
log.Info("Received SIGTERM. Terminating...") log.Info("Received SIGTERM. Terminating...")
close(stopChan) cancel()
} }
func serveMetrics(address string) { func serveMetrics(address string) {

View File

@ -109,6 +109,7 @@ type Config struct {
Registry string Registry string
TXTOwnerID string TXTOwnerID string
TXTPrefix string TXTPrefix string
TXTSuffix string
Interval time.Duration Interval time.Duration
Once bool Once bool
DryRun bool DryRun bool
@ -139,6 +140,7 @@ type Config struct {
NS1IgnoreSSL bool NS1IgnoreSSL bool
TransIPAccountName string TransIPAccountName string
TransIPPrivateKeyFile string TransIPPrivateKeyFile string
DigitalOceanAPIPageSize int
} }
var defaultConfig = &Config{ var defaultConfig = &Config{
@ -205,6 +207,7 @@ var defaultConfig = &Config{
Registry: "txt", Registry: "txt",
TXTOwnerID: "default", TXTOwnerID: "default",
TXTPrefix: "", TXTPrefix: "",
TXTSuffix: "",
TXTCacheInterval: 0, TXTCacheInterval: 0,
Interval: time.Minute, Interval: time.Minute,
Once: false, Once: false,
@ -235,6 +238,7 @@ var defaultConfig = &Config{
NS1IgnoreSSL: false, NS1IgnoreSSL: false,
TransIPAccountName: "", TransIPAccountName: "",
TransIPPrivateKeyFile: "", TransIPPrivateKeyFile: "",
DigitalOceanAPIPageSize: 50,
} }
// NewConfig returns new Config object // 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("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-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("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 // 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) 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 // 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("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-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 // 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) app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)

View File

@ -98,6 +98,7 @@ var (
RcodezeroTXTEncrypt: false, RcodezeroTXTEncrypt: false,
TransIPAccountName: "", TransIPAccountName: "",
TransIPPrivateKeyFile: "", TransIPPrivateKeyFile: "",
DigitalOceanAPIPageSize: 50,
} }
overriddenConfig = &Config{ overriddenConfig = &Config{
@ -177,6 +178,7 @@ var (
NS1IgnoreSSL: true, NS1IgnoreSSL: true,
TransIPAccountName: "transip", TransIPAccountName: "transip",
TransIPPrivateKeyFile: "/path/to/transip.key", TransIPPrivateKeyFile: "/path/to/transip.key",
DigitalOceanAPIPageSize: 100,
} }
) )
@ -280,6 +282,7 @@ func TestParseFlags(t *testing.T) {
"--ns1-ignoressl", "--ns1-ignoressl",
"--transip-account=transip", "--transip-account=transip",
"--transip-keyfile=/path/to/transip.key", "--transip-keyfile=/path/to/transip.key",
"--digitalocean-api-page-size=100",
}, },
envVars: map[string]string{}, envVars: map[string]string{},
expected: overriddenConfig, expected: overriddenConfig,
@ -364,6 +367,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_NS1_IGNORESSL": "1", "EXTERNAL_DNS_NS1_IGNORESSL": "1",
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip", "EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key", "EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
"EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100",
}, },
expected: overriddenConfig, expected: overriddenConfig,
}, },

View File

@ -91,5 +91,10 @@ func ValidateConfig(cfg *externaldns.Config) error {
if cfg.IgnoreHostnameAnnotation && cfg.FQDNTemplate == "" { if cfg.IgnoreHostnameAnnotation && cfg.FQDNTemplate == "" {
return errors.New("FQDN Template must be set if ignoring annotations") 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 return nil
} }

View File

@ -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 <run> 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)
}
}

View File

@ -18,11 +18,15 @@ package plan
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"sigs.k8s.io/external-dns/endpoint" "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, // Plan can convert a list of desired and current records to a series of create,
// update and delete actions. // update and delete actions.
type Plan struct { type Plan struct {
@ -37,6 +41,8 @@ type Plan struct {
Changes *Changes Changes *Changes
// DomainFilter matches DNS names // DomainFilter matches DNS names
DomainFilter endpoint.DomainFilter DomainFilter endpoint.DomainFilter
// Property comparator compares custom properties of providers
PropertyComparator PropertyComparator
} }
// Changes holds lists of actions to be executed by dns providers // 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 if row.current != nil && len(row.candidates) > 0 { //dns name is taken
update := t.resolver.ResolveUpdate(row.current, row.candidates) update := t.resolver.ResolveUpdate(row.current, row.candidates)
// compare "update" to "current" to figure out if actual update is required // 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) inheritOwner(row.current, update)
changes.UpdateNew = append(changes.UpdateNew, update) changes.UpdateNew = append(changes.UpdateNew, update)
changes.UpdateOld = append(changes.UpdateOld, row.current) changes.UpdateOld = append(changes.UpdateOld, row.current)
@ -178,10 +184,15 @@ func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool {
return desired.RecordTTL != current.RecordTTL return desired.RecordTTL != current.RecordTTL
} }
func shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool { func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool {
if current.ProviderSpecific == nil && len(desired.ProviderSpecific) == 0 { desiredProperties := map[string]endpoint.ProviderSpecificProperty{}
return false
if desired.ProviderSpecific != nil {
for _, d := range desired.ProviderSpecific {
desiredProperties[d.Name] = d
} }
}
if current.ProviderSpecific != nil {
for _, c := range current.ProviderSpecific { for _, c := range current.ProviderSpecific {
// don't consider target health when detecting changes // don't consider target health when detecting changes
// see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954 // see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954
@ -189,33 +200,23 @@ func shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool {
continue continue
} }
found := false if d, ok := desiredProperties[c.Name]; ok {
for _, d := range desired.ProviderSpecific { if p.PropertyComparator != nil {
if d.Name == c.Name { if !p.PropertyComparator(c.Name, c.Value, d.Value) {
if d.Value != c.Value {
// provider-specific attribute updated
return true return true
} }
found = true } else if c.Value != d.Value {
break return true
} }
} else {
if p.PropertyComparator != nil {
if !p.PropertyComparator(c.Name, c.Value, "") {
return true
} }
if !found { } else if c.Value != "" {
// provider-specific attribute deleted
return true 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
} }
} }
@ -260,3 +261,28 @@ func normalizeDNSName(dnsName string) string {
} }
return s 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
}

View File

@ -38,6 +38,7 @@ type PlanTestSuite struct {
bar127AWithTTL *endpoint.Endpoint bar127AWithTTL *endpoint.Endpoint
bar127AWithProviderSpecificTrue *endpoint.Endpoint bar127AWithProviderSpecificTrue *endpoint.Endpoint
bar127AWithProviderSpecificFalse *endpoint.Endpoint bar127AWithProviderSpecificFalse *endpoint.Endpoint
bar127AWithProviderSpecificUnset *endpoint.Endpoint
bar192A *endpoint.Endpoint bar192A *endpoint.Endpoint
multiple1 *endpoint.Endpoint multiple1 *endpoint.Endpoint
multiple2 *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{ suite.bar192A = &endpoint.Endpoint{
DNSName: "bar", DNSName: "bar",
Targets: endpoint.Targets{"192.168.0.1"}, Targets: endpoint.Targets{"192.168.0.1"},
@ -291,6 +301,54 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() {
validateEntries(suite.T(), changes.Delete, expectedDelete) 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() { func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
current := []*endpoint.Endpoint{suite.fooV1Cname} current := []*endpoint.Endpoint{suite.fooV1Cname}
desired := []*endpoint.Endpoint{suite.fooV2Cname} desired := []*endpoint.Endpoint{suite.fooV2Cname}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package akamai
import ( import (
"bytes" "bytes"
@ -30,6 +30,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type akamaiClient interface { type akamaiClient interface {
@ -50,7 +51,7 @@ func (*akamaiOpenClient) Do(config edgegrid.Config, req *http.Request) (*http.Re
// AkamaiConfig clarifies the method signature // AkamaiConfig clarifies the method signature
type AkamaiConfig struct { type AkamaiConfig struct {
DomainFilter endpoint.DomainFilter DomainFilter endpoint.DomainFilter
ZoneIDFilter ZoneIDFilter ZoneIDFilter provider.ZoneIDFilter
ServiceConsumerDomain string ServiceConsumerDomain string
ClientToken string ClientToken string
ClientSecret string ClientSecret string
@ -60,8 +61,9 @@ type AkamaiConfig struct {
// AkamaiProvider implements the DNS provider for Akamai. // AkamaiProvider implements the DNS provider for Akamai.
type AkamaiProvider struct { type AkamaiProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
config edgegrid.Config config edgegrid.Config
dryRun bool dryRun bool
client akamaiClient 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. // ApplyChanges applies a given set of changes in a given zone.
func (p *AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { func (p *AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
zones, err := p.fetchZones() zones, err := p.fetchZones()
if err != nil { if err != nil {
log.Warnf("No zones to fetch endpoints from!") 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 { for _, endpoint := range endpoints {
if !p.domainFilter.Match(endpoint.DNSName) { 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) log.Debugf("Skipping creation at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
continue continue
@ -320,9 +321,8 @@ func (p *AkamaiProvider) createRecords(zoneNameIDMapper zoneIDName, endpoints []
return created, failed 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 { for _, endpoint := range endpoints {
if !p.domainFilter.Match(endpoint.DNSName) { 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) log.Debugf("Skipping deletion at Akamai of endpoint: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
continue continue
@ -349,9 +349,8 @@ func (p *AkamaiProvider) deleteRecords(zoneNameIDMapper zoneIDName, endpoints []
return deleted, failed 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 { for _, endpoint := range endpoints {
if !p.domainFilter.Match(endpoint.DNSName) { 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) log.Debugf("Skipping update at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
continue continue

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package akamai
import ( import (
"bytes" "bytes"
@ -32,6 +32,7 @@ import (
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type mockAkamaiClient struct { type mockAkamaiClient struct {
@ -92,7 +93,7 @@ func TestRequestError(t *testing.T) {
func TestFetchZonesZoneIDFilter(t *testing.T) { func TestFetchZonesZoneIDFilter(t *testing.T) {
config := AkamaiConfig{ config := AkamaiConfig{
ZoneIDFilter: NewZoneIDFilter([]string{"Test"}), ZoneIDFilter: provider.NewZoneIDFilter([]string{"Test"}),
} }
client := &mockAkamaiClient{} client := &mockAkamaiClient{}
@ -109,7 +110,7 @@ func TestFetchZonesZoneIDFilter(t *testing.T) {
func TestFetchZonesEmpty(t *testing.T) { func TestFetchZonesEmpty(t *testing.T) {
config := AkamaiConfig{ config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"Nonexistent"}), DomainFilter: endpoint.NewDomainFilter([]string{"Nonexistent"}),
ZoneIDFilter: NewZoneIDFilter([]string{"Nonexistent"}), ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
} }
client := &mockAkamaiClient{} client := &mockAkamaiClient{}
@ -171,7 +172,7 @@ func TestAkamaiRecords(t *testing.T) {
func TestAkamaiRecordsEmpty(t *testing.T) { func TestAkamaiRecordsEmpty(t *testing.T) {
config := AkamaiConfig{ config := AkamaiConfig{
ZoneIDFilter: NewZoneIDFilter([]string{"Nonexistent"}), ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
} }
client := &mockAkamaiClient{} client := &mockAkamaiClient{}
@ -185,7 +186,7 @@ func TestAkamaiRecordsEmpty(t *testing.T) {
func TestAkamaiRecordsFilters(t *testing.T) { func TestAkamaiRecordsFilters(t *testing.T) {
config := AkamaiConfig{ config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"www.exclude.me"}), DomainFilter: endpoint.NewDomainFilter([]string{"www.exclude.me"}),
ZoneIDFilter: NewZoneIDFilter([]string{"Exclude-Me"}), ZoneIDFilter: provider.NewZoneIDFilter([]string{"Exclude-Me"}),
} }
client := &mockAkamaiClient{} client := &mockAkamaiClient{}
@ -208,7 +209,7 @@ func TestCreateRecords(t *testing.T) {
c := NewAkamaiProvider(config) c := NewAkamaiProvider(config)
c.client = client c.client = client
zoneNameIDMapper := zoneIDName{"example.com": "example.com"} zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0) 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.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")) 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 := NewAkamaiProvider(config)
c.client = client c.client = client
zoneNameIDMapper := zoneIDName{"example.com": "example.com"} zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0) 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.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")) 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 := NewAkamaiProvider(config)
c.client = client c.client = client
zoneNameIDMapper := zoneIDName{"example.com": "example.com"} zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0) 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.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")) 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 := NewAkamaiProvider(config)
c.client = client c.client = client
zoneNameIDMapper := zoneIDName{"example.com": "example.com"} zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0) 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.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")) 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 := NewAkamaiProvider(config)
c.client = client c.client = client
zoneNameIDMapper := zoneIDName{"example.com": "example.com"} zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0) 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.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")) 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 := NewAkamaiProvider(config)
c.client = client c.client = client
zoneNameIDMapper := zoneIDName{"example.com": "example.com"} zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0) 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.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")) endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package alibabacloud
import ( import (
"context" "context"
@ -33,6 +33,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -66,8 +67,9 @@ type AlibabaCloudPrivateZoneAPI interface {
// AlibabaCloudProvider implements the DNS provider for Alibaba Cloud. // AlibabaCloudProvider implements the DNS provider for Alibaba Cloud.
type AlibabaCloudProvider struct { type AlibabaCloudProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter // Private Zone only zoneIDFilter provider.ZoneIDFilter // Private Zone only
MaxChangeCount int MaxChangeCount int
EvaluateTargetHealth bool EvaluateTargetHealth bool
AssumeRole string AssumeRole string
@ -93,22 +95,22 @@ type alibabaCloudConfig struct {
// NewAlibabaCloudProvider creates a new Alibaba Cloud provider. // NewAlibabaCloudProvider creates a new Alibaba Cloud provider.
// //
// Returns the provider or an error if a provider could not be created. // 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{} cfg := alibabaCloudConfig{}
if configFile != "" { if configFile != "" {
contents, err := ioutil.ReadFile(configFile) contents, err := ioutil.ReadFile(configFile)
if err != nil { 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) err = yaml.Unmarshal(contents, &cfg)
if err != nil { 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 { } else {
var tmpError error var tmpError error
cfg, tmpError = getCloudConfigFromStsToken() cfg, tmpError = getCloudConfigFromStsToken()
if tmpError != nil { 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 := "" roleName := ""
var err error var err error
if roleName, err = m.RoleName(); err != nil { 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() vpcID, err := m.VpcID()
if err != nil { 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() regionID, err := m.Region()
if err != nil { 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) role, err := m.RamRoleToken(roleName)
if err != nil { 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.RegionID = regionID
cfg.RoleName = roleName 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. // Returns the current records or an error if the operation failed.
func (p *AlibabaCloudProvider) recordsForDNS() (endpoints []*endpoint.Endpoint, _ error) { func (p *AlibabaCloudProvider) recordsForDNS() (endpoints []*endpoint.Endpoint, _ error) {
records, err := p.records() records, err := p.records()
if err != nil { if err != nil {
return nil, err return nil, err
@ -344,7 +345,6 @@ func (p *AlibabaCloudProvider) recordsForDNS() (endpoints []*endpoint.Endpoint,
} }
func getNextPageNumber(pageNumber, pageSize, totalCount int) int { func getNextPageNumber(pageNumber, pageSize, totalCount int) int {
if pageNumber*pageSize >= totalCount { if pageNumber*pageSize >= totalCount {
return 0 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) { func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) (endpointMap map[string][]alidns.Record) {
endpointMap = make(map[string][]alidns.Record) endpointMap = make(map[string][]alidns.Record)
for _, record := range records { for _, record := range records {
key := p.getRecordKey(record) key := p.getRecordKey(record)
recordList := endpointMap[key] recordList := endpointMap[key]
endpointMap[key] = append(recordList, record) endpointMap[key] = append(recordList, record)
} }
return endpointMap return endpointMap
} }
@ -449,18 +444,15 @@ func (p *AlibabaCloudProvider) getDomainRecords(domainName string) ([]alidns.Rec
} }
for _, record := range response.DomainRecords.Record { for _, record := range response.DomainRecords.Record {
domainName := record.DomainName domainName := record.DomainName
recordType := record.Type recordType := record.Type
if !p.domainFilter.Match(domainName) { if !p.domainFilter.Match(domainName) {
continue continue
} }
if !provider.SupportedRecordType(recordType) {
if !supportedRecordType(recordType) {
continue continue
} }
//TODO filter Locked record //TODO filter Locked record
results = append(results, 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 { func (p *AlibabaCloudProvider) updateRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint) error {
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
key := p.getRecordKeyByEndpoint(endpoint) key := p.getRecordKeyByEndpoint(endpoint)
records := recordMap[key] 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) { func (p *AlibabaCloudProvider) splitDNSName(endpoint *endpoint.Endpoint) (rr string, domain string) {
name := strings.TrimSuffix(endpoint.DNSName, ".") name := strings.TrimSuffix(endpoint.DNSName, ".")
found := false found := false
@ -727,7 +717,6 @@ func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool {
} }
func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) { func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) {
var zones []pvtz.Zone var zones []pvtz.Zone
request := pvtz.CreateDescribeZonesRequest() request := pvtz.CreateDescribeZonesRequest()
@ -782,7 +771,6 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone
} }
for _, zone := range zones { for _, zone := range zones {
request := pvtz.CreateDescribeZoneRecordsRequest() request := pvtz.CreateDescribeZoneRecordsRequest()
request.ZoneId = zone.ZoneId request.ZoneId = zone.ZoneId
request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
@ -799,10 +787,9 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone
} }
for _, record := range response.Records.Record { for _, record := range response.Records.Record {
recordType := record.Type recordType := record.Type
if !supportedRecordType(recordType) { if !provider.SupportedRecordType(recordType) {
continue continue
} }
@ -829,7 +816,6 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone
} }
func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) (endpointMap map[string][]pvtz.Record) { func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) (endpointMap map[string][]pvtz.Record) {
endpointMap = make(map[string][]pvtz.Record) endpointMap = make(map[string][]pvtz.Record)
for _, record := range zone.records { 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. // Returns the current records or an error if the operation failed.
func (p *AlibabaCloudProvider) privateZoneRecords() (endpoints []*endpoint.Endpoint, _ error) { func (p *AlibabaCloudProvider) privateZoneRecords() (endpoints []*endpoint.Endpoint, _ error) {
zones, err := p.getPrivateZones() zones, err := p.getPrivateZones()
if err != nil { if err != nil {
return nil, err return nil, err
@ -879,7 +864,7 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibaba
rr, domain := p.splitDNSName(endpoint) rr, domain := p.splitDNSName(endpoint)
zone := zones[domain] zone := zones[domain]
if zone == nil { 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) 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 return err
} }
@ -925,7 +910,6 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecords(zones map[string]*alibab
} }
func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int) error { func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int) error {
if p.dryRun { if p.dryRun {
log.Infof("Dry run: Delete record id '%d' in Alibaba Cloud Private Zone", recordID) 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] zone := zones[domain]
if zone == nil { 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) log.Errorf("Failed to delete %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err)
continue 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 { func (p *AlibabaCloudProvider) updatePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error {
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
rr, domain := p.splitDNSName(endpoint) rr, domain := p.splitDNSName(endpoint)
zone := zones[domain] zone := zones[domain]
if zone == nil { 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) log.Errorf("Failed to update %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err)
continue continue
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package alibabacloud
import ( import (
"context" "context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package aws
import ( import (
"context" "context"
@ -34,6 +34,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -51,8 +52,7 @@ const (
) )
var ( var (
// see: https://docs.aws.amazon.com/general/latest/gr/rande.html#elb_region // see: https://docs.aws.amazon.com/general/latest/gr/elb.html
// and: https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/using-govcloud-endpoints.html
canonicalHostedZones = map[string]string{ canonicalHostedZones = map[string]string{
// Application Load Balancers and Classic Load Balancers // Application Load Balancers and Classic Load Balancers
"us-east-2.elb.amazonaws.com": "Z3AADJGX6KTTL2", "us-east-2.elb.amazonaws.com": "Z3AADJGX6KTTL2",
@ -73,9 +73,10 @@ var (
"eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4", "eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4",
"eu-north-1.elb.amazonaws.com": "Z23TAZ6LKFMNIO", "eu-north-1.elb.amazonaws.com": "Z23TAZ6LKFMNIO",
"sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU", "sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU",
"cn-north-1.elb.amazonaws.com.cn": "Z3BX2TMKNYI13Y", "cn-north-1.elb.amazonaws.com.cn": "Z1GDH35T77C1KE",
"cn-northwest-1.elb.amazonaws.com.cn": "Z3BX2TMKNYI13Y", "cn-northwest-1.elb.amazonaws.com.cn": "ZM7IZAIOVVDZF",
"us-gov-west-1.amazonaws.com": "Z1K6XKP9SAGWDV", "us-gov-west-1.elb.amazonaws.com": "Z33AYJ8TM3BH4J",
"us-gov-east-1.elb.amazonaws.com": "Z166TLBEWOO7G0",
"me-south-1.elb.amazonaws.com": "ZS929ML54UICD", "me-south-1.elb.amazonaws.com": "ZS929ML54UICD",
// Network Load Balancers // Network Load Balancers
"elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP", "elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP",
@ -97,6 +98,8 @@ var (
"elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU", "elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU",
"elb.cn-north-1.amazonaws.com.cn": "Z3QFB96KMJ7ED6", "elb.cn-north-1.amazonaws.com.cn": "Z3QFB96KMJ7ED6",
"elb.cn-northwest-1.amazonaws.com.cn": "ZQEIKTCZ8352D", "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", "elb.me-south-1.amazonaws.com": "Z3QSRYVP46NYYV",
} }
) )
@ -113,6 +116,7 @@ type Route53API interface {
// AWSProvider is an implementation of Provider for AWS Route53. // AWSProvider is an implementation of Provider for AWS Route53.
type AWSProvider struct { type AWSProvider struct {
provider.BaseProvider
client Route53API client Route53API
dryRun bool dryRun bool
batchChangeSize int batchChangeSize int
@ -121,20 +125,20 @@ type AWSProvider struct {
// only consider hosted zones managing domains ending in this suffix // only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
// filter hosted zones by id // filter hosted zones by id
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
// filter hosted zones by type (e.g. private or public) // filter hosted zones by type (e.g. private or public)
zoneTypeFilter ZoneTypeFilter zoneTypeFilter provider.ZoneTypeFilter
// filter hosted zones by tags // filter hosted zones by tags
zoneTagFilter ZoneTagFilter zoneTagFilter provider.ZoneTagFilter
preferCNAME bool preferCNAME bool
} }
// AWSConfig contains configuration to create a new AWS provider. // AWSConfig contains configuration to create a new AWS provider.
type AWSConfig struct { type AWSConfig struct {
DomainFilter endpoint.DomainFilter DomainFilter endpoint.DomainFilter
ZoneIDFilter ZoneIDFilter ZoneIDFilter provider.ZoneIDFilter
ZoneTypeFilter ZoneTypeFilter ZoneTypeFilter provider.ZoneTypeFilter
ZoneTagFilter ZoneTagFilter ZoneTagFilter provider.ZoneTagFilter
BatchChangeSize int BatchChangeSize int
BatchChangeInterval time.Duration BatchChangeInterval time.Duration
EvaluateTargetHealth bool 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. // TODO(linki, ownership): Remove once ownership system is in place.
// See: https://github.com/kubernetes-sigs/external-dns/pull/122/files/74e2c3d3e237411e619aefc5aab694742001cdec#r109863370 // 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 continue
} }
@ -377,7 +381,7 @@ func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) e
return err return err
} }
records, ok := ctx.Value(RecordsContextKey).([]*endpoint.Endpoint) records, ok := ctx.Value(provider.RecordsContextKey).([]*endpoint.Endpoint)
if !ok { if !ok {
var err error var err error
records, err = p.records(ctx, zones) records, err = p.records(ctx, zones)
@ -449,7 +453,7 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes []*route53.Chan
} }
if len(failedZones) > 0 { 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 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 { func batchChangeSet(cs []*route53.Change, batchSize int) [][]*route53.Change {
if len(cs) <= batchSize { if len(cs) <= batchSize {
return [][]*route53.Change{cs} res := sortChangesByActionNameType(cs)
return [][]*route53.Change{res}
} }
batchChanges := make([][]*route53.Change, 0) 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 { func sortChangesByActionNameType(cs []*route53.Change) []*route53.Change {
sort.SliceStable(cs, func(i, j int) bool { sort.SliceStable(cs, func(i, j int) bool {
if *cs[i].Action < *cs[j].Action { if *cs[i].Action > *cs[j].Action {
return true return true
} }
if *cs[i].Action > *cs[j].Action { if *cs[i].Action < *cs[j].Action {
return false return false
} }
if *cs[i].ResourceRecordSet.Name < *cs[j].ResourceRecordSet.Name { 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 { for _, c := range changeSet {
hostname := ensureTrailingDot(aws.StringValue(c.ResourceRecordSet.Name)) hostname := provider.EnsureTrailingDot(aws.StringValue(c.ResourceRecordSet.Name))
zones := suitableZones(hostname, zones) zones := suitableZones(hostname, zones)
if len(zones) == 0 { if len(zones) == 0 {
@ -733,7 +738,6 @@ func isAWSAlias(ep *endpoint.Endpoint, addrs []*endpoint.Endpoint) string {
if hostedZone := canonicalHostedZone(addr.Targets[0]); hostedZone != "" { if hostedZone := canonicalHostedZone(addr.Targets[0]); hostedZone != "" {
return hostedZone return hostedZone
} }
} }
} }
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package aws
import ( import (
"context" "context"
@ -35,6 +35,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( 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 { 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 { 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) { switch aws.StringValue(change.Action) {
case route53.ChangeActionCreate: case route53.ChangeActionCreate:
if _, found := recordSets[key]; found { if _, found := recordSets[key]; found {
@ -287,17 +288,17 @@ func TestAWSZones(t *testing.T) {
for _, ti := range []struct { for _, ti := range []struct {
msg string msg string
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
zoneTypeFilter ZoneTypeFilter zoneTypeFilter provider.ZoneTypeFilter
zoneTagFilter ZoneTagFilter zoneTagFilter provider.ZoneTagFilter
expectedZones map[string]*route53.HostedZone expectedZones map[string]*route53.HostedZone
}{ }{
{"no filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), allZones}, {"no filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), allZones},
{"public filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("public"), NewZoneTagFilter([]string{}), publicZones}, {"public filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("public"), provider.NewZoneTagFilter([]string{}), publicZones},
{"private filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("private"), NewZoneTagFilter([]string{}), privateZones}, {"private filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("private"), provider.NewZoneTagFilter([]string{}), privateZones},
{"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), NewZoneTagFilter([]string{}), noZones}, {"unknown filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("unknown"), provider.NewZoneTagFilter([]string{}), noZones},
{"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), privateZones}, {"zone id filter", provider.NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), privateZones},
{"tag filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{"zone=3"}), 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{}) 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) { 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-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("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"), 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) { func TestAWSCreateRecords(t *testing.T) {
customTTL := endpoint.TTL(60) 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{ records := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 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) { 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-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.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"), 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"), 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)) require.NoError(t, provider.DeleteRecords(context.Background(), originalEndpoints))
@ -441,12 +442,12 @@ func TestAWSApplyChanges(t *testing.T) {
ctx := context.Background() ctx := context.Background()
records, err := p.Records(ctx) records, err := p.Records(ctx)
require.NoError(t, err) require.NoError(t, err)
return context.WithValue(ctx, RecordsContextKey, records) return context.WithValue(ctx, provider.RecordsContextKey, records)
}, 0}, }, 0},
} }
for _, tt := range tests { 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("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("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"), 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"), 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{ createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 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) { 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 subnets = 16
const hosts = defaultBatchChangeSize / subnets const hosts = defaultBatchChangeSize / subnets
@ -715,7 +716,7 @@ func TestAWSsubmitChanges(t *testing.T) {
} }
func TestAWSsubmitChangesError(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")) clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure"))
ctx := context.Background() ctx := context.Background()
@ -851,7 +852,7 @@ func validateAWSChangeRecord(t *testing.T, record *route53.Change, expected *rou
} }
func TestAWSCreateRecordsWithCNAME(t *testing.T) { 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{ records := []*endpoint.Endpoint{
{DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.example.org"}, RecordType: endpoint.RecordTypeCNAME}, {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": 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 // Test dualstack and ipv4 load balancer targets
records := []*endpoint.Endpoint{ records := []*endpoint.Endpoint{
@ -1023,8 +1024,8 @@ func TestAWSCanonicalHostedZone(t *testing.T) {
{"foo.eu-west-2.elb.amazonaws.com", "ZHURV8PSTC4K8"}, {"foo.eu-west-2.elb.amazonaws.com", "ZHURV8PSTC4K8"},
{"foo.eu-west-3.elb.amazonaws.com", "Z3Q77PNBQS71R4"}, {"foo.eu-west-3.elb.amazonaws.com", "Z3Q77PNBQS71R4"},
{"foo.sa-east-1.elb.amazonaws.com", "Z2P70J7HTTTPLU"}, {"foo.sa-east-1.elb.amazonaws.com", "Z2P70J7HTTTPLU"},
{"foo.cn-north-1.elb.amazonaws.com.cn", "Z3BX2TMKNYI13Y"}, {"foo.cn-north-1.elb.amazonaws.com.cn", "Z1GDH35T77C1KE"},
{"foo.cn-northwest-1.elb.amazonaws.com.cn", "Z3BX2TMKNYI13Y"}, {"foo.cn-northwest-1.elb.amazonaws.com.cn", "ZM7IZAIOVVDZF"},
// Network Load Balancers // Network Load Balancers
{"foo.elb.us-east-2.amazonaws.com", "ZLMOA37VPKANP"}, {"foo.elb.us-east-2.amazonaws.com", "ZLMOA37VPKANP"},
{"foo.elb.us-east-1.amazonaws.com", "Z26RNL4JYFTOTI"}, {"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) require.NoError(t, err)
} }
} }
func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { 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, NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records) 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() client := NewRoute53APIStub()
provider := &AWSProvider{ provider := &AWSProvider{

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package awssd
import ( import (
"context" "context"
@ -37,6 +37,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/apis/externaldns" "sigs.k8s.io/external-dns/pkg/apis/externaldns"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -73,6 +74,7 @@ type AWSSDClient interface {
// AWSSDProvider is an implementation of Provider for AWS Cloud Map. // AWSSDProvider is an implementation of Provider for AWS Cloud Map.
type AWSSDProvider struct { type AWSSDProvider struct {
provider.BaseProvider
client AWSSDClient client AWSSDClient
dryRun bool dryRun bool
// only consider namespaces ending in this suffix // only consider namespaces ending in this suffix

View File

@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package awssd
import ( import (
"context" "context"
"errors" "errors"
"math/rand" "math/rand"
"reflect" "reflect"
"strconv"
"testing" "testing"
"time" "time"
@ -51,7 +52,7 @@ type AWSSDClientStub struct {
func (s *AWSSDClientStub) CreateService(input *sd.CreateServiceInput) (*sd.CreateServiceOutput, error) { func (s *AWSSDClientStub) CreateService(input *sd.CreateServiceInput) (*sd.CreateServiceOutput, error) {
srv := &sd.Service{ srv := &sd.Service{
Id: aws.String(string(rand.Intn(10000))), Id: aws.String(strconv.Itoa(rand.Intn(10000))),
DnsConfig: input.DnsConfig, DnsConfig: input.DnsConfig,
Name: input.Name, Name: input.Name,
Description: input.Description, Description: input.Description,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package azure
import ( import (
"context" "context"
@ -34,6 +34,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -66,8 +67,9 @@ type RecordSetsClient interface {
// AzureProvider implements the DNS provider for Microsoft's Azure cloud platform. // AzureProvider implements the DNS provider for Microsoft's Azure cloud platform.
type AzureProvider struct { type AzureProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
dryRun bool dryRun bool
resourceGroup string resourceGroup string
userAssignedIdentityClientID string userAssignedIdentityClientID string
@ -78,7 +80,7 @@ type AzureProvider struct {
// NewAzureProvider creates a new Azure provider. // NewAzureProvider creates a new Azure provider.
// //
// Returns the provider or an error if a provider could not be created. // 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) contents, err := ioutil.ReadFile(configFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) 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 return true
} }
recordType := strings.TrimPrefix(*recordSet.Type, "Microsoft.Network/dnszones/") recordType := strings.TrimPrefix(*recordSet.Type, "Microsoft.Network/dnszones/")
if !supportedRecordType(recordType) { if !provider.SupportedRecordType(recordType) {
return true return true
} }
name := formatAzureDNSName(*recordSet.Name, *zone.Name) 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{} ignored := map[string]bool{}
deleted := azureChangeMap{} deleted := azureChangeMap{}
updated := azureChangeMap{} updated := azureChangeMap{}
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
if z.Name != nil { if z.Name != nil {
zoneNameIDMapper.Add(*z.Name, *z.Name) zoneNameIDMapper.Add(*z.Name, *z.Name)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package azure
import ( import (
"context" "context"
@ -29,6 +29,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "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. // 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 // AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service
type AzurePrivateDNSProvider struct { type AzurePrivateDNSProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
dryRun bool dryRun bool
subscriptionID string subscriptionID string
resourceGroup string resourceGroup string
@ -57,7 +59,7 @@ type AzurePrivateDNSProvider struct {
// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider. // NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
// //
// Returns the provider or an error if a provider could not be created. // 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() authorizer, err := auth.NewAuthorizerFromEnvironment()
if err != nil { if err != nil {
return nil, err return nil, err
@ -208,7 +210,7 @@ func (p *AzurePrivateDNSProvider) mapChanges(zones []privatedns.PrivateZone, cha
ignored := map[string]bool{} ignored := map[string]bool{}
deleted := azurePrivateDNSChangeMap{} deleted := azurePrivateDNSChangeMap{}
updated := azurePrivateDNSChangeMap{} updated := azurePrivateDNSChangeMap{}
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
if z.Name != nil { if z.Name != nil {
zoneNameIDMapper.Add(*z.Name, *z.Name) zoneNameIDMapper.Add(*z.Name, *z.Name)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package azure
import ( import (
"context" "context"
@ -27,6 +27,11 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "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 // 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 // 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 // init zone-related parts of the mock-client
pageIterator := mockPrivateZoneListResultPageIterator{ pageIterator := mockPrivateZoneListResultPageIterator{
results: []privatedns.PrivateZoneListResult{ results: []privatedns.PrivateZoneListResult{
@ -236,7 +241,7 @@ func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneID
return newAzurePrivateDNSProvider(domainFilter, zoneIDFilter, dryRun, resourceGroup, &zonesClient, &recordSetsClient), nil 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{ return &AzurePrivateDNSProvider{
domainFilter: domainFilter, domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter, zoneIDFilter: zoneIDFilter,
@ -248,7 +253,7 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter
} }
func TestAzurePrivateDNSRecord(t *testing.T) { 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{ &[]privatedns.PrivateZone{
createMockPrivateZone("example.com", "/privateDnsZones/example.com"), createMockPrivateZone("example.com", "/privateDnsZones/example.com"),
}, },
@ -284,7 +289,7 @@ func TestAzurePrivateDNSRecord(t *testing.T) {
} }
func TestAzurePrivateDNSMultiRecord(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{ &[]privatedns.PrivateZone{
createMockPrivateZone("example.com", "/privateDnsZones/example.com"), createMockPrivateZone("example.com", "/privateDnsZones/example.com"),
}, },
@ -383,7 +388,7 @@ func testAzurePrivateDNSApplyChangesInternal(t *testing.T, dryRun bool, client P
provider := newAzurePrivateDNSProvider( provider := newAzurePrivateDNSProvider(
endpoint.NewDomainFilter([]string{""}), endpoint.NewDomainFilter([]string{""}),
NewZoneIDFilter([]string{""}), provider.NewZoneIDFilter([]string{""}),
dryRun, dryRun,
"group", "group",
&zonesClient, &zonesClient,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package azure
import ( import (
"context" "context"
@ -30,6 +30,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "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 // 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 // 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 // init zone-related parts of the mock-client
pageIterator := mockZoneListResultPageIterator{ pageIterator := mockZoneListResultPageIterator{
results: []dns.ZoneListResult{ results: []dns.ZoneListResult{
@ -239,7 +240,7 @@ func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter Zon
return newAzureProvider(domainFilter, zoneIDFilter, dryRun, resourceGroup, userAssignedIdentityClientID, &zonesClient, &recordSetsClient), nil 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{ return &AzureProvider{
domainFilter: domainFilter, domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter, zoneIDFilter: zoneIDFilter,
@ -256,7 +257,7 @@ func validateAzureEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expect
} }
func TestAzureRecord(t *testing.T) { 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{ &[]dns.Zone{
createMockZone("example.com", "/dnszones/example.com"), createMockZone("example.com", "/dnszones/example.com"),
}, },
@ -293,7 +294,7 @@ func TestAzureRecord(t *testing.T) {
} }
func TestAzureMultiRecord(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{ &[]dns.Zone{
createMockZone("example.com", "/dnszones/example.com"), createMockZone("example.com", "/dnszones/example.com"),
}, },
@ -393,7 +394,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC
provider := newAzureProvider( provider := newAzureProvider(
endpoint.NewDomainFilter([]string{""}), endpoint.NewDomainFilter([]string{""}),
NewZoneIDFilter([]string{""}), provider.NewZoneIDFilter([]string{""}),
dryRun, dryRun,
"group", "group",
"", "",

View File

@ -0,0 +1,2 @@
approvers:
- sheerun

View File

@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package cloudflare
import ( import (
"context" "context"
"fmt" "fmt"
"os" "os"
"sort"
"strconv" "strconv"
"strings" "strings"
@ -29,6 +28,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/source" "sigs.k8s.io/external-dns/source"
) )
@ -58,6 +58,7 @@ type cloudFlareDNS interface {
ZoneIDByName(zoneName string) (string, error) ZoneIDByName(zoneName string) (string, error)
ListZones(zoneID ...string) ([]cloudflare.Zone, error) ListZones(zoneID ...string) ([]cloudflare.Zone, error)
ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, 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) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error)
CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error)
DeleteDNSRecord(zoneID, recordID string) 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...) 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. // CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
type CloudFlareProvider struct { type CloudFlareProvider struct {
provider.BaseProvider
Client cloudFlareDNS Client cloudFlareDNS
// only consider hosted zones managing domains ending in this suffix // only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
proxiedByDefault bool proxiedByDefault bool
DryRun bool DryRun bool
PaginationOptions cloudflare.PaginationOptions PaginationOptions cloudflare.PaginationOptions
@ -112,11 +118,11 @@ type CloudFlareProvider struct {
// cloudFlareChange differentiates between ChangActions // cloudFlareChange differentiates between ChangActions
type cloudFlareChange struct { type cloudFlareChange struct {
Action string Action string
ResourceRecordSet []cloudflare.DNSRecord ResourceRecord cloudflare.DNSRecord
} }
// NewCloudFlareProvider initializes a new CloudFlare DNS based Provider. // 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 // initialize via chosen auth method and returns new API object
var ( var (
config *cloudflare.API config *cloudflare.API
@ -150,6 +156,27 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
result := []cloudflare.Zone{} result := []cloudflare.Zone{}
p.PaginationOptions.Page = 1 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 { for {
zonesResponse, err := p.Client.ListZonesContext(ctx, cloudflare.WithPagination(p.PaginationOptions)) zonesResponse, err := p.Client.ListZonesContext(ctx, cloudflare.WithPagination(p.PaginationOptions))
if err != nil { if err != nil {
@ -158,10 +185,7 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
for _, zone := range zonesResponse.Result { for _, zone := range zonesResponse.Result {
if !p.domainFilter.Match(zone.Name) { if !p.domainFilter.Match(zone.Name) {
continue log.Debugf("zone %s not in domain filter", zone.Name)
}
if !p.zoneIDFilter.Match(zone.ID) {
continue continue
} }
result = append(result, zone) 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. // ApplyChanges applies a given set of changes in a given zone.
func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 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)...) for i, desired := range changes.UpdateNew {
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareUpdate, changes.UpdateNew, proxiedByDefault)...) current := changes.UpdateOld[i]
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareDelete, changes.Delete, proxiedByDefault)...)
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. // submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
@ -231,10 +287,9 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
} }
for _, change := range changes { for _, change := range changes {
logFields := log.Fields{ logFields := log.Fields{
"record": change.ResourceRecordSet[0].Name, "record": change.ResourceRecord.Name,
"type": change.ResourceRecordSet[0].Type, "type": change.ResourceRecord.Type,
"ttl": change.ResourceRecordSet[0].TTL, "ttl": change.ResourceRecord.TTL,
"targets": len(change.ResourceRecordSet),
"action": change.Action, "action": change.Action,
"zone": zoneID, "zone": zoneID,
} }
@ -245,35 +300,41 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
continue continue
} }
recordIDs := p.getRecordIDs(records, change.ResourceRecordSet[0]) if change.Action == cloudFlareUpdate {
recordID := p.getRecordID(records, change.ResourceRecord)
// to simplify bookkeeping for multiple records, an update is executed as delete+create if recordID == "" {
if change.Action == cloudFlareDelete || change.Action == cloudFlareUpdate { log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord)
for _, recordID := range recordIDs { continue
}
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) err := p.Client.DeleteDNSRecord(zoneID, recordID)
if err != nil { if err != nil {
log.WithFields(logFields).Errorf("failed to delete record: %v", err) log.WithFields(logFields).Errorf("failed to delete record: %v", err)
} }
} } else if change.Action == cloudFlareCreate {
} _, err := p.Client.CreateDNSRecord(zoneID, change.ResourceRecord)
if change.Action == cloudFlareCreate || change.Action == cloudFlareUpdate {
for _, record := range change.ResourceRecordSet {
_, err := p.Client.CreateDNSRecord(zoneID, record)
if err != nil { if err != nil {
log.WithFields(logFields).Errorf("failed to create record: %v", err) log.WithFields(logFields).Errorf("failed to create record: %v", err)
} }
} }
} }
} }
}
return nil return nil
} }
// changesByZone separates a multi-zone change into a single change per zone. // changesByZone separates a multi-zone change into a single change per zone.
func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange { func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
changes := make(map[string][]*cloudFlareChange) changes := make(map[string][]*cloudFlareChange)
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
zoneNameIDMapper.Add(z.ID, z.Name) zoneNameIDMapper.Add(z.ID, z.Name)
@ -281,9 +342,9 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []
} }
for _, c := range changeSet { for _, c := range changeSet {
zoneID, _ := zoneNameIDMapper.FindZone(c.ResourceRecordSet[0].Name) zoneID, _ := zoneNameIDMapper.FindZone(c.ResourceRecord.Name)
if zoneID == "" { 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 continue
} }
changes[zoneID] = append(changes[zoneID], c) changes[zoneID] = append(changes[zoneID], c)
@ -292,51 +353,36 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []
return changes return changes
} }
func (p *CloudFlareProvider) getRecordIDs(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) []string { func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) string {
recordIDs := make([]string, 0)
for _, zoneRecord := range records { for _, zoneRecord := range records {
if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type { if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type && zoneRecord.Content == record.Content {
recordIDs = append(recordIDs, zoneRecord.ID) return zoneRecord.ID
} }
} }
sort.Strings(recordIDs) return ""
return recordIDs
} }
// newCloudFlareChanges returns a collection of Changes based on the given records and action. func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoint.Endpoint, target string) *cloudFlareChange {
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 {
ttl := defaultCloudFlareRecordTTL ttl := defaultCloudFlareRecordTTL
proxied := shouldBeProxied(endpoint, proxiedByDefault) proxied := shouldBeProxied(endpoint, p.proxiedByDefault)
if endpoint.RecordTTL.IsConfigured() { if endpoint.RecordTTL.IsConfigured() {
ttl = int(endpoint.RecordTTL) 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{
Name: endpoint.DNSName,
TTL: ttl,
Proxied: proxied,
Type: endpoint.RecordType,
Content: endpoint.Targets[i],
}
} }
return &cloudFlareChange{ return &cloudFlareChange{
Action: action, Action: action,
ResourceRecordSet: resourceRecordSet, ResourceRecord: cloudflare.DNSRecord{
Name: endpoint.DNSName,
TTL: ttl,
Proxied: proxied,
Type: endpoint.RecordType,
Content: target,
},
} }
} }
@ -368,7 +414,7 @@ func groupByNameAndType(records []cloudflare.DNSRecord) []*endpoint.Endpoint {
groups := map[string][]cloudflare.DNSRecord{} groups := map[string][]cloudflare.DNSRecord{}
for _, r := range records { for _, r := range records {
if !supportedRecordType(r.Type) { if !provider.SupportedRecordType(r.Type) {
continue continue
} }

File diff suppressed because it is too large Load Diff

View File

@ -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)
}
}

2
provider/coredns/OWNERS Normal file
View File

@ -0,0 +1,2 @@
approvers:
- ytsarev

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package coredns
import ( import (
"context" "context"
@ -35,6 +35,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
func init() { func init() {
@ -56,6 +57,7 @@ type coreDNSClient interface {
} }
type coreDNSProvider struct { type coreDNSProvider struct {
provider.BaseProvider
dryRun bool dryRun bool
coreDNSPrefix string coreDNSPrefix string
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
@ -84,7 +86,7 @@ type Service struct {
// answer. // answer.
Group string `json:"group,omitempty"` 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:"-"` Key string `json:"-"`
} }
@ -244,7 +246,7 @@ func newETCDClient() (coreDNSClient, error) {
} }
// NewCoreDNSProvider is a CoreDNS provider constructor // 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() client, err := newETCDClient()
if err != nil { if err != nil {
return nil, err return nil, err
@ -395,7 +397,6 @@ func (p coreDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
} }
} }
} }
} }
index := 0 index := 0
for _, ep := range group { for _, ep := range group {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package coredns
import ( import (
"context" "context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package designate
import ( import (
"context" "context"
@ -35,6 +35,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/tlsutils" "sigs.k8s.io/external-dns/pkg/tlsutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -226,6 +227,7 @@ func (c designateClient) DeleteRecordSet(zoneID, recordSetID string) error {
// designate provider type // designate provider type
type designateProvider struct { type designateProvider struct {
provider.BaseProvider
client designateClientInterface client designateClientInterface
// only consider hosted zones managing domains ending in this suffix // 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 // 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() client, err := newDesignateClient()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package designate
import ( import (
"context" "context"
@ -34,6 +34,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
var lastGeneratedDesignateID int32 var lastGeneratedDesignateID int32
@ -130,7 +131,7 @@ func (c fakeDesignateClient) DeleteRecordSet(zoneID, recordSetID string) error {
return nil return nil
} }
func (c fakeDesignateClient) ToProvider() Provider { func (c fakeDesignateClient) ToProvider() provider.Provider {
return &designateProvider{client: c} return &designateProvider{client: c}
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package digitalocean
import ( import (
"context" "context"
@ -28,6 +28,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -44,9 +45,12 @@ const (
// DigitalOceanProvider is an implementation of Provider for Digital Ocean's DNS. // DigitalOceanProvider is an implementation of Provider for Digital Ocean's DNS.
type DigitalOceanProvider struct { type DigitalOceanProvider struct {
provider.BaseProvider
Client godo.DomainsService Client godo.DomainsService
// only consider hosted zones managing domains ending in this suffix // only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
// page size when querying paginated APIs
apiPageSize int
DryRun bool DryRun bool
} }
@ -57,10 +61,10 @@ type DigitalOceanChange struct {
} }
// NewDigitalOceanProvider initializes a new DigitalOcean DNS based Provider. // 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") token, ok := os.LookupEnv("DO_TOKEN")
if !ok { if !ok {
return nil, fmt.Errorf("No token found") return nil, fmt.Errorf("no token found")
} }
oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: token, AccessToken: token,
@ -70,6 +74,7 @@ func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFi
provider := &DigitalOceanProvider{ provider := &DigitalOceanProvider{
Client: client.Domains, Client: client.Domains,
domainFilter: domainFilter, domainFilter: domainFilter,
apiPageSize: apiPageSize,
DryRun: dryRun, DryRun: dryRun,
} }
return provider, nil return provider, nil
@ -107,7 +112,7 @@ func (p *DigitalOceanProvider) Records(ctx context.Context) ([]*endpoint.Endpoin
} }
for _, r := range records { for _, r := range records {
if supportedRecordType(r.Type) { if provider.SupportedRecordType(r.Type) {
name := r.Name + "." + zone.Name name := r.Name + "." + zone.Name
// root name is identified by @ and should be // 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) { func (p *DigitalOceanProvider) fetchRecords(ctx context.Context, zoneName string) ([]godo.DomainRecord, error) {
allRecords := []godo.DomainRecord{} allRecords := []godo.DomainRecord{}
listOptions := &godo.ListOptions{} listOptions := &godo.ListOptions{PerPage: p.apiPageSize}
for { for {
records, resp, err := p.Client.Records(ctx, zoneName, listOptions) records, resp, err := p.Client.Records(ctx, zoneName, listOptions)
if err != nil { 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) { func (p *DigitalOceanProvider) fetchZones(ctx context.Context) ([]godo.Domain, error) {
allZones := []godo.Domain{} allZones := []godo.Domain{}
listOptions := &godo.ListOptions{} listOptions := &godo.ListOptions{PerPage: p.apiPageSize}
for { for {
zones, resp, err := p.Client.List(ctx, listOptions) zones, resp, err := p.Client.List(ctx, listOptions)
if err != nil { 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. // digitalOceanchangesByZone separates a multi-zone change into a single change per zone.
func digitalOceanChangesByZone(zones []godo.Domain, changeSet []*DigitalOceanChange) map[string][]*DigitalOceanChange { func digitalOceanChangesByZone(zones []godo.Domain, changeSet []*DigitalOceanChange) map[string][]*DigitalOceanChange {
changes := make(map[string][]*DigitalOceanChange) changes := make(map[string][]*DigitalOceanChange)
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
zoneNameIDMapper.Add(z.Name, z.Name) zoneNameIDMapper.Add(z.Name, z.Name)
changes[z.Name] = []*DigitalOceanChange{} changes[z.Name] = []*DigitalOceanChange{}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package digitalocean
import ( import (
"context" "context"
@ -187,12 +187,12 @@ func TestDigitalOceanApplyChanges(t *testing.T) {
func TestNewDigitalOceanProvider(t *testing.T) { func TestNewDigitalOceanProvider(t *testing.T) {
_ = os.Setenv("DO_TOKEN", "xxxxxxxxxxxxxxxxx") _ = 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 { if err != nil {
t.Errorf("should not fail, %s", err) t.Errorf("should not fail, %s", err)
} }
_ = os.Unsetenv("DO_TOKEN") _ = 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 { if err == nil {
t.Errorf("expected to fail") t.Errorf("expected to fail")
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package dnsimple
import ( import (
"context" "context"
@ -25,70 +25,64 @@ import (
"github.com/dnsimple/dnsimple-go/dnsimple" "github.com/dnsimple/dnsimple-go/dnsimple"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"sigs.k8s.io/external-dns/endpoint" "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/plan"
"sigs.k8s.io/external-dns/provider"
) )
const dnsimpleRecordTTL = 3600 // Default TTL of 1 hour if not set (DNSimple's default) const dnsimpleRecordTTL = 3600 // Default TTL of 1 hour if not set (DNSimple's default)
type identityService struct { type dnsimpleIdentityService struct {
service *dnsimple.IdentityService service *dnsimple.IdentityService
} }
func (i identityService) Whoami() (*dnsimple.WhoamiResponse, error) { func (i dnsimpleIdentityService) Whoami(ctx context.Context) (*dnsimple.WhoamiResponse, error) {
return i.service.Whoami() return i.service.Whoami(ctx)
} }
// Returns the account ID given dnsimple credentials // dnsimpleZoneServiceInterface is an interface that contains all necessary zone services from DNSimple
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
type dnsimpleZoneServiceInterface interface { type dnsimpleZoneServiceInterface interface {
ListZones(accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error)
ListRecords(accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error)
CreateRecord(accountID string, zoneID string, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error)
DeleteRecord(accountID string, zoneID string, recordID int) (*dnsimple.ZoneRecordResponse, error) DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error)
UpdateRecord(accountID string, zoneID string, recordID int, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error)
} }
type dnsimpleZoneService struct { type dnsimpleZoneService struct {
service *dnsimple.ZonesService service *dnsimple.ZonesService
} }
func (z dnsimpleZoneService) ListZones(accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) { func (z dnsimpleZoneService) ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) {
return z.service.ListZones(accountID, options) return z.service.ListZones(ctx, accountID, options)
} }
func (z dnsimpleZoneService) ListRecords(accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) { func (z dnsimpleZoneService) ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) {
return z.service.ListRecords(accountID, zoneID, options) return z.service.ListRecords(ctx, accountID, zoneID, options)
} }
func (z dnsimpleZoneService) CreateRecord(accountID string, zoneID string, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) { func (z dnsimpleZoneService) CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) {
return z.service.CreateRecord(accountID, zoneID, recordAttributes) return z.service.CreateRecord(ctx, accountID, zoneID, recordAttributes)
} }
func (z dnsimpleZoneService) DeleteRecord(accountID string, zoneID string, recordID int) (*dnsimple.ZoneRecordResponse, error) { func (z dnsimpleZoneService) DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error) {
return z.service.DeleteRecord(accountID, zoneID, recordID) return z.service.DeleteRecord(ctx, accountID, zoneID, recordID)
} }
func (z dnsimpleZoneService) UpdateRecord(accountID string, zoneID string, recordID int, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) { func (z dnsimpleZoneService) UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) {
return z.service.UpdateRecord(accountID, zoneID, recordID, recordAttributes) return z.service.UpdateRecord(ctx, accountID, zoneID, recordID, recordAttributes)
} }
type dnsimpleProvider struct { type dnsimpleProvider struct {
provider.BaseProvider
client dnsimpleZoneServiceInterface client dnsimpleZoneServiceInterface
identity identityService identity dnsimpleIdentityService
accountID string accountID string
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
dryRun bool dryRun bool
} }
@ -104,35 +98,52 @@ const (
) )
// NewDnsimpleProvider initializes a new Dnsimple based provider // 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") oauthToken := os.Getenv("DNSIMPLE_OAUTH")
if len(oauthToken) == 0 { 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{ provider := &dnsimpleProvider{
client: dnsimpleZoneService{service: client.Zones}, client: dnsimpleZoneService{service: client.Zones},
identity: identityService{service: client.Identity}, identity: dnsimpleIdentityService{service: client.Identity},
domainFilter: domainFilter, domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter, zoneIDFilter: zoneIDFilter,
dryRun: dryRun, dryRun: dryRun,
} }
whoamiResponse, err := provider.identity.service.Whoami()
whoamiResponse, err := provider.identity.Whoami(context.Background())
if err != nil { if err != nil {
return nil, err return nil, err
} }
provider.accountID = strconv.Itoa(whoamiResponse.Data.Account.ID) provider.accountID = int64ToString(whoamiResponse.Data.Account.ID)
return provider, nil 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 // 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) zones := make(map[string]dnsimple.Zone)
page := 1 page := 1
listOptions := &dnsimple.ZoneListOptions{} listOptions := &dnsimple.ZoneListOptions{}
for { for {
listOptions.Page = page listOptions.Page = &page
zonesResponse, err := p.client.ListZones(p.accountID, listOptions) zonesResponse, err := p.client.ListZones(ctx, p.accountID, listOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -141,11 +152,11 @@ func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) {
continue continue
} }
if !p.zoneIDFilter.Match(strconv.Itoa(zone.ID)) { if !p.zoneIDFilter.Match(int64ToString(zone.ID)) {
continue continue
} }
zones[strconv.Itoa(zone.ID)] = zone zones[int64ToString(zone.ID)] = zone
} }
page++ page++
@ -158,7 +169,7 @@ func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) {
// Records returns a list of endpoints in a given zone // Records returns a list of endpoints in a given zone
func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
zones, err := p.Zones() zones, err := p.Zones(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -166,8 +177,8 @@ func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.E
page := 1 page := 1
listOptions := &dnsimple.ZoneRecordListOptions{} listOptions := &dnsimple.ZoneRecordListOptions{}
for { for {
listOptions.Page = page listOptions.Page = &page
records, err := p.client.ListRecords(p.accountID, zone.Name, listOptions) records, err := p.client.ListRecords(ctx, p.accountID, zone.Name, listOptions)
if err != nil { if err != nil {
return nil, err 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 // 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 { if len(changes) == 0 {
log.Infof("All records are already up to date") log.Infof("All records are already up to date")
return nil return nil
} }
zones, err := p.Zones() zones, err := p.Zones(ctx)
if err != nil { if err != nil {
return err 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)) 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 { if !p.dryRun {
switch change.Action { switch change.Action {
case dnsimpleCreate: 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 { if err != nil {
return err return err
} }
case dnsimpleDelete: case dnsimpleDelete:
recordID, err := p.GetRecordID(zone.Name, change.ResourceRecordSet.Name) recordID, err := p.GetRecordID(ctx, zone.Name, *recordAttributes.Name)
if err != nil { if err != nil {
return err return err
} }
_, err = p.client.DeleteRecord(p.accountID, zone.Name, recordID) _, err = p.client.DeleteRecord(ctx, p.accountID, zone.Name, recordID)
if err != nil { if err != nil {
return err return err
} }
case dnsimpleUpdate: case dnsimpleUpdate:
recordID, err := p.GetRecordID(zone.Name, change.ResourceRecordSet.Name) recordID, err := p.GetRecordID(ctx, zone.Name, *recordAttributes.Name)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -279,13 +297,13 @@ func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error {
return nil return nil
} }
// Returns the record ID for a given record name and zone // GetRecordID returns the record ID for a given record name and zone.
func (p *dnsimpleProvider) GetRecordID(zone string, recordName string) (recordID int, err error) { func (p *dnsimpleProvider) GetRecordID(ctx context.Context, zone string, recordName string) (recordID int64, err error) {
page := 1 page := 1
listOptions := &dnsimple.ZoneRecordListOptions{Name: recordName} listOptions := &dnsimple.ZoneRecordListOptions{Name: &recordName}
for { for {
listOptions.Page = page listOptions.Page = &page
records, err := p.client.ListRecords(p.accountID, zone, listOptions) records, err := p.client.ListRecords(ctx, p.accountID, zone, listOptions)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -301,7 +319,7 @@ func (p *dnsimpleProvider) GetRecordID(zone string, recordName string) (recordID
break 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. // 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 // CreateRecords creates records for a given slice of endpoints
func (p *dnsimpleProvider) CreateRecords(endpoints []*endpoint.Endpoint) error { func (p *dnsimpleProvider) CreateRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
return p.submitChanges(newDnsimpleChanges(dnsimpleCreate, endpoints)) return p.submitChanges(ctx, newDnsimpleChanges(dnsimpleCreate, endpoints))
} }
// DeleteRecords deletes records for a given slice of endpoints // DeleteRecords deletes records for a given slice of endpoints
func (p *dnsimpleProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error { func (p *dnsimpleProvider) DeleteRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
return p.submitChanges(newDnsimpleChanges(dnsimpleDelete, endpoints)) return p.submitChanges(ctx, newDnsimpleChanges(dnsimpleDelete, endpoints))
} }
// UpdateRecords updates records for a given slice of endpoints // UpdateRecords updates records for a given slice of endpoints
func (p *dnsimpleProvider) UpdateRecords(endpoints []*endpoint.Endpoint) error { func (p *dnsimpleProvider) UpdateRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
return p.submitChanges(newDnsimpleChanges(dnsimpleUpdate, endpoints)) return p.submitChanges(ctx, newDnsimpleChanges(dnsimpleUpdate, endpoints))
} }
// ApplyChanges applies a given set of changes // 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(dnsimpleUpdate, changes.UpdateNew)...)
combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleDelete, changes.Delete)...) 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)
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package dnsimple
import ( import (
"context" "context"
@ -22,8 +22,6 @@ import (
"os" "os"
"testing" "testing"
"strconv"
"github.com/dnsimple/dnsimple-go/dnsimple" "github.com/dnsimple/dnsimple-go/dnsimple"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
@ -31,6 +29,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
var mockProvider dnsimpleProvider var mockProvider dnsimpleProvider
@ -102,15 +101,17 @@ func TestDnsimpleServices(t *testing.T) {
} }
// Setup mock services // Setup mock services
// Note: AnythingOfType doesn't work with interfaces https://github.com/stretchr/testify/issues/519
mockDNS := &mockDnsimpleZoneServiceInterface{} mockDNS := &mockDnsimpleZoneServiceInterface{}
mockDNS.On("ListZones", "1", &dnsimple.ZoneListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimpleListZonesResponse, nil) mockDNS.On("ListZones", mock.AnythingOfType("*context.emptyCtx"), "1", &dnsimple.ZoneListOptions{ListOptions: dnsimple.ListOptions{Page: dnsimple.Int(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("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", "1", "example.com", &dnsimple.ZoneRecordListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimpleListRecordsResponse, nil) 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", "1", "example-beta.com", &dnsimple.ZoneRecordListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimple.ZoneRecordsResponse{Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}}}, 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 { for _, record := range records {
simpleRecord := dnsimple.ZoneRecord{ recordName := record.Name
Name: record.Name, simpleRecord := dnsimple.ZoneRecordAttributes{
Name: &recordName,
Type: record.Type, Type: record.Type,
Content: record.Content, Content: record.Content,
TTL: record.TTL, TTL: record.TTL,
@ -121,10 +122,10 @@ func TestDnsimpleServices(t *testing.T) {
Data: []dnsimple.ZoneRecord{record}, 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("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", "1", record.ZoneID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil) mockDNS.On("CreateRecord", mock.AnythingOfType("*context.emptyCtx"), "1", record.ZoneID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil)
mockDNS.On("DeleteRecord", "1", record.ZoneID, record.ID).Return(&dnsimple.ZoneRecordResponse{}, nil) mockDNS.On("DeleteRecord", mock.AnythingOfType("*context.emptyCtx"), "1", record.ZoneID, record.ID).Return(&dnsimple.ZoneRecordResponse{}, nil)
mockDNS.On("UpdateRecord", "1", record.ZoneID, record.ID, simpleRecord).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} mockProvider = dnsimpleProvider{client: mockDNS}
@ -139,13 +140,14 @@ func TestDnsimpleServices(t *testing.T) {
} }
func testDnsimpleProviderZones(t *testing.T) { func testDnsimpleProviderZones(t *testing.T) {
ctx := context.Background()
mockProvider.accountID = "1" mockProvider.accountID = "1"
result, err := mockProvider.Zones() result, err := mockProvider.Zones(ctx)
assert.Nil(t, err) assert.Nil(t, err)
validateDnsimpleZones(t, result, dnsimpleListZonesResponse.Data) validateDnsimpleZones(t, result, dnsimpleListZonesResponse.Data)
mockProvider.accountID = "2" mockProvider.accountID = "2"
_, err = mockProvider.Zones() _, err = mockProvider.Zones(ctx)
assert.NotNil(t, err) assert.NotNil(t, err)
} }
@ -160,13 +162,16 @@ func testDnsimpleProviderRecords(t *testing.T) {
_, err = mockProvider.Records(ctx) _, err = mockProvider.Records(ctx)
assert.NotNil(t, err) assert.NotNil(t, err)
} }
func testDnsimpleProviderApplyChanges(t *testing.T) { func testDnsimpleProviderApplyChanges(t *testing.T) {
changes := &plan.Changes{} changes := &plan.Changes{}
changes.Create = []*endpoint.Endpoint{ changes.Create = []*endpoint.Endpoint{
{DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}, {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}, {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{ changes.UpdateNew = []*endpoint.Endpoint{
{DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}, {DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME},
{DNSName: "example.com", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: endpoint.RecordTypeA}, {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) { func testDnsimpleSuitableZone(t *testing.T) {
ctx := context.Background()
mockProvider.accountID = "1" mockProvider.accountID = "1"
zones, err := mockProvider.Zones() zones, err := mockProvider.Zones(ctx)
assert.Nil(t, err) assert.Nil(t, err)
zone := dnsimpleSuitableZone("example-beta.example.com", zones) zone := dnsimpleSuitableZone("example-beta.example.com", zones)
@ -203,7 +209,7 @@ func testDnsimpleSuitableZone(t *testing.T) {
func TestNewDnsimpleProvider(t *testing.T) { func TestNewDnsimpleProvider(t *testing.T) {
os.Setenv("DNSIMPLE_OAUTH", "xxxxxxxxxxxxxxxxxxxxxxxxxx") 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 { if err == nil {
t.Errorf("Expected to fail new provider on bad token") t.Errorf("Expected to fail new provider on bad token")
} }
@ -214,21 +220,24 @@ func TestNewDnsimpleProvider(t *testing.T) {
} }
func testDnsimpleGetRecordID(t *testing.T) { func testDnsimpleGetRecordID(t *testing.T) {
mockProvider.accountID = "1" var result int64
result, err := mockProvider.GetRecordID("example.com", "example") var err error
assert.Nil(t, err)
assert.Equal(t, 2, result)
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.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) { func validateDnsimpleZones(t *testing.T, zones map[string]dnsimple.Zone, expected []dnsimple.Zone) {
require.Len(t, zones, len(expected)) require.Len(t, zones, len(expected))
for _, e := range 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 mock.Mock
} }
func (_m *mockDnsimpleZoneServiceInterface) CreateRecord(accountID string, zoneID string, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) { func (_m *mockDnsimpleZoneServiceInterface) CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) {
args := _m.Called(accountID, zoneID, recordAttributes) args := _m.Called(ctx, accountID, zoneID, recordAttributes)
var r0 *dnsimple.ZoneRecordResponse var r0 *dnsimple.ZoneRecordResponse
if args.Get(0) != nil { if args.Get(0) != nil {
@ -247,8 +256,8 @@ func (_m *mockDnsimpleZoneServiceInterface) CreateRecord(accountID string, zoneI
return r0, args.Error(1) return r0, args.Error(1)
} }
func (_m *mockDnsimpleZoneServiceInterface) DeleteRecord(accountID string, zoneID string, recordID int) (*dnsimple.ZoneRecordResponse, error) { func (_m *mockDnsimpleZoneServiceInterface) DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error) {
args := _m.Called(accountID, zoneID, recordID) args := _m.Called(ctx, accountID, zoneID, recordID)
var r0 *dnsimple.ZoneRecordResponse var r0 *dnsimple.ZoneRecordResponse
if args.Get(0) != nil { if args.Get(0) != nil {
@ -258,8 +267,8 @@ func (_m *mockDnsimpleZoneServiceInterface) DeleteRecord(accountID string, zoneI
return r0, args.Error(1) return r0, args.Error(1)
} }
func (_m *mockDnsimpleZoneServiceInterface) ListRecords(accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) { func (_m *mockDnsimpleZoneServiceInterface) ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) {
args := _m.Called(accountID, zoneID, options) args := _m.Called(ctx, accountID, zoneID, options)
var r0 *dnsimple.ZoneRecordsResponse var r0 *dnsimple.ZoneRecordsResponse
if args.Get(0) != nil { if args.Get(0) != nil {
@ -269,8 +278,8 @@ func (_m *mockDnsimpleZoneServiceInterface) ListRecords(accountID string, zoneID
return r0, args.Error(1) return r0, args.Error(1)
} }
func (_m *mockDnsimpleZoneServiceInterface) ListZones(accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) { func (_m *mockDnsimpleZoneServiceInterface) ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) {
args := _m.Called(accountID, options) args := _m.Called(ctx, accountID, options)
var r0 *dnsimple.ZonesResponse var r0 *dnsimple.ZonesResponse
if args.Get(0) != nil { if args.Get(0) != nil {
@ -280,8 +289,8 @@ func (_m *mockDnsimpleZoneServiceInterface) ListZones(accountID string, options
return r0, args.Error(1) return r0, args.Error(1)
} }
func (_m *mockDnsimpleZoneServiceInterface) UpdateRecord(accountID string, zoneID string, recordID int, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) { func (_m *mockDnsimpleZoneServiceInterface) UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) {
args := _m.Called(accountID, zoneID, recordID, recordAttributes) args := _m.Called(ctx, accountID, zoneID, recordID, recordAttributes)
var r0 *dnsimple.ZoneRecordResponse var r0 *dnsimple.ZoneRecordResponse
if args.Get(0) != nil { if args.Get(0) != nil {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package dyn
import ( import (
"context" "context"
@ -31,6 +31,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -57,7 +58,7 @@ func unixNow() int64 {
// DynConfig hold connection parameters to dyn.com and internal state // DynConfig hold connection parameters to dyn.com and internal state
type DynConfig struct { type DynConfig struct {
DomainFilter endpoint.DomainFilter DomainFilter endpoint.DomainFilter
ZoneIDFilter ZoneIDFilter ZoneIDFilter provider.ZoneIDFilter
DryRun bool DryRun bool
CustomerName string CustomerName string
Username string Username string
@ -103,6 +104,7 @@ func (snap *ZoneSnapshot) StoreRecordsForSerial(zone string, serial int, records
// DynProvider is the actual interface impl. // DynProvider is the actual interface impl.
type dynProviderState struct { type dynProviderState struct {
provider.BaseProvider
DynConfig DynConfig
LastLoginErrorTime int64 LastLoginErrorTime int64
@ -141,7 +143,7 @@ type ZonePublishResponse struct {
} }
// NewDynProvider initializes a new Dyn Provider. // NewDynProvider initializes a new Dyn Provider.
func NewDynProvider(config DynConfig) (Provider, error) { func NewDynProvider(config DynConfig) (provider.Provider, error) {
return &dynProviderState{ return &dynProviderState{
DynConfig: config, DynConfig: config,
ZoneSnapshot: &ZoneSnapshot{ ZoneSnapshot: &ZoneSnapshot{
@ -156,7 +158,6 @@ func NewDynProvider(config DynConfig) (Provider, error) {
func filterAndFixLinks(links []string, filter endpoint.DomainFilter) []string { func filterAndFixLinks(links []string, filter endpoint.DomainFilter) []string {
var result []string var result []string
for _, link := range links { for _, link := range links {
// link looks like /REST/CNAMERecord/acme.com/exchange.acme.com/349386875 // link looks like /REST/CNAMERecord/acme.com/exchange.acme.com/349386875
// strip /REST/ // strip /REST/
@ -290,7 +291,6 @@ func (d *dynProviderState) allRecordsToEndpoints(records *dynectsoap.GetAllRecor
} }
return result return result
} }
func errorOrValue(err error, value interface{}) interface{} { func errorOrValue(err error, value interface{}) interface{} {
@ -392,7 +392,6 @@ func (d *dynProviderState) fetchAllRecordsInZone(zone string) (*dynectsoap.GetAl
} }
return &records, nil return &records, nil
} }
// buildLinkToRecord build a resource link. The symmetry of the dyn API is used to save // 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 "" return ""
} }
var matchingZone = "" var matchingZone = ""
for _, zone := range d.ZoneIDFilter.zoneIDs { for _, zone := range d.ZoneIDFilter.ZoneIDs {
if strings.HasSuffix(ep.DNSName, zone) { if strings.HasSuffix(ep.DNSName, zone) {
matchingZone = zone matchingZone = zone
break 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 // the zones we are allowed to touch. Currently only exact matches are considered, not all
// zones with the given suffix // zones with the given suffix
func (d *dynProviderState) zones(client *dynect.Client) []string { 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) { func (d *dynProviderState) buildRecordRequest(ep *endpoint.Endpoint) (string, *dynect.RecordRequest) {
@ -568,7 +567,7 @@ func (d *dynProviderState) commit(client *dynect.Client) error {
case 1: case 1:
return errs[0] return errs[0]
default: 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: case 1:
return errs[0] return errs[0]
default: default:
return fmt.Errorf("Multiple errors committing: %+v", errs) return fmt.Errorf("multiple errors committing: %+v", errs)
} }
if needsCommit { if needsCommit {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package dyn
import ( import (
"errors" "errors"
@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/provider"
) )
func TestDynMerge_NoUpdateOnTTL0Changes(t *testing.T) { func TestDynMerge_NoUpdateOnTTL0Changes(t *testing.T) {
@ -187,7 +188,7 @@ func TestDyn_endpointToRecord(t *testing.T) {
func TestDyn_buildLinkToRecord(t *testing.T) { func TestDyn_buildLinkToRecord(t *testing.T) {
provider := &dynProviderState{ provider := &dynProviderState{
DynConfig: DynConfig{ DynConfig: DynConfig{
ZoneIDFilter: NewZoneIDFilter([]string{"example.com"}), ZoneIDFilter: provider.NewZoneIDFilter([]string{"example.com"}),
DomainFilter: endpoint.NewDomainFilter([]string{"the-target.example.com"}), DomainFilter: endpoint.NewDomainFilter([]string{"the-target.example.com"}),
}, },
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package exoscale
import ( import (
"context" "context"
@ -25,6 +25,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
// EgoscaleClientI for replaceable implementation // EgoscaleClientI for replaceable implementation
@ -38,6 +39,7 @@ type EgoscaleClientI interface {
// ExoscaleProvider initialized as dns provider with no records // ExoscaleProvider initialized as dns provider with no records
type ExoscaleProvider struct { type ExoscaleProvider struct {
provider.BaseProvider
domain endpoint.DomainFilter domain endpoint.DomainFilter
client EgoscaleClientI client EgoscaleClientI
filter *zoneFilter filter *zoneFilter
@ -257,3 +259,38 @@ func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[int64
} }
return matchZoneID, name 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
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package exoscale
import ( import (
"context" "context"
@ -189,3 +189,144 @@ func TestExoscaleApplyChanges(t *testing.T) {
assert.Equal(t, "foo.com", updateExoscale[0].name) assert.Equal(t, "foo.com", updateExoscale[0].name)
assert.Equal(t, int64(1), updateExoscale[0].updateDNSRecord.ID) 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))
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package google
import ( import (
"context" "context"
@ -33,6 +33,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( 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. // GoogleProvider is an implementation of Provider for Google CloudDNS.
type GoogleProvider struct { type GoogleProvider struct {
provider.BaseProvider
// The Google project to work in // The Google project to work in
project string project string
// Enabled dry-run will print any modifying actions rather than execute them. // 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 // only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
// only consider hosted zones ending with this zone id // only consider hosted zones ending with this zone id
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
// A client for managing resource record sets // A client for managing resource record sets
resourceRecordSetsClient resourceRecordSetsClientInterface resourceRecordSetsClient resourceRecordSetsClientInterface
// A client for managing hosted zones // A client for managing hosted zones
@ -121,7 +123,7 @@ type GoogleProvider struct {
} }
// NewGoogleProvider initializes a new Google CloudDNS based Provider. // 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) gcloud, err := google.DefaultClient(ctx, dns.NdevClouddnsReadwriteScope)
if err != nil { if err != nil {
return nil, err return nil, err
@ -209,7 +211,7 @@ func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.End
f := func(resp *dns.ResourceRecordSetsListResponse) error { f := func(resp *dns.ResourceRecordSetsListResponse) error {
for _, r := range resp.Rrsets { for _, r := range resp.Rrsets {
if !supportedRecordType(r.Type) { if !provider.SupportedRecordType(r.Type) {
continue continue
} }
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), r.Rrdatas...)) 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. // 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 { func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[string]*dns.Change {
changes := make(map[string]*dns.Change) changes := make(map[string]*dns.Change)
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
zoneNameIDMapper[z.Name] = z.DnsName zoneNameIDMapper[z.Name] = z.DnsName
changes[z.Name] = &dns.Change{ 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 { 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) changes[zoneName].Additions = append(changes[zoneName].Additions, a)
} else { } else {
log.Warnf("No matching zone for record addition: %s %s %s %d", a.Name, a.Type, a.Rrdatas, a.Ttl) 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 { 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) changes[zoneName].Deletions = append(changes[zoneName].Deletions, d)
} else { } else {
log.Warnf("No matching zone for record deletion: %s %s %s %d", d.Name, d.Type, d.Rrdatas, d.Ttl) 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)) targets := make([]string, len(ep.Targets))
copy(targets, []string(ep.Targets)) copy(targets, []string(ep.Targets))
if ep.RecordType == endpoint.RecordTypeCNAME { 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 // 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{ return &dns.ResourceRecordSet{
Name: ensureTrailingDot(ep.DNSName), Name: provider.EnsureTrailingDot(ep.DNSName),
Rrdatas: targets, Rrdatas: targets,
Ttl: ttl, Ttl: ttl,
Type: ep.RecordType, Type: ep.RecordType,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package google
import ( import (
"fmt" "fmt"
@ -30,7 +30,9 @@ import (
"google.golang.org/api/googleapi" "google.golang.org/api/googleapi"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
var ( var (
@ -192,7 +194,7 @@ func hasTrailingDot(target string) bool {
} }
func TestGoogleZonesIDFilter(t *testing.T) { 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()) zones, err := provider.Zones(context.Background())
require.NoError(t, err) require.NoError(t, err)
@ -203,7 +205,7 @@ func TestGoogleZonesIDFilter(t *testing.T) {
} }
func TestGoogleZonesNameFilter(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()) zones, err := provider.Zones(context.Background())
require.NoError(t, err) require.NoError(t, err)
@ -214,7 +216,7 @@ func TestGoogleZonesNameFilter(t *testing.T) {
} }
func TestGoogleZones(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()) zones, err := provider.Zones(context.Background())
require.NoError(t, err) 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"), 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()) records, err := provider.Records(context.Background())
require.NoError(t, err) require.NoError(t, err)
@ -261,7 +263,7 @@ func TestGoogleRecordsFilter(t *testing.T) {
"zone-0.ext-dns-test-2.gcp.zalan.do.", "zone-0.ext-dns-test-2.gcp.zalan.do.",
// there exists a third zone "zone-3" that we want to exclude from being managed. // there exists a third zone "zone-3" that we want to exclude from being managed.
}), }),
NewZoneIDFilter([]string{""}), provider.NewZoneIDFilter([]string{""}),
false, false,
originalEndpoints, originalEndpoints,
) )
@ -286,7 +288,7 @@ func TestGoogleRecordsFilter(t *testing.T) {
} }
func TestGoogleCreateRecords(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{ records := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 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-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"), 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{ updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), 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"), 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"), 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)) require.NoError(t, provider.DeleteRecords(originalEndpoints))
@ -359,7 +361,7 @@ func TestGoogleApplyChanges(t *testing.T) {
"zone-0.ext-dns-test-2.gcp.zalan.do.", "zone-0.ext-dns-test-2.gcp.zalan.do.",
// there exists a third zone "zone-3" that we want to exclude from being managed. // there exists a third zone "zone-3" that we want to exclude from being managed.
}), }),
NewZoneIDFilter([]string{""}), provider.NewZoneIDFilter([]string{""}),
false, false,
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"), 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"), 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{ createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), 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) { 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{})) assert.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{}))
} }
func TestNewFilteredRecords(t *testing.T) { 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{ 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"), 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) 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{ provider := &GoogleProvider{
project: "zalando-external-dns-test", project: "zalando-external-dns-test",
dryRun: false, 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{ provider := &GoogleProvider{
project: "zalando-external-dns-test", project: "zalando-external-dns-test",
dryRun: false, dryRun: false,
@ -792,3 +794,7 @@ func clearGoogleRecords(t *testing.T, provider *GoogleProvider, zone string) {
require.NoError(t, err) 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)
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package infoblox
import ( import (
"context" "context"
@ -29,12 +29,13 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
// InfobloxConfig clarifies the method signature // InfobloxConfig clarifies the method signature
type InfobloxConfig struct { type InfobloxConfig struct {
DomainFilter endpoint.DomainFilter DomainFilter endpoint.DomainFilter
ZoneIDFilter ZoneIDFilter ZoneIDFilter provider.ZoneIDFilter
Host string Host string
Port int Port int
Username string Username string
@ -48,9 +49,10 @@ type InfobloxConfig struct {
// InfobloxProvider implements the DNS provider for Infoblox. // InfobloxProvider implements the DNS provider for Infoblox.
type InfobloxProvider struct { type InfobloxProvider struct {
provider.BaseProvider
client ibclient.IBConnector client ibclient.IBConnector
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
view string view string
dryRun bool dryRun bool
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package infoblox
import ( import (
"context" "context"
@ -28,7 +28,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type mockIBConnector struct { type mockIBConnector struct {
@ -329,7 +331,7 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject
return nil 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{ return &InfobloxProvider{
client: client, client: client,
domainFilter: domainFilter, 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()) actual, err := provider.Records(context.Background())
if err != nil { if err != nil {
@ -428,7 +430,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
provider := newInfobloxProvider( provider := newInfobloxProvider(
endpoint.NewDomainFilter([]string{""}), endpoint.NewDomainFilter([]string{""}),
NewZoneIDFilter([]string{""}), provider.NewZoneIDFilter([]string{""}),
dryRun, dryRun,
client, client,
) )
@ -486,7 +488,7 @@ func TestInfobloxZones(t *testing.T) {
mockInfobloxObjects: &[]ibclient.IBObject{}, 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() zones, _ := provider.zones()
var emptyZoneAuth *ibclient.ZoneAuth var emptyZoneAuth *ibclient.ZoneAuth
assert.Equal(t, provider.findZone(zones, "example.com").Fqdn, "example.com") 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") == "") 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)
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package inmemory
import ( import (
"context" "context"
@ -25,6 +25,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
var ( var (
@ -43,6 +44,7 @@ var (
// InMemoryProvider - dns provider only used for testing purposes // InMemoryProvider - dns provider only used for testing purposes
// initialized as dns provider with no records // initialized as dns provider with no records
type InMemoryProvider struct { type InMemoryProvider struct {
provider.BaseProvider
domain endpoint.DomainFilter domain endpoint.DomainFilter
client *inMemoryClient client *inMemoryClient
filter *filter filter *filter

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package inmemory
import ( import (
"context" "context"
@ -26,10 +26,11 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
var ( var (
_ Provider = &InMemoryProvider{} _ provider.Provider = &InMemoryProvider{}
) )
func TestInMemoryProvider(t *testing.T) { func TestInMemoryProvider(t *testing.T) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package linode
import ( import (
"context" "context"
@ -30,12 +30,13 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
// LinodeDomainClient interface to ease testing // LinodeDomainClient interface to ease testing
type LinodeDomainClient interface { type LinodeDomainClient interface {
ListDomainRecords(ctx context.Context, domainID int, opts *linodego.ListOptions) ([]*linodego.DomainRecord, error) ListDomainRecords(ctx context.Context, domainID int, opts *linodego.ListOptions) ([]linodego.DomainRecord, error)
ListDomains(ctx context.Context, opts *linodego.ListOptions) ([]*linodego.Domain, error) ListDomains(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Domain, error)
CreateDomainRecord(ctx context.Context, domainID int, domainrecord linodego.DomainRecordCreateOptions) (*linodego.DomainRecord, error) CreateDomainRecord(ctx context.Context, domainID int, domainrecord linodego.DomainRecordCreateOptions) (*linodego.DomainRecord, error)
DeleteDomainRecord(ctx context.Context, domainID int, id int) error DeleteDomainRecord(ctx context.Context, domainID int, id int) error
UpdateDomainRecord(ctx context.Context, domainID int, id int, domainrecord linodego.DomainRecordUpdateOptions) (*linodego.DomainRecord, 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. // LinodeProvider is an implementation of Provider for Digital Ocean's DNS.
type LinodeProvider struct { type LinodeProvider struct {
provider.BaseProvider
Client LinodeDomainClient Client LinodeDomainClient
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
DryRun bool DryRun bool
@ -50,28 +52,28 @@ type LinodeProvider struct {
// LinodeChanges All API calls calculated from the plan // LinodeChanges All API calls calculated from the plan
type LinodeChanges struct { type LinodeChanges struct {
Creates []*LinodeChangeCreate Creates []LinodeChangeCreate
Deletes []*LinodeChangeDelete Deletes []LinodeChangeDelete
Updates []*LinodeChangeUpdate Updates []LinodeChangeUpdate
} }
// LinodeChangeCreate Linode Domain Record Creates // LinodeChangeCreate Linode Domain Record Creates
type LinodeChangeCreate struct { type LinodeChangeCreate struct {
Domain *linodego.Domain Domain linodego.Domain
Options linodego.DomainRecordCreateOptions Options linodego.DomainRecordCreateOptions
} }
// LinodeChangeUpdate Linode Domain Record Updates // LinodeChangeUpdate Linode Domain Record Updates
type LinodeChangeUpdate struct { type LinodeChangeUpdate struct {
Domain *linodego.Domain Domain linodego.Domain
DomainRecord *linodego.DomainRecord DomainRecord linodego.DomainRecord
Options linodego.DomainRecordUpdateOptions Options linodego.DomainRecordUpdateOptions
} }
// LinodeChangeDelete Linode Domain Record Deletes // LinodeChangeDelete Linode Domain Record Deletes
type LinodeChangeDelete struct { type LinodeChangeDelete struct {
Domain *linodego.Domain Domain linodego.Domain
DomainRecord *linodego.DomainRecord DomainRecord linodego.DomainRecord
} }
// NewLinodeProvider initializes a new Linode DNS based Provider. // 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. // 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) zones, err := p.fetchZones(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -126,7 +128,7 @@ func (p *LinodeProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err
} }
for _, r := range records { for _, r := range records {
if supportedRecordType(string(r.Type)) { if provider.SupportedRecordType(string(r.Type)) {
name := fmt.Sprintf("%s.%s", r.Name, zone.Domain) name := fmt.Sprintf("%s.%s", r.Name, zone.Domain)
// root name is identified by the empty string and should be // 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 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) records, err := p.Client.ListDomainRecords(ctx, domainID, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -152,8 +154,8 @@ func (p *LinodeProvider) fetchRecords(ctx context.Context, domainID int) ([]*lin
return records, nil return records, nil
} }
func (p *LinodeProvider) fetchZones(ctx context.Context) ([]*linodego.Domain, error) { func (p *LinodeProvider) fetchZones(ctx context.Context) ([]linodego.Domain, error) {
var zones []*linodego.Domain var zones []linodego.Domain
allZones, err := p.Client.ListDomains(ctx, linodego.NewListOptions(0, "")) 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. // ApplyChanges applies a given set of changes in a given zone.
func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 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) zones, err := p.fetchZones(ctx)
@ -265,9 +267,9 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
return err return err
} }
zonesByID := make(map[string]*linodego.Domain) zonesByID := make(map[string]linodego.Domain)
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
zoneNameIDMapper.Add(strconv.Itoa(z.ID), z.Domain) 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) updatesByZone := endpointsByZone(zoneNameIDMapper, changes.UpdateNew)
deletesByZone := endpointsByZone(zoneNameIDMapper, changes.Delete) deletesByZone := endpointsByZone(zoneNameIDMapper, changes.Delete)
var linodeCreates []*LinodeChangeCreate var linodeCreates []LinodeChangeCreate
var linodeUpdates []*LinodeChangeUpdate var linodeUpdates []LinodeChangeUpdate
var linodeDeletes []*LinodeChangeDelete var linodeDeletes []LinodeChangeDelete
// Generate Creates // Generate Creates
for zoneID, creates := range createsByZone { for zoneID, creates := range createsByZone {
@ -326,7 +328,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
} }
for _, target := range ep.Targets { for _, target := range ep.Targets {
linodeCreates = append(linodeCreates, &LinodeChangeCreate{ linodeCreates = append(linodeCreates, LinodeChangeCreate{
Domain: zone, Domain: zone,
Options: linodego.DomainRecordCreateOptions{ Options: linodego.DomainRecordCreateOptions{
Target: target, Target: target,
@ -374,7 +376,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
return err return err
} }
matchedRecordsByTarget := make(map[string]*linodego.DomainRecord) matchedRecordsByTarget := make(map[string]linodego.DomainRecord)
for _, record := range matchedRecords { for _, record := range matchedRecords {
matchedRecordsByTarget[record.Target] = record matchedRecordsByTarget[record.Target] = record
@ -390,7 +392,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
"target": target, "target": target,
}).Warn("Updating Existing Target") }).Warn("Updating Existing Target")
linodeUpdates = append(linodeUpdates, &LinodeChangeUpdate{ linodeUpdates = append(linodeUpdates, LinodeChangeUpdate{
Domain: zone, Domain: zone,
DomainRecord: record, DomainRecord: record,
Options: linodego.DomainRecordUpdateOptions{ Options: linodego.DomainRecordUpdateOptions{
@ -415,7 +417,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
"target": target, "target": target,
}).Warn("Creating New Target") }).Warn("Creating New Target")
linodeCreates = append(linodeCreates, &LinodeChangeCreate{ linodeCreates = append(linodeCreates, LinodeChangeCreate{
Domain: zone, Domain: zone,
Options: linodego.DomainRecordCreateOptions{ Options: linodego.DomainRecordCreateOptions{
Target: target, Target: target,
@ -440,12 +442,11 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
"target": record.Target, "target": record.Target,
}).Warn("Deleting Target") }).Warn("Deleting Target")
linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{ linodeDeletes = append(linodeDeletes, LinodeChangeDelete{
Domain: zone, Domain: zone,
DomainRecord: record, DomainRecord: record,
}) })
} }
} }
} }
@ -476,7 +477,7 @@ func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
} }
for _, record := range matchedRecords { for _, record := range matchedRecords {
linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{ linodeDeletes = append(linodeDeletes, LinodeChangeDelete{
Domain: zone, Domain: zone,
DomainRecord: record, 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 { func endpointsByZone(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]endpoint.Endpoint {
endpointsByZone := make(map[string][]*endpoint.Endpoint) endpointsByZone := make(map[string][]endpoint.Endpoint)
for _, ep := range endpoints { for _, ep := range endpoints {
zoneID, _ := zoneNameIDMapper.FindZone(ep.DNSName) 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) log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", ep.DNSName)
continue continue
} }
endpointsByZone[zoneID] = append(endpointsByZone[zoneID], ep) endpointsByZone[zoneID] = append(endpointsByZone[zoneID], *ep)
} }
return endpointsByZone 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 // Handle root
if ep.DNSName == zone.Domain { if ep.DNSName == zone.Domain {
return "" return ""
@ -532,8 +533,8 @@ func getStrippedRecordName(zone *linodego.Domain, ep *endpoint.Endpoint) string
return strings.TrimSuffix(ep.DNSName, "."+zone.Domain) return strings.TrimSuffix(ep.DNSName, "."+zone.Domain)
} }
func getRecordID(records []*linodego.DomainRecord, zone *linodego.Domain, ep *endpoint.Endpoint) []*linodego.DomainRecord { func getRecordID(records []linodego.DomainRecord, zone linodego.Domain, ep endpoint.Endpoint) []linodego.DomainRecord {
var matchedRecords []*linodego.DomainRecord var matchedRecords []linodego.DomainRecord
for _, record := range records { for _, record := range records {
if record.Name == getStrippedRecordName(zone, ep) && string(record.Type) == ep.RecordType { if record.Name == getStrippedRecordName(zone, ep) && string(record.Type) == ep.RecordType {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package linode
import ( import (
"context" "context"
@ -34,14 +34,14 @@ type MockDomainClient struct {
mock.Mock 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) 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) 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) { func (m *MockDomainClient) CreateDomainRecord(ctx context.Context, domainID int, opts linodego.DomainRecordCreateOptions) (*linodego.DomainRecord, error) {
args := m.Called(ctx, domainID, opts) 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) return args.Get(0).(*linodego.DomainRecord), args.Error(1)
} }
func createZones() []*linodego.Domain { func createZones() []linodego.Domain {
return []*linodego.Domain{ return []linodego.Domain{
{ID: 1, Domain: "foo.com"}, {ID: 1, Domain: "foo.com"},
{ID: 2, Domain: "bar.io"}, {ID: 2, Domain: "bar.io"},
{ID: 3, Domain: "baz.com"}, {ID: 3, Domain: "baz.com"},
} }
} }
func createFooRecords() []*linodego.DomainRecord { func createFooRecords() []linodego.DomainRecord {
return []*linodego.DomainRecord{{ return []linodego.DomainRecord{{
ID: 11, ID: 11,
Type: linodego.RecordTypeA, Type: linodego.RecordTypeA,
Name: "", Name: "",
@ -83,12 +83,12 @@ func createFooRecords() []*linodego.DomainRecord {
}} }}
} }
func createBarRecords() []*linodego.DomainRecord { func createBarRecords() []linodego.DomainRecord {
return []*linodego.DomainRecord{} return []linodego.DomainRecord{}
} }
func createBazRecords() []*linodego.DomainRecord { func createBazRecords() []linodego.DomainRecord {
return []*linodego.DomainRecord{{ return []linodego.DomainRecord{{
ID: 31, ID: 31,
Type: linodego.RecordTypeA, Type: linodego.RecordTypeA,
Name: "", Name: "",
@ -147,15 +147,15 @@ func TestNewLinodeProvider(t *testing.T) {
} }
func TestLinodeStripRecordName(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", Domain: "example.com",
}, &endpoint.Endpoint{ }, endpoint.Endpoint{
DNSName: "api.example.com", DNSName: "api.example.com",
})) }))
assert.Equal(t, "", getStrippedRecordName(&linodego.Domain{ assert.Equal(t, "", getStrippedRecordName(linodego.Domain{
Domain: "example.com", Domain: "example.com",
}, &endpoint.Endpoint{ }, endpoint.Endpoint{
DNSName: "example.com", DNSName: "example.com",
})) }))
} }
@ -198,7 +198,7 @@ func TestLinodeFetchZonesWithFilter(t *testing.T) {
mock.Anything, mock.Anything,
).Return(createZones(), nil).Once() ).Return(createZones(), nil).Once()
expected := []*linodego.Domain{ expected := []linodego.Domain{
{ID: 1, Domain: "foo.com"}, {ID: 1, Domain: "foo.com"},
{ID: 3, Domain: "baz.com"}, {ID: 3, Domain: "baz.com"},
} }
@ -210,15 +210,15 @@ func TestLinodeFetchZonesWithFilter(t *testing.T) {
} }
func TestLinodeGetStrippedRecordName(t *testing.T) { func TestLinodeGetStrippedRecordName(t *testing.T) {
assert.Equal(t, "", getStrippedRecordName(&linodego.Domain{ assert.Equal(t, "", getStrippedRecordName(linodego.Domain{
Domain: "foo.com", Domain: "foo.com",
}, &endpoint.Endpoint{ }, endpoint.Endpoint{
DNSName: "foo.com", DNSName: "foo.com",
})) }))
assert.Equal(t, "api", getStrippedRecordName(&linodego.Domain{ assert.Equal(t, "api", getStrippedRecordName(linodego.Domain{
Domain: "foo.com", Domain: "foo.com",
}, &endpoint.Endpoint{ }, endpoint.Endpoint{
DNSName: "api.foo.com", DNSName: "api.foo.com",
})) }))
} }
@ -398,14 +398,14 @@ func TestLinodeApplyChangesTargetAdded(t *testing.T) {
"ListDomains", "ListDomains",
mock.Anything, mock.Anything,
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( mockDomainClient.On(
"ListDomainRecords", "ListDomainRecords",
mock.Anything, mock.Anything,
1, 1,
mock.Anything, 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 // Apply Actions
mockDomainClient.On( mockDomainClient.On(
@ -457,14 +457,14 @@ func TestLinodeApplyChangesTargetRemoved(t *testing.T) {
"ListDomains", "ListDomains",
mock.Anything, mock.Anything,
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( mockDomainClient.On(
"ListDomainRecords", "ListDomainRecords",
mock.Anything, mock.Anything,
1, 1,
mock.Anything, 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 // Apply Actions
mockDomainClient.On( mockDomainClient.On(
@ -513,14 +513,14 @@ func TestLinodeApplyChangesNoChanges(t *testing.T) {
"ListDomains", "ListDomains",
mock.Anything, mock.Anything,
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( mockDomainClient.On(
"ListDomainRecords", "ListDomainRecords",
mock.Anything, mock.Anything,
1, 1,
mock.Anything, 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{}) err := provider.ApplyChanges(context.Background(), &plan.Changes{})
require.NoError(t, err) require.NoError(t, err)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package ns1
import ( import (
"context" "context"
@ -30,6 +30,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -85,7 +86,7 @@ func (n NS1DomainService) ListZones() ([]*dns.Zone, *http.Response, error) {
// NS1Config passes cli args to the NS1Provider // NS1Config passes cli args to the NS1Provider
type NS1Config struct { type NS1Config struct {
DomainFilter endpoint.DomainFilter DomainFilter endpoint.DomainFilter
ZoneIDFilter ZoneIDFilter ZoneIDFilter provider.ZoneIDFilter
NS1Endpoint string NS1Endpoint string
NS1IgnoreSSL bool NS1IgnoreSSL bool
DryRun bool DryRun bool
@ -93,9 +94,10 @@ type NS1Config struct {
// NS1Provider is the NS1 provider // NS1Provider is the NS1 provider
type NS1Provider struct { type NS1Provider struct {
provider.BaseProvider
client NS1DomainClient client NS1DomainClient
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
dryRun bool dryRun bool
} }
@ -150,7 +152,6 @@ func (p *NS1Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
for _, zone := range zones { for _, zone := range zones {
// TODO handle Header Codes // TODO handle Header Codes
zoneData, _, err := p.client.GetZone(zone.String()) zoneData, _, err := p.client.GetZone(zone.String())
if err != nil { if err != nil {
@ -158,7 +159,7 @@ func (p *NS1Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
} }
for _, record := range zoneData.Records { for _, record := range zoneData.Records {
if supportedRecordType(record.Type) { if provider.SupportedRecordType(record.Type) {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL( endpoints = append(endpoints, endpoint.NewEndpointWithTTL(
record.Domain, record.Domain,
record.Type, 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. // ns1ChangesByZone separates a multi-zone change into a single change per zone.
func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change { func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change {
changes := make(map[string][]*ns1Change) changes := make(map[string][]*ns1Change)
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
zoneNameIDMapper.Add(z.Zone, z.Zone) zoneNameIDMapper.Add(z.Zone, z.Zone)
changes[z.Zone] = []*ns1Change{} changes[z.Zone] = []*ns1Change{}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package ns1
import ( import (
"context" "context"
@ -31,6 +31,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type MockNS1DomainClient struct { type MockNS1DomainClient struct {
@ -130,7 +131,7 @@ func TestNS1Records(t *testing.T) {
provider := &NS1Provider{ provider := &NS1Provider{
client: &MockNS1DomainClient{}, client: &MockNS1DomainClient{},
domainFilter: endpoint.NewDomainFilter([]string{"foo.com."}), domainFilter: endpoint.NewDomainFilter([]string{"foo.com."}),
zoneIDFilter: NewZoneIDFilter([]string{""}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
} }
ctx := context.Background() ctx := context.Background()
@ -151,7 +152,7 @@ func TestNewNS1Provider(t *testing.T) {
_ = os.Setenv("NS1_APIKEY", "xxxxxxxxxxxxxxxxx") _ = os.Setenv("NS1_APIKEY", "xxxxxxxxxxxxxxxxx")
testNS1Config := NS1Config{ testNS1Config := NS1Config{
DomainFilter: endpoint.NewDomainFilter([]string{"foo.com."}), DomainFilter: endpoint.NewDomainFilter([]string{"foo.com."}),
ZoneIDFilter: NewZoneIDFilter([]string{""}), ZoneIDFilter: provider.NewZoneIDFilter([]string{""}),
DryRun: false, DryRun: false,
} }
_, err := NewNS1Provider(testNS1Config) _, err := NewNS1Provider(testNS1Config)
@ -166,7 +167,7 @@ func TestNS1Zones(t *testing.T) {
provider := &NS1Provider{ provider := &NS1Provider{
client: &MockNS1DomainClient{}, client: &MockNS1DomainClient{},
domainFilter: endpoint.NewDomainFilter([]string{"foo.com."}), domainFilter: endpoint.NewDomainFilter([]string{"foo.com."}),
zoneIDFilter: NewZoneIDFilter([]string{""}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
} }
zones, err := provider.zonesFiltered() zones, err := provider.zonesFiltered()

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package oci
import ( import (
"context" "context"
@ -29,6 +29,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ociRecordTTL = 300 const ociRecordTTL = 300
@ -52,11 +53,12 @@ type OCIConfig struct {
// OCIProvider is an implementation of Provider for Oracle Cloud Infrastructure // OCIProvider is an implementation of Provider for Oracle Cloud Infrastructure
// (OCI) DNS. // (OCI) DNS.
type OCIProvider struct { type OCIProvider struct {
provider.BaseProvider
client ociDNSClient client ociDNSClient
cfg OCIConfig cfg OCIConfig
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
dryRun bool dryRun bool
} }
@ -82,8 +84,8 @@ func LoadOCIConfig(path string) (*OCIConfig, error) {
return &cfg, nil return &cfg, nil
} }
// NewOCIProvider initialises a new OCI DNS based Provider. // NewOCIProvider initializes a new OCI DNS based Provider.
func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) (*OCIProvider, error) { func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (*OCIProvider, error) {
var client ociDNSClient var client ociDNSClient
client, err := dns.NewDnsClientWithConfigurationProvider(common.NewRawConfigurationProvider( client, err := dns.NewDnsClientWithConfigurationProvider(common.NewRawConfigurationProvider(
cfg.Auth.TenancyID, cfg.Auth.TenancyID,
@ -94,7 +96,7 @@ func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFil
&cfg.Auth.Passphrase, &cfg.Auth.Passphrase,
)) ))
if err != nil { 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{ return &OCIProvider{
@ -177,7 +179,7 @@ func (p *OCIProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
} }
for _, record := range resp.Items { for _, record := range resp.Items {
if !supportedRecordType(*record.Rtype) { if !provider.SupportedRecordType(*record.Rtype) {
continue continue
} }
endpoints = append(endpoints, endpoints = append(endpoints,
@ -252,7 +254,7 @@ func newRecordOperation(ep *endpoint.Endpoint, opType dns.RecordOperationOperati
targets := make([]string, len(ep.Targets)) targets := make([]string, len(ep.Targets))
copy(targets, []string(ep.Targets)) copy(targets, []string(ep.Targets))
if ep.RecordType == endpoint.RecordTypeCNAME { if ep.RecordType == endpoint.RecordTypeCNAME {
targets[0] = ensureTrailingDot(targets[0]) targets[0] = provider.EnsureTrailingDot(targets[0])
} }
rdata := strings.Join(targets, " ") 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 { func operationsByZone(zones map[string]dns.ZoneSummary, ops []dns.RecordOperation) map[string][]dns.RecordOperation {
changes := make(map[string][]dns.RecordOperation) changes := make(map[string][]dns.RecordOperation)
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
zoneNameIDMapper.Add(*z.Id, *z.Name) zoneNameIDMapper.Add(*z.Id, *z.Name)
changes[*z.Id] = []dns.RecordOperation{} changes[*z.Id] = []dns.RecordOperation{}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package oci
import ( import (
"context" "context"
@ -28,6 +28,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type mockOCIDNSClient struct{} 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. // 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{ return &OCIProvider{
client: client, client: client,
cfg: OCIConfig{ 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 { for name, tc := range testCases {
@ -184,7 +185,7 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K
_, err := NewOCIProvider( _, err := NewOCIProvider(
tc.config, tc.config,
endpoint.NewDomainFilter([]string{"com"}), endpoint.NewDomainFilter([]string{"com"}),
NewZoneIDFilter([]string{""}), provider.NewZoneIDFilter([]string{""}),
false, false,
) )
if err == nil { if err == nil {
@ -200,13 +201,13 @@ func TestOCIZones(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
expected map[string]dns.ZoneSummary expected map[string]dns.ZoneSummary
}{ }{
{ {
name: "DomainFilter_com", name: "DomainFilter_com",
domainFilter: endpoint.NewDomainFilter([]string{"com"}), domainFilter: endpoint.NewDomainFilter([]string{"com"}),
zoneIDFilter: NewZoneIDFilter([]string{""}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
expected: map[string]dns.ZoneSummary{ expected: map[string]dns.ZoneSummary{
"foo.com": { "foo.com": {
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"), Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
@ -220,7 +221,7 @@ func TestOCIZones(t *testing.T) {
}, { }, {
name: "DomainFilter_foo.com", name: "DomainFilter_foo.com",
domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}), domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}),
zoneIDFilter: NewZoneIDFilter([]string{""}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
expected: map[string]dns.ZoneSummary{ expected: map[string]dns.ZoneSummary{
"foo.com": { "foo.com": {
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"), Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
@ -230,7 +231,7 @@ func TestOCIZones(t *testing.T) {
}, { }, {
name: "ZoneIDFilter_ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959", name: "ZoneIDFilter_ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959",
domainFilter: endpoint.NewDomainFilter([]string{""}), 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{ expected: map[string]dns.ZoneSummary{
"foo.com": { "foo.com": {
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"), Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
@ -253,13 +254,13 @@ func TestOCIRecords(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
expected []*endpoint.Endpoint expected []*endpoint.Endpoint
}{ }{
{ {
name: "unfiltered", name: "unfiltered",
domainFilter: endpoint.NewDomainFilter([]string{""}), domainFilter: endpoint.NewDomainFilter([]string{""}),
zoneIDFilter: NewZoneIDFilter([]string{""}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
expected: []*endpoint.Endpoint{ expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"), 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"), 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", name: "DomainFilter_foo.com",
domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}), domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}),
zoneIDFilter: NewZoneIDFilter([]string{""}), zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
expected: []*endpoint.Endpoint{ expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"), 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"), 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", name: "ZoneIDFilter_ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404",
domainFilter: endpoint.NewDomainFilter([]string{""}), domainFilter: endpoint.NewDomainFilter([]string{""}),
zoneIDFilter: NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"}), zoneIDFilter: provider.NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"}),
expected: []*endpoint.Endpoint{ expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("foo.bar.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"), 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( provider := newOCIProvider(
client, client,
endpoint.NewDomainFilter([]string{""}), endpoint.NewDomainFilter([]string{""}),
NewZoneIDFilter([]string{""}), provider.NewZoneIDFilter([]string{""}),
tc.dryRun, tc.dryRun,
) )

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package ovh
import ( import (
"context" "context"
@ -28,6 +28,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -45,6 +46,8 @@ var (
// OVHProvider is an implementation of Provider for OVH DNS. // OVHProvider is an implementation of Provider for OVH DNS.
type OVHProvider struct { type OVHProvider struct {
provider.BaseProvider
client ovhClient client ovhClient
domainFilter endpoint.DomainFilter 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. // Records returns the list of records in all relevant zones.
func (p *OVHProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { func (p *OVHProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
_, records, err := p.zonesRecords(ctx) _, records, err := p.zonesRecords(ctx)
if err != nil { if err != nil {
return nil, err 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 { if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record/%d", *zone, id), &record); err != nil {
return err return err
} }
if supportedRecordType(record.FieldType) { if provider.SupportedRecordType(record.FieldType) {
log.Debugf("OVH: Record %d for %s is %+v", id, *zone, record) log.Debugf("OVH: Record %d for %s is %+v", id, *zone, record)
records <- 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 { func newOvhChange(action int, endpoints []*endpoint.Endpoint, zones []string, records []ovhRecord) []ovhChange {
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
ovhChanges := make([]ovhChange, 0, countTargets(endpoints)) ovhChanges := make([]ovhChange, 0, countTargets(endpoints))
for _, zone := range zones { for _, zone := range zones {
zoneNameIDMapper.Add(zone, zone) zoneNameIDMapper.Add(zone, zone)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package ovh
import ( import (
"context" "context"
@ -142,9 +142,9 @@ func TestOvhRecords(t *testing.T) {
sort.Strings(endoint.Targets) sort.Strings(endoint.Targets)
} }
assert.ElementsMatch(endpoints, []*endpoint.Endpoint{ assert.ElementsMatch(endpoints, []*endpoint.Endpoint{
&endpoint.Endpoint{DNSName: "example.org", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42"}}, {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"}}, {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: "ovh.example.net", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42", "203.0.113.43"}},
}) })
client.AssertExpectations(t) client.AssertExpectations(t)
@ -283,10 +283,10 @@ func TestOvhCountTargets(t *testing.T) {
endpoints [][]*endpoint.Endpoint endpoints [][]*endpoint.Endpoint
count int count int
}{ }{
{[][]*endpoint.Endpoint{[]*endpoint.Endpoint{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 1}, {[][]*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{{{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{{{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", "target"}}}, {{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}}, 4},
} }
for _, test := range cases { for _, test := range cases {
count := countTargets(test.endpoints...) count := countTargets(test.endpoints...)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package pdns
import ( import (
"bytes" "bytes"
@ -35,6 +35,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/tlsutils" "sigs.k8s.io/external-dns/pkg/tlsutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type pdnsChangeType string type pdnsChangeType string
@ -116,7 +117,6 @@ func (tlsConfig *TLSConfig) setHTTPClient(pdnsClientConfig *pgo.Configuration) e
// Function for debug printing // Function for debug printing
func stringifyHTTPResponseBody(r *http.Response) (body string) { func stringifyHTTPResponseBody(r *http.Response) (body string) {
if r == nil { if r == nil {
return "" return ""
} }
@ -125,7 +125,6 @@ func stringifyHTTPResponseBody(r *http.Response) (body string) {
buf.ReadFrom(r.Body) buf.ReadFrom(r.Body)
body = buf.String() body = buf.String()
return body return body
} }
// PDNSAPIProvider : Interface used and extended by the PDNSAPIClient struct as // 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) log.Errorf("Unable to fetch zones. %v", err)
return zones, resp, 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 // 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) log.Debugf("Retrying ListZone() ... %d", i)
time.Sleep(retryAfterTime * (1 << uint(i))) time.Sleep(retryAfterTime * (1 << uint(i)))
continue continue
} }
return zone, resp, err return zone, resp, err
} }
log.Errorf("Unable to list zone. %v", err) log.Errorf("Unable to list zone. %v", err)
return zone, resp, err return zone, resp, err
} }
// PatchZone : Method used to update the contents of a particular zone from PowerDNS // 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) log.Debugf("Retrying PatchZone() ... %d", i)
time.Sleep(retryAfterTime * (1 << uint(i))) time.Sleep(retryAfterTime * (1 << uint(i)))
continue continue
} }
return resp, err 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 // PDNSProvider is an implementation of the Provider interface for PowerDNS
type PDNSProvider struct { type PDNSProvider struct {
provider.BaseProvider
client PDNSAPIProvider client PDNSAPIProvider
} }
// NewPDNSProvider initializes a new PowerDNS based Provider. // NewPDNSProvider initializes a new PowerDNS based Provider.
func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, error) { func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, error) {
// Do some input validation // Do some input validation
if config.APIKey == "" { 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 // 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, domainFilter: config.DomainFilter,
}, },
} }
return provider, nil 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)) endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rr.Type_, endpoint.TTL(rr.Ttl), record.Content))
} }
} }
return endpoints, nil return endpoints, nil
} }
// ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs // ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs
func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) { func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) {
zonelist = []pgo.Zone{} zonelist = []pgo.Zone{}
endpoints := make([]*endpoint.Endpoint, len(eps)) endpoints := make([]*endpoint.Endpoint, len(eps))
copy(endpoints, eps) copy(endpoints, eps)
@ -310,15 +302,15 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
zone.Rrsets = []pgo.RrSet{} zone.Rrsets = []pgo.RrSet{}
for i := 0; i < len(endpoints); { for i := 0; i < len(endpoints); {
ep := endpoints[i] ep := endpoints[i]
dnsname := ensureTrailingDot(ep.DNSName) dnsname := provider.EnsureTrailingDot(ep.DNSName)
if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) { if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) {
// The assumption here is that there will only ever be one target // The assumption here is that there will only ever be one target
// per (ep.DNSName, ep.RecordType) tuple, which holds true for // per (ep.DNSName, ep.RecordType) tuple, which holds true for
// external-dns v5.0.0-alpha onwards // external-dns v5.0.0-alpha onwards
records := []pgo.Record{} records := []pgo.Record{}
for _, t := range ep.Targets { for _, t := range ep.Targets {
if "CNAME" == ep.RecordType { if ep.RecordType == "CNAME" {
t = ensureTrailingDot(t) t = provider.EnsureTrailingDot(t)
} }
records = append(records, pgo.Record{Content: 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 // DELETEs explicitly forbid a TTL, therefore only PATCHes need the TTL
if changetype == PdnsReplace { if changetype == PdnsReplace {
if int64(ep.RecordTTL) > int64(math.MaxInt32) { 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 { if ep.RecordTTL == 0 {
// No TTL was specified for the record, we use the default // 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 // If we didn't pop anything, we move to the next item in the list
i++ i++
} }
} }
if len(zone.Rrsets) > 0 { if len(zone.Rrsets) > 0 {
zonelist = append(zonelist, zone) zonelist = append(zonelist, zone)
} }
} }
// residualZones is unsorted by name length like its counterpart // 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 _, zone := range residualZones {
for i := 0; i < len(endpoints); { for i := 0; i < len(endpoints); {
ep := endpoints[i] ep := endpoints[i]
dnsname := ensureTrailingDot(ep.DNSName) dnsname := provider.EnsureTrailingDot(ep.DNSName)
if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) { if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) {
// "pop" endpoint if it's matched to a residual zone... essentially a no-op // "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) 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 // 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 // We warn instead of hard fail here because we don't want a misconfig to cause everything to go down
if len(endpoints) > 0 { if len(endpoints) > 0 {
@ -400,20 +388,17 @@ func (p *PDNSProvider) mutateRecords(endpoints []*endpoint.Endpoint, changetype
} else { } else {
log.Debugf("Struct for PatchZone:\n%s", string(jso)) log.Debugf("Struct for PatchZone:\n%s", string(jso))
} }
resp, err := p.client.PatchZone(zone.Id, zone) resp, err := p.client.PatchZone(zone.Id, zone)
if err != nil { if err != nil {
log.Debugf("PDNS API response: %s", stringifyHTTPResponseBody(resp)) log.Debugf("PDNS API response: %s", stringifyHTTPResponseBody(resp))
return err return err
} }
} }
return nil return nil
} }
// Records returns all DNS records controlled by the configured PDNS server (for all zones) // 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) { func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
zones, _, err := p.client.ListZones() zones, _, err := p.client.ListZones()
if err != nil { if err != nil {
return nil, err 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 // ApplyChanges takes a list of changes (endpoints) and updates the PDNS server
// by sending the correct HTTP PATCH requests to a matching zone // by sending the correct HTTP PATCH requests to a matching zone
func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
startTime := time.Now() startTime := time.Now()
// Create // Create
@ -491,7 +475,6 @@ func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes)
return err return err
} }
} }
log.Debugf("Changes pushed out to PowerDNS in %s\n", time.Since(startTime)) log.Debugf("Changes pushed out to PowerDNS in %s\n", time.Since(startTime))
return nil return nil
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package pdns
import ( import (
"context" "context"
@ -735,7 +735,7 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreateTLS() {
DomainFilter: endpoint.NewDomainFilter([]string{""}), DomainFilter: endpoint.NewDomainFilter([]string{""}),
TLSConfig: TLSConfig{ TLSConfig: TLSConfig{
TLSEnabled: true, 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") 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{""}), DomainFilter: endpoint.NewDomainFilter([]string{""}),
TLSConfig: TLSConfig{ TLSConfig: TLSConfig{
TLSEnabled: true, TLSEnabled: true,
CAFilePath: "../internal/testresources/ca.pem", CAFilePath: "../../internal/testresources/ca.pem",
ClientCertFilePath: "../internal/testresources/client-cert.pem", ClientCertFilePath: "../../internal/testresources/client-cert.pem",
}, },
}) })
assert.Error(suite.T(), err, "Enabled TLS Config with --tls-client-cert only should raise an error") 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{""}), DomainFilter: endpoint.NewDomainFilter([]string{""}),
TLSConfig: TLSConfig{ TLSConfig: TLSConfig{
TLSEnabled: true, TLSEnabled: true,
CAFilePath: "../internal/testresources/ca.pem", CAFilePath: "../../internal/testresources/ca.pem",
ClientCertKeyFilePath: "../internal/testresources/client-cert-key.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") 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{""}), DomainFilter: endpoint.NewDomainFilter([]string{""}),
TLSConfig: TLSConfig{ TLSConfig: TLSConfig{
TLSEnabled: true, TLSEnabled: true,
CAFilePath: "../internal/testresources/ca.pem", CAFilePath: "../../internal/testresources/ca.pem",
ClientCertFilePath: "../internal/testresources/client-cert.pem", ClientCertFilePath: "../../internal/testresources/client-cert.pem",
ClientCertKeyFilePath: "../internal/testresources/client-cert-key.pem", ClientCertKeyFilePath: "../../internal/testresources/client-cert-key.pem",
}, },
}) })
assert.Nil(suite.T(), err, "Enabled TLS Config with all flags should raise no error") assert.Nil(suite.T(), err, "Enabled TLS Config with all flags should raise no error")

View File

@ -29,6 +29,14 @@ import (
type Provider interface { type Provider interface {
Records(ctx context.Context) ([]*endpoint.Endpoint, error) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
ApplyChanges(ctx context.Context, changes *plan.Changes) 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 { type contextKey struct {
@ -42,11 +50,34 @@ func (k *contextKey) String() string { return "provider context value " + k.name
// type []*endpoint.Endpoint. // type []*endpoint.Endpoint.
var RecordsContextKey = &contextKey{"records"} var RecordsContextKey = &contextKey{"records"}
// ensureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already. // EnsureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already.
func ensureTrailingDot(hostname string) string { func EnsureTrailingDot(hostname string) string {
if net.ParseIP(hostname) != nil { if net.ParseIP(hostname) != nil {
return hostname return hostname
} }
return strings.TrimSuffix(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
}

View File

@ -17,9 +17,19 @@ limitations under the License.
package provider package provider
import ( import (
"io/ioutil"
"os"
"testing" "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) { func TestEnsureTrailingDot(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
input, expected string input, expected string
@ -28,10 +38,28 @@ func TestEnsureTrailingDot(t *testing.T) {
{"example.org.", "example.org."}, {"example.org.", "example.org."},
{"8.8.8.8", "8.8.8.8"}, {"8.8.8.8", "8.8.8.8"},
} { } {
output := ensureTrailingDot(tc.input) output := EnsureTrailingDot(tc.input)
if output != tc.expected { if output != tc.expected {
t.Errorf("expected %s, got %s", tc.expected, output) 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")
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package rcode0
import ( import (
"context" "context"
@ -28,10 +28,12 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
// RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS. // RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS.
type RcodeZeroProvider struct { type RcodeZeroProvider struct {
provider.BaseProvider
Client *rc0.Client Client *rc0.Client
DomainFilter endpoint.DomainFilter DomainFilter endpoint.DomainFilter
@ -44,7 +46,6 @@ type RcodeZeroProvider struct {
// //
// Returns the provider or an error if a provider could not be created. // Returns the provider or an error if a provider could not be created.
func NewRcodeZeroProvider(domainFilter endpoint.DomainFilter, dryRun bool, txtEnc bool) (*RcodeZeroProvider, error) { func NewRcodeZeroProvider(domainFilter endpoint.DomainFilter, dryRun bool, txtEnc bool) (*RcodeZeroProvider, error) {
client, err := rc0.NewClient(os.Getenv("RC0_API_KEY")) client, err := rc0.NewClient(os.Getenv("RC0_API_KEY"))
if err != nil { if err != nil {
@ -76,7 +77,6 @@ func NewRcodeZeroProvider(domainFilter endpoint.DomainFilter, dryRun bool, txtEn
// Zones returns filtered zones if filter is set // Zones returns filtered zones if filter is set
func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) { func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) {
var result []*rc0.Zone var result []*rc0.Zone
zones, err := p.fetchZones() 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 // Decrypts TXT records if TXT-Encrypt flag is set and key is provided
func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
zones, err := p.Zones() zones, err := p.Zones()
if err != nil { if err != nil {
return nil, err return nil, err
@ -106,7 +105,6 @@ func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
for _, zone := range zones { for _, zone := range zones {
rrset, err := p.fetchRecords(zone.Domain) rrset, err := p.fetchRecords(zone.Domain)
if err != nil { if err != nil {
@ -114,25 +112,19 @@ func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
} }
for _, r := range rrset { for _, r := range rrset {
if provider.SupportedRecordType(r.Type) {
if supportedRecordType(r.Type) {
if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(r.Type, "TXT") { if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(r.Type, "TXT") {
p.Client.RRSet.DecryptTXT(p.Key, r) p.Client.RRSet.DecryptTXT(p.Key, r)
} }
if len(r.Records) > 1 { if len(r.Records) > 1 {
for _, _r := range r.Records { for _, _r := range r.Records {
if !_r.Disabled { if !_r.Disabled {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), _r.Content)) endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), _r.Content))
} }
} }
} else if !r.Records[0].Disabled { } else if !r.Records[0].Disabled {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Records[0].Content)) 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. // ApplyChanges applies a given set of changes in a given zone.
func (p *RcodeZeroProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 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 := make([]*rc0.RRSetChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeADD, changes.Create)...) 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 // Helper function
func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map[string][]*rc0.RRSetChange { func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map[string][]*rc0.RRSetChange {
changes := make(map[string][]*rc0.RRSetChange) changes := make(map[string][]*rc0.RRSetChange)
zoneNameIDMapper := zoneIDName{} zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
zoneNameIDMapper.Add(z.Domain, z.Domain) zoneNameIDMapper.Add(z.Domain, z.Domain)
changes[z.Domain] = []*rc0.RRSetChange{} changes[z.Domain] = []*rc0.RRSetChange{}
@ -176,7 +166,6 @@ func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map
// Helper function // Helper function
func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) { func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) {
var allRecords []*rc0.RRType var allRecords []*rc0.RRType
listOptions := rc0.NewListOptions() listOptions := rc0.NewListOptions()
@ -202,7 +191,6 @@ func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error)
// Helper function // Helper function
func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) { func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) {
var allZones []*rc0.Zone var allZones []*rc0.Zone
listOptions := rc0.NewListOptions() listOptions := rc0.NewListOptions()
@ -228,7 +216,6 @@ func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) {
// //
// Changes are submitted by change type. // Changes are submitted by change type.
func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error { func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error {
if len(changes) == 0 { if len(changes) == 0 {
return nil 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. // separate into per-zone change sets to be passed to the API.
changesByZone := rcodezeroChangesByZone(zones, changes) changesByZone := rcodezeroChangesByZone(zones, changes)
for zoneName, changes := range changesByZone { for zoneName, changes := range changesByZone {
for _, change := range changes { for _, change := range changes {
logFields := log.Fields{ logFields := log.Fields{
"record": change.Name, "record": change.Name,
"content": change.Records[0].Content, "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. // NewRcodezeroChanges returns a RcodeZero specific array with rrset change objects.
func (p *RcodeZeroProvider) NewRcodezeroChanges(action string, endpoints []*endpoint.Endpoint) []*rc0.RRSetChange { func (p *RcodeZeroProvider) NewRcodezeroChanges(action string, endpoints []*endpoint.Endpoint) []*rc0.RRSetChange {
changes := make([]*rc0.RRSetChange, 0, len(endpoints)) changes := make([]*rc0.RRSetChange, 0, len(endpoints))
for _, _endpoint := range 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. // NewRcodezeroChange returns a RcodeZero specific rrset change object.
func (p *RcodeZeroProvider) NewRcodezeroChange(action string, endpoint *endpoint.Endpoint) *rc0.RRSetChange { func (p *RcodeZeroProvider) NewRcodezeroChange(action string, endpoint *endpoint.Endpoint) *rc0.RRSetChange {
change := &rc0.RRSetChange{ change := &rc0.RRSetChange{
Type: endpoint.RecordType, Type: endpoint.RecordType,
ChangeType: action, ChangeType: action,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package rcode0
import ( import (
"context" "context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package rdns
import ( import (
"context" "context"
@ -35,9 +35,11 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
etcdTimeout = 5 * time.Second
rdnsMaxHosts = 10 rdnsMaxHosts = 10
rdnsOriginalLabel = "originalText" rdnsOriginalLabel = "originalText"
rdnsPrefix = "/rdnsv3" rdnsPrefix = "/rdnsv3"
@ -65,6 +67,7 @@ type RDNSConfig struct {
// RDNSProvider is an implementation of Provider for Rancher DNS(RDNS). // RDNSProvider is an implementation of Provider for Rancher DNS(RDNS).
type RDNSProvider struct { type RDNSProvider struct {
provider.BaseProvider
client RDNSClient client RDNSClient
dryRun bool dryRun bool
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package rdns
import ( import (
"context" "context"

View File

@ -16,9 +16,9 @@ limitations under the License.
package provider 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. // Currently A, CNAME, SRV, and TXT record types are supported.
func supportedRecordType(recordType string) bool { func SupportedRecordType(recordType string) bool {
switch recordType { switch recordType {
case "A", "CNAME", "SRV", "TXT": case "A", "CNAME", "SRV", "TXT":
return true return true

View File

@ -41,7 +41,7 @@ func TestRecordTypeFilter(t *testing.T) {
}, },
} }
for _, r := range records { for _, r := range records {
got := supportedRecordType(r.rtype) got := SupportedRecordType(r.rtype)
if r.expect != got { if r.expect != got {
t.Errorf("wrong record type %s: expect %v, but got %v", r.rtype, r.expect, got) t.Errorf("wrong record type %s: expect %v, but got %v", r.rtype, r.expect, got)
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package rfc2136
import ( import (
"context" "context"
@ -30,10 +30,17 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "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 // rfc2136 provider type
type rfc2136Provider struct { type rfc2136Provider struct {
provider.BaseProvider
nameserver string nameserver string
zoneName string zoneName string
tsigKeyName string tsigKeyName string
@ -65,7 +72,7 @@ type rfc2136Actions interface {
} }
// NewRfc2136Provider is a factory function for OpenStack rfc2136 providers // 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] secretAlgChecked, ok := tsigAlgs[secretAlg]
if !ok && !insecure { if !ok && !insecure {
return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg) 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) m.SetUpdate(r.zoneName)
for _, ep := range changes.Create { for _, ep := range changes.Create {
if !r.domainFilter.Match(ep.DNSName) { if !r.domainFilter.Match(ep.DNSName) {
log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName) log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
continue continue
@ -214,17 +220,15 @@ func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes
r.AddRecord(m, ep) r.AddRecord(m, ep)
} }
for _, ep := range changes.UpdateNew { for i, ep := range changes.UpdateNew {
if !r.domainFilter.Match(ep.DNSName) { if !r.domainFilter.Match(ep.DNSName) {
log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName) log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
continue continue
} }
r.UpdateRecord(m, ep) r.UpdateRecord(m, changes.UpdateOld[i], ep)
} }
for _, ep := range changes.Delete { for _, ep := range changes.Delete {
if !r.domainFilter.Match(ep.DNSName) { if !r.domainFilter.Match(ep.DNSName) {
log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName) log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
continue continue
@ -244,13 +248,13 @@ func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes
return nil return nil
} }
func (r rfc2136Provider) UpdateRecord(m *dns.Msg, ep *endpoint.Endpoint) error { func (r rfc2136Provider) UpdateRecord(m *dns.Msg, oldEp *endpoint.Endpoint, newEp *endpoint.Endpoint) error {
err := r.RemoveRecord(m, ep) err := r.RemoveRecord(m, oldEp)
if err != nil { if err != nil {
return err return err
} }
return r.AddRecord(m, ep) return r.AddRecord(m, newEp)
} }
func (r rfc2136Provider) AddRecord(m *dns.Msg, ep *endpoint.Endpoint) error { 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()) msg.SetTsig(r.tsigKeyName, r.tsigSecretAlg, 300, time.Now().Unix())
} }
if msg.Len() > udpMaxMsgSize {
c.Net = "tcp"
}
resp, _, err := c.Exchange(msg, r.nameserver) resp, _, err := c.Exchange(msg, r.nameserver)
if err != nil { if err != nil {
log.Infof("error in dns.Client.Exchange: %s", err) log.Infof("error in dns.Client.Exchange: %s", err)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package rfc2136
import ( import (
"context" "context"
@ -29,6 +29,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type rfc2136Stub struct { type rfc2136Stub struct {
@ -93,7 +94,7 @@ func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelo
return outChan, nil 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) 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")) 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
}

View File

@ -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 ( import (
"context" "context"
@ -12,6 +28,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -22,6 +39,7 @@ const (
// TransIPProvider is an implementation of Provider for TransIP. // TransIPProvider is an implementation of Provider for TransIP.
type TransIPProvider struct { type TransIPProvider struct {
provider.BaseProvider
client gotransip.SOAPClient client gotransip.SOAPClient
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
dryRun bool dryRun bool
@ -72,7 +90,7 @@ func (p *TransIPProvider) ApplyChanges(ctx context.Context, changes *plan.Change
return err return err
} }
zoneNameMapper := zoneIDName{} zoneNameMapper := provider.ZoneIDName{}
zonesByName := make(map[string]transip.Domain) zonesByName := make(map[string]transip.Domain)
updatedZones := make(map[string]bool) updatedZones := make(map[string]bool)
for _, zone := range zones { 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 // go over all zones and their DNS entries and create endpoints for them
for _, zone := range zones { for _, zone := range zones {
for _, r := range zone.DNSEntries { for _, r := range zone.DNSEntries {
if !supportedRecordType(string(r.Type)) { if !provider.SupportedRecordType(string(r.Type)) {
continue 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 // zoneForZoneName returns the zone mapped to given name or error if zone could
// not be found // 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) _, zoneName := m.FindZone(name)
if zoneName == "" { if zoneName == "" {
return transip.Domain{}, fmt.Errorf("could not find zoneName for %s", name) return transip.Domain{}, fmt.Errorf("could not find zoneName for %s", name)

View File

@ -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 ( import (
"testing" "testing"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package vinyldns
import ( import (
"context" "context"
@ -27,6 +27,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -47,8 +48,9 @@ type vinyldnsZoneInterface interface {
} }
type vinyldnsProvider struct { type vinyldnsProvider struct {
provider.BaseProvider
client vinyldnsZoneInterface client vinyldnsZoneInterface
zoneFilter ZoneIDFilter zoneFilter provider.ZoneIDFilter
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
dryRun bool dryRun bool
} }
@ -59,7 +61,7 @@ type vinyldnsChange struct {
} }
// NewVinylDNSProvider provides support for VinylDNS records // 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") _, ok := os.LookupEnv("VINYLDNS_ACCESS_KEY")
if !ok { if !ok {
return nil, fmt.Errorf("no vinyldns access key found") 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 { for _, r := range records {
if supportedRecordType(r.Type) { if provider.SupportedRecordType(r.Type) {
recordsCount := len(r.Records) recordsCount := len(r.Records)
log.Debugf(fmt.Sprintf("%s.%s.%d.%s", r.Name, r.Type, recordsCount, zone.Name)) 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 { func (p *vinyldnsProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package vinyldns
import ( import (
"context" "context"
@ -28,6 +28,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type mockVinyldnsZoneInterface struct { type mockVinyldnsZoneInterface struct {
@ -97,12 +98,12 @@ func testVinylDNSProviderRecords(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, len(vinylDNSRecords), len(result)) assert.Equal(t, len(vinylDNSRecords), len(result))
mockVinylDNSProvider.zoneFilter = NewZoneIDFilter([]string{"0"}) mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"0"})
result, err = mockVinylDNSProvider.Records(ctx) result, err = mockVinylDNSProvider.Records(ctx)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, len(vinylDNSRecords), len(result)) assert.Equal(t, len(vinylDNSRecords), len(result))
mockVinylDNSProvider.zoneFilter = NewZoneIDFilter([]string{"1"}) mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"1"})
result, err = mockVinylDNSProvider.Records(ctx) result, err = mockVinylDNSProvider.Records(ctx)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, len(result)) 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}} 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) err := mockVinylDNSProvider.ApplyChanges(context.Background(), changes)
if err != nil { if err != nil {
t.Errorf("Failed to apply changes: %v", err) t.Errorf("Failed to apply changes: %v", err)
@ -126,7 +127,7 @@ func testVinylDNSProviderApplyChanges(t *testing.T) {
} }
func testVinylDNSSuitableZone(t *testing.T) { func testVinylDNSSuitableZone(t *testing.T) {
mockVinylDNSProvider.zoneFilter = NewZoneIDFilter([]string{"0"}) mockVinylDNSProvider.zoneFilter = provider.NewZoneIDFilter([]string{"0"})
zone := vinyldnsSuitableZone("example.com", vinylDNSZones) zone := vinyldnsSuitableZone("example.com", vinylDNSZones)
assert.Equal(t, zone.Name, "example.com.") assert.Equal(t, zone.Name, "example.com.")
@ -134,11 +135,11 @@ func testVinylDNSSuitableZone(t *testing.T) {
func TestNewVinylDNSProvider(t *testing.T) { func TestNewVinylDNSProvider(t *testing.T) {
os.Setenv("VINYLDNS_ACCESS_KEY", "xxxxxxxxxxxxxxxxxxxxxxxxxx") 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) assert.Nil(t, err)
os.Unsetenv("VINYLDNS_ACCESS_KEY") 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) assert.NotNil(t, err)
if err == nil { if err == nil {
t.Errorf("Expected to fail new provider on empty token") t.Errorf("Expected to fail new provider on empty token")
@ -146,7 +147,7 @@ func TestNewVinylDNSProvider(t *testing.T) {
} }
func testVinylDNSFindRecordSetID(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.") result, err := mockVinylDNSProvider.findRecordSetID("0", "example.com.")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "", result) assert.Equal(t, "", result)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package vultr
import ( import (
"context" "context"
@ -27,6 +27,7 @@ import (
"github.com/vultr/govultr" "github.com/vultr/govultr"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
const ( const (
@ -36,13 +37,16 @@ const (
vultrTTL = 3600 vultrTTL = 3600
) )
// VultrProvider is an implementation of Provider for Vultr DNS.
type VultrProvider struct { type VultrProvider struct {
provider.BaseProvider
client govultr.Client client govultr.Client
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
DryRun bool DryRun bool
} }
// VultrChanges differentiates between ChangActions.
type VultrChanges struct { type VultrChanges struct {
Action string Action string
@ -78,6 +82,7 @@ func (p *VultrProvider) Zones(ctx context.Context) ([]govultr.DNSDomain, error)
return zones, nil return zones, nil
} }
// Records returns the list of records.
func (p *VultrProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { func (p *VultrProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
zones, err := p.Zones(ctx) zones, err := p.Zones(ctx)
if err != nil { if err != nil {
@ -93,7 +98,7 @@ func (p *VultrProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, erro
} }
for _, r := range records { for _, r := range records {
if supportedRecordType(r.Type) { if provider.SupportedRecordType(r.Type) {
name := fmt.Sprintf("%s.%s", r.Name, zone.Domain) name := fmt.Sprintf("%s.%s", r.Name, zone.Domain)
// root name is identified by the empty string and should be // 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 zoneName, changes := range zoneChanges {
for _, change := range changes { for _, change := range changes {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"record": change.ResourceRecordSet.Name, "record": change.ResourceRecordSet.Name,
"type": change.ResourceRecordSet.Type, "type": change.ResourceRecordSet.Type,
@ -197,10 +201,10 @@ func (p *VultrProvider) submitChanges(ctx context.Context, changes []*VultrChang
} }
} }
} }
return nil return nil
} }
// ApplyChanges applies a given set of changes in a given zone.
func (p *VultrProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { func (p *VultrProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
combinedChanges := make([]*VultrChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) 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)) changes := make([]*VultrChanges, 0, len(endpoints))
ttl := vultrTTL ttl := vultrTTL
for _, e := range endpoints { for _, e := range endpoints {
if e.RecordTTL.IsConfigured() { if e.RecordTTL.IsConfigured() {
ttl = int(e.RecordTTL) 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 { func seperateChangesByZone(zones []govultr.DNSDomain, changes []*VultrChanges) map[string][]*VultrChanges {
change := make(map[string][]*VultrChanges) change := make(map[string][]*VultrChanges)
zoneNameID := zoneIDName{} zoneNameID := provider.ZoneIDName{}
for _, z := range zones { for _, z := range zones {
zoneNameID.Add(z.Domain, z.Domain) zoneNameID.Add(z.Domain, z.Domain)
@ -250,7 +253,6 @@ func seperateChangesByZone(zones []govultr.DNSDomain, changes []*VultrChanges) m
continue continue
} }
change[zone] = append(change[zone], c) change[zone] = append(change[zone], c)
} }
return change return change
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package vultr
import ( import (
"context" "context"

View File

@ -20,7 +20,7 @@ import "strings"
// ZoneIDFilter holds a list of zone ids to filter by // ZoneIDFilter holds a list of zone ids to filter by
type ZoneIDFilter struct { type ZoneIDFilter struct {
zoneIDs []string ZoneIDs []string
} }
// NewZoneIDFilter returns a new ZoneIDFilter given a list of zone ids // 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 // Match checks whether a zone matches one of the provided zone ids
func (f ZoneIDFilter) Match(zoneID string) bool { func (f ZoneIDFilter) Match(zoneID string) bool {
// An empty filter includes all zones. // An empty filter includes all zones.
if len(f.zoneIDs) == 0 { if len(f.ZoneIDs) == 0 {
return true return true
} }
for _, id := range f.zoneIDs { for _, id := range f.ZoneIDs {
if strings.HasSuffix(zoneID, id) { if strings.HasSuffix(zoneID, id) {
return true return true
} }

View File

@ -18,13 +18,13 @@ package provider
import "strings" 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 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 { for zoneID, zoneName := range z {
if hostname == zoneName || strings.HasSuffix(hostname, "."+zoneName) { if hostname == zoneName || strings.HasSuffix(hostname, "."+zoneName) {
if suitableZoneName == "" || len(zoneName) > len(suitableZoneName) { if suitableZoneName == "" || len(zoneName) > len(suitableZoneName) {

View File

@ -23,12 +23,12 @@ import (
) )
func TestZoneIDName(t *testing.T) { func TestZoneIDName(t *testing.T) {
z := zoneIDName{} z := ZoneIDName{}
z.Add("123456", "foo.bar") z.Add("123456", "foo.bar")
z.Add("123456", "qux.baz") z.Add("123456", "qux.baz")
z.Add("654321", "foo.qux.baz") z.Add("654321", "foo.qux.baz")
assert.Equal(t, zoneIDName{ assert.Equal(t, ZoneIDName{
"123456": "qux.baz", "123456": "qux.baz",
"654321": "foo.qux.baz", "654321": "foo.qux.baz",
}, z) }, z)

View File

@ -87,3 +87,7 @@ func (sdr *AWSSDRegistry) updateLabels(endpoints []*endpoint.Endpoint) {
ep.Labels[endpoint.AWSSDDescriptionLabel] = ep.Labels.Serialize(false) 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)
}

View File

@ -26,9 +26,11 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
) )
type inMemoryProvider struct { type inMemoryProvider struct {
provider.BaseProvider
endpoints []*endpoint.Endpoint endpoints []*endpoint.Endpoint
onApplyChanges func(changes *plan.Changes) onApplyChanges func(changes *plan.Changes)
} }

View File

@ -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 { func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
return im.provider.ApplyChanges(ctx, changes) 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)
}

View File

@ -26,7 +26,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/provider/inmemory"
) )
var _ Registry = &NoopRegistry{} var _ Registry = &NoopRegistry{}
@ -38,7 +38,7 @@ func TestNoopRegistry(t *testing.T) {
} }
func testNoopInit(t *testing.T) { func testNoopInit(t *testing.T) {
p := provider.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
r, err := NewNoopRegistry(p) r, err := NewNoopRegistry(p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, p, r.provider) assert.Equal(t, p, r.provider)
@ -46,9 +46,9 @@ func testNoopInit(t *testing.T) {
func testNoopRecords(t *testing.T) { func testNoopRecords(t *testing.T) {
ctx := context.Background() ctx := context.Background()
p := provider.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
p.CreateZone("org") p.CreateZone("org")
providerRecords := []*endpoint.Endpoint{ inmemoryRecords := []*endpoint.Endpoint{
{ {
DNSName: "example.org", DNSName: "example.org",
Targets: endpoint.Targets{"example-lb.com"}, Targets: endpoint.Targets{"example-lb.com"},
@ -56,21 +56,21 @@ func testNoopRecords(t *testing.T) {
}, },
} }
p.ApplyChanges(ctx, &plan.Changes{ p.ApplyChanges(ctx, &plan.Changes{
Create: providerRecords, Create: inmemoryRecords,
}) })
r, _ := NewNoopRegistry(p) r, _ := NewNoopRegistry(p)
eps, err := r.Records(ctx) eps, err := r.Records(ctx)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, testutils.SameEndpoints(eps, providerRecords)) assert.True(t, testutils.SameEndpoints(eps, inmemoryRecords))
} }
func testNoopApplyChanges(t *testing.T) { func testNoopApplyChanges(t *testing.T) {
// do some prep // do some prep
p := provider.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
p.CreateZone("org") p.CreateZone("org")
providerRecords := []*endpoint.Endpoint{ inmemoryRecords := []*endpoint.Endpoint{
{ {
DNSName: "example.org", DNSName: "example.org",
Targets: endpoint.Targets{"old-lb.com"}, Targets: endpoint.Targets{"old-lb.com"},
@ -92,7 +92,7 @@ func testNoopApplyChanges(t *testing.T) {
ctx := context.Background() ctx := context.Background()
p.ApplyChanges(ctx, &plan.Changes{ p.ApplyChanges(ctx, &plan.Changes{
Create: providerRecords, Create: inmemoryRecords,
}) })
// wrong changes // 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 //correct changes
require.NoError(t, r.ApplyChanges(ctx, &plan.Changes{ require.NoError(t, r.ApplyChanges(ctx, &plan.Changes{

View File

@ -32,6 +32,7 @@ import (
type Registry interface { type Registry interface {
Records(ctx context.Context) ([]*endpoint.Endpoint, error) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
ApplyChanges(ctx context.Context, changes *plan.Changes) error ApplyChanges(ctx context.Context, changes *plan.Changes) error
PropertyValuesEqual(attribute string, previous string, current string) bool
} }
//TODO(ideahitme): consider moving this to Plan //TODO(ideahitme): consider moving this to Plan

View File

@ -43,12 +43,16 @@ type TXTRegistry struct {
} }
// NewTXTRegistry returns new TXTRegistry object // 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 == "" { if ownerID == "" {
return nil, errors.New("owner id cannot be empty") 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{ return &TXTRegistry{
provider: provider, provider: provider,
@ -187,6 +191,11 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
return im.provider.ApplyChanges(ctx, filteredChanges) 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 TXT registry specific private methods
*/ */
@ -201,26 +210,35 @@ type nameMapper interface {
toTXTName(string) string toTXTName(string) string
} }
type prefixNameMapper struct { type affixNameMapper struct {
prefix string prefix string
suffix string
} }
var _ nameMapper = prefixNameMapper{} var _ nameMapper = affixNameMapper{}
func newPrefixNameMapper(prefix string) prefixNameMapper { func newaffixNameMapper(prefix string, suffix string) affixNameMapper {
return prefixNameMapper{prefix: strings.ToLower(prefix)} 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) 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) 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 "" return ""
} }
func (pr prefixNameMapper) toTXTName(endpointDNSName string) string { func (pr affixNameMapper) toTXTName(endpointDNSName string) string {
return pr.prefix + endpointDNSName DNSName := strings.SplitN(endpointDNSName, ".", 2)
return pr.prefix + DNSName[0] + pr.suffix + "." + DNSName[1]
} }
func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) { func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) {

View File

@ -29,6 +29,7 @@ import (
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/provider/inmemory"
) )
const ( const (
@ -42,33 +43,44 @@ func TestTXTRegistry(t *testing.T) {
} }
func testTXTRegistryNew(t *testing.T) { func testTXTRegistryNew(t *testing.T) {
p := provider.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
_, err := NewTXTRegistry(p, "txt", "", time.Hour) _, err := NewTXTRegistry(p, "txt", "", "", time.Hour)
require.Error(t, err) 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) 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) require.True(t, ok)
assert.Equal(t, "owner", r.ownerID) assert.Equal(t, "owner", r.ownerID)
assert.Equal(t, p, r.provider) assert.Equal(t, p, r.provider)
r, err = NewTXTRegistry(p, "", "owner", time.Hour) r, err = NewTXTRegistry(p, "", "", "owner", time.Hour)
require.NoError(t, err) require.NoError(t, err)
_, ok = r.mapper.(prefixNameMapper) _, ok = r.mapper.(affixNameMapper)
assert.True(t, ok) assert.True(t, ok)
} }
func testTXTRegistryRecords(t *testing.T) { func testTXTRegistryRecords(t *testing.T) {
t.Run("With prefix", testTXTRegistryRecordsPrefixed) t.Run("With prefix", testTXTRegistryRecordsPrefixed)
t.Run("With suffix", testTXTRegistryRecordsSuffixed)
t.Run("No prefix", testTXTRegistryRecordsNoPrefix) t.Run("No prefix", testTXTRegistryRecordsNoPrefix)
} }
func testTXTRegistryRecordsPrefixed(t *testing.T) { func testTXTRegistryRecordsPrefixed(t *testing.T) {
ctx := context.Background() ctx := context.Background()
p := provider.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone) p.CreateZone(testZone)
p.ApplyChanges(ctx, &plan.Changes{ p.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{ 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) records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive // 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) records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
} }
func testTXTRegistryRecordsNoPrefix(t *testing.T) { func testTXTRegistryRecordsNoPrefix(t *testing.T) {
p := provider.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
ctx := context.Background() ctx := context.Background()
p.CreateZone(testZone) p.CreateZone(testZone)
p.ApplyChanges(ctx, &plan.Changes{ 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) records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -248,11 +365,12 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
func testTXTRegistryApplyChanges(t *testing.T) { func testTXTRegistryApplyChanges(t *testing.T) {
t.Run("With Prefix", testTXTRegistryApplyChangesWithPrefix) t.Run("With Prefix", testTXTRegistryApplyChangesWithPrefix)
t.Run("With Suffix", testTXTRegistryApplyChangesWithSuffix)
t.Run("No prefix", testTXTRegistryApplyChangesNoPrefix) t.Run("No prefix", testTXTRegistryApplyChangesNoPrefix)
} }
func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
p := provider.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone) p.CreateZone(testZone)
ctxEndpoints := []*endpoint.Endpoint{} ctxEndpoints := []*endpoint.Endpoint{}
ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) 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"), 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{ changes := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
@ -342,8 +460,99 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
require.NoError(t, err) 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) { func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
p := provider.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone) p.CreateZone(testZone)
ctxEndpoints := []*endpoint.Endpoint{} ctxEndpoints := []*endpoint.Endpoint{}
ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) 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, ""), 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{ changes := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
@ -485,11 +694,9 @@ func newEndpointWithOwner(dnsName, target, recordType, ownerID string) *endpoint
func newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID string, labels endpoint.Labels) *endpoint.Endpoint { func newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID string, labels endpoint.Labels) *endpoint.Endpoint {
e := endpoint.NewEndpoint(dnsName, recordType, target) e := endpoint.NewEndpoint(dnsName, recordType, target)
e.Labels[endpoint.OwnerLabelKey] = ownerID e.Labels[endpoint.OwnerLabelKey] = ownerID
if labels != nil {
for k, v := range labels { for k, v := range labels {
e.Labels[k] = v e.Labels[k] = v
} }
}
return e return e
} }

View File

@ -17,8 +17,8 @@ limitations under the License.
package source package source
import ( import (
"context"
"net/url" "net/url"
"time"
cfclient "github.com/cloudfoundry-community/go-cfclient" cfclient "github.com/cloudfoundry-community/go-cfclient"
@ -36,7 +36,7 @@ func NewCloudFoundrySource(cfClient *cfclient.Client) (Source, error) {
}, nil }, 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 // Endpoints returns endpoint objects

View File

@ -17,6 +17,7 @@ limitations under the License.
package source package source
import ( import (
"context"
"encoding/gob" "encoding/gob"
"net" "net"
"time" "time"
@ -65,5 +66,5 @@ func (cs *connectorSource) Endpoints() ([]*endpoint.Endpoint, error) {
return endpoints, nil 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()) {
} }

View File

@ -17,10 +17,10 @@ limitations under the License.
package source package source
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -92,7 +92,7 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, kube
config.ContentConfig.GroupVersion = &groupVersion config.ContentConfig.GroupVersion = &groupVersion
config.APIPath = "/apis" config.APIPath = "/apis"
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)} config.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)}
crdClient, err := rest.UnversionedRESTClientFor(config) crdClient, err := rest.UnversionedRESTClientFor(config)
if err != nil { if err != nil {
@ -112,7 +112,7 @@ func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFi
}, nil }, 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. // Endpoints returns endpoint objects.

Some files were not shown because too many files have changed in this diff Show More