Merge branch 'master' into metrics-names

This commit is contained in:
Martin Linkhorst 2019-05-14 13:48:20 +02:00 committed by GitHub
commit a07bad86a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 6556 additions and 2633 deletions

19
.golangci.yml Normal file
View File

@ -0,0 +1,19 @@
run:
concurrency: 4
modules-download-mode: readonly
linters-settings:
golint:
min-confidence: 0.9
gocyclo:
min-complexity: 15
linters:
disable-all: true
enable:
- govet
- ineffassign
- golint
- goimports

View File

@ -1,4 +1,4 @@
dist: trusty
dist: xenial
os:
- linux
@ -6,27 +6,23 @@ os:
language: go
go:
- 1.x
- "1.12.x"
- tip
matrix:
allow_failures:
- go: tip
env:
- GO111MODULE=on GOLANGCI_RELEASE="v1.16.0"
before_install:
- make dep
- go get github.com/mattn/goveralls
- go get github.com/lawrencewoodman/roveralls
- go get github.com/alecthomas/gometalinter
install:
- gometalinter --install
env:
- GOMETALINTER_DEADLINE="600s"
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_RELEASE}
script:
- vendor/github.com/kubernetes/repo-infra/verify/verify-boilerplate.sh --rootdir=$(pwd)
- vendor/github.com/kubernetes/repo-infra/verify/verify-go-src.sh -v --rootdir $(pwd)
- make test
- make lint
- travis_wait 20 roveralls
- goveralls -coverprofile=roveralls.coverprofile -service=travis-ci

View File

@ -1,3 +1,83 @@
## v0.5.14 - 2019-04-23
- Core: Supress Kubernetes logs (#991) @njuettner
## v0.5.13 - 2019-04-18
- Azure: Support multiple A targets (#987) @michaelfig
- Core: Fixing what seems an obvious omission of /github.com/ dir in Dockerfile (#985) @llamahunter
- Docs: GKE tutorial remove disable-addon argument (#978) @ggordan
- Docs: Alibaba Cloud config file missing by enable sts token (#977) @xianlubird
- Docs: Alibaba Cloud fix wrong arg in manifest (#976) @iamzhout
- AWS: Set a default TTL for Alias records (#975) @fraenkel
- Cloudflare: Add support for multiple target addresses (#970) @nta
- AWS: Adding China ELB endpoints and hosted zone id's (#968) @jfillo
- AWS: Streamline ApplyChanges (#966) @fraenkel
- Core: Switch to go modules (#960) @njuettner
- Docs: AWS how to check if your cluster has a RBAC (#959) @confiq
- Docs: AWS remove superfluous trailing period from hostname (#952) @hobti01
- Core: Add generic logic to remove secrets from logs (#951) @dsbrng25b
- RFC2136: Remove unnecessary parameter (#948) @ChristianMoesl
- Infoblox: Reduce verbosity of logs (#945) @dsbrng25b
## v0.5.12 - 2019-03-26
- Bumping istio to 1.1.0 (#942) @venezia
- Docs: Added stability matrix and minor improvements to README (#938) @Raffo
- Docs: Added a reference to a blogpost which uses ExternalDNS in a CI/CD setup (#928) @vanhumbeecka
- Use k8s informer cache instead of making active API GET requests (#917) @jlamillan
- Docs: Tiny clarification about two available deployment methods (#935) @przemolb
- Add support for multiple Istio IngressGateway LoadBalancer Services (#907) @LorbusChris
- Set log level to debug when axfr is disabled (#932) @arief-hidayat
- Infoblox provider support for DNS view (#895) @dsbrng25b
- Add RcodeZero Anycast DNS provider (#874) @dklesev
- Docs: Dropping owners (#929) @njuettner
- Docs: Added description for multiple dns name (#911) @st1t
- Docs: Clarify that hosted zone identifier is to be used (#915) @dirkgomez
- Docs: Make dep step which may be needed to run make build (#913) @dirkgomez
- PowerDNS: Fixed Domain Filter Bug (#827) @anandsinghkunwar
- Allow hostname annotations to be ignored (#745) @anandkumarpatel
- RFC2136: Fixed typo in debug output (#899) @hpandeycodeit
## v0.5.11 - 2019-02-11
- Fix constant updating issue introduced with v0.5.10 (#886) @jhohertz
- Ignore evaluate target health for calculating changes for AWS (#880) @linki
- Pagination for cloudflare zones (#873) @njuettner
## v0.5.10 - 2019-01-28
- Docs: Improve documentation regarding Alias (#868) @alexnederlof
- Adds a new flag `--aws-api-retries` which allows overriding the number of retries (#858) @viafoura
- Docs: Make awscli commands use JSON output (#849) @ifosch
- Docs: Add missing apiVersion to Ingress resource (#847) @shlao
- Fix for AWS private DNS zone (#844) @xianlubird
- Add support for AWS ELBs in eu-north-1 (#843) @argoyle
- Create a SECURITY_CONTACTS file (#842) @njuettner
- Use correct product name for Google Cloud DNS (#841) @seils
- Change default AWSBatchChangeSize to 1000 (#839) @medzin
- Fix dry-run mode in rfc2136 provider (#838) @lachlancooper
- Fix typos in rfc2136 provider (#837) @lachlancooper
- rfc2136 provider: one IP Target per RRSET (#836) @ivanfilippov
- Normalize DNS names during planning (#833) @justinsb
- Implement Stringer for planTableRow (#832) @justinsb
- Docs: Better security granularity concerning external dns service principal for Azure (#829) @DenisBiondic
- Docs: Update links in Cloudflare docs (#824) @PascalKu
- Docs: Add metrics info to FAQ (#822) @zachyam
- Docs: Update nameserver IPs in coredns.md (#820) @mozhuli
- Docs: Fix commands to cleanup Cloudflare (#818) @acrogenesis
- Avoid unnecessary updating for CRD resource (#810) @xunpan
- Fix issues with CoreDNS provider and more than 1 targets (#807) @xunpan
- AWS: Add zone tag filter (#804) @csrwng
- Docs: Update CoreDNS tutorial with RBAC manifest (#803) @Lujeni
- Use SOAP API to improve DYN's provider's performance (#799) @sanyu
- Expose managed resources and records as metrics (#793) @linki
- Docs: Updating Azure tutorial (#788) @pelithne
- Improve errors in Records() of Infoblox provider (#785) @dsbrng25b
- Change default apiVersion of CRD Source (#774) @dsbrng25b
- Allow setting Cloudflare proxying on a per-Ingress basis (#650) @eswets
- Support A record for multiple IPs for headless services (#645) @toshipp
## v0.5.9 - 2018-11-22
- Core: Update delivery.yaml to new format (#782) @linki

View File

@ -13,11 +13,11 @@
# limitations under the License.
# builder image
FROM golang as builder
FROM golang:1.12.4 as builder
WORKDIR /go/src/github.com/kubernetes-incubator/external-dns
WORKDIR /github.com/kubernetes-incubator/external-dns
COPY . .
RUN make dep
RUN go mod vendor
RUN make test
RUN make build
@ -25,7 +25,7 @@ RUN make build
FROM registry.opensource.zalan.do/stups/alpine:latest
LABEL maintainer="Team Teapot @ Zalando SE <team-teapot@zalando.de>"
COPY --from=builder /go/src/github.com/kubernetes-incubator/external-dns/build/external-dns /bin/external-dns
COPY --from=builder /github.com/kubernetes-incubator/external-dns/build/external-dns /bin/external-dns
USER nobody

8
Dockerfile.mini Normal file
View File

@ -0,0 +1,8 @@
FROM golang:1.12.4 as builder
WORKDIR /external-dns
COPY . .
RUN make build
FROM gcr.io/distroless/static
COPY --from=builder /external-dns/build/external-dns /external-dns
ENTRYPOINT ["./external-dns"]

1182
Gopkg.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,117 +0,0 @@
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
required = ["github.com/kubernetes/repo-infra/verify/boilerplate/test"]
ignored = ["github.com/kubernetes/repo-infra/kazel"]
[[constraint]]
name = "github.com/Azure/azure-sdk-for-go"
version = "~10.0.4-beta"
[[constraint]]
name = "github.com/Azure/go-autorest"
version = "~10.9.0"
[[constraint]]
name = "github.com/alecthomas/kingpin"
version = "~2.2.4"
[[constraint]]
name = "github.com/aws/aws-sdk-go"
version = "~1.13.7"
[[constraint]]
name = "github.com/cloudflare/cloudflare-go"
version = "0.7.3"
[[constraint]]
name = "github.com/digitalocean/godo"
version = "~1.1.0"
[[constraint]]
name = "github.com/dnsimple/dnsimple-go"
version = "0.14.0"
[[constraint]]
branch = "master"
name = "github.com/infobloxopen/infoblox-go-client"
[[constraint]]
name = "github.com/linki/instrumented_http"
version = "0.2.0"
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.9.0-pre1"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "~1.0.3"
[[constraint]]
name = "github.com/stretchr/testify"
version = "~1.2.1"
[[override]]
name = "github.com/kubernetes/repo-infra"
branch = "master"
[[constraint]]
name = "github.com/nesv/go-dynect"
version = "0.6.0"
[[constraint]]
name = "github.com/exoscale/egoscale"
version = "~0.11.0"
[[constraint]]
name = "github.com/oracle/oci-go-sdk"
version = "1.8.0"
[[constraint]]
name = "github.com/linode/linodego"
version = "0.3.0"
[[constraint]]
name = "github.com/aliyun/alibaba-cloud-sdk-go"
version = "1.27.7"
[[constraint]]
name = "istio.io/istio"
version = "1.0.0"
[[override]]
name = "github.com/golang/glog"
source = "github.com/kubermatic/glog-logrus"
[[override]]
name = "github.com/golang/protobuf"
version = "1.1.0"
[[constraint]]
name = "k8s.io/client-go"
version = "8.0.0"
[[override]]
name = "k8s.io/apimachinery"
version = "kubernetes-1.11.0"
[[override]]
name = "k8s.io/api"
version = "kubernetes-1.11.0"
[[override]]
name = "golang.org/x/sys"
revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835"
[[override]]
name = "github.com/spf13/pflag"
version = "1.0.2"
[[override]]
name = "golang.org/x/net"
revision = "161cd47e91fd58ac17490ef4d742dc98bb4cf60e"
[[constraint]]
name = "github.com/miekg/dns"
version = "1.0.8"

View File

@ -27,9 +27,12 @@ cover:
cover-html: cover
go tool cover -html cover.out
dep:
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure -vendor-only
.PHONY: lint
# Run all the linters
lint:
golangci-lint run ./...
# The verify target runs tasks similar to the CI tasks, but without code coverage
.PHONY: verify test
@ -37,12 +40,8 @@ dep:
test:
go test -v -race $(shell go list ./... | grep -v /vendor/)
verify: test
vendor/github.com/kubernetes/repo-infra/verify/verify-boilerplate.sh --rootdir=${CURDIR}
vendor/github.com/kubernetes/repo-infra/verify/verify-go-src.sh -v --rootdir ${CURDIR}
# The build targets allow to build the binary and docker image
.PHONY: build build.docker
.PHONY: build build.docker build.mini
BINARY ?= external-dns
SOURCES = $(shell find . -name '*.go')
@ -62,5 +61,8 @@ build.push: build.docker
build.docker:
docker build --rm --tag "$(IMAGE):$(VERSION)" .
build.mini:
docker build --rm --tag "$(IMAGE):$(VERSION)" -f Dockerfile.mini .
clean:
@rm -rf build

5
OWNERS
View File

@ -2,12 +2,7 @@
# https://github.com/kubernetes/kubernetes/blob/master/docs/devel/owners.md
approvers:
- justinsb
- hjacobs
- raffo
- linki
- ideahitme
- chrislovecnm
- kris-nova
- iterion
- njuettner

View File

@ -13,22 +13,23 @@ ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS prov
## What It Does
Inspired by [Kubernetes DNS](https://github.com/kubernetes/dns), Kubernetes' cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers. Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the [Kubernetes API](https://kubernetes.io/docs/api/) to determine a desired list of DNS records. *Unlike* KubeDNS, however, it's not a DNS server itself, but merely configures other DNS providers accordingly—e.g. [AWS Route 53](https://aws.amazon.com/route53/) or [Google CloudDNS](https://cloud.google.com/dns/docs/).
Inspired by [Kubernetes DNS](https://github.com/kubernetes/dns), Kubernetes' cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers. Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the [Kubernetes API](https://kubernetes.io/docs/api/) to determine a desired list of DNS records. *Unlike* KubeDNS, however, it's not a DNS server itself, but merely configures other DNS providers accordingly—e.g. [AWS Route 53](https://aws.amazon.com/route53/) or [Google Cloud DNS](https://cloud.google.com/dns/docs/).
In a broader sense, ExternalDNS allows you to control DNS records dynamically via Kubernetes resources in a DNS provider-agnostic way.
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).
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).
## The Latest Release: v0.5
ExternalDNS' current release is `v0.5`. This version allows you to keep selected zones (via `--domain-filter`) synchronized with Ingresses and Services of `type=LoadBalancer` in various cloud providers:
* [Google CloudDNS](https://cloud.google.com/dns/docs/)
* [Google Cloud DNS](https://cloud.google.com/dns/docs/)
* [AWS Route 53](https://aws.amazon.com/route53/)
* [AWS Service Discovery](https://docs.aws.amazon.com/Route53/latest/APIReference/overview-service-discovery.html)
* [AzureDNS](https://azure.microsoft.com/en-us/services/dns)
* [CloudFlare](https://www.cloudflare.com/dns)
* [RcodeZero](https://www.rcodezero.at/)
* [DigitalOcean](https://www.digitalocean.com/products/networking)
* [DNSimple](https://dnsimple.com/)
* [Infoblox](https://www.infoblox.com/products/dns/)
@ -39,7 +40,10 @@ ExternalDNS' current release is `v0.5`. This version allows you to keep selected
* [Exoscale](https://www.exoscale.com/dns/)
* [Oracle Cloud Infrastructure DNS](https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm)
* [Linode DNS](https://www.linode.com/docs/networking/dns/)
* [RFC2136](https://tools.ietf.org/html/rfc2136)
* [RFC2136](https://tools.ietf.org/html/rfc2136)
* [NS1](https://ns1.com/)
* [TransIP](https://www.transip.eu/domain-name/)
From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API.
@ -47,7 +51,48 @@ Note that all flags can be replaced with environment variables; for instance,
`--dry-run` could be replaced with `EXTERNAL_DNS_DRY_RUN=1`, or
`--registry txt` could be replaced with `EXTERNAL_DNS_REGISTRY=txt`.
## Deploying to a Cluster
## Status of providers
ExternalDNS supports multiple DNS providers which have been implemented by the [ExternalDNS contributors](https://github.com/kubernetes-incubator/external-dns/graphs/contributors). Maintaining all of those in a central repository is a challenge and we have limited resources to test changes. This means that it is very hard to test all providers for possible regressions and, as written in the [Contributing](## Contributing) section, we encourage contributors to step in as maintainers for the individual providers and help by testing the integrations.
We define the following stability levels for providers:
- **Stable**: Used for smoke tests before a release, used in production and maintainers are active.
- **Beta**: Community supported, well tested, but maintainers have no access to resources to execute integration tests on the real platform and/or are not using it in production.
- **Alpha**: Community provided with no support from the maintainers apart from reviewing PRs.
The following table clarifies the current status of the providers according to the aforementioned stability levels:
| Provider | Status |
| -------- | ------ |
| Google Cloud DNS | Stable |
| AWS Route 53 | Stable |
| AWS Service Discovery | Beta |
| AzureDNS | Beta |
| CloudFlare | Beta
| RcodeZero | Alpha |
| DigitalOcean | Alpha |
| DNSimple | Alpha |
| Infoblox | Alpha |
| Dyn | Alpha |
| OpenStack Designate | Alpha |
| PowerDNS | Alpha |
| CoreDNS | Alpha |
| Exoscale | Alpha |
| Oracle Cloud Infrastructure DNS | Alpha |
| Linode DNS | Alpha |
| RFC2136 | Alpha |
| NS1 | Alpha |
| TransIP | Alpha |
## Running ExternalDNS:
The are two ways of running ExternalDNS:
* Deploying to a Cluster
* Running Locally
### Deploying to a Cluster
The following tutorials are provided:
@ -57,6 +102,7 @@ The following tutorials are provided:
* [Azure](docs/tutorials/azure.md)
* [CoreDNS](docs/tutorials/coredns.md)
* [Cloudflare](docs/tutorials/cloudflare.md)
* [RcodeZero](docs/tutorials/rcodezero.md)
* [DigitalOcean](docs/tutorials/digitalocean.md)
* [Infoblox](docs/tutorials/infoblox.md)
* [Dyn](docs/tutorials/dyn.md)
@ -67,25 +113,35 @@ The following tutorials are provided:
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
* [Linode](docs/tutorials/linode.md)
* [RFC2136](docs/tutorials/rfc2136.md)
* [NS1](docs/tutorials/ns1.md)
* [TransIP](docs/tutorials/transip.md)
## Running Locally
### Running Locally
### Technical Requirements
#### Technical Requirements
Make sure you have the following prerequisites:
* A local Go 1.7+ development environment.
* Access to a Google/AWS account with the DNS API enabled.
* Access to a Kubernetes cluster that supports exposing Services, e.g. GKE.
### Setup Steps
#### Setup Steps
First, get ExternalDNS:
**To install all dependencies, make sure to install [dep](https://github.com/golang/dep) first.**
```console
$ git clone https://github.com/kubernetes-incubator/external-dns.git && cd external-dns
$ dep ensure -vendor-only
```
**This project uses [Go modules](https://github.com/golang/go/wiki/Modules) as
introduced in Go 1.11 therefore you need Go >=1.11 installed in order to build.**
If using Go 1.11 you also need to [activate Module
support](https://github.com/golang/go/wiki/Modules#installing-and-activating-module-support).
Assuming Go has been setup with module support it can be built simply by running:
```console
$ export GO111MODULE=on # needed if the project is checked out in your $GOPATH.
$ make
```
@ -180,6 +236,9 @@ Here's a rough outline on what is to come (subject to change):
- [x] Support for OpenStack Designate
- [x] Support for PowerDNS
- [x] Support for Linode
- [x] Support for RcodeZero
- [x] Support for NS1
- [x] Support for TransIP
### v0.6

15
SECURITY_CONTACTS Normal file
View File

@ -0,0 +1,15 @@
# Defined below are the security contacts for this repo.
#
# They are the contact point for the Product Security Team to reach out
# to for triaging and handling of incoming issues.
#
# The below names agree to abide by the
# [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy)
# and will be removed and replaced if they violate that agreement.
#
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://kubernetes.io/security/
njuettner
hjacobs
raffo

View File

@ -17,12 +17,14 @@ limitations under the License.
package controller
import (
"context"
"time"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"github.com/kubernetes-incubator/external-dns/plan"
"github.com/kubernetes-incubator/external-dns/provider"
"github.com/kubernetes-incubator/external-dns/registry"
"github.com/kubernetes-incubator/external-dns/source"
)
@ -44,11 +46,29 @@ var (
Help: "Number of Source errors.",
},
)
sourceEndpointsTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "source",
Name: "endpoints_total",
Help: "Number of Endpoints in all sources",
},
)
registryEndpointsTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "registry",
Name: "endpoints_total",
Help: "Number of Endpoints in the registry",
},
)
)
func init() {
prometheus.MustRegister(registryErrorsTotal)
prometheus.MustRegister(sourceErrorsTotal)
prometheus.MustRegister(sourceEndpointsTotal)
prometheus.MustRegister(registryEndpointsTotal)
}
// Controller is responsible for orchestrating the different components.
@ -73,12 +93,16 @@ func (c *Controller) RunOnce() error {
registryErrorsTotal.Inc()
return err
}
registryEndpointsTotal.Set(float64(len(records)))
ctx := context.WithValue(context.Background(), provider.RecordsContextKey, records)
endpoints, err := c.Source.Endpoints()
if err != nil {
sourceErrorsTotal.Inc()
return err
}
sourceEndpointsTotal.Set(float64(len(endpoints)))
plan := &plan.Plan{
Policies: []plan.Policy{c.Policy},
@ -88,7 +112,7 @@ func (c *Controller) RunOnce() error {
plan = plan.Calculate()
err = c.Registry.ApplyChanges(plan.Changes)
err = c.Registry.ApplyChanges(ctx, plan.Changes)
if err != nil {
registryErrorsTotal.Inc()
return err

View File

@ -17,7 +17,9 @@ limitations under the License.
package controller
import (
"context"
"errors"
"reflect"
"testing"
"github.com/kubernetes-incubator/external-dns/endpoint"
@ -42,7 +44,7 @@ func (p *mockProvider) Records() ([]*endpoint.Endpoint, error) {
}
// ApplyChanges validates that the passed in changes satisfy the assumtions.
func (p *mockProvider) ApplyChanges(changes *plan.Changes) error {
func (p *mockProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
if len(changes.Create) != len(p.ExpectChanges.Create) {
return errors.New("number of created records is wrong")
}
@ -71,6 +73,9 @@ func (p *mockProvider) ApplyChanges(changes *plan.Changes) error {
}
}
if !reflect.DeepEqual(ctx.Value(provider.RecordsContextKey), p.RecordsStore) {
return errors.New("context is wrong")
}
return nil
}

View File

@ -1,15 +1,23 @@
version: "2017-09-20"
pipeline:
- id: build
overlay: ci/golang
cache:
paths:
- /go/pkg/mod # pkg cache for Go modules
- ~/.cache/go-build # Go build cache
type: script
commands:
- desc: Build and push Docker image
- desc: build
cmd: |
make build.docker
- desc: push
cmd: |
if [[ $CDP_TARGET_BRANCH == master && ! $CDP_PULL_REQUEST_NUMBER ]]; then
RELEASE_VERSION=$(git describe --tags --always --dirty)
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns:${RELEASE_VERSION}
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns
VERSION=$(git describe --tags --always --dirty)
else
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns-test:${CDP_BUILD_VERSION}
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns-test
VERSION=$CDP_BUILD_VERSION
fi
docker build --squash --tag "$IMAGE" .
docker push "$IMAGE"
IMAGE=$IMAGE VERSION=$VERSION make build.push

View File

@ -14,7 +14,11 @@ Here is typical example of [CRD API type](https://github.com/kubernetes-incubato
```go
type TTL int64
type Targets []string
type ProviderSpecific map[string]string
type ProviderSpecificProperty struct {
Name string
Value string
}
type ProviderSpecific []ProviderSpecificProperty
type Endpoint struct {
// The hostname of the DNS record

View File

@ -33,7 +33,14 @@ spec:
labels:
type: object
providerSpecific:
type: object
items:
properties:
name:
type: string
value:
type: string
type: object
type: array
recordTTL:
format: int64
type: integer

View File

@ -2,7 +2,7 @@
### Building
You can build ExternalDNS for your platform with `make build`. The binary will land at `build/external-dns`.
You can build ExternalDNS for your platform with `make build`, you may have to install the necessary dependencies with `make dep`. The binary will land at `build/external-dns`.
### Design
@ -14,7 +14,7 @@ This list of endpoints is passed to the [Plan](../../plan) which determines the
Once the difference has been figured out the list of intended changes is passed to a `Registry` which live in the [registry](../../registry) package. The registry is a wrapper and access point to DNS provider. Registry implements the ownership concept by marking owned records and filtering out records not owned by ExternalDNS before passing them to DNS provider.
The [provider](../../provider) is the adapter to the DNS provider, e.g. Google CloudDNS. It implements two methods: `ApplyChanges` to apply a set of changes filtered by `Registry` and `Records` to retrieve the current list of records from the DNS provider.
The [provider](../../provider) is the adapter to the DNS provider, e.g. Google Cloud DNS. It implements two methods: `ApplyChanges` to apply a set of changes filtered by `Registry` and `Records` to retrieve the current list of records from the DNS provider.
The orchestration between the different components is controlled by the [controller](../../controller).

View File

@ -29,7 +29,7 @@ All sources live in package `source`.
### Providers
Providers are an abstraction over any kind of sink for desired Endpoints, e.g.:
* storing them in Google CloudDNS
* storing them in Google Cloud DNS
* printing them to stdout for testing purposes
* fanning out to multiple nested providers
@ -46,7 +46,7 @@ The interface tries to be generic and assumes a flat list of records for both fu
All providers live in package `provider`.
* `GoogleProvider`: returns and creates DNS records in Google CloudDNS
* `GoogleProvider`: returns and creates DNS records in Google Cloud DNS
* `AWSProvider`: returns and creates DNS records in AWS Route 53
* `AzureProvider`: returns and creates DNS records in Azure DNS
* `InMemoryProvider`: Keeps a list of records in local memory

View File

@ -28,9 +28,9 @@ ExternalDNS can solve this for you as well.
### Which DNS providers are supported?
Currently, the following providers are supported:
Currently, the following providers are supported:
- Google CloudDNS
- Google Cloud DNS
- AWS Route 53
- AzureDNS
- CloudFlare
@ -40,6 +40,12 @@ Currently, the following providers are supported:
- Dyn
- OpenStack Designate
- PowerDNS
- CoreDNS
- Exoscale
- Oracle Cloud Infrastructure DNS
- Linode DNS
- RFC2136
- TransIP
As stated in the README, we are currently looking for stable maintainers for those providers, to ensure that bugfixes and new features will be available for all of those.
@ -155,6 +161,18 @@ CNAMEs cannot co-exist with other records, therefore you can use the `--txt-pref
You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite or https://www.googleapis.com/auth/cloud-platform on your instance group's scope.
### What metrics can I get from ExternalDNS and what do they mean?
ExternalDNS exposes 2 types of metrics: Sources and Registry errors.
`Source`s are mostly Kubernetes API objects. Examples of `source` errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions. It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate, etc.
`Registry` errors are mostly Provider errors, unless there's some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors will arise if the changeset applied is incompatible with the current state.
In case of an increased error count, you could correlate them with the `http_request_duration_seconds{handler="instrumented_http"}` metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down).
You can use the host label in the metric to figure out if the request was against the Kubernetes API server (Source errors) or the DNS provider API (Registry/Provider errors).
### How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects?
Have a look at https://github.com/linki/mate/blob/v0.6.2/examples/google/README.md#permissions
@ -226,3 +244,26 @@ To do this with ExternalDNS you can use the `--annotation-filter` to specificall
an instance of a ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external`
then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class=nginx-internal`
and one with `--annotation-filter=kubernetes.io/ingress.class=nginx-external`.
### Can external-dns manage(add/remove) records in a hosted zone which is setup in different AWS account?
Yes, give it the correct cross-account/assume-role permissions and use the `--aws-assume-role` flag https://github.com/kubernetes-incubator/external-dns/pull/524#issue-181256561
### How do I provide multiple values to the annotation `external-dns.alpha.kubernetes.io/hostname`?
Separate them by `,`.
### Are there official Docker images provided?
When we tag a new release, we push a Docker image on Zalando's public Docker registry with the following name:
```
registry.opensource.zalan.do/teapot/external-dns
```
As tags, you can use your version of choice or use `latest` that always resolves to the latest tag.
If you wish to build your own image, you can use the provided [Dockerfile](../Dockerfile) as a starting point.
We are currently working with the Kubernetes community to provide official images for the project similarly to what is done with the other official Kubernetes projects, but we don't have an ETA on when those images will be available.

View File

@ -27,6 +27,7 @@ Providers
- [x] Google
- [ ] InMemory
- [x] Linode
- [x] TransIP
PRs welcome!
@ -51,4 +52,7 @@ For the moment, it is impossible to use a TTL value of 0 with the AWS, DigitalOc
This behavior may change in the future.
### Linode Provider
The Linode Provider default TTL is used when the TTL is 0. The default is 24 hours
The Linode Provider default TTL is used when the TTL is 0. The default is 24 hours
### TransIP Provider
The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s.

View File

@ -117,7 +117,7 @@ spec:
- --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
- --provider=alibabacloud
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
- --alibaba-cloud-zone=public # only look at public hosted zones (valid values are public, private or no value for both)
- --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
- --registry=txt
- --txt-owner-id=my-identifier
volumeMounts:
@ -194,6 +194,7 @@ spec:
- --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
- --registry=txt
- --txt-owner-id=my-identifier
- --alibaba-cloud-config-file= # enable sts token
volumeMounts:
- mountPath: /usr/share/zoneinfo
name: hostpath
@ -210,9 +211,9 @@ spec:
This list is not the full list, but a few arguments that where chosen.
### alibabacloud-zone-type
### alibaba-cloud-zone-type
`alibabacloud-zone-type` allows filtering for private and public zones
`alibaba-cloud-zone-type` allows filtering for private and public zones
* If value is `public`, it will sync with records in Alibaba Cloud DNS Service
* If value is `private`, it will sync with records in Alibaba Cloud Private Zone Service
@ -379,3 +380,5 @@ Give ExternalDNS some time to clean up the DNS records for you. Then delete the
```console
$ aliyun alidns DeleteDomain --DomainName external-dns-test.com
```
For more info about Alibaba Cloud external dns, please refer this [docs](https://yq.aliyun.com/articles/633412)

View File

@ -44,17 +44,17 @@ $ aws route53 create-hosted-zone --name "external-dns-test.my-org.com." --caller
```
Make a note of the ID of the hosted zone you just created.
Make a note of the ID of the hosted zone you just created, which will serve as the value for my-hostedzone-identifier.
```console
$ aws route53 list-hosted-zones-by-name --dns-name "external-dns-test.my-org.com." | jq -r '.HostedZones[0].Id'
$ aws route53 list-hosted-zones-by-name --output json --dns-name "external-dns-test.my-org.com." | jq -r '.HostedZones[0].Id'
/hostedzone/ZEWFWZ4R16P7IB
```
Make a note of the nameservers that were assigned to your new zone.
```console
$ aws route53 list-resource-record-sets --hosted-zone-id "/hostedzone/ZEWFWZ4R16P7IB" \
$ aws route53 list-resource-record-sets --output json --hosted-zone-id "/hostedzone/ZEWFWZ4R16P7IB" \
--query "ResourceRecordSets[?Type == 'NS']" | jq -r '.[0].ResourceRecords[].Value'
ns-5514.awsdns-53.org.
...
@ -65,7 +65,7 @@ In this case it's the ones shown above but your's will differ.
## Deploy ExternalDNS
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply one of the following manifests file to deploy ExternalDNS.
Then apply one of the following manifests file to deploy ExternalDNS. You can check if your cluster has RBAC by `kubectl api-versions | grep rbac.authorization.k8s.io`.
### Manifest (for clusters without RBAC enabled)
```yaml
@ -92,7 +92,7 @@ spec:
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
- --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
- --registry=txt
- --txt-owner-id=my-identifier
- --txt-owner-id=my-hostedzone-identifier
```
### Manifest (for clusters with RBAC enabled)
@ -119,7 +119,7 @@ rules:
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
verbs: ["list","watch"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
@ -158,7 +158,7 @@ spec:
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
- --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
- --registry=txt
- --txt-owner-id=my-identifier
- --txt-owner-id=my-hostedzone-identifier
```
@ -177,7 +177,7 @@ Annotations which are specific to AWS.
### alias
`external-dns.alpha.kubernetes.io/alias` if set to `true` on an ingress, it will create an ALIAS record when the target is an ALIAS as well.
`external-dns.alpha.kubernetes.io/alias` if set to `true` on an ingress, it will create an ALIAS record when the target is an ALIAS as well. To make the target an alias, the ingress needs to be configured correctly as described in [the docs](./nginx-ingress.md#with-a-separate-tcp-load-balancer).
## Verify ExternalDNS works (Ingress example)
@ -208,13 +208,15 @@ Create the following sample application to test that ExternalDNS works.
> For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the corresponding value.
> If you want to give multiple names to service, you can set it to external-dns.alpha.kubernetes.io/hostname with a comma separator.
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com.
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
spec:
type: LoadBalancer
ports:
@ -247,7 +249,7 @@ spec:
After roughly two minutes check that a corresponding DNS record for your service was created.
```console
$ aws route53 list-resource-record-sets --hosted-zone-id "/hostedzone/ZEWFWZ4R16P7IB" \
$ aws route53 list-resource-record-sets --output json --hosted-zone-id "/hostedzone/ZEWFWZ4R16P7IB" \
--query "ResourceRecordSets[?Name == 'nginx.external-dns-test.my-org.com.']|[?Type == 'A']"
[
{
@ -264,7 +266,7 @@ $ aws route53 list-resource-record-sets --hosted-zone-id "/hostedzone/ZEWFWZ4R16
"TTL": 300,
"ResourceRecords": [
{
"Value": "\"heritage=external-dns,external-dns/owner=my-identifier\""
"Value": "\"heritage=external-dns,external-dns/owner=my-hostedzone-identifier\""
}
],
"Type": "TXT"
@ -310,7 +312,7 @@ kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com.
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
external-dns.alpha.kubernetes.io/ttl: 60
spec:
...

View File

@ -48,7 +48,7 @@ The preferred way to inject the configuration file is by using a Kubernetes secr
"subscriptionId": "01234abc-de56-ff78-abc1-234567890def",
"resourceGroup": "MyDnsResourceGroup",
"aadClientId": "01234abc-de56-ff78-abc1-234567890def",
"aadClientSecret": "uKiuXeiwui4jo9quae9o",
"aadClientSecret": "uKiuXeiwui4jo9quae9o"
}
```
@ -61,13 +61,18 @@ The `resourceGroup` is the Resource Group created in a previous step.
The `aadClientID` and `aaClientSecret` are assoiated with the Service Principal, that you need to create next.
### Creating service principal
A Service Principal with a minimum access level of contribute to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps.
A Service Principal with a minimum access level of `contributor` to the DNS zone(s) and `reader` to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. However, other more permissive access levels will work too (e.g. `contributor` to the resource group or the whole subscription).
This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps.
``` bash
> az login
```
>az login
...
# find the relevant subscription and set the az context. id = subscriptionId value in the azure.json.
>az account list
Find the relevant subscription and make sure it is selected (the same subscriptionId should be set into azure.json)
``` bash
> az account list
{
"cloudName": "AzureCloud",
"id": "<subscriptionId GUID>",
@ -79,16 +84,15 @@ A Service Principal with a minimum access level of contribute to the resource gr
"name": "name",
"type": "user"
}
>az account set -s id
...
>az group show --name externaldns
{
"id": "/subscriptions/id/resourceGroups/externaldns",
...
}
# use the id from the previous step in the scopes argument
>az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/id/resourceGroups/externaldns" -n ExternalDnsServicePrincipal
# select the subscription
> az account set -s <subscriptionId GUID>
...
```
Create the service principal
``` bash
> az ad sp create-for-rbac -n ExternalDnsServicePrincipal
{
"appId": "appId GUID", <-- aadClientId value
...
@ -97,6 +101,33 @@ A Service Principal with a minimum access level of contribute to the resource gr
}
```
Assign the rights for the service principal
```
# find out the resource ids of the resource group where the dns zone is deployed, and the dns zone itself
> az group show --name externaldns
{
"id": "/subscriptions/id/resourceGroups/externaldns",
...
}
> az network dns zone show --name example.com -g externaldns
{
"id": "/subscriptions/.../resourceGroups/externaldns/providers/Microsoft.Network/dnszones/example.com",
...
}
```
```
# assign the rights to the created service principal, using the resource ids from previous step
# 1. as a reader to the resource group
> az role assignment create --role "Reader" --assignee <appId GUID> --scope <resource group resource id>
# 2. as a contributor to DNS Zone itself
> az role assignment create --role "Contributor" --assignee <appId GUID> --scope <dns zone resource id>
```
Now you can create a file named 'azure.json' with values gathered above and with the structure of the example above. Use this file to create a Kubernetes secret:
```

View File

@ -16,7 +16,7 @@ Snippet from [Cloudflare - Getting Started](https://api.cloudflare.com/#getting-
>Cloudflare's API exposes the entire Cloudflare infrastructure via a standardized programmatic interface. Using Cloudflare's API, you can do just about anything you can do on cloudflare.com via the customer dashboard.
>The Cloudflare API is a RESTful API based on HTTPS requests and JSON responses. If you are registered with Cloudflare, you can obtain your API key from the bottom of the "My Account" page, found here: [Go to My account](https://www.cloudflare.com/a/account).
>The Cloudflare API is a RESTful API based on HTTPS requests and JSON responses. If you are registered with Cloudflare, you can obtain your API key from the bottom of the "My Account" page, found here: [Go to My account](https://dash.cloudflare.com/profile).
The environment vars `CF_API_KEY` and `CF_API_EMAIL` will be needed to run ExternalDNS with Cloudflare.
@ -193,6 +193,10 @@ This should show the external IP address of the service as the A record for your
Now that we have verified that ExternalDNS will automatically manage Cloudflare DNS records, we can delete the tutorial's example:
```
$ kubectl delete service -f nginx.yaml
$ kubectl delete service -f externaldns.yaml
$ kubectl delete -f nginx.yaml
$ kubectl delete -f externaldns.yaml
```
## Setting cloudflare-proxied on a per-ingress basis
Using the `external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"` annotation on your ingress, you can specify if the proxy feature of Cloudflare should be enabled for that record. This setting will override the global `--cloudflare-proxied` setting.

View File

@ -86,8 +86,10 @@ helm install --name my-coredns --values values.yaml stable/coredns
## Installing ExternalDNS
### Install external ExternalDNS
ETCD_URLS is configured to etcd client service address.
```
$ cat external-dns.yaml
#### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
@ -97,7 +99,7 @@ spec:
strategy:
type: Recreate
selector:
matchLabels:
matchLabels:
app: external-dns
template:
metadata:
@ -114,7 +116,76 @@ spec:
env:
- name: ETCD_URLS
value: http://10.105.68.165:2379
$ kubectl apply -f external-dns.yaml
```
#### Manifest (for clusters with RBAC enabled)
```yaml
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: kube-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: kube-system
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=ingress
- --provider=coredns
- --log-level=debug # debug only
env:
- name: ETCD_URLS
value: http://10.105.68.165:2379
```
## Enable the ingress controller
@ -126,6 +197,7 @@ minikube addons enable ingress
## Testing ingress example
```
$ cat ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx
@ -157,8 +229,7 @@ nginx nginx.example.org 10.0.2.15 80 2m
$ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools
If you don't see a command prompt, try pressing enter.
dnstools# dig @10.102.213.122 nginx.example.org +short
dnstools# dig @10.102.213.122 nginx.example.org +short
dnstools# dig @10.100.4.143 nginx.example.org +short
10.0.2.15
dnstools#
```

View File

@ -25,7 +25,7 @@ spec:
- --source=service
- --source=ingress
- --source=istio-gateway
- --istio-ingress-gateway=custom-istio-namespace/custom-istio-ingressgateway # omit to use the default (istio-system/istio-ingressgateway)
- --istio-ingress-gateway=custom-istio-namespace/custom-istio-ingressgateway # load balancer service to be used; can be specified multiple times. Omit to use the default (istio-system/istio-ingressgateway)
- --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
- --provider=aws
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
@ -95,7 +95,7 @@ spec:
- --source=service
- --source=ingress
- --source=istio-gateway
- --istio-ingress-gateway=custom-istio-namespace/custom-istio-ingressgateway # omit to use the default (istio-system/istio-ingressgateway)
- --istio-ingress-gateway=custom-istio-namespace/custom-istio-ingressgateway # load balancer service to be used; can be specified multiple times. Omit to use the default (istio-system/istio-ingressgateway)
- --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
- --provider=aws
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization

View File

@ -15,8 +15,7 @@ Create a GKE cluster without using the default ingress controller.
```console
$ gcloud container clusters create "external-dns" \
--num-nodes 1 \
--scopes "https://www.googleapis.com/auth/ndev.clouddns.readwrite" \
--disable-addons=HttpLoadBalancing
--scopes "https://www.googleapis.com/auth/ndev.clouddns.readwrite"
```
Create a DNS zone which will contain the managed DNS records.

200
docs/tutorials/ns1.md Normal file
View File

@ -0,0 +1,200 @@
# Setting up ExternalDNS for Services on NS1
This tutorial describes how to setup ExternalDNS for use within a
Kubernetes cluster using NS1 DNS.
Make sure to use **>=0.5** version of ExternalDNS for this tutorial.
## Creating a zone with NS1 DNS
If you are new to NS1, we recommend you first read the following
instructions for creating a zone.
[Creating a zone using the NS1
portal](https://ns1.com/knowledgebase/creating-a-zone)
[Creating a zone using the NS1
API](https://ns1.com/api#put-create-a-new-dns-zone)
## Creating NS1 Credentials
All NS1 products are API-first, meaning everything that can be done on
the portal---including managing zones and records, data sources and
feeds, and account settings and users---can be done via API.
The NS1 API is a standard REST API with JSON responses. The environment
var `NS1_APIKEY` will be needed to run ExternalDNS with NS1.
### To add or delete an API key
1. Log into the NS1 portal at [my.nsone.net](http://my.nsone.net).
2. Click your username in the upper-right corner, and navigate to **Account Settings** \> **Users & Teams**.
3. Navigate to the _API Keys_ tab, and click **Add Key**.
4. Enter the name of the application and modify permissions and settings as desired. Once complete, click **Create Key**. The new API key appears in the list.
Note: Set the permissions for your API keys just as you would for a user or team associated with your organization's NS1 account. For more information, refer to the article [Creating and Managing API Keys](https://ns1.com/knowledgebase/creating-and-managing-users) in the NS1 Knowledge Base.
## Deploy ExternalDNS
Connect your `kubectl` client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --provider=ns1
env:
- name: NS1_APIKEY
value: "YOUR_NS1_API_KEY"
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --provider=ns1
env:
- name: NS1_APIKEY
value: "YOUR_NS1_API_KEY"
```
## Deploying an Nginx Service
Create a service file called 'nginx.yaml' with the following contents:
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: example.com
external-dns.alpha.kubernetes.io/ttl: "120" #optional
spec:
selector:
app: nginx
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
```
**A note about annotations**
Verify that the annotation on the service uses the same hostname as the NS1 DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. 'www.example.com').
The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10.
ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.
### Create the deployment and service
```
$ kubectl create -f nginx.yaml
```
Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the NS1 DNS records.
## Verifying NS1 DNS records
Use the NS1 portal or API to verify that the A record for your domain shows the external IP address of the services.
## Cleanup
Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial's example:
```
$ kubectl delete -f nginx.yaml
$ kubectl delete -f externaldns.yaml
```

200
docs/tutorials/rcodezero.md Normal file
View File

@ -0,0 +1,200 @@
# Setting up ExternalDNS for Services on RcodeZero
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using [RcodeZero Anycast DNS](https://www.rcodezero.at). Make sure to use **>=0.5.0** version of ExternalDNS for this tutorial.
The following steps are required to use RcodeZero with ExternalDNS:
1. Sign up for an RcodeZero account (or use an existing account).
2. Add your zone to the RcodeZero DNS
3. Enable the RcodeZero API, and generate an API key.
4. Deploy ExternalDNS to use the RcodeZero provider.
5. Verify the setup bey deploying a test services (optional)
## Creating a RcodeZero DNS zone
Before records can be added to your domain name automatically, you need to add your domain name to the set of zones managed by RcodeZero. In order to add the zone, perform the following steps:
1. Log in to the RcodeZero Dashboard, and move to the [Add Zone](https://my.rcodezero.at/domain/create) page.
2. Select "MASTER" as domain type, and add your domain name there. Use this domain name instead of "example.com" throughout the rest of this tutorial.
Note that "SECONDARY" domains cannot be managed by ExternalDNS, because this would not allow modification of records in the zone.
## Enable the API, and create Credentials
> The RcodeZero Anycast-Network is provisioned via web interface or REST-API.
Enable the RcodeZero API to generate an API key on [RcodeZero API](https://my.rcodezero.at/enableapi). The API key will be added to the environment variable 'RC0_API_KEY' via one of the Manifest templates (as described below).
## Deploy ExternalDNS
Connect your `kubectl` client to the cluster you want to test ExternalDNS with. Choose a Manifest from below, depending on whether or not you have RBAC enabled. Before applying it, modify the Manifest as follows:
- Replace "example.com" with the domain name you added to RcodeZero.
- Replace YOUR_RCODEZERO_API_KEY with the API key created above.
- Replace YOUR_ENCRYPTION_KEY_STRING with a string to encrypt the TXT records
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --provider=rcodezero
- --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var.
env:
- name: RC0_API_KEY
value: "YOUR_RCODEZERO_API_KEY"
- name: RC0_ENC_VAR
value: "YOUR_ENCRYPTION_KEY_STRING"
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --provider=rcodezero
- --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var.
env:
- name: RC0_API_KEY
value: "YOUR_RCODEZERO_API_KEY"
- name: RC0_ENC_VAR
value: "YOUR_ENCRYPTION_KEY_STRING"
```
## Deploying an Nginx Service
After you have deployed ExternalDNS with RcodeZero, you can deploy a simple service based on Nginx to test the setup. This is optional, though highly recommended before using ExternalDNS in production.
Create a service file called 'nginx.yaml' with the following contents:
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: example.com
external-dns.alpha.kubernetes.io/ttl: "120" #optional
spec:
selector:
app: nginx
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
```
Change the file as follows:
- Replace the annotation of the service; use the same hostname as the RcodeZero DNS zone created above. The annotation may also be a subdomain
of the DNS zone (e.g. 'www.example.com').
- Set the TTL annotation of the service. A valid TTL of 120 or above must be given. This annotation is optional, and defaults to "300" if no value is given.
These annotations will be used to determine what services should be registered with DNS. Removing these annotations will cause ExternalDNS to remove the corresponding DNS records.
Create the Deployment and Service:
```bash
$ kubectl create -f nginx.yaml
```
Depending on your cloud provider, it might take a while to create an external IP for the service. Once an external IP address is assigned to the service, ExternalDNS will notice the new address and synchronize the RcodeZero DNS records accordingly.
## Verifying RcodeZero DNS records
Check your [RcodeZero Configured Zones](https://my.rcodezero.at/domain) and select the respective zone name. The zone should now contain the external IP address of the service as an A record.
## Cleanup
Once you have verified that ExternalDNS successfully manages RcodeZero DNS records for external services, you can delete the tutorial example as follows:
```bash
$ kubectl delete -f nginx.yaml
$ kubectl delete -f externaldns.yaml
```

View File

@ -12,6 +12,39 @@ key "externaldns-key" {
```
- `Warning!` Bind server configuration should enable for this key AFXR zone transfer protocol. It is used for listing DNS records.
```text
# cat /etc/named.conf
...
include "/etc/rndc.key";
controls {
inet 123.123.123.123 port 953 allow { 10.x.y.151; } keys { "externaldns-key"; };
};
options {
include "/etc/named/options.conf";
};
include "/etc/named/zones.conf";
...
# cat /etc/named/options.conf
...
dnssec-enable yes;
dnssec-validation yes;
...
# cat /etc/named/zones.conf
...
zone "example.com" {
type master;
file "/var/named/dynamic/db.example.com";
update-policy {
grant externaldns-key zonesub ANY;
};
};
...
```
## RFC2136 provider configuration:
- Example fragment of real configuration of ExternalDNS service pod.
@ -31,4 +64,4 @@ key "externaldns-key" {
- `rfc2136-tsig-keyname` - this is string parameter with secret key name it is should `MATCH!` with server key name. In example it is `externaldns-key`.

181
docs/tutorials/transip.md Normal file
View File

@ -0,0 +1,181 @@
# Setting up ExternalDNS for Services on TransIP
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using TransIP.
Make sure to use **>=0.5.14** version of ExternalDNS for this tutorial, have at least 1 domain registered at TransIP and enabled the API.
## Enable TransIP API and prepare your API key
To use the TransIP API you need an account at TransIP and enable API usage as described in the [knowledge base](https://www.transip.eu/knowledgebase/entry/77-want-use-the-transip-api/). With the private key generated by the API, we create a kubernetes secret:
```console
$ kubectl create secret generic transip-api-key --from-file=transip-api-key=/path/to/private.key
```
## Deploy ExternalDNS
Below are example manifests, for both cluster without or with RBAC enabled. Don't forget to replace `YOUR_TRANSIP_ACCOUNT_NAME` with your TransIP account name. In these examples, an example domain-filter is defined. Such a filter can be used to prevent ExternalDNS from touching any domain not listed in the filter. Refer to the docs for any other command-line parameters you might want to use.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains
- --provider=transip
- --transip-account=YOUR_TRANSIP_ACCOUNT_NAME
- --transip-keyfile=/transip/transip-api-key
volumeMounts:
- mountPath: /transip
name: transip-api-key
readOnly: true
volumes:
- name: transip-api-key
secret:
secretName: transip-api-key
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains
- --provider=transip
- --transip-account=YOUR_TRANSIP_ACCOUNT_NAME
- --transip-keyfile=/transip/transip-api-key
volumeMounts:
- mountPath: /transip
name: transip-api-key
readOnly: true
volumes:
- name: transip-api-key
secret:
secretName: transip-api-key
```
## Deploying an Nginx Service
Create a service file called 'nginx.yaml' with the following contents:
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: my-app.example.com
spec:
selector:
app: nginx
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
```
Note the annotation on the service; this is the name ExternalDNS will create and manage DNS records for.
ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.
Create the deployment and service:
```console
$ kubectl create -f nginx.yaml
```
Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.
Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the TransIP DNS records.
## Verifying TransIP DNS records
Check your [TransIP Control Panel](https://transip.eu/cp) to view the records for your TransIP DNS zone.
Click on the zone for the one created above if a different domain was used.
This should show the external IP address of the service as the A record for your domain.

View File

@ -109,8 +109,14 @@ func (t Targets) IsLess(o Targets) bool {
return false
}
// ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers
type ProviderSpecificProperty struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
}
// ProviderSpecific holds configuration which is specific to individual DNS providers
type ProviderSpecific map[string]string
type ProviderSpecific []ProviderSpecificProperty
// Endpoint is a high-level way of a connection between a service and an IP
type Endpoint struct {
@ -160,10 +166,21 @@ func (e *Endpoint) WithProviderSpecific(key, value string) *Endpoint {
if e.ProviderSpecific == nil {
e.ProviderSpecific = ProviderSpecific{}
}
e.ProviderSpecific[key] = value
e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{Name: key, Value: value})
return e
}
// GetProviderSpecificProperty returns a ProviderSpecificProperty if the property exists.
func (e *Endpoint) GetProviderSpecificProperty(key string) (ProviderSpecificProperty, bool) {
for _, providerSpecific := range e.ProviderSpecific {
if providerSpecific.Name == key {
return providerSpecific, true
}
}
return ProviderSpecificProperty{}, false
}
func (e *Endpoint) String() string {
return fmt.Sprintf("%s %d IN %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Targets, e.ProviderSpecific)
}

108
go.mod Normal file
View File

@ -0,0 +1,108 @@
module github.com/kubernetes-incubator/external-dns
go 1.12
require (
cloud.google.com/go v0.34.0
github.com/Azure/azure-sdk-for-go v10.0.4-beta+incompatible
github.com/Azure/go-autorest v10.9.0+incompatible
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 // indirect
github.com/alecthomas/kingpin v2.2.5+incompatible
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f
github.com/aws/aws-sdk-go v1.13.32
github.com/cenkalti/backoff v2.1.1+incompatible // indirect
github.com/cloudflare/cloudflare-go v0.0.0-20190102215809-0c85496d8730
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/coreos/bbolt v1.3.2 // indirect
github.com/coreos/etcd v3.3.10+incompatible
github.com/coreos/go-semver v0.2.0 // indirect
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e // indirect
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
github.com/denverdino/aliyungo v0.0.0-20180815121905-69560d9530f5
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/digitalocean/godo v1.1.1
github.com/dnaeon/go-vcr v1.0.1 // indirect
github.com/dnsimple/dnsimple-go v0.14.0
github.com/envoyproxy/go-control-plane v0.6.9 // indirect
github.com/exoscale/egoscale v0.11.0
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
github.com/go-ini/ini v1.32.0 // indirect
github.com/go-resty/resty v1.8.0 // indirect
github.com/gogo/googleapis v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
github.com/golang/mock v1.2.0 // indirect
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a // indirect
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c // indirect
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.8.5 // indirect
github.com/hashicorp/go-multierror v1.0.0 // indirect
github.com/imdario/mergo v0.3.5 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 // indirect
github.com/jonboulle/clockwork v0.1.0 // indirect
github.com/json-iterator/go v1.1.6 // indirect
github.com/linki/instrumented_http v0.2.0
github.com/linode/linodego v0.3.0
github.com/lyft/protoc-gen-validate v0.0.14 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/miekg/dns v1.0.8
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/nesv/go-dynect v0.6.0
github.com/nic-at/rc0go v1.1.0
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/oracle/oci-go-sdk v1.8.0
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.8.1
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0
github.com/satori/go.uuid v1.2.0 // indirect
github.com/sergi/go-diff v1.0.0 // indirect
github.com/sirupsen/logrus v1.2.0
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect
github.com/soheilhy/cmux v0.1.3 // indirect
github.com/spf13/cobra v0.0.3 // indirect
github.com/spf13/pflag v1.0.2 // indirect
github.com/stretchr/testify v1.2.2
github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 // indirect
github.com/transip/gotransip v5.8.2+incompatible
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
github.com/yl2chen/cidranger v0.0.0-20180214081945-928b519e5268 // indirect
go.etcd.io/bbolt v1.3.2 // indirect
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1 // indirect
golang.org/x/net v0.0.0-20190311183353-d8887717615a
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
google.golang.org/api v0.3.0
google.golang.org/appengine v1.5.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
gopkg.in/yaml.v2 v2.2.2
istio.io/api v0.0.0-20190321180614-db16d82d3672
istio.io/istio v0.0.0-20190322063008-2b1331886076
k8s.io/api v0.0.0-20180628040859-072894a440bd
k8s.io/apiextensions-apiserver v0.0.0-20180628053655-3de98c57bc05 // indirect
k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d
k8s.io/client-go v8.0.0+incompatible
k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c // indirect
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect
)
replace github.com/golang/glog => github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d

385
go.sum Normal file
View File

@ -0,0 +1,385 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk=
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI=
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/Azure/azure-sdk-for-go v10.0.4-beta+incompatible h1:FhnlL7/4O3gAB7EBgN43vA3Bb0fAlCBIMm9avXbcHlE=
github.com/Azure/azure-sdk-for-go v10.0.4-beta+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-autorest v10.9.0+incompatible h1:3ccqKLQg+scl0J6krcDgih2Rl+GC1eNuHZeRQYQxKkk=
github.com/Azure/go-autorest v10.9.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kingpin v2.2.5+incompatible h1:umWl1NNd72+ZvRti3T9C0SYean2hPZ7ZhxU8bsgc9BQ=
github.com/alecthomas/kingpin v2.2.5+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 h1:GDQdwm/gAcJcLAKQQZGOJ4knlw+7rfEQQcmwTbt4p5E=
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f h1:hinXH9rcBjRoIih5tl4f1BCbNjOmPJ2UnZwcYDhEHR0=
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/aws/aws-sdk-go v1.13.32 h1:AoV2boU+diwKoMaschMtUJim3nmBpM/4y45UqY708F4=
github.com/aws/aws-sdk-go v1.13.32/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY=
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.0.0-20190102215809-0c85496d8730 h1:+TK6ytATp7coqI4UlTBboFYD0kSkWZt6L6/T+1yBK6k=
github.com/cloudflare/cloudflare-go v0.0.0-20190102215809-0c85496d8730/go.mod h1:qKQ9S///VKEax9N8kFel9/AvmnkYgvb8uiKTnoVFvpg=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denverdino/aliyungo v0.0.0-20180815121905-69560d9530f5 h1:YjnQWGUNtqeKqndapy9V1BzlfMwc/dBJf2MU9dmuXSQ=
github.com/denverdino/aliyungo v0.0.0-20180815121905-69560d9530f5/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/digitalocean/godo v1.1.1 h1:v0A7yF3xmKLjjdJGIeBbINfMufcrrRhqZsxuVQMoT+U=
github.com/digitalocean/godo v1.1.1/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU=
github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnsimple/dnsimple-go v0.14.0 h1:JGYtVVA/uHc91q0LjDWqR1oVj6EGu9Kn0lMRxjH/w30=
github.com/dnsimple/dnsimple-go v0.14.0/go.mod h1:0FYu4qVNv/UcfZPNwa9zi68IkggJu3TIwM54D7rhmI4=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/envoyproxy/go-control-plane v0.6.9 h1:deEH9W8ZAUGNbCdX+9iNzBOGrAOrnpJGoy0PcTqk/tE=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/exoscale/egoscale v0.11.0 h1:g+UBsxLDouKWW2BK/UTgQFAVnM2aHygheF0Dxj0ycC8=
github.com/exoscale/egoscale v0.11.0/go.mod h1:Ee3U4ZjSDpbbEc9VkQ/jttUU8USE8Nv7L3YzVi03Y1U=
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 h1:jmwW6QWvUO2OPe22YfgFvBaaZlSr8Rlrac5lZvG6IdM=
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99/go.mod h1:4mP9w9+vYGw2jUx2+2v03IA+phyQQjNRR4AL3uxlNrs=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-ini/ini v1.32.0 h1:/MArBHSS0TFR28yPPDK1vPIjt4wUnPBfb81i6iiyKvA=
github.com/go-ini/ini v1.32.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-resty/resty v1.8.0 h1:vbNCxbHOWCototzwxf3L63PQCKx6xgT6v8SHfoqkp6U=
github.com/go-resty/resty v1.8.0/go.mod h1:n37daLLGIHq2FFYHxg+FYQiwA95FpfNI+A9uxoIYGRk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a h1:ZJu5NB1Bk5ms4vw0Xu4i+jD32SE9jQXyfnOvwhHqlT0=
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8 h1:L9JPKrtsHMQ4VCRQfHvbbHBfB2Urn8xf6QZeXZ+OrN4=
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q=
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79 h1:lR9ssWAqp9qL0bALxqEEkuudiP1eweOdv9jsRK3e7lE=
github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJlb8Kqsd41CTE=
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65 h1:FP5rOFP4ifbtFIjFHJmwhFrsbDyONILK/FNntl/Pou8=
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d h1:JV46OtdhH2vVt8mJ1EWUE94k99vbN9fZs1WQ8kcEapU=
github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d/go.mod h1:CHQ3o5KBH1PIS2Fb1mRLTIWO5YzP9kSUB3KoCICwlvA=
github.com/linki/instrumented_http v0.2.0 h1:zLhcB3Q/McQQqml3qd5kzdZ0cGnL3vquPFIW2338f5Y=
github.com/linki/instrumented_http v0.2.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk=
github.com/linode/linodego v0.3.0 h1:I83pEPg4owSy5pCPaKix7xkGbWIjPxmAoc/Yu5OYDDY=
github.com/linode/linodego v0.3.0/go.mod h1:ga11n3ivecUrPCHN0rANxKmfWBJVkOXfLMZinAbj2sY=
github.com/lyft/protoc-gen-validate v0.0.14 h1:xbdDVIHd0Xq5Bfzu+8JR9s7mFmJPMvNLmfGhgcHJdFU=
github.com/lyft/protoc-gen-validate v0.0.14/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.8 h1:Zi8HNpze3NeRWH1PQV6O71YcvJRQ6j0lORO6DAEmAAI=
github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/nesv/go-dynect v0.6.0 h1:Ow/DiSm4LAISwnFku/FITSQHnU6pBvhQMsUE5Gu6Oq4=
github.com/nesv/go-dynect v0.6.0/go.mod h1:GHRBRKzTwjAMhosHJQq/KrZaFkXIFyJ5zRE7thGXXrs=
github.com/nic-at/rc0go v1.1.0 h1:k6/Bru/npTjmCSFw65ulYRw/b3ycIS30t6/YM4r42V4=
github.com/nic-at/rc0go v1.1.0/go.mod h1:KEa3H5fmDNXCaXSqOeAZxkKnG/8ggr1OHIG25Ve7fjU=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/oracle/oci-go-sdk v1.8.0 h1:4SO45bKV0I3/Mn1os3ANDZmV0eSE5z5CLdSUIkxtyzs=
github.com/oracle/oci-go-sdk v1.8.0/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0 h1:vOcHdR1nu7DO4BAx1rwzdHV7jQTzW3gqcBT5qxHSc6A=
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0/go.mod h1:FeplEtXXejBYC4NPAFTrs5L7KuK+5RL9bf5nB2vZe9o=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.3 h1:09wy7WZk4AqO03yH85Ex1X+Uo3vDsil3Fa9AgF8Emss=
github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 h1:/Bsw4C+DEdqPjt8vAqaC9LAqpAQnaCQQqmolqq3S1T4=
github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9/go.mod h1:RHkNRtSLfOK7qBTHaeSX1D6BNpI3qw7NTxsmNr4RvN8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/transip/gotransip v5.8.2+incompatible h1:aNJhw/w/3QBqFcHAIPz1ytoK5FexeMzbUCGrrhWr3H0=
github.com/transip/gotransip v5.8.2+incompatible/go.mod h1:uacMoJVmrfOcscM4Bi5NVg708b7c6rz2oDTWqa7i2Ic=
github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs=
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 h1:vG/gY/PxA3v3l04qxe3tDjXyu3bozii8ulSlIPOYKhI=
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yl2chen/cidranger v0.0.0-20180214081945-928b519e5268 h1:lkoOjizoHqOcEFsvYGE5c8Ykdijjnd0R3r1yDYHzLno=
github.com/yl2chen/cidranger v0.0.0-20180214081945-928b519e5268/go.mod h1:mq0zhomp/G6rRTb0dvHWXRHr/2+Qgeq5hMXfJ670+i4=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A=
go.opencensus.io v0.19.2 h1:ZZpq6xI6kv/LuE/5s5UQvBU5vMjvRnPb8PvJrIntAnc=
go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU=
google.golang.org/api v0.3.0 h1:UIJY20OEo3+tK5MBlcdx37kmdH6EnRjGkW78mc6+EeA=
google.golang.org/api v0.3.0/go.mod h1:IuvZyQh8jgscv8qWfQ4ABd8m7hEudgBFM/EdhA3BnXw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1 h1:+fgY/3ngqdBW9oLQCMwL5g+QRkKFPJH05fx2/pipqRQ=
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw=
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
istio.io/api v0.0.0-20190321180614-db16d82d3672 h1:luY97pBVarSo1v++zf2kgb84Q55G5hv/ult2A4KPQuk=
istio.io/api v0.0.0-20190321180614-db16d82d3672/go.mod h1:hhLFQmpHia8zgaM37vb2ml9iS5NfNfqZGRt1pS9aVEo=
istio.io/istio v0.0.0-20190322063008-2b1331886076 h1:gZhCrmVzfQJoDl4oav8i5+NF7p7v0M1Pou+2O+hZBtc=
istio.io/istio v0.0.0-20190322063008-2b1331886076/go.mod h1:OWBySrQjjk549IhxWCt7DTl9ZSsXdvbgm+SmgGVRsGA=
k8s.io/api v0.0.0-20180628040859-072894a440bd h1:HzgYeLDS1jLxw8DGr68KJh9cdQ5iZJizG0HZWstIhfQ=
k8s.io/api v0.0.0-20180628040859-072894a440bd/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apiextensions-apiserver v0.0.0-20180628053655-3de98c57bc05 h1:uKDX+1GgQuV/J6TTgrtHYGRFZUPWxC13mJwBhjIhm/w=
k8s.io/apiextensions-apiserver v0.0.0-20180628053655-3de98c57bc05/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d h1:MZjlsu9igBoVPZkXpIGoxI6EonqNsXXZU7hhvfQLkd4=
k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/client-go v8.0.0+incompatible h1:tTI4hRmb1DRMl4fG6Vclfdi6nTM82oIrTT7HfitmxC4=
k8s.io/client-go v8.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c h1:kJCzg2vGCzah5icgkKR7O1Dzn0NA2iGlym27sb0ZfGE=
k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54=
launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM=

View File

@ -17,6 +17,7 @@ limitations under the License.
package testutils
import (
"reflect"
"sort"
"github.com/kubernetes-incubator/external-dns/endpoint"
@ -49,7 +50,7 @@ func SameEndpoint(a, b *endpoint.Endpoint) bool {
return a.DNSName == b.DNSName && a.Targets.Same(b.Targets) && a.RecordType == b.RecordType &&
a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] && a.RecordTTL == b.RecordTTL &&
a.Labels[endpoint.ResourceLabelKey] == b.Labels[endpoint.ResourceLabelKey] &&
SameMap(a.ProviderSpecific, b.ProviderSpecific)
SameProverSpecific(a.ProviderSpecific, b.ProviderSpecific)
}
// SameEndpoints compares two slices of endpoints regardless of order
@ -81,17 +82,7 @@ func SamePlanChanges(a, b map[string][]*endpoint.Endpoint) bool {
SameEndpoints(a["UpdateOld"], b["UpdateOld"]) && SameEndpoints(a["UpdateNew"], b["UpdateNew"])
}
// SameMap verifies that two maps contain the same string/string key/value pairs
func SameMap(a, b map[string]string) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if v != b[k] {
return false
}
}
return true
// SameProverSpecific verifies that two maps contain the same string/string key/value pairs
func SameProverSpecific(a, b endpoint.ProviderSpecific) bool {
return reflect.DeepEqual(a, b)
}

View File

@ -56,9 +56,11 @@ func ExampleSameEndpoints() {
RecordTTL: endpoint.TTL(60),
},
{
DNSName: "example.org",
Targets: endpoint.Targets{"load-balancer.org"},
ProviderSpecific: endpoint.ProviderSpecific{"foo": "bar"},
DNSName: "example.org",
Targets: endpoint.Targets{"load-balancer.org"},
ProviderSpecific: endpoint.ProviderSpecific{
endpoint.ProviderSpecificProperty{Name: "foo", Value: "bar"},
},
},
}
sort.Sort(byAllFields(eps))
@ -66,11 +68,11 @@ func ExampleSameEndpoints() {
fmt.Println(ep)
}
// Output:
// abc.com 0 IN A 1.2.3.4 map[]
// abc.com 0 IN TXT something map[]
// bbc.com 0 IN CNAME foo.com map[]
// cbc.com 60 IN CNAME foo.com map[]
// example.org 0 IN load-balancer.org map[]
// example.org 0 IN load-balancer.org map[foo:bar]
// example.org 0 IN TXT load-balancer.org map[]
// abc.com 0 IN A 1.2.3.4 []
// abc.com 0 IN TXT something []
// bbc.com 0 IN CNAME foo.com []
// cbc.com 60 IN CNAME foo.com []
// example.org 0 IN load-balancer.org []
// example.org 0 IN load-balancer.org [{foo bar}]
// example.org 0 IN TXT load-balancer.org []
}

52
main.go
View File

@ -67,20 +67,24 @@ func main() {
// Create a source.Config from the flags passed by the user.
sourceCfg := &source.Config{
Namespace: cfg.Namespace,
AnnotationFilter: cfg.AnnotationFilter,
FQDNTemplate: cfg.FQDNTemplate,
CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
Compatibility: cfg.Compatibility,
PublishInternal: cfg.PublishInternal,
PublishHostIP: cfg.PublishHostIP,
ConnectorServer: cfg.ConnectorSourceServer,
CRDSourceAPIVersion: cfg.CRDSourceAPIVersion,
CRDSourceKind: cfg.CRDSourceKind,
KubeConfig: cfg.KubeConfig,
KubeMaster: cfg.Master,
ServiceTypeFilter: cfg.ServiceTypeFilter,
IstioIngressGateway: cfg.IstioIngressGateway,
Namespace: cfg.Namespace,
AnnotationFilter: cfg.AnnotationFilter,
FQDNTemplate: cfg.FQDNTemplate,
CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation,
Compatibility: cfg.Compatibility,
PublishInternal: cfg.PublishInternal,
PublishHostIP: cfg.PublishHostIP,
ConnectorServer: cfg.ConnectorSourceServer,
CRDSourceAPIVersion: cfg.CRDSourceAPIVersion,
CRDSourceKind: cfg.CRDSourceKind,
KubeConfig: cfg.KubeConfig,
KubeMaster: cfg.Master,
ServiceTypeFilter: cfg.ServiceTypeFilter,
IstioIngressGatewayServices: cfg.IstioIngressGatewayServices,
CFAPIEndpoint: cfg.CFAPIEndpoint,
CFUsername: cfg.CFUsername,
CFPassword: cfg.CFPassword,
}
// Lookup all the selected sources by names and pass them the desired configuration.
@ -99,6 +103,7 @@ func main() {
domainFilter := provider.NewDomainFilter(cfg.DomainFilter)
zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
var p provider.Provider
switch cfg.Provider {
@ -110,10 +115,12 @@ func main() {
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
ZoneTypeFilter: zoneTypeFilter,
ZoneTagFilter: zoneTagFilter,
BatchChangeSize: cfg.AWSBatchChangeSize,
BatchChangeInterval: cfg.AWSBatchChangeInterval,
EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth,
AssumeRole: cfg.AWSAssumeRole,
APIRetries: cfg.AWSAPIRetries,
DryRun: cfg.DryRun,
},
)
@ -127,7 +134,9 @@ func main() {
case "azure":
p, err = provider.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.DryRun)
case "cloudflare":
p, err = provider.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareProxied, cfg.DryRun)
p, err = provider.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun)
case "rcodezero":
p, err = provider.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt)
case "google":
p, err = provider.NewGoogleProvider(cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.DryRun)
case "digitalocean":
@ -147,6 +156,7 @@ func main() {
Password: cfg.InfobloxWapiPassword,
Version: cfg.InfobloxWapiVersion,
SSLVerify: cfg.InfobloxSSLVerify,
View: cfg.InfobloxView,
DryRun: cfg.DryRun,
},
)
@ -194,6 +204,18 @@ func main() {
}
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, nil)
case "ns1":
p, err = provider.NewNS1Provider(
provider.NS1Config{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
NS1Endpoint: cfg.NS1Endpoint,
NS1IgnoreSSL: cfg.NS1IgnoreSSL,
DryRun: cfg.DryRun,
},
)
case "transip":
p, err = provider.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}

View File

@ -18,6 +18,7 @@ package externaldns
import (
"fmt"
"reflect"
"strconv"
"time"
@ -36,143 +37,169 @@ var (
// Config is a project-wide configuration
type Config struct {
Master string
KubeConfig string
RequestTimeout time.Duration
IstioIngressGateway string
Sources []string
Namespace string
AnnotationFilter string
FQDNTemplate string
CombineFQDNAndAnnotation bool
Compatibility string
PublishInternal bool
PublishHostIP bool
ConnectorSourceServer string
Provider string
GoogleProject string
DomainFilter []string
ZoneIDFilter []string
AlibabaCloudConfigFile string
AlibabaCloudZoneType string
AWSZoneType string
AWSAssumeRole string
AWSBatchChangeSize int
AWSBatchChangeInterval time.Duration
AWSEvaluateTargetHealth bool
AzureConfigFile string
AzureResourceGroup string
CloudflareProxied bool
InfobloxGridHost string
InfobloxWapiPort int
InfobloxWapiUsername string
InfobloxWapiPassword string
InfobloxWapiVersion string
InfobloxSSLVerify bool
DynCustomerName string
DynUsername string
DynPassword string
DynMinTTLSeconds int
OCIConfigFile string
InMemoryZones []string
PDNSServer string
PDNSAPIKey string
PDNSTLSEnabled bool
TLSCA string
TLSClientCert string
TLSClientCertKey string
Policy string
Registry string
TXTOwnerID string
TXTPrefix string
Interval time.Duration
Once bool
DryRun bool
LogFormat string
MetricsAddress string
LogLevel string
TXTCacheInterval time.Duration
ExoscaleEndpoint string
ExoscaleAPIKey string
ExoscaleAPISecret string
CRDSourceAPIVersion string
CRDSourceKind string
ServiceTypeFilter []string
RFC2136Host string
RFC2136Port int
RFC2136Zone string
RFC2136Insecure bool
RFC2136TSIGKeyName string
RFC2136TSIGSecret string
RFC2136TSIGSecretAlg string
RFC2136TAXFR bool
Master string
KubeConfig string
RequestTimeout time.Duration
IstioIngressGatewayServices []string
Sources []string
Namespace string
AnnotationFilter string
FQDNTemplate string
CombineFQDNAndAnnotation bool
IgnoreHostnameAnnotation bool
Compatibility string
PublishInternal bool
PublishHostIP bool
ConnectorSourceServer string
Provider string
GoogleProject string
DomainFilter []string
ZoneIDFilter []string
AlibabaCloudConfigFile string
AlibabaCloudZoneType string
AWSZoneType string
AWSZoneTagFilter []string
AWSAssumeRole string
AWSBatchChangeSize int
AWSBatchChangeInterval time.Duration
AWSEvaluateTargetHealth bool
AWSAPIRetries int
AzureConfigFile string
AzureResourceGroup string
CloudflareProxied bool
CloudflareZonesPerPage int
RcodezeroTXTEncrypt bool
InfobloxGridHost string
InfobloxWapiPort int
InfobloxWapiUsername string
InfobloxWapiPassword string `secure:"yes"`
InfobloxWapiVersion string
InfobloxSSLVerify bool
InfobloxView string
DynCustomerName string
DynUsername string
DynPassword string `secure:"yes"`
DynMinTTLSeconds int
OCIConfigFile string
InMemoryZones []string
PDNSServer string
PDNSAPIKey string `secure:"yes"`
PDNSTLSEnabled bool
TLSCA string
TLSClientCert string
TLSClientCertKey string
Policy string
Registry string
TXTOwnerID string
TXTPrefix string
Interval time.Duration
Once bool
DryRun bool
LogFormat string
MetricsAddress string
LogLevel string
TXTCacheInterval time.Duration
ExoscaleEndpoint string
ExoscaleAPIKey string `secure:"yes"`
ExoscaleAPISecret string `secure:"yes"`
CRDSourceAPIVersion string
CRDSourceKind string
ServiceTypeFilter []string
CFAPIEndpoint string
CFUsername string
CFPassword string
RFC2136Host string
RFC2136Port int
RFC2136Zone string
RFC2136Insecure bool
RFC2136TSIGKeyName string
RFC2136TSIGSecret string `secure:"yes"`
RFC2136TSIGSecretAlg string
RFC2136TAXFR bool
NS1Endpoint string
NS1IgnoreSSL bool
TransIPAccountName string
TransIPPrivateKeyFile string
}
var defaultConfig = &Config{
Master: "",
KubeConfig: "",
RequestTimeout: time.Second * 30,
IstioIngressGateway: "istio-system/istio-ingressgateway",
Sources: nil,
Namespace: "",
AnnotationFilter: "",
FQDNTemplate: "",
CombineFQDNAndAnnotation: false,
Compatibility: "",
PublishInternal: false,
PublishHostIP: false,
ConnectorSourceServer: "localhost:8080",
Provider: "",
GoogleProject: "",
DomainFilter: []string{},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSAssumeRole: "",
AWSBatchChangeSize: 4000,
AWSBatchChangeInterval: time.Second,
AWSEvaluateTargetHealth: true,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
CloudflareProxied: false,
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
InMemoryZones: []string{},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
PDNSTLSEnabled: false,
TLSCA: "",
TLSClientCert: "",
TLSClientCertKey: "",
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
TXTPrefix: "",
TXTCacheInterval: 0,
Interval: time.Minute,
Once: false,
DryRun: false,
LogFormat: "text",
MetricsAddress: ":7979",
LogLevel: logrus.InfoLevel.String(),
ExoscaleEndpoint: "https://api.exoscale.ch/dns",
ExoscaleAPIKey: "",
ExoscaleAPISecret: "",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
CRDSourceKind: "DNSEndpoint",
ServiceTypeFilter: []string{},
RFC2136Host: "",
RFC2136Port: 0,
RFC2136Zone: "",
RFC2136Insecure: false,
RFC2136TSIGKeyName: "",
RFC2136TSIGSecret: "",
RFC2136TSIGSecretAlg: "",
RFC2136TAXFR: true,
Master: "",
KubeConfig: "",
RequestTimeout: time.Second * 30,
IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"},
Sources: nil,
Namespace: "",
AnnotationFilter: "",
FQDNTemplate: "",
CombineFQDNAndAnnotation: false,
IgnoreHostnameAnnotation: false,
Compatibility: "",
PublishInternal: false,
PublishHostIP: false,
ConnectorSourceServer: "localhost:8080",
Provider: "",
GoogleProject: "",
DomainFilter: []string{},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSZoneTagFilter: []string{},
AWSAssumeRole: "",
AWSBatchChangeSize: 1000,
AWSBatchChangeInterval: time.Second,
AWSEvaluateTargetHealth: true,
AWSAPIRetries: 3,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
RcodezeroTXTEncrypt: false,
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
InfobloxView: "",
OCIConfigFile: "/etc/kubernetes/oci.yaml",
InMemoryZones: []string{},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
PDNSTLSEnabled: false,
TLSCA: "",
TLSClientCert: "",
TLSClientCertKey: "",
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
TXTPrefix: "",
TXTCacheInterval: 0,
Interval: time.Minute,
Once: false,
DryRun: false,
LogFormat: "text",
MetricsAddress: ":7979",
LogLevel: logrus.InfoLevel.String(),
ExoscaleEndpoint: "https://api.exoscale.ch/dns",
ExoscaleAPIKey: "",
ExoscaleAPISecret: "",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
CRDSourceKind: "DNSEndpoint",
ServiceTypeFilter: []string{},
CFAPIEndpoint: "",
CFUsername: "",
CFPassword: "",
RFC2136Host: "",
RFC2136Port: 0,
RFC2136Zone: "",
RFC2136Insecure: false,
RFC2136TSIGKeyName: "",
RFC2136TSIGSecret: "",
RFC2136TSIGSecretAlg: "",
RFC2136TAXFR: true,
NS1Endpoint: "",
NS1IgnoreSSL: false,
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
}
// NewConfig returns new Config object
@ -183,14 +210,19 @@ func NewConfig() *Config {
func (cfg *Config) String() string {
// prevent logging of sensitive information
temp := *cfg
if temp.DynPassword != "" {
temp.DynPassword = passwordMask
}
if temp.InfobloxWapiPassword != "" {
temp.InfobloxWapiPassword = passwordMask
}
if temp.PDNSAPIKey != "" {
temp.PDNSAPIKey = ""
t := reflect.TypeOf(temp)
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if val, ok := f.Tag.Lookup("secure"); ok && val == "yes" {
if f.Type.Kind() != reflect.String {
continue
}
v := reflect.ValueOf(&temp).Elem().Field(i)
if v.String() != "" {
v.SetString(passwordMask)
}
}
}
return fmt.Sprintf("%+v", temp)
@ -217,14 +249,20 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("request-timeout", "Request timeout when calling Kubernetes APIs. 0s means no timeout").Default(defaultConfig.RequestTimeout.String()).DurationVar(&cfg.RequestTimeout)
// Flags related to Istio
app.Flag("istio-ingress-gateway", "The fully-qualified name of the Istio ingress gateway service (default: istio-system/istio-ingressgateway)").Default(defaultConfig.IstioIngressGateway).StringVar(&cfg.IstioIngressGateway)
app.Flag("istio-ingress-gateway", "The fully-qualified name of the Istio ingress gateway service. Flag can be specified multiple times (default: istio-system/istio-ingressgateway)").Default("istio-system/istio-ingressgateway").StringsVar(&cfg.IstioIngressGatewayServices)
// Flags related to cloud foundry
app.Flag("cf-api-endpoint", "The fully-qualified domain name of the cloud foundry instance you are targeting").Default(defaultConfig.CFAPIEndpoint).StringVar(&cfg.CFAPIEndpoint)
app.Flag("cf-username", "The username to log into the cloud foundry API").Default(defaultConfig.CFUsername).StringVar(&cfg.CFUsername)
app.Flag("cf-password", "The password to log into the cloud foundry API").Default(defaultConfig.CFPassword).StringVar(&cfg.CFPassword)
// Flags related to processing sources
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake, connector, istio-gateway, crd").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "istio-gateway", "fake", "connector", "crd")
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake, connector, istio-gateway, cloudfoundry, crd").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "istio-gateway", "cloudfoundry", "fake", "connector", "crd")
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate)
app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation)
app.Flag("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when using fqdn-template is set (optional, default: false)").BoolVar(&cfg.IgnoreHostnameAnnotation)
app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule")
app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)
app.Flag("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)").BoolVar(&cfg.PublishHostIP)
@ -234,36 +272,43 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter)
// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode, rfc2136)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "alibabacloud", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, rcodezero, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode, rfc2136, ns1, transip)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip")
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter)
app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject)
app.Flag("alibaba-cloud-config-file", "When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud").Default(defaultConfig.AlibabaCloudConfigFile).StringVar(&cfg.AlibabaCloudConfigFile)
app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private")
app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter)
app.Flag("aws-assume-role", "When using the AWS provider, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole)
app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize)
app.Flag("aws-batch-change-interval", "When using the AWS provider, set the interval between batch changes.").Default(defaultConfig.AWSBatchChangeInterval.String()).DurationVar(&cfg.AWSBatchChangeInterval)
app.Flag("aws-evaluate-target-health", "When using the AWS provider, set whether to evaluate the health of a DNS target (default: enabled, disable with --no-aws-evaluate-target-health)").Default(strconv.FormatBool(defaultConfig.AWSEvaluateTargetHealth)).BoolVar(&cfg.AWSEvaluateTargetHealth)
app.Flag("aws-api-retries", "When using the AWS provider, set the maximum number of retries for API calls before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries)
app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile)
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
app.Flag("cloudflare-zones-per-page", "When using the Cloudflare provider, specify how many zones per page listed, max. possible 50 (default: 50)").Default(strconv.Itoa(defaultConfig.CloudflareZonesPerPage)).IntVar(&cfg.CloudflareZonesPerPage)
app.Flag("infoblox-grid-host", "When using the Infoblox provider, specify the Grid Manager host (required when --provider=infoblox)").Default(defaultConfig.InfobloxGridHost).StringVar(&cfg.InfobloxGridHost)
app.Flag("infoblox-wapi-port", "When using the Infoblox provider, specify the WAPI port (default: 443)").Default(strconv.Itoa(defaultConfig.InfobloxWapiPort)).IntVar(&cfg.InfobloxWapiPort)
app.Flag("infoblox-wapi-username", "When using the Infoblox provider, specify the WAPI username (default: admin)").Default(defaultConfig.InfobloxWapiUsername).StringVar(&cfg.InfobloxWapiUsername)
app.Flag("infoblox-wapi-password", "When using the Infoblox provider, specify the WAPI password (required when --provider=infoblox)").Default(defaultConfig.InfobloxWapiPassword).StringVar(&cfg.InfobloxWapiPassword)
app.Flag("infoblox-wapi-version", "When using the Infoblox provider, specify the WAPI version (default: 2.3.1)").Default(defaultConfig.InfobloxWapiVersion).StringVar(&cfg.InfobloxWapiVersion)
app.Flag("infoblox-ssl-verify", "When using the Infoblox provider, specify whether to verify the SSL certificate (default: true, disable with --no-infoblox-ssl-verify)").Default(strconv.FormatBool(defaultConfig.InfobloxSSLVerify)).BoolVar(&cfg.InfobloxSSLVerify)
app.Flag("infoblox-view", "DNS view (default: \"\")").Default(defaultConfig.InfobloxView).StringVar(&cfg.InfobloxView)
app.Flag("dyn-customer-name", "When using the Dyn provider, specify the Customer Name").Default("").StringVar(&cfg.DynCustomerName)
app.Flag("dyn-username", "When using the Dyn provider, specify the Username").Default("").StringVar(&cfg.DynUsername)
app.Flag("dyn-password", "When using the Dyn provider, specify the pasword").Default("").StringVar(&cfg.DynPassword)
app.Flag("dyn-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.DynMinTTLSeconds)
app.Flag("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile)
app.Flag("rcodezero-txt-encrypt", "When using the Rcodezero provider with txt registry option, set if TXT rrs are encrypted (default: false)").Default(strconv.FormatBool(defaultConfig.RcodezeroTXTEncrypt)).BoolVar(&cfg.RcodezeroTXTEncrypt)
app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones)
app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)
app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)
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)
// 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)
@ -284,6 +329,10 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("rfc2136-tsig-secret-alg", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").Default(defaultConfig.RFC2136TSIGSecretAlg).StringVar(&cfg.RFC2136TSIGSecretAlg)
app.Flag("rfc2136-tsig-axfr", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").BoolVar(&cfg.RFC2136TAXFR)
// Flags related to TransIP provider
app.Flag("transip-account", "When using the TransIP provider, specify the account name (required when --provider=transip)").Default(defaultConfig.TransIPAccountName).StringVar(&cfg.TransIPAccountName)
app.Flag("transip-keyfile", "When using the TransIP provider, specify the path to the private key file (required when --provider=transip)").Default(defaultConfig.TransIPPrivateKeyFile).StringVar(&cfg.TransIPPrivateKeyFile)
// Flags related to policies
app.Flag("policy", "Modify how DNS records are synchronized between sources and providers (default: sync, options: sync, upsert-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only")

View File

@ -29,109 +29,183 @@ import (
var (
minimalConfig = &Config{
Master: "",
KubeConfig: "",
RequestTimeout: time.Second * 30,
IstioIngressGateway: "istio-system/istio-ingressgateway",
Sources: []string{"service"},
Namespace: "",
FQDNTemplate: "",
Compatibility: "",
Provider: "google",
GoogleProject: "",
DomainFilter: []string{""},
ZoneIDFilter: []string{""},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSAssumeRole: "",
AWSBatchChangeSize: 4000,
AWSBatchChangeInterval: time.Second,
AWSEvaluateTargetHealth: true,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
CloudflareProxied: false,
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
InMemoryZones: []string{""},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
TXTPrefix: "",
TXTCacheInterval: 0,
Interval: time.Minute,
Once: false,
DryRun: false,
LogFormat: "text",
MetricsAddress: ":7979",
LogLevel: logrus.InfoLevel.String(),
ConnectorSourceServer: "localhost:8080",
ExoscaleEndpoint: "https://api.exoscale.ch/dns",
ExoscaleAPIKey: "",
ExoscaleAPISecret: "",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
CRDSourceKind: "DNSEndpoint",
Master: "",
KubeConfig: "",
RequestTimeout: time.Second * 30,
IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"},
Sources: []string{"service"},
Namespace: "",
FQDNTemplate: "",
Compatibility: "",
Provider: "google",
GoogleProject: "",
DomainFilter: []string{""},
ZoneIDFilter: []string{""},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSZoneTagFilter: []string{""},
AWSAssumeRole: "",
AWSBatchChangeSize: 1000,
AWSBatchChangeInterval: time.Second,
AWSEvaluateTargetHealth: true,
AWSAPIRetries: 3,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxView: "",
InfobloxSSLVerify: true,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
InMemoryZones: []string{""},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
TXTPrefix: "",
TXTCacheInterval: 0,
Interval: time.Minute,
Once: false,
DryRun: false,
LogFormat: "text",
MetricsAddress: ":7979",
LogLevel: logrus.InfoLevel.String(),
ConnectorSourceServer: "localhost:8080",
ExoscaleEndpoint: "https://api.exoscale.ch/dns",
ExoscaleAPIKey: "",
ExoscaleAPISecret: "",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
CRDSourceKind: "DNSEndpoint",
RcodezeroTXTEncrypt: false,
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
}
overriddenConfig = &Config{
Master: "http://127.0.0.1:8080",
KubeConfig: "/some/path",
RequestTimeout: time.Second * 77,
IstioIngressGateway: "istio-other/istio-otheringressgateway",
Sources: []string{"service", "ingress", "connector"},
Namespace: "namespace",
FQDNTemplate: "{{.Name}}.service.example.com",
Compatibility: "mate",
Provider: "google",
GoogleProject: "project",
DomainFilter: []string{"example.org", "company.com"},
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "private",
AWSAssumeRole: "some-other-role",
AWSBatchChangeSize: 100,
AWSBatchChangeInterval: time.Second * 2,
AWSEvaluateTargetHealth: false,
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
CloudflareProxied: true,
InfobloxGridHost: "127.0.0.1",
InfobloxWapiPort: 8443,
InfobloxWapiUsername: "infoblox",
InfobloxWapiPassword: "infoblox",
InfobloxWapiVersion: "2.6.1",
InfobloxSSLVerify: false,
OCIConfigFile: "oci.yaml",
InMemoryZones: []string{"example.org", "company.com"},
PDNSServer: "http://ns.example.com:8081",
PDNSAPIKey: "some-secret-key",
PDNSTLSEnabled: true,
TLSCA: "/path/to/ca.crt",
TLSClientCert: "/path/to/cert.pem",
TLSClientCertKey: "/path/to/key.pem",
Policy: "upsert-only",
Registry: "noop",
TXTOwnerID: "owner-1",
TXTPrefix: "associated-txt-record",
TXTCacheInterval: 12 * time.Hour,
Interval: 10 * time.Minute,
Once: true,
DryRun: true,
LogFormat: "json",
MetricsAddress: "127.0.0.1:9099",
LogLevel: logrus.DebugLevel.String(),
ConnectorSourceServer: "localhost:8081",
ExoscaleEndpoint: "https://api.foo.ch/dns",
ExoscaleAPIKey: "1",
ExoscaleAPISecret: "2",
CRDSourceAPIVersion: "test.k8s.io/v1alpha1",
CRDSourceKind: "Endpoint",
Master: "http://127.0.0.1:8080",
KubeConfig: "/some/path",
RequestTimeout: time.Second * 77,
IstioIngressGatewayServices: []string{"istio-other/istio-otheringressgateway"},
Sources: []string{"service", "ingress", "connector"},
Namespace: "namespace",
IgnoreHostnameAnnotation: true,
FQDNTemplate: "{{.Name}}.service.example.com",
Compatibility: "mate",
Provider: "google",
GoogleProject: "project",
DomainFilter: []string{"example.org", "company.com"},
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "private",
AWSZoneTagFilter: []string{"tag=foo"},
AWSAssumeRole: "some-other-role",
AWSBatchChangeSize: 100,
AWSBatchChangeInterval: time.Second * 2,
AWSEvaluateTargetHealth: false,
AWSAPIRetries: 13,
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
CloudflareProxied: true,
CloudflareZonesPerPage: 20,
InfobloxGridHost: "127.0.0.1",
InfobloxWapiPort: 8443,
InfobloxWapiUsername: "infoblox",
InfobloxWapiPassword: "infoblox",
InfobloxWapiVersion: "2.6.1",
InfobloxView: "internal",
InfobloxSSLVerify: false,
OCIConfigFile: "oci.yaml",
InMemoryZones: []string{"example.org", "company.com"},
PDNSServer: "http://ns.example.com:8081",
PDNSAPIKey: "some-secret-key",
PDNSTLSEnabled: true,
TLSCA: "/path/to/ca.crt",
TLSClientCert: "/path/to/cert.pem",
TLSClientCertKey: "/path/to/key.pem",
Policy: "upsert-only",
Registry: "noop",
TXTOwnerID: "owner-1",
TXTPrefix: "associated-txt-record",
TXTCacheInterval: 12 * time.Hour,
Interval: 10 * time.Minute,
Once: true,
DryRun: true,
LogFormat: "json",
MetricsAddress: "127.0.0.1:9099",
LogLevel: logrus.DebugLevel.String(),
ConnectorSourceServer: "localhost:8081",
ExoscaleEndpoint: "https://api.foo.ch/dns",
ExoscaleAPIKey: "1",
ExoscaleAPISecret: "2",
CRDSourceAPIVersion: "test.k8s.io/v1alpha1",
CRDSourceKind: "Endpoint",
RcodezeroTXTEncrypt: true,
NS1Endpoint: "https://api.example.com/v1",
NS1IgnoreSSL: true,
TransIPAccountName: "transip",
TransIPPrivateKeyFile: "/path/to/transip.key",
}
// minimal config with istio gateway source and multiple ingressgateway load balancer services
multipleIstioIngressGatewaysConfig = &Config{
Master: "",
KubeConfig: "",
RequestTimeout: time.Second * 30,
IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway", "istio-other/istio-otheringressgateway"},
Sources: []string{"istio-gateway"},
Namespace: "",
FQDNTemplate: "",
Compatibility: "",
Provider: "google",
GoogleProject: "",
DomainFilter: []string{""},
ZoneIDFilter: []string{""},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSZoneTagFilter: []string{""},
AWSAssumeRole: "",
AWSBatchChangeSize: 1000,
AWSBatchChangeInterval: time.Second,
AWSEvaluateTargetHealth: true,
AWSAPIRetries: 3,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxView: "",
InfobloxSSLVerify: true,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
InMemoryZones: []string{""},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
TXTPrefix: "",
TXTCacheInterval: 0,
Interval: time.Minute,
Once: false,
DryRun: false,
LogFormat: "text",
MetricsAddress: ":7979",
LogLevel: logrus.InfoLevel.String(),
ConnectorSourceServer: "localhost:8080",
ExoscaleEndpoint: "https://api.exoscale.ch/dns",
ExoscaleAPIKey: "",
ExoscaleAPISecret: "",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
CRDSourceKind: "DNSEndpoint",
RcodezeroTXTEncrypt: false,
}
)
@ -163,17 +237,20 @@ func TestParseFlags(t *testing.T) {
"--source=connector",
"--namespace=namespace",
"--fqdn-template={{.Name}}.service.example.com",
"--ignore-hostname-annotation",
"--compatibility=mate",
"--provider=google",
"--google-project=project",
"--azure-config-file=azure.json",
"--azure-resource-group=arg",
"--cloudflare-proxied",
"--cloudflare-zones-per-page=20",
"--infoblox-grid-host=127.0.0.1",
"--infoblox-wapi-port=8443",
"--infoblox-wapi-username=infoblox",
"--infoblox-wapi-password=infoblox",
"--infoblox-wapi-version=2.6.1",
"--infoblox-view=internal",
"--inmemory-zone=example.org",
"--inmemory-zone=company.com",
"--pdns-server=http://ns.example.com:8081",
@ -189,9 +266,11 @@ func TestParseFlags(t *testing.T) {
"--zone-id-filter=/hostedzone/ZTST1",
"--zone-id-filter=/hostedzone/ZTST2",
"--aws-zone-type=private",
"--aws-zone-tags=tag=foo",
"--aws-assume-role=some-other-role",
"--aws-batch-change-size=100",
"--aws-batch-change-interval=2s",
"--aws-api-retries=13",
"--no-aws-evaluate-target-health",
"--policy=upsert-only",
"--registry=noop",
@ -210,6 +289,11 @@ func TestParseFlags(t *testing.T) {
"--exoscale-apisecret=2",
"--crd-source-apiversion=test.k8s.io/v1alpha1",
"--crd-source-kind=Endpoint",
"--rcodezero-txt-encrypt",
"--ns1-endpoint=https://api.example.com/v1",
"--ns1-ignoressl",
"--transip-account=transip",
"--transip-keyfile=/path/to/transip.key",
},
envVars: map[string]string{},
expected: overriddenConfig,
@ -225,17 +309,20 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_SOURCE": "service\ningress\nconnector",
"EXTERNAL_DNS_NAMESPACE": "namespace",
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
"EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1",
"EXTERNAL_DNS_COMPATIBILITY": "mate",
"EXTERNAL_DNS_PROVIDER": "google",
"EXTERNAL_DNS_GOOGLE_PROJECT": "project",
"EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json",
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_CLOUDFLARE_ZONES_PER_PAGE": "20",
"EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1",
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_VERSION": "2.6.1",
"EXTERNAL_DNS_INFOBLOX_VIEW": "internal",
"EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0",
"EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml",
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
@ -248,10 +335,12 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem",
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo",
"EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s",
"EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0",
"EXTERNAL_DNS_AWS_API_RETRIES": "13",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
@ -269,9 +358,35 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_EXOSCALE_APISECRET": "2",
"EXTERNAL_DNS_CRD_SOURCE_APIVERSION": "test.k8s.io/v1alpha1",
"EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint",
"EXTERNAL_DNS_RCODEZERO_TXT_ENCRYPT": "1",
"EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1",
"EXTERNAL_DNS_NS1_IGNORESSL": "1",
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
},
expected: overriddenConfig,
},
{
title: "istio config with 2 ingressgateways",
args: []string{
"--provider=google",
"--source=istio-gateway",
"--istio-ingress-gateway=istio-system/istio-ingressgateway",
"--istio-ingress-gateway=istio-other/istio-otheringressgateway",
},
envVars: map[string]string{},
expected: multipleIstioIngressGatewaysConfig,
},
{
title: "override everything via environment variables with multiple istio ingress gateway load balancer services",
args: []string{},
envVars: map[string]string{
"EXTERNAL_DNS_PROVIDER": "google",
"EXTERNAL_DNS_SOURCE": "istio-gateway",
"EXTERNAL_DNS_ISTIO_INGRESS_GATEWAY": "istio-system/istio-ingressgateway\nistio-other/istio-otheringressgateway",
},
expected: multipleIstioIngressGatewaysConfig,
},
} {
t.Run(ti.title, func(t *testing.T) {
originalEnv := setEnv(t, ti.envVars)
@ -308,6 +423,7 @@ func TestPasswordsNotLogged(t *testing.T) {
DynPassword: "dyn-pass",
InfobloxWapiPassword: "infoblox-pass",
PDNSAPIKey: "pdns-api-key",
RFC2136TSIGSecret: "tsig-secret",
}
s := cfg.String()
@ -315,4 +431,5 @@ func TestPasswordsNotLogged(t *testing.T) {
assert.False(t, strings.Contains(s, "dyn-pass"))
assert.False(t, strings.Contains(s, "infoblox-pass"))
assert.False(t, strings.Contains(s, "pdns-api-key"))
assert.False(t, strings.Contains(s, "tsig-secret"))
}

View File

@ -65,5 +65,9 @@ func ValidateConfig(cfg *externaldns.Config) error {
return errors.New("TTL specified for Dyn is negative")
}
}
if cfg.IgnoreHostnameAnnotation && cfg.FQDNTemplate == "" {
return errors.New("FQDN Template must be set if ignoring annotations")
}
return nil
}

View File

@ -116,3 +116,11 @@ func TestValidateGoodDynConfig(t *testing.T) {
assert.Nil(t, err, "Configuration should be valid, got this error instead", err)
}
}
func TestValidateBadIgnoreHostnameAnnotationsConfig(t *testing.T) {
cfg := externaldns.NewConfig()
cfg.IgnoreHostnameAnnotation = true
cfg.FQDNTemplate = ""
assert.Error(t, ValidateConfig(cfg))
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package plan
import (
"fmt"
"strings"
"github.com/kubernetes-incubator/external-dns/endpoint"
@ -78,8 +79,12 @@ type planTableRow struct {
candidates []*endpoint.Endpoint
}
func (t planTableRow) String() string {
return fmt.Sprintf("planTableRow{current=%v, candidates=%v}", t.current, t.candidates)
}
func (t planTable) addCurrent(e *endpoint.Endpoint) {
dnsName := sanitizeDNSName(e.DNSName)
dnsName := normalizeDNSName(e.DNSName)
if _, ok := t.rows[dnsName]; !ok {
t.rows[dnsName] = &planTableRow{}
}
@ -87,7 +92,7 @@ func (t planTable) addCurrent(e *endpoint.Endpoint) {
}
func (t planTable) addCandidate(e *endpoint.Endpoint) {
dnsName := sanitizeDNSName(e.DNSName)
dnsName := normalizeDNSName(e.DNSName)
if _, ok := t.rows[dnsName]; !ok {
t.rows[dnsName] = &planTableRow{}
}
@ -100,7 +105,7 @@ func (t planTable) getUpdates() (updateNew []*endpoint.Endpoint, updateOld []*en
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) {
if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || shouldUpdateProviderSpecific(update, row.current) {
inheritOwner(row.current, update)
updateNew = append(updateNew, update)
updateOld = append(updateOld, row.current)
@ -180,6 +185,27 @@ func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool {
return desired.RecordTTL != current.RecordTTL
}
func shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool {
if current.ProviderSpecific == nil && len(desired.ProviderSpecific) == 0 {
return false
}
for _, c := range current.ProviderSpecific {
// don't consider target health when detecting changes
// see: https://github.com/kubernetes-incubator/external-dns/issues/869#issuecomment-458576954
if c.Name == "aws/evaluate-target-health" {
continue
}
for _, d := range desired.ProviderSpecific {
if d.Name == c.Name && d.Value != c.Value {
return true
}
}
}
return false
}
// filterRecordsForPlan removes records that are not relevant to the planner.
// Currently this just removes TXT records to prevent them from being
// deleted erroneously by the planner (only the TXT registry should do this.)
@ -204,8 +230,12 @@ func filterRecordsForPlan(records []*endpoint.Endpoint) []*endpoint.Endpoint {
return filtered
}
// sanitizeDNSName checks if the DNS name is correct
// for now it only removes space and lower case
func sanitizeDNSName(dnsName string) string {
return strings.TrimSpace(strings.ToLower(dnsName))
// normalizeDNSName converts a DNS name to a canonical form, so that we can use string equality
// it: removes space, converts to lower case, ensures there is a trailing dot
func normalizeDNSName(dnsName string) string {
s := strings.TrimSpace(strings.ToLower(dnsName))
if !strings.HasSuffix(s, ".") {
s += "."
}
return s
}

View File

@ -27,15 +27,17 @@ import (
type PlanTestSuite struct {
suite.Suite
fooV1Cname *endpoint.Endpoint
fooV2Cname *endpoint.Endpoint
fooV2TXT *endpoint.Endpoint
fooV2CnameNoLabel *endpoint.Endpoint
fooV3CnameSameResource *endpoint.Endpoint
fooA5 *endpoint.Endpoint
bar127A *endpoint.Endpoint
bar127AWithTTL *endpoint.Endpoint
bar192A *endpoint.Endpoint
fooV1Cname *endpoint.Endpoint
fooV2Cname *endpoint.Endpoint
fooV2TXT *endpoint.Endpoint
fooV2CnameNoLabel *endpoint.Endpoint
fooV3CnameSameResource *endpoint.Endpoint
fooA5 *endpoint.Endpoint
bar127A *endpoint.Endpoint
bar127AWithTTL *endpoint.Endpoint
bar127AWithProviderSpecificTrue *endpoint.Endpoint
bar127AWithProviderSpecificFalse *endpoint.Endpoint
bar192A *endpoint.Endpoint
}
func (suite *PlanTestSuite) SetupTest() {
@ -100,6 +102,34 @@ func (suite *PlanTestSuite) SetupTest() {
endpoint.ResourceLabelKey: "ingress/default/bar-127",
},
}
suite.bar127AWithProviderSpecificTrue = &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{
endpoint.ProviderSpecificProperty{
Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied",
Value: "true",
},
},
}
suite.bar127AWithProviderSpecificFalse = &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{
endpoint.ProviderSpecificProperty{
Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied",
Value: "false",
},
},
}
suite.bar192A = &endpoint.Endpoint{
DNSName: "bar",
Targets: endpoint.Targets{"192.168.0.1"},
@ -108,6 +138,7 @@ func (suite *PlanTestSuite) SetupTest() {
endpoint.ResourceLabelKey: "ingress/default/bar-192",
},
}
}
func (suite *PlanTestSuite) TestSyncFirstRound() {
@ -194,6 +225,27 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithTTLChange() {
validateEntries(suite.T(), changes.Delete, expectedDelete)
}
func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() {
current := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
desired := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
expectedCreate := []*endpoint.Endpoint{}
expectedUpdateOld := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificTrue}
expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithProviderSpecificFalse}
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
}
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}
@ -354,6 +406,7 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() {
//TODO: remove once multiple-target per endpoint is supported
func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() {
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A}
desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource}
expectedCreate := []*endpoint.Endpoint{}
@ -385,54 +438,58 @@ func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) {
}
}
func TestSanitizeDNSName(t *testing.T) {
func TestNormalizeDNSName(t *testing.T) {
records := []struct {
dnsName string
expect string
}{
{
"3AAAA.FOO.BAR.COM ",
"3aaaa.foo.bar.com",
"3aaaa.foo.bar.com.",
},
{
" example.foo.com",
"example.foo.com",
" example.foo.com.",
"example.foo.com.",
},
{
"example123.foo.com ",
"example123.foo.com",
"example123.foo.com.",
},
{
"foo",
"foo",
"foo.",
},
{
"123foo.bar",
"123foo.bar",
"123foo.bar.",
},
{
"foo.com",
"foo.com",
"foo.com.",
},
{
"foo.com.",
"foo.com.",
},
{
"foo123.COM",
"foo123.com",
"foo123.com.",
},
{
"my-exaMple3.FOO.BAR.COM",
"my-example3.foo.bar.com",
"my-example3.foo.bar.com.",
},
{
" my-example1214.FOO-1235.BAR-foo.COM ",
"my-example1214.foo-1235.bar-foo.com",
"my-example1214.foo-1235.bar-foo.com.",
},
{
"my-example-my-example-1214.FOO-1235.BAR-foo.COM",
"my-example-my-example-1214.foo-1235.bar-foo.com",
"my-example-my-example-1214.foo-1235.bar-foo.com.",
},
}
for _, r := range records {
gotName := sanitizeDNSName(r.dnsName)
gotName := normalizeDNSName(r.dnsName)
assert.Equal(t, r.expect, gotName)
}
}

View File

@ -17,24 +17,21 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"io/ioutil"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz"
"github.com/denverdino/aliyungo/metadata"
"strings"
"sync"
"time"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
"github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz"
"github.com/denverdino/aliyungo/metadata"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
)
const (
@ -42,6 +39,7 @@ const (
defaultAlibabaCloudPrivateZoneRecordTTL = 60
defaultAlibabaCloudPageSize = 50
nullHostAlibabaCloud = "@"
pVTZDoamin = "pvtz.aliyuncs.com"
)
// AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing.
@ -154,6 +152,10 @@ func NewAlibabaCloudProvider(configFile string, domainFilter DomainFilter, zoneI
)
}
if err != nil {
return nil, err
}
provider := &AlibabaCloudProvider{
domainFilter: domainFilter,
zoneIDFilter: zoneIDFileter,
@ -290,7 +292,7 @@ func (p *AlibabaCloudProvider) Records() (endpoints []*endpoint.Endpoint, err er
// ApplyChanges applies the given changes.
//
// Returns nil if the operation was successful or an error if the operation failed.
func (p *AlibabaCloudProvider) ApplyChanges(changes *plan.Changes) error {
func (p *AlibabaCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
if changes == nil || len(changes.Create)+len(changes.Delete)+len(changes.UpdateNew) == 0 {
// No op
return nil
@ -708,6 +710,7 @@ func (p *AlibabaCloudProvider) splitDNSName(endpoint *endpoint.Endpoint) (rr str
func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool {
request := pvtz.CreateDescribeZoneInfoRequest()
request.ZoneId = zoneID
request.Domain = pVTZDoamin
response, err := p.getPvtzClient().DescribeZoneInfo(request)
if err != nil {
log.Errorf("Failed to describe zone info %s in Alibaba Cloud DNS: %v", zoneID, err)
@ -730,7 +733,7 @@ func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) {
request := pvtz.CreateDescribeZonesRequest()
request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
request.PageNumber = "1"
request.Domain = pVTZDoamin
for {
response, err := p.getPvtzClient().DescribeZones(request)
if err != nil {
@ -738,7 +741,7 @@ func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) {
return nil, err
}
for _, zone := range response.Zones.Zone {
log.Debugf("Zone: %++v", zone)
log.Infof("PrivateZones zone: %++v", zone)
if !p.zoneIDFilter.Match(zone.ZoneId) {
continue
@ -784,6 +787,7 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone
request.ZoneId = zone.ZoneId
request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
request.PageNumber = "1"
request.Domain = pVTZDoamin
var records []pvtz.Record
for {
@ -884,6 +888,7 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibaba
request.ZoneId = zone.ZoneId
request.Type = endpoint.RecordType
request.Rr = rr
request.Domain = pVTZDoamin
ttl := int(endpoint.RecordTTL)
if ttl != 0 {
@ -927,6 +932,7 @@ func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int) error {
request := pvtz.CreateDeleteZoneRecordRequest()
request.RecordId = requests.NewInteger(recordID)
request.Domain = pVTZDoamin
response, err := p.getPvtzClient().DeleteZoneRecord(request)
if err == nil {
@ -998,6 +1004,7 @@ func (p *AlibabaCloudProvider) updatePrivateZoneRecord(record pvtz.Record, endpo
request.Rr = record.Rr
request.Type = record.Type
request.Value = record.Value
request.Domain = pVTZDoamin
ttl := int(endpoint.RecordTTL)
if ttl != 0 {
request.Ttl = requests.NewInteger(ttl)

View File

@ -17,10 +17,12 @@ limitations under the License.
package provider
import (
"context"
"testing"
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
"github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz"
"github.com/kubernetes-incubator/external-dns/endpoint"
"testing"
"github.com/kubernetes-incubator/external-dns/plan"
)
@ -300,7 +302,7 @@ func TestAlibabaCloudProvider_ApplyChanges(t *testing.T) {
},
},
}
p.ApplyChanges(&changes)
p.ApplyChanges(context.Background(), &changes)
endpoints, err := p.Records()
if err != nil {
t.Errorf("Failed to get records: %v", err)
@ -357,7 +359,7 @@ func TestAlibabaCloudProvider_ApplyChanges_PrivateZone(t *testing.T) {
},
},
}
p.ApplyChanges(&changes)
p.ApplyChanges(context.Background(), &changes)
endpoints, err := p.Records()
if err != nil {
t.Errorf("Failed to get records: %v", err)

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"sort"
"strings"
@ -43,38 +44,44 @@ var (
// see: https://docs.aws.amazon.com/general/latest/gr/rande.html#elb_region
canonicalHostedZones = map[string]string{
// Application Load Balancers and Classic Load Balancers
"us-east-2.elb.amazonaws.com": "Z3AADJGX6KTTL2",
"us-east-1.elb.amazonaws.com": "Z35SXDOTRQ7X7K",
"us-west-1.elb.amazonaws.com": "Z368ELLRRE2KJ0",
"us-west-2.elb.amazonaws.com": "Z1H1FL5HABSF5",
"ca-central-1.elb.amazonaws.com": "ZQSVJUPU6J1EY",
"ap-south-1.elb.amazonaws.com": "ZP97RAFLXTNZK",
"ap-northeast-2.elb.amazonaws.com": "ZWKZPGTI48KDX",
"ap-northeast-3.elb.amazonaws.com": "Z5LXEXXYW11ES",
"ap-southeast-1.elb.amazonaws.com": "Z1LMS91P8CMLE5",
"ap-southeast-2.elb.amazonaws.com": "Z1GM3OXH4ZPM65",
"ap-northeast-1.elb.amazonaws.com": "Z14GRHDCWA56QT",
"eu-central-1.elb.amazonaws.com": "Z215JYRZR1TBD5",
"eu-west-1.elb.amazonaws.com": "Z32O12XQLNTSW2",
"eu-west-2.elb.amazonaws.com": "ZHURV8PSTC4K8",
"eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4",
"sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU",
"us-east-2.elb.amazonaws.com": "Z3AADJGX6KTTL2",
"us-east-1.elb.amazonaws.com": "Z35SXDOTRQ7X7K",
"us-west-1.elb.amazonaws.com": "Z368ELLRRE2KJ0",
"us-west-2.elb.amazonaws.com": "Z1H1FL5HABSF5",
"ca-central-1.elb.amazonaws.com": "ZQSVJUPU6J1EY",
"ap-south-1.elb.amazonaws.com": "ZP97RAFLXTNZK",
"ap-northeast-2.elb.amazonaws.com": "ZWKZPGTI48KDX",
"ap-northeast-3.elb.amazonaws.com": "Z5LXEXXYW11ES",
"ap-southeast-1.elb.amazonaws.com": "Z1LMS91P8CMLE5",
"ap-southeast-2.elb.amazonaws.com": "Z1GM3OXH4ZPM65",
"ap-northeast-1.elb.amazonaws.com": "Z14GRHDCWA56QT",
"eu-central-1.elb.amazonaws.com": "Z215JYRZR1TBD5",
"eu-west-1.elb.amazonaws.com": "Z32O12XQLNTSW2",
"eu-west-2.elb.amazonaws.com": "ZHURV8PSTC4K8",
"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",
// Network Load Balancers
"elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP",
"elb.us-east-1.amazonaws.com": "Z26RNL4JYFTOTI",
"elb.us-west-1.amazonaws.com": "Z24FKFUX50B4VW",
"elb.us-west-2.amazonaws.com": "Z18D5FSROUN65G",
"elb.ca-central-1.amazonaws.com": "Z2EPGBW3API2WT",
"elb.ap-south-1.amazonaws.com": "ZVDDRBQ08TROA",
"elb.ap-northeast-2.amazonaws.com": "ZIBE1TIR4HY56",
"elb.ap-southeast-1.amazonaws.com": "ZKVM4W9LS7TM",
"elb.ap-southeast-2.amazonaws.com": "ZCT6FZBF4DROD",
"elb.ap-northeast-1.amazonaws.com": "Z31USIVHYNEOWT",
"elb.eu-central-1.amazonaws.com": "Z3F0SRJ5LGBH90",
"elb.eu-west-1.amazonaws.com": "Z2IFOLAFXWLO4F",
"elb.eu-west-2.amazonaws.com": "ZD4D7Y8KGAS4G",
"elb.eu-west-3.amazonaws.com": "Z1CMS0P5QUZ6D5",
"elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU",
"elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP",
"elb.us-east-1.amazonaws.com": "Z26RNL4JYFTOTI",
"elb.us-west-1.amazonaws.com": "Z24FKFUX50B4VW",
"elb.us-west-2.amazonaws.com": "Z18D5FSROUN65G",
"elb.ca-central-1.amazonaws.com": "Z2EPGBW3API2WT",
"elb.ap-south-1.amazonaws.com": "ZVDDRBQ08TROA",
"elb.ap-northeast-2.amazonaws.com": "ZIBE1TIR4HY56",
"elb.ap-southeast-1.amazonaws.com": "ZKVM4W9LS7TM",
"elb.ap-southeast-2.amazonaws.com": "ZCT6FZBF4DROD",
"elb.ap-northeast-1.amazonaws.com": "Z31USIVHYNEOWT",
"elb.eu-central-1.amazonaws.com": "Z3F0SRJ5LGBH90",
"elb.eu-west-1.amazonaws.com": "Z2IFOLAFXWLO4F",
"elb.eu-west-2.amazonaws.com": "ZD4D7Y8KGAS4G",
"elb.eu-west-3.amazonaws.com": "Z1CMS0P5QUZ6D5",
"elb.eu-north-1.amazonaws.com": "Z1UDT6IFJ4EJM",
"elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU",
"elb.cn-north-1.amazonaws.com.cn": "Z3QFB96KMJ7ED6",
"elb.cn-northwest-1.amazonaws.com.cn": "ZQEIKTCZ8352D",
}
)
@ -85,6 +92,7 @@ type Route53API interface {
ChangeResourceRecordSets(*route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error)
CreateHostedZone(*route53.CreateHostedZoneInput) (*route53.CreateHostedZoneOutput, error)
ListHostedZonesPages(input *route53.ListHostedZonesInput, fn func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool)) error
ListTagsForResource(input *route53.ListTagsForResourceInput) (*route53.ListTagsForResourceOutput, error)
}
// AWSProvider is an implementation of Provider for AWS Route53.
@ -100,6 +108,8 @@ type AWSProvider struct {
zoneIDFilter ZoneIDFilter
// filter hosted zones by type (e.g. private or public)
zoneTypeFilter ZoneTypeFilter
// filter hosted zones by tags
zoneTagFilter ZoneTagFilter
}
// AWSConfig contains configuration to create a new AWS provider.
@ -107,16 +117,18 @@ type AWSConfig struct {
DomainFilter DomainFilter
ZoneIDFilter ZoneIDFilter
ZoneTypeFilter ZoneTypeFilter
ZoneTagFilter ZoneTagFilter
BatchChangeSize int
BatchChangeInterval time.Duration
EvaluateTargetHealth bool
AssumeRole string
APIRetries int
DryRun bool
}
// NewAWSProvider initializes a new AWS Route53 based Provider.
func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
config := aws.NewConfig()
config := aws.NewConfig().WithMaxRetries(awsConfig.APIRetries)
config.WithHTTPClient(
instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{
@ -145,6 +157,7 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
domainFilter: awsConfig.DomainFilter,
zoneIDFilter: awsConfig.ZoneIDFilter,
zoneTypeFilter: awsConfig.ZoneTypeFilter,
zoneTagFilter: awsConfig.ZoneTagFilter,
batchChangeSize: awsConfig.BatchChangeSize,
batchChangeInterval: awsConfig.BatchChangeInterval,
evaluateTargetHealth: awsConfig.EvaluateTargetHealth,
@ -158,6 +171,7 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) {
zones := make(map[string]*route53.HostedZone)
var tagErr error
f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) {
for _, zone := range resp.HostedZones {
if !p.zoneIDFilter.Match(aws.StringValue(zone.Id)) {
@ -172,6 +186,18 @@ func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) {
continue
}
// Only fetch tags if a tag filter was specified
if !p.zoneTagFilter.IsEmpty() {
tags, err := p.tagsForZone(*zone.Id)
if err != nil {
tagErr = err
return false
}
if !p.zoneTagFilter.Match(tags) {
continue
}
}
zones[aws.StringValue(zone.Id)] = zone
}
@ -182,6 +208,9 @@ func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) {
if err != nil {
return nil, err
}
if tagErr != nil {
return nil, tagErr
}
for _, zone := range zones {
log.Debugf("Considering zone: %s (domain: %s)", aws.StringValue(zone.Id), aws.StringValue(zone.Name))
@ -206,6 +235,11 @@ func (p *AWSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
return nil, err
}
return p.records(zones)
}
func (p *AWSProvider) records(zones map[string]*route53.HostedZone) ([]*endpoint.Endpoint, error) {
endpoints := make([]*endpoint.Endpoint, 0)
f := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) {
for _, r := range resp.ResourceRecordSets {
// TODO(linki, ownership): Remove once ownership system is in place.
@ -230,6 +264,10 @@ func (p *AWSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
}
if r.AliasTarget != nil {
// Alias records don't have TTLs so provide the default to match the TXT generation
if ttl == 0 {
ttl = recordTTL
}
ep := endpoint.
NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), endpoint.RecordTypeCNAME, ttl, aws.StringValue(r.AliasTarget.DNSName)).
WithProviderSpecific(providerSpecificEvaluateTargetHealth, fmt.Sprintf("%t", aws.BoolValue(r.AliasTarget.EvaluateTargetHealth)))
@ -255,43 +293,65 @@ func (p *AWSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
// CreateRecords creates a given set of DNS records in the given hosted zone.
func (p *AWSProvider) CreateRecords(endpoints []*endpoint.Endpoint) error {
return p.submitChanges(p.newChanges(route53.ChangeActionCreate, endpoints))
return p.doRecords(route53.ChangeActionCreate, endpoints)
}
// UpdateRecords updates a given set of old records to a new set of records in a given hosted zone.
func (p *AWSProvider) UpdateRecords(endpoints, _ []*endpoint.Endpoint) error {
return p.submitChanges(p.newChanges(route53.ChangeActionUpsert, endpoints))
return p.doRecords(route53.ChangeActionUpsert, endpoints)
}
// DeleteRecords deletes a given set of DNS records in a given zone.
func (p *AWSProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error {
return p.submitChanges(p.newChanges(route53.ChangeActionDelete, endpoints))
return p.doRecords(route53.ChangeActionDelete, endpoints)
}
func (p *AWSProvider) doRecords(action string, endpoints []*endpoint.Endpoint) error {
zones, err := p.Zones()
if err != nil {
return err
}
records, err := p.records(zones)
if err != nil {
log.Errorf("getting records failed: %v", err)
}
return p.submitChanges(p.newChanges(action, endpoints, records, zones), zones)
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *AWSProvider) ApplyChanges(changes *plan.Changes) error {
func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zones, err := p.Zones()
if err != nil {
return err
}
records, ok := ctx.Value(RecordsContextKey).([]*endpoint.Endpoint)
if !ok {
var err error
records, err = p.records(zones)
if err != nil {
log.Errorf("getting records failed: %v", err)
}
}
combinedChanges := make([]*route53.Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionCreate, changes.Create)...)
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionUpsert, changes.UpdateNew)...)
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionDelete, changes.Delete)...)
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionCreate, changes.Create, records, zones)...)
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionUpsert, changes.UpdateNew, records, zones)...)
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionDelete, changes.Delete, records, zones)...)
return p.submitChanges(combinedChanges)
return p.submitChanges(combinedChanges, zones)
}
// submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
func (p *AWSProvider) submitChanges(changes []*route53.Change) error {
func (p *AWSProvider) submitChanges(changes []*route53.Change, zones map[string]*route53.HostedZone) error {
// return early if there is nothing to change
if len(changes) == 0 {
log.Info("All records are already up to date")
return nil
}
zones, err := p.Zones()
if err != nil {
return err
}
// separate into per-zone change sets to be passed to the API.
changesByZone := changesByZone(zones, changes)
if len(changesByZone) == 0 {
@ -343,11 +403,11 @@ func (p *AWSProvider) submitChanges(changes []*route53.Change) error {
}
// newChanges returns a collection of Changes based on the given records and action.
func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint) []*route53.Change {
func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint, recordsCache []*endpoint.Endpoint, zones map[string]*route53.HostedZone) []*route53.Change {
changes := make([]*route53.Change, 0, len(endpoints))
for _, endpoint := range endpoints {
changes = append(changes, p.newChange(action, endpoint))
changes = append(changes, p.newChange(action, endpoint, recordsCache, zones))
}
return changes
@ -356,7 +416,7 @@ func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint)
// newChange returns a Change of the given record by the given action, e.g.
// action=ChangeActionCreate returns a change for creation of the record and
// action=ChangeActionDelete returns a change for deletion of the record.
func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *route53.Change {
func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint, recordsCache []*endpoint.Endpoint, zones map[string]*route53.HostedZone) *route53.Change {
change := &route53.Change{
Action: aws.String(action),
ResourceRecordSet: &route53.ResourceRecordSet{
@ -364,15 +424,10 @@ func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *rou
},
}
rec, err := p.Records()
if err != nil {
log.Infof("getting records failed: %v", err)
}
if isAWSLoadBalancer(endpoint) {
evalTargetHealth := p.evaluateTargetHealth
if _, ok := endpoint.ProviderSpecific[providerSpecificEvaluateTargetHealth]; ok {
evalTargetHealth = endpoint.ProviderSpecific[providerSpecificEvaluateTargetHealth] == "true"
if prop, ok := endpoint.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok {
evalTargetHealth = prop.Value == "true"
}
change.ResourceRecordSet.Type = aws.String(route53.RRTypeA)
@ -381,11 +436,7 @@ func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *rou
HostedZoneId: aws.String(canonicalHostedZone(endpoint.Targets[0])),
EvaluateTargetHealth: aws.Bool(evalTargetHealth),
}
} else if hostedZone := isAWSAlias(endpoint, rec); hostedZone != "" {
zones, err := p.Zones()
if err != nil {
log.Errorf("getting zones failed: %v", err)
}
} else if hostedZone := isAWSAlias(endpoint, recordsCache); hostedZone != "" {
for _, zone := range zones {
change.ResourceRecordSet.Type = aws.String(route53.RRTypeA)
change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{
@ -412,6 +463,21 @@ func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *rou
return change
}
func (p *AWSProvider) tagsForZone(zoneID string) (map[string]string, error) {
response, err := p.client.ListTagsForResource(&route53.ListTagsForResourceInput{
ResourceType: aws.String("hostedzone"),
ResourceId: aws.String(zoneID),
})
if err != nil {
return nil, err
}
tagMap := map[string]string{}
for _, tag := range response.ResourceTagSet.Tags {
tagMap[*tag.Key] = *tag.Value
}
return tagMap, nil
}
func batchChangeSet(cs []*route53.Change, batchSize int) [][]*route53.Change {
if len(cs) <= batchSize {
return [][]*route53.Change{cs}
@ -549,7 +615,7 @@ func isAWSLoadBalancer(ep *endpoint.Endpoint) bool {
// isAWSAlias determines if a given hostname belongs to an AWS Alias record by doing an reverse lookup.
func isAWSAlias(ep *endpoint.Endpoint, addrs []*endpoint.Endpoint) string {
if val, exists := ep.ProviderSpecific["alias"]; ep.RecordType == endpoint.RecordTypeCNAME && exists && val == "true" {
if prop, exists := ep.GetProviderSpecificProperty("alias"); ep.RecordType == endpoint.RecordTypeCNAME && exists && prop.Value == "true" {
for _, addr := range addrs {
if addr.DNSName == ep.Targets[0] {
if hostedZone := canonicalHostedZone(addr.Targets[0]); hostedZone != "" {

View File

@ -17,11 +17,13 @@ limitations under the License.
package provider
import (
"context"
"strings"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/request"
@ -192,7 +194,7 @@ func (p *AWSSDProvider) instancesToEndpoint(ns *sd.NamespaceSummary, srv *sd.Ser
}
// ApplyChanges applies Kubernetes changes in endpoints to AWS API
func (p *AWSSDProvider) ApplyChanges(changes *plan.Changes) error {
func (p *AWSSDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
// return early if there is nothing to change
if len(changes.Create) == 0 && len(changes.Delete) == 0 && len(changes.UpdateNew) == 0 {
log.Info("All records are already up to date")

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"errors"
"math/rand"
"reflect"
@ -316,7 +317,7 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) {
provider := newTestAWSSDProvider(api, NewDomainFilter([]string{}), "")
// apply creates
provider.ApplyChanges(&plan.Changes{
provider.ApplyChanges(context.Background(), &plan.Changes{
Create: expectedEndpoints,
})
@ -332,7 +333,7 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) {
assert.True(t, testutils.SameEndpoints(expectedEndpoints, endpoints), "expected and actual endpoints don't match, expected=%v, actual=%v", expectedEndpoints, endpoints)
// apply deletes
provider.ApplyChanges(&plan.Changes{
provider.ApplyChanges(context.Background(), &plan.Changes{
Delete: expectedEndpoints,
})

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"net"
"sort"
@ -50,6 +51,7 @@ var _ Route53API = &Route53APIStub{}
type Route53APIStub struct {
zones map[string]*route53.HostedZone
recordSets map[string]map[string][]*route53.ResourceRecordSet
zoneTags map[string][]*route53.Tag
m dynamicMock
}
@ -66,6 +68,7 @@ func NewRoute53APIStub() *Route53APIStub {
return &Route53APIStub{
zones: make(map[string]*route53.HostedZone),
recordSets: make(map[string]map[string][]*route53.ResourceRecordSet),
zoneTags: make(map[string][]*route53.Tag),
}
}
@ -87,6 +90,43 @@ func (r *Route53APIStub) ListResourceRecordSetsPages(input *route53.ListResource
return nil
}
type Route53APICounter struct {
wrapped Route53API
calls map[string]int
}
func NewRoute53APICounter(w Route53API) *Route53APICounter {
return &Route53APICounter{
wrapped: w,
calls: map[string]int{},
}
}
func (c *Route53APICounter) ListResourceRecordSetsPages(input *route53.ListResourceRecordSetsInput, fn func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool)) error {
c.calls["ListResourceRecordSetsPages"]++
return c.wrapped.ListResourceRecordSetsPages(input, fn)
}
func (c *Route53APICounter) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) {
c.calls["ChangeResourceRecordSets"]++
return c.wrapped.ChangeResourceRecordSets(input)
}
func (c *Route53APICounter) CreateHostedZone(input *route53.CreateHostedZoneInput) (*route53.CreateHostedZoneOutput, error) {
c.calls["CreateHostedZone"]++
return c.wrapped.CreateHostedZone(input)
}
func (c *Route53APICounter) ListHostedZonesPages(input *route53.ListHostedZonesInput, fn func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool)) error {
c.calls["ListHostedZonesPages"]++
return c.wrapped.ListHostedZonesPages(input, fn)
}
func (c *Route53APICounter) ListTagsForResource(input *route53.ListTagsForResourceInput) (*route53.ListTagsForResourceOutput, error) {
c.calls["ListTagsForResource"]++
return c.wrapped.ListTagsForResource(input)
}
// Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk
func wildcardEscape(s string) string {
if strings.Contains(s, "*") {
@ -95,6 +135,20 @@ func wildcardEscape(s string) string {
return s
}
func (r *Route53APIStub) ListTagsForResource(input *route53.ListTagsForResourceInput) (*route53.ListTagsForResourceOutput, error) {
if aws.StringValue(input.ResourceType) == "hostedzone" {
tags := r.zoneTags[aws.StringValue(input.ResourceId)]
return &route53.ListTagsForResourceOutput{
ResourceTagSet: &route53.ResourceTagSet{
ResourceId: input.ResourceId,
ResourceType: input.ResourceType,
Tags: tags,
},
}, nil
}
return &route53.ListTagsForResourceOutput{}, nil
}
func (r *Route53APIStub) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) {
if r.m.isMocked("ChangeResourceRecordSets", input) {
return r.m.ChangeResourceRecordSets(input)
@ -231,15 +285,17 @@ func TestAWSZones(t *testing.T) {
msg string
zoneIDFilter ZoneIDFilter
zoneTypeFilter ZoneTypeFilter
zoneTagFilter ZoneTagFilter
expectedZones map[string]*route53.HostedZone
}{
{"no filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), allZones},
{"public filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("public"), publicZones},
{"private filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("private"), privateZones},
{"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), noZones},
{"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), privateZones},
{"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},
} {
provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
provider, _ := newAWSProviderWithTagFilter(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
zones, err := provider.Zones()
require.NoError(t, err)
@ -267,9 +323,9 @@ func TestAWSRecords(t *testing.T) {
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"),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"),
endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"),
endpoint.NewEndpoint("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"),
endpoint.NewEndpointWithTTL("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"),
endpoint.NewEndpointWithTTL("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpointWithTTL("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "random"),
})
@ -357,74 +413,96 @@ func TestAWSDeleteRecords(t *testing.T) {
}
func TestAWSApplyChanges(t *testing.T) {
provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), 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"),
endpoint.NewEndpointWithTTL("delete-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), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
})
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
tests := []struct {
name string
setup func(p *AWSProvider) context.Context
listRRSets int
}{
{"no cache", func(p *AWSProvider) context.Context { return context.Background() }, 3},
{"cached", func(p *AWSProvider) context.Context {
records, err := p.Records()
require.NoError(t, err)
return context.WithValue(context.Background(), RecordsContextKey, records)
}, 0},
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
for _, tt := range tests {
provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), 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"),
endpoint.NewEndpointWithTTL("delete-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), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
})
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
changes := &plan.Changes{
Create: createRecords,
UpdateNew: updatedRecords,
UpdateOld: currentRecords,
Delete: deleteRecords,
}
ctx := tt.setup(provider)
counter := NewRoute53APICounter(provider.client)
provider.client = counter
require.NoError(t, provider.ApplyChanges(ctx, changes))
assert.Equal(t, 1, counter.calls["ListHostedZonesPages"], tt.name)
assert.Equal(t, tt.listRRSets, counter.calls["ListResourceRecordSetsPages"], tt.name)
records, err := provider.Records()
require.NoError(t, err, tt.name)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"),
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
})
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
changes := &plan.Changes{
Create: createRecords,
UpdateNew: updatedRecords,
UpdateOld: currentRecords,
Delete: deleteRecords,
}
require.NoError(t, provider.ApplyChanges(changes))
records, err := provider.Records()
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"),
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
})
}
func TestAWSApplyChangesDryRun(t *testing.T) {
@ -481,7 +559,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) {
Delete: deleteRecords,
}
require.NoError(t, provider.ApplyChanges(changes))
require.NoError(t, provider.ApplyChanges(context.Background(), changes))
records, err := provider.Records()
require.NoError(t, err)
@ -601,10 +679,12 @@ func TestAWSsubmitChanges(t *testing.T) {
}
}
zones, _ := provider.Zones()
records, _ := provider.Records()
cs := make([]*route53.Change, 0, len(endpoints))
cs = append(cs, provider.newChanges(route53.ChangeActionCreate, endpoints)...)
cs = append(cs, provider.newChanges(route53.ChangeActionCreate, endpoints, records, zones)...)
require.NoError(t, provider.submitChanges(cs))
require.NoError(t, provider.submitChanges(cs, zones))
records, err := provider.Records()
require.NoError(t, err)
@ -616,10 +696,15 @@ func TestAWSsubmitChangesError(t *testing.T) {
provider, clientStub := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure"))
ep := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1")
cs := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep})
zones, err := provider.Zones()
require.NoError(t, err)
records, err := provider.Records()
require.NoError(t, err)
require.Error(t, provider.submitChanges(cs))
ep := endpoint.NewEndpointWithTTL("fail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.0.0.1")
cs := provider.newChanges(route53.ChangeActionCreate, []*endpoint.Endpoint{ep}, records, zones)
require.Error(t, provider.submitChanges(cs, zones))
}
func TestAWSBatchChangeSet(t *testing.T) {
@ -781,7 +866,10 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) {
Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"},
RecordType: endpoint.RecordTypeCNAME,
ProviderSpecific: endpoint.ProviderSpecific{
providerSpecificEvaluateTargetHealth: key,
endpoint.ProviderSpecificProperty{
Name: providerSpecificEvaluateTargetHealth,
Value: key,
},
},
},
}
@ -832,9 +920,14 @@ func TestAWSisAWSAlias(t *testing.T) {
{"foo.example.org", endpoint.RecordTypeCNAME, "true", ""},
} {
ep := &endpoint.Endpoint{
Targets: endpoint.Targets{tc.target},
RecordType: tc.recordType,
ProviderSpecific: map[string]string{"alias": tc.alias},
Targets: endpoint.Targets{tc.target},
RecordType: tc.recordType,
ProviderSpecific: endpoint.ProviderSpecific{
endpoint.ProviderSpecificProperty{
Name: "alias",
Value: tc.alias,
},
},
}
addrs := []*endpoint.Endpoint{
{
@ -872,6 +965,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"},
// Network Load Balancers
{"foo.elb.us-east-2.amazonaws.com", "ZLMOA37VPKANP"},
{"foo.elb.us-east-1.amazonaws.com", "Z26RNL4JYFTOTI"},
@ -888,6 +983,8 @@ func TestAWSCanonicalHostedZone(t *testing.T) {
{"foo.elb.eu-west-2.amazonaws.com", "ZD4D7Y8KGAS4G"},
{"foo.elb.eu-west-3.amazonaws.com", "Z1CMS0P5QUZ6D5"},
{"foo.elb.sa-east-1.amazonaws.com", "ZTK26PT1VY4CU"},
{"foo.elb.cn-north-1.amazonaws.com.cn", "Z3QFB96KMJ7ED6"},
{"foo.elb.cn-northwest-1.amazonaws.com.cn", "ZQEIKTCZ8352D"},
// No Load Balancer
{"foo.example.org", ""},
} {
@ -964,7 +1061,7 @@ func setupAWSRecords(t *testing.T, provider *AWSProvider, endpoints []*endpoint.
escapeAWSRecords(t, provider, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.")
escapeAWSRecords(t, provider, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.")
records, err = provider.Records()
_, err = provider.Records()
require.NoError(t, err)
}
@ -1027,8 +1124,11 @@ func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) {
require.NoError(t, err)
}
}
func newAWSProvider(t *testing.T, domainFilter 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 newAWSProviderWithTagFilter(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, zoneTagFilter ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) {
client := NewRoute53APIStub()
provider := &AWSProvider{
@ -1039,6 +1139,7 @@ func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneID
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
zoneTypeFilter: zoneTypeFilter,
zoneTagFilter: zoneTagFilter,
dryRun: false,
}
@ -1067,6 +1168,8 @@ func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneID
Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)},
})
setupZoneTags(provider.client.(*Route53APIStub))
setupAWSRecords(t, provider, records)
provider.dryRun = dryRun
@ -1074,6 +1177,40 @@ func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneID
return provider, client
}
func setupZoneTags(client *Route53APIStub) {
addZoneTags(client.zoneTags, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.", map[string]string{
"zone-1-tag-1": "tag-1-value",
"domain": "test-2",
"zone": "1",
})
addZoneTags(client.zoneTags, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.", map[string]string{
"zone-2-tag-1": "tag-1-value",
"domain": "test-2",
"zone": "2",
})
addZoneTags(client.zoneTags, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.", map[string]string{
"zone-3-tag-1": "tag-1-value",
"domain": "test-2",
"zone": "3",
})
addZoneTags(client.zoneTags, "/hostedzone/zone-4.ext-dns-test-2.teapot.zalan.do.", map[string]string{
"zone-4-tag-1": "tag-1-value",
"domain": "test-3",
"zone": "4",
})
}
func addZoneTags(tagMap map[string][]*route53.Tag, zoneID string, tags map[string]string) {
tagList := make([]*route53.Tag, 0, len(tags))
for k, v := range tags {
tagList = append(tagList, &route53.Tag{
Key: aws.String(k),
Value: aws.String(v),
})
}
tagMap[zoneID] = tagList
}
func validateRecords(t *testing.T, records []*route53.ResourceRecordSet, expected []*route53.ResourceRecordSet) {
assert.Equal(t, expected, records)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"io/ioutil"
"strings"
@ -179,9 +180,9 @@ func (p *AzureProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
return true
}
name := formatAzureDNSName(*recordSet.Name, *zone.Name)
target := extractAzureTarget(&recordSet)
if target == "" {
log.Errorf("Failed to extract target for '%s' with type '%s'.", name, recordType)
targets := extractAzureTargets(&recordSet)
if len(targets) == 0 {
log.Errorf("Failed to extract targets for '%s' with type '%s'.", name, recordType)
return true
}
var ttl endpoint.TTL
@ -189,7 +190,7 @@ func (p *AzureProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
ttl = endpoint.TTL(*recordSet.TTL)
}
ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), target)
ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...)
log.Debugf(
"Found %s record for '%s' with target '%s'.",
ep.RecordType,
@ -209,7 +210,7 @@ func (p *AzureProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
// ApplyChanges applies the given changes.
//
// Returns nil if the operation was successful or an error if the operation failed.
func (p *AzureProvider) ApplyChanges(changes *plan.Changes) error {
func (p *AzureProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zones, err := p.zones()
if err != nil {
return err
@ -414,14 +415,16 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
}
switch dns.RecordType(endpoint.RecordType) {
case dns.A:
aRecords := make([]dns.ARecord, len(endpoint.Targets))
for i, target := range endpoint.Targets {
aRecords[i] = dns.ARecord{
Ipv4Address: to.StringPtr(target),
}
}
return dns.RecordSet{
RecordSetProperties: &dns.RecordSetProperties{
TTL: to.Int64Ptr(ttl),
ARecords: &[]dns.ARecord{
{
Ipv4Address: to.StringPtr(endpoint.Targets[0]),
},
},
TTL: to.Int64Ptr(ttl),
ARecords: &aRecords,
},
}, nil
case dns.CNAME:
@ -459,22 +462,26 @@ func formatAzureDNSName(recordName, zoneName string) string {
}
// Helper function (shared with text code)
func extractAzureTarget(recordSet *dns.RecordSet) string {
func extractAzureTargets(recordSet *dns.RecordSet) []string {
properties := recordSet.RecordSetProperties
if properties == nil {
return ""
return []string{}
}
// Check for A records
aRecords := properties.ARecords
if aRecords != nil && len(*aRecords) > 0 && (*aRecords)[0].Ipv4Address != nil {
return *(*aRecords)[0].Ipv4Address
targets := make([]string, len(*aRecords))
for i, aRecord := range *aRecords {
targets[i] = *aRecord.Ipv4Address
}
return targets
}
// Check for CNAME records
cnameRecord := properties.CnameRecord
if cnameRecord != nil && cnameRecord.Cname != nil {
return *cnameRecord.Cname
return []string{*cnameRecord.Cname}
}
// Check for TXT records
@ -482,8 +489,8 @@ func extractAzureTarget(recordSet *dns.RecordSet) string {
if txtRecords != nil && len(*txtRecords) > 0 && (*txtRecords)[0].Value != nil {
values := (*txtRecords)[0].Value
if values != nil && len(*values) > 0 {
return (*values)[0]
return []string{(*values)[0]}
}
}
return ""
return []string{}
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"testing"
"github.com/Azure/azure-sdk-for-go/arm/dns"
@ -57,47 +58,52 @@ func (client *mockZonesClient) ListByResourceGroupNextResults(lastResults dns.Zo
return dns.ZoneListResult{}, nil
}
func aRecordSetPropertiesGetter(value string, ttl int64) *dns.RecordSetProperties {
func aRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
aRecords := make([]dns.ARecord, len(values))
for i, value := range values {
aRecords[i] = dns.ARecord{
Ipv4Address: to.StringPtr(value),
}
}
return &dns.RecordSetProperties{
TTL: to.Int64Ptr(ttl),
ARecords: &[]dns.ARecord{
{
Ipv4Address: to.StringPtr(value),
},
},
TTL: to.Int64Ptr(ttl),
ARecords: &aRecords,
}
}
func cNameRecordSetPropertiesGetter(value string, ttl int64) *dns.RecordSetProperties {
func cNameRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
return &dns.RecordSetProperties{
TTL: to.Int64Ptr(ttl),
CnameRecord: &dns.CnameRecord{
Cname: to.StringPtr(value),
Cname: to.StringPtr(values[0]),
},
}
}
func txtRecordSetPropertiesGetter(value string, ttl int64) *dns.RecordSetProperties {
func txtRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
return &dns.RecordSetProperties{
TTL: to.Int64Ptr(ttl),
TxtRecords: &[]dns.TxtRecord{
{
Value: &[]string{value},
Value: &[]string{values[0]},
},
},
}
}
func othersRecordSetPropertiesGetter(value string, ttl int64) *dns.RecordSetProperties {
func othersRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties {
return &dns.RecordSetProperties{
TTL: to.Int64Ptr(ttl),
}
}
func createMockRecordSet(name, recordType, value string) dns.RecordSet {
return createMockRecordSetWithTTL(name, recordType, value, 0)
func createMockRecordSet(name, recordType string, values ...string) dns.RecordSet {
return createMockRecordSetMultiWithTTL(name, recordType, 0, values...)
}
func createMockRecordSetWithTTL(name, recordType, value string, ttl int64) dns.RecordSet {
var getterFunc func(value string, ttl int64) *dns.RecordSetProperties
return createMockRecordSetMultiWithTTL(name, recordType, ttl, value)
}
func createMockRecordSetMultiWithTTL(name, recordType string, ttl int64, values ...string) dns.RecordSet {
var getterFunc func(values []string, ttl int64) *dns.RecordSetProperties
switch recordType {
case endpoint.RecordTypeA:
@ -112,7 +118,7 @@ func createMockRecordSetWithTTL(name, recordType, value string, ttl int64) dns.R
return dns.RecordSet{
Name: to.StringPtr(name),
Type: to.StringPtr("Microsoft.Network/dnszones/" + recordType),
RecordSetProperties: getterFunc(value, ttl),
RecordSetProperties: getterFunc(values, ttl),
}
}
@ -148,7 +154,7 @@ func (client *mockRecordsClient) CreateOrUpdate(resourceGroupName string, zoneNa
formatAzureDNSName(relativeRecordSetName, zoneName),
string(recordType),
ttl,
extractAzureTarget(&parameters),
extractAzureTargets(&parameters)...,
),
)
return parameters, nil
@ -209,6 +215,46 @@ func TestAzureRecord(t *testing.T) {
}
func TestAzureMultiRecord(t *testing.T) {
zonesClient := mockZonesClient{
mockZoneListResult: &dns.ZoneListResult{
Value: &[]dns.Zone{
createMockZone("example.com", "/dnszones/example.com"),
},
},
}
recordsClient := mockRecordsClient{
mockRecordSet: &[]dns.RecordSet{
createMockRecordSet("@", "NS", "ns1-03.azure-dns.com."),
createMockRecordSet("@", "SOA", "Email: azuredns-hostmaster.microsoft.com"),
createMockRecordSet("@", endpoint.RecordTypeA, "123.123.123.122", "234.234.234.233"),
createMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
createMockRecordSetMultiWithTTL("nginx", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"),
createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
},
}
provider := newAzureProvider(NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s", &zonesClient, &recordsClient)
actual, err := provider.Records()
if err != nil {
t.Fatal(err)
}
expected := []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "123.123.123.122", "234.234.234.233"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"),
endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"),
endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"),
}
validateAzureEndpoints(t, actual, expected)
}
func TestAzureApplyChanges(t *testing.T) {
recordsClient := mockRecordsClient{}
@ -224,7 +270,7 @@ func TestAzureApplyChanges(t *testing.T) {
validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "1.2.3.5"),
endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "other.com"),
endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
@ -265,7 +311,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordsClie
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.5", "1.2.3.4"),
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeTXT, "tag"),
@ -299,7 +345,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordsClie
Delete: deleteRecords,
}
if err := provider.ApplyChanges(changes); err != nil {
if err := provider.ApplyChanges(context.Background(), changes); err != nil {
t.Fatal(err)
}
}

View File

@ -17,8 +17,11 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"os"
"sort"
"strconv"
"strings"
cloudflare "github.com/cloudflare/cloudflare-go"
@ -26,6 +29,7 @@ import (
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
"github.com/kubernetes-incubator/external-dns/source"
)
const (
@ -53,6 +57,7 @@ type cloudFlareDNS interface {
UserDetails() (cloudflare.User, error)
ZoneIDByName(zoneName string) (string, error)
ListZones(zoneID ...string) ([]cloudflare.Zone, error)
ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error)
DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error)
CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error)
DeleteDNSRecord(zoneID, recordID string) error
@ -89,24 +94,29 @@ func (z zoneService) DeleteDNSRecord(zoneID, recordID string) error {
return z.service.DeleteDNSRecord(zoneID, recordID)
}
func (z zoneService) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
return z.service.ListZonesContext(ctx, opts...)
}
// CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
type CloudFlareProvider struct {
Client cloudFlareDNS
// only consider hosted zones managing domains ending in this suffix
domainFilter DomainFilter
zoneIDFilter ZoneIDFilter
proxied bool
DryRun bool
domainFilter DomainFilter
zoneIDFilter ZoneIDFilter
proxiedByDefault bool
DryRun bool
PaginationOptions cloudflare.PaginationOptions
}
// cloudFlareChange differentiates between ChangActions
type cloudFlareChange struct {
Action string
ResourceRecordSet cloudflare.DNSRecord
ResourceRecordSet []cloudflare.DNSRecord
}
// NewCloudFlareProvider initializes a new CloudFlare DNS based Provider.
func NewCloudFlareProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, proxied bool, dryRun bool) (*CloudFlareProvider, error) {
func NewCloudFlareProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zonesPerPage int, proxiedByDefault bool, dryRun bool) (*CloudFlareProvider, error) {
// initialize via API email and API key and returns new API object
config, err := cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL"))
if err != nil {
@ -114,11 +124,15 @@ func NewCloudFlareProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter,
}
provider := &CloudFlareProvider{
//Client: config,
Client: zoneService{config},
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
proxied: proxied,
DryRun: dryRun,
Client: zoneService{config},
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
proxiedByDefault: proxiedByDefault,
DryRun: dryRun,
PaginationOptions: cloudflare.PaginationOptions{
PerPage: zonesPerPage,
Page: 1,
},
}
return provider, nil
}
@ -126,24 +140,30 @@ func NewCloudFlareProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter,
// Zones returns the list of hosted zones.
func (p *CloudFlareProvider) Zones() ([]cloudflare.Zone, error) {
result := []cloudflare.Zone{}
ctx := context.TODO()
p.PaginationOptions.Page = 1
zones, err := p.Client.ListZones()
if err != nil {
return nil, err
}
for _, zone := range zones {
if !p.domainFilter.Match(zone.Name) {
continue
for {
zonesResponse, err := p.Client.ListZonesContext(ctx, cloudflare.WithPagination(p.PaginationOptions))
if err != nil {
return nil, err
}
if !p.zoneIDFilter.Match(zone.ID) {
continue
for _, zone := range zonesResponse.Result {
if !p.domainFilter.Match(zone.Name) {
continue
}
if !p.zoneIDFilter.Match(zone.ID) {
continue
}
result = append(result, zone)
}
result = append(result, zone)
if p.PaginationOptions.Page == zonesResponse.ResultInfo.TotalPages {
break
}
p.PaginationOptions.Page++
}
return result, nil
}
@ -163,7 +183,7 @@ func (p *CloudFlareProvider) Records() ([]*endpoint.Endpoint, error) {
for _, r := range records {
if supportedRecordType(r.Type) {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Content))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Content).WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(r.Proxied)))
}
}
}
@ -172,12 +192,14 @@ func (p *CloudFlareProvider) Records() ([]*endpoint.Endpoint, error) {
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *CloudFlareProvider) ApplyChanges(changes *plan.Changes) error {
func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
proxiedByDefault := p.proxiedByDefault
combinedChanges := make([]*cloudFlareChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareCreate, changes.Create, p.proxied)...)
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareUpdate, changes.UpdateNew, p.proxied)...)
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareDelete, changes.Delete, p.proxied)...)
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareCreate, changes.Create, proxiedByDefault)...)
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareUpdate, changes.UpdateNew, proxiedByDefault)...)
combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareDelete, changes.Delete, proxiedByDefault)...)
return p.submitChanges(combinedChanges)
}
@ -203,11 +225,12 @@ func (p *CloudFlareProvider) submitChanges(changes []*cloudFlareChange) error {
}
for _, change := range changes {
logFields := log.Fields{
"record": change.ResourceRecordSet.Name,
"type": change.ResourceRecordSet.Type,
"ttl": change.ResourceRecordSet.TTL,
"action": change.Action,
"zone": zoneID,
"record": change.ResourceRecordSet[0].Name,
"type": change.ResourceRecordSet[0].Type,
"ttl": change.ResourceRecordSet[0].TTL,
"targets": len(change.ResourceRecordSet),
"action": change.Action,
"zone": zoneID,
}
log.WithFields(logFields).Info("Changing record.")
@ -215,22 +238,25 @@ func (p *CloudFlareProvider) submitChanges(changes []*cloudFlareChange) error {
if p.DryRun {
continue
}
recordID := p.getRecordID(records, change.ResourceRecordSet)
switch change.Action {
case cloudFlareCreate:
_, err := p.Client.CreateDNSRecord(zoneID, change.ResourceRecordSet)
if err != nil {
log.WithFields(logFields).Errorf("failed to create record: %v", err)
recordIDs := p.getRecordIDs(records, change.ResourceRecordSet[0])
// to simplify bookkeeping for multiple records, an update is executed as delete+create
if change.Action == cloudFlareDelete || change.Action == cloudFlareUpdate {
for _, recordID := range recordIDs {
err := p.Client.DeleteDNSRecord(zoneID, recordID)
if err != nil {
log.WithFields(logFields).Errorf("failed to delete record: %v", err)
}
}
case cloudFlareDelete:
err := p.Client.DeleteDNSRecord(zoneID, recordID)
if err != nil {
log.WithFields(logFields).Errorf("failed to delete record: %v", err)
}
case cloudFlareUpdate:
err := p.Client.UpdateDNSRecord(zoneID, recordID, change.ResourceRecordSet)
if err != nil {
log.WithFields(logFields).Errorf("failed to update record: %v", err)
}
if change.Action == cloudFlareCreate || change.Action == cloudFlareUpdate {
for _, record := range change.ResourceRecordSet {
_, err := p.Client.CreateDNSRecord(zoneID, record)
if err != nil {
log.WithFields(logFields).Errorf("failed to create record: %v", err)
}
}
}
}
@ -249,9 +275,9 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []
}
for _, c := range changeSet {
zoneID, _ := zoneNameIDMapper.FindZone(c.ResourceRecordSet.Name)
zoneID, _ := zoneNameIDMapper.FindZone(c.ResourceRecordSet[0].Name)
if zoneID == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", c.ResourceRecordSet.Name)
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", c.ResourceRecordSet[0].Name)
continue
}
changes[zoneID] = append(changes[zoneID], c)
@ -260,43 +286,71 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []
return changes
}
func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) string {
func (p *CloudFlareProvider) getRecordIDs(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) []string {
recordIDs := make([]string, 0)
for _, zoneRecord := range records {
if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type {
return zoneRecord.ID
recordIDs = append(recordIDs, zoneRecord.ID)
}
}
return ""
sort.Strings(recordIDs)
return recordIDs
}
// newCloudFlareChanges returns a collection of Changes based on the given records and action.
func newCloudFlareChanges(action string, endpoints []*endpoint.Endpoint, proxied bool) []*cloudFlareChange {
func newCloudFlareChanges(action string, endpoints []*endpoint.Endpoint, proxiedByDefault bool) []*cloudFlareChange {
changes := make([]*cloudFlareChange, 0, len(endpoints))
for _, endpoint := range endpoints {
changes = append(changes, newCloudFlareChange(action, endpoint, proxied))
changes = append(changes, newCloudFlareChange(action, endpoint, proxiedByDefault))
}
return changes
}
func newCloudFlareChange(action string, endpoint *endpoint.Endpoint, proxied bool) *cloudFlareChange {
func newCloudFlareChange(action string, endpoint *endpoint.Endpoint, proxiedByDefault bool) *cloudFlareChange {
ttl := defaultCloudFlareRecordTTL
if proxied && (cloudFlareTypeNotSupported[endpoint.RecordType] || strings.Contains(endpoint.DNSName, "*")) {
proxied = false
}
proxied := shouldBeProxied(endpoint, proxiedByDefault)
if endpoint.RecordTTL.IsConfigured() {
ttl = int(endpoint.RecordTTL)
}
return &cloudFlareChange{
Action: action,
ResourceRecordSet: cloudflare.DNSRecord{
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[0],
},
Content: endpoint.Targets[i],
}
}
return &cloudFlareChange{
Action: action,
ResourceRecordSet: resourceRecordSet,
}
}
func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool {
proxied := proxiedByDefault
for _, v := range endpoint.ProviderSpecific {
if v.Name == source.CloudflareProxiedKey {
b, err := strconv.ParseBool(v.Value)
if err != nil {
log.Errorf("Failed to parse annotation [%s]: %v", source.CloudflareProxiedKey, err)
} else {
proxied = b
}
break
}
}
if cloudFlareTypeNotSupported[endpoint.RecordType] || strings.Contains(endpoint.DNSName, "*") {
proxied = false
}
return proxied
}

View File

@ -17,15 +17,14 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"os"
"testing"
cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -66,6 +65,18 @@ func (m *mockCloudFlareClient) ListZones(zoneID ...string) ([]cloudflare.Zone, e
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 mockCloudFlareUserDetailsFail struct{}
func (m *mockCloudFlareUserDetailsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
@ -96,6 +107,10 @@ func (m *mockCloudFlareUserDetailsFail) ListZones(zoneID ...string) ([]cloudflar
return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil
}
func (m *mockCloudFlareUserDetailsFail) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
return cloudflare.ZonesResponse{}, nil
}
type mockCloudFlareCreateZoneFail struct{}
func (m *mockCloudFlareCreateZoneFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
@ -155,6 +170,17 @@ func (m *mockCloudFlareDNSRecordsFail) ListZones(zoneID ...string) ([]cloudflare
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 mockCloudFlareZoneIDByNameFail struct{}
func (m *mockCloudFlareZoneIDByNameFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
@ -245,6 +271,10 @@ func (m *mockCloudFlareListZonesFail) ListZones(zoneID ...string) ([]cloudflare.
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")
}
type mockCloudFlareCreateRecordsFail struct{}
func (m *mockCloudFlareCreateRecordsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
@ -275,6 +305,10 @@ func (m *mockCloudFlareCreateRecordsFail) ListZones(zoneID ...string) ([]cloudfl
return []cloudflare.Zone{{}}, fmt.Errorf("no zones available")
}
func (m *mockCloudFlareCreateRecordsFail) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
return cloudflare.ZonesResponse{}, nil
}
type mockCloudFlareDeleteRecordsFail struct{}
func (m *mockCloudFlareDeleteRecordsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
@ -305,6 +339,10 @@ func (m *mockCloudFlareDeleteRecordsFail) ListZones(zoneID ...string) ([]cloudfl
return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil
}
func (m *mockCloudFlareDeleteRecordsFail) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
return cloudflare.ZonesResponse{}, nil
}
type mockCloudFlareUpdateRecordsFail struct{}
func (m *mockCloudFlareUpdateRecordsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
@ -335,6 +373,10 @@ func (m *mockCloudFlareUpdateRecordsFail) ListZones(zoneID ...string) ([]cloudfl
return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil
}
func (m *mockCloudFlareUpdateRecordsFail) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
return cloudflare.ZonesResponse{}, nil
}
func TestNewCloudFlareChanges(t *testing.T) {
expect := []struct {
Name string
@ -357,7 +399,7 @@ func TestNewCloudFlareChanges(t *testing.T) {
for i, change := range changes {
assert.Equal(
t,
change.ResourceRecordSet.TTL,
change.ResourceRecordSet[0].TTL,
expect[i].TTL,
expect[i].Name)
}
@ -365,7 +407,37 @@ func TestNewCloudFlareChanges(t *testing.T) {
func TestNewCloudFlareChangeNoProxied(t *testing.T) {
change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Targets: endpoint.Targets{"target"}}, false)
assert.False(t, change.ResourceRecordSet.Proxied)
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) {
@ -387,14 +459,14 @@ func TestNewCloudFlareChangeProxiable(t *testing.T) {
change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: cloudFlareType.recordType, Targets: endpoint.Targets{"target"}}, true)
if cloudFlareType.proxiable {
assert.True(t, change.ResourceRecordSet.Proxied)
assert.True(t, change.ResourceRecordSet[0].Proxied)
} else {
assert.False(t, change.ResourceRecordSet.Proxied)
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.Proxied)
assert.False(t, change.ResourceRecordSet[0].Proxied)
}
func TestCloudFlareZones(t *testing.T) {
@ -439,13 +511,23 @@ func TestRecords(t *testing.T) {
func TestNewCloudFlareProvider(t *testing.T) {
_ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx")
_ = os.Setenv("CF_API_EMAIL", "test@test.com")
_, err := NewCloudFlareProvider(NewDomainFilter([]string{"ext-dns-test.zalando.to."}), NewZoneIDFilter([]string{""}), false, true)
_, err := NewCloudFlareProvider(
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(NewDomainFilter([]string{"ext-dns-test.zalando.to."}), NewZoneIDFilter([]string{""}), false, true)
_, err = NewCloudFlareProvider(
NewDomainFilter([]string{"ext-dns-test.zalando.to."}),
NewZoneIDFilter([]string{""}),
50,
false,
true)
if err == nil {
t.Errorf("expected to fail")
}
@ -460,7 +542,7 @@ func TestApplyChanges(t *testing.T) {
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(changes)
err := provider.ApplyChanges(context.Background(), changes)
if err != nil {
t.Errorf("should not fail, %s", err)
}
@ -471,7 +553,7 @@ func TestApplyChanges(t *testing.T) {
changes.UpdateOld = []*endpoint.Endpoint{}
changes.UpdateNew = []*endpoint.Endpoint{}
err = provider.ApplyChanges(changes)
err = provider.ApplyChanges(context.Background(), changes)
if err != nil {
t.Errorf("should not fail, %s", err)
}
@ -492,14 +574,18 @@ func TestCloudFlareGetRecordID(t *testing.T) {
},
}
assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{
assert.Len(t, p.getRecordIDs(records, cloudflare.DNSRecord{
Name: "foo.com",
Type: endpoint.RecordTypeA,
}))
assert.Equal(t, "2", p.getRecordID(records, cloudflare.DNSRecord{
}), 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) {

View File

@ -153,7 +153,7 @@ func (c etcdClient) DeleteService(key string) error {
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
defer cancel()
_, err := c.client.Delete(ctx, key)
_, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix())
return err
}
@ -298,7 +298,7 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
}
// ApplyChanges stores changes back to etcd converting them to CoreDNS format and aggregating A/CNAME and TXT records
func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
func (p coreDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
grouped := map[string][]*endpoint.Endpoint{}
for _, ep := range changes.Create {
grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
@ -317,22 +317,26 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
if ep.RecordType == endpoint.RecordTypeTXT {
continue
}
prefix := ep.Labels[randomPrefixLabel]
if prefix == "" {
prefix = fmt.Sprintf("%08x", rand.Int31())
for _, target := range ep.Targets {
prefix := ep.Labels[randomPrefixLabel]
if prefix == "" {
prefix = fmt.Sprintf("%08x", rand.Int31())
}
service := Service{
Host: target,
Text: ep.Labels["originalText"],
Key: etcdKeyFor(prefix + "." + dnsName),
TargetStrip: strings.Count(prefix, ".") + 1,
TTL: uint32(ep.RecordTTL),
}
services = append(services, service)
}
service := Service{
Host: ep.Targets[0],
Text: ep.Labels["originalText"],
Key: etcdKeyFor(prefix + "." + dnsName),
TargetStrip: strings.Count(prefix, ".") + 1,
TTL: uint32(ep.RecordTTL),
}
services = append(services, service)
}
index := 0
for _, ep := range group {
if ep.RecordType != "TXT" {
if ep.RecordType != endpoint.RecordTypeTXT {
continue
}
if index >= len(services) {

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"strings"
"testing"
@ -227,7 +228,7 @@ func TestCoreDNSApplyChanges(t *testing.T) {
endpoint.NewEndpoint("domain2.local", endpoint.RecordTypeCNAME, "site.local"),
},
}
coredns.ApplyChanges(changes1)
coredns.ApplyChanges(context.Background(), changes1)
expectedServices1 := map[string]*Service{
"/skydns/local/domain1": {Host: "5.5.5.5", Text: "string1"},
@ -285,7 +286,7 @@ func applyServiceChanges(provider coreDNSProvider, changes *plan.Changes) {
}
}
}
provider.ApplyChanges(changes)
provider.ApplyChanges(context.Background(), changes)
}
func validateServices(services, expectedServices map[string]*Service, t *testing.T, step int) {

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"net"
"net/http"
@ -379,7 +380,7 @@ func addEndpoint(ep *endpoint.Endpoint, recordSets map[string]*recordSet, delete
}
// ApplyChanges applies a given set of changes in a given zone.
func (p designateProvider) ApplyChanges(changes *plan.Changes) error {
func (p designateProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
managedZones, err := p.getZones()
if err != nil {
return err

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"encoding/pem"
"fmt"
"io/ioutil"
@ -110,7 +111,9 @@ func (c fakeDesignateClient) UpdateRecordSet(zoneID, recordSetID string, opts re
if rs == nil {
return fmt.Errorf("unknown record-set %s", recordSetID)
}
rs.Description = opts.Description
if opts.Description != nil {
rs.Description = *opts.Description
}
rs.TTL = opts.TTL
rs.Records = opts.Records
return nil
@ -405,7 +408,7 @@ func testDesignateCreateRecords(t *testing.T, client *fakeDesignateClient) []*re
expectedCopy := make([]*recordsets.RecordSet, len(expected))
copy(expectedCopy, expected)
err := client.ToProvider().ApplyChanges(&plan.Changes{Create: endpoints})
err := client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{Create: endpoints})
if err != nil {
t.Fatal(err)
}
@ -493,7 +496,7 @@ func testDesignateUpdateRecords(t *testing.T, client *fakeDesignateClient) []*re
expected[2].Records = []string{"10.3.3.1"}
expected[3].Records = []string{"10.2.1.1", "10.3.3.2"}
err := client.ToProvider().ApplyChanges(&plan.Changes{UpdateOld: updatesOld, UpdateNew: updatesNew})
err := client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{UpdateOld: updatesOld, UpdateNew: updatesNew})
if err != nil {
t.Fatal(err)
}
@ -551,7 +554,7 @@ func testDesignateDeleteRecords(t *testing.T, client *fakeDesignateClient) {
expected[3].Records = []string{"10.3.3.2"}
expected = expected[1:]
err := client.ToProvider().ApplyChanges(&plan.Changes{Delete: deletes})
err := client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{Delete: deletes})
if err != nil {
t.Fatal(err)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
goctx "context"
"fmt"
"os"
"strings"
@ -261,7 +262,7 @@ func (p *DigitalOceanProvider) submitChanges(changes []*DigitalOceanChange) erro
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *DigitalOceanProvider) ApplyChanges(changes *plan.Changes) error {
func (p *DigitalOceanProvider) ApplyChanges(ctx goctx.Context, changes *plan.Changes) error {
combinedChanges := make([]*DigitalOceanChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
combinedChanges = append(combinedChanges, newDigitalOceanChanges(DigitalOceanCreate, changes.Create)...)

View File

@ -438,7 +438,7 @@ func TestDigitalOceanApplyChanges(t *testing.T) {
changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.com", Targets: endpoint.Targets{"target"}}}
changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.de", Targets: endpoint.Targets{"target-old"}}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 100}}
err := provider.ApplyChanges(changes)
err := provider.ApplyChanges(context.Background(), changes)
if err != nil {
t.Errorf("should not fail, %s", err)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"os"
"strconv"
@ -176,7 +177,13 @@ func (p *dnsimpleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
default:
continue
}
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(record.Name+"."+record.ZoneID, record.Type, endpoint.TTL(record.TTL), record.Content))
// Apex records have an empty string for their name.
// Consider this when creating the endpoint dnsName
dnsName := fmt.Sprintf("%s.%s", record.Name, record.ZoneID)
if record.Name == "" {
dnsName = record.ZoneID
}
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(dnsName, record.Type, endpoint.TTL(record.TTL), record.Content))
}
page++
if page > records.Pagination.TotalPages {
@ -234,7 +241,12 @@ func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error {
log.Infof("Changing records: %s %v in zone: %s", change.Action, change.ResourceRecordSet, zone.Name)
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, "."+zone.Name)
if change.ResourceRecordSet.Name == zone.Name {
change.ResourceRecordSet.Name = "" // Apex records have an empty name
} else {
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, fmt.Sprintf(".%s", zone.Name))
}
if !p.dryRun {
switch change.Action {
case dnsimpleCreate:
@ -321,7 +333,7 @@ func (p *dnsimpleProvider) UpdateRecords(endpoints []*endpoint.Endpoint) error {
}
// ApplyChanges applies a given set of changes
func (p *dnsimpleProvider) ApplyChanges(changes *plan.Changes) error {
func (p *dnsimpleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
combinedChanges := make([]*dnsimpleChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleCreate, changes.Create)...)

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"os"
"testing"
@ -84,8 +85,18 @@ func TestDnsimpleServices(t *testing.T) {
Priority: 0,
Type: "CNAME",
}
fourthRecord := dnsimple.ZoneRecord{
ID: 4,
ZoneID: "example.com",
ParentID: 0,
Name: "", // Apex domain A record
Content: "127.0.0.1",
TTL: 3600,
Priority: 0,
Type: "A",
}
records := []dnsimple.ZoneRecord{firstRecord, secondRecord, thirdRecord}
records := []dnsimple.ZoneRecord{firstRecord, secondRecord, thirdRecord, fourthRecord}
dnsimpleListRecordsResponse = dnsimple.ZoneRecordsResponse{
Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}},
Data: records,
@ -115,7 +126,6 @@ func TestDnsimpleServices(t *testing.T) {
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("UpdateRecord", "1", record.ZoneID, record.ID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil)
}
mockProvider = dnsimpleProvider{client: mockDNS}
@ -136,7 +146,7 @@ func testDnsimpleProviderZones(t *testing.T) {
validateDnsimpleZones(t, result, dnsimpleListZonesResponse.Data)
mockProvider.accountID = "2"
result, err = mockProvider.Zones()
_, err = mockProvider.Zones()
assert.NotNil(t, err)
}
@ -147,7 +157,7 @@ func testDnsimpleProviderRecords(t *testing.T) {
assert.Equal(t, len(dnsimpleListRecordsResponse.Data), len(result))
mockProvider.accountID = "2"
result, err = mockProvider.Records()
_, err = mockProvider.Records()
assert.NotNil(t, err)
}
func testDnsimpleProviderApplyChanges(t *testing.T) {
@ -157,10 +167,13 @@ func testDnsimpleProviderApplyChanges(t *testing.T) {
{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.UpdateNew = []*endpoint.Endpoint{{DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}}
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},
}
mockProvider.accountID = "1"
err := mockProvider.ApplyChanges(changes)
err := mockProvider.ApplyChanges(context.Background(), changes)
if err != nil {
t.Errorf("Failed to apply changes: %v", err)
}
@ -173,7 +186,7 @@ func testDnsimpleProviderApplyChangesSkipsUnknown(t *testing.T) {
}
mockProvider.accountID = "1"
err := mockProvider.ApplyChanges(changes)
err := mockProvider.ApplyChanges(context.Background(), changes)
if err != nil {
t.Errorf("Failed to ignore unknown zones: %v", err)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"os"
"strconv"
@ -26,6 +27,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/nesv/go-dynect/dynect"
"github.com/sanyu/dynectsoap/dynectsoap"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
@ -34,9 +36,6 @@ import (
const (
// 10 minutes default timeout if not configured using flags
dynDefaultTTL = 600
// can store 20000 entries globally, that's about 4MB of memory
// may be made configurable in the future but 20K records seems like enough for a few zones
cacheMaxSize = 20000
// when rate limit is hit retry up to 5 times after sleep 1m between retries
dynMaxRetriesOnErrRateLimited = 5
@ -51,50 +50,10 @@ const (
restAPIPrefix = "/REST/"
)
// A simple non-thread-safe cache with TTL. The TTL of the records is used here to
// This cache is used to save on requests to DynAPI
type cache struct {
contents map[string]*entry
}
type entry struct {
expires int64
ep *endpoint.Endpoint
}
func (c *cache) Put(link string, ep *endpoint.Endpoint) {
// flush the whole cache on overflow
if len(c.contents) >= cacheMaxSize {
log.Debugf("Flushing cache")
c.contents = make(map[string]*entry)
}
c.contents[link] = &entry{
ep: ep,
expires: unixNow() + int64(ep.RecordTTL),
}
}
func unixNow() int64 {
return int64(time.Now().Unix())
}
func (c *cache) Get(link string) *endpoint.Endpoint {
result, ok := c.contents[link]
if !ok {
return nil
}
now := unixNow()
if result.expires < now {
delete(c.contents, link)
return nil
}
return result.ep
}
// DynConfig hold connection parameters to dyn.com and internal state
type DynConfig struct {
DomainFilter DomainFilter
@ -145,7 +104,6 @@ func (snap *ZoneSnapshot) StoreRecordsForSerial(zone string, serial int, records
// DynProvider is the actual interface impl.
type dynProviderState struct {
DynConfig
Cache *cache
LastLoginErrorTime int64
ZoneSnapshot *ZoneSnapshot
@ -186,9 +144,6 @@ type ZonePublishResponse struct {
func NewDynProvider(config DynConfig) (Provider, error) {
return &dynProviderState{
DynConfig: config,
Cache: &cache{
contents: make(map[string]*entry),
},
ZoneSnapshot: &ZoneSnapshot{
endpoints: map[string][]*endpoint.Endpoint{},
serials: map[string]int{},
@ -277,27 +232,6 @@ func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint {
return result
}
// extractTarget populates the correct field given a record type.
// See dynect.DataBlock comments for details. Empty response means nothing
// was populated - basically an error
func extractTarget(recType string, data *dynect.DataBlock) string {
result := ""
if recType == endpoint.RecordTypeA {
result = data.Address
}
if recType == endpoint.RecordTypeCNAME {
result = data.CName
result = strings.TrimSuffix(result, ".")
}
if recType == endpoint.RecordTypeTXT {
result = data.TxtData
}
return result
}
func apiRetryLoop(f func() error) error {
var err error
for i := 0; i < dynMaxRetriesOnErrRateLimited; i++ {
@ -315,40 +249,48 @@ func apiRetryLoop(f func() error) error {
return err
}
// recordLinkToEndpoint makes an Endpoint given a resource link optinally making a remote call if a cached entry is expired
func (d *dynProviderState) recordLinkToEndpoint(client *dynect.Client, recordLink string) (*endpoint.Endpoint, error) {
result := d.Cache.Get(recordLink)
if result != nil {
log.Infof("Using cached endpoint for %s: %+v", recordLink, result)
return result, nil
func (d *dynProviderState) allRecordsToEndpoints(records *dynectsoap.GetAllRecordsResponseType) []*endpoint.Endpoint {
result := []*endpoint.Endpoint{}
//Convert each record to an endpoint
//Process A Records
for _, rec := range records.Data.A_records {
ep := &endpoint.Endpoint{
DNSName: rec.Fqdn,
RecordTTL: endpoint.TTL(rec.Ttl),
RecordType: rec.Record_type,
Targets: endpoint.Targets{rec.Rdata.Address},
}
log.Debugf("A record: %v", *ep)
result = append(result, ep)
}
rec := dynect.RecordResponse{}
err := apiRetryLoop(func() error {
return client.Do("GET", recordLink, nil, &rec)
})
if err != nil {
return nil, err
//Process CNAME Records
for _, rec := range records.Data.Cname_records {
ep := &endpoint.Endpoint{
DNSName: rec.Fqdn,
RecordTTL: endpoint.TTL(rec.Ttl),
RecordType: rec.Record_type,
Targets: endpoint.Targets{strings.TrimSuffix(rec.Rdata.Cname, ".")},
}
log.Debugf("CNAME record: %v", *ep)
result = append(result, ep)
}
// ignore all records but the types supported by external-
target := extractTarget(rec.Data.RecordType, &rec.Data.RData)
if target == "" {
return nil, nil
//Process TXT Records
for _, rec := range records.Data.Txt_records {
ep := &endpoint.Endpoint{
DNSName: rec.Fqdn,
RecordTTL: endpoint.TTL(rec.Ttl),
RecordType: rec.Record_type,
Targets: endpoint.Targets{rec.Rdata.Txtdata},
}
log.Debugf("TXT record: %v", *ep)
result = append(result, ep)
}
result = &endpoint.Endpoint{
DNSName: rec.Data.FQDN,
RecordTTL: endpoint.TTL(rec.Data.TTL),
RecordType: rec.Data.RecordType,
Targets: endpoint.Targets{target},
}
return result
log.Debugf("Fetched new endpoint for %s: %+v", recordLink, result)
d.Cache.Put(recordLink, result)
return result, nil
}
func errorOrValue(err error, value interface{}) interface{} {
@ -387,6 +329,72 @@ func (d *dynProviderState) fetchZoneSerial(client *dynect.Client, zone string) (
return resp.Data.Serial, nil
}
//Use SOAP to fetch all records with a single call
func (d *dynProviderState) fetchAllRecordsInZone(zone string) (*dynectsoap.GetAllRecordsResponseType, error) {
var err error
client := dynectsoap.NewClient("https://api2.dynect.net/SOAP/")
service := dynectsoap.NewDynect(client)
sessionRequest := dynectsoap.SessionLoginRequestType{
Customer_name: d.CustomerName,
User_name: d.Username,
Password: d.Password,
Fault_incompat: 0,
}
resp := dynectsoap.SessionLoginResponseType{}
err = apiRetryLoop(func() error {
return service.Do(&sessionRequest, &resp)
})
if err != nil {
return nil, err
}
token := resp.Data.Token
logoutRequest := dynectsoap.SessionLogoutRequestType{
Token: token,
Fault_incompat: 0,
}
logoutResponse := dynectsoap.SessionLogoutResponseType{}
defer service.Do(&logoutRequest, &logoutResponse)
req := dynectsoap.GetAllRecordsRequestType{
Token: token,
Zone: zone,
Fault_incompat: 0,
}
records := dynectsoap.GetAllRecordsResponseType{}
err = apiRetryLoop(func() error {
return service.Do(&req, &records)
})
if err != nil {
return nil, err
}
log.Debugf("Got all Records, status is %s", records.Status)
if strings.ToLower(records.Status) == "incomplete" {
jobRequest := dynectsoap.GetJobRequestType{
Token: token,
Job_id: records.Job_id,
Fault_incompat: 0,
}
jobResults := dynectsoap.GetJobResponseType{}
err = apiRetryLoop(func() error {
return service.GetJobRetry(&jobRequest, &jobResults)
})
if err != nil {
return nil, err
}
return jobResults.Data.(*dynectsoap.GetAllRecordsResponseType), nil
}
return &records, nil
}
// fetchAllRecordLinksInZone list all records in a zone with a single call. Records not matched by the
// DomainFilter are ignored. The response is a list of links that can be fed to dynect.Client.Do()
// directly
@ -611,22 +619,14 @@ func (d *dynProviderState) Records() ([]*endpoint.Endpoint, error) {
continue
}
recordLinks, err := d.fetchAllRecordLinksInZone(client, zone)
//Fetch All Records
records, err := d.fetchAllRecordsInZone(zone)
if err != nil {
return nil, err
}
relevantRecords = d.allRecordsToEndpoints(records)
log.Infof("Found %d relevant records found in zone %s: %+v", len(recordLinks), zone, recordLinks)
for _, link := range recordLinks {
ep, err := d.recordLinkToEndpoint(client, link)
if err != nil {
return nil, err
}
if ep != nil {
relevantRecords = append(relevantRecords, ep)
}
}
log.Debugf("Relevant records %+v", relevantRecords)
d.ZoneSnapshot.StoreRecordsForSerial(zone, serial, relevantRecords)
log.Infof("Stored %d records for %s@%d", len(relevantRecords), zone, serial)
@ -638,7 +638,7 @@ func (d *dynProviderState) Records() ([]*endpoint.Endpoint, error) {
// this method does C + 2*Z requests: C=total number of changes, Z = number of
// affected zones (1 login + 1 commit)
func (d *dynProviderState) ApplyChanges(changes *plan.Changes) error {
func (d *dynProviderState) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
log.Debugf("Processing chages: %+v", changes)
if d.DryRun {

View File

@ -20,7 +20,6 @@ import (
"errors"
"fmt"
"testing"
"time"
"github.com/nesv/go-dynect/dynect"
"github.com/stretchr/testify/assert"
@ -169,22 +168,6 @@ func TestDynMerge_NoUpdateIfTTLUnchanged(t *testing.T) {
assert.Equal(t, 0, len(merged))
}
func TestDyn_extractTarget(t *testing.T) {
tests := []struct {
recordType string
block *dynect.DataBlock
target string
}{
{"A", &dynect.DataBlock{Address: "address"}, "address"},
{"CNAME", &dynect.DataBlock{CName: "name."}, "name"}, // note trailing dot is trimmed for CNAMEs
{"TXT", &dynect.DataBlock{TxtData: "text."}, "text."},
}
for _, tc := range tests {
assert.Equal(t, tc.target, extractTarget(tc.recordType, tc.block))
}
}
func TestDyn_endpointToRecord(t *testing.T) {
tests := []struct {
ep *endpoint.Endpoint
@ -264,42 +247,6 @@ func TestDyn_fixMissingTTL(t *testing.T) {
assert.Equal(t, "1992", fixMissingTTL(endpoint.TTL(111), 1992))
}
func TestDyn_cachePut(t *testing.T) {
c := cache{
contents: make(map[string]*entry),
}
c.Put("link", &endpoint.Endpoint{
DNSName: "name",
Targets: endpoint.Targets{"target"},
RecordTTL: endpoint.TTL(10000),
RecordType: "A",
})
found := c.Get("link")
assert.NotNil(t, found)
}
func TestDyn_cachePutExpired(t *testing.T) {
c := cache{
contents: make(map[string]*entry),
}
c.Put("link", &endpoint.Endpoint{
DNSName: "name",
Targets: endpoint.Targets{"target"},
RecordTTL: endpoint.TTL(0),
RecordType: "A",
})
time.Sleep(2 * time.Second)
found := c.Get("link")
assert.Nil(t, found)
assert.Nil(t, c.Get("no-such-records"))
}
func TestDyn_Snapshot(t *testing.T) {
snap := ZoneSnapshot{
serials: map[string]int{},

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"strings"
"github.com/exoscale/egoscale"
@ -81,7 +82,7 @@ func (ep *ExoscaleProvider) getZones() (map[int64]string, error) {
}
// ApplyChanges simply modifies DNS via exoscale API
func (ep *ExoscaleProvider) ApplyChanges(changes *plan.Changes) error {
func (ep *ExoscaleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
ep.OnApplyChanges(changes)
if ep.dryRun {

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"strings"
"testing"
@ -173,7 +174,7 @@ func TestExoscaleApplyChanges(t *testing.T) {
createExoscale = make([]createRecordExoscale, 0)
deleteExoscale = make([]deleteRecordExoscale, 0)
provider.ApplyChanges(plan)
provider.ApplyChanges(context.Background(), plan)
assert.Equal(t, 1, len(createExoscale))
assert.Equal(t, "foo.com", createExoscale[0].name)

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
goctx "context"
"fmt"
"strings"
@ -143,9 +144,9 @@ func NewGoogleProvider(project string, domainFilter DomainFilter, zoneIDFilter Z
provider := &GoogleProvider{
project: project,
dryRun: dryRun,
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets},
managedZonesClient: managedZonesService{dnsClient.ManagedZones},
changesClient: changesService{dnsClient.Changes},
@ -247,7 +248,7 @@ func (p *GoogleProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error {
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *GoogleProvider) ApplyChanges(changes *plan.Changes) error {
func (p *GoogleProvider) ApplyChanges(ctx goctx.Context, changes *plan.Changes) error {
change := &dns.Change{}
change.Additions = append(change.Additions, p.newFilteredRecords(changes.Create)...)

View File

@ -387,7 +387,7 @@ func TestGoogleApplyChanges(t *testing.T) {
Delete: deleteRecords,
}
require.NoError(t, provider.ApplyChanges(changes))
require.NoError(t, provider.ApplyChanges(context.Background(), changes))
records, err := provider.Records()
require.NoError(t, err)
@ -444,7 +444,7 @@ func TestGoogleApplyChangesDryRun(t *testing.T) {
Delete: deleteRecords,
}
require.NoError(t, provider.ApplyChanges(changes))
require.NoError(t, provider.ApplyChanges(context.Background(), changes))
records, err := provider.Records()
require.NoError(t, err)
@ -454,7 +454,7 @@ func TestGoogleApplyChangesDryRun(t *testing.T) {
func TestGoogleApplyChangesEmpty(t *testing.T) {
provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
assert.NoError(t, provider.ApplyChanges(&plan.Changes{}))
assert.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{}))
}
func TestNewFilteredRecords(t *testing.T) {
@ -565,9 +565,9 @@ func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected
func newGoogleProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider {
provider := &GoogleProvider{
project: "zalando-external-dns-test",
dryRun: false,
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: false,
resourceRecordSetsClient: &mockResourceRecordSetsClient{},
managedZonesClient: &mockManagedZonesClient{},
changesClient: &mockChangesClient{},

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"os"
"strconv"
@ -39,6 +40,7 @@ type InfobloxConfig struct {
Version string
SSLVerify bool
DryRun bool
View string
}
// InfobloxProvider implements the DNS provider for Infoblox.
@ -46,6 +48,7 @@ type InfobloxProvider struct {
client ibclient.IBConnector
domainFilter DomainFilter
zoneIDFilter ZoneIDFilter
view string
dryRun bool
}
@ -87,6 +90,7 @@ func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, erro
domainFilter: infobloxConfig.DomainFilter,
zoneIDFilter: infobloxConfig.ZoneIDFilter,
dryRun: infobloxConfig.DryRun,
view: infobloxConfig.View,
}
return provider, nil
@ -105,6 +109,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
objA := ibclient.NewRecordA(
ibclient.RecordA{
Zone: zone.Fqdn,
View: p.view,
},
)
err = p.client.GetObject(objA, "", &resA)
@ -120,6 +125,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
objH := ibclient.NewHostRecord(
ibclient.HostRecord{
Zone: zone.Fqdn,
View: p.view,
},
)
err = p.client.GetObject(objH, "", &resH)
@ -136,6 +142,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
objC := ibclient.NewRecordCNAME(
ibclient.RecordCNAME{
Zone: zone.Fqdn,
View: p.view,
},
)
err = p.client.GetObject(objC, "", &resC)
@ -150,6 +157,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
objT := ibclient.NewRecordTXT(
ibclient.RecordTXT{
Zone: zone.Fqdn,
View: p.view,
},
)
err = p.client.GetObject(objT, "", &resT)
@ -170,7 +178,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
}
// ApplyChanges applies the given changes.
func (p *InfobloxProvider) ApplyChanges(changes *plan.Changes) error {
func (p *InfobloxProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zones, err := p.zones()
if err != nil {
return err
@ -215,7 +223,7 @@ func (p *InfobloxProvider) mapChanges(zones []ibclient.ZoneAuth, changes *plan.C
mapChange := func(changeMap infobloxChangeMap, change *endpoint.Endpoint) {
zone := p.findZone(zones, change.DNSName)
if zone == nil {
logrus.Infof("Ignoring changes to '%s' because a suitable Infoblox DNS zone was not found.", change.DNSName)
logrus.Debugf("Ignoring changes to '%s' because a suitable Infoblox DNS zone was not found.", change.DNSName)
return
}
// Ensure the record type is suitable
@ -261,6 +269,7 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (rec
ibclient.RecordA{
Name: ep.DNSName,
Ipv4Addr: ep.Targets[0],
View: p.view,
},
)
if getObject {
@ -279,6 +288,7 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (rec
ibclient.RecordCNAME{
Name: ep.DNSName,
Canonical: ep.Targets[0],
View: p.view,
},
)
if getObject {
@ -302,6 +312,7 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (rec
ibclient.RecordTXT{
Name: ep.DNSName,
Text: ep.Targets[0],
View: p.view,
},
)
if getObject {

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"encoding/base64"
"fmt"
"regexp"
@ -469,7 +470,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
Delete: deleteRecords,
}
if err := provider.ApplyChanges(changes); err != nil {
if err := provider.ApplyChanges(context.Background(), changes); err != nil {
t.Fatal(err)
}
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"errors"
"strings"
@ -45,7 +46,7 @@ type InMemoryProvider struct {
domain DomainFilter
client *inMemoryClient
filter *filter
OnApplyChanges func(changes *plan.Changes)
OnApplyChanges func(ctx context.Context, changes *plan.Changes)
OnRecords func()
}
@ -55,7 +56,7 @@ type InMemoryOption func(*InMemoryProvider)
// InMemoryWithLogging injects logging when ApplyChanges is called
func InMemoryWithLogging() InMemoryOption {
return func(p *InMemoryProvider) {
p.OnApplyChanges = func(changes *plan.Changes) {
p.OnApplyChanges = func(ctx context.Context, changes *plan.Changes) {
for _, v := range changes.Create {
log.Infof("CREATE: %v", v)
}
@ -94,7 +95,7 @@ func InMemoryInitZones(zones []string) InMemoryOption {
func NewInMemoryProvider(opts ...InMemoryOption) *InMemoryProvider {
im := &InMemoryProvider{
filter: &filter{},
OnApplyChanges: func(changes *plan.Changes) {},
OnApplyChanges: func(ctx context.Context, changes *plan.Changes) {},
OnRecords: func() {},
domain: NewDomainFilter([]string{""}),
client: newInMemoryClient(),
@ -142,8 +143,8 @@ func (im *InMemoryProvider) Records() ([]*endpoint.Endpoint, error) {
// create record - record should not exist
// update/delete record - record should exist
// create/update/delete lists should not have overlapping records
func (im *InMemoryProvider) ApplyChanges(changes *plan.Changes) error {
defer im.OnApplyChanges(changes)
func (im *InMemoryProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
defer im.OnApplyChanges(ctx, changes)
perZoneChanges := map[string]*plan.Changes{}
@ -188,7 +189,7 @@ func (im *InMemoryProvider) ApplyChanges(changes *plan.Changes) error {
UpdateOld: convertToInMemoryRecord(perZoneChanges[zoneID].UpdateOld),
Delete: convertToInMemoryRecord(perZoneChanges[zoneID].Delete),
}
err := im.client.ApplyChanges(zoneID, change)
err := im.client.ApplyChanges(ctx, zoneID, change)
if err != nil {
return err
}
@ -293,7 +294,7 @@ func (c *inMemoryClient) CreateZone(zone string) error {
return nil
}
func (c *inMemoryClient) ApplyChanges(zoneID string, changes *inMemoryChange) error {
func (c *inMemoryClient) ApplyChanges(ctx context.Context, zoneID string, changes *inMemoryChange) error {
if err := c.validateChangeBatch(zoneID, changes); err != nil {
return err
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"testing"
"github.com/kubernetes-incubator/external-dns/endpoint"
@ -773,7 +774,7 @@ func testInMemoryApplyChanges(t *testing.T) {
c.zones = getInitData()
im.client = c
err := im.ApplyChanges(ti.changes)
err := im.ApplyChanges(context.Background(), ti.changes)
if ti.expectError {
assert.Error(t, err)
} else {

View File

@ -263,7 +263,7 @@ func getPriority() *int {
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *LinodeProvider) ApplyChanges(changes *plan.Changes) error {
func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
recordsByZoneID := make(map[string][]*linodego.DomainRecord)
zones, err := p.fetchZones()

View File

@ -131,7 +131,7 @@ func TestLinodeConvertRecordType(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, linodego.RecordTypeSRV, record)
record, err = convertRecordType("INVALID")
_, err = convertRecordType("INVALID")
require.Error(t, err)
}
@ -353,7 +353,7 @@ func TestLinodeApplyChanges(t *testing.T) {
},
).Return(&linodego.DomainRecord{}, nil).Once()
err := provider.ApplyChanges(&plan.Changes{
err := provider.ApplyChanges(context.Background(), &plan.Changes{
Create: []*endpoint.Endpoint{{
DNSName: "create.bar.io",
RecordType: "A",
@ -428,7 +428,7 @@ func TestLinodeApplyChangesTargetAdded(t *testing.T) {
},
).Return(&linodego.DomainRecord{}, nil).Once()
err := provider.ApplyChanges(&plan.Changes{
err := provider.ApplyChanges(context.Background(), &plan.Changes{
// From 1 target to 2
UpdateNew: []*endpoint.Endpoint{{
DNSName: "example.com",
@ -484,7 +484,7 @@ func TestLinodeApplyChangesTargetRemoved(t *testing.T) {
11,
).Return(nil).Once()
err := provider.ApplyChanges(&plan.Changes{
err := provider.ApplyChanges(context.Background(), &plan.Changes{
// From 2 targets to 1
UpdateNew: []*endpoint.Endpoint{{
DNSName: "example.com",
@ -521,7 +521,7 @@ func TestLinodeApplyChangesNoChanges(t *testing.T) {
mock.Anything,
).Return([]*linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}}, nil).Once()
err := provider.ApplyChanges(&plan.Changes{})
err := provider.ApplyChanges(context.Background(), &plan.Changes{})
require.NoError(t, err)
mockDomainClient.AssertExpectations(t)

319
provider/ns1.go Normal file
View File

@ -0,0 +1,319 @@
/*
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"
"crypto/tls"
"fmt"
"net/http"
"os"
"strings"
log "github.com/sirupsen/logrus"
api "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
const (
// ns1Create is a ChangeAction enum value
ns1Create = "CREATE"
// ns1Delete is a ChangeAction enum value
ns1Delete = "DELETE"
// ns1Update is a ChangeAction enum value
ns1Update = "UPDATE"
// ns1DefaultTTL is the default ttl for ttls that are not set
ns1DefaultTTL = 10
)
// NS1DomainClient is a subset of the NS1 API the the provider uses, to ease testing
type NS1DomainClient interface {
CreateRecord(r *dns.Record) (*http.Response, error)
DeleteRecord(zone string, domain string, t string) (*http.Response, error)
UpdateRecord(r *dns.Record) (*http.Response, error)
GetZone(zone string) (*dns.Zone, *http.Response, error)
ListZones() ([]*dns.Zone, *http.Response, error)
}
// NS1DomainService wraps the API and fulfills the NS1DomainClient interface
type NS1DomainService struct {
service *api.Client
}
// CreateRecord wraps the Create method of the API's Record service
func (n NS1DomainService) CreateRecord(r *dns.Record) (*http.Response, error) {
return n.service.Records.Create(r)
}
// DeleteRecord wraps the Delete method of the API's Record service
func (n NS1DomainService) DeleteRecord(zone string, domain string, t string) (*http.Response, error) {
return n.service.Records.Delete(zone, domain, t)
}
// UpdateRecord wraps the Update method of the API's Record service
func (n NS1DomainService) UpdateRecord(r *dns.Record) (*http.Response, error) {
return n.service.Records.Update(r)
}
// GetZone wraps the Get method of the API's Zones service
func (n NS1DomainService) GetZone(zone string) (*dns.Zone, *http.Response, error) {
return n.service.Zones.Get(zone)
}
// ListZones wraps the List method of the API's Zones service
func (n NS1DomainService) ListZones() ([]*dns.Zone, *http.Response, error) {
return n.service.Zones.List()
}
// NS1Config passes cli args to the NS1Provider
type NS1Config struct {
DomainFilter DomainFilter
ZoneIDFilter ZoneIDFilter
NS1Endpoint string
NS1IgnoreSSL bool
DryRun bool
}
// NS1Provider is the NS1 provider
type NS1Provider struct {
client NS1DomainClient
domainFilter DomainFilter
zoneIDFilter ZoneIDFilter
dryRun bool
}
// NewNS1Provider creates a new NS1 Provider
func NewNS1Provider(config NS1Config) (*NS1Provider, error) {
return newNS1ProviderWithHTTPClient(config, http.DefaultClient)
}
func newNS1ProviderWithHTTPClient(config NS1Config, client *http.Client) (*NS1Provider, error) {
token, ok := os.LookupEnv("NS1_APIKEY")
if !ok {
return nil, fmt.Errorf("NS1_APIKEY environment variable is not set")
}
clientArgs := []func(*api.Client){api.SetAPIKey(token)}
if config.NS1Endpoint != "" {
log.Infof("ns1-endpoint flag is set, targeting endpoint at %s", config.NS1Endpoint)
clientArgs = append(clientArgs, api.SetEndpoint(config.NS1Endpoint))
}
if config.NS1IgnoreSSL == true {
log.Info("ns1-ignoressl flag is True, skipping SSL verification")
defaultTransport := http.DefaultTransport.(*http.Transport)
tr := &http.Transport{
Proxy: defaultTransport.Proxy,
DialContext: defaultTransport.DialContext,
MaxIdleConns: defaultTransport.MaxIdleConns,
IdleConnTimeout: defaultTransport.IdleConnTimeout,
ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client.Transport = tr
}
apiClient := api.NewClient(client, clientArgs...)
provider := &NS1Provider{
client: NS1DomainService{apiClient},
domainFilter: config.DomainFilter,
zoneIDFilter: config.ZoneIDFilter,
}
return provider, nil
}
// Records returns the endpoints this provider knows about
func (p *NS1Provider) Records() ([]*endpoint.Endpoint, error) {
zones, err := p.zonesFiltered()
if err != nil {
return nil, err
}
var endpoints []*endpoint.Endpoint
for _, zone := range zones {
// TODO handle Header Codes
zoneData, _, err := p.client.GetZone(zone.String())
if err != nil {
return nil, err
}
for _, record := range zoneData.Records {
if supportedRecordType(record.Type) {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(
record.Domain,
record.Type,
endpoint.TTL(record.TTL),
record.ShortAns...,
),
)
}
}
}
return endpoints, nil
}
// ns1BuildRecord returns a dns.Record for a change set
func ns1BuildRecord(zoneName string, change *ns1Change) *dns.Record {
record := dns.NewRecord(zoneName, change.Endpoint.DNSName, change.Endpoint.RecordType)
for _, v := range change.Endpoint.Targets {
record.AddAnswer(dns.NewAnswer(strings.Split(v, " ")))
}
// set detault ttl
var ttl = ns1DefaultTTL
if change.Endpoint.RecordTTL.IsConfigured() {
ttl = int(change.Endpoint.RecordTTL)
}
record.TTL = ttl
return record
}
// ns1SubmitChanges takes an array of changes and sends them to NS1
func (p *NS1Provider) ns1SubmitChanges(changes []*ns1Change) error {
// return early if there is nothing to change
if len(changes) == 0 {
return nil
}
zones, err := p.zonesFiltered()
if err != nil {
return err
}
// separate into per-zone change sets to be passed to the API.
changesByZone := ns1ChangesByZone(zones, changes)
for zoneName, changes := range changesByZone {
for _, change := range changes {
record := ns1BuildRecord(zoneName, change)
logFields := log.Fields{
"record": record.Domain,
"type": record.Type,
"ttl": record.TTL,
"action": change.Action,
"zone": zoneName,
}
log.WithFields(logFields).Info("Changing record.")
if p.dryRun {
continue
}
switch change.Action {
case ns1Create:
_, err := p.client.CreateRecord(record)
if err != nil {
return err
}
case ns1Delete:
_, err := p.client.DeleteRecord(zoneName, record.Domain, record.Type)
if err != nil {
return err
}
case ns1Update:
_, err := p.client.UpdateRecord(record)
if err != nil {
return err
}
}
}
}
return nil
}
// Zones returns the list of hosted zones.
func (p *NS1Provider) zonesFiltered() ([]*dns.Zone, error) {
// TODO handle Header Codes
zones, _, err := p.client.ListZones()
if err != nil {
return nil, err
}
toReturn := []*dns.Zone{}
for _, z := range zones {
if p.domainFilter.Match(z.Zone) && p.zoneIDFilter.Match(z.ID) {
toReturn = append(toReturn, z)
log.Debugf("Matched %s", z.Zone)
} else {
log.Debugf("Filtered %s", z.Zone)
}
}
return toReturn, nil
}
// ns1Change differentiates between ChangeActions
type ns1Change struct {
Action string
Endpoint *endpoint.Endpoint
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *NS1Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
combinedChanges := make([]*ns1Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
combinedChanges = append(combinedChanges, newNS1Changes(ns1Create, changes.Create)...)
combinedChanges = append(combinedChanges, newNS1Changes(ns1Update, changes.UpdateNew)...)
combinedChanges = append(combinedChanges, newNS1Changes(ns1Delete, changes.Delete)...)
return p.ns1SubmitChanges(combinedChanges)
}
// newNS1Changes returns a collection of Changes based on the given records and action.
func newNS1Changes(action string, endpoints []*endpoint.Endpoint) []*ns1Change {
changes := make([]*ns1Change, 0, len(endpoints))
for _, endpoint := range endpoints {
changes = append(changes, &ns1Change{
Action: action,
Endpoint: endpoint,
},
)
}
return changes
}
// 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{}
for _, z := range zones {
zoneNameIDMapper.Add(z.Zone, z.Zone)
changes[z.Zone] = []*ns1Change{}
}
for _, c := range changeSets {
zone, _ := zoneNameIDMapper.FindZone(c.Endpoint.DNSName)
if zone == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", c.Endpoint.DNSName)
continue
}
changes[zone] = append(changes[zone], c)
}
return changes
}

307
provider/ns1_test.go Normal file
View File

@ -0,0 +1,307 @@
/*
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"
"net/http"
"os"
"testing"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
api "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
)
type MockNS1DomainClient struct {
mock.Mock
}
func (m *MockNS1DomainClient) CreateRecord(r *dns.Record) (*http.Response, error) {
return nil, nil
}
func (m *MockNS1DomainClient) DeleteRecord(zone string, domain string, t string) (*http.Response, error) {
return nil, nil
}
func (m *MockNS1DomainClient) UpdateRecord(r *dns.Record) (*http.Response, error) {
return nil, nil
}
func (m *MockNS1DomainClient) GetZone(zone string) (*dns.Zone, *http.Response, error) {
r := &dns.ZoneRecord{
Domain: "test.foo.com",
ShortAns: []string{"2.2.2.2"},
TTL: 3600,
Type: "A",
ID: "123456789abcdefghijklmno",
}
z := &dns.Zone{
Zone: "foo.com",
Records: []*dns.ZoneRecord{r},
TTL: 3600,
ID: "12345678910111213141516a",
}
if zone == "foo.com" {
return z, nil, nil
}
return nil, nil, nil
}
func (m *MockNS1DomainClient) ListZones() ([]*dns.Zone, *http.Response, error) {
zones := []*dns.Zone{
{Zone: "foo.com", ID: "12345678910111213141516a"},
{Zone: "bar.com", ID: "12345678910111213141516b"},
}
return zones, nil, nil
}
type MockNS1GetZoneFail struct{}
func (m *MockNS1GetZoneFail) CreateRecord(r *dns.Record) (*http.Response, error) {
return nil, nil
}
func (m *MockNS1GetZoneFail) DeleteRecord(zone string, domain string, t string) (*http.Response, error) {
return nil, nil
}
func (m *MockNS1GetZoneFail) UpdateRecord(r *dns.Record) (*http.Response, error) {
return nil, nil
}
func (m *MockNS1GetZoneFail) GetZone(zone string) (*dns.Zone, *http.Response, error) {
return nil, nil, api.ErrZoneMissing
}
func (m *MockNS1GetZoneFail) ListZones() ([]*dns.Zone, *http.Response, error) {
zones := []*dns.Zone{
{Zone: "foo.com", ID: "12345678910111213141516a"},
{Zone: "bar.com", ID: "12345678910111213141516b"},
}
return zones, nil, nil
}
type MockNS1ListZonesFail struct{}
func (m *MockNS1ListZonesFail) CreateRecord(r *dns.Record) (*http.Response, error) {
return nil, nil
}
func (m *MockNS1ListZonesFail) DeleteRecord(zone string, domain string, t string) (*http.Response, error) {
return nil, nil
}
func (m *MockNS1ListZonesFail) UpdateRecord(r *dns.Record) (*http.Response, error) {
return nil, nil
}
func (m *MockNS1ListZonesFail) GetZone(zone string) (*dns.Zone, *http.Response, error) {
return &dns.Zone{}, nil, nil
}
func (m *MockNS1ListZonesFail) ListZones() ([]*dns.Zone, *http.Response, error) {
return nil, nil, fmt.Errorf("no zones available")
}
func TestNS1Records(t *testing.T) {
provider := &NS1Provider{
client: &MockNS1DomainClient{},
domainFilter: NewDomainFilter([]string{"foo.com."}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
}
records, err := provider.Records()
require.NoError(t, err)
assert.Equal(t, 1, len(records))
provider.client = &MockNS1GetZoneFail{}
_, err = provider.Records()
require.Error(t, err)
provider.client = &MockNS1ListZonesFail{}
_, err = provider.Records()
require.Error(t, err)
}
func TestNewNS1Provider(t *testing.T) {
_ = os.Setenv("NS1_APIKEY", "xxxxxxxxxxxxxxxxx")
testNS1Config := NS1Config{
DomainFilter: NewDomainFilter([]string{"foo.com."}),
ZoneIDFilter: NewZoneIDFilter([]string{""}),
DryRun: false,
}
_, err := NewNS1Provider(testNS1Config)
require.NoError(t, err)
_ = os.Unsetenv("NS1_APIKEY")
_, err = NewNS1Provider(testNS1Config)
require.Error(t, err)
}
func TestNS1Zones(t *testing.T) {
provider := &NS1Provider{
client: &MockNS1DomainClient{},
domainFilter: NewDomainFilter([]string{"foo.com."}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
}
zones, err := provider.zonesFiltered()
require.NoError(t, err)
validateNS1Zones(t, zones, []*dns.Zone{
{Zone: "foo.com"},
})
}
func validateNS1Zones(t *testing.T, zones []*dns.Zone, expected []*dns.Zone) {
require.Len(t, zones, len(expected))
for i, zone := range zones {
assert.Equal(t, expected[i].Zone, zone.Zone)
}
}
func TestNS1BuildRecord(t *testing.T) {
change := &ns1Change{
Action: ns1Create,
Endpoint: &endpoint.Endpoint{
DNSName: "new",
Targets: endpoint.Targets{"target"},
RecordType: "A",
},
}
record := ns1BuildRecord("foo.com", change)
assert.Equal(t, "foo.com", record.Zone)
assert.Equal(t, "new.foo.com", record.Domain)
assert.Equal(t, ns1DefaultTTL, record.TTL)
changeWithTTL := &ns1Change{
Action: ns1Create,
Endpoint: &endpoint.Endpoint{
DNSName: "new-b",
Targets: endpoint.Targets{"target"},
RecordType: "A",
RecordTTL: 100,
},
}
record = ns1BuildRecord("foo.com", changeWithTTL)
assert.Equal(t, "foo.com", record.Zone)
assert.Equal(t, "new-b.foo.com", record.Domain)
assert.Equal(t, 100, record.TTL)
}
func TestNS1ApplyChanges(t *testing.T) {
changes := &plan.Changes{}
provider := &NS1Provider{
client: &MockNS1DomainClient{},
}
changes.Create = []*endpoint.Endpoint{
{DNSName: "new.foo.com", Targets: endpoint.Targets{"target"}},
{DNSName: "new.subdomain.bar.com", Targets: endpoint.Targets{"target"}},
}
changes.Delete = []*endpoint.Endpoint{{DNSName: "test.foo.com", Targets: endpoint.Targets{"target"}}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.foo.com", Targets: endpoint.Targets{"target-new"}}}
err := provider.ApplyChanges(context.Background(), changes)
require.NoError(t, err)
// empty changes
changes.Create = []*endpoint.Endpoint{}
changes.Delete = []*endpoint.Endpoint{}
changes.UpdateNew = []*endpoint.Endpoint{}
err = provider.ApplyChanges(context.Background(), changes)
require.NoError(t, err)
}
func TestNewNS1Changes(t *testing.T) {
endpoints := []*endpoint.Endpoint{
{
DNSName: "testa.foo.com",
Targets: endpoint.Targets{"target-old"},
RecordType: "A",
},
{
DNSName: "testba.bar.com",
Targets: endpoint.Targets{"target-new"},
RecordType: "A",
},
}
expected := []*ns1Change{
{
Action: "ns1Create",
Endpoint: endpoints[0],
},
{
Action: "ns1Create",
Endpoint: endpoints[1],
},
}
changes := newNS1Changes("ns1Create", endpoints)
require.Len(t, changes, len(expected))
assert.Equal(t, expected, changes)
}
func TestNewNS1ChangesByZone(t *testing.T) {
provider := &NS1Provider{
client: &MockNS1DomainClient{},
}
zones, _ := provider.zonesFiltered()
changeSets := []*ns1Change{
{
Action: "ns1Create",
Endpoint: &endpoint.Endpoint{
DNSName: "new.foo.com",
Targets: endpoint.Targets{"target"},
RecordType: "A",
},
},
{
Action: "ns1Create",
Endpoint: &endpoint.Endpoint{
DNSName: "unrelated.bar.com",
Targets: endpoint.Targets{"target"},
RecordType: "A",
},
},
{
Action: "ns1Delete",
Endpoint: &endpoint.Endpoint{
DNSName: "test.foo.com",
Targets: endpoint.Targets{"target"},
RecordType: "A",
},
},
{
Action: "ns1Update",
Endpoint: &endpoint.Endpoint{
DNSName: "test.foo.com",
Targets: endpoint.Targets{"target-new"},
RecordType: "A",
},
},
}
changes := ns1ChangesByZone(zones, changeSets)
assert.Len(t, changes["bar.com"], 1)
assert.Len(t, changes["foo.com"], 3)
}

View File

@ -201,7 +201,7 @@ func (p *OCIProvider) Records() ([]*endpoint.Endpoint, error) {
}
// ApplyChanges applies a given set of changes to a given zone.
func (p *OCIProvider) ApplyChanges(changes *plan.Changes) error {
func (p *OCIProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
log.Debugf("Processing chages: %+v", changes)
ops := []dns.RecordOperation{}
@ -217,7 +217,6 @@ func (p *OCIProvider) ApplyChanges(changes *plan.Changes) error {
return nil
}
ctx := context.Background()
zones, err := p.zones(ctx)
if err != nil {
return errors.Wrap(err, "fetching zones")

View File

@ -829,7 +829,7 @@ func TestOCIApplyChanges(t *testing.T) {
NewZoneIDFilter([]string{""}),
tc.dryRun,
)
err := provider.ApplyChanges(tc.changes)
err := provider.ApplyChanges(context.Background(), tc.changes)
require.Equal(t, tc.err, err)
endpoints, err := provider.Records()
require.NoError(t, err)

View File

@ -30,11 +30,12 @@ import (
log "github.com/sirupsen/logrus"
"crypto/tls"
"net"
pgo "github.com/ffledgling/pdns-go"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/pkg/tlsutils"
"github.com/kubernetes-incubator/external-dns/plan"
"net"
)
type pdnsChangeType string
@ -175,7 +176,7 @@ func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zo
}
}
} else {
residualZones = zones
filteredZones = zones
}
return filteredZones, residualZones
}
@ -442,7 +443,7 @@ func (p *PDNSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
// 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(changes *plan.Changes) error {
func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
startTime := time.Now()

View File

@ -17,7 +17,9 @@ limitations under the License.
package provider
import (
"context"
"errors"
//"fmt"
"net/http"
"strings"
@ -475,6 +477,44 @@ var (
},
},
}
DomainFilterListSingle = DomainFilter{
filters: []string{
"example.com",
},
}
DomainFilterListMultiple = DomainFilter{
filters: []string{
"example.com",
"mock.com",
},
}
DomainFilterListEmpty = DomainFilter{
filters: []string{},
}
DomainFilterEmptyClient = &PDNSAPIClient{
dryRun: false,
authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: "TEST-API-KEY"}),
client: pgo.NewAPIClient(pgo.NewConfiguration()),
domainFilter: DomainFilterListEmpty,
}
DomainFilterSingleClient = &PDNSAPIClient{
dryRun: false,
authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: "TEST-API-KEY"}),
client: pgo.NewAPIClient(pgo.NewConfiguration()),
domainFilter: DomainFilterListSingle,
}
DomainFilterMultipleClient = &PDNSAPIClient{
dryRun: false,
authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: "TEST-API-KEY"}),
client: pgo.NewAPIClient(pgo.NewConfiguration()),
domainFilter: DomainFilterListMultiple,
}
)
/******************************************************************************/
@ -766,13 +806,13 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSRecords() {
p = &PDNSProvider{
client: &PDNSAPIClientStubListZoneFailure{},
}
eps, err = p.Records()
_, err = p.Records()
assert.NotNil(suite.T(), err)
p = &PDNSProvider{
client: &PDNSAPIClientStubListZonesFailure{},
}
eps, err = p.Records()
_, err = p.Records()
assert.NotNil(suite.T(), err)
}
@ -912,6 +952,51 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSmutateRecords() {
}
func (suite *NewPDNSProviderTestSuite) TestPDNSClientPartitionZones() {
zoneList := []pgo.Zone{
ZoneEmpty,
ZoneEmpty2,
}
partitionResultFilteredEmptyFilter := []pgo.Zone{
ZoneEmpty,
ZoneEmpty2,
}
partitionResultResidualEmptyFilter := ([]pgo.Zone)(nil)
partitionResultFilteredSingleFilter := []pgo.Zone{
ZoneEmpty,
}
partitionResultResidualSingleFilter := []pgo.Zone{
ZoneEmpty2,
}
partitionResultFilteredMultipleFilter := []pgo.Zone{
ZoneEmpty,
}
partitionResultResidualMultipleFilter := []pgo.Zone{
ZoneEmpty2,
}
// Check filtered, residual zones when no domain filter specified
filteredZones, residualZones := DomainFilterEmptyClient.PartitionZones(zoneList)
assert.Equal(suite.T(), partitionResultFilteredEmptyFilter, filteredZones)
assert.Equal(suite.T(), partitionResultResidualEmptyFilter, residualZones)
// Check filtered, residual zones when a single domain filter specified
filteredZones, residualZones = DomainFilterSingleClient.PartitionZones(zoneList)
assert.Equal(suite.T(), partitionResultFilteredSingleFilter, filteredZones)
assert.Equal(suite.T(), partitionResultResidualSingleFilter, residualZones)
// Check filtered, residual zones when a multiple domain filter specified
filteredZones, residualZones = DomainFilterMultipleClient.PartitionZones(zoneList)
assert.Equal(suite.T(), partitionResultFilteredMultipleFilter, filteredZones)
assert.Equal(suite.T(), partitionResultResidualMultipleFilter, residualZones)
}
func TestNewPDNSProviderTestSuite(t *testing.T) {
suite.Run(t, new(NewPDNSProviderTestSuite))
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"net"
"strings"
@ -27,9 +28,20 @@ import (
// Provider defines the interface DNS providers should implement.
type Provider interface {
Records() ([]*endpoint.Endpoint, error)
ApplyChanges(changes *plan.Changes) error
ApplyChanges(ctx context.Context, changes *plan.Changes) error
}
type contextKey struct {
name string
}
func (k *contextKey) String() string { return "provider context value " + k.name }
// RecordsContextKey is a context key. It can be used during ApplyChanges
// to access previously cached records. The associated value will be of
// 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 {
if net.ParseIP(hostname) != nil {

338
provider/rcode0.go Normal file
View File

@ -0,0 +1,338 @@
/*
Copyright 2019 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"
"net/url"
"os"
"strings"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
rc0 "github.com/nic-at/rc0go"
log "github.com/sirupsen/logrus"
)
// RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS.
type RcodeZeroProvider struct {
Client *rc0.Client
DomainFilter DomainFilter
DryRun bool
TXTEncrypt bool
Key []byte
}
// NewRcodeZeroProvider creates a new RcodeZero Anycast DNS provider.
//
// Returns the provider or an error if a provider could not be created.
func NewRcodeZeroProvider(domainFilter DomainFilter, dryRun bool, txtEnc bool) (*RcodeZeroProvider, error) {
client, err := rc0.NewClient(os.Getenv("RC0_API_KEY"))
if err != nil {
return nil, err
}
value := os.Getenv("RC0_BASE_URL")
if len(value) != 0 {
client.BaseURL, err = url.Parse(os.Getenv("RC0_BASE_URL"))
}
if err != nil {
return nil, fmt.Errorf("failed to initialize rcodezero provider: %v", err)
}
provider := &RcodeZeroProvider{
Client: client,
DomainFilter: domainFilter,
DryRun: dryRun,
TXTEncrypt: txtEnc,
}
if txtEnc {
provider.Key = []byte(os.Getenv("RC0_ENC_KEY"))
}
return provider, nil
}
// Zones returns filtered zones if filter is set
func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) {
var result []*rc0.Zone
zones, err := p.fetchZones()
if err != nil {
return nil, err
}
for _, zone := range zones {
if p.DomainFilter.Match(zone.Domain) {
result = append(result, zone)
}
}
return result, nil
}
// Records returns resource records
//
// Decrypts TXT records if TXT-Encrypt flag is set and key is provided
func (p *RcodeZeroProvider) Records() ([]*endpoint.Endpoint, error) {
zones, err := p.Zones()
if err != nil {
return nil, err
}
var endpoints []*endpoint.Endpoint
for _, zone := range zones {
rrset, err := p.fetchRecords(zone.Domain)
if err != nil {
return nil, err
}
for _, r := range rrset {
if 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))
}
}
}
}
}
return endpoints, nil
}
// 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)...)
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeUPDATE, changes.UpdateNew)...)
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeDELETE, changes.Delete)...)
return p.submitChanges(combinedChanges)
}
// Helper function
func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map[string][]*rc0.RRSetChange {
changes := make(map[string][]*rc0.RRSetChange)
zoneNameIDMapper := zoneIDName{}
for _, z := range zones {
zoneNameIDMapper.Add(z.Domain, z.Domain)
changes[z.Domain] = []*rc0.RRSetChange{}
}
for _, c := range changeSet {
zone, _ := zoneNameIDMapper.FindZone(c.Name)
if zone == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", c.Name)
continue
}
changes[zone] = append(changes[zone], c)
}
return changes
}
// Helper function
func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) {
var allRecords []*rc0.RRType
listOptions := rc0.NewListOptions()
for {
records, page, err := p.Client.RRSet.List(zoneName, listOptions)
if err != nil {
return nil, err
}
allRecords = append(allRecords, records...)
if page == nil || (page.CurrentPage == page.LastPage) {
break
}
listOptions.SetPageNumber(page.CurrentPage + 1)
}
return allRecords, nil
}
// Helper function
func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) {
var allZones []*rc0.Zone
listOptions := rc0.NewListOptions()
for {
zones, page, err := p.Client.Zones.List(listOptions)
if err != nil {
return nil, err
}
allZones = append(allZones, zones...)
if page == nil || page.IsLastPage() {
break
}
listOptions.SetPageNumber(page.CurrentPage + 1)
}
return allZones, nil
}
// Helper function to submit changes.
//
// Changes are submitted by change type.
func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error {
if len(changes) == 0 {
return nil
}
zones, err := p.Zones()
if err != nil {
return err
}
// 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,
"type": change.Type,
"action": change.ChangeType,
"zone": zoneName,
}
log.WithFields(logFields).Info("Changing record.")
if p.DryRun {
continue
}
// to avoid accidentally adding extra dot if already present
change.Name = strings.TrimSuffix(change.Name, ".") + "."
switch change.ChangeType {
case rc0.ChangeTypeADD:
sr, err := p.Client.RRSet.Create(zoneName, []*rc0.RRSetChange{change})
if err != nil {
return err
}
if sr.HasError() {
return fmt.Errorf("adding new RR resulted in an error: %v", sr.Message)
}
case rc0.ChangeTypeUPDATE:
sr, err := p.Client.RRSet.Edit(zoneName, []*rc0.RRSetChange{change})
if err != nil {
return err
}
if sr.HasError() {
return fmt.Errorf("updating existing RR resulted in an error: %v", sr.Message)
}
case rc0.ChangeTypeDELETE:
sr, err := p.Client.RRSet.Delete(zoneName, []*rc0.RRSetChange{change})
if err != nil {
return err
}
if sr.HasError() {
return fmt.Errorf("deleting existing RR resulted in an error: %v", sr.Message)
}
default:
return fmt.Errorf("unsupported changeType submitted: %v", change.ChangeType)
}
}
}
return nil
}
// 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 {
changes = append(changes, p.NewRcodezeroChange(action, _endpoint))
}
return changes
}
// 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,
Name: endpoint.DNSName,
Records: []*rc0.Record{{
Disabled: false,
Content: endpoint.Targets[0],
}},
}
if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(endpoint.RecordType, "TXT") {
p.Client.RRSet.EncryptTXT(p.Key, change)
}
return change
}

422
provider/rcode0_test.go Normal file
View File

@ -0,0 +1,422 @@
/*
Copyright 2019 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"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
rc0 "github.com/nic-at/rc0go"
"github.com/stretchr/testify/require"
)
const (
testZoneOne = "testzone1.at"
testZoneTwo = "testzone2.at"
rrsetChangesUnsupportedChangeType = 0
)
type mockRcodeZeroClient rc0.Client
type mockZoneManagementService struct {
TestNilZonesReturned bool
TestErrorReturned bool
}
type mockRRSetService struct {
TestErrorReturned bool
}
func (m *mockRcodeZeroClient) resetMockServices() {
m.Zones = &mockZoneManagementService{}
m.RRSet = &mockRRSetService{}
}
func (m *mockZoneManagementService) resetTestConditions() {
m.TestNilZonesReturned = false
m.TestErrorReturned = false
}
func (m *mockRRSetService) resetTestConditions() {
m.TestErrorReturned = false
}
func TestRcodeZeroProvider_Records(t *testing.T) {
mockRRSetService := &mockRRSetService{}
mockZoneManagementService := &mockZoneManagementService{}
provider := &RcodeZeroProvider{
Client: (*rc0.Client)(&mockRcodeZeroClient{
Zones: mockZoneManagementService,
RRSet: mockRRSetService,
}),
}
endpoints, err := provider.Records() // should return 6 rrs
if err != nil {
t.Errorf("should not fail, %s", err)
}
require.Equal(t, 6, len(endpoints))
mockRRSetService.TestErrorReturned = true
_, err = provider.Records()
if err == nil {
t.Errorf("expected to fail, %s", err)
}
}
func TestRcodeZeroProvider_ApplyChanges(t *testing.T) {
mockRRSetService := &mockRRSetService{}
mockZoneManagementService := &mockZoneManagementService{}
provider := &RcodeZeroProvider{
Client: (*rc0.Client)(&mockRcodeZeroClient{
Zones: mockZoneManagementService,
RRSet: mockRRSetService,
}),
DomainFilter: NewDomainFilter([]string{testZoneOne}),
}
changes := mockChanges()
err := provider.ApplyChanges(context.Background(), changes)
if err != nil {
t.Errorf("should not fail, %s", err)
}
}
func TestRcodeZeroProvider_NewRcodezeroChanges(t *testing.T) {
provider := &RcodeZeroProvider{}
changes := mockChanges()
createChanges := provider.NewRcodezeroChanges(testZoneOne, changes.Create)
require.Equal(t, 4, len(createChanges))
deleteChanges := provider.NewRcodezeroChanges(testZoneOne, changes.Delete)
require.Equal(t, 1, len(deleteChanges))
updateOldChanges := provider.NewRcodezeroChanges(testZoneOne, changes.UpdateOld)
require.Equal(t, 1, len(updateOldChanges))
updateNewChanges := provider.NewRcodezeroChanges(testZoneOne, changes.UpdateNew)
require.Equal(t, 1, len(updateNewChanges))
}
func TestRcodeZeroProvider_NewRcodezeroChange(t *testing.T) {
_endpoint := &endpoint.Endpoint{
RecordType: "A",
DNSName: "app." + testZoneOne,
RecordTTL: 300,
Targets: endpoint.Targets{"target"},
}
provider := &RcodeZeroProvider{}
rrsetChange := provider.NewRcodezeroChange(testZoneOne, _endpoint)
require.Equal(t, _endpoint.RecordType, rrsetChange.Type)
require.Equal(t, _endpoint.DNSName, rrsetChange.Name)
require.Equal(t, _endpoint.Targets[0], rrsetChange.Records[0].Content)
//require.Equal(t, endpoint.RecordTTL, rrsetChange.TTL)
}
func Test_submitChanges(t *testing.T) {
mockRRSetService := &mockRRSetService{}
mockZoneManagementService := &mockZoneManagementService{}
provider := &RcodeZeroProvider{
Client: (*rc0.Client)(&mockRcodeZeroClient{
Zones: mockZoneManagementService,
RRSet: mockRRSetService,
}),
DomainFilter: NewDomainFilter([]string{testZoneOne}),
}
changes := mockRRSetChanges(rrsetChangesUnsupportedChangeType)
err := provider.submitChanges(changes)
if err == nil {
t.Errorf("expected to fail, %s", err)
}
}
func mockRRSetChanges(condition int) []*rc0.RRSetChange {
switch condition {
case rrsetChangesUnsupportedChangeType:
return []*rc0.RRSetChange{
{
Name: testZoneOne,
Type: "A",
ChangeType: "UNSUPPORTED",
Records: []*rc0.Record{{Content: "fail"}},
},
}
default:
return nil
}
}
func mockChanges() *plan.Changes {
changes := &plan.Changes{}
changes.Create = []*endpoint.Endpoint{
{DNSName: "new.ext-dns-test." + testZoneOne, Targets: endpoint.Targets{"target"}, RecordType: "A"},
{DNSName: "new.ext-dns-test-with-ttl." + testZoneOne, Targets: endpoint.Targets{"target"}, RecordType: "A", RecordTTL: 100},
{DNSName: "new.ext-dns-test.unexpected.com", Targets: endpoint.Targets{"target"}, RecordType: "AAAA"},
{DNSName: testZoneOne, Targets: endpoint.Targets{"target"}, RecordType: "CNAME"},
}
changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test." + testZoneOne, Targets: endpoint.Targets{"target"}}}
changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test." + testZoneOne, Targets: endpoint.Targets{"target-old"}}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test." + testZoneOne, Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 100}}
return changes
}
func TestRcodeZeroProvider_Zones(t *testing.T) {
mockRRSetService := &mockRRSetService{}
mockZoneManagementService := &mockZoneManagementService{}
provider := &RcodeZeroProvider{
Client: (*rc0.Client)(&mockRcodeZeroClient{
Zones: mockZoneManagementService,
RRSet: mockRRSetService,
}),
}
mockZoneManagementService.TestNilZonesReturned = true
zones, err := provider.Zones()
if err != nil {
t.Fatal(err)
}
require.Equal(t, 0, len(zones))
mockZoneManagementService.resetTestConditions()
mockZoneManagementService.TestErrorReturned = true
_, err = provider.Zones()
if err == nil {
t.Errorf("expected to fail, %s", err)
}
}
func TestNewRcodeZeroProvider(t *testing.T) {
_ = os.Setenv("RC0_API_KEY", "123")
p, err := NewRcodeZeroProvider(NewDomainFilter([]string{"ext-dns-test." + testZoneOne + "."}), true, true)
if err != nil {
t.Errorf("should not fail, %s", err)
}
require.Equal(t, true, p.DryRun)
require.Equal(t, true, p.TXTEncrypt)
require.Equal(t, true, p.DomainFilter.IsConfigured())
require.Equal(t, false, p.DomainFilter.Match("ext-dns-test."+testZoneTwo+".")) // filter is set, so it should match only provided domains
p, err = NewRcodeZeroProvider(DomainFilter{}, false, false)
if err != nil {
t.Errorf("should not fail, %s", err)
}
require.Equal(t, false, p.DryRun)
require.Equal(t, false, p.DomainFilter.IsConfigured())
require.Equal(t, true, p.DomainFilter.Match("ext-dns-test."+testZoneOne+".")) // filter is not set, so it should match any
_ = os.Unsetenv("RC0_API_KEY")
_, err = NewRcodeZeroProvider(DomainFilter{}, false, false)
if err == nil {
t.Errorf("expected to fail")
}
}
/* mocking mockRRSetServiceInterface */
func (m *mockRRSetService) List(zone string, options *rc0.ListOptions) ([]*rc0.RRType, *rc0.Page, error) {
if m.TestErrorReturned {
return nil, nil, fmt.Errorf("operation RRSet.List failed")
}
return mockRRSet(zone), nil, nil
}
func mockRRSet(zone string) []*rc0.RRType {
return []*rc0.RRType{
{
Name: "app." + zone + ".",
Type: "TXT",
TTL: 300,
Records: []*rc0.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/default/app\"",
Disabled: false,
},
},
},
{
Name: "app." + zone + ".",
Type: "A",
TTL: 300,
Records: []*rc0.Record{
{
Content: "127.0.0.1",
Disabled: false,
},
},
},
{
Name: "www." + zone + ".",
Type: "A",
TTL: 300,
Records: []*rc0.Record{
{
Content: "127.0.0.1",
Disabled: false,
},
},
},
{
Name: zone + ".",
Type: "SOA",
TTL: 3600,
Records: []*rc0.Record{
{
Content: "sec1.rcode0.net. rcodezero-soa.ipcom.at. 2019011616 10800 3600 604800 3600",
Disabled: false,
},
},
},
{
Name: zone + ".",
Type: "NS",
TTL: 3600,
Records: []*rc0.Record{
{
Content: "sec2.rcode0.net.",
Disabled: false,
},
{
Content: "sec1.rcode0.net.",
Disabled: false,
},
},
},
}
}
func (m *mockRRSetService) Create(zone string, rrsetCreate []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
}
func (m *mockRRSetService) Edit(zone string, rrsetEdit []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
}
func (m *mockRRSetService) Delete(zone string, rrsetDelete []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
}
func (m *mockRRSetService) SubmitChangeSet(zone string, changeSet []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
}
func (m *mockRRSetService) EncryptTXT(key []byte, rrType *rc0.RRSetChange) {}
func (m *mockRRSetService) DecryptTXT(key []byte, rrType *rc0.RRType) {}
/* mocking ZoneManagementServiceInterface */
func (m *mockZoneManagementService) List(options *rc0.ListOptions) ([]*rc0.Zone, *rc0.Page, error) {
if m.TestNilZonesReturned {
return nil, nil, nil
}
if m.TestErrorReturned {
return nil, nil, fmt.Errorf("operation Zone.List failed")
}
zones := []*rc0.Zone{
{
Domain: testZoneOne,
Type: "SLAVE",
// "dnssec": "yes", @todo: add this
// "created": "2018-04-09T09:27:31Z", @todo: add this
LastCheck: "",
Serial: 20180411,
Masters: []string{
"193.0.2.2",
"2001:db8::2",
},
},
{
Domain: testZoneTwo,
Type: "MASTER",
// "dnssec": "no", @todo: add this
// "created": "2019-01-15T13:20:10Z", @todo: add this
LastCheck: "",
Serial: 2019011616,
Masters: []string{
"",
},
},
}
return zones, nil, nil
}
func (m *mockZoneManagementService) Get(zone string) (*rc0.Zone, error) { return nil, nil }
func (m *mockZoneManagementService) Create(zoneCreate *rc0.ZoneCreate) (*rc0.StatusResponse, error) {
return nil, nil
}
func (m *mockZoneManagementService) Edit(zone string, zoneEdit *rc0.ZoneEdit) (*rc0.StatusResponse, error) {
return nil, nil
}
func (m *mockZoneManagementService) Delete(zone string) (*rc0.StatusResponse, error) { return nil, nil }
func (m *mockZoneManagementService) Transfer(zone string) (*rc0.StatusResponse, error) {
return nil, nil
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"fmt"
"net"
"strconv"
@ -43,7 +44,7 @@ type rfc2136Provider struct {
// only consider hosted zones managing domains ending in this suffix
domainFilter DomainFilter
dryRun bool
actions rfc1236Actions
actions rfc2136Actions
}
var (
@ -56,15 +57,15 @@ var (
}
)
type rfc1236Actions interface {
type rfc2136Actions interface {
SendMessage(msg *dns.Msg) error
IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error)
}
// 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 DomainFilter, dryRun bool, actions rfc1236Actions) (Provider, error) {
func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter DomainFilter, dryRun bool, actions rfc2136Actions) (Provider, error) {
secretAlgChecked, ok := tsigAlgs[secretAlg]
if !ok {
if !ok && !insecure {
return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg)
}
@ -161,7 +162,7 @@ func (r rfc2136Provider) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Env
func (r rfc2136Provider) List() ([]dns.RR, error) {
if !r.axfr {
log.Info("axfr is disabled")
log.Debug("axfr is disabled")
return make([]dns.RR, 0), nil
}
@ -195,7 +196,7 @@ func (r rfc2136Provider) List() ([]dns.RR, error) {
}
// ApplyChanges applies a given set of changes in a given zone.
func (r rfc2136Provider) ApplyChanges(changes *plan.Changes) error {
func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
log.Debugf("ApplyChanges")
for _, ep := range changes.Create {
@ -240,25 +241,26 @@ func (r rfc2136Provider) UpdateRecord(ep *endpoint.Endpoint) error {
func (r rfc2136Provider) AddRecord(ep *endpoint.Endpoint) error {
log.Debugf("AddRecord.ep=%s", ep)
for _, target := range ep.Targets {
newRR := fmt.Sprintf("%s %d %s %s", ep.DNSName, ep.RecordTTL, ep.RecordType, target)
log.Debugf("Adding RR: %s", newRR)
newRR := fmt.Sprintf("%s %d %s %s", ep.DNSName, ep.RecordTTL, ep.RecordType, ep.Targets)
log.Debugf("Adding RR: %s", newRR)
rr, err := dns.NewRR(newRR)
if err != nil {
return fmt.Errorf("failed to build RR: %v", err)
}
rr, err := dns.NewRR(newRR)
if err != nil {
return fmt.Errorf("failed to build RR: %v", err)
}
rrs := make([]dns.RR, 1)
rrs[0] = rr
rrs := make([]dns.RR, 1)
rrs[0] = rr
m := new(dns.Msg)
m.SetUpdate(r.zoneName)
m.Insert(rrs)
m := new(dns.Msg)
m.SetUpdate(r.zoneName)
m.Insert(rrs)
err = r.actions.SendMessage(m)
if err != nil {
return fmt.Errorf("RFC2136 query failed: %v", err)
err = r.actions.SendMessage(m)
if err != nil {
return fmt.Errorf("RFC2136 query failed: %v", err)
}
}
return nil
@ -268,7 +270,7 @@ func (r rfc2136Provider) RemoveRecord(ep *endpoint.Endpoint) error {
log.Debugf("RemoveRecord.ep=%s", ep)
newRR := fmt.Sprintf("%s 0 %s 0.0.0.0", ep.DNSName, ep.RecordType)
log.Debugf("Adding RR: %s", newRR)
log.Debugf("Removing RR: %s", newRR)
rr, err := dns.NewRR(newRR)
if err != nil {
@ -293,6 +295,7 @@ func (r rfc2136Provider) RemoveRecord(ep *endpoint.Endpoint) error {
func (r rfc2136Provider) SendMessage(msg *dns.Msg) error {
if r.dryRun {
log.Debugf("SendMessage.skipped")
return nil
}
log.Debugf("SendMessage")

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"context"
"strings"
"testing"
@ -93,7 +94,7 @@ func createRfc2136StubProvider(stub *rfc2136Stub) (Provider, error) {
return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, DomainFilter{}, false, stub)
}
func TestRfc1236GetRecords(t *testing.T) {
func TestRfc2136GetRecords(t *testing.T) {
stub := newStub()
err := stub.setOutput([]string{
"v4.barfoo.com 3600 TXT test1",
@ -149,7 +150,7 @@ func TestRfc2136ApplyChanges(t *testing.T) {
},
}
err = provider.ApplyChanges(p)
err = provider.ApplyChanges(context.Background(), p)
assert.NoError(t, err)
assert.Equal(t, 2, len(stub.createMsgs))

374
provider/transip.go Normal file
View File

@ -0,0 +1,374 @@
package provider
import (
"context"
"errors"
"fmt"
"strings"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
log "github.com/sirupsen/logrus"
"github.com/transip/gotransip"
transip "github.com/transip/gotransip/domain"
)
const (
// 60 seconds is the current minimal TTL for TransIP and will replace unconfigured
// TTL's for Endpoints
transipMinimalValidTTL = 60
)
// TransIPProvider is an implementation of Provider for TransIP.
type TransIPProvider struct {
client gotransip.SOAPClient
domainFilter DomainFilter
dryRun bool
}
// NewTransIPProvider initializes a new TransIP Provider.
func NewTransIPProvider(accountName, privateKeyFile string, domainFilter DomainFilter, dryRun bool) (*TransIPProvider, error) {
// check given arguments
if accountName == "" {
return nil, errors.New("required --transip-account not set")
}
if privateKeyFile == "" {
return nil, errors.New("required --transip-keyfile not set")
}
var apiMode gotransip.APIMode
if dryRun {
apiMode = gotransip.APIModeReadOnly
} else {
apiMode = gotransip.APIModeReadWrite
}
// create new TransIP API client
c, err := gotransip.NewSOAPClient(gotransip.ClientConfig{
AccountName: accountName,
PrivateKeyPath: privateKeyFile,
Mode: apiMode,
})
if err != nil {
return nil, fmt.Errorf("could not setup TransIP API client: %s", err.Error())
}
// return tipCloud struct
return &TransIPProvider{
client: c,
domainFilter: domainFilter,
dryRun: dryRun,
}, nil
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *TransIPProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
// build zonefinder with all our zones so we can use FindZone
// and a mapping of zones and their domain name
zones, err := p.fetchZones()
if err != nil {
return err
}
zoneNameMapper := zoneIDName{}
zonesByName := make(map[string]transip.Domain)
updatedZones := make(map[string]bool)
for _, zone := range zones {
// TransIP API doesn't expose a unique identifier for zones, other than than
// the domain name itself
zoneNameMapper.Add(zone.Name, zone.Name)
zonesByName[zone.Name] = zone
}
// first see if we need to delete anything
for _, ep := range changes.Delete {
log.WithFields(log.Fields{"record": ep.DNSName, "type": ep.RecordType}).Info("endpoint has to go")
zone, err := p.zoneForZoneName(ep.DNSName, zoneNameMapper, zonesByName)
if err != nil {
log.Errorf("could not find zone for %s: %s", ep.DNSName, err.Error())
continue
}
log.Debugf("removing records for %s", zone.Name)
// remove current records from DNS entry set
entries := p.removeEndpointFromEntries(ep, zone)
// update zone in zone map
zone.DNSEntries = entries
zonesByName[zone.Name] = zone
// flag zone for updating
updatedZones[zone.Name] = true
}
for _, ep := range changes.Create {
log.WithFields(log.Fields{"record": ep.DNSName, "type": ep.RecordType}).Info("endpoint is missing")
zone, err := p.zoneForZoneName(ep.DNSName, zoneNameMapper, zonesByName)
if err != nil {
log.Errorf("could not find zone for %s: %s", ep.DNSName, err.Error())
continue
}
log.Debugf("creating records for %s", zone.Name)
// add new entries to set
zone.DNSEntries = p.addEndpointToEntries(ep, zone, zone.DNSEntries)
// update zone in zone map
zonesByName[zone.Name] = zone
// flag zone for updating
updatedZones[zone.Name] = true
log.WithFields(log.Fields{"zone": zone.Name}).Debug("flagging for update")
}
for _, ep := range changes.UpdateNew {
log.WithFields(log.Fields{"record": ep.DNSName, "type": ep.RecordType}).Debug("needs updating")
zone, err := p.zoneForZoneName(ep.DNSName, zoneNameMapper, zonesByName)
if err != nil {
log.WithFields(log.Fields{"record": ep.DNSName}).Warn(err.Error())
continue
}
// updating the records is basically finding all matching records according
// to the name and the type, removing them from the set and add the new
// records
log.WithFields(log.Fields{
"zone": zone.Name,
"dnsname": ep.DNSName,
"recordtype": ep.RecordType,
}).Debug("removing matching entries")
// remove current records from DNS entry set
entries := p.removeEndpointFromEntries(ep, zone)
// add new entries to set
entries = p.addEndpointToEntries(ep, zone, entries)
// check to see if actually anything changed in the DNSEntry set
if p.dnsEntriesAreEqual(entries, zone.DNSEntries) {
log.WithFields(log.Fields{"zone": zone.Name}).Debug("not updating identical entries")
continue
}
// update zone in zone map
zone.DNSEntries = entries
zonesByName[zone.Name] = zone
// flag zone for updating
updatedZones[zone.Name] = true
log.WithFields(log.Fields{"zone": zone.Name}).Debug("flagging for update")
}
// go over all updated zones and set new DNSEntry set
for uz := range updatedZones {
zone, ok := zonesByName[uz]
if !ok {
log.WithFields(log.Fields{"zone": uz}).Debug("updated zone no longer found")
continue
}
if p.dryRun {
log.WithFields(log.Fields{"zone": zone.Name}).Info("not updating in dry-run mode")
continue
}
log.WithFields(log.Fields{"zone": zone.Name}).Info("updating DNS entries")
if err := transip.SetDNSEntries(p.client, zone.Name, zone.DNSEntries); err != nil {
log.WithFields(log.Fields{"zone": zone.Name, "error": err.Error()}).Warn("failed to update")
}
}
return nil
}
// fetchZones returns a list of all domains within the account
func (p *TransIPProvider) fetchZones() ([]transip.Domain, error) {
domainNames, err := transip.GetDomainNames(p.client)
if err != nil {
return nil, err
}
domains, err := transip.BatchGetInfo(p.client, domainNames)
if err != nil {
return nil, err
}
var zones []transip.Domain
for _, d := range domains {
if !p.domainFilter.Match(d.Name) {
continue
}
zones = append(zones, d)
}
return zones, nil
}
// Zones returns the list of hosted zones.
func (p *TransIPProvider) Zones() ([]transip.Domain, error) {
zones, err := p.fetchZones()
if err != nil {
return nil, err
}
return zones, nil
}
// Records returns the list of records in a given zone.
func (p *TransIPProvider) Records() ([]*endpoint.Endpoint, error) {
zones, err := p.Zones()
if err != nil {
return nil, err
}
var endpoints []*endpoint.Endpoint
var name string
// 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)) {
continue
}
name = p.endpointNameForRecord(r, zone)
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, string(r.Type), endpoint.TTL(r.TTL), r.Content))
}
}
return endpoints, nil
}
// endpointNameForRecord returns "www.example.org" for DNSEntry with Name "www" and
// Doman with Name "example.org"
func (p *TransIPProvider) endpointNameForRecord(r transip.DNSEntry, d transip.Domain) string {
// root name is identified by "@" and should be translated to domain name for
// the endpoint entry.
if r.Name == "@" {
return d.Name
}
return fmt.Sprintf("%s.%s", r.Name, d.Name)
}
// recordNameForEndpoint returns "www" for Endpoint with DNSName "www.example.org"
// and Domain with Name "example.org"
func (p *TransIPProvider) recordNameForEndpoint(ep *endpoint.Endpoint, d transip.Domain) string {
// root name is identified by "@" and should be translated to domain name for
// the endpoint entry.
if ep.DNSName == d.Name {
return "@"
}
return strings.TrimSuffix(ep.DNSName, "."+d.Name)
}
// getMinimalValidTTL returns max between given Endpoint's RecordTTL and
// transipMinimalValidTTL
func (p *TransIPProvider) getMinimalValidTTL(ep *endpoint.Endpoint) int64 {
// TTL cannot be lower than transipMinimalValidTTL
if ep.RecordTTL < transipMinimalValidTTL {
return transipMinimalValidTTL
}
return int64(ep.RecordTTL)
}
// dnsEntriesAreEqual compares the entries in 2 sets and returns true if the
// content of the entries is equal
func (p *TransIPProvider) dnsEntriesAreEqual(a, b transip.DNSEntries) bool {
if len(a) != len(b) {
return false
}
match := 0
for _, aa := range a {
for _, bb := range b {
if aa.Content != bb.Content {
continue
}
if aa.Name != bb.Name {
continue
}
if aa.TTL != bb.TTL {
continue
}
if aa.Type != bb.Type {
continue
}
match += 1
}
}
return (len(a) == match)
}
// removeEndpointFromEntries removes DNS entries from zone's set that match the
// type and name from given endpoint and returns the resulting DNS entry set
func (p *TransIPProvider) removeEndpointFromEntries(ep *endpoint.Endpoint, zone transip.Domain) transip.DNSEntries {
// create new entry set
entries := transip.DNSEntries{}
// go over each DNS entry to see if it is a match
for _, e := range zone.DNSEntries {
// if we have match, don't copy it to the new entry set
if p.endpointNameForRecord(e, zone) == ep.DNSName && string(e.Type) == ep.RecordType {
log.WithFields(log.Fields{
"name": e.Name,
"content": e.Content,
"type": e.Type,
}).Debug("found match")
continue
}
entries = append(entries, e)
}
return entries
}
// addEndpointToEntries creates DNS entries for given endpoint and returns
// resulting DNS entry set
func (p *TransIPProvider) addEndpointToEntries(ep *endpoint.Endpoint, zone transip.Domain, entries transip.DNSEntries) transip.DNSEntries {
ttl := p.getMinimalValidTTL(ep)
for _, target := range ep.Targets {
log.WithFields(log.Fields{
"zone": zone.Name,
"dnsname": ep.DNSName,
"recordtype": ep.RecordType,
"ttl": ttl,
"target": target,
}).Debugf("adding new record")
entries = append(entries, transip.DNSEntry{
Name: p.recordNameForEndpoint(ep, zone),
TTL: ttl,
Type: transip.DNSEntryType(ep.RecordType),
Content: target,
})
}
return entries
}
// 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) {
_, zoneName := m.FindZone(name)
if zoneName == "" {
return transip.Domain{}, fmt.Errorf("could not find zoneName for %s", name)
}
zone, ok := z[zoneName]
if !ok {
return zone, fmt.Errorf("could not find zone for %s", zoneName)
}
return zone, nil
}

215
provider/transip_test.go Normal file
View File

@ -0,0 +1,215 @@
package provider
import (
"testing"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/stretchr/testify/assert"
transip "github.com/transip/gotransip/domain"
)
func TestTransIPDnsEntriesAreEqual(t *testing.T) {
p := TransIPProvider{}
// test with equal set
a := transip.DNSEntries{
transip.DNSEntry{
Name: "www.example.org",
Type: transip.DNSEntryTypeCNAME,
TTL: 3600,
Content: "www.example.com",
},
transip.DNSEntry{
Name: "www.example.com",
Type: transip.DNSEntryTypeA,
TTL: 3600,
Content: "192.168.0.1",
},
}
b := transip.DNSEntries{
transip.DNSEntry{
Name: "www.example.com",
Type: transip.DNSEntryTypeA,
TTL: 3600,
Content: "192.168.0.1",
},
transip.DNSEntry{
Name: "www.example.org",
Type: transip.DNSEntryTypeCNAME,
TTL: 3600,
Content: "www.example.com",
},
}
assert.Equal(t, true, p.dnsEntriesAreEqual(a, b))
// change type on one of b's records
b[1].Type = transip.DNSEntryTypeNS
assert.Equal(t, false, p.dnsEntriesAreEqual(a, b))
b[1].Type = transip.DNSEntryTypeCNAME
// change ttl on one of b's records
b[1].TTL = 1800
assert.Equal(t, false, p.dnsEntriesAreEqual(a, b))
b[1].TTL = 3600
// change name on one of b's records
b[1].Name = "example.org"
assert.Equal(t, false, p.dnsEntriesAreEqual(a, b))
// remove last entry of b
b = b[:1]
assert.Equal(t, false, p.dnsEntriesAreEqual(a, b))
}
func TestTransIPGetMinimalValidTTL(t *testing.T) {
p := TransIPProvider{}
// test with 'unconfigured' TTL
ep := &endpoint.Endpoint{}
assert.Equal(t, int64(transipMinimalValidTTL), p.getMinimalValidTTL(ep))
// test with lower than minimal ttl
ep.RecordTTL = (transipMinimalValidTTL - 1)
assert.Equal(t, int64(transipMinimalValidTTL), p.getMinimalValidTTL(ep))
// test with higher than minimal ttl
ep.RecordTTL = (transipMinimalValidTTL + 1)
assert.Equal(t, int64(transipMinimalValidTTL+1), p.getMinimalValidTTL(ep))
}
func TestTransIPRecordNameForEndpoint(t *testing.T) {
p := TransIPProvider{}
ep := &endpoint.Endpoint{
DNSName: "example.org",
}
d := transip.Domain{
Name: "example.org",
}
assert.Equal(t, "@", p.recordNameForEndpoint(ep, d))
ep.DNSName = "www.example.org"
assert.Equal(t, "www", p.recordNameForEndpoint(ep, d))
}
func TestTransIPEndpointNameForRecord(t *testing.T) {
p := TransIPProvider{}
r := transip.DNSEntry{
Name: "@",
}
d := transip.Domain{
Name: "example.org",
}
assert.Equal(t, d.Name, p.endpointNameForRecord(r, d))
r.Name = "www"
assert.Equal(t, "www.example.org", p.endpointNameForRecord(r, d))
}
func TestTransIPAddEndpointToEntries(t *testing.T) {
p := TransIPProvider{}
// prepare endpoint
ep := &endpoint.Endpoint{
DNSName: "www.example.org",
RecordType: "A",
RecordTTL: 1800,
Targets: []string{
"192.168.0.1",
"192.168.0.2",
},
}
// prepare zone with DNS entry set
zone := transip.Domain{
Name: "example.org",
// 2 matching A records
DNSEntries: transip.DNSEntries{
// 1 non-matching A record
transip.DNSEntry{
Name: "mail",
Type: transip.DNSEntryTypeA,
Content: "192.168.0.1",
TTL: 3600,
},
// 1 non-matching MX record
transip.DNSEntry{
Name: "@",
Type: transip.DNSEntryTypeMX,
Content: "mail.example.org",
TTL: 3600,
},
},
}
// add endpoint to zone's entries
result := p.addEndpointToEntries(ep, zone, zone.DNSEntries)
assert.Equal(t, 4, len(result))
assert.Equal(t, "mail", result[0].Name)
assert.Equal(t, transip.DNSEntryTypeA, result[0].Type)
assert.Equal(t, "@", result[1].Name)
assert.Equal(t, transip.DNSEntryTypeMX, result[1].Type)
assert.Equal(t, "www", result[2].Name)
assert.Equal(t, transip.DNSEntryTypeA, result[2].Type)
assert.Equal(t, "192.168.0.1", result[2].Content)
assert.Equal(t, int64(1800), result[2].TTL)
assert.Equal(t, "www", result[3].Name)
assert.Equal(t, transip.DNSEntryTypeA, result[3].Type)
assert.Equal(t, "192.168.0.2", result[3].Content)
assert.Equal(t, int64(1800), result[3].TTL)
}
func TestTransIPRemoveEndpointFromEntries(t *testing.T) {
p := TransIPProvider{}
// prepare endpoint
ep := &endpoint.Endpoint{
DNSName: "www.example.org",
RecordType: "A",
}
// prepare zone with DNS entry set
zone := transip.Domain{
Name: "example.org",
// 2 matching A records
DNSEntries: transip.DNSEntries{
transip.DNSEntry{
Name: "www",
Type: transip.DNSEntryTypeA,
Content: "192.168.0.1",
TTL: 3600,
},
transip.DNSEntry{
Name: "www",
Type: transip.DNSEntryTypeA,
Content: "192.168.0.2",
TTL: 3600,
},
// 1 non-matching A record
transip.DNSEntry{
Name: "mail",
Type: transip.DNSEntryTypeA,
Content: "192.168.0.1",
TTL: 3600,
},
// 1 non-matching MX record
transip.DNSEntry{
Name: "@",
Type: transip.DNSEntryTypeMX,
Content: "mail.example.org",
TTL: 3600,
},
},
}
// remove endpoint from zone's entries
result := p.removeEndpointFromEntries(ep, zone)
assert.Equal(t, 2, len(result))
assert.Equal(t, "mail", result[0].Name)
assert.Equal(t, transip.DNSEntryTypeA, result[0].Type)
assert.Equal(t, "@", result[1].Name)
assert.Equal(t, transip.DNSEntryTypeMX, result[1].Type)
}

View File

@ -0,0 +1,57 @@
/*
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 (
"strings"
)
// ZoneTagFilter holds a list of zone tags to filter by
type ZoneTagFilter struct {
zoneTags []string
}
// NewZoneTagFilter returns a new ZoneTagFilter given a list of zone tags
func NewZoneTagFilter(tags []string) ZoneTagFilter {
if len(tags) == 1 && len(tags[0]) == 0 {
tags = []string{}
}
return ZoneTagFilter{zoneTags: tags}
}
// Match checks whether a zone's set of tags matches the provided tag values
func (f ZoneTagFilter) Match(tagsMap map[string]string) bool {
for _, tagFilter := range f.zoneTags {
filterParts := strings.SplitN(tagFilter, "=", 2)
switch len(filterParts) {
case 1:
if _, hasTag := tagsMap[filterParts[0]]; !hasTag {
return false
}
case 2:
if value, hasTag := tagsMap[filterParts[0]]; !hasTag || value != filterParts[1] {
return false
}
}
}
return true
}
// IsEmpty returns true if there are no tags for the filter
func (f ZoneTagFilter) IsEmpty() bool {
return len(f.zoneTags) == 0
}

View File

@ -0,0 +1,62 @@
/*
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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestZoneTagFilterMatch(t *testing.T) {
for _, tc := range []struct {
name string
zoneTagFilter []string
zoneTags map[string]string
matches bool
}{
{
"single tag no match", []string{"tag1=value1"}, map[string]string{"tag0": "value0"}, false,
},
{
"single tag matches", []string{"tag1=value1"}, map[string]string{"tag1": "value1"}, true,
},
{
"multiple tags no value match", []string{"tag1=value1"}, map[string]string{"tag0": "value0", "tag1": "value2"}, false,
},
{
"multiple tags matches", []string{"tag1=value1"}, map[string]string{"tag0": "value0", "tag1": "value1"}, true,
},
{
"tag name no match", []string{"tag1"}, map[string]string{"tag0": "value0"}, false,
},
{
"tag name matches", []string{"tag1"}, map[string]string{"tag1": "value1"}, true,
},
{
"multiple filter no match", []string{"tag1=value1", "tag2=value2"}, map[string]string{"tag1": "value1"}, false,
},
{
"multiple filter matches", []string{"tag1=value1", "tag2=value2"}, map[string]string{"tag2": "value2", "tag1": "value1", "tag3": "value3"}, true,
},
} {
zoneTagFilter := NewZoneTagFilter(tc.zoneTagFilter)
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.matches, zoneTagFilter.Match(tc.zoneTags))
})
}
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package registry
import (
"context"
"errors"
"github.com/kubernetes-incubator/external-dns/endpoint"
@ -64,7 +65,7 @@ func (sdr *AWSSDRegistry) Records() ([]*endpoint.Endpoint, error) {
// ApplyChanges filters out records not owned the External-DNS, additionally it adds the required label
// inserted in the AWS SD instance as a CreateID field
func (sdr *AWSSDRegistry) ApplyChanges(changes *plan.Changes) error {
func (sdr *AWSSDRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
filteredChanges := &plan.Changes{
Create: changes.Create,
UpdateNew: filterOwnedRecords(sdr.ownerID, changes.UpdateNew),
@ -77,7 +78,7 @@ func (sdr *AWSSDRegistry) ApplyChanges(changes *plan.Changes) error {
sdr.updateLabels(filteredChanges.UpdateOld)
sdr.updateLabels(filteredChanges.Delete)
return sdr.provider.ApplyChanges(filteredChanges)
return sdr.provider.ApplyChanges(ctx, filteredChanges)
}
func (sdr *AWSSDRegistry) updateLabels(endpoints []*endpoint.Endpoint) {

View File

@ -17,6 +17,7 @@ limitations under the License.
package registry
import (
"context"
"testing"
"github.com/kubernetes-incubator/external-dns/endpoint"
@ -35,7 +36,7 @@ func (p *inMemoryProvider) Records() ([]*endpoint.Endpoint, error) {
return p.endpoints, nil
}
func (p *inMemoryProvider) ApplyChanges(changes *plan.Changes) error {
func (p *inMemoryProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
p.onApplyChanges(changes)
return nil
}
@ -151,7 +152,7 @@ func TestAWSSDRegistry_Records_ApplyChanges(t *testing.T) {
r, err := NewAWSSDRegistry(p, "owner")
require.NoError(t, err)
err = r.ApplyChanges(changes)
err = r.ApplyChanges(context.Background(), changes)
require.NoError(t, err)
}

View File

@ -17,6 +17,8 @@ limitations under the License.
package registry
import (
"context"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
"github.com/kubernetes-incubator/external-dns/provider"
@ -40,6 +42,6 @@ func (im *NoopRegistry) Records() ([]*endpoint.Endpoint, error) {
}
// ApplyChanges propagates changes to the dns provider
func (im *NoopRegistry) ApplyChanges(changes *plan.Changes) error {
return im.provider.ApplyChanges(changes)
func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
return im.provider.ApplyChanges(ctx, changes)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package registry
import (
"context"
"testing"
"github.com/kubernetes-incubator/external-dns/endpoint"
@ -53,7 +54,7 @@ func testNoopRecords(t *testing.T) {
RecordType: endpoint.RecordTypeCNAME,
},
}
p.ApplyChanges(&plan.Changes{
p.ApplyChanges(context.Background(), &plan.Changes{
Create: providerRecords,
})
@ -88,13 +89,14 @@ func testNoopApplyChanges(t *testing.T) {
},
}
p.ApplyChanges(&plan.Changes{
ctx := context.Background()
p.ApplyChanges(ctx, &plan.Changes{
Create: providerRecords,
})
// wrong changes
r, _ := NewNoopRegistry(p)
err := r.ApplyChanges(&plan.Changes{
err := r.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "example.org",
@ -106,7 +108,7 @@ func testNoopApplyChanges(t *testing.T) {
assert.EqualError(t, err, provider.ErrRecordAlreadyExists.Error())
//correct changes
require.NoError(t, r.ApplyChanges(&plan.Changes{
require.NoError(t, r.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "new-record.org",

View File

@ -17,6 +17,8 @@ limitations under the License.
package registry
import (
"context"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
log "github.com/sirupsen/logrus"
@ -28,7 +30,7 @@ import (
// ApplyChanges(changes *plan.Changes) propagates the changes to the DNS Provider API and correspondingly updates ownership depending on type of registry being used
type Registry interface {
Records() ([]*endpoint.Endpoint, error)
ApplyChanges(changes *plan.Changes) error
ApplyChanges(ctx context.Context, changes *plan.Changes) error
}
//TODO(ideahitme): consider moving this to Plan

View File

@ -17,6 +17,7 @@ limitations under the License.
package registry
import (
"context"
"errors"
"time"
@ -117,7 +118,7 @@ func (im *TXTRegistry) Records() ([]*endpoint.Endpoint, error) {
// ApplyChanges updates dns provider with the changes
// for each created/deleted record it will also take into account TXT records for creation/deletion
func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error {
func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
filteredChanges := &plan.Changes{
Create: changes.Create,
UpdateNew: filterOwnedRecords(im.ownerID, changes.UpdateNew),
@ -171,7 +172,11 @@ func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error {
}
}
return im.provider.ApplyChanges(filteredChanges)
// when caching is enabled, disable the provider from using the cache
if im.cacheInterval > 0 {
ctx = context.WithValue(ctx, provider.RecordsContextKey, nil)
}
return im.provider.ApplyChanges(ctx, filteredChanges)
}
/**

View File

@ -17,6 +17,7 @@ limitations under the License.
package registry
import (
"context"
"reflect"
"testing"
"time"
@ -68,7 +69,7 @@ func testTXTRegistryRecords(t *testing.T) {
func testTXTRegistryRecordsPrefixed(t *testing.T) {
p := provider.NewInMemoryProvider()
p.CreateZone(testZone)
p.ApplyChanges(&plan.Changes{
p.ApplyChanges(context.Background(), &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, ""),
@ -141,7 +142,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
func testTXTRegistryRecordsNoPrefix(t *testing.T) {
p := provider.NewInMemoryProvider()
p.CreateZone(testZone)
p.ApplyChanges(&plan.Changes{
p.ApplyChanges(context.Background(), &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, ""),
@ -220,7 +221,12 @@ func testTXTRegistryApplyChanges(t *testing.T) {
func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
p := provider.NewInMemoryProvider()
p.CreateZone(testZone)
p.ApplyChanges(&plan.Changes{
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, ""),
@ -267,7 +273,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
},
}
p.OnApplyChanges = func(got *plan.Changes) {
p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) {
mExpected := map[string][]*endpoint.Endpoint{
"Create": expected.Create,
"UpdateNew": expected.UpdateNew,
@ -281,15 +287,21 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
"Delete": got.Delete,
}
assert.True(t, testutils.SamePlanChanges(mGot, mExpected))
assert.Equal(t, nil, ctx.Value(provider.RecordsContextKey))
}
err := r.ApplyChanges(changes)
err := r.ApplyChanges(ctx, changes)
require.NoError(t, err)
}
func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
p := provider.NewInMemoryProvider()
p.CreateZone(testZone)
p.ApplyChanges(&plan.Changes{
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, ""),
@ -330,7 +342,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
UpdateNew: []*endpoint.Endpoint{},
UpdateOld: []*endpoint.Endpoint{},
}
p.OnApplyChanges = func(got *plan.Changes) {
p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) {
mExpected := map[string][]*endpoint.Endpoint{
"Create": expected.Create,
"UpdateNew": expected.UpdateNew,
@ -344,8 +356,9 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
"Delete": got.Delete,
}
assert.True(t, testutils.SamePlanChanges(mGot, mExpected))
assert.Equal(t, nil, ctx.Value(provider.RecordsContextKey))
}
err := r.ApplyChanges(changes)
err := r.ApplyChanges(ctx, changes)
require.NoError(t, err)
}

59
source/cloudfoundry.go Normal file
View File

@ -0,0 +1,59 @@
/*
Copyright 2019 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 source
import (
"net/url"
cfclient "github.com/cloudfoundry-community/go-cfclient"
"github.com/kubernetes-incubator/external-dns/endpoint"
)
type cloudfoundrySource struct {
client *cfclient.Client
config Config
}
// NewCloudFoundrySource creates a new cloudfoundrySource with the given config
func NewCloudFoundrySource(cfClient *cfclient.Client) (Source, error) {
return &cloudfoundrySource{
client: cfClient,
}, nil
}
// Endpoints returns endpoint objects
func (rs *cloudfoundrySource) Endpoints() ([]*endpoint.Endpoint, error) {
endpoints := []*endpoint.Endpoint{}
u, err := url.Parse(rs.client.Config.ApiAddress)
if err != nil {
panic(err)
}
domains, _ := rs.client.ListDomains()
for _, domain := range domains {
q := url.Values{}
q.Set("q", "domain_guid:"+domain.Guid)
routes, _ := rs.client.ListRoutesByQuery(q)
for _, element := range routes {
endpoints = append(endpoints,
endpoint.NewEndpointWithTTL(element.Host+"."+domain.Name, endpoint.RecordTypeCNAME, 300, u.Host))
}
}
return endpoints, nil
}

View File

@ -0,0 +1,38 @@
/*
Copyright 2019 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 source
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type RouteSuite struct {
suite.Suite
}
func TestRouteSource(t *testing.T) {
suite.Run(t, new(RouteSuite))
t.Run("Interface", testRouteSourceImplementsSource)
}
// testRouteSourceImplementsSource tests that cloudfoundrySource is a valid Source.
func testRouteSourceImplementsSource(t *testing.T) {
require.Implements(t, (*Source)(nil), new(cloudfoundrySource))
}

View File

@ -19,9 +19,8 @@ package source
import (
"strings"
"k8s.io/api/core/v1"
"github.com/kubernetes-incubator/external-dns/endpoint"
v1 "k8s.io/api/core/v1"
)
const (

View File

@ -118,6 +118,11 @@ func (cs *crdSource) Endpoints() ([]*endpoint.Endpoint, error) {
for _, dnsEndpoint := range result.Items {
endpoints = append(endpoints, dnsEndpoint.Spec.Endpoints...)
if dnsEndpoint.Status.ObservedGeneration == dnsEndpoint.Generation {
continue
}
dnsEndpoint.Status.ObservedGeneration = dnsEndpoint.Generation
// Update the ObservedGeneration
_, err = cs.UpdateStatus(&dnsEndpoint)

View File

@ -18,6 +18,7 @@ package source
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@ -54,20 +55,21 @@ func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser {
return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
}
func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string) rest.Interface {
func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string, t *testing.T) rest.Interface {
groupVersion, _ := schema.ParseGroupVersion(apiVersion)
scheme := runtime.NewScheme()
addKnownTypes(scheme, groupVersion)
dnsEndpointList := endpoint.DNSEndpointList{}
dnsEndpoint := endpoint.DNSEndpoint{
dnsEndpoint := &endpoint.DNSEndpoint{
TypeMeta: metav1.TypeMeta{
APIVersion: apiVersion,
Kind: kind,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Name: name,
Namespace: namespace,
Generation: 1,
},
Spec: endpoint.DNSEndpointSpec{
Endpoints: endpoints,
@ -88,10 +90,18 @@ func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, ki
case p == "/apis/"+apiVersion+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet:
fallthrough
case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet:
dnsEndpointList.Items = append(dnsEndpointList.Items, dnsEndpoint)
dnsEndpointList.Items = dnsEndpointList.Items[:0]
dnsEndpointList.Items = append(dnsEndpointList.Items, *dnsEndpoint)
return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil
case strings.HasPrefix(p, "/apis/"+apiVersion+"/namespaces/") && strings.HasSuffix(p, strings.ToLower(kind)+"s") && m == http.MethodGet:
return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil
case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s/"+name+"/status" && m == http.MethodPut:
decoder := json.NewDecoder(req.Body)
var body endpoint.DNSEndpoint
decoder.Decode(&body)
dnsEndpoint.Status.ObservedGeneration = body.Status.ObservedGeneration
return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, dnsEndpoint)}, nil
default:
return nil, fmt.Errorf("unexpected request: %#v\n%#v", req.URL, req)
}
@ -200,6 +210,8 @@ func testCRDSourceEndpoints(t *testing.T) {
apiVersion: "test.k8s.io/v1alpha1",
registeredKind: "DNSEndpoint",
kind: "DNSEndpoint",
namespace: "foo",
registeredNamespace: "foo",
endpoints: []*endpoint.Endpoint{
{DNSName: "abc.example.org",
Targets: endpoint.Targets{"1.2.3.4"},
@ -216,6 +228,8 @@ func testCRDSourceEndpoints(t *testing.T) {
apiVersion: "test.k8s.io/v1alpha1",
registeredKind: "DNSEndpoint",
kind: "DNSEndpoint",
namespace: "foo",
registeredNamespace: "foo",
endpoints: []*endpoint.Endpoint{
{DNSName: "abc.example.org",
Targets: endpoint.Targets{"1.2.3.4"},
@ -233,7 +247,7 @@ func testCRDSourceEndpoints(t *testing.T) {
},
} {
t.Run(ti.title, func(t *testing.T) {
restClient := startCRDServerToServeTargets(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "")
restClient := startCRDServerToServeTargets(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "test", t)
groupVersion, err := schema.ParseGroupVersion(ti.apiVersion)
require.NoError(t, err)
@ -253,8 +267,28 @@ func testCRDSourceEndpoints(t *testing.T) {
return
}
if err == nil {
validateCRDResource(t, cs, ti.expectError)
}
// Validate received endpoints against expected endpoints.
validateEndpoints(t, receivedEndpoints, ti.endpoints)
})
}
}
func validateCRDResource(t *testing.T, src Source, expectError bool) {
cs := src.(*crdSource)
result, err := cs.List(&metav1.ListOptions{})
if expectError {
require.Errorf(t, err, "Received err %v", err)
} else {
require.NoErrorf(t, err, "Received err %v", err)
}
for _, dnsEndpoint := range result.Items {
if dnsEndpoint.Status.ObservedGeneration != dnsEndpoint.Generation {
require.Errorf(t, err, "Unexpected CRD resource result: ObservedGenerations <%v> is not equal to Generation<%v>", dnsEndpoint.Status.ObservedGeneration, dnsEndpoint.Generation)
}
}
}

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