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:
exhaustive:
default-signifies-exhaustive: false
goimports:
local-prefixes: github.com/kubernetes-sigs/external-dns
golint:
min-confidence: 0.9
gocyclo:
min-complexity: 15
maligned:
suggest-new: true
misspell:
locale: US
linters:
# please, do not use `enable-all`: it's deprecated and will be removed soon.
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true
enable:
- deadcode
- depguard
- dogsled
- gofmt
- goimports
- golint
- goprintffuncname
- gosimple
- govet
- ineffassign
- golint
- goimports
- misspell
- unconvert
- megacheck
- interfacer
- misspell
- rowserrcheck
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace
issues:
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
- path: _test\.go
linters:
- deadcode
- depguard
- dogsled
- gofmt
- goimports
- golint
- goprintffuncname
- gosimple
- govet
- ineffassign
- interfacer
- misspell
- nolintlint
- rowserrcheck
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace
run:
skip-files:
- endpoint/zz_generated.deepcopy.go

View File

@ -6,23 +6,35 @@ os:
language: go
go:
- "1.13.x"
- "1.14.x"
- tip
matrix:
allow_failures:
- go: tip
env:
- GOLANGCI_RELEASE="v1.23.1"
before_install:
- GO111MODULE=off go get github.com/mattn/goveralls
- GO111MODULE=off go get github.com/lawrencewoodman/roveralls
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_RELEASE}
cache:
directories:
- $GOPATH/pkg/mod
script:
- make test
- make lint
- travis_wait 20 roveralls
- goveralls -coverprofile=roveralls.coverprofile -service=travis-ci
jobs:
include:
- name: "Linting"
go: "1.14.x"
env:
- GOLANGCI_RELEASE="v1.26.0"
before_install:
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_RELEASE}
script:
- make lint
- name: "Coverage"
go: "1.14.x"
before_install:
- GO111MODULE=off go get github.com/mattn/goveralls
- GO111MODULE=off go get github.com/lawrencewoodman/roveralls
script:
- travis_wait 20 roveralls
- goveralls -coverprofile=roveralls.coverprofile -service=travis-ci

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
- 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
# limitations under the License.
FROM golang:1.13 as builder
FROM golang:1.14 as builder
WORKDIR /sigs.k8s.io/external-dns

View File

@ -31,14 +31,14 @@ cover-html: cover
# Run all the linters
lint:
golangci-lint run --timeout=5m ./...
golangci-lint run --timeout=15m ./...
# The verify target runs tasks similar to the CI tasks, but without code coverage
.PHONY: verify test
test:
go test -v -race $(shell go list ./... | grep -v /vendor/)
go test -race ./...
# The build targets allow to build the binary and docker image
.PHONY: build build.docker build.mini

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.
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

View File

@ -18,6 +18,7 @@ package controller
import (
"context"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
@ -112,6 +113,10 @@ type Controller struct {
Interval time.Duration
// The DomainFilter defines which DNS records to keep or exclude
DomainFilter endpoint.DomainFilter
// The nextRunAt used for throttling and batching reconciliation
nextRunAt time.Time
// The nextRunAtMux is for atomic updating of nextRunAt
nextRunAtMux sync.Mutex
}
// RunOnce runs a single iteration of a reconciliation loop.
@ -139,6 +144,7 @@ func (c *Controller) RunOnce(ctx context.Context) error {
Current: records,
Desired: endpoints,
DomainFilter: c.DomainFilter,
PropertyComparator: c.Registry.PropertyValuesEqual,
}
plan = plan.Calculate()
@ -154,18 +160,39 @@ func (c *Controller) RunOnce(ctx context.Context) error {
return nil
}
// Run runs RunOnce in a loop with a delay until stopChan receives a value.
func (c *Controller) Run(ctx context.Context, stopChan <-chan struct{}) {
ticker := time.NewTicker(c.Interval)
// MinInterval is used as window for batching events
const MinInterval = 5 * time.Second
// RunOnceThrottled makes sure execution happens at most once per interval.
func (c *Controller) ScheduleRunOnce(now time.Time) {
c.nextRunAtMux.Lock()
defer c.nextRunAtMux.Unlock()
c.nextRunAt = now.Add(MinInterval)
}
func (c *Controller) ShouldRunOnce(now time.Time) bool {
c.nextRunAtMux.Lock()
defer c.nextRunAtMux.Unlock()
if now.Before(c.nextRunAt) {
return false
}
c.nextRunAt = now.Add(c.Interval)
return true
}
// Run runs RunOnce in a loop with a delay until context is canceled
func (c *Controller) Run(ctx context.Context) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
err := c.RunOnce(ctx)
if err != nil {
if c.ShouldRunOnce(time.Now()) {
if err := c.RunOnce(ctx); err != nil {
log.Error(err)
}
}
select {
case <-ticker.C:
case <-stopChan:
case <-ctx.Done():
log.Info("Terminating main controller loop")
return
}

View File

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

View File

@ -51,7 +51,7 @@ PRs welcome!
Notes
=====
When the `external-dns.alpha.kubernetes.io/ttl` annotation is not provided, the TTL will default to 0 seconds and `enpoint.TTL.isConfigured()` will be false.
When the `external-dns.alpha.kubernetes.io/ttl` annotation is not provided, the TTL will default to 0 seconds and `endpoint.TTL.isConfigured()` will be false.
### AWS Provider
The AWS Provider overrides the value to 300s when the TTL is 0.

View File

@ -36,9 +36,6 @@ spec:
app: external-dns
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
@ -102,9 +99,6 @@ spec:
app: external-dns
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
@ -195,3 +189,13 @@ Now that we have verified that ExternalDNS will automatically manage DigitalOcea
$ kubectl delete service -f nginx.yaml
$ kubectl delete service -f externaldns.yaml
```
## Advanced Usage
### API Page Size
If you have a large number of domains and/or records within a domain, you may encounter API
rate limiting because of the number of API calls that external-dns must make to the DigitalOcean API to retrieve
the current DNS configuration during every reconciliation loop. If this is the case, use the
`--digitalocean-api-page-size` option to increase the size of the pages used when querying the DigitalOcean API.
(Note: external-dns uses a default of 50.)

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.
### Exernal DNS
### External DNS
A simple deploy could look like this:
### Manifest (for clusters without RBAC enabled)
@ -17,7 +17,7 @@ A simple deploy could look like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: exeternal-dns
name: external-dns
spec:
strategy:
type: Recreate
@ -81,7 +81,7 @@ subjects:
apiVersion: apps/v1
kind: Deployment
metadata:
name: exeternal-dns
name: external-dns
spec:
strategy:
type: Recreate
@ -111,7 +111,7 @@ spec:
### Kafka Stateful Set
First lets deploy a Kafka Stateful set, a simple example(a lot of stuff is missing) with a headless service called `kafka-hsvc`
First lets deploy a Kafka Stateful set, a simple example(a lot of stuff is missing) with a headless service called `ksvc`
```yaml
apiVersion: apps/v1beta1
@ -155,7 +155,7 @@ spec:
requests:
storage: 500Gi
```
Very important here, is to set the `hostport`(only works if the PodSecurityPolicy allows it)! and in case your app requires an actual hostname inside the container, unlike Kafka, which can advertise on another address, you have to set the hostname yourself.
Very important here, is to set the `hostPort`(only works if the PodSecurityPolicy allows it)! and in case your app requires an actual hostname inside the container, unlike Kafka, which can advertise on another address, you have to set the hostname yourself.
### Headless Service

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 with RBAC enabled)
* Update existing Existing DNS Deployment
* Update existing ExternalDNS Deployment
### Manifest (for clusters without RBAC enabled)
@ -48,7 +48,7 @@ kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
@ -66,7 +66,7 @@ rules:
resources: ["gateways"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
@ -110,7 +110,7 @@ spec:
- --txt-owner-id=my-identifier
```
### Update existing Existing DNS Deployment
### Update existing ExternalDNS Deployment
* For clusters with running `external-dns`, you can just update the deployment.
* With access to the `kube-system` namespace, update the existing `external-dns` deployment.
@ -130,7 +130,7 @@ kubectl patch clusterrole external-dns --type='json' \
-p='[{"op": "add", "path": "/rules/4", "value": { "apiGroups": [ "networking.istio.io"], "resources": ["gateways"],"verbs": ["get", "watch", "list" ]} }]'
```
### Verify External DNS works (Gateway example)
### Verify ExternalDNS works (Gateway example)
Follow the [Istio ingress traffic tutorial](https://istio.io/docs/tasks/traffic-management/ingress/)
to deploy a sample service that will be exposed outside of the service mesh.
@ -217,7 +217,7 @@ transfer-encoding: chunked
**Note:** The `-H` flag in the original Istio tutorial is no longer necessary in the `curl` commands.
### Debug External-DNS
### Debug ExternalDNS
* Look for the deployment pod to see the status
@ -239,8 +239,8 @@ At this point, you can `create` or `update` any `Istio Gateway` object with `hos
```console
time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.com A"
time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.comm TXT"
time="2020-01-17T06:08:08Z" level=info msg="2 record(s) in zone example.comm. were successfully updated"
time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.com TXT"
time="2020-01-17T06:08:08Z" level=info msg="2 record(s) in zone example.com. were successfully updated"
time="2020-01-17T06:09:08Z" level=info msg="All records are already up to date, there are no changes for the matching hosted zones"
```

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 {
e.SetIdentifier = setIdentifier
return e

57
go.mod
View File

@ -3,46 +3,46 @@ module sigs.k8s.io/external-dns
go 1.14
require (
cloud.google.com/go v0.44.3
cloud.google.com/go v0.50.0
github.com/Azure/azure-sdk-for-go v36.0.0+incompatible
github.com/Azure/go-autorest/autorest v0.9.0
github.com/Azure/go-autorest/autorest/adal v0.6.0
github.com/Azure/go-autorest/autorest v0.9.4
github.com/Azure/go-autorest/autorest/adal v0.8.3
github.com/Azure/go-autorest/autorest/azure/auth v0.0.0-00010101000000-000000000000
github.com/Azure/go-autorest/autorest/to v0.3.0
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
github.com/alecthomas/colour v0.1.0 // indirect
github.com/alecthomas/kingpin v2.2.5+incompatible
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f
github.com/aws/aws-sdk-go v1.27.4
github.com/aws/aws-sdk-go v1.31.4
github.com/cloudflare/cloudflare-go v0.10.1
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/denverdino/aliyungo v0.0.0-20180815121905-69560d9530f5
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
github.com/digitalocean/godo v1.34.0
github.com/dnaeon/go-vcr v1.0.1 // indirect
github.com/dnsimple/dnsimple-go v0.14.0
github.com/dnsimple/dnsimple-go v0.60.0
github.com/exoscale/egoscale v0.18.1
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
github.com/go-resty/resty v1.8.0 // indirect
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b // indirect
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f
github.com/gophercloud/gophercloud v0.1.0
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/heptio/contour v0.15.0
github.com/gorilla/mux v1.7.4 // indirect
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65
github.com/linki/instrumented_http v0.2.0
github.com/linode/linodego v0.3.0
github.com/mattn/go-isatty v0.0.11 // indirect
github.com/linode/linodego v0.15.0
github.com/maxatome/go-testdeep v1.4.0
github.com/miekg/dns v1.1.25
github.com/nesv/go-dynect v0.6.0
github.com/nic-at/rc0go v1.1.0
github.com/openshift/api v0.0.0-20190322043348-8741ff068a47
github.com/openshift/client-go v3.9.0+incompatible
github.com/openshift/api v0.0.0-20200302134843-001335d6cc34
github.com/openshift/client-go v0.0.0-20200116145930-eb24d03d8420
github.com/oracle/oci-go-sdk v1.8.0
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.0.0
github.com/projectcontour/contour v1.4.0
github.com/prometheus/client_golang v1.1.0
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0
github.com/sergi/go-diff v1.1.0 // indirect
github.com/sirupsen/logrus v1.6.0
@ -50,22 +50,27 @@ require (
github.com/smartystreets/gunit v1.1.1 // indirect
github.com/stretchr/testify v1.4.0
github.com/terra-farm/udnssdk v1.3.5 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/smartystreets/gunit v1.3.4 // indirect
github.com/stretchr/testify v1.5.1
github.com/transip/gotransip v5.8.2+incompatible
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92
github.com/vultr/govultr v0.3.2
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875
golang.org/x/net v0.0.0-20190923162816-aa69164e4478
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
google.golang.org/api v0.9.0
google.golang.org/api v0.15.0
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
gopkg.in/yaml.v2 v2.2.5
istio.io/api v0.0.0-20190820204432-483f2547d882
istio.io/istio v0.0.0-20190322063008-2b1331886076
k8s.io/api v0.0.0-20190620084959-7cf5895f2711
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
k8s.io/client-go v10.0.0+incompatible
k8s.io/klog v0.3.1
gopkg.in/yaml.v2 v2.2.8
istio.io/api v0.0.0-20200324230725-4b064f75ad8f
istio.io/client-go v0.0.0-20200324231043-96a582576da1
k8s.io/api v0.17.5
k8s.io/apimachinery v0.17.5
k8s.io/client-go v0.17.5
k8s.io/klog v1.0.0
)
replace (
@ -74,11 +79,5 @@ replace (
github.com/Azure/go-autorest/autorest/adal => github.com/Azure/go-autorest/autorest/adal v0.6.0
github.com/Azure/go-autorest/autorest/azure/auth => github.com/Azure/go-autorest/autorest/azure/auth v0.3.0
github.com/golang/glog => github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d
istio.io/api => istio.io/api v0.0.0-20190820204432-483f2547d882
istio.io/istio => istio.io/istio v0.0.0-20190911205955-c2bd59595ce6
k8s.io/api => k8s.io/api v0.0.0-20190817221950-ebce17126a01
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190919022157-e8460a76b3ad
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190817221809-bf4de9df677c
k8s.io/client-go => k8s.io/client-go v0.0.0-20190817222206-ee6c071a42cf
k8s.io/klog => github.com/mikkeloscar/knolog v0.0.0-20190326191552-80742771eb6b
)

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

132
main.go
View File

@ -28,6 +28,32 @@ import (
log "github.com/sirupsen/logrus"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"sigs.k8s.io/external-dns/provider/akamai"
"sigs.k8s.io/external-dns/provider/alibabacloud"
"sigs.k8s.io/external-dns/provider/aws"
"sigs.k8s.io/external-dns/provider/awssd"
"sigs.k8s.io/external-dns/provider/azure"
"sigs.k8s.io/external-dns/provider/cloudflare"
"sigs.k8s.io/external-dns/provider/coredns"
"sigs.k8s.io/external-dns/provider/designate"
"sigs.k8s.io/external-dns/provider/digitalocean"
"sigs.k8s.io/external-dns/provider/dnsimple"
"sigs.k8s.io/external-dns/provider/dyn"
"sigs.k8s.io/external-dns/provider/exoscale"
"sigs.k8s.io/external-dns/provider/google"
"sigs.k8s.io/external-dns/provider/infoblox"
"sigs.k8s.io/external-dns/provider/inmemory"
"sigs.k8s.io/external-dns/provider/linode"
"sigs.k8s.io/external-dns/provider/ns1"
"sigs.k8s.io/external-dns/provider/oci"
"sigs.k8s.io/external-dns/provider/ovh"
"sigs.k8s.io/external-dns/provider/pdns"
"sigs.k8s.io/external-dns/provider/rcode0"
"sigs.k8s.io/external-dns/provider/rdns"
"sigs.k8s.io/external-dns/provider/rfc2136"
"sigs.k8s.io/external-dns/provider/transip"
"sigs.k8s.io/external-dns/provider/vinyldns"
"sigs.k8s.io/external-dns/provider/vultr"
"sigs.k8s.io/external-dns/controller"
"sigs.k8s.io/external-dns/endpoint"
@ -63,12 +89,10 @@ func main() {
}
log.SetLevel(ll)
ctx := context.Background()
stopChan := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(context.Background())
go serveMetrics(cfg.MetricsAddress)
go handleSigterm(stopChan)
go handleSigterm(cancel)
// Create a source.Config from the flags passed by the user.
sourceCfg := &source.Config{
@ -123,8 +147,8 @@ func main() {
var p provider.Provider
switch cfg.Provider {
case "akamai":
p = provider.NewAkamaiProvider(
provider.AkamaiConfig{
p = akamai.NewAkamaiProvider(
akamai.AkamaiConfig{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
ServiceConsumerDomain: cfg.AkamaiServiceConsumerDomain,
@ -135,10 +159,10 @@ func main() {
},
)
case "alibabacloud":
p, err = provider.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
case "aws":
p, err = provider.NewAWSProvider(
provider.AWSConfig{
p, err = aws.NewAWSProvider(
aws.AWSConfig{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
ZoneTypeFilter: zoneTypeFilter,
@ -158,36 +182,34 @@ func main() {
log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
cfg.Registry = "aws-sd"
}
p, err = provider.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun)
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun)
case "azure-dns", "azure":
p, err = provider.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "azure-private-dns":
p, err = provider.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun)
p, err = azure.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun)
case "vinyldns":
p, err = provider.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "vultr":
p, err = provider.NewVultrProvider(domainFilter, cfg.DryRun)
case "ultradns":
p, err = provider.NewUltraDNSProvider(domainFilter, cfg.DryRun )
case "cloudflare":
p, err = provider.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun)
p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun)
case "rcodezero":
p, err = provider.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt)
p, err = rcode0.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt)
case "google":
p, err = provider.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.DryRun)
p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.DryRun)
case "digitalocean":
p, err = provider.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun)
p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize)
case "ovh":
p, err = provider.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.DryRun)
p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.DryRun)
case "linode":
p, err = provider.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
case "dnsimple":
p, err = provider.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "infoblox":
p, err = provider.NewInfobloxProvider(
provider.InfobloxConfig{
p, err = infoblox.NewInfobloxProvider(
infoblox.InfobloxConfig{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
Host: cfg.InfobloxGridHost,
@ -202,8 +224,8 @@ func main() {
},
)
case "dyn":
p, err = provider.NewDynProvider(
provider.DynConfig{
p, err = dyn.NewDynProvider(
dyn.DynConfig{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
DryRun: cfg.DryRun,
@ -215,29 +237,29 @@ func main() {
},
)
case "coredns", "skydns":
p, err = provider.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
case "rdns":
p, err = provider.NewRDNSProvider(
provider.RDNSConfig{
p, err = rdns.NewRDNSProvider(
rdns.RDNSConfig{
DomainFilter: domainFilter,
DryRun: cfg.DryRun,
},
)
case "exoscale":
p, err = provider.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, cfg.DryRun, provider.ExoscaleWithDomain(domainFilter), provider.ExoscaleWithLogging()), nil
p, err = exoscale.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, cfg.DryRun, exoscale.ExoscaleWithDomain(domainFilter), exoscale.ExoscaleWithLogging()), nil
case "inmemory":
p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil
p, err = inmemory.NewInMemoryProvider(inmemory.InMemoryInitZones(cfg.InMemoryZones), inmemory.InMemoryWithDomain(domainFilter), inmemory.InMemoryWithLogging()), nil
case "designate":
p, err = provider.NewDesignateProvider(domainFilter, cfg.DryRun)
p, err = designate.NewDesignateProvider(domainFilter, cfg.DryRun)
case "pdns":
p, err = provider.NewPDNSProvider(
p, err = pdns.NewPDNSProvider(
ctx,
provider.PDNSConfig{
pdns.PDNSConfig{
DomainFilter: domainFilter,
DryRun: cfg.DryRun,
Server: cfg.PDNSServer,
APIKey: cfg.PDNSAPIKey,
TLSConfig: provider.TLSConfig{
TLSConfig: pdns.TLSConfig{
TLSEnabled: cfg.PDNSTLSEnabled,
CAFilePath: cfg.TLSCA,
ClientCertFilePath: cfg.TLSClientCert,
@ -246,16 +268,16 @@ func main() {
},
)
case "oci":
var config *provider.OCIConfig
config, err = provider.LoadOCIConfig(cfg.OCIConfigFile)
var config *oci.OCIConfig
config, err = oci.LoadOCIConfig(cfg.OCIConfigFile)
if err == nil {
p, err = provider.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
}
case "rfc2136":
p, err = provider.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, nil)
p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, nil)
case "ns1":
p, err = provider.NewNS1Provider(
provider.NS1Config{
p, err = ns1.NewNS1Provider(
ns1.NS1Config{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
NS1Endpoint: cfg.NS1Endpoint,
@ -264,7 +286,7 @@ func main() {
},
)
case "transip":
p, err = provider.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun)
p, err = transip.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}
@ -277,9 +299,9 @@ func main() {
case "noop":
r, err = registry.NewNoopRegistry(p)
case "txt":
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTOwnerID, cfg.TXTCacheInterval)
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval)
case "aws-sd":
r, err = registry.NewAWSSDRegistry(p.(*provider.AWSSDProvider), cfg.TXTOwnerID)
r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID)
default:
log.Fatalf("unknown registry: %s", cfg.Registry)
}
@ -301,13 +323,6 @@ func main() {
DomainFilter: domainFilter,
}
if cfg.UpdateEvents {
// Add RunOnce as the handler function that will be called when ingress/service sources have changed.
// Note that k8s Informers will perform an initial list operation, which results in the handler
// function initially being called for every Service/Ingress that exists limted by minInterval.
ctrl.Source.AddEventHandler(func() error { return ctrl.RunOnce(ctx) }, stopChan, 1*time.Minute)
}
if cfg.Once {
err := ctrl.RunOnce(ctx)
if err != nil {
@ -316,15 +331,24 @@ func main() {
os.Exit(0)
}
ctrl.Run(ctx, stopChan)
if cfg.UpdateEvents {
// Add RunOnce as the handler function that will be called when ingress/service sources have changed.
// Note that k8s Informers will perform an initial list operation, which results in the handler
// function initially being called for every Service/Ingress that exists
ctrl.Source.AddEventHandler(ctx, func() { ctrl.ScheduleRunOnce(time.Now()) })
}
ctrl.ScheduleRunOnce(time.Now())
ctrl.Run(ctx)
}
func handleSigterm(stopChan chan struct{}) {
func handleSigterm(cancel func()) {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGTERM)
<-signals
log.Info("Received SIGTERM. Terminating...")
close(stopChan)
cancel()
}
func serveMetrics(address string) {

View File

@ -109,6 +109,7 @@ type Config struct {
Registry string
TXTOwnerID string
TXTPrefix string
TXTSuffix string
Interval time.Duration
Once bool
DryRun bool
@ -139,6 +140,7 @@ type Config struct {
NS1IgnoreSSL bool
TransIPAccountName string
TransIPPrivateKeyFile string
DigitalOceanAPIPageSize int
}
var defaultConfig = &Config{
@ -205,6 +207,7 @@ var defaultConfig = &Config{
Registry: "txt",
TXTOwnerID: "default",
TXTPrefix: "",
TXTSuffix: "",
TXTCacheInterval: 0,
Interval: time.Minute,
Once: false,
@ -235,6 +238,7 @@ var defaultConfig = &Config{
NS1IgnoreSSL: false,
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
DigitalOceanAPIPageSize: 50,
}
// NewConfig returns new Config object
@ -361,6 +365,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("pdns-tls-enabled", "When using the PowerDNS/PDNS provider, specify whether to use TLS (default: false, requires --tls-ca, optionally specify --tls-client-cert and --tls-client-cert-key)").Default(strconv.FormatBool(defaultConfig.PDNSTLSEnabled)).BoolVar(&cfg.PDNSTLSEnabled)
app.Flag("ns1-endpoint", "When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/)").Default(defaultConfig.NS1Endpoint).StringVar(&cfg.NS1Endpoint)
app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL)
app.Flag("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIPageSize)).IntVar(&cfg.DigitalOceanAPIPageSize)
// Flags related to TLS communication
app.Flag("tls-ca", "When using TLS communication, the path to the certificate authority to verify server communications (optionally specify --tls-client-cert for two-way TLS)").Default(defaultConfig.TLSCA).StringVar(&cfg.TLSCA)
@ -392,7 +397,8 @@ func (cfg *Config) ParseFlags(args []string) error {
// Flags related to the registry
app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "aws-sd")
app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional)").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix)
// Flags related to the main control loop
app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)

View File

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

View File

@ -91,5 +91,10 @@ func ValidateConfig(cfg *externaldns.Config) error {
if cfg.IgnoreHostnameAnnotation && cfg.FQDNTemplate == "" {
return errors.New("FQDN Template must be set if ignoring annotations")
}
if len(cfg.TXTPrefix) > 0 && len(cfg.TXTSuffix) > 0 {
return errors.New("txt-prefix and txt-suffix are mutual exclusive")
}
return nil
}

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 (
"fmt"
"strconv"
"strings"
"sigs.k8s.io/external-dns/endpoint"
)
// PropertyComparator is used in Plan for comparing the previous and current custom annotations.
type PropertyComparator func(name string, previous string, current string) bool
// Plan can convert a list of desired and current records to a series of create,
// update and delete actions.
type Plan struct {
@ -37,6 +41,8 @@ type Plan struct {
Changes *Changes
// DomainFilter matches DNS names
DomainFilter endpoint.DomainFilter
// Property comparator compares custom properties of providers
PropertyComparator PropertyComparator
}
// Changes holds lists of actions to be executed by dns providers
@ -135,7 +141,7 @@ func (p *Plan) Calculate() *Plan {
if row.current != nil && len(row.candidates) > 0 { //dns name is taken
update := t.resolver.ResolveUpdate(row.current, row.candidates)
// compare "update" to "current" to figure out if actual update is required
if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || shouldUpdateProviderSpecific(update, row.current) {
if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || p.shouldUpdateProviderSpecific(update, row.current) {
inheritOwner(row.current, update)
changes.UpdateNew = append(changes.UpdateNew, update)
changes.UpdateOld = append(changes.UpdateOld, row.current)
@ -178,10 +184,15 @@ func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool {
return desired.RecordTTL != current.RecordTTL
}
func shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool {
if current.ProviderSpecific == nil && len(desired.ProviderSpecific) == 0 {
return false
func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool {
desiredProperties := map[string]endpoint.ProviderSpecificProperty{}
if desired.ProviderSpecific != nil {
for _, d := range desired.ProviderSpecific {
desiredProperties[d.Name] = d
}
}
if current.ProviderSpecific != nil {
for _, c := range current.ProviderSpecific {
// don't consider target health when detecting changes
// see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954
@ -189,33 +200,23 @@ func shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool {
continue
}
found := false
for _, d := range desired.ProviderSpecific {
if d.Name == c.Name {
if d.Value != c.Value {
// provider-specific attribute updated
if d, ok := desiredProperties[c.Name]; ok {
if p.PropertyComparator != nil {
if !p.PropertyComparator(c.Name, c.Value, d.Value) {
return true
}
found = true
break
} else if c.Value != d.Value {
return true
}
} else {
if p.PropertyComparator != nil {
if !p.PropertyComparator(c.Name, c.Value, "") {
return true
}
if !found {
// provider-specific attribute deleted
} else if c.Value != "" {
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
}
// 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
bar127AWithProviderSpecificTrue *endpoint.Endpoint
bar127AWithProviderSpecificFalse *endpoint.Endpoint
bar127AWithProviderSpecificUnset *endpoint.Endpoint
bar192A *endpoint.Endpoint
multiple1 *endpoint.Endpoint
multiple2 *endpoint.Endpoint
@ -138,6 +139,15 @@ func (suite *PlanTestSuite) SetupTest() {
},
},
}
suite.bar127AWithProviderSpecificUnset = &endpoint.Endpoint{
DNSName: "bar",
Targets: endpoint.Targets{"127.0.0.1"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/bar-127",
},
ProviderSpecific: endpoint.ProviderSpecific{},
}
suite.bar192A = &endpoint.Endpoint{
DNSName: "bar",
Targets: endpoint.Targets{"192.168.0.1"},
@ -291,6 +301,54 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() {
validateEntries(suite.T(), changes.Delete, expectedDelete)
}
func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefaultFalse() {
current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
expectedCreate := []*endpoint.Endpoint{}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
PropertyComparator: func(name, previous, current string) bool {
return CompareBoolean(false, name, previous, current)
},
}
changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Create, expectedCreate)
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
validateEntries(suite.T(), changes.Delete, expectedDelete)
}
func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefualtTrue() {
current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificUnset}
expectedCreate := []*endpoint.Endpoint{}
expectedUpdateOld := []*endpoint.Endpoint{}
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
PropertyComparator: func(name, previous, current string) bool {
return CompareBoolean(true, name, previous, current)
},
}
changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Create, expectedCreate)
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
validateEntries(suite.T(), changes.Delete, expectedDelete)
}
func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
current := []*endpoint.Endpoint{suite.fooV1Cname}
desired := []*endpoint.Endpoint{suite.fooV2Cname}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package aws
import (
"context"
@ -35,6 +35,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
const (
@ -178,17 +179,17 @@ func (r *Route53APIStub) ChangeResourceRecordSetsWithContext(ctx context.Context
}
}
change.ResourceRecordSet.Name = aws.String(wildcardEscape(ensureTrailingDot(aws.StringValue(change.ResourceRecordSet.Name))))
change.ResourceRecordSet.Name = aws.String(wildcardEscape(provider.EnsureTrailingDot(aws.StringValue(change.ResourceRecordSet.Name))))
if change.ResourceRecordSet.AliasTarget != nil {
change.ResourceRecordSet.AliasTarget.DNSName = aws.String(wildcardEscape(ensureTrailingDot(aws.StringValue(change.ResourceRecordSet.AliasTarget.DNSName))))
change.ResourceRecordSet.AliasTarget.DNSName = aws.String(wildcardEscape(provider.EnsureTrailingDot(aws.StringValue(change.ResourceRecordSet.AliasTarget.DNSName))))
}
setId := ""
setID := ""
if change.ResourceRecordSet.SetIdentifier != nil {
setId = aws.StringValue(change.ResourceRecordSet.SetIdentifier)
setID = aws.StringValue(change.ResourceRecordSet.SetIdentifier)
}
key := aws.StringValue(change.ResourceRecordSet.Name) + "::" + aws.StringValue(change.ResourceRecordSet.Type) + "::" + setId
key := aws.StringValue(change.ResourceRecordSet.Name) + "::" + aws.StringValue(change.ResourceRecordSet.Type) + "::" + setID
switch aws.StringValue(change.Action) {
case route53.ChangeActionCreate:
if _, found := recordSets[key]; found {
@ -287,17 +288,17 @@ func TestAWSZones(t *testing.T) {
for _, ti := range []struct {
msg string
zoneIDFilter ZoneIDFilter
zoneTypeFilter ZoneTypeFilter
zoneTagFilter ZoneTagFilter
zoneIDFilter provider.ZoneIDFilter
zoneTypeFilter provider.ZoneTypeFilter
zoneTagFilter provider.ZoneTagFilter
expectedZones map[string]*route53.HostedZone
}{
{"no filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), allZones},
{"public filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("public"), NewZoneTagFilter([]string{}), publicZones},
{"private filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("private"), NewZoneTagFilter([]string{}), privateZones},
{"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), NewZoneTagFilter([]string{}), noZones},
{"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), privateZones},
{"tag filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{"zone=3"}), privateZones},
{"no filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), allZones},
{"public filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("public"), provider.NewZoneTagFilter([]string{}), publicZones},
{"private filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("private"), provider.NewZoneTagFilter([]string{}), privateZones},
{"unknown filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter("unknown"), provider.NewZoneTagFilter([]string{}), noZones},
{"zone id filter", provider.NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), privateZones},
{"tag filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{"zone=3"}), privateZones},
} {
provider, _ := newAWSProviderWithTagFilter(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
@ -309,7 +310,7 @@ func TestAWSZones(t *testing.T) {
}
func TestAWSRecords(t *testing.T) {
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, false, []*endpoint.Endpoint{
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
@ -351,7 +352,7 @@ func TestAWSRecords(t *testing.T) {
func TestAWSCreateRecords(t *testing.T) {
customTTL := endpoint.TTL(60)
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
records := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
@ -376,7 +377,7 @@ func TestAWSCreateRecords(t *testing.T) {
}
func TestAWSUpdateRecords(t *testing.T) {
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
@ -419,7 +420,7 @@ func TestAWSDeleteRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("delete-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
}
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, false, originalEndpoints)
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, originalEndpoints)
require.NoError(t, provider.DeleteRecords(context.Background(), originalEndpoints))
@ -441,12 +442,12 @@ func TestAWSApplyChanges(t *testing.T) {
ctx := context.Background()
records, err := p.Records(ctx)
require.NoError(t, err)
return context.WithValue(ctx, RecordsContextKey, records)
return context.WithValue(ctx, provider.RecordsContextKey, records)
}, 0},
}
for _, tt := range tests {
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
@ -538,7 +539,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) {
endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
}
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalEndpoints)
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalEndpoints)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
@ -686,7 +687,7 @@ func TestAWSChangesByZones(t *testing.T) {
}
func TestAWSsubmitChanges(t *testing.T) {
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
const subnets = 16
const hosts = defaultBatchChangeSize / subnets
@ -715,7 +716,7 @@ func TestAWSsubmitChanges(t *testing.T) {
}
func TestAWSsubmitChangesError(t *testing.T) {
provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure"))
ctx := context.Background()
@ -851,7 +852,7 @@ func validateAWSChangeRecord(t *testing.T, record *route53.Change, expected *rou
}
func TestAWSCreateRecordsWithCNAME(t *testing.T) {
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
records := []*endpoint.Endpoint{
{DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.example.org"}, RecordType: endpoint.RecordTypeCNAME},
@ -881,7 +882,7 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) {
"false": false,
"": false,
} {
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
// Test dualstack and ipv4 load balancer targets
records := []*endpoint.Endpoint{
@ -1023,8 +1024,8 @@ func TestAWSCanonicalHostedZone(t *testing.T) {
{"foo.eu-west-2.elb.amazonaws.com", "ZHURV8PSTC4K8"},
{"foo.eu-west-3.elb.amazonaws.com", "Z3Q77PNBQS71R4"},
{"foo.sa-east-1.elb.amazonaws.com", "Z2P70J7HTTTPLU"},
{"foo.cn-north-1.elb.amazonaws.com.cn", "Z3BX2TMKNYI13Y"},
{"foo.cn-northwest-1.elb.amazonaws.com.cn", "Z3BX2TMKNYI13Y"},
{"foo.cn-north-1.elb.amazonaws.com.cn", "Z1GDH35T77C1KE"},
{"foo.cn-northwest-1.elb.amazonaws.com.cn", "ZM7IZAIOVVDZF"},
// Network Load Balancers
{"foo.elb.us-east-2.amazonaws.com", "ZLMOA37VPKANP"},
{"foo.elb.us-east-1.amazonaws.com", "Z26RNL4JYFTOTI"},
@ -1182,11 +1183,11 @@ func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) {
require.NoError(t, err)
}
}
func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) {
return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records)
func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) {
return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, provider.NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records)
}
func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, zoneTagFilter ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) {
func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, zoneTagFilter provider.ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) {
client := NewRoute53APIStub()
provider := &AWSProvider{

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package azure
import (
"context"
@ -27,6 +27,11 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
const (
recordTTL = 300
)
// mockPrivateZonesClient implements the methods of the Azure Private DNS Zones Client which are used in the Azure Private DNS Provider
@ -203,7 +208,7 @@ func (client *mockPrivateRecordSetsClient) CreateOrUpdate(ctx context.Context, r
}
// newMockedAzurePrivateDNSProvider creates an AzureProvider comprising the mocked clients for zones and recordsets
func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, resourceGroup string, zones *[]privatedns.PrivateZone, recordSets *[]privatedns.RecordSet) (*AzurePrivateDNSProvider, error) {
func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, zones *[]privatedns.PrivateZone, recordSets *[]privatedns.RecordSet) (*AzurePrivateDNSProvider, error) {
// init zone-related parts of the mock-client
pageIterator := mockPrivateZoneListResultPageIterator{
results: []privatedns.PrivateZoneListResult{
@ -236,7 +241,7 @@ func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneID
return newAzurePrivateDNSProvider(domainFilter, zoneIDFilter, dryRun, resourceGroup, &zonesClient, &recordSetsClient), nil
}
func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, resourceGroup string, privateZonesClient PrivateZonesClient, privateRecordsClient PrivateRecordSetsClient) *AzurePrivateDNSProvider {
func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, privateZonesClient PrivateZonesClient, privateRecordsClient PrivateRecordSetsClient) *AzurePrivateDNSProvider {
return &AzurePrivateDNSProvider{
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
@ -248,7 +253,7 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter
}
func TestAzurePrivateDNSRecord(t *testing.T) {
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s",
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s",
&[]privatedns.PrivateZone{
createMockPrivateZone("example.com", "/privateDnsZones/example.com"),
},
@ -284,7 +289,7 @@ func TestAzurePrivateDNSRecord(t *testing.T) {
}
func TestAzurePrivateDNSMultiRecord(t *testing.T) {
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s",
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s",
&[]privatedns.PrivateZone{
createMockPrivateZone("example.com", "/privateDnsZones/example.com"),
},
@ -383,7 +388,7 @@ func testAzurePrivateDNSApplyChangesInternal(t *testing.T, dryRun bool, client P
provider := newAzurePrivateDNSProvider(
endpoint.NewDomainFilter([]string{""}),
NewZoneIDFilter([]string{""}),
provider.NewZoneIDFilter([]string{""}),
dryRun,
"group",
&zonesClient,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package azure
import (
"context"
@ -30,6 +30,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
// mockZonesClient implements the methods of the Azure DNS Zones Client which are used in the Azure Provider
@ -206,7 +207,7 @@ func (client *mockRecordSetsClient) CreateOrUpdate(ctx context.Context, resource
}
// newMockedAzureProvider creates an AzureProvider comprising the mocked clients for zones and recordsets
func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zones *[]dns.Zone, recordSets *[]dns.RecordSet) (*AzureProvider, error) {
func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zones *[]dns.Zone, recordSets *[]dns.RecordSet) (*AzureProvider, error) {
// init zone-related parts of the mock-client
pageIterator := mockZoneListResultPageIterator{
results: []dns.ZoneListResult{
@ -239,7 +240,7 @@ func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter Zon
return newAzureProvider(domainFilter, zoneIDFilter, dryRun, resourceGroup, userAssignedIdentityClientID, &zonesClient, &recordSetsClient), nil
}
func newAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zonesClient ZonesClient, recordsClient RecordSetsClient) *AzureProvider {
func newAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zonesClient ZonesClient, recordsClient RecordSetsClient) *AzureProvider {
return &AzureProvider{
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
@ -256,7 +257,7 @@ func validateAzureEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expect
}
func TestAzureRecord(t *testing.T) {
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s", "",
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
&[]dns.Zone{
createMockZone("example.com", "/dnszones/example.com"),
},
@ -293,7 +294,7 @@ func TestAzureRecord(t *testing.T) {
}
func TestAzureMultiRecord(t *testing.T) {
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s", "",
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
&[]dns.Zone{
createMockZone("example.com", "/dnszones/example.com"),
},
@ -393,7 +394,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC
provider := newAzureProvider(
endpoint.NewDomainFilter([]string{""}),
NewZoneIDFilter([]string{""}),
provider.NewZoneIDFilter([]string{""}),
dryRun,
"group",
"",

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.
*/
package provider
package cloudflare
import (
"context"
"fmt"
"os"
"sort"
"strconv"
"strings"
@ -29,6 +28,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/source"
)
@ -58,6 +58,7 @@ type cloudFlareDNS interface {
ZoneIDByName(zoneName string) (string, error)
ListZones(zoneID ...string) ([]cloudflare.Zone, error)
ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error)
ZoneDetails(zoneID string) (cloudflare.Zone, error)
DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error)
CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error)
DeleteDNSRecord(zoneID, recordID string) error
@ -98,12 +99,17 @@ func (z zoneService) ListZonesContext(ctx context.Context, opts ...cloudflare.Re
return z.service.ListZonesContext(ctx, opts...)
}
func (z zoneService) ZoneDetails(zoneID string) (cloudflare.Zone, error) {
return z.service.ZoneDetails(zoneID)
}
// CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
type CloudFlareProvider struct {
provider.BaseProvider
Client cloudFlareDNS
// only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter
zoneIDFilter provider.ZoneIDFilter
proxiedByDefault bool
DryRun bool
PaginationOptions cloudflare.PaginationOptions
@ -112,11 +118,11 @@ type CloudFlareProvider struct {
// cloudFlareChange differentiates between ChangActions
type cloudFlareChange struct {
Action string
ResourceRecordSet []cloudflare.DNSRecord
ResourceRecord cloudflare.DNSRecord
}
// NewCloudFlareProvider initializes a new CloudFlare DNS based Provider.
func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, zonesPerPage int, proxiedByDefault bool, dryRun bool) (*CloudFlareProvider, error) {
func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zonesPerPage int, proxiedByDefault bool, dryRun bool) (*CloudFlareProvider, error) {
// initialize via chosen auth method and returns new API object
var (
config *cloudflare.API
@ -150,6 +156,27 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
result := []cloudflare.Zone{}
p.PaginationOptions.Page = 1
// if there is a zoneIDfilter configured
// && if the filter isnt just a blank string (used in tests)
if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" {
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
log.Debugf("looking up zone %s", zoneID)
detailResponse, err := p.Client.ZoneDetails(zoneID)
if err != nil {
log.Errorf("zone %s lookup failed, %v", zoneID, err)
continue
}
log.WithFields(log.Fields{
"zoneName": detailResponse.Name,
"zoneID": detailResponse.ID,
}).Debugln("adding zone for consideration")
result = append(result, detailResponse)
}
return result, nil
}
log.Debugln("no zoneIDFilter configured, looking at all zones")
for {
zonesResponse, err := p.Client.ListZonesContext(ctx, cloudflare.WithPagination(p.PaginationOptions))
if err != nil {
@ -158,10 +185,7 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
for _, zone := range zonesResponse.Result {
if !p.domainFilter.Match(zone.Name) {
continue
}
if !p.zoneIDFilter.Match(zone.ID) {
log.Debugf("zone %s not in domain filter", zone.Name)
continue
}
result = append(result, zone)
@ -199,15 +223,47 @@ func (p *CloudFlareProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
// ApplyChanges applies a given set of changes in a given zone.
func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
proxiedByDefault := p.proxiedByDefault
cloudflareChanges := []*cloudFlareChange{}
combinedChanges := make([]*cloudFlareChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
for _, endpoint := range changes.Create {
for _, target := range endpoint.Targets {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, endpoint, target))
}
}
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareCreate, changes.Create, proxiedByDefault)...)
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareUpdate, changes.UpdateNew, proxiedByDefault)...)
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareDelete, changes.Delete, proxiedByDefault)...)
for i, desired := range changes.UpdateNew {
current := changes.UpdateOld[i]
return p.submitChanges(ctx, combinedChanges)
add, remove, leave := provider.Difference(current.Targets, desired.Targets)
for _, a := range add {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareCreate, desired, a))
}
for _, a := range leave {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareUpdate, desired, a))
}
for _, a := range remove {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, current, a))
}
}
for _, endpoint := range changes.Delete {
for _, target := range endpoint.Targets {
cloudflareChanges = append(cloudflareChanges, p.newCloudFlareChange(cloudFlareDelete, endpoint, target))
}
}
return p.submitChanges(ctx, cloudflareChanges)
}
func (p *CloudFlareProvider) PropertyValuesEqual(name string, previous string, current string) bool {
if name == source.CloudflareProxiedKey {
return plan.CompareBoolean(p.proxiedByDefault, name, previous, current)
}
return p.BaseProvider.PropertyValuesEqual(name, previous, current)
}
// submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
@ -231,10 +287,9 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
}
for _, change := range changes {
logFields := log.Fields{
"record": change.ResourceRecordSet[0].Name,
"type": change.ResourceRecordSet[0].Type,
"ttl": change.ResourceRecordSet[0].TTL,
"targets": len(change.ResourceRecordSet),
"record": change.ResourceRecord.Name,
"type": change.ResourceRecord.Type,
"ttl": change.ResourceRecord.TTL,
"action": change.Action,
"zone": zoneID,
}
@ -245,35 +300,41 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
continue
}
recordIDs := p.getRecordIDs(records, change.ResourceRecordSet[0])
// to simplify bookkeeping for multiple records, an update is executed as delete+create
if change.Action == cloudFlareDelete || change.Action == cloudFlareUpdate {
for _, recordID := range recordIDs {
if change.Action == cloudFlareUpdate {
recordID := p.getRecordID(records, change.ResourceRecord)
if recordID == "" {
log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord)
continue
}
err := p.Client.UpdateDNSRecord(zoneID, recordID, change.ResourceRecord)
if err != nil {
log.WithFields(logFields).Errorf("failed to delete record: %v", err)
}
} else if change.Action == cloudFlareDelete {
recordID := p.getRecordID(records, change.ResourceRecord)
if recordID == "" {
log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord)
continue
}
err := p.Client.DeleteDNSRecord(zoneID, recordID)
if err != nil {
log.WithFields(logFields).Errorf("failed to delete record: %v", err)
}
}
}
if change.Action == cloudFlareCreate || change.Action == cloudFlareUpdate {
for _, record := range change.ResourceRecordSet {
_, err := p.Client.CreateDNSRecord(zoneID, record)
} else if change.Action == cloudFlareCreate {
_, err := p.Client.CreateDNSRecord(zoneID, change.ResourceRecord)
if err != nil {
log.WithFields(logFields).Errorf("failed to create record: %v", err)
}
}
}
}
}
return nil
}
// changesByZone separates a multi-zone change into a single change per zone.
func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
changes := make(map[string][]*cloudFlareChange)
zoneNameIDMapper := zoneIDName{}
zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones {
zoneNameIDMapper.Add(z.ID, z.Name)
@ -281,9 +342,9 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []
}
for _, c := range changeSet {
zoneID, _ := zoneNameIDMapper.FindZone(c.ResourceRecordSet[0].Name)
zoneID, _ := zoneNameIDMapper.FindZone(c.ResourceRecord.Name)
if zoneID == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSet[0].Name)
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecord.Name)
continue
}
changes[zoneID] = append(changes[zoneID], c)
@ -292,51 +353,36 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []
return changes
}
func (p *CloudFlareProvider) getRecordIDs(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) []string {
recordIDs := make([]string, 0)
func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) string {
for _, zoneRecord := range records {
if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type {
recordIDs = append(recordIDs, zoneRecord.ID)
if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type && zoneRecord.Content == record.Content {
return zoneRecord.ID
}
}
sort.Strings(recordIDs)
return recordIDs
return ""
}
// newCloudFlareChanges returns a collection of Changes based on the given records and action.
func newCloudFlareChanges(action string, endpoints []*endpoint.Endpoint, proxiedByDefault bool) []*cloudFlareChange {
changes := make([]*cloudFlareChange, 0, len(endpoints))
for _, endpoint := range endpoints {
changes = append(changes, newCloudFlareChange(action, endpoint, proxiedByDefault))
}
return changes
}
func newCloudFlareChange(action string, endpoint *endpoint.Endpoint, proxiedByDefault bool) *cloudFlareChange {
func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoint.Endpoint, target string) *cloudFlareChange {
ttl := defaultCloudFlareRecordTTL
proxied := shouldBeProxied(endpoint, proxiedByDefault)
proxied := shouldBeProxied(endpoint, p.proxiedByDefault)
if endpoint.RecordTTL.IsConfigured() {
ttl = int(endpoint.RecordTTL)
}
resourceRecordSet := make([]cloudflare.DNSRecord, len(endpoint.Targets))
for i := range endpoint.Targets {
resourceRecordSet[i] = cloudflare.DNSRecord{
Name: endpoint.DNSName,
TTL: ttl,
Proxied: proxied,
Type: endpoint.RecordType,
Content: endpoint.Targets[i],
}
if len(endpoint.Targets) > 1 {
log.Errorf("Updates should have just one target")
}
return &cloudFlareChange{
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{}
for _, r := range records {
if !supportedRecordType(r.Type) {
if !provider.SupportedRecordType(r.Type) {
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.
*/
package provider
package coredns
import (
"context"
@ -35,6 +35,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
func init() {
@ -56,6 +57,7 @@ type coreDNSClient interface {
}
type coreDNSProvider struct {
provider.BaseProvider
dryRun bool
coreDNSPrefix string
domainFilter endpoint.DomainFilter
@ -84,7 +86,7 @@ type Service struct {
// answer.
Group string `json:"group,omitempty"`
// Etcd key where we found this service and ignored from json un-/marshalling
// Etcd key where we found this service and ignored from json un-/marshaling
Key string `json:"-"`
}
@ -244,7 +246,7 @@ func newETCDClient() (coreDNSClient, error) {
}
// NewCoreDNSProvider is a CoreDNS provider constructor
func NewCoreDNSProvider(domainFilter endpoint.DomainFilter, prefix string, dryRun bool) (Provider, error) {
func NewCoreDNSProvider(domainFilter endpoint.DomainFilter, prefix string, dryRun bool) (provider.Provider, error) {
client, err := newETCDClient()
if err != nil {
return nil, err
@ -395,7 +397,6 @@ func (p coreDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
}
}
}
}
index := 0
for _, ep := range group {

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package designate
import (
"context"
@ -35,6 +35,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/tlsutils"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
const (
@ -226,6 +227,7 @@ func (c designateClient) DeleteRecordSet(zoneID, recordSetID string) error {
// designate provider type
type designateProvider struct {
provider.BaseProvider
client designateClientInterface
// only consider hosted zones managing domains ending in this suffix
@ -234,7 +236,7 @@ type designateProvider struct {
}
// NewDesignateProvider is a factory function for OpenStack designate providers
func NewDesignateProvider(domainFilter endpoint.DomainFilter, dryRun bool) (Provider, error) {
func NewDesignateProvider(domainFilter endpoint.DomainFilter, dryRun bool) (provider.Provider, error) {
client, err := newDesignateClient()
if err != nil {
return nil, err

View File

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

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package digitalocean
import (
"context"
@ -187,12 +187,12 @@ func TestDigitalOceanApplyChanges(t *testing.T) {
func TestNewDigitalOceanProvider(t *testing.T) {
_ = os.Setenv("DO_TOKEN", "xxxxxxxxxxxxxxxxx")
_, err := NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true)
_, err := NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50)
if err != nil {
t.Errorf("should not fail, %s", err)
}
_ = os.Unsetenv("DO_TOKEN")
_, err = NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true)
_, err = NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50)
if err == nil {
t.Errorf("expected to fail")
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package dnsimple
import (
"context"
@ -25,70 +25,64 @@ import (
"github.com/dnsimple/dnsimple-go/dnsimple"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
const dnsimpleRecordTTL = 3600 // Default TTL of 1 hour if not set (DNSimple's default)
type identityService struct {
type dnsimpleIdentityService struct {
service *dnsimple.IdentityService
}
func (i identityService) Whoami() (*dnsimple.WhoamiResponse, error) {
return i.service.Whoami()
func (i dnsimpleIdentityService) Whoami(ctx context.Context) (*dnsimple.WhoamiResponse, error) {
return i.service.Whoami(ctx)
}
// Returns the account ID given dnsimple credentials
func (p *dnsimpleProvider) GetAccountID(credentials dnsimple.Credentials, client dnsimple.Client) (accountID string, err error) {
// get DNSimple client accountID
whoamiResponse, err := client.Identity.Whoami()
if err != nil {
return "", err
}
return strconv.Itoa(whoamiResponse.Data.Account.ID), nil
}
// dnsimpleZoneServiceInterface is an interface that contains all necessary zone services from dnsimple
// dnsimpleZoneServiceInterface is an interface that contains all necessary zone services from DNSimple
type dnsimpleZoneServiceInterface interface {
ListZones(accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error)
ListRecords(accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error)
CreateRecord(accountID string, zoneID string, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error)
DeleteRecord(accountID string, zoneID string, recordID int) (*dnsimple.ZoneRecordResponse, error)
UpdateRecord(accountID string, zoneID string, recordID int, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error)
ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error)
ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error)
CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error)
DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error)
UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error)
}
type dnsimpleZoneService struct {
service *dnsimple.ZonesService
}
func (z dnsimpleZoneService) ListZones(accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) {
return z.service.ListZones(accountID, options)
func (z dnsimpleZoneService) ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) {
return z.service.ListZones(ctx, accountID, options)
}
func (z dnsimpleZoneService) ListRecords(accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) {
return z.service.ListRecords(accountID, zoneID, options)
func (z dnsimpleZoneService) ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) {
return z.service.ListRecords(ctx, accountID, zoneID, options)
}
func (z dnsimpleZoneService) CreateRecord(accountID string, zoneID string, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) {
return z.service.CreateRecord(accountID, zoneID, recordAttributes)
func (z dnsimpleZoneService) CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) {
return z.service.CreateRecord(ctx, accountID, zoneID, recordAttributes)
}
func (z dnsimpleZoneService) DeleteRecord(accountID string, zoneID string, recordID int) (*dnsimple.ZoneRecordResponse, error) {
return z.service.DeleteRecord(accountID, zoneID, recordID)
func (z dnsimpleZoneService) DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error) {
return z.service.DeleteRecord(ctx, accountID, zoneID, recordID)
}
func (z dnsimpleZoneService) UpdateRecord(accountID string, zoneID string, recordID int, recordAttributes dnsimple.ZoneRecord) (*dnsimple.ZoneRecordResponse, error) {
return z.service.UpdateRecord(accountID, zoneID, recordID, recordAttributes)
func (z dnsimpleZoneService) UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) {
return z.service.UpdateRecord(ctx, accountID, zoneID, recordID, recordAttributes)
}
type dnsimpleProvider struct {
provider.BaseProvider
client dnsimpleZoneServiceInterface
identity identityService
identity dnsimpleIdentityService
accountID string
domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter
zoneIDFilter provider.ZoneIDFilter
dryRun bool
}
@ -104,35 +98,52 @@ const (
)
// NewDnsimpleProvider initializes a new Dnsimple based provider
func NewDnsimpleProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) (Provider, error) {
func NewDnsimpleProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (provider.Provider, error) {
oauthToken := os.Getenv("DNSIMPLE_OAUTH")
if len(oauthToken) == 0 {
return nil, fmt.Errorf("No dnsimple oauth token provided")
return nil, fmt.Errorf("no dnsimple oauth token provided")
}
client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(oauthToken))
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: oauthToken})
tc := oauth2.NewClient(context.Background(), ts)
client := dnsimple.NewClient(tc)
client.SetUserAgent(fmt.Sprintf("Kubernetes ExternalDNS/%s", externaldns.Version))
provider := &dnsimpleProvider{
client: dnsimpleZoneService{service: client.Zones},
identity: identityService{service: client.Identity},
identity: dnsimpleIdentityService{service: client.Identity},
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
}
whoamiResponse, err := provider.identity.service.Whoami()
whoamiResponse, err := provider.identity.Whoami(context.Background())
if err != nil {
return nil, err
}
provider.accountID = strconv.Itoa(whoamiResponse.Data.Account.ID)
provider.accountID = int64ToString(whoamiResponse.Data.Account.ID)
return provider, nil
}
// GetAccountID returns the account ID given DNSimple credentials.
func (p *dnsimpleProvider) GetAccountID(ctx context.Context) (accountID string, err error) {
// get DNSimple client accountID
whoamiResponse, err := p.identity.Whoami(ctx)
if err != nil {
return "", err
}
return int64ToString(whoamiResponse.Data.Account.ID), nil
}
// Returns a list of filtered Zones
func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) {
func (p *dnsimpleProvider) Zones(ctx context.Context) (map[string]dnsimple.Zone, error) {
zones := make(map[string]dnsimple.Zone)
page := 1
listOptions := &dnsimple.ZoneListOptions{}
for {
listOptions.Page = page
zonesResponse, err := p.client.ListZones(p.accountID, listOptions)
listOptions.Page = &page
zonesResponse, err := p.client.ListZones(ctx, p.accountID, listOptions)
if err != nil {
return nil, err
}
@ -141,11 +152,11 @@ func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) {
continue
}
if !p.zoneIDFilter.Match(strconv.Itoa(zone.ID)) {
if !p.zoneIDFilter.Match(int64ToString(zone.ID)) {
continue
}
zones[strconv.Itoa(zone.ID)] = zone
zones[int64ToString(zone.ID)] = zone
}
page++
@ -158,7 +169,7 @@ func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) {
// Records returns a list of endpoints in a given zone
func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
zones, err := p.Zones()
zones, err := p.Zones(ctx)
if err != nil {
return nil, err
}
@ -166,8 +177,8 @@ func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.E
page := 1
listOptions := &dnsimple.ZoneRecordListOptions{}
for {
listOptions.Page = page
records, err := p.client.ListRecords(p.accountID, zone.Name, listOptions)
listOptions.Page = &page
records, err := p.client.ListRecords(ctx, p.accountID, zone.Name, listOptions)
if err != nil {
return nil, err
}
@ -224,12 +235,12 @@ func newDnsimpleChanges(action string, endpoints []*endpoint.Endpoint) []*dnsimp
}
// submitChanges takes a zone and a collection of changes and makes all changes from the collection
func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error {
func (p *dnsimpleProvider) submitChanges(ctx context.Context, changes []*dnsimpleChange) error {
if len(changes) == 0 {
log.Infof("All records are already up to date")
return nil
}
zones, err := p.Zones()
zones, err := p.Zones(ctx)
if err != nil {
return err
}
@ -248,28 +259,35 @@ func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error {
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, fmt.Sprintf(".%s", zone.Name))
}
recordAttributes := dnsimple.ZoneRecordAttributes{
Name: &change.ResourceRecordSet.Name,
Type: change.ResourceRecordSet.Type,
Content: change.ResourceRecordSet.Content,
TTL: change.ResourceRecordSet.TTL,
}
if !p.dryRun {
switch change.Action {
case dnsimpleCreate:
_, err := p.client.CreateRecord(p.accountID, zone.Name, change.ResourceRecordSet)
_, err := p.client.CreateRecord(ctx, p.accountID, zone.Name, recordAttributes)
if err != nil {
return err
}
case dnsimpleDelete:
recordID, err := p.GetRecordID(zone.Name, change.ResourceRecordSet.Name)
recordID, err := p.GetRecordID(ctx, zone.Name, *recordAttributes.Name)
if err != nil {
return err
}
_, err = p.client.DeleteRecord(p.accountID, zone.Name, recordID)
_, err = p.client.DeleteRecord(ctx, p.accountID, zone.Name, recordID)
if err != nil {
return err
}
case dnsimpleUpdate:
recordID, err := p.GetRecordID(zone.Name, change.ResourceRecordSet.Name)
recordID, err := p.GetRecordID(ctx, zone.Name, *recordAttributes.Name)
if err != nil {
return err
}
_, err = p.client.UpdateRecord(p.accountID, zone.Name, recordID, change.ResourceRecordSet)
_, err = p.client.UpdateRecord(ctx, p.accountID, zone.Name, recordID, recordAttributes)
if err != nil {
return err
}
@ -279,13 +297,13 @@ func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error {
return nil
}
// Returns the record ID for a given record name and zone
func (p *dnsimpleProvider) GetRecordID(zone string, recordName string) (recordID int, err error) {
// GetRecordID returns the record ID for a given record name and zone.
func (p *dnsimpleProvider) GetRecordID(ctx context.Context, zone string, recordName string) (recordID int64, err error) {
page := 1
listOptions := &dnsimple.ZoneRecordListOptions{Name: recordName}
listOptions := &dnsimple.ZoneRecordListOptions{Name: &recordName}
for {
listOptions.Page = page
records, err := p.client.ListRecords(p.accountID, zone, listOptions)
listOptions.Page = &page
records, err := p.client.ListRecords(ctx, p.accountID, zone, listOptions)
if err != nil {
return 0, err
}
@ -301,7 +319,7 @@ func (p *dnsimpleProvider) GetRecordID(zone string, recordName string) (recordID
break
}
}
return 0, fmt.Errorf("No record id found")
return 0, fmt.Errorf("no record id found")
}
// dnsimpleSuitableZone returns the most suitable zone for a given hostname and a set of zones.
@ -319,18 +337,18 @@ func dnsimpleSuitableZone(hostname string, zones map[string]dnsimple.Zone) *dnsi
}
// CreateRecords creates records for a given slice of endpoints
func (p *dnsimpleProvider) CreateRecords(endpoints []*endpoint.Endpoint) error {
return p.submitChanges(newDnsimpleChanges(dnsimpleCreate, endpoints))
func (p *dnsimpleProvider) CreateRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
return p.submitChanges(ctx, newDnsimpleChanges(dnsimpleCreate, endpoints))
}
// DeleteRecords deletes records for a given slice of endpoints
func (p *dnsimpleProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error {
return p.submitChanges(newDnsimpleChanges(dnsimpleDelete, endpoints))
func (p *dnsimpleProvider) DeleteRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
return p.submitChanges(ctx, newDnsimpleChanges(dnsimpleDelete, endpoints))
}
// UpdateRecords updates records for a given slice of endpoints
func (p *dnsimpleProvider) UpdateRecords(endpoints []*endpoint.Endpoint) error {
return p.submitChanges(newDnsimpleChanges(dnsimpleUpdate, endpoints))
func (p *dnsimpleProvider) UpdateRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
return p.submitChanges(ctx, newDnsimpleChanges(dnsimpleUpdate, endpoints))
}
// ApplyChanges applies a given set of changes
@ -341,5 +359,9 @@ func (p *dnsimpleProvider) ApplyChanges(ctx context.Context, changes *plan.Chang
combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleUpdate, changes.UpdateNew)...)
combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleDelete, changes.Delete)...)
return p.submitChanges(combinedChanges)
return p.submitChanges(ctx, combinedChanges)
}
func int64ToString(i int64) string {
return strconv.FormatInt(i, 10)
}

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package exoscale
import (
"context"
@ -25,6 +25,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
// EgoscaleClientI for replaceable implementation
@ -38,6 +39,7 @@ type EgoscaleClientI interface {
// ExoscaleProvider initialized as dns provider with no records
type ExoscaleProvider struct {
provider.BaseProvider
domain endpoint.DomainFilter
client EgoscaleClientI
filter *zoneFilter
@ -257,3 +259,38 @@ func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[int64
}
return matchZoneID, name
}
func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint {
findMatch := func(template *endpoint.Endpoint) *endpoint.Endpoint {
for _, new := range updateNew {
if template.DNSName == new.DNSName &&
template.RecordType == new.RecordType {
return new
}
}
return nil
}
var result []*endpoint.Endpoint
for _, old := range updateOld {
matchingNew := findMatch(old)
if matchingNew == nil {
// no match, shouldn't happen
continue
}
if !matchingNew.Targets.Same(old.Targets) {
// new target: always update, TTL will be overwritten too if necessary
result = append(result, matchingNew)
continue
}
if matchingNew.RecordTTL != 0 && matchingNew.RecordTTL != old.RecordTTL {
// same target, but new non-zero TTL set in k8s, must update
// probably would happen only if there is a bug in the code calling the provider
result = append(result, matchingNew)
}
}
return result
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package exoscale
import (
"context"
@ -189,3 +189,144 @@ func TestExoscaleApplyChanges(t *testing.T) {
assert.Equal(t, "foo.com", updateExoscale[0].name)
assert.Equal(t, int64(1), updateExoscale[0].updateDNSRecord.ID)
}
func TestExoscaleMerge_NoUpdateOnTTL0Changes(t *testing.T) {
updateOld := []*endpoint.Endpoint{
{
DNSName: "name1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "name2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeA,
},
}
updateNew := []*endpoint.Endpoint{
{
DNSName: "name1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(0),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(0),
RecordType: endpoint.RecordTypeCNAME,
},
}
assert.Equal(t, 0, len(merge(updateOld, updateNew)))
}
func TestExoscaleMerge_UpdateOnTTLChanges(t *testing.T) {
updateOld := []*endpoint.Endpoint{
{
DNSName: "name1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeCNAME,
},
}
updateNew := []*endpoint.Endpoint{
{
DNSName: "name1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(77),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(10),
RecordType: endpoint.RecordTypeCNAME,
},
}
merged := merge(updateOld, updateNew)
assert.Equal(t, 2, len(merged))
assert.Equal(t, "name1", merged[0].DNSName)
}
func TestExoscaleMerge_AlwaysUpdateTarget(t *testing.T) {
updateOld := []*endpoint.Endpoint{
{
DNSName: "name1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeCNAME,
},
}
updateNew := []*endpoint.Endpoint{
{
DNSName: "name1",
Targets: endpoint.Targets{"target1-changed"},
RecordTTL: endpoint.TTL(0),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(0),
RecordType: endpoint.RecordTypeCNAME,
},
}
merged := merge(updateOld, updateNew)
assert.Equal(t, 1, len(merged))
assert.Equal(t, "target1-changed", merged[0].Targets[0])
}
func TestExoscaleMerge_NoUpdateIfTTLUnchanged(t *testing.T) {
updateOld := []*endpoint.Endpoint{
{
DNSName: "name1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(55),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(55),
RecordType: endpoint.RecordTypeCNAME,
},
}
updateNew := []*endpoint.Endpoint{
{
DNSName: "name1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(55),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(55),
RecordType: endpoint.RecordTypeCNAME,
},
}
merged := merge(updateOld, updateNew)
assert.Equal(t, 0, len(merged))
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package google
import (
"context"
@ -33,6 +33,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
const (
@ -98,6 +99,7 @@ func (c changesService) Create(project string, managedZone string, change *dns.C
// GoogleProvider is an implementation of Provider for Google CloudDNS.
type GoogleProvider struct {
provider.BaseProvider
// The Google project to work in
project string
// Enabled dry-run will print any modifying actions rather than execute them.
@ -109,7 +111,7 @@ type GoogleProvider struct {
// only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter
// only consider hosted zones ending with this zone id
zoneIDFilter ZoneIDFilter
zoneIDFilter provider.ZoneIDFilter
// A client for managing resource record sets
resourceRecordSetsClient resourceRecordSetsClientInterface
// A client for managing hosted zones
@ -121,7 +123,7 @@ type GoogleProvider struct {
}
// NewGoogleProvider initializes a new Google CloudDNS based Provider.
func NewGoogleProvider(ctx context.Context, project string, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, batchChangeSize int, batchChangeInterval time.Duration, dryRun bool) (*GoogleProvider, error) {
func NewGoogleProvider(ctx context.Context, project string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, batchChangeSize int, batchChangeInterval time.Duration, dryRun bool) (*GoogleProvider, error) {
gcloud, err := google.DefaultClient(ctx, dns.NdevClouddnsReadwriteScope)
if err != nil {
return nil, err
@ -209,7 +211,7 @@ func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.End
f := func(resp *dns.ResourceRecordSetsListResponse) error {
for _, r := range resp.Rrsets {
if !supportedRecordType(r.Type) {
if !provider.SupportedRecordType(r.Type) {
continue
}
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), r.Rrdatas...))
@ -398,7 +400,7 @@ func batchChange(change *dns.Change, batchSize int) []*dns.Change {
// separateChange separates a multi-zone change into a single change per zone.
func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[string]*dns.Change {
changes := make(map[string]*dns.Change)
zoneNameIDMapper := zoneIDName{}
zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones {
zoneNameIDMapper[z.Name] = z.DnsName
changes[z.Name] = &dns.Change{
@ -407,7 +409,7 @@ func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[s
}
}
for _, a := range change.Additions {
if zoneName, _ := zoneNameIDMapper.FindZone(ensureTrailingDot(a.Name)); zoneName != "" {
if zoneName, _ := zoneNameIDMapper.FindZone(provider.EnsureTrailingDot(a.Name)); zoneName != "" {
changes[zoneName].Additions = append(changes[zoneName].Additions, a)
} else {
log.Warnf("No matching zone for record addition: %s %s %s %d", a.Name, a.Type, a.Rrdatas, a.Ttl)
@ -415,7 +417,7 @@ func separateChange(zones map[string]*dns.ManagedZone, change *dns.Change) map[s
}
for _, d := range change.Deletions {
if zoneName, _ := zoneNameIDMapper.FindZone(ensureTrailingDot(d.Name)); zoneName != "" {
if zoneName, _ := zoneNameIDMapper.FindZone(provider.EnsureTrailingDot(d.Name)); zoneName != "" {
changes[zoneName].Deletions = append(changes[zoneName].Deletions, d)
} else {
log.Warnf("No matching zone for record deletion: %s %s %s %d", d.Name, d.Type, d.Rrdatas, d.Ttl)
@ -440,7 +442,7 @@ func newRecord(ep *endpoint.Endpoint) *dns.ResourceRecordSet {
targets := make([]string, len(ep.Targets))
copy(targets, []string(ep.Targets))
if ep.RecordType == endpoint.RecordTypeCNAME {
targets[0] = ensureTrailingDot(targets[0])
targets[0] = provider.EnsureTrailingDot(targets[0])
}
// no annotation results in a Ttl of 0, default to 300 for backwards-compatibility
@ -450,7 +452,7 @@ func newRecord(ep *endpoint.Endpoint) *dns.ResourceRecordSet {
}
return &dns.ResourceRecordSet{
Name: ensureTrailingDot(ep.DNSName),
Name: provider.EnsureTrailingDot(ep.DNSName),
Rrdatas: targets,
Ttl: ttl,
Type: ep.RecordType,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package google
import (
"fmt"
@ -30,7 +30,9 @@ import (
"google.golang.org/api/googleapi"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
var (
@ -192,7 +194,7 @@ func hasTrailingDot(target string) bool {
}
func TestGoogleZonesIDFilter(t *testing.T) {
provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), NewZoneIDFilter([]string{"10002"}), false, []*endpoint.Endpoint{})
provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), provider.NewZoneIDFilter([]string{"10002"}), false, []*endpoint.Endpoint{})
zones, err := provider.Zones(context.Background())
require.NoError(t, err)
@ -203,7 +205,7 @@ func TestGoogleZonesIDFilter(t *testing.T) {
}
func TestGoogleZonesNameFilter(t *testing.T) {
provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), NewZoneIDFilter([]string{"internal-2"}), false, []*endpoint.Endpoint{})
provider := newGoogleProviderZoneOverlap(t, endpoint.NewDomainFilter([]string{"cluster.local."}), provider.NewZoneIDFilter([]string{"internal-2"}), false, []*endpoint.Endpoint{})
zones, err := provider.Zones(context.Background())
require.NoError(t, err)
@ -214,7 +216,7 @@ func TestGoogleZonesNameFilter(t *testing.T) {
}
func TestGoogleZones(t *testing.T) {
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
zones, err := provider.Zones(context.Background())
require.NoError(t, err)
@ -233,7 +235,7 @@ func TestGoogleRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(3), "foo.elb.amazonaws.com"),
}
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, originalEndpoints)
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, originalEndpoints)
records, err := provider.Records(context.Background())
require.NoError(t, err)
@ -261,7 +263,7 @@ func TestGoogleRecordsFilter(t *testing.T) {
"zone-0.ext-dns-test-2.gcp.zalan.do.",
// there exists a third zone "zone-3" that we want to exclude from being managed.
}),
NewZoneIDFilter([]string{""}),
provider.NewZoneIDFilter([]string{""}),
false,
originalEndpoints,
)
@ -286,7 +288,7 @@ func TestGoogleRecordsFilter(t *testing.T) {
}
func TestGoogleCreateRecords(t *testing.T) {
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
records := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
@ -312,7 +314,7 @@ func TestGoogleUpdateRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("update-test-ttl.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(15), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "foo.elb.amazonaws.com"),
}
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, currentRecords)
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, currentRecords)
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpointWithTTL("update-test-ttl.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(25), "4.3.2.1"),
@ -338,7 +340,7 @@ func TestGoogleDeleteRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "baz.elb.amazonaws.com"),
}
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, originalEndpoints)
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, originalEndpoints)
require.NoError(t, provider.DeleteRecords(originalEndpoints))
@ -359,7 +361,7 @@ func TestGoogleApplyChanges(t *testing.T) {
"zone-0.ext-dns-test-2.gcp.zalan.do.",
// there exists a third zone "zone-3" that we want to exclude from being managed.
}),
NewZoneIDFilter([]string{""}),
provider.NewZoneIDFilter([]string{""}),
false,
[]*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, googleRecordTTL, "8.8.8.8"),
@ -433,7 +435,7 @@ func TestGoogleApplyChangesDryRun(t *testing.T) {
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "qux.elb.amazonaws.com"),
}
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), true, originalEndpoints)
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), true, originalEndpoints)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
@ -475,12 +477,12 @@ func TestGoogleApplyChangesDryRun(t *testing.T) {
}
func TestGoogleApplyChangesEmpty(t *testing.T) {
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
assert.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{}))
}
func TestNewFilteredRecords(t *testing.T) {
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
records := provider.newFilteredRecords([]*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 1, "8.8.4.4"),
@ -670,7 +672,7 @@ func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected
assert.Equal(t, expected.Type, record.Type)
}
func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider {
func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider {
provider := &GoogleProvider{
project: "zalando-external-dns-test",
dryRun: false,
@ -705,7 +707,7 @@ func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilt
}
func newGoogleProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider {
func newGoogleProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider {
provider := &GoogleProvider{
project: "zalando-external-dns-test",
dryRun: false,
@ -792,3 +794,7 @@ func clearGoogleRecords(t *testing.T, provider *GoogleProvider, zone string) {
require.NoError(t, err)
}
}
func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %s:%s", endpoints, expected)
}

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package infoblox
import (
"context"
@ -28,7 +28,9 @@ import (
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
type mockIBConnector struct {
@ -329,7 +331,7 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject
return nil
}
func newInfobloxProvider(domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, client ibclient.IBConnector) *InfobloxProvider {
func newInfobloxProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, client ibclient.IBConnector) *InfobloxProvider {
return &InfobloxProvider{
client: client,
domainFilter: domainFilter,
@ -354,7 +356,7 @@ func TestInfobloxRecords(t *testing.T) {
},
}
provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, &client)
provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, &client)
actual, err := provider.Records(context.Background())
if err != nil {
@ -428,7 +430,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
provider := newInfobloxProvider(
endpoint.NewDomainFilter([]string{""}),
NewZoneIDFilter([]string{""}),
provider.NewZoneIDFilter([]string{""}),
dryRun,
client,
)
@ -486,7 +488,7 @@ func TestInfobloxZones(t *testing.T) {
mockInfobloxObjects: &[]ibclient.IBObject{},
}
provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, &client)
provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, &client)
zones, _ := provider.zones()
var emptyZoneAuth *ibclient.ZoneAuth
assert.Equal(t, provider.findZone(zones, "example.com").Fqdn, "example.com")
@ -521,3 +523,7 @@ func TestMaxResultsRequestBuilder(t *testing.T) {
assert.True(t, req.URL.Query().Get("_max_results") == "")
}
func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %s:%s", endpoints, expected)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package oci
import (
"context"
@ -28,6 +28,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
type mockOCIDNSClient struct{}
@ -100,7 +101,7 @@ func (c *mockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.Pat
}
// newOCIProvider creates an OCI provider with API calls mocked out.
func newOCIProvider(client ociDNSClient, domainFilter endpoint.DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) *OCIProvider {
func newOCIProvider(client ociDNSClient, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) *OCIProvider {
return &OCIProvider{
client: client,
cfg: OCIConfig{
@ -176,7 +177,7 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K
`,
},
},
err: errors.New("initialising OCI DNS API client: can not create client, bad configuration: PEM data was not found in buffer"),
err: errors.New("initializing OCI DNS API client: can not create client, bad configuration: PEM data was not found in buffer"),
},
}
for name, tc := range testCases {
@ -184,7 +185,7 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K
_, err := NewOCIProvider(
tc.config,
endpoint.NewDomainFilter([]string{"com"}),
NewZoneIDFilter([]string{""}),
provider.NewZoneIDFilter([]string{""}),
false,
)
if err == nil {
@ -200,13 +201,13 @@ func TestOCIZones(t *testing.T) {
testCases := []struct {
name string
domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter
zoneIDFilter provider.ZoneIDFilter
expected map[string]dns.ZoneSummary
}{
{
name: "DomainFilter_com",
domainFilter: endpoint.NewDomainFilter([]string{"com"}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
expected: map[string]dns.ZoneSummary{
"foo.com": {
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
@ -220,7 +221,7 @@ func TestOCIZones(t *testing.T) {
}, {
name: "DomainFilter_foo.com",
domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
expected: map[string]dns.ZoneSummary{
"foo.com": {
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
@ -230,7 +231,7 @@ func TestOCIZones(t *testing.T) {
}, {
name: "ZoneIDFilter_ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959",
domainFilter: endpoint.NewDomainFilter([]string{""}),
zoneIDFilter: NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"}),
zoneIDFilter: provider.NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"}),
expected: map[string]dns.ZoneSummary{
"foo.com": {
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
@ -253,13 +254,13 @@ func TestOCIRecords(t *testing.T) {
testCases := []struct {
name string
domainFilter endpoint.DomainFilter
zoneIDFilter ZoneIDFilter
zoneIDFilter provider.ZoneIDFilter
expected []*endpoint.Endpoint
}{
{
name: "unfiltered",
domainFilter: endpoint.NewDomainFilter([]string{""}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
@ -269,7 +270,7 @@ func TestOCIRecords(t *testing.T) {
}, {
name: "DomainFilter_foo.com",
domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
@ -278,7 +279,7 @@ func TestOCIRecords(t *testing.T) {
}, {
name: "ZoneIDFilter_ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404",
domainFilter: endpoint.NewDomainFilter([]string{""}),
zoneIDFilter: NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"}),
zoneIDFilter: provider.NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"}),
expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("foo.bar.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
},
@ -826,7 +827,7 @@ func TestOCIApplyChanges(t *testing.T) {
provider := newOCIProvider(
client,
endpoint.NewDomainFilter([]string{""}),
NewZoneIDFilter([]string{""}),
provider.NewZoneIDFilter([]string{""}),
tc.dryRun,
)

View File

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

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package pdns
import (
"bytes"
@ -35,6 +35,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/tlsutils"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
type pdnsChangeType string
@ -116,7 +117,6 @@ func (tlsConfig *TLSConfig) setHTTPClient(pdnsClientConfig *pgo.Configuration) e
// Function for debug printing
func stringifyHTTPResponseBody(r *http.Response) (body string) {
if r == nil {
return ""
}
@ -125,7 +125,6 @@ func stringifyHTTPResponseBody(r *http.Response) (body string) {
buf.ReadFrom(r.Body)
body = buf.String()
return body
}
// PDNSAPIProvider : Interface used and extended by the PDNSAPIClient struct as
@ -161,7 +160,6 @@ func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err
log.Errorf("Unable to fetch zones. %v", err)
return zones, resp, err
}
// PartitionZones : Method returns a slice of zones that adhere to the domain filter and a slice of ones that does not adhere to the filter
@ -190,14 +188,12 @@ func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Respo
log.Debugf("Retrying ListZone() ... %d", i)
time.Sleep(retryAfterTime * (1 << uint(i)))
continue
}
return zone, resp, err
}
log.Errorf("Unable to list zone. %v", err)
return zone, resp, err
}
// PatchZone : Method used to update the contents of a particular zone from PowerDNS
@ -210,7 +206,6 @@ func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *htt
log.Debugf("Retrying PatchZone() ... %d", i)
time.Sleep(retryAfterTime * (1 << uint(i)))
continue
}
return resp, err
}
@ -221,16 +216,16 @@ func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *htt
// PDNSProvider is an implementation of the Provider interface for PowerDNS
type PDNSProvider struct {
provider.BaseProvider
client PDNSAPIProvider
}
// NewPDNSProvider initializes a new PowerDNS based Provider.
func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, error) {
// Do some input validation
if config.APIKey == "" {
return nil, errors.New("Missing API Key for PDNS. Specify using --pdns-api-key=")
return nil, errors.New("missing API Key for PDNS. Specify using --pdns-api-key=")
}
// We do not support dry running, exit safely instead of surprising the user
@ -257,7 +252,6 @@ func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, err
domainFilter: config.DomainFilter,
},
}
return provider, nil
}
@ -270,13 +264,11 @@ func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpo
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rr.Type_, endpoint.TTL(rr.Ttl), record.Content))
}
}
return endpoints, nil
}
// ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs
func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) {
zonelist = []pgo.Zone{}
endpoints := make([]*endpoint.Endpoint, len(eps))
copy(endpoints, eps)
@ -310,15 +302,15 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
zone.Rrsets = []pgo.RrSet{}
for i := 0; i < len(endpoints); {
ep := endpoints[i]
dnsname := ensureTrailingDot(ep.DNSName)
dnsname := provider.EnsureTrailingDot(ep.DNSName)
if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) {
// The assumption here is that there will only ever be one target
// per (ep.DNSName, ep.RecordType) tuple, which holds true for
// external-dns v5.0.0-alpha onwards
records := []pgo.Record{}
for _, t := range ep.Targets {
if "CNAME" == ep.RecordType {
t = ensureTrailingDot(t)
if ep.RecordType == "CNAME" {
t = provider.EnsureTrailingDot(t)
}
records = append(records, pgo.Record{Content: t})
@ -333,7 +325,7 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
// DELETEs explicitly forbid a TTL, therefore only PATCHes need the TTL
if changetype == PdnsReplace {
if int64(ep.RecordTTL) > int64(math.MaxInt32) {
return nil, errors.New("Value of record TTL overflows, limited to int32")
return nil, errors.New("value of record TTL overflows, limited to int32")
}
if ep.RecordTTL == 0 {
// No TTL was specified for the record, we use the default
@ -351,13 +343,10 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
// If we didn't pop anything, we move to the next item in the list
i++
}
}
if len(zone.Rrsets) > 0 {
zonelist = append(zonelist, zone)
}
}
// residualZones is unsorted by name length like its counterpart
@ -365,7 +354,7 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
for _, zone := range residualZones {
for i := 0; i < len(endpoints); {
ep := endpoints[i]
dnsname := ensureTrailingDot(ep.DNSName)
dnsname := provider.EnsureTrailingDot(ep.DNSName)
if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) {
// "pop" endpoint if it's matched to a residual zone... essentially a no-op
log.Debugf("Ignoring Endpoint because it was matched to a zone that was not specified within Domain Filter(s): %s", dnsname)
@ -375,7 +364,6 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
}
}
}
// If we still have some endpoints left, it means we couldn't find a matching zone (filtered or residual) for them
// We warn instead of hard fail here because we don't want a misconfig to cause everything to go down
if len(endpoints) > 0 {
@ -400,20 +388,17 @@ func (p *PDNSProvider) mutateRecords(endpoints []*endpoint.Endpoint, changetype
} else {
log.Debugf("Struct for PatchZone:\n%s", string(jso))
}
resp, err := p.client.PatchZone(zone.Id, zone)
if err != nil {
log.Debugf("PDNS API response: %s", stringifyHTTPResponseBody(resp))
return err
}
}
return nil
}
// Records returns all DNS records controlled by the configured PDNS server (for all zones)
func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
zones, _, err := p.client.ListZones()
if err != nil {
return nil, err
@ -443,7 +428,6 @@ func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpo
// ApplyChanges takes a list of changes (endpoints) and updates the PDNS server
// by sending the correct HTTP PATCH requests to a matching zone
func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
startTime := time.Now()
// Create
@ -491,7 +475,6 @@ func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes)
return err
}
}
log.Debugf("Changes pushed out to PowerDNS in %s\n", time.Since(startTime))
return nil
}

View File

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

View File

@ -29,6 +29,14 @@ import (
type Provider interface {
Records(ctx context.Context) ([]*endpoint.Endpoint, error)
ApplyChanges(ctx context.Context, changes *plan.Changes) error
PropertyValuesEqual(name string, previous string, current string) bool
}
type BaseProvider struct {
}
func (b BaseProvider) PropertyValuesEqual(name, previous, current string) bool {
return previous == current
}
type contextKey struct {
@ -42,11 +50,34 @@ func (k *contextKey) String() string { return "provider context value " + k.name
// type []*endpoint.Endpoint.
var RecordsContextKey = &contextKey{"records"}
// ensureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already.
func ensureTrailingDot(hostname string) string {
// EnsureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already.
func EnsureTrailingDot(hostname string) string {
if net.ParseIP(hostname) != nil {
return hostname
}
return strings.TrimSuffix(hostname, ".") + "."
}
// Difference tells which entries need to be respectively
// added, removed, or left untouched for "current" to be transformed to "desired"
func Difference(current, desired []string) ([]string, []string, []string) {
add, remove, leave := []string{}, []string{}, []string{}
index := make(map[string]struct{}, len(current))
for _, x := range current {
index[x] = struct{}{}
}
for _, x := range desired {
if _, found := index[x]; found {
leave = append(leave, x)
delete(index, x)
} else {
add = append(add, x)
delete(index, x)
}
}
for x := range index {
remove = append(remove, x)
}
return add, remove, leave
}

View File

@ -17,9 +17,19 @@ limitations under the License.
package provider
import (
"io/ioutil"
"os"
"testing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
log.SetOutput(ioutil.Discard)
os.Exit(m.Run())
}
func TestEnsureTrailingDot(t *testing.T) {
for _, tc := range []struct {
input, expected string
@ -28,10 +38,28 @@ func TestEnsureTrailingDot(t *testing.T) {
{"example.org.", "example.org."},
{"8.8.8.8", "8.8.8.8"},
} {
output := ensureTrailingDot(tc.input)
output := EnsureTrailingDot(tc.input)
if output != tc.expected {
t.Errorf("expected %s, got %s", tc.expected, output)
}
}
}
func TestDifference(t *testing.T) {
current := []string{"foo", "bar"}
desired := []string{"bar", "baz"}
add, remove, leave := Difference(current, desired)
assert.Equal(t, add, []string{"baz"})
assert.Equal(t, remove, []string{"foo"})
assert.Equal(t, leave, []string{"bar"})
}
func TestBaseProviderPropertyEquality(t *testing.T) {
p := BaseProvider{}
assert.True(t, p.PropertyValuesEqual("some.property", "", ""), "Both properties not present")
assert.False(t, p.PropertyValuesEqual("some.property", "", "Foo"), "First property missing")
assert.False(t, p.PropertyValuesEqual("some.property", "Foo", ""), "Second property missing")
assert.True(t, p.PropertyValuesEqual("some.property", "Foo", "Foo"), "Properties the same")
assert.False(t, p.PropertyValuesEqual("some.property", "Foo", "Bar"), "Attributes differ")
}

View File

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

View File

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

View File

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

View File

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

View File

@ -16,9 +16,9 @@ limitations under the License.
package provider
// supportedRecordType returns true only for supported record types.
// SupportedRecordType returns true only for supported record types.
// Currently A, CNAME, SRV, and TXT record types are supported.
func supportedRecordType(recordType string) bool {
func SupportedRecordType(recordType string) bool {
switch recordType {
case "A", "CNAME", "SRV", "TXT":
return true

View File

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

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package rfc2136
import (
"context"
@ -29,6 +29,7 @@ import (
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
type rfc2136Stub struct {
@ -93,7 +94,7 @@ func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelo
return outChan, nil
}
func createRfc2136StubProvider(stub *rfc2136Stub) (Provider, error) {
func createRfc2136StubProvider(stub *rfc2136Stub) (provider.Provider, error) {
return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, stub)
}
@ -246,3 +247,90 @@ func TestRfc2136ApplyChangesWithDifferentTTLs(t *testing.T) {
assert.True(t, strings.Contains(createRecords[2], "300"))
}
func TestRfc2136ApplyChangesWithUpdate(t *testing.T) {
stub := newStub()
provider, err := createRfc2136StubProvider(stub)
assert.NoError(t, err)
p := &plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "v1.foo.com",
RecordType: "A",
Targets: []string{"1.2.3.4"},
RecordTTL: endpoint.TTL(400),
},
{
DNSName: "v1.foobar.com",
RecordType: "TXT",
Targets: []string{"boom"},
},
},
}
err = provider.ApplyChanges(context.Background(), p)
assert.NoError(t, err)
p = &plan.Changes{
UpdateOld: []*endpoint.Endpoint{
{
DNSName: "v1.foo.com",
RecordType: "A",
Targets: []string{"1.2.3.4"},
RecordTTL: endpoint.TTL(400),
},
{
DNSName: "v1.foobar.com",
RecordType: "TXT",
Targets: []string{"boom"},
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "v1.foo.com",
RecordType: "A",
Targets: []string{"1.2.3.5"},
RecordTTL: endpoint.TTL(400),
},
{
DNSName: "v1.foobar.com",
RecordType: "TXT",
Targets: []string{"kablui"},
},
},
}
err = provider.ApplyChanges(context.Background(), p)
assert.NoError(t, err)
assert.Equal(t, 4, len(stub.createMsgs))
assert.Equal(t, 2, len(stub.updateMsgs))
assert.True(t, strings.Contains(stub.createMsgs[0].String(), "v1.foo.com"))
assert.True(t, strings.Contains(stub.createMsgs[0].String(), "1.2.3.4"))
assert.True(t, strings.Contains(stub.createMsgs[2].String(), "v1.foo.com"))
assert.True(t, strings.Contains(stub.createMsgs[2].String(), "1.2.3.5"))
assert.True(t, strings.Contains(stub.updateMsgs[0].String(), "v1.foo.com"))
assert.True(t, strings.Contains(stub.updateMsgs[0].String(), "1.2.3.4"))
assert.True(t, strings.Contains(stub.createMsgs[1].String(), "v1.foobar.com"))
assert.True(t, strings.Contains(stub.createMsgs[1].String(), "boom"))
assert.True(t, strings.Contains(stub.createMsgs[3].String(), "v1.foobar.com"))
assert.True(t, strings.Contains(stub.createMsgs[3].String(), "kablui"))
assert.True(t, strings.Contains(stub.updateMsgs[1].String(), "v1.foobar.com"))
assert.True(t, strings.Contains(stub.updateMsgs[1].String(), "boom"))
}
func contains(arr []*endpoint.Endpoint, name string) bool {
for _, a := range arr {
if a.DNSName == name {
return true
}
}
return false
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,13 +18,13 @@ package provider
import "strings"
type zoneIDName map[string]string
type ZoneIDName map[string]string
func (z zoneIDName) Add(zoneID, zoneName string) {
func (z ZoneIDName) Add(zoneID, zoneName string) {
z[zoneID] = zoneName
}
func (z zoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) {
func (z ZoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) {
for zoneID, zoneName := range z {
if hostname == zoneName || strings.HasSuffix(hostname, "."+zoneName) {
if suitableZoneName == "" || len(zoneName) > len(suitableZoneName) {

View File

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

View File

@ -87,3 +87,7 @@ func (sdr *AWSSDRegistry) updateLabels(endpoints []*endpoint.Endpoint) {
ep.Labels[endpoint.AWSSDDescriptionLabel] = ep.Labels.Serialize(false)
}
}
func (sdr *AWSSDRegistry) PropertyValuesEqual(name string, previous string, current string) bool {
return sdr.provider.PropertyValuesEqual(name, previous, current)
}

View File

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

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

View File

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

View File

@ -43,12 +43,16 @@ type TXTRegistry struct {
}
// NewTXTRegistry returns new TXTRegistry object
func NewTXTRegistry(provider provider.Provider, txtPrefix, ownerID string, cacheInterval time.Duration) (*TXTRegistry, error) {
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration) (*TXTRegistry, error) {
if ownerID == "" {
return nil, errors.New("owner id cannot be empty")
}
mapper := newPrefixNameMapper(txtPrefix)
if len(txtPrefix) > 0 && len(txtSuffix) > 0 {
return nil, errors.New("txt-prefix and txt-suffix are mutual exclusive")
}
mapper := newaffixNameMapper(txtPrefix, txtSuffix)
return &TXTRegistry{
provider: provider,
@ -187,6 +191,11 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
return im.provider.ApplyChanges(ctx, filteredChanges)
}
// PropertyValuesEqual compares two attribute values for equality
func (im *TXTRegistry) PropertyValuesEqual(name string, previous string, current string) bool {
return im.provider.PropertyValuesEqual(name, previous, current)
}
/**
TXT registry specific private methods
*/
@ -201,26 +210,35 @@ type nameMapper interface {
toTXTName(string) string
}
type prefixNameMapper struct {
type affixNameMapper struct {
prefix string
suffix string
}
var _ nameMapper = prefixNameMapper{}
var _ nameMapper = affixNameMapper{}
func newPrefixNameMapper(prefix string) prefixNameMapper {
return prefixNameMapper{prefix: strings.ToLower(prefix)}
func newaffixNameMapper(prefix string, suffix string) affixNameMapper {
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix)}
}
func (pr prefixNameMapper) toEndpointName(txtDNSName string) string {
func (pr affixNameMapper) toEndpointName(txtDNSName string) string {
lowerDNSName := strings.ToLower(txtDNSName)
if strings.HasPrefix(lowerDNSName, pr.prefix) {
if strings.HasPrefix(lowerDNSName, pr.prefix) && len(pr.suffix) == 0 {
return strings.TrimPrefix(lowerDNSName, pr.prefix)
}
if len(pr.suffix) > 0 {
DNSName := strings.SplitN(lowerDNSName, ".", 2)
if strings.HasSuffix(DNSName[0], pr.suffix) {
return strings.TrimSuffix(DNSName[0], pr.suffix) + "." + DNSName[1]
}
}
return ""
}
func (pr prefixNameMapper) toTXTName(endpointDNSName string) string {
return pr.prefix + endpointDNSName
func (pr affixNameMapper) toTXTName(endpointDNSName string) string {
DNSName := strings.SplitN(endpointDNSName, ".", 2)
return pr.prefix + DNSName[0] + pr.suffix + "." + DNSName[1]
}
func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) {

View File

@ -29,6 +29,7 @@ import (
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/provider/inmemory"
)
const (
@ -42,33 +43,44 @@ func TestTXTRegistry(t *testing.T) {
}
func testTXTRegistryNew(t *testing.T) {
p := provider.NewInMemoryProvider()
_, err := NewTXTRegistry(p, "txt", "", time.Hour)
p := inmemory.NewInMemoryProvider()
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour)
require.Error(t, err)
r, err := NewTXTRegistry(p, "txt", "owner", time.Hour)
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour)
require.Error(t, err)
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour)
require.NoError(t, err)
assert.Equal(t, p, r.provider)
r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour)
require.NoError(t, err)
_, ok := r.mapper.(prefixNameMapper)
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour)
require.Error(t, err)
_, ok := r.mapper.(affixNameMapper)
require.True(t, ok)
assert.Equal(t, "owner", r.ownerID)
assert.Equal(t, p, r.provider)
r, err = NewTXTRegistry(p, "", "owner", time.Hour)
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour)
require.NoError(t, err)
_, ok = r.mapper.(prefixNameMapper)
_, ok = r.mapper.(affixNameMapper)
assert.True(t, ok)
}
func testTXTRegistryRecords(t *testing.T) {
t.Run("With prefix", testTXTRegistryRecordsPrefixed)
t.Run("With suffix", testTXTRegistryRecordsSuffixed)
t.Run("No prefix", testTXTRegistryRecordsNoPrefix)
}
func testTXTRegistryRecordsPrefixed(t *testing.T) {
ctx := context.Background()
p := provider.NewInMemoryProvider()
p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone)
p.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{
@ -159,20 +171,125 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "TxT.", "owner", time.Hour)
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour)
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
}
func testTXTRegistryRecordsSuffixed(t *testing.T) {
ctx := context.Background()
p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone)
p.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerAndLabels("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"foo": "somefoo"}),
newEndpointWithOwnerAndLabels("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"bar": "somebar"}),
newEndpointWithOwner("bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("bar-txt.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""),
newEndpointWithOwnerAndLabels("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "", endpoint.Labels{"tar": "sometar"}),
newEndpointWithOwner("tar-TxT.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), // case-insensitive TXT prefix
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
})
expectedRecords := []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
"foo": "somefoo",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner",
"bar": "somebar",
},
},
{
DNSName: "bar-txt.test-zone.example.org",
Targets: endpoint.Targets{"baz.test-zone.example.org"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "qux.test-zone.example.org",
Targets: endpoint.Targets{"random"},
RecordType: endpoint.RecordTypeTXT,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "tar.test-zone.example.org",
Targets: endpoint.Targets{"tar.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner-2",
"tar": "sometar",
},
},
{
DNSName: "foobar.test-zone.example.org",
Targets: endpoint.Targets{"foobar.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "multiple.test-zone.example.org",
Targets: endpoint.Targets{"lb1.loadbalancer.com"},
SetIdentifier: "test-set-1",
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "multiple.test-zone.example.org",
Targets: endpoint.Targets{"lb2.loadbalancer.com"},
SetIdentifier: "test-set-2",
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
}
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour)
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
}
func testTXTRegistryRecordsNoPrefix(t *testing.T) {
p := provider.NewInMemoryProvider()
p := inmemory.NewInMemoryProvider()
ctx := context.Background()
p.CreateZone(testZone)
p.ApplyChanges(ctx, &plan.Changes{
@ -240,7 +357,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -248,11 +365,12 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
func testTXTRegistryApplyChanges(t *testing.T) {
t.Run("With Prefix", testTXTRegistryApplyChangesWithPrefix)
t.Run("With Suffix", testTXTRegistryApplyChangesWithSuffix)
t.Run("No prefix", testTXTRegistryApplyChangesNoPrefix)
}
func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
p := provider.NewInMemoryProvider()
p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone)
ctxEndpoints := []*endpoint.Endpoint{}
ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints)
@ -276,7 +394,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
})
r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
@ -342,8 +460,99 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
require.NoError(t, err)
}
func testTXTRegistryApplyChangesWithSuffix(t *testing.T) {
p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone)
ctxEndpoints := []*endpoint.Endpoint{}
ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints)
p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) {
assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey))
}
p.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("bar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("bar-txt.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
})
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"),
},
UpdateNew: []*endpoint.Endpoint{
newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"),
},
UpdateOld: []*endpoint.Endpoint{
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"),
},
}
expected := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"),
newEndpointWithOwner("new-record-1-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "owner", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"),
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-3"),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwner("foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"),
},
UpdateNew: []*endpoint.Endpoint{
newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"),
newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
UpdateOld: []*endpoint.Endpoint{
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwner("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
}
p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) {
mExpected := map[string][]*endpoint.Endpoint{
"Create": expected.Create,
"UpdateNew": expected.UpdateNew,
"UpdateOld": expected.UpdateOld,
"Delete": expected.Delete,
}
mGot := map[string][]*endpoint.Endpoint{
"Create": got.Create,
"UpdateNew": got.UpdateNew,
"UpdateOld": got.UpdateOld,
"Delete": got.Delete,
}
assert.True(t, testutils.SamePlanChanges(mGot, mExpected))
assert.Equal(t, nil, ctx.Value(provider.RecordsContextKey))
}
err := r.ApplyChanges(ctx, changes)
require.NoError(t, err)
}
func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
p := provider.NewInMemoryProvider()
p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone)
ctxEndpoints := []*endpoint.Endpoint{}
ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints)
@ -363,7 +572,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
},
})
r, _ := NewTXTRegistry(p, "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
@ -485,11 +694,9 @@ func newEndpointWithOwner(dnsName, target, recordType, ownerID string) *endpoint
func newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID string, labels endpoint.Labels) *endpoint.Endpoint {
e := endpoint.NewEndpoint(dnsName, recordType, target)
e.Labels[endpoint.OwnerLabelKey] = ownerID
if labels != nil {
for k, v := range labels {
e.Labels[k] = v
}
}
return e
}

View File

@ -17,8 +17,8 @@ limitations under the License.
package source
import (
"context"
"net/url"
"time"
cfclient "github.com/cloudfoundry-community/go-cfclient"
@ -36,7 +36,7 @@ func NewCloudFoundrySource(cfClient *cfclient.Client) (Source, error) {
}, nil
}
func (rs *cloudfoundrySource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) {
func (rs *cloudfoundrySource) AddEventHandler(ctx context.Context, handler func()) {
}
// Endpoints returns endpoint objects

View File

@ -17,6 +17,7 @@ limitations under the License.
package source
import (
"context"
"encoding/gob"
"net"
"time"
@ -65,5 +66,5 @@ func (cs *connectorSource) Endpoints() ([]*endpoint.Endpoint, error) {
return endpoints, nil
}
func (cs *connectorSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) {
func (cs *connectorSource) AddEventHandler(ctx context.Context, handler func()) {
}

View File

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

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