Merge branch 'master' into oci-auth-instance-principal

This commit is contained in:
Eric R. Rath 2021-01-30 17:41:58 -08:00 committed by GitHub
commit e3feec4c8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 3846 additions and 1211 deletions

View File

@ -2,7 +2,7 @@
name: "❓Support Request"
about: Support request or question relating to external-dns
title: ''
labels: triage/support
labels: kind/support
assignees: ''
---

View File

@ -16,7 +16,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.14
go-version: ^1.15
id: go
- name: Check out code into the Go module directory
@ -30,6 +30,12 @@ jobs:
dep ensure
fi
- name: Install additional CI for nektos/act
run: |
apt update
apt install -y make gcc libc-dev git
if: github.actor == 'nektos/act'
- name: Lint
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0
@ -42,3 +48,4 @@ jobs:
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: profile.cov
if: github.actor != 'nektos/act'

2
.gitignore vendored
View File

@ -46,3 +46,5 @@ external-dns
# vendor dir
vendor/
profile.cov

View File

@ -17,6 +17,8 @@
- Update contributing section in README (#1760) @seanmalloy
- Option to cache AWS zones list @bpineau
- Oracle OCI provider: add support for instance principal authentication (#1700) @ericrrath
- Refactor, enhance and test Akamai provider and documentation (#1846) @edglynes
- Fix: only use absolute CNAMEs in Scaleway provider (#1859) @Sh4d1
## v0.7.3 - 2020-08-05

View File

@ -13,22 +13,19 @@
# limitations under the License.
# builder image
FROM golang:1.14 as builder
ARG ARCH
FROM golang:1.15 as builder
ARG ARCH
WORKDIR /sigs.k8s.io/external-dns
COPY . .
RUN go mod vendor && \
make test && \
make build
RUN make test && make build.$ARCH
# final image
FROM alpine:3.12
LABEL maintainer="Team Teapot @ Zalando SE <team-teapot@zalando.de>"
RUN apk add --update --no-cache ca-certificates && \
update-ca-certificates
FROM $ARCH/alpine:3.12
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /sigs.k8s.io/external-dns/build/external-dns /bin/external-dns
# Run as UID for nobody since k8s pod securityContext runAsNonRoot can't resolve the user ID:

View File

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

View File

@ -68,17 +68,45 @@ IMAGE ?= us.gcr.io/k8s-artifacts-prod/external-dns/$(BINARY)
VERSION ?= $(shell git describe --tags --always --dirty)
BUILD_FLAGS ?= -v
LDFLAGS ?= -X sigs.k8s.io/external-dns/pkg/apis/externaldns.Version=$(VERSION) -w -s
ARCHS = amd64 arm64v8 arm32v7
SHELL = /bin/bash
build: build/$(BINARY)
build/$(BINARY): $(SOURCES)
CGO_ENABLED=0 go build -o build/$(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" .
build.push/multiarch:
arch_specific_tags=()
for arch in $(ARCHS); do \
image="$(IMAGE):$(VERSION)-$${arch}" ;\
# pre-pull due to https://github.com/kubernetes-sigs/cluster-addons/pull/84/files ;\
docker pull $${arch}/alpine:3.12 ;\
DOCKER_BUILDKIT=1 docker build --rm --tag $${image} --build-arg VERSION="$(VERSION)" --build-arg ARCH="$${arch}" . ;\
docker push $${image} ;\
arch_specific_tags+=( "--amend $${image}" ) ;\
done ;\
DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create "$(IMAGE):$(VERSION)" $${arch_specific_tags[@]} ;\
for arch in $(ARCHS); do \
DOCKER_CLI_EXPERIMENTAL=enabled docker manifest annotate --arch $${arch} "$(IMAGE):$(VERSION)" "$(IMAGE):$(VERSION)-$${arch}" ;\
done;\
DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(IMAGE):$(VERSION)" \
build.push: build.docker
docker push "$(IMAGE):$(VERSION)"
build.arm64v8:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o build/$(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" .
build.amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/$(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" .
build.arm32v7:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o build/$(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" .
build.docker:
docker build --rm --tag "$(IMAGE):$(VERSION)" --build-arg VERSION="$(VERSION)" .
docker build --rm --tag "$(IMAGE):$(VERSION)" --build-arg VERSION="$(VERSION)" --build-arg ARCH="amd64" .
build.mini:
docker build --rm --tag "$(IMAGE):$(VERSION)-mini" --build-arg VERSION="$(VERSION)" -f Dockerfile.mini .
@ -90,7 +118,7 @@ clean:
.PHONY: release.staging
release.staging:
IMAGE=$(IMAGE_STAGING) $(MAKE) build.docker build.push
IMAGE=$(IMAGE_STAGING) $(MAKE) build.push/multiarch
release.prod:
$(MAKE) build.docker build.push
$(MAKE) build.push/multiarch

View File

@ -45,8 +45,10 @@ ExternalDNS' current release is `v0.7`. This version allows you to keep selected
* [NS1](https://ns1.com/)
* [TransIP](https://www.transip.eu/domain-name/)
* [VinylDNS](https://www.vinyldns.io)
* [Vultr](https://www.vultr.com)
* [OVH](https://www.ovh.com)
* [Scaleway](https://www.scaleway.com)
* [Akamai Edge DNS](https://learn.akamai.com/en-us/products/cloud_security/edge_dns.html)
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.
@ -77,6 +79,7 @@ The following table clarifies the current status of the providers according to t
| Google Cloud DNS | Stable | |
| AWS Route 53 | Stable | |
| AWS Cloud Map | Beta | |
| Akamai Edge DNS | Beta | |
| AzureDNS | Beta | |
| CloudFlare | Beta | |
| RcodeZero | Alpha | |
@ -96,7 +99,6 @@ The following table clarifies the current status of the providers according to t
| TransIP | Alpha | |
| VinylDNS | Alpha | |
| RancherDNS | Alpha | |
| Akamai FastDNS | Alpha | |
| OVH | Alpha | |
| Scaleway DNS | Alpha | @Sh4d1 |
| Vultr | Alpha | |
@ -140,6 +142,7 @@ The following tutorials are provided:
* [Linode](docs/tutorials/linode.md)
* [Nginx Ingress Controller](docs/tutorials/nginx-ingress.md)
* [NS1](docs/tutorials/ns1.md)
* [NS Record Creation with CRD Source](docs/tutorials/ns-record.md)
* [OpenStack Designate](docs/tutorials/designate.md)
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
* [PowerDNS](docs/tutorials/pdns.md)
@ -163,8 +166,8 @@ from source.
Next, run an application and expose it via a Kubernetes Service:
```console
$ kubectl run nginx --image=nginx --replicas=1 --port=80
$ kubectl expose deployment nginx --port=80 --target-port=80 --type=LoadBalancer
$ kubectl run nginx --image=nginx --port=80
$ kubectl expose pod nginx --port=80 --target-port=80 --type=LoadBalancer
```
Annotate the Service with your desired external DNS name. Make sure to change `example.org` to your domain.
@ -181,6 +184,14 @@ $ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/ttl=10"
For more details on configuring TTL, see [here](docs/ttl.md).
Use the internal-hostname annotation to create DNS records with ClusterIP as the target.
```console
$ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/internal-hostname=nginx.internal.example.org."
```
If the service is not of type Loadbalancer you need the --publish-internal-services flag.
Locally run a single sync loop of ExternalDNS.
```console
@ -216,6 +227,8 @@ The [tutorials](docs/tutorials) section contains examples, including Ingress res
If using a txt registry and attempting to use a CNAME the `--txt-prefix` must be set to avoid conflicts. Changing `--txt-prefix` will result in lost ownership over previously created records.
If `externalIPs` list is defined for a `LoadBalancer` service, this list will be used instead of an assigned load balancer IP to create a DNS record. It's useful when you run bare metal Kubernetes clusters behind NAT or in a similar setup, where a load balancer IP differs from a public IP (e.g. with [MetalLB](https://metallb.universe.tf)).
# Roadmap
ExternalDNS was built with extensibility in mind. Adding and experimenting with new DNS providers and sources of desired DNS records should be as easy as possible. It should also be possible to modify how ExternalDNS behaves—e.g. whether it should add records but never delete them.

View File

@ -1,9 +1,9 @@
# See https://cloud.google.com/cloud-build/docs/build-config
timeout: 1200s
timeout: 3000s
options:
substitution_option: ALLOW_LOOSE
steps:
- name: "gcr.io/k8s-testimages/gcb-docker-gcloud:v20190906-745fed4"
- name: "gcr.io/k8s-testimages/gcb-docker-gcloud:v20200824-5d057db"
entrypoint: make
env:
- DOCKER_CLI_EXPERIMENTAL=enabled

View File

@ -103,7 +103,7 @@ func init() {
// * Ask the DNS provider for current list of endpoints.
// * Ask the Source for the desired list of endpoints.
// * Take both lists and calculate a Plan to move current towards desired state.
// * Tell the DNS provider to apply the changes calucated by the Plan.
// * Tell the DNS provider to apply the changes calculated by the Plan.
type Controller struct {
Source source.Source
Registry registry.Registry
@ -117,6 +117,8 @@ type Controller struct {
nextRunAt time.Time
// The nextRunAtMux is for atomic updating of nextRunAt
nextRunAtMux sync.Mutex
// DNS record types that will be considered for management
ManagedRecordTypes []string
}
// RunOnce runs a single iteration of a reconciliation loop.
@ -139,12 +141,15 @@ func (c *Controller) RunOnce(ctx context.Context) error {
}
sourceEndpointsTotal.Set(float64(len(endpoints)))
endpoints = c.Registry.AdjustEndpoints(endpoints)
plan := &plan.Plan{
Policies: []plan.Policy{c.Policy},
Current: records,
Desired: endpoints,
DomainFilter: c.DomainFilter,
PropertyComparator: c.Registry.PropertyValuesEqual,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
plan = plan.Calculate()
@ -163,7 +168,7 @@ func (c *Controller) RunOnce(ctx context.Context) error {
// MinInterval is used as window for batching events
const MinInterval = 5 * time.Second
// RunOnceThrottled makes sure execution happens at most once per interval.
// ScheduleRunOnce makes sure execution happens at most once per interval.
func (c *Controller) ScheduleRunOnce(now time.Time) {
c.nextRunAtMux.Lock()
defer c.nextRunAtMux.Unlock()

View File

@ -45,7 +45,7 @@ func (p *mockProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error
return p.RecordsStore, nil
}
// ApplyChanges validates that the passed in changes satisfy the assumtions.
// ApplyChanges validates that the passed in changes satisfy the assumptions.
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")

View File

@ -17,7 +17,7 @@
## Summary
[ExternalDNS](https://github.com/kubernetes-sigs/external-dns) is a project that synchronizes Kubernetes Services, Ingresses and other Kubernetes resources to DNS backends for several DNS providers.
[ExternalDNS](https://github.com/kubernetes-sigs/external-dns) is a project that synchronizes Kubernetes' Services, Ingresses and other Kubernetes resources to DNS backends for several DNS providers.
The projects was started as a Kubernetes Incubator project in February 2017 and being the Kubernetes incubation initiative officially over, the maintainers want to propose the project to be moved to the kubernetes GitHub organization or to kubernetes-sigs, under the sponsorship of sig-network.
@ -33,7 +33,7 @@ When the project was proposed (see the [original discussion](https://github.com/
* Route53-kubernetes - [https://github.com/wearemolecule/route53-kubernetes](https://github.com/wearemolecule/route53-kubernetes)
ExternalDNS goal from the beginning was to provide an officially supported solution to those problems.
ExternalDNS' goal from the beginning was to provide an officially supported solution to those problems.
After two years of development, the project is still in the kubernetes-sigs.
@ -74,7 +74,7 @@ Moving the ExternalDNS project outside of Kubernetes projects would cause:
* Problems (re-)establishing user trust which could eventually lead to fragmentation and duplication.
* It would be hard to establish in which organization the project should be moved to. The most natural would be Zalandos organization, being the company that put most of the work on the project. While it is possible to assume Zalandos commitment to open-source, that would be a strategic mistake for the project community and for the Kubernetes ecosystem due to the obvious lack of neutrality.
* It would be hard to establish in which organization the project should be moved to. The most natural would be Zalando's organization, being the company that put most of the work on the project. While it is possible to assume Zalando's commitment to open-source, that would be a strategic mistake for the project community and for the Kubernetes ecosystem due to the obvious lack of neutrality.
* Lack of resources to test, lack of issue management via automation.
@ -91,7 +91,7 @@ We have evidence that many companies are using ExternalDNS in production, but it
The project was quoted by a number of tutorials on the web, including the [official tutorials from AWS](https://aws.amazon.com/blogs/opensource/unified-service-discovery-ecs-kubernetes/).
ExternalDNS cant be consider to be "done": while the core functionality has been implemented, there is lack of integration testing and structural changes that are needed.
ExternalDNS can't be consider to be "done": while the core functionality has been implemented, there is lack of integration testing and structural changes that are needed.
Those are identified in the project roadmap, which is roughly made of the following items:
@ -129,7 +129,7 @@ The high number of providers contributed to the project pose a maintainability c
The project uses the free quota of TravisCI to run tests for the project.
The release pipeline for the project is currently fully owned by Zalando. It runs on the internal system of the company (closed source) which external maintainers/users cant access and that pushes images to the publicly accessible docker registry available at the URL `registry.opensource.zalan.do`.
The release pipeline for the project is currently fully owned by Zalando. It runs on the internal system of the company (closed source) which external maintainers/users can't access and that pushes images to the publicly accessible docker registry available at the URL `registry.opensource.zalan.do`.
The docker registry service is provided as best effort with no sort of SLA and the maintainers team openly suggests the users to build and maintain their own docker image based on the provided Dockerfiles.
@ -149,8 +149,8 @@ The following are risks that were identified:
We think that the following actions will constitute appropriate mitigations:
* Decoupling the providers via an API will allow us to resolve the problem of the providers. Being the project already more than 2 years old and given that there are 18 providers implemented, we possess enough informations to define an API that we can be stable in a short timeframe. Once this is stable, the problem of testing the providers can be deferred to be a providers responsibility. This will also reduce the scope of External DNS core code, which means that there will be no need for a further increase of the maintaining team.
* Decoupling the providers via an API will allow us to resolve the problem of the providers. Being the project already more than 2 years old and given that there are 18 providers implemented, we possess enough information to define an API that we can be stable in a short timeframe. Once this is stable, the problem of testing the providers can be deferred to be a provider's responsibility. This will also reduce the scope of External DNS core code, which means that there will be no need for a further increase of the maintaining team.
* We added integration testing for the main cloud providers to the roadmap for the 1.0 release to make sure that we cover the mostly used ones. We believe that this item should be tackled independently from the decoupling of providers as it would be capable of generating value independently from the result of the decoupling efforts.
* With the move to the Kubernetes incubation, we hope that we will be able to access the testing resources of the Kubernetes project. In this way, we hope to decouple the project from the dependency on Zalandos internal CI tool. This will help open up the possibility to increase the visibility on the project from external contributors, which currently would be blocked by the lack of access to the software used for the whole release pipeline.
* With the move to the Kubernetes incubation, we hope that we will be able to access the testing resources of the Kubernetes project. In this way, we hope to decouple the project from the dependency on Zalando's internal CI tool. This will help open up the possibility to increase the visibility on the project from external contributors, which currently would be blocked by the lack of access to the software used for the whole release pipeline.

View File

@ -1,14 +1,14 @@
# Quick Start
- [Git](https://git-scm.com/downloads)
- [Go 1.14+](https://golang.org/dl/)
- [Go 1.15+](https://golang.org/dl/)
- [Go modules](https://github.com/golang/go/wiki/Modules)
- [golangci-lint](https://github.com/golangci/golangci-lint)
- [Docker](https://docs.docker.com/install/)
- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl)
Compile and run locally against a remote k8s cluster.
```
```shell
git clone https://github.com/kubernetes-sigs/external-dns.git && cd external-dns
make build
# login to remote k8s cluster
@ -16,14 +16,14 @@ make build
```
Run linting, unit tests, and coverage report.
```
```shell
make lint
make test
make cover-html
```
Build container image.
```
```shell
make build.docker
```
@ -31,7 +31,7 @@ make build.docker
ExternalDNS's sources of DNS records live in package [source](../../source). They implement the `Source` interface that has a single method `Endpoints` which returns the represented source's objects converted to `Endpoints`. Endpoints are just a tuple of DNS name and target where target can be an IP or another hostname.
For example, the `ServiceSource` returns all Services converted to `Endpoints` where the hostname is the value of the `external-dns.alpha.kubernetes.io/hostname` annotation and the target is the IP of the load balancer.
For example, the `ServiceSource` returns all Services converted to `Endpoints` where the hostname is the value of the `external-dns.alpha.kubernetes.io/hostname` annotation and the target is the IP of the load balancer or where the hostname is the value of the `external-dns.alpha.kubernetes.io/internal-hostname` annotation and the target is the IP of the service CLusterIP.
This list of endpoints is passed to the [Plan](../../plan) which determines the difference between the current DNS records and the desired list of `Endpoints`.
@ -48,3 +48,13 @@ You can pick which `Source` and `Provider` to use at runtime via the `--source`
A typical way to start on, e.g. a CoreDNS provider, would be to add a `coredns.go` to the providers package and implement the interface methods. Then you would have to register your provider under a name in `main.go`, e.g. `coredns`, and would be able to trigger it's functions via setting `--provider=coredns`.
Note, how your provider doesn't need to know anything about where the DNS records come from, nor does it have to figure out the difference between the current and the desired state, it merely executes the actions calculated by the plan.
# Running GitHub Actions locally
You can also extend the CI workflow which is currently implemented as GitHub Action within the [workflow](https://github.com/kubernetes-sigs/external-dns/tree/HEAD/.github/workflows) folder.
In order to test your changes before committing you can leverage [act](https://github.com/nektos/act) to run the GitHub Action locally.
Follow the installation instructions in the nektos/act [README.md](https://github.com/nektos/act/blob/master/README.md).
Afterwards just run `act` within the root folder of the project.
For further usage of `act` refer to its documentation.

View File

@ -25,7 +25,7 @@ All sources live in package `source`.
* `ContourIngressRouteSource`: collects all Contour IngressRoutes and returns them as Endpoint objects. The desired DNS name corresponds to the `virtualhost.fqdn` listed within the spec of each IngressRoute object.
* `FakeSource`: returns a random list of Endpoints for the purpose of testing providers without having access to a Kubernetes cluster.
* `ConnectorSource`: returns a list of Endpoint objects which are served by a tcp server configured through `connector-source-server` flag.
* `CRDSource`: returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to [CRD source](../crd-source.md) documentation.
* `CRDSource`: returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to [CRD source](crd-source.md) documentation.
* `EmptySource`: returns an empty list of Endpoint objects for the purpose of testing and cleaning out entries.
### Providers

View File

@ -57,7 +57,7 @@ Services exposed via `type=LoadBalancer`, `type=ExternalName` and for the hostna
There are three sources of information for ExternalDNS to decide on DNS name. ExternalDNS will pick one in order as listed below:
1. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the corresponding value.
1. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the loadbalancer IP, it also will look for the annotation `external-dns.alpha.kubernetes.io/internal-hostname` on the service and use the service IP.
2. If compatibility mode is enabled (e.g. `--compatibility={mate,molecule}` flag), External DNS will parse annotations used by Zalando/Mate, wearemolecule/route53-kubernetes. Compatibility mode with Kops DNS Controller is planned to be added in the future.
@ -275,6 +275,16 @@ and one with `--annotation-filter=kubernetes.io/ingress.class=nginx-external`.
Beware when using multiple sources, e.g. `--source=service --source=ingress`, `--annotation-filter` will filter every given source objects.
If you need to filter only one specific source you have to run a separated external dns service containing only the wanted `--source` and `--annotation-filter`.
### How do I specify that I want the DNS record to point to either the Node's public or private IP when it has both?
If your Nodes have both public and private IP addresses, you might want to write DNS records with one or the other.
For example, you may want to write a DNS record in a private zone that resolves to your Nodes' private IPs so that traffic never leaves your private network.
To accomplish this, set this annotation on your service: `external-dns.alpha.kubernetes.io/access=private`
Conversely, to force the public IP: `external-dns.alpha.kubernetes.io/access=public`
If this annotation is not set, and the node has both public and private IP addresses, then the public IP will be used by default.
### 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-sigs/external-dns/pull/524#issue-181256561
@ -296,6 +306,13 @@ As tags, you use the external-dns release of choice(i.e. `v0.7.3`). A `latest` t
If you wish to build your own image, you can use the provided [Dockerfile](../Dockerfile) as a starting point.
### Which architectures are supported?
From `v0.7.5` on we support `amd64`, `arm32v7` and `arm64v8`. This means that you can run ExternalDNS on a Kubernetes cluster backed by Rasperry Pis or on ARM instances in the cloud as well as more traditional machines backed by `amd64` compatible CPUs.
### Which operating systems are supported?
At the time of writing we only support GNU/linux and we have no plans of supporting Windows or other operating systems.
### Why am I seeing time out errors even though I have connectivity to my cluster?

View File

@ -2,7 +2,7 @@
## Background
[Project proposal](https://groups.google.com/forum/#!searchin/kubernetes-dev/external$20dns%7Csort:relevance/kubernetes-dev/2wGQUB0fUuE/9OXz01i2BgAJ)
[Project proposal](https://groups.google.com/forum/#!searching/kubernetes-dev/external$20dns%7Csort:relevance/kubernetes-dev/2wGQUB0fUuE/9OXz01i2BgAJ)
[Initial discussion](https://docs.google.com/document/d/1ML_q3OppUtQKXan6Q42xIq2jelSoIivuXI8zExbc6ec/edit#heading=h.1pgkuagjhm4p)

View File

@ -2,10 +2,22 @@
## Release cycle
Currently we don't release regularly. Whenever we think it makes sense to release a new version we do it. You might want to ask in our Slack channel [external-dns](https://kubernetes.slack.com/archives/C771MKDKQ) when the next release will come out.
Currently we don't release regularly. Whenever we think it makes sense to release a new version we do it, but we aim to do a new release every month. You might want to ask in our Slack channel [external-dns](https://kubernetes.slack.com/archives/C771MKDKQ) when the next release will come out.
## How to release a new image
When releasing a new version of external-dns, we tag the branch by using **vX.Y.Z** as tag name. To prepare the release, a PR is created to update the **CHANGELOG.md** with the latest commits since last tag, as well as the [kustomization configuration](../kustomization/external-dns-deployment.yaml) to utilize the new tag. As soon as PR is merged into the default branch, the Kubernetes based CI/CD system [Prow](https://prow.k8s.io/?repo=kubernetes-sigs%2Fexternal-dns) will trigger a job to push the image. We're using the Google Container Registry for our Docker images.
### Prerequisite
The job itself looks at external-dns `cloudbuild.yaml` and executes the given steps. Inside it runs `make release.staging` which is basically only a `docker build` and `docker push`. The docker image is pushed `gcr.io/k8s-staging-external-dns/external-dns`, which is only a staging image and shouldn't be used. Promoting the official image we need to create another PR in [k8s.io](https://github.com/kubernetes/k8s.io), e.g. https://github.com/kubernetes/k8s.io/pull/540 by taking the current staging image using sha256.
We use https://github.com/cli/cli to automate the release process. Please install it according to the [official documentation](https://github.com/cli/cli#installation).
You must be an official maintainer of the project to be able to do a release.
### Steps
- Run `scripts/releaser.sh` to create a new GitHub release.
- The step above will trigger the Kubernetes based CI/CD system [Prow](https://prow.k8s.io/?repo=kubernetes-sigs%2Fexternal-dns). Verify that a new image was built and uploaded to `gcr.io/k8s-staging-external-dns/external-dns`.
- Create a PR in the [k8s.io repo](https://github.com/kubernetes/k8s.io) (see https://github.com/kubernetes/k8s.io/pull/540 for reference) by taking the current staging image using the sha256 digest. Once the PR is merged, the image will be live with the corresponding tag specified in the PR.
- Verify that the image is pullable with the given tag (i.e. `v0.7.5`).
- Branch out from the default branch and run `scripts/kustomize-version-udapter.sh` to update the image tag used in the kustomization.yaml.
- Create a PR with the kustomize change.
- Once the PR is merged, all is done :-)

View File

@ -0,0 +1,251 @@
# Setting up External-DNS for Services on Akamai Edge DNS
## Prerequisites
Akamai Edge DNS (formally known as Fast DNS) provider support was first released in External-DNS v0.5.18
### Zones
External-DNS manages service endpoints in existing DNS zones. The Akamai provider does not add, remove or configure new zones in anyway. Edge DNS zones can be created and managed thru the [Akamai Control Center](https://control.akamai.com) or [Akamai DevOps Tools](https://developer.akamai.com/devops), [Akamai CLI](https://developer.akamai.com/cli) and [Akamai Terraform Provider](https://developer.akamai.com/tools/integrations/terraform)
### Akamai Edge DNS Authentication
The Akamai Edge DNS provider requires valid Akamai Edgegrid API authentication credentials to access zones and manage associated DNS records.
Credentials can be provided to the provider either directly by key or indirectly via a file. The Akamai credential keys and mappings to the Akamai provider utilizing different presentation methods are:
| Edgegrid Auth Key | External-DNS Cmd Line Key | Environment/ConfigMap Key | Description |
| ----------------- | ------------------------- | ------------------------- | ----------- |
| host | akamai-serviceconsumerdomain | EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN | Akamai Edgegrid API server |
| access_token | akamai-access-token | EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN | Akamai Edgegrid API access token |
| client_token | akamai-client-token | EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN |Akamai Edgegrid API client token |
| client-secret | akamai-client-secret | EXTERNAL_DNS_AKAMAI_CLIENT_SECRET |Akamai Edgegrid API client secret |
In addition to specifying auth credentials individually, the credentials may be referenced indirectly by using the Akamai Edgegrid .edgerc file convention.
| External-DNS Cmd Line | Environment/ConfigMap | Description |
| --------------------- | --------------------- | ----------- |
| akamai-edgerc-path | EXTERNAL_DNS_AKAMAI_EDGERC_PATH | Accessible path to Edgegrid credentials file, e.g /home/test/.edgerc |
| akamai-edgerc-section | EXTERNAL_DNS_AKAMAI_EDGERC_SECTION | Section in Edgegrid credentials file containing credentials |
Note: akamai-edgerc-path and akamai-edgerc-section are present in External-DNS versions after v0.7.5
[Akamai API Authentication](https://developer.akamai.com/getting-started/edgegrid) provides an overview and further information pertaining to the generation of auth credentials for API base applications and tools.
The following example defines and references a Kubernetes ConfigMap secret, applied by referencing the secret and its keys in the env section of the deployment.
## Deploy External-DNS
An operational External-DNS deployment consists of an External-DNS container and service. The following sections demonstrate the ConfigMap objects that would make up an example functional external DNS kubernetes configuration utilizing NGINX as the exposed service.
Connect your `kubectl` client to the cluster with which you want to test External-DNS, and then apply one of the following manifest files for deployment:
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.5
args:
- --source=service # or ingress or both
- --provider=akamai
- --domain-filter=example.com
# zone-id-filter may be specified as well to filter on contract ID
- --registry=txt
- --txt-owner-id={{ owner-id-for-this-external-dns }}
env:
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
- name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.5
args:
- --source=service # or ingress or both
- --provider=akamai
- --domain-filter=example.com
# zone-id-filter may be specified as well to filter on contract ID
- --registry=txt
- --txt-owner-id={{ owner-id-for-this-external-dns }}
env:
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
- name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
```
Create the deployment for External-DNS:
```
$ kubectl create -f externaldns.yaml
```
## Deploying an Nginx Service
Create a service file called 'nginx.yaml' with the following contents:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx.example.com
external-dns.alpha.kubernetes.io/ttl: "600" #optional
spec:
selector:
app: nginx
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
```
Create the deployment, service and ingress object:
```
$ kubectl create -f nginx.yaml
```
## Verify Akamai Edge DNS Records
It is recommended to wait 3-5 minutes before validating the records to allow the record changes to propagate to all the Akamai name servers worldwide.
The records can be validated using the [Akamai Control Center](http://control.akamai.com) or by executing a dig, nslookup or similar DNS command.
## Cleanup
Once you successfully configure and verify record management via External-DNS, you can delete the tutorial's example:
```
$ kubectl delete -f nginx.yaml
$ kubectl delete -f externaldns.yaml
```
## Additional Information
* The Akamai provider allows the administrative user to filter zones by both name (domain-filter) and contract Id (zone-id-filter). The Edge DNS API will return a '500 Internal Error' if an invalid contract Id is provided.
* The provider will substitute any embedded quotes in TXT records with `` ` `` (back tick) when writing the records to the API.

View File

@ -1,192 +0,0 @@
# Setting up Akamai FastDNS
## Prerequisites
Akamai FastDNS provider support was added via [this PR](https://github.com/kubernetes-sigs/external-dns/pull/1384), thus you need to use a release where this pr is included. This should be at least v0.5.18
The Akamai FastDNS provider expects that your zones, you wish to add records to, already exists
and are configured correctly. It does not add, remove or configure new zones in anyway.
To do this please refer to the [FastDNS documentation](https://learn.akamai.com/en-us/products/web_performance/fast_dns.html).
Additional data you will have to provide:
* Service Consumer Domain
* Access token
* Client token
* Client Secret
Make these available to external DNS somehow. In the following example a secret is used by referencing the secret and its keys in the env section of the deployment.
If you happen to have questions regarding authentication, please refer to the [API Client Authentication documentation](https://developer.akamai.com/legacy/introduction/Client_Auth.html)
## Deployment
Deploying external DNS for Akamai is actually nearly identical to deploying
it for other providers. This is what a sample `deployment.yaml` looks like:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/version: v0.6.0
spec:
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: external-dns
template:
metadata:
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/version: v0.6.0
spec:
# Only use if you're also using RBAC
# serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
args:
- --source=ingress # or service or both
- --provider=akamai
- --registry=txt
- --txt-owner-id={{ owner-id-for-this-external-dns }}
env:
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
- name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
```
## RBAC
If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/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
```
## Verify ExternalDNS works (Ingress example)
Create an ingress resource manifest file.
> For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object.
```yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: foo
annotations:
kubernetes.io/ingress.class: "nginx" # use the one that corresponds to your ingress controller.
spec:
rules:
- host: foo.bar.com
http:
paths:
- backend:
serviceName: foo
servicePort: 80
```
## Verify ExternalDNS works (Service example)
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
spec:
type: LoadBalancer
ports:
- port: 80
name: http
targetPort: 80
selector:
app: nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
name: http
```
**Important!**: Don't run dig, nslookup or similar immediately. You'll get hit by [negative DNS caching](https://tools.ietf.org/html/rfc2308), which is hard to flush.
Wait about 30s-1m (interval for external-dns to kick in)

View File

@ -407,6 +407,46 @@ For any given DNS name, only **one** of the following routing policies can be us
* `external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code`
* Multi-value answer:`external-dns.alpha.kubernetes.io/aws-multi-value-answer`
## Associating DNS records with healthchecks
You can configure Route53 to associate DNS records with healthchecks for automated DNS failover using
`external-dns.alpha.kubernetes.io/aws-health-check-id: <health-check-id>` annotation.
Note: ExternalDNS does not support creating healthchecks, and assumes that `<health-check-id>` already exists.
## Govcloud caveats
Due to the special nature with how Route53 runs in Govcloud, there are a few tweaks in the deployment settings.
* An Environment variable with name of AWS_REGION set to either us-gov-west-1 or us-gov-east-1 is required. Otherwise it tries to lookup a region that does not exist in Govcloud and it errors out.
```yaml
env:
- name: AWS_REGION
value: us-gov-west-1
```
* Route53 in Govcloud does not allow aliases. Therefore, container args must be set so that it uses CNAMES and a txt-prefix must be set to something. Otherwise, it will try to create a TXT record with the same value than the CNAME itself, which is not allowed.
```yaml
args:
- --aws-prefer-cname
- --txt-prefix={{ YOUR_PREFIX }}
```
* The first two changes are needed if you use Route53 in Govcloud, which only supports private zones. There are also no cross account IAM whatsoever between Govcloud and commerical AWS accounts. If services and ingresses need to make Route 53 entries to an public zone in a commerical account, you will have set env variables of AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with a key and secret to the commerical account that has the sufficient rights.
```yaml
env:
- name: AWS_ACCESS_KEY_ID
value: XXXXXXXXX
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ YOUR_SECRET_NAME }}
key: {{ YOUR_SECRET_KEY }}
```
## Clean up
Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly.

View File

@ -1,12 +1,12 @@
# Set up ExternalDNS for Azure Private DNS
This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS.
This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS.
It comprises of the following steps:
1) Install NGINX Ingress Controller
2) Provision Azure Private DNS
3) Configure service principal for managing the zone
4) Deploy ExternalDNS
4) Deploy ExternalDNS
Everything will be deployed on Kubernetes.
Therefore, please see the subsequent prerequisites.
@ -26,25 +26,27 @@ $ helm install stable/nginx-ingress \
--name nginx-ingress \
--set controller.publishService.enabled=true
```
The parameter `controller.publishService.enabled` needs to be set to `true.`
The parameter `controller.publishService.enabled` needs to be set to `true.`
It will make the ingress controller update the endpoint records of ingress-resources to contain the external-ip of the loadbalancer serving the ingress-controller.
This is crucial as ExternalDNS reads those endpoints records when creating DNS-Records from ingress-resources.
In the subsequent parameter we will make use of this. If you don't want to work with ingress-resources in your later use, you can leave the parameter out.
Verify the correct propagation of the loadbalancer's ip by listing the ingresses.
```
$ kubectl get ingress
```
The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information.
```
NAME HOSTS ADDRESS PORTS AGE
nginx1 sample1.aks.com 52.167.195.110 80 6d22h
nginx2 sample2.aks.com 52.167.195.110 80 6d21h
```
If you do not want to deploy the ingress controller with Helm, ensure to pass the following cmdline-flags to it through the mechanism of your choice:
```
@ -144,6 +146,8 @@ This is per default done through the file `~/.kube/config`.
For general background information on this see [kubernetes-docs](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/).
Azure-CLI features functionality for automatically maintaining this file for AKS-Clusters. See [Azure-Docs](https://docs.microsoft.com/de-de/cli/azure/aks?view=azure-cli-latest#az-aks-get-credentials).
Follow the steps for [azure-dns provider](./azure.md#creating-configuration-file) to create a configuration file.
Then apply one of the following manifests depending on whether you use RBAC or not.
The credentials of the service principal are provided to ExternalDNS as environment-variables.
@ -175,13 +179,14 @@ spec:
- --provider=azure-private-dns
- --azure-resource-group=externaldns
- --azure-subscription-id=<use the id of your subscription>
env:
- name: AZURE_TENANT_ID
value: "<use the tenantId discovered during creation of service principal>"
- name: AZURE_CLIENT_ID
value: "<use the aadClientId discovered during creation of service principal>"
- name: AZURE_CLIENT_SECRET
value: "<use the aadClientSecret discovered during creation of service principal>"
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
```
### Manifest (for clusters with RBAC enabled, cluster access)
@ -200,7 +205,7 @@ rules:
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
@ -245,13 +250,14 @@ spec:
- --provider=azure-private-dns
- --azure-resource-group=externaldns
- --azure-subscription-id=<use the id of your subscription>
env:
- name: AZURE_TENANT_ID
value: "<use the tenantId discovered during creation of service principal>"
- name: AZURE_CLIENT_ID
value: "<use the aadClientId discovered during creation of service principal>"
- name: AZURE_CLIENT_SECRET
value: "<use the aadClientSecret discovered during creation of service principal>"
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
```
### Manifest (for clusters with RBAC enabled, namespace access)
@ -315,13 +321,14 @@ spec:
- --provider=azure-private-dns
- --azure-resource-group=externaldns
- --azure-subscription-id=<use the id of your subscription>
env:
- name: AZURE_TENANT_ID
value: "<use the tenantId discovered during creation of service principal>"
- name: AZURE_CLIENT_ID
value: "<use the aadClientId discovered during creation of service principal>"
- name: AZURE_CLIENT_SECRET
value: "<use the aadClientSecret discovered during creation of service principal>"
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
```
Create the deployment for ExternalDNS:

View File

@ -85,7 +85,7 @@ rules:
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding

View File

@ -1,5 +1,5 @@
# Setting up ExternalDNS for CoreDNS with minikube
This tutorial describes how to setup ExternalDNS for usage within a [minikube](https://github.com/kubernetes/minikube) cluster that makes use of [CoreDNS](https://github.com/coredns/coredns) and [nginx ingress controller](https://github.com/kubernetes/ingress-nginx).
This tutorial describes how to setup ExternalDNS for usage within a [minikube](https://github.com/kubernetes/minikube) cluster that makes use of [CoreDNS](https://github.com/coredns/coredns) and [nginx ingress controller](https://github.com/kubernetes/ingress-nginx).
You need to:
* install CoreDNS with [etcd](https://github.com/etcd-io/etcd) enabled
* install external-dns with coredns as a provider
@ -24,7 +24,7 @@ helm install stable/etcd-operator --name my-etcd-op
```
etcd cluster is installed with example yaml from etcd operator website.
```
kubectl apply -f https://raw.githubusercontent.com/coreos/etcd-operator/HEAD/example/example-etcd-cluster.yaml
kubectl apply -f https://raw.githubusercontent.com/coreos/etcd-operator/HEAD/example/example-etcd-cluster.yaml
```
### Installing CoreDNS
@ -34,7 +34,7 @@ wget https://raw.githubusercontent.com/helm/charts/HEAD/stable/coredns/values.ya
```
You need to edit/patch the file with below diff
```
```diff
diff --git a/values.yaml b/values.yaml
index 964e72b..e2fa934 100644
--- a/values.yaml
@ -68,7 +68,7 @@ index 964e72b..e2fa934 100644
# Complete example with all the options:
# - zones: # the `zones` block can be left out entirely, defaults to "."
```
**Note**:
**Note**:
* IP address of etcd's endpoint should be get from etcd client service. It should be "example-etcd-cluster-client" in this example. This IP address is used through this document for etcd endpoint configuration.
```
$ kubectl get svc example-etcd-cluster-client
@ -228,5 +228,5 @@ $ 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.100.4.143 nginx.example.org +short
10.0.2.15
dnstools#
dnstools#
```

View File

@ -188,7 +188,7 @@ kafka-1.example.org
kafka-2.example.org
```
If you set `--fqdn-template={{name}}.example.org` you can ommit the annotation.
If you set `--fqdn-template={{name}}.example.org` you can omit the annotation.
Generally it is a better approach to use `--fqdn-template={{name}}.example.org`, because then
you would get the service name inside the generated A records:

View File

@ -157,6 +157,7 @@ apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: httpbin-gateway
namespace: istio-system
spec:
selector:
istio: ingressgateway # use Istio default gateway implementation

View File

@ -78,7 +78,7 @@ rules:
See also current RBAC yaml files:
- [kube-ingress-aws-controller](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/ingress-controller/01-rbac.yaml)
- [skipper](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/skipper/rbac.yaml)
- [external-dns](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/external-dns/rbac.yaml)
- [external-dns](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/external-dns/01-rbac.yaml)
[3]: https://opensource.zalando.com/skipper/kubernetes/routegroups/#routegroups
[4]: https://opensource.zalando.com/skipper
@ -269,7 +269,7 @@ status:
```
ExternalDNS will create a A-records `echoserver.example.org`, that
use AWS ALIAS record to automatically maintain IP adresses of the NLB.
use AWS ALIAS record to automatically maintain IP addresses of the NLB.
## RouteGroup (optional)

View File

@ -0,0 +1,23 @@
# Creating NS record with CRD source
You can create NS records with the help of [CRD source](/docs/contributing/crd-source.md)
and `DNSEndpoint` CRD.
Consider the following example
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
name: ns-record
spec:
endpoints:
- dnsName: zone.example.com
recordTTL: 300
recordType: NS
targets:
- ns1.example.com
- ns2.example.com
```
After instantiation of this Custom Resource external-dns will create NS record with the help of configured provider, e.g. `aws`

View File

@ -125,6 +125,9 @@ rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding

View File

@ -155,7 +155,7 @@ $ kubectl get services echo
$ kubectl get endpoints echo
```
Make sure everything looks correct, i.e the service is defined and recieves a
Make sure everything looks correct, i.e the service is defined and receives a
public IP, and that the endpoint also has a pod IP.
Once that's done, wait about 30s-1m (interval for external-dns to kick in), then do:

View File

@ -166,6 +166,7 @@ rules:
- services
- endpoints
- pods
- nodes
verbs:
- get
- watch
@ -289,9 +290,9 @@ You'll want to configure `external-dns` similarly to the following:
```text
...
- --provider=rfc2136
- --rfc2136-host=123.123.123.123
- --rfc2136-host=192.168.0.1
- --rfc2136-port=53
- --rfc2136-zone=your-domain.com
- --rfc2136-zone=k8s.example.org
- --rfc2136-insecure
- --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
...

View File

@ -2,7 +2,7 @@
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Scaleway DNS.
Make sure to use **>=0.7.3** version of ExternalDNS for this tutorial.
Make sure to use **>=0.7.4** version of ExternalDNS for this tutorial.
**Warning**: Scaleway DNS is currently in Public Beta and may not be suited for production usage.
@ -10,13 +10,13 @@ Make sure to use **>=0.7.3** version of ExternalDNS for this tutorial.
In order to use your domain, you need to import it into Scaleway DNS. If it's not already done, you can follow [this documentation](https://www.scaleway.com/en/docs/scaleway-dns/)
Once the domain is imported you can either use the root zone, or create a subzone to use.
Once the domain is imported you can either use the root zone, or create a subzone to use.
In this example we will use `example.com` as an example.
## Creating Scaleway Credentials
To use ExternalDNS with Scaleway DNS, you need to create an API token (composed of the Access Key and the Secret Key).
To use ExternalDNS with Scaleway DNS, you need to create an API token (composed of the Access Key and the Secret Key).
You can either use existing ones or you can create a new token, as explained in [How to generate an API token](https://www.scaleway.com/en/docs/generate-an-api-token/) or directly by going to the [credentials page](https://console.scaleway.com/account/organization/credentials).
Note that you will also need to the Organization ID, which can be retrieve on the same page.
@ -53,7 +53,7 @@ spec:
spec:
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
image: k8s.gcr.io/external-dns/external-dns:v0.7.4
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.
@ -82,8 +82,8 @@ rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
@ -121,7 +121,7 @@ spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
image: k8s.gcr.io/external-dns/external-dns:v0.7.4
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.

View File

@ -263,7 +263,7 @@ $ kubectl create -f external-dns.yaml
```
- Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service.
- Please verify on the [UltraDNS UI](https://portal.ultradns.neustar) that the records have been created under the zone "example.com".
- Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone example.com:
- Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone "example.com":
```console
$ kubectl delete -f apple-banana-echo.yaml
$ kubectl delete -f expose-apple-banana-app.yaml

View File

@ -33,6 +33,8 @@ const (
RecordTypeTXT = "TXT"
// RecordTypeSRV is a RecordType enum value
RecordTypeSRV = "SRV"
// RecordTypeNS is a RecordType enum value
RecordTypeNS = "NS"
)
// TTL is a structure defining the TTL of a DNS record
@ -85,7 +87,7 @@ func (t Targets) Same(o Targets) bool {
return true
}
// IsLess should fulfill the requirement to compare two targets and chosse the 'lesser' one.
// IsLess should fulfill the requirement to compare two targets and choose the 'lesser' one.
// In the past target was a simple string so simple string comparison could be used. Now we define 'less'
// as either being the shorter list of targets or where the first entry is less.
// FIXME We really need to define under which circumstances a list Targets is considered 'less'

22
go.mod
View File

@ -1,16 +1,15 @@
module sigs.k8s.io/external-dns
go 1.14
go 1.15
require (
cloud.google.com/go v0.50.0
git.blindage.org/21h/hcloud-dns v0.0.0-20200525170043-def10a4a28e0
git.blindage.org/21h/hcloud-dns v0.0.0-20200807003420-f768ffe03f8d
github.com/Azure/azure-sdk-for-go v45.1.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.4
github.com/Azure/go-autorest/autorest/adal v0.9.2
github.com/Azure/go-autorest/autorest/azure/auth v0.5.1
github.com/Azure/go-autorest/autorest v0.11.10
github.com/Azure/go-autorest/autorest/adal v0.9.5
github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.0
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
github.com/alecthomas/colour v0.1.0 // indirect
github.com/alecthomas/kingpin v2.2.5+incompatible
@ -19,6 +18,7 @@ require (
github.com/aws/aws-sdk-go v1.31.4
github.com/cloudflare/cloudflare-go v0.10.1
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/datawire/ambassador v1.6.0
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
github.com/digitalocean/godo v1.36.0
github.com/dnsimple/dnsimple-go v0.60.0
@ -46,7 +46,8 @@ require (
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200623155123-84df6c4b5301
github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.5.1
github.com/smartystreets/gunit v1.3.4 // indirect
github.com/stretchr/testify v1.6.1
github.com/terra-farm/udnssdk v1.3.5 // indirect
github.com/transip/gotransip v5.8.2+incompatible
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
@ -54,16 +55,19 @@ require (
github.com/vultr/govultr v0.4.2
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875
go.uber.org/ratelimit v0.1.0
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
golang.org/x/net v0.0.0-20200625001655-4c5254603344
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/tools v0.0.0-20200708003708-134513de8882 // indirect
google.golang.org/api v0.15.0
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
gopkg.in/yaml.v2 v2.2.8
gopkg.in/yaml.v2 v2.3.0
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
istio.io/api v0.0.0-20200529165953-72dad51d4ffc
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751
k8s.io/api v0.18.8
k8s.io/apimachinery v0.18.8
k8s.io/client-go v0.18.8
k8s.io/kubernetes v1.13.0
)
replace (

507
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ kind: Kustomization
images:
- name: k8s.gcr.io/external-dns/external-dns
newTag: v0.7.3
newTag: v0.7.6
resources:
- ./external-dns-deployment.yaml

27
main.go
View File

@ -101,9 +101,11 @@ func main() {
sourceCfg := &source.Config{
Namespace: cfg.Namespace,
AnnotationFilter: cfg.AnnotationFilter,
LabelFilter: cfg.LabelFilter,
FQDNTemplate: cfg.FQDNTemplate,
CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation,
IgnoreIngressTLSSpec: cfg.IgnoreIngressTLSSpec,
Compatibility: cfg.Compatibility,
PublishInternal: cfg.PublishInternal,
PublishHostIP: cfg.PublishHostIP,
@ -142,6 +144,7 @@ func main() {
endpointsSource := source.NewDedupSource(source.NewMultiSource(sources))
domainFilter := endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
zoneNameFilter := endpoint.NewDomainFilter(cfg.ZoneNameFilter)
zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
@ -149,7 +152,7 @@ func main() {
var p provider.Provider
switch cfg.Provider {
case "akamai":
p = akamai.NewAkamaiProvider(
p, err = akamai.NewAkamaiProvider(
akamai.AkamaiConfig{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
@ -157,9 +160,10 @@ func main() {
ClientToken: cfg.AkamaiClientToken,
ClientSecret: cfg.AkamaiClientSecret,
AccessToken: cfg.AkamaiAccessToken,
EdgercPath: cfg.AkamaiEdgercPath,
EdgercSection: cfg.AkamaiEdgercSection,
DryRun: cfg.DryRun,
},
)
}, nil)
case "alibabacloud":
p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
case "aws":
@ -187,9 +191,9 @@ func main() {
}
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun)
case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "azure-private-dns":
p, err = azure.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun)
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "vinyldns":
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "vultr":
@ -319,7 +323,7 @@ func main() {
case "noop":
r, err = registry.NewNoopRegistry(p)
case "txt":
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval)
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement)
case "aws-sd":
r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID)
default:
@ -336,11 +340,12 @@ func main() {
}
ctrl := controller.Controller{
Source: endpointsSource,
Registry: r,
Policy: policy,
Interval: cfg.Interval,
DomainFilter: domainFilter,
Source: endpointsSource,
Registry: r,
Policy: policy,
Interval: cfg.Interval,
DomainFilter: domainFilter,
ManagedRecordTypes: cfg.ManagedDNSRecordTypes,
}
if cfg.Once {

View File

@ -22,6 +22,8 @@ import (
"strconv"
"time"
"sigs.k8s.io/external-dns/endpoint"
"github.com/alecthomas/kingpin"
"github.com/sirupsen/logrus"
@ -47,9 +49,11 @@ type Config struct {
Sources []string
Namespace string
AnnotationFilter string
LabelFilter string
FQDNTemplate string
CombineFQDNAndAnnotation bool
IgnoreHostnameAnnotation bool
IgnoreIngressTLSSpec bool
Compatibility string
PublishInternal bool
PublishHostIP bool
@ -61,6 +65,7 @@ type Config struct {
GoogleBatchChangeInterval time.Duration
DomainFilter []string
ExcludeDomains []string
ZoneNameFilter []string
ZoneIDFilter []string
AlibabaCloudConfigFile string
AlibabaCloudZoneType string
@ -85,6 +90,8 @@ type Config struct {
AkamaiClientToken string
AkamaiClientSecret string
AkamaiAccessToken string
AkamaiEdgercPath string
AkamaiEdgercSection string
InfobloxGridHost string
InfobloxWapiPort int
InfobloxWapiUsername string
@ -122,6 +129,7 @@ type Config struct {
MetricsAddress string
LogLevel string
TXTCacheInterval time.Duration
TXTWildcardReplacement string
ExoscaleEndpoint string
ExoscaleAPIKey string `secure:"yes"`
ExoscaleAPISecret string `secure:"yes"`
@ -146,6 +154,7 @@ type Config struct {
TransIPAccountName string
TransIPPrivateKeyFile string
DigitalOceanAPIPageSize int
ManagedDNSRecordTypes []string
}
var defaultConfig = &Config{
@ -157,9 +166,11 @@ var defaultConfig = &Config{
Sources: nil,
Namespace: "",
AnnotationFilter: "",
LabelFilter: "",
FQDNTemplate: "",
CombineFQDNAndAnnotation: false,
IgnoreHostnameAnnotation: false,
IgnoreIngressTLSSpec: false,
Compatibility: "",
PublishInternal: false,
PublishHostIP: false,
@ -191,6 +202,8 @@ var defaultConfig = &Config{
AkamaiClientToken: "",
AkamaiClientSecret: "",
AkamaiAccessToken: "",
AkamaiEdgercSection: "",
AkamaiEdgercPath: "",
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
@ -215,6 +228,7 @@ var defaultConfig = &Config{
TXTPrefix: "",
TXTSuffix: "",
TXTCacheInterval: 0,
TXTWildcardReplacement: "",
Interval: time.Minute,
Once: false,
DryRun: false,
@ -245,6 +259,7 @@ var defaultConfig = &Config{
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
DigitalOceanAPIPageSize: 50,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
// NewConfig returns new Config object
@ -305,13 +320,15 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
// 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, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, crd, empty, skipper-routegroup,openshift-route)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route")
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host")
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("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently only supported by source CRD").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter)
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("ignore-ingress-tls-spec", "Ignore tls spec section in ingresses resources, applicable only for ingress sources (optional, default: false)").BoolVar(&cfg.IgnoreIngressTLSSpec)
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)
@ -320,11 +337,13 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion)
app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind)
app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter)
app.Flag("managed-record-types", "Comma separated list of record types to manage (default: A, CNAME) (supported records: CNAME, A, NS").Default("A", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes)
// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns")
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
app.Flag("zone-name-filter", "Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneNameFilter)
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("google-batch-change-size", "When using the Google provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.GoogleBatchChangeSize)).IntVar(&cfg.GoogleBatchChangeSize)
@ -347,10 +366,12 @@ func (cfg *Config) ParseFlags(args []string) error {
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("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)
app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain)
app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
app.Flag("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret)
app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain)
app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
app.Flag("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret)
app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
app.Flag("akamai-edgerc-path", "When using the Akamai provider, specify the .edgerc file path. Path must be reachable form invocation environment. (required when --provider=akamai and *-token, secret serviceconsumerdomain not specified)").Default(defaultConfig.AkamaiEdgercPath).StringVar(&cfg.AkamaiEdgercPath)
app.Flag("akamai-edgerc-section", "When using the Akamai provider, specify the .edgerc file path (Optional when edgerc-path is specified)").Default(defaultConfig.AkamaiEdgercSection).StringVar(&cfg.AkamaiEdgercSection)
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)
@ -361,7 +382,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("infoblox-max-results", "Add _max_results as query parameter to the URL on all API requests. The default is 0 which means _max_results is not set and the default of the server is used.").Default(strconv.Itoa(defaultConfig.InfobloxMaxResults)).IntVar(&cfg.InfobloxMaxResults)
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-password", "When using the Dyn provider, specify the password").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("oci-compartment-ocid", "When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records. Required when using OCI IAM instance principal authentication.").StringVar(&cfg.OCICompartmentOCID)
@ -410,6 +431,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix)
app.Flag("txt-wildcard-replacement", "When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional)").Default(defaultConfig.TXTWildcardReplacement).StringVar(&cfg.TXTWildcardReplacement)
// Flags related to the main control loop
app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)

View File

@ -22,6 +22,8 @@ import (
"testing"
"time"
"sigs.k8s.io/external-dns/endpoint"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -44,6 +46,7 @@ var (
GoogleBatchChangeInterval: time.Second,
DomainFilter: []string{""},
ExcludeDomains: []string{""},
ZoneNameFilter: []string{""},
ZoneIDFilter: []string{""},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
@ -65,6 +68,8 @@ var (
AkamaiClientToken: "",
AkamaiClientSecret: "",
AkamaiAccessToken: "",
AkamaiEdgercPath: "",
AkamaiEdgercSection: "",
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
@ -101,6 +106,7 @@ var (
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
DigitalOceanAPIPageSize: 50,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
overriddenConfig = &Config{
@ -112,6 +118,7 @@ var (
Sources: []string{"service", "ingress", "connector"},
Namespace: "namespace",
IgnoreHostnameAnnotation: true,
IgnoreIngressTLSSpec: true,
FQDNTemplate: "{{.Name}}.service.example.com",
Compatibility: "mate",
Provider: "google",
@ -120,6 +127,7 @@ var (
GoogleBatchChangeInterval: time.Second * 2,
DomainFilter: []string{"example.org", "company.com"},
ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"},
ZoneNameFilter: []string{"yapi.example.org", "yapi.company.com"},
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "private",
@ -141,6 +149,8 @@ var (
AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46",
AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46",
AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46",
AkamaiEdgercPath: "/home/test/.edgerc",
AkamaiEdgercSection: "default",
InfobloxGridHost: "127.0.0.1",
InfobloxWapiPort: 8443,
InfobloxWapiUsername: "infoblox",
@ -183,6 +193,7 @@ var (
TransIPAccountName: "transip",
TransIPPrivateKeyFile: "/path/to/transip.key",
DigitalOceanAPIPageSize: 100,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
)
@ -216,6 +227,7 @@ func TestParseFlags(t *testing.T) {
"--namespace=namespace",
"--fqdn-template={{.Name}}.service.example.com",
"--ignore-hostname-annotation",
"--ignore-ingress-tls-spec",
"--compatibility=mate",
"--provider=google",
"--google-project=project",
@ -231,6 +243,8 @@ func TestParseFlags(t *testing.T) {
"--akamai-client-token=o184671d5307a388180fbf7f11dbdf46",
"--akamai-client-secret=o184671d5307a388180fbf7f11dbdf46",
"--akamai-access-token=o184671d5307a388180fbf7f11dbdf46",
"--akamai-edgerc-path=/home/test/.edgerc",
"--akamai-edgerc-section=default",
"--infoblox-grid-host=127.0.0.1",
"--infoblox-wapi-port=8443",
"--infoblox-wapi-username=infoblox",
@ -254,6 +268,8 @@ func TestParseFlags(t *testing.T) {
"--domain-filter=company.com",
"--exclude-domains=xapi.example.org",
"--exclude-domains=xapi.company.com",
"--zone-name-filter=yapi.example.org",
"--zone-name-filter=yapi.company.com",
"--zone-id-filter=/hostedzone/ZTST1",
"--zone-id-filter=/hostedzone/ZTST2",
"--aws-zone-type=private",
@ -306,6 +322,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_NAMESPACE": "namespace",
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
"EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1",
"EXTERNAL_DNS_IGNORE_INGRESS_TLS_SPEC": "1",
"EXTERNAL_DNS_COMPATIBILITY": "mate",
"EXTERNAL_DNS_PROVIDER": "google",
"EXTERNAL_DNS_GOOGLE_PROJECT": "project",
@ -321,6 +338,8 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_EDGERC_PATH": "/home/test/.edgerc",
"EXTERNAL_DNS_AKAMAI_EDGERC_SECTION": "default",
"EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1",
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",
@ -342,6 +361,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_TLS_CA": "/path/to/ca.crt",
"EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem",
"EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem",
"EXTERNAL_DNS_ZONE_NAME_FILTER": "yapi.example.org\nyapi.company.com",
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo",

View File

@ -45,16 +45,16 @@ func ValidateConfig(cfg *externaldns.Config) error {
// Akamai provider specific validations
if cfg.Provider == "akamai" {
if cfg.AkamaiServiceConsumerDomain == "" {
if cfg.AkamaiServiceConsumerDomain == "" && cfg.AkamaiEdgercPath != "" {
return errors.New("no Akamai ServiceConsumerDomain specified")
}
if cfg.AkamaiClientToken == "" {
if cfg.AkamaiClientToken == "" && cfg.AkamaiEdgercPath != "" {
return errors.New("no Akamai client token specified")
}
if cfg.AkamaiClientSecret == "" {
if cfg.AkamaiClientSecret == "" && cfg.AkamaiEdgercPath != "" {
return errors.New("no Akamai client secret specified")
}
if cfg.AkamaiAccessToken == "" {
if cfg.AkamaiAccessToken == "" && cfg.AkamaiEdgercPath != "" {
return errors.New("no Akamai access token specified")
}
}

View File

@ -43,6 +43,8 @@ type Plan struct {
DomainFilter endpoint.DomainFilter
// Property comparator compares custom properties of providers
PropertyComparator PropertyComparator
// DNS record types that will be considered for management
ManagedRecords []string
}
// Changes holds lists of actions to be executed by dns providers
@ -119,10 +121,10 @@ func (t planTable) addCandidate(e *endpoint.Endpoint) {
func (p *Plan) Calculate() *Plan {
t := newPlanTable()
for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter) {
for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter, p.ManagedRecords) {
t.addCurrent(current)
}
for _, desired := range filterRecordsForPlan(p.Desired, p.DomainFilter) {
for _, desired := range filterRecordsForPlan(p.Desired, p.DomainFilter, p.ManagedRecords) {
t.addCandidate(desired)
}
@ -155,9 +157,10 @@ func (p *Plan) Calculate() *Plan {
}
plan := &Plan{
Current: p.Current,
Desired: p.Desired,
Changes: changes,
Current: p.Current,
Desired: p.Desired,
Changes: changes,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
return plan
@ -194,12 +197,6 @@ func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint)
}
if current.ProviderSpecific != nil {
for _, c := range current.ProviderSpecific {
// don't consider target health when detecting changes
// see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954
if c.Name == "aws/evaluate-target-health" {
continue
}
if d, ok := desiredProperties[c.Name]; ok {
if p.PropertyComparator != nil {
if !p.PropertyComparator(c.Name, c.Value, d.Value) {
@ -230,7 +227,7 @@ func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint)
// Per RFC 1034, CNAME records conflict with all other records - it is the
// only record with this property. The behavior of the planner may need to be
// made more sophisticated to codify this.
func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.DomainFilter) []*endpoint.Endpoint {
func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.DomainFilter, managedRecords []string) []*endpoint.Endpoint {
filtered := []*endpoint.Endpoint{}
for _, record := range records {
@ -238,14 +235,8 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Do
if !domainFilter.Match(record.DNSName) {
continue
}
// Explicitly specify which records we want to use for planning.
// TODO: Add AAAA records as well when they are supported.
switch record.RecordType {
case endpoint.RecordTypeA, endpoint.RecordTypeCNAME:
if isManagedRecord(record.RecordType, managedRecords) {
filtered = append(filtered, record)
default:
continue
}
}
@ -286,3 +277,12 @@ func CompareBoolean(defaultValue bool, name, current, previous string) bool {
return v1 == v2
}
func isManagedRecord(record string, managedRecords []string) bool {
for _, r := range managedRecords {
if record == r {
return true
}
}
return false
}

View File

@ -205,9 +205,10 @@ func (suite *PlanTestSuite) TestSyncFirstRound() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -226,9 +227,10 @@ func (suite *PlanTestSuite) TestSyncSecondRound() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -247,9 +249,10 @@ func (suite *PlanTestSuite) TestSyncSecondRoundMigration() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -268,9 +271,10 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithTTLChange() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -289,9 +293,10 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -316,6 +321,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefaultFalse(
PropertyComparator: func(name, previous, current string) bool {
return CompareBoolean(false, name, previous, current)
},
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -368,9 +374,10 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -410,9 +417,10 @@ func (suite *PlanTestSuite) TestDifferentTypes() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -431,9 +439,10 @@ func (suite *PlanTestSuite) TestIgnoreTXT() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -452,9 +461,10 @@ func (suite *PlanTestSuite) TestRemoveEndpoint() {
expectedDelete := []*endpoint.Endpoint{suite.bar192A}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -473,9 +483,10 @@ func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&UpsertOnlyPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&UpsertOnlyPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -495,9 +506,10 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() {
expectedDelete := []*endpoint.Endpoint{suite.bar192A}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -518,9 +530,10 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() {
expectedDelete := []*endpoint.Endpoint{suite.bar192A}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -540,9 +553,10 @@ func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier()
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -562,9 +576,10 @@ func (suite *PlanTestSuite) TestSetIdentifierUpdateCreatesAndDeletes() {
expectedDelete := []*endpoint.Endpoint{suite.multiple2}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -584,10 +599,11 @@ func (suite *PlanTestSuite) TestDomainFiltersInitial() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -607,10 +623,11 @@ func (suite *PlanTestSuite) TestDomainFiltersUpdate() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -686,3 +703,133 @@ func TestNormalizeDNSName(t *testing.T) {
assert.Equal(t, r.expect, gotName)
}
}
func TestShouldUpdateProviderSpecific(tt *testing.T) {
comparator := func(name, previous, current string) bool {
return previous == current
}
for _, test := range []struct {
name string
current *endpoint.Endpoint
desired *endpoint.Endpoint
propertyComparator func(name, previous, current string) bool
shouldUpdate bool
}{
{
name: "skip AWS target health",
current: &endpoint.Endpoint{
DNSName: "foo.com",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
},
},
desired: &endpoint.Endpoint{
DNSName: "bar.com",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
},
},
propertyComparator: comparator,
shouldUpdate: false,
},
{
name: "custom property unchanged",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
propertyComparator: comparator,
shouldUpdate: false,
},
{
name: "custom property value changed",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "false"},
},
},
propertyComparator: comparator,
shouldUpdate: true,
},
{
name: "custom property key changed",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "new/property", Value: "true"},
},
},
propertyComparator: comparator,
shouldUpdate: true,
},
{
name: "desired has same key and value as current but not comparator is set",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
shouldUpdate: false,
},
{
name: "desired has same key and different value as current but not comparator is set",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "false"},
},
},
shouldUpdate: true,
},
{
name: "desired has different key from current but not comparator is set",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "new/property", Value: "true"},
},
},
shouldUpdate: true,
},
} {
tt.Run(test.name, func(t *testing.T) {
plan := &Plan{
Current: []*endpoint.Endpoint{test.current},
Desired: []*endpoint.Endpoint{test.desired},
PropertyComparator: test.propertyComparator,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
b := plan.shouldUpdateProviderSpecific(test.desired, test.current)
assert.Equal(t, test.shouldUpdate, b)
})
}
}

View File

@ -17,15 +17,13 @@ limitations under the License.
package akamai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
c "github.com/akamai/AkamaiOPEN-edgegrid-golang/client-v1"
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
log "github.com/sirupsen/logrus"
@ -34,22 +32,23 @@ import (
"sigs.k8s.io/external-dns/provider"
)
type akamaiClient interface {
NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error)
Do(config edgegrid.Config, req *http.Request) (*http.Response, error)
const (
// Default Record TTL
edgeDNSRecordTTL = 600
maxUint = ^uint(0)
maxInt = int(maxUint >> 1)
)
// edgeDNSClient is a proxy interface of the Akamai edgegrid configdns-v2 package that can be stubbed for testing.
type AkamaiDNSService interface {
ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error)
GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error)
GetRecord(zone string, name string, recordtype string) (*dns.RecordBody, error)
DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error
UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error
CreateRecordsets(recordsets *dns.Recordsets, zone string, recLock bool) error
}
type akamaiOpenClient struct{}
func (*akamaiOpenClient) NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error) {
return c.NewRequest(config, method, path, body)
}
func (*akamaiOpenClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) {
return c.Do(config, req)
}
// AkamaiConfig clarifies the method signature
type AkamaiConfig struct {
DomainFilter endpoint.DomainFilter
ZoneIDFilter provider.ZoneIDFilter
@ -57,17 +56,25 @@ type AkamaiConfig struct {
ClientToken string
ClientSecret string
AccessToken string
EdgercPath string
EdgercSection string
MaxBody int
AccountKey string
DryRun bool
}
// AkamaiProvider implements the DNS provider for Akamai.
type AkamaiProvider struct {
provider.BaseProvider
// Edgedns zones to filter on
domainFilter endpoint.DomainFilter
// Contract Ids to filter on
zoneIDFilter provider.ZoneIDFilter
config edgegrid.Config
dryRun bool
client akamaiClient
// Edgegrid library configuration
config *edgegrid.Config
dryRun bool
// Defines client. Allows for mocking.
client AkamaiDNSService
}
type akamaiZones struct {
@ -79,84 +86,124 @@ type akamaiZone struct {
Zone string `json:"zone"`
}
type akamaiRecordsets struct {
Recordsets []akamaiRecord `json:"recordsets"`
}
type akamaiRecord struct {
Name string `json:"name"`
Type string `json:"type"`
TTL int64 `json:"ttl"`
Rdata []interface{} `json:"rdata"`
}
// NewAkamaiProvider initializes a new Akamai DNS based Provider.
func NewAkamaiProvider(akamaiConfig AkamaiConfig) *AkamaiProvider {
edgeGridConfig := edgegrid.Config{
Host: akamaiConfig.ServiceConsumerDomain,
ClientToken: akamaiConfig.ClientToken,
ClientSecret: akamaiConfig.ClientSecret,
AccessToken: akamaiConfig.AccessToken,
MaxBody: 1024,
HeaderToSign: []string{
"X-External-DNS",
},
Debug: false,
func NewAkamaiProvider(akamaiConfig AkamaiConfig, akaService AkamaiDNSService) (provider.Provider, error) {
var edgeGridConfig edgegrid.Config
/*
log.Debugf("Host: %s", akamaiConfig.ServiceConsumerDomain)
log.Debugf("ClientToken: %s", akamaiConfig.ClientToken)
log.Debugf("ClientSecret: %s", akamaiConfig.ClientSecret)
log.Debugf("AccessToken: %s", akamaiConfig.AccessToken)
log.Debugf("EdgePath: %s", akamaiConfig.EdgercPath)
log.Debugf("EdgeSection: %s", akamaiConfig.EdgercSection)
*/
// environment overrides edgerc file but config needs to be complete
if akamaiConfig.ServiceConsumerDomain == "" || akamaiConfig.ClientToken == "" || akamaiConfig.ClientSecret == "" || akamaiConfig.AccessToken == "" {
// Kubernetes config incomplete or non existent. Can't mix and match.
// Look for Akamai environment or .edgerd creds
var err error
edgeGridConfig, err = edgegrid.Init(akamaiConfig.EdgercPath, akamaiConfig.EdgercSection) // use default .edgerc location and section
if err != nil {
log.Errorf("Edgegrid Init Failed")
return &AkamaiProvider{}, err // return empty provider for backward compatibility
}
edgeGridConfig.HeaderToSign = append(edgeGridConfig.HeaderToSign, "X-External-DNS")
} else {
// Use external-dns config
edgeGridConfig = edgegrid.Config{
Host: akamaiConfig.ServiceConsumerDomain,
ClientToken: akamaiConfig.ClientToken,
ClientSecret: akamaiConfig.ClientSecret,
AccessToken: akamaiConfig.AccessToken,
MaxBody: 131072, // same default val as used by Edgegrid
HeaderToSign: []string{
"X-External-DNS",
},
Debug: false,
}
// Check for edgegrid overrides
if envval, ok := os.LookupEnv("AKAMAI_MAX_BODY"); ok {
if i, err := strconv.Atoi(envval); err == nil {
edgeGridConfig.MaxBody = i
log.Debugf("Edgegrid maxbody set to %s", envval)
}
}
if envval, ok := os.LookupEnv("AKAMAI_ACCOUNT_KEY"); ok {
edgeGridConfig.AccountKey = envval
log.Debugf("Edgegrid applying account key %s", envval)
}
if envval, ok := os.LookupEnv("AKAMAI_DEBUG"); ok {
if dbgval, err := strconv.ParseBool(envval); err == nil {
edgeGridConfig.Debug = dbgval
log.Debugf("Edgegrid debug set to %s", envval)
}
}
}
provider := &AkamaiProvider{
domainFilter: akamaiConfig.DomainFilter,
zoneIDFilter: akamaiConfig.ZoneIDFilter,
config: edgeGridConfig,
config: &edgeGridConfig,
dryRun: akamaiConfig.DryRun,
client: &akamaiOpenClient{},
}
return provider
if akaService != nil {
log.Debugf("Using STUB")
provider.client = akaService
} else {
provider.client = provider
}
// Init library for direct endpoint calls
dns.Init(edgeGridConfig)
return provider, nil
}
func (p *AkamaiProvider) request(method, path string, body io.Reader) (*http.Response, error) {
req, err := p.client.NewRequest(p.config, method, fmt.Sprintf("https://%s/%s", p.config.Host, path), body)
if err != nil {
log.Errorf("Akamai client failed to prepare the request")
return nil, err
}
resp, err := p.client.Do(p.config, req)
if err != nil {
log.Errorf("Akamai client failed to do the request")
return nil, err
}
if !c.IsSuccess(resp) {
return nil, c.NewAPIError(resp)
}
return resp, err
func (p AkamaiProvider) ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) {
return dns.ListZones(queryArgs)
}
//Look here for endpoint documentation -> https://developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html#getzones
func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) {
log.Debugf("Trying to fetch zones from Akamai")
resp, err := p.request("GET", "config-dns/v2/zones?showAll=true&types=primary%2Csecondary", nil)
func (p AkamaiProvider) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) {
return dns.GetRecordsets(zone, queryArgs)
}
func (p AkamaiProvider) CreateRecordsets(recordsets *dns.Recordsets, zone string, reclock bool) error {
return recordsets.Save(zone, reclock)
}
func (p AkamaiProvider) GetRecord(zone string, name string, recordtype string) (*dns.RecordBody, error) {
return dns.GetRecord(zone, name, recordtype)
}
func (p AkamaiProvider) DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error {
return record.Delete(zone, recLock)
}
func (p AkamaiProvider) UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error {
return record.Update(zone, recLock)
}
// Fetch zones using Edgegrid DNS v2 API
func (p AkamaiProvider) fetchZones() (akamaiZones, error) {
filteredZones := akamaiZones{Zones: make([]akamaiZone, 0)}
queryArgs := dns.ZoneListQueryArgs{Types: "primary", ShowAll: true}
// filter based on contractIds
if len(p.zoneIDFilter.ZoneIDs) > 0 {
queryArgs.ContractIds = strings.Join(p.zoneIDFilter.ZoneIDs, ",")
}
resp, err := p.client.ListZones(queryArgs) // retrieve all primary zones filtered by contract ids
if err != nil {
log.Errorf("Failed to fetch zones from Akamai")
return zones, err
return filteredZones, err
}
err = json.NewDecoder(resp.Body).Decode(&zones)
if err != nil {
log.Errorf("Could not decode json response from Akamai on zone request")
return zones, err
}
defer resp.Body.Close()
filteredZones := akamaiZones{}
for _, zone := range zones.Zones {
if !p.zoneIDFilter.Match(zone.ContractID) {
log.Debugf("Skipping zone: '%s' with ZoneID: '%s', it does not match against ZoneID filters", zone.Zone, zone.ContractID)
continue
for _, zone := range resp.Zones {
if p.domainFilter.Match(zone.Zone) || !p.domainFilter.IsConfigured() {
filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractId, Zone: zone.Zone})
log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractId)
}
filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractID, Zone: zone.Zone})
log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractID)
}
lenFilteredZones := len(filteredZones.Zones)
if lenFilteredZones == 0 {
@ -168,53 +215,39 @@ func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) {
return filteredZones, nil
}
//Look here for endpoint documentation -> https://developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html#getzonerecordsets
func (p *AkamaiProvider) fetchRecordSet(zone string) (recordSet akamaiRecordsets, err error) {
log.Debugf("Trying to fetch endpoints for zone: '%s' from Akamai", zone)
resp, err := p.request("GET", "config-dns/v2/zones/"+zone+"/recordsets?showAll=true&types=A%2CTXT%2CCNAME", nil)
if err != nil {
log.Errorf("Failed to fetch records from Akamai for zone: '%s'", zone)
return recordSet, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&recordSet)
if err != nil {
log.Errorf("Could not decode json response from Akamai for zone: '%s' on request", zone)
return recordSet, err
}
return recordSet, nil
}
//Records returns the list of records in a given zone.
func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) {
zones, err := p.fetchZones()
func (p AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) {
zones, err := p.fetchZones() // returns a filtered set of zones
if err != nil {
log.Warnf("No zones to fetch endpoints from!")
log.Warnf("Failed to identify target zones! Error: %s", err.Error())
return endpoints, err
}
for _, zone := range zones.Zones {
records, err := p.fetchRecordSet(zone.Zone)
recordsets, err := p.client.GetRecordsets(zone.Zone, dns.RecordsetQueryArgs{})
if err != nil {
log.Warnf("No recordsets could be fetched for zone: '%s'!", zone.Zone)
log.Errorf("Recordsets retrieval for zone: '%s' failed! %s", zone.Zone, err.Error())
continue
}
if len(recordsets.Recordsets) == 0 {
log.Warnf("Zone %s contains no recordsets", zone.Zone)
}
for _, record := range records.Recordsets {
rdata := make([]string, len(record.Rdata))
for i, v := range record.Rdata {
rdata[i] = v.(string)
}
if !p.domainFilter.Match(record.Name) {
log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", record.Name, record.Type)
for _, recordset := range recordsets.Recordsets {
if !provider.SupportedRecordType(recordset.Type) {
log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s'. Record type not supported.", recordset.Name, recordset.Type)
continue
}
endpoints = append(endpoints, endpoint.NewEndpoint(record.Name, record.Type, rdata...))
log.Debugf("Fetched endpoint DNSName: '%s' RecordType: '%s' Rdata: '%s')", record.Name, record.Type, rdata)
if !p.domainFilter.Match(recordset.Name) {
log.Debugf("Skipping endpoint. Record name %s doesn't match containing zone %s.", recordset.Name, zone)
continue
}
var temp interface{} = int64(recordset.TTL)
var ttl endpoint.TTL = endpoint.TTL(temp.(int64))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(recordset.Name,
recordset.Type,
ttl,
trimTxtRdata(recordset.Rdata, recordset.Type)...))
log.Debugf("Fetched endpoint DNSName: '%s' RecordType: '%s' Rdata: '%s')", recordset.Name, recordset.Type, recordset.Rdata)
}
}
lenEndpoints := len(endpoints)
@ -222,161 +255,237 @@ func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoin
log.Warnf("No endpoints could be fetched")
} else {
log.Debugf("Fetched '%d' endpoints from Akamai", lenEndpoints)
log.Debugf("Endpoints [%v]", endpoints)
}
return endpoints, nil
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
func (p AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zoneNameIDMapper := provider.ZoneIDName{}
zones, err := p.fetchZones()
if err != nil {
log.Warnf("No zones to fetch endpoints from!")
return nil
log.Errorf("Failed to fetch zones from Akamai")
return err
}
for _, z := range zones.Zones {
zoneNameIDMapper[z.Zone] = z.Zone
}
log.Debugf("Processing zones: [%v]", zoneNameIDMapper)
_, cf := p.createRecords(zoneNameIDMapper, changes.Create)
if !p.dryRun {
if len(cf) > 0 {
log.Warnf("Not all desired endpoints could be created, retrying next iteration")
for _, f := range cf {
log.Warnf("Not created was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
// Create recordsets
log.Debugf("Create Changes requested [%v]", changes.Create)
if err := p.createRecordsets(zoneNameIDMapper, changes.Create); err != nil {
return err
}
// Delete recordsets
log.Debugf("Delete Changes requested [%v]", changes.Delete)
if err := p.deleteRecordsets(zoneNameIDMapper, changes.Delete); err != nil {
return err
}
// Update recordsets
log.Debugf("Update Changes requested [%v]", changes.UpdateNew)
if err := p.updateNewRecordsets(zoneNameIDMapper, changes.UpdateNew); err != nil {
return err
}
// Check that all old endpoints were accounted for
revRecs := changes.Delete
revRecs = append(revRecs, changes.UpdateNew...)
for _, rec := range changes.UpdateOld {
found := false
for _, r := range revRecs {
if rec.DNSName == r.DNSName {
found = true
break
}
}
}
_, df := p.deleteRecords(zoneNameIDMapper, changes.Delete)
if !p.dryRun {
if len(df) > 0 {
log.Warnf("Not all endpoints that require deletion could be deleted, retrying next iteration")
for _, f := range df {
log.Warnf("Not deleted was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
}
}
}
_, uf := p.updateNewRecords(zoneNameIDMapper, changes.UpdateNew)
if !p.dryRun {
if len(uf) > 0 {
log.Warnf("Not all endpoints that require updating could be updated, retrying next iteration")
for _, f := range uf {
log.Warnf("Not updated was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
}
}
}
for _, uold := range changes.UpdateOld {
if !p.dryRun {
log.Debugf("UpdateOld (ignored) for DNSName: '%s' RecordType: '%s'", uold.DNSName, uold.RecordType)
if !found {
log.Warnf("UpdateOld endpoint '%s' is not accounted for in UpdateNew|Delete endpoint list", rec.DNSName)
}
}
return nil
}
func (p *AkamaiProvider) newAkamaiRecord(dnsName, recordType string, targets ...string) *akamaiRecord {
cleanTargets := make([]interface{}, len(targets))
for idx, target := range targets {
cleanTargets[idx] = strings.TrimSuffix(target, ".")
}
return &akamaiRecord{
// Create DNS Recordset
func newAkamaiRecordset(dnsName, recordType string, ttl int, targets []string) dns.Recordset {
return dns.Recordset{
Name: strings.TrimSuffix(dnsName, "."),
Rdata: cleanTargets,
Rdata: targets,
Type: recordType,
TTL: 300,
TTL: ttl,
}
}
func (p *AkamaiProvider) createRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (created []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
for _, endpoint := range endpoints {
if !p.domainFilter.Match(endpoint.DNSName) {
log.Debugf("Skipping creation at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
// cleanTargets preps recordset rdata if necessary for EdgeDNS
func cleanTargets(rtype string, targets ...string) []string {
log.Debugf("Targets to clean: [%v]", targets)
if rtype == "CNAME" || rtype == "SRV" {
for idx, target := range targets {
targets[idx] = strings.TrimSuffix(target, ".")
}
} else if rtype == "TXT" {
for idx, target := range targets {
log.Debugf("TXT data to clean: [%s]", target)
// need to embed text data in quotes. Make sure not piling on
target = strings.Trim(target, "\"")
// bug in DNS API with embedded quotes.
if strings.Contains(target, "owner") && strings.Contains(target, "\"") {
target = strings.ReplaceAll(target, "\"", "`")
}
targets[idx] = "\"" + target + "\""
}
}
log.Debugf("Clean targets: [%v]", targets)
return targets
}
// trimTxtRdata removes surrounding quotes for received TXT rdata
func trimTxtRdata(rdata []string, rtype string) []string {
if rtype == "TXT" {
for idx, d := range rdata {
if strings.Contains(d, "`") {
rdata[idx] = strings.ReplaceAll(d, "`", "\"")
}
}
}
log.Debugf("Trimmed data: [%v]", rdata)
return rdata
}
func ttlAsInt(src endpoint.TTL) int {
var temp interface{} = int64(src)
var temp64 = temp.(int64)
var ttl int = edgeDNSRecordTTL
if temp64 > 0 && temp64 <= int64(maxInt) {
ttl = int(temp64)
}
return ttl
}
// Create Endpoint Recordsets
func (p AkamaiProvider) createRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
if len(endpoints) == 0 {
log.Info("No endpoints to create")
return nil
}
endpointsByZone := edgeChangesByZone(zoneNameIDMapper, endpoints)
// create all recordsets by zone
for zone, endpoints := range endpointsByZone {
recordsets := &dns.Recordsets{Recordsets: make([]dns.Recordset, 0)}
for _, endpoint := range endpoints {
newrec := newAkamaiRecordset(endpoint.DNSName,
endpoint.RecordType,
ttlAsInt(endpoint.RecordTTL),
cleanTargets(endpoint.RecordType, endpoint.Targets...))
logfields := log.Fields{
"record": newrec.Name,
"type": newrec.Type,
"ttl": newrec.TTL,
"target": fmt.Sprintf("%v", newrec.Rdata),
"zone": zone,
}
log.WithFields(logfields).Info("Creating recordsets")
recordsets.Recordsets = append(recordsets.Recordsets, newrec)
}
if p.dryRun {
continue
}
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
akamaiRecord := p.newAkamaiRecord(endpoint.DNSName, endpoint.RecordType, endpoint.Targets...)
body, _ := json.MarshalIndent(akamaiRecord, "", " ")
log.Infof("Create new Endpoint at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
if p.dryRun {
continue
}
_, err := p.request("POST", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body))
if err != nil {
log.Errorf("Failed to create Akamai endpoint DNSName: '%s' RecordType: '%s' for zone: '%s'", endpoint.DNSName, endpoint.RecordType, zoneName)
failed = append(failed, endpoint)
continue
}
created = append(created, endpoint)
} else {
log.Warnf("No matching zone for endpoint addition DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
failed = append(failed, endpoint)
// Create recordsets all at once
err := p.client.CreateRecordsets(recordsets, zone, true)
if err != nil {
log.Errorf("Failed to create endpoints for DNS zone %s. Error: %s", zone, err.Error())
return err
}
}
return created, failed
return nil
}
func (p *AkamaiProvider) deleteRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (deleted []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
func (p AkamaiProvider) deleteRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
for _, endpoint := range endpoints {
if !p.domainFilter.Match(endpoint.DNSName) {
log.Debugf("Skipping deletion at Akamai of endpoint: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName)
if zoneName == "" {
log.Debugf("Skipping Akamai Edge DNS endpoint deletion: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
continue
}
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
log.Infof("Deletion at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
log.Infof("Akamai Edge DNS recordset deletion- Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
if p.dryRun {
continue
}
_, err := p.request("DELETE", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, nil)
if err != nil {
log.Errorf("Failed to delete Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName)
failed = append(failed, endpoint)
continue
}
deleted = append(deleted, endpoint)
} else {
log.Warnf("No matching zone for endpoint deletion DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
failed = append(failed, endpoint)
}
}
return deleted, failed
}
func (p *AkamaiProvider) updateNewRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (updated []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
for _, endpoint := range endpoints {
if !p.domainFilter.Match(endpoint.DNSName) {
log.Debugf("Skipping update at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
if p.dryRun {
continue
}
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
akamaiRecord := p.newAkamaiRecord(endpoint.DNSName, endpoint.RecordType, endpoint.Targets...)
body, _ := json.MarshalIndent(akamaiRecord, "", " ")
log.Infof("Updating endpoint at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
if p.dryRun {
continue
recName := strings.TrimSuffix(endpoint.DNSName, ".")
rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType)
if err != nil {
if _, ok := err.(*dns.RecordError); !ok {
return fmt.Errorf("endpoint deletion. record validation failed. error: %s", err.Error())
}
_, err := p.request("PUT", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body))
if err != nil {
log.Errorf("Failed to update Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName)
failed = append(failed, endpoint)
continue
}
updated = append(updated, endpoint)
} else {
log.Warnf("No matching zone for endpoint update DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
failed = append(failed, endpoint)
log.Infof("Endpoint deletion. Record doesn't exist. Name: %s, Type: %s", recName, endpoint.RecordType)
continue
}
if err := p.client.DeleteRecord(rec, zoneName, true); err != nil {
log.Errorf("edge dns recordset deletion failed. error: %s", err.Error())
return err
}
}
return updated, failed
return nil
}
// Update endpoint recordsets
func (p AkamaiProvider) updateNewRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
for _, endpoint := range endpoints {
zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName)
if zoneName == "" {
log.Debugf("Skipping Akamai Edge DNS endpoint update: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
continue
}
log.Infof("Akamai Edge DNS recordset update - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
if p.dryRun {
continue
}
recName := strings.TrimSuffix(endpoint.DNSName, ".")
rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType)
if err != nil {
log.Errorf("Endpoint update. Record validation failed. Error: %s", err.Error())
return err
}
rec.TTL = ttlAsInt(endpoint.RecordTTL)
rec.Target = cleanTargets(endpoint.RecordType, endpoint.Targets...)
if err := p.client.UpdateRecord(rec, zoneName, true); err != nil {
log.Errorf("Akamai Edge DNS recordset update failed. Error: %s", err.Error())
return err
}
}
return nil
}
// edgeChangesByZone separates a multi-zone change into a single change per zone.
func edgeChangesByZone(zoneMap provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]*endpoint.Endpoint {
createsByZone := make(map[string][]*endpoint.Endpoint, len(zoneMap))
for _, z := range zoneMap {
createsByZone[z] = make([]*endpoint.Endpoint, 0)
}
for _, ep := range endpoints {
zone, _ := zoneMap.FindZone(ep.DNSName)
if zone != "" {
createsByZone[zone] = append(createsByZone[zone], ep)
continue
}
log.Debugf("Skipping Akamai Edge DNS creation of endpoint: '%s' type: '%s', it does not match against Domain filters", ep.DNSName, ep.RecordType)
}
return createsByZone
}

View File

@ -17,148 +17,206 @@ limitations under the License.
package akamai
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
log "github.com/sirupsen/logrus"
"testing"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
type mockAkamaiClient struct {
mock.Mock
type edgednsStubData struct {
objType string // zone, record, recordsets
output []interface{}
updateRecords []interface{}
createRecords []interface{}
}
func (m *mockAkamaiClient) NewRequest(config edgegrid.Config, met, p string, b io.Reader) (*http.Request, error) {
switch {
case met == "GET":
switch {
case strings.HasPrefix(p, "https:///config-dns/v2/zones?"):
b = bytes.NewReader([]byte("{\"zones\":[{\"contractId\":\"Test\",\"zone\":\"example.com\"},{\"contractId\":\"Exclude-Me\",\"zone\":\"exclude.me\"}]}"))
case strings.HasPrefix(p, "https:///config-dns/v2/zones/example.com/"):
b = bytes.NewReader([]byte("{\"recordsets\":[{\"name\":\"www.example.com\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"10.0.0.2\",\"10.0.0.3\"]},{\"name\":\"www.example.com\",\"type\":\"TXT\",\"ttl\":300,\"rdata\":[\"heritage=external-dns,external-dns/owner=default\"]}]}"))
case strings.HasPrefix(p, "https:///config-dns/v2/zones/exclude.me/"):
b = bytes.NewReader([]byte("{\"recordsets\":[{\"name\":\"www.exclude.me\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"192.168.0.1\",\"192.168.0.2\"]}]}"))
}
case met == "DELETE":
b = bytes.NewReader([]byte("{\"title\": \"Success\", \"status\": 200, \"detail\": \"Record deleted\", \"requestId\": \"4321\"}"))
case met == "ERROR":
b = bytes.NewReader([]byte("{\"status\": 404 }"))
}
req := httptest.NewRequest(met, p, b)
return req, nil
type edgednsStub struct {
stubData map[string]edgednsStubData
}
func (m *mockAkamaiClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) {
handler := func(w http.ResponseWriter, r *http.Request) (isError bool) {
b, _ := ioutil.ReadAll(r.Body)
io.WriteString(w, string(b))
return string(b) == "{\"status\": 404 }"
func newStub() *edgednsStub {
return &edgednsStub{
stubData: make(map[string]edgednsStubData),
}
w := httptest.NewRecorder()
err := handler(w, req)
resp := w.Result()
}
if err == true {
resp.StatusCode = 400
func createAkamaiStubProvider(stub *edgednsStub, domfilter endpoint.DomainFilter, idfilter provider.ZoneIDFilter) (*AkamaiProvider, error) {
akamaiConfig := AkamaiConfig{
DomainFilter: domfilter,
ZoneIDFilter: idfilter,
ServiceConsumerDomain: "testzone.com",
ClientToken: "test_token",
ClientSecret: "test_client_secret",
AccessToken: "test_access_token",
}
prov, err := NewAkamaiProvider(akamaiConfig, stub)
aprov := prov.(*AkamaiProvider)
return aprov, err
}
func (r *edgednsStub) createStubDataEntry(objtype string) {
log.Debugf("Creating stub data entry")
if _, exists := r.stubData[objtype]; !exists {
r.stubData[objtype] = edgednsStubData{objType: objtype}
}
return
}
func (r *edgednsStub) setOutput(objtype string, output []interface{}) {
log.Debugf("Setting output to %v", output)
r.createStubDataEntry(objtype)
stubdata := r.stubData[objtype]
stubdata.output = output
r.stubData[objtype] = stubdata
return
}
func (r *edgednsStub) setUpdateRecords(objtype string, records []interface{}) {
log.Debugf("Setting updaterecords to %v", records)
r.createStubDataEntry(objtype)
stubdata := r.stubData[objtype]
stubdata.updateRecords = records
r.stubData[objtype] = stubdata
return
}
func (r *edgednsStub) setCreateRecords(objtype string, records []interface{}) {
log.Debugf("Setting createrecords to %v", records)
r.createStubDataEntry(objtype)
stubdata := r.stubData[objtype]
stubdata.createRecords = records
r.stubData[objtype] = stubdata
return
}
func (r *edgednsStub) ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) {
log.Debugf("Entering ListZones")
// Ignore Metadata`
resp := &dns.ZoneListResponse{}
zones := make([]*dns.ZoneResponse, 0)
for _, zname := range r.stubData["zone"].output {
log.Debugf("Processing output: %v", zname)
zn := &dns.ZoneResponse{Zone: zname.(string), ContractId: "contract"}
log.Debugf("Created Zone Object: %v", zn)
zones = append(zones, zn)
}
resp.Zones = zones
return resp, nil
}
func (r *edgednsStub) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) {
log.Debugf("Entering GetRecordsets")
// Ignore Metadata`
resp := &dns.RecordSetResponse{}
sets := make([]dns.Recordset, 0)
for _, rec := range r.stubData["recordset"].output {
rset := rec.(dns.Recordset)
sets = append(sets, rset)
}
resp.Recordsets = sets
return resp, nil
}
func TestRequestError(t *testing.T) {
config := AkamaiConfig{}
func (r *edgednsStub) CreateRecordsets(recordsets *dns.Recordsets, zone string, reclock bool) error {
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
m := "ERROR"
p := ""
b := ""
x, err := c.request(m, p, bytes.NewReader([]byte(b)))
assert.Nil(t, x)
assert.NotNil(t, err)
return nil
}
func TestFetchZonesZoneIDFilter(t *testing.T) {
config := AkamaiConfig{
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Test"}),
}
func (r *edgednsStub) GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error) {
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
resp := &dns.RecordBody{}
return resp, nil
}
func (r *edgednsStub) DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error {
return nil
}
func (r *edgednsStub) UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error {
return nil
}
// Test FetchZones
func TestFetchZonesZoneIDFilter(t *testing.T) {
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.NewZoneIDFilter([]string{"Test"})
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"test1.testzone.com", "test2.testzone.com"})
x, _ := c.fetchZones()
y, _ := json.Marshal(x)
if assert.NotNil(t, y) {
assert.Equal(t, "{\"zones\":[{\"contractId\":\"Test\",\"zone\":\"example.com\"}]}", string(y))
assert.Equal(t, "{\"zones\":[{\"contractId\":\"contract\",\"zone\":\"test1.testzone.com\"},{\"contractId\":\"contract\",\"zone\":\"test2.testzone.com\"}]}", string(y))
}
}
func TestFetchZonesEmpty(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"Nonexistent"}),
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"Nonexistent"})
idfilter := provider.NewZoneIDFilter([]string{"Nonexistent"})
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{})
x, _ := c.fetchZones()
y, _ := json.Marshal(x)
if assert.NotNil(t, y) {
assert.Equal(t, "{\"zones\":null}", string(y))
}
}
func TestFetchRecordset1(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
x, _ := c.fetchRecordSet("example.com")
y, _ := json.Marshal(x)
if assert.NotNil(t, y) {
assert.Equal(t, "{\"recordsets\":[{\"name\":\"www.example.com\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"10.0.0.2\",\"10.0.0.3\"]},{\"name\":\"www.example.com\",\"type\":\"TXT\",\"ttl\":300,\"rdata\":[\"heritage=external-dns,external-dns/owner=default\"]}]}", string(y))
}
}
func TestFetchRecordset2(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
x, _ := c.fetchRecordSet("exclude.me")
y, _ := json.Marshal(x)
if assert.NotNil(t, y) {
assert.Equal(t, "{\"recordsets\":[{\"name\":\"www.exclude.me\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"192.168.0.1\",\"192.168.0.2\"]}]}", string(y))
assert.Equal(t, "{\"zones\":[]}", string(y))
}
}
// TestAkamaiRecords tests record endpoint
func TestAkamaiRecords(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"test1.testzone.com"})
recordsets := make([]interface{}, 0)
recordsets = append(recordsets, dns.Recordset{
Name: "www.example.com",
Type: endpoint.RecordTypeA,
Rdata: []string{"10.0.0.2", "10.0.0.3"},
})
recordsets = append(recordsets, dns.Recordset{
Name: "www.example.com",
Type: endpoint.RecordTypeTXT,
Rdata: []string{"heritage=external-dns,external-dns/owner=default"},
})
recordsets = append(recordsets, dns.Recordset{
Name: "www.exclude.me",
Type: endpoint.RecordTypeA,
Rdata: []string{"192.168.0.1", "192.168.0.2"},
})
stub.setOutput("recordset", recordsets)
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
@ -171,28 +229,40 @@ func TestAkamaiRecords(t *testing.T) {
}
func TestAkamaiRecordsEmpty(t *testing.T) {
config := AkamaiConfig{
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.NewZoneIDFilter([]string{"Nonexistent"})
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"test1.testzone.com"})
recordsets := make([]interface{}, 0)
stub.setOutput("recordset", recordsets)
x, _ := c.Records(context.Background())
assert.Nil(t, x)
}
func TestAkamaiRecordsFilters(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"www.exclude.me"}),
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Exclude-Me"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"www.exclude.me"})
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"www.exclude.me"})
recordsets := make([]interface{}, 0)
recordsets = append(recordsets, dns.Recordset{
Name: "www.example.com",
Type: endpoint.RecordTypeA,
Rdata: []string{"10.0.0.2", "10.0.0.3"},
})
recordsets = append(recordsets, dns.Recordset{
Name: "www.exclude.me",
Type: endpoint.RecordTypeA,
Rdata: []string{"192.168.0.1", "192.168.0.2"},
})
stub.setOutput("recordset", recordsets)
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "192.168.0.1", "192.168.0.2"))
@ -202,32 +272,32 @@ func TestAkamaiRecordsFilters(t *testing.T) {
}
}
// TestCreateRecords tests create function
// (p AkamaiProvider) createRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error
func TestCreateRecords(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
x, _ := c.createRecords(zoneNameIDMapper, endpoints)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.createRecordsets(zoneNameIDMapper, endpoints)
assert.Nil(t, err)
}
func TestCreateRecordsDomainFilter(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
@ -235,38 +305,36 @@ func TestCreateRecordsDomainFilter(t *testing.T) {
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
x, _ := c.createRecords(zoneNameIDMapper, exclude)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.createRecordsets(zoneNameIDMapper, exclude)
assert.Nil(t, err)
}
// TestDeleteRecords validate delete
func TestDeleteRecords(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
x, _ := c.deleteRecords(zoneNameIDMapper, endpoints)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.deleteRecordsets(zoneNameIDMapper, endpoints)
assert.Nil(t, err)
}
//
func TestDeleteRecordsDomainFilter(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
@ -274,38 +342,36 @@ func TestDeleteRecordsDomainFilter(t *testing.T) {
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
x, _ := c.deleteRecords(zoneNameIDMapper, exclude)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.deleteRecordsets(zoneNameIDMapper, exclude)
assert.Nil(t, err)
}
// Test record update func
func TestUpdateRecords(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
x, _ := c.updateNewRecords(zoneNameIDMapper, endpoints)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.updateNewRecordsets(zoneNameIDMapper, endpoints)
assert.Nil(t, err)
}
//
func TestUpdateRecordsDomainFilter(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
@ -313,19 +379,19 @@ func TestUpdateRecordsDomainFilter(t *testing.T) {
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
x, _ := c.updateNewRecords(zoneNameIDMapper, exclude)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.updateNewRecordsets(zoneNameIDMapper, exclude)
assert.Nil(t, err)
}
func TestAkamaiApplyChanges(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"example.com"})
changes := &plan.Changes{}
changes.Create = []*endpoint.Endpoint{
{DNSName: "www.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}, RecordTTL: 300},

View File

@ -50,6 +50,7 @@ const (
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
providerSpecificHealthCheckID = "aws/health-check-id"
)
var (
@ -79,6 +80,7 @@ var (
"us-gov-west-1.elb.amazonaws.com": "Z33AYJ8TM3BH4J",
"us-gov-east-1.elb.amazonaws.com": "Z166TLBEWOO7G0",
"me-south-1.elb.amazonaws.com": "ZS929ML54UICD",
"af-south-1.elb.amazonaws.com": "Z268VQBMOI5EKX",
// Network Load Balancers
"elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP",
"elb.us-east-1.amazonaws.com": "Z26RNL4JYFTOTI",
@ -102,6 +104,7 @@ var (
"elb.us-gov-west-1.amazonaws.com": "ZMG1MZ2THAWF1",
"elb.us-gov-east-1.amazonaws.com": "Z1ZSMQQ6Q24QQ8",
"elb.me-south-1.amazonaws.com": "Z3QSRYVP46NYYV",
"elb.af-south-1.amazonaws.com": "Z203XCE67M25HM",
// Global Accelerator
"awsglobalaccelerator.com": "Z2BJ6XQ5FK7U4H",
}
@ -202,6 +205,13 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
return provider, nil
}
func (p *AWSProvider) PropertyValuesEqual(name string, previous string, current string) bool {
if name == "aws/evaluate-target-health" {
return true
}
return p.BaseProvider.PropertyValuesEqual(name, previous, current)
}
// Zones returns the list of hosted zones.
func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone, error) {
if p.zonesCache.zones != nil && time.Since(p.zonesCache.age) < p.zonesCache.duration {
@ -349,6 +359,11 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos
// one of the above needs to be set, otherwise SetIdentifier doesn't make sense
}
}
if r.HealthCheckId != nil {
ep.WithProviderSpecific(providerSpecificHealthCheckID, aws.StringValue(r.HealthCheckId))
}
endpoints = append(endpoints, ep)
}
}
@ -374,11 +389,6 @@ func (p *AWSProvider) CreateRecords(ctx context.Context, endpoints []*endpoint.E
return p.doRecords(ctx, 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(ctx context.Context, endpoints, _ []*endpoint.Endpoint) error {
return p.doRecords(ctx, route53.ChangeActionUpsert, endpoints)
}
// DeleteRecords deletes a given set of DNS records in a given zone.
func (p *AWSProvider) DeleteRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
return p.doRecords(ctx, route53.ChangeActionDelete, endpoints)
@ -397,6 +407,47 @@ func (p *AWSProvider) doRecords(ctx context.Context, action string, endpoints []
return p.submitChanges(ctx, p.newChanges(action, endpoints, records, zones), zones)
}
// UpdateRecords updates a given set of old records to a new set of records in a given hosted zone.
func (p *AWSProvider) UpdateRecords(ctx context.Context, updates, current []*endpoint.Endpoint) error {
zones, err := p.Zones(ctx)
if err != nil {
return errors.Wrapf(err, "failed to list zones, aborting UpdateRecords")
}
records, err := p.records(ctx, zones)
if err != nil {
log.Errorf("failed to list records while preparing UpdateRecords: %s", err)
}
return p.submitChanges(ctx, p.createUpdateChanges(updates, current, records, zones), zones)
}
func (p *AWSProvider) createUpdateChanges(newEndpoints, oldEndpoints []*endpoint.Endpoint, recordsCache []*endpoint.Endpoint, zones map[string]*route53.HostedZone) []*route53.Change {
var deletes []*endpoint.Endpoint
var creates []*endpoint.Endpoint
var updates []*endpoint.Endpoint
for i, new := range newEndpoints {
old := oldEndpoints[i]
if new.RecordType != old.RecordType ||
// Handle the case where an AWS ALIAS record is changing to/from a CNAME.
(old.RecordType == endpoint.RecordTypeCNAME && useAlias(old, p.preferCNAME) != useAlias(new, p.preferCNAME)) {
// The record type changed, so UPSERT will fail. Instead perform a DELETE followed by a CREATE.
deletes = append(deletes, old)
creates = append(creates, new)
} else {
// Safe to perform an UPSERT.
updates = append(updates, new)
}
}
combined := make([]*route53.Change, 0, len(deletes)+len(creates)+len(updates))
combined = append(combined, p.newChanges(route53.ChangeActionCreate, creates, recordsCache, zones)...)
combined = append(combined, p.newChanges(route53.ChangeActionUpsert, updates, recordsCache, zones)...)
combined = append(combined, p.newChanges(route53.ChangeActionDelete, deletes, recordsCache, zones)...)
return combined
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zones, err := p.Zones(ctx)
@ -413,11 +464,12 @@ func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) e
}
}
combinedChanges := make([]*route53.Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
updateChanges := p.createUpdateChanges(changes.UpdateNew, changes.UpdateOld, records, zones)
combinedChanges := make([]*route53.Change, 0, len(changes.Delete)+len(changes.Create)+len(updateChanges))
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)...)
combinedChanges = append(combinedChanges, updateChanges...)
return p.submitChanges(ctx, combinedChanges, zones)
}
@ -595,6 +647,10 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint, recordsCac
}
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificHealthCheckID); ok {
change.ResourceRecordSet.HealthCheckId = aws.String(prop.Value)
}
return change, dualstack
}

View File

@ -326,6 +326,8 @@ func TestAWSRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"),
endpoint.NewEndpoint("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id"),
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"),
})
records, err := provider.Records(context.Background())
@ -347,6 +349,8 @@ func TestAWSRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"),
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id"),
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"),
})
}
@ -380,6 +384,7 @@ func TestAWSUpdateRecords(t *testing.T) {
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.1.1.1"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
})
@ -387,12 +392,14 @@ func TestAWSUpdateRecords(t *testing.T) {
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-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-1.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-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
@ -405,6 +412,7 @@ func TestAWSUpdateRecords(t *testing.T) {
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.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("update-test-a-to-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), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
})
@ -452,6 +460,8 @@ func TestAWSApplyChanges(t *testing.T) {
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-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.1.1.1"),
endpoint.NewEndpointWithTTL("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com"),
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"),
@ -471,6 +481,8 @@ func TestAWSApplyChanges(t *testing.T) {
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-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"),
endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
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"),
@ -478,6 +490,8 @@ func TestAWSApplyChanges(t *testing.T) {
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-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "my-internal-host.example.com"),
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"),
@ -516,6 +530,8 @@ func TestAWSApplyChanges(t *testing.T) {
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("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "my-internal-host.example.com"),
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"),
@ -532,6 +548,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) {
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-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.1.1.1"),
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"),
@ -553,6 +570,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) {
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-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"),
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"),
@ -560,6 +578,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) {
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-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
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"),
@ -1027,6 +1046,7 @@ func TestAWSCanonicalHostedZone(t *testing.T) {
{"foo.sa-east-1.elb.amazonaws.com", "Z2P70J7HTTTPLU"},
{"foo.cn-north-1.elb.amazonaws.com.cn", "Z1GDH35T77C1KE"},
{"foo.cn-northwest-1.elb.amazonaws.com.cn", "ZM7IZAIOVVDZF"},
{"foo.af-south-1.elb.amazonaws.com", "Z268VQBMOI5EKX"},
// Network Load Balancers
{"foo.elb.us-east-2.amazonaws.com", "ZLMOA37VPKANP"},
{"foo.elb.us-east-1.amazonaws.com", "Z26RNL4JYFTOTI"},
@ -1046,6 +1066,7 @@ func TestAWSCanonicalHostedZone(t *testing.T) {
{"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"},
{"foo.elb.af-south-1.amazonaws.com", "Z203XCE67M25HM"},
// No Load Balancer
{"foo.example.org", ""},
} {
@ -1094,6 +1115,51 @@ func TestAWSSuitableZones(t *testing.T) {
}
}
func TestAWSHealthTargetAnnotation(tt *testing.T) {
comparator := func(name, previous, current string) bool {
return previous == current
}
for _, test := range []struct {
name string
current *endpoint.Endpoint
desired *endpoint.Endpoint
propertyComparator func(name, previous, current string) bool
shouldUpdate bool
}{
{
name: "skip AWS target health",
current: &endpoint.Endpoint{
RecordType: "A",
DNSName: "foo.com",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
},
},
desired: &endpoint.Endpoint{
DNSName: "foo.com",
RecordType: "A",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "false"},
},
},
propertyComparator: comparator,
shouldUpdate: false,
},
} {
tt.Run(test.name, func(t *testing.T) {
provider := &AWSProvider{}
plan := &plan.Plan{
Current: []*endpoint.Endpoint{test.current},
Desired: []*endpoint.Endpoint{test.desired},
PropertyComparator: provider.PropertyValuesEqual,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
plan = plan.Calculate()
assert.Equal(t, test.shouldUpdate, len(plan.Changes.UpdateNew) == 1)
})
}
}
func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) {
params := &route53.CreateHostedZoneInput{
CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"),

View File

@ -19,17 +19,12 @@ package azure
import (
"context"
"fmt"
"io/ioutil"
"strings"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"sigs.k8s.io/external-dns/endpoint"
@ -41,18 +36,6 @@ const (
azureRecordTTL = 300
)
type config struct {
Cloud string `json:"cloud" yaml:"cloud"`
TenantID string `json:"tenantId" yaml:"tenantId"`
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
Location string `json:"location" yaml:"location"`
ClientID string `json:"aadClientId" yaml:"aadClientId"`
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
}
// ZonesClient is an interface of dns.ZoneClient that can be stubbed for testing.
type ZonesClient interface {
ListByResourceGroupComplete(ctx context.Context, resourceGroupName string, top *int32) (result dns.ZoneListResultIterator, err error)
@ -69,6 +52,7 @@ type RecordSetsClient interface {
type AzureProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter
zoneNameFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
dryRun bool
resourceGroup string
@ -80,109 +64,32 @@ type AzureProvider struct {
// NewAzureProvider creates a new Azure provider.
//
// Returns the provider or an error if a provider could not be created.
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) {
contents, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
cfg := config{}
err = yaml.Unmarshal(contents, &cfg)
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) {
cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
// If a resource group was given, override what was present in the config file
if resourceGroup != "" {
cfg.ResourceGroup = resourceGroup
}
// If userAssignedIdentityClientID is provided explicitly, override existing one in config file
if userAssignedIdentityClientID != "" {
cfg.UserAssignedIdentityID = userAssignedIdentityClientID
}
var environment azure.Environment
if cfg.Cloud == "" {
environment = azure.PublicCloud
} else {
environment, err = azure.EnvironmentFromName(cfg.Cloud)
if err != nil {
return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err)
}
}
token, err := getAccessToken(cfg, environment)
token, err := getAccessToken(*cfg, cfg.Environment)
if err != nil {
return nil, fmt.Errorf("failed to get token: %v", err)
}
zonesClient := dns.NewZonesClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID)
zonesClient := dns.NewZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
recordSetsClient := dns.NewRecordSetsClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID)
recordSetsClient := dns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
provider := &AzureProvider{
return &AzureProvider{
domainFilter: domainFilter,
zoneNameFilter: zoneNameFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
resourceGroup: cfg.ResourceGroup,
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
zonesClient: zonesClient,
recordSetsClient: recordSetsClient,
}
return provider, nil
}
// getAccessToken retrieves Azure API access token.
func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) {
// Try to retrieve token with service principal credentials.
// Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true`
// and service principal exists. In this case, we still want to use service principal to authenticate.
if len(cfg.ClientID) > 0 &&
len(cfg.ClientSecret) > 0 &&
// due to some historical reason, for pure MSI cluster,
// they will use "msi" as placeholder in azure.json.
// In this case, we shouldn't try to use SPN to authenticate.
!strings.EqualFold(cfg.ClientID, "msi") &&
!strings.EqualFold(cfg.ClientSecret, "msi") {
log.Info("Using client_id+client_secret to retrieve access token for Azure API.")
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
}
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create service principal token: %v", err)
}
return token, nil
}
// Try to retrieve token with MSI.
if cfg.UseManagedIdentityExtension {
log.Info("Using managed identity extension to retrieve access token for Azure API.")
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err)
}
if cfg.UserAssignedIdentityID != "" {
log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID)
token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
log.Info("Resolving to system assigned identity.")
token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
return nil, fmt.Errorf("no credentials provided for Azure API")
}, nil
}
// Records gets the current records.
@ -205,6 +112,11 @@ func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp
return true
}
name := formatAzureDNSName(*recordSet.Name, *zone.Name)
if len(p.zoneNameFilter.Filters) > 0 && !p.domainFilter.Match(name) {
log.Debugf("Skipping return of record %s because it was filtered out by the specified --domain-filter", name)
return true
}
targets := extractAzureTargets(&recordSet)
if len(targets) == 0 {
log.Errorf("Failed to extract targets for '%s' with type '%s'.", name, recordType)
@ -262,6 +174,9 @@ func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) {
if zone.Name != nil && p.domainFilter.Match(*zone.Name) && p.zoneIDFilter.Match(*zone.ID) {
zones = append(zones, zone)
} else if zone.Name != nil && len(p.zoneNameFilter.Filters) > 0 && p.zoneNameFilter.Match(*zone.Name) {
// Handle zoneNameFilter
zones = append(zones, zone)
}
err := zonesIterator.NextWithContext(ctx)
@ -342,16 +257,20 @@ func (p *AzureProvider) mapChanges(zones []dns.Zone, changes *plan.Changes) (azu
func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMap) {
// Delete records first
for zone, endpoints := range deleted {
for _, endpoint := range endpoints {
name := p.recordSetNameForZone(zone, endpoint)
for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, ep)
if !p.domainFilter.Match(ep.DNSName) {
log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
continue
}
if p.dryRun {
log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone)
log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone)
} else {
log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(endpoint.RecordType), ""); err != nil {
log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(ep.RecordType), ""); err != nil {
log.Errorf(
"Failed to delete %s record named '%s' for Azure DNS zone '%s': %v",
endpoint.RecordType,
ep.RecordType,
name,
zone,
err,
@ -364,14 +283,18 @@ func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMa
func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMap) {
for zone, endpoints := range updated {
for _, endpoint := range endpoints {
name := p.recordSetNameForZone(zone, endpoint)
for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, ep)
if !p.domainFilter.Match(ep.DNSName) {
log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
continue
}
if p.dryRun {
log.Infof(
"Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
)
continue
@ -379,20 +302,20 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
log.Infof(
"Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
)
recordSet, err := p.newRecordSet(endpoint)
recordSet, err := p.newRecordSet(ep)
if err == nil {
_, err = p.recordSetsClient.CreateOrUpdate(
ctx,
p.resourceGroup,
zone,
name,
dns.RecordType(endpoint.RecordType),
dns.RecordType(ep.RecordType),
recordSet,
"",
"",
@ -401,9 +324,9 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
if err != nil {
log.Errorf(
"Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
err,
)

View File

@ -21,9 +21,8 @@ import (
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns"
"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/to"
log "github.com/sirupsen/logrus"
@ -47,44 +46,43 @@ type PrivateRecordSetsClient interface {
// AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service
type AzurePrivateDNSProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
dryRun bool
subscriptionID string
resourceGroup string
zonesClient PrivateZonesClient
recordSetsClient PrivateRecordSetsClient
domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
dryRun bool
resourceGroup string
userAssignedIdentityClientID string
zonesClient PrivateZonesClient
recordSetsClient PrivateRecordSetsClient
}
// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
//
// Returns the provider or an error if a provider could not be created.
func NewAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, subscriptionID string, dryRun bool) (*AzurePrivateDNSProvider, error) {
authorizer, err := auth.NewAuthorizerFromEnvironment()
func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup, userAssignedIdentityClientID string, dryRun bool) (*AzurePrivateDNSProvider, error) {
cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
settings, err := auth.GetSettingsFromEnvironment()
token, err := getAccessToken(*cfg, cfg.Environment)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get token: %v", err)
}
zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID)
zonesClient.Authorizer = authorizer
recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID)
recordSetsClient.Authorizer = authorizer
zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
provider := &AzurePrivateDNSProvider{
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
subscriptionID: subscriptionID,
resourceGroup: resourceGroup,
zonesClient: zonesClient,
recordSetsClient: recordSetsClient,
}
return provider, nil
return &AzurePrivateDNSProvider{
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
resourceGroup: cfg.ResourceGroup,
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
zonesClient: zonesClient,
recordSetsClient: recordSetsClient,
}, nil
}
// Records gets the current records.
@ -256,16 +254,16 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu
log.Debugf("Records to be deleted: %d", len(deleted))
// Delete records first
for zone, endpoints := range deleted {
for _, endpoint := range endpoints {
name := p.recordSetNameForZone(zone, endpoint)
for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, ep)
if p.dryRun {
log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone)
log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone)
} else {
log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(endpoint.RecordType), name, ""); err != nil {
log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(ep.RecordType), name, ""); err != nil {
log.Errorf(
"Failed to delete %s record named '%s' for Azure Private DNS zone '%s': %v",
endpoint.RecordType,
ep.RecordType,
name,
zone,
err,
@ -279,14 +277,14 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu
func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azurePrivateDNSChangeMap) {
log.Debugf("Records to be updated: %d", len(updated))
for zone, endpoints := range updated {
for _, endpoint := range endpoints {
name := p.recordSetNameForZone(zone, endpoint)
for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, ep)
if p.dryRun {
log.Infof(
"Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
)
continue
@ -294,19 +292,19 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
log.Infof(
"Updating %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
)
recordSet, err := p.newRecordSet(endpoint)
recordSet, err := p.newRecordSet(ep)
if err == nil {
_, err = p.recordSetsClient.CreateOrUpdate(
ctx,
p.resourceGroup,
zone,
privatedns.RecordType(endpoint.RecordType),
privatedns.RecordType(ep.RecordType),
name,
recordSet,
"",
@ -316,9 +314,9 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
if err != nil {
log.Errorf(
"Failed to update %s record named '%s' to '%s' for Azure Private DNS zone '%s': %v",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
err,
)

View File

@ -18,16 +18,11 @@ package azure
import (
"context"
"os"
"testing"
"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/to"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
@ -255,36 +250,6 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter
}
}
func validateAzurePrivateDNSClientsResourceManager(t *testing.T, environmentName string, expectedResourceManagerEndpoint string) {
err := os.Setenv(auth.EnvironmentName, environmentName)
if err != nil {
t.Fatal(err)
}
azurePrivateDNSProvider, err := NewAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), "k8s", "sub", true)
if err != nil {
t.Fatal(err)
}
zonesClientBaseURI := azurePrivateDNSProvider.zonesClient.(privatedns.PrivateZonesClient).BaseURI
recordSetsClientBaseURI := azurePrivateDNSProvider.recordSetsClient.(privatedns.RecordSetsClient).BaseURI
assert.Equal(t, zonesClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, zonesClientBaseURI)
assert.Equal(t, recordSetsClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, recordSetsClientBaseURI)
}
func TestNewAzurePrivateDNSProvider(t *testing.T) {
// make sure to reset the environment variables at the end again
originalEnv := os.Getenv(auth.EnvironmentName)
defer os.Setenv(auth.EnvironmentName, originalEnv)
validateAzurePrivateDNSClientsResourceManager(t, "", azure.PublicCloud.ResourceManagerEndpoint)
validateAzurePrivateDNSClientsResourceManager(t, "AZURECHINACLOUD", azure.ChinaCloud.ResourceManagerEndpoint)
validateAzurePrivateDNSClientsResourceManager(t, "AZUREGERMANCLOUD", azure.GermanCloud.ResourceManagerEndpoint)
validateAzurePrivateDNSClientsResourceManager(t, "AZUREUSGOVERNMENTCLOUD", azure.USGovernmentCloud.ResourceManagerEndpoint)
}
func TestAzurePrivateDNSRecord(t *testing.T) {
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s",
&[]privatedns.PrivateZone{

View File

@ -207,7 +207,7 @@ func (client *mockRecordSetsClient) CreateOrUpdate(ctx context.Context, resource
}
// newMockedAzureProvider creates an AzureProvider comprising the mocked clients for zones and recordsets
func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zones *[]dns.Zone, recordSets *[]dns.RecordSet) (*AzureProvider, error) {
func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zones *[]dns.Zone, recordSets *[]dns.RecordSet) (*AzureProvider, error) {
// init zone-related parts of the mock-client
pageIterator := mockZoneListResultPageIterator{
results: []dns.ZoneListResult{
@ -237,12 +237,13 @@ func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter pro
mockRecordSetListIterator: &mockRecordSetListIterator,
}
return newAzureProvider(domainFilter, zoneIDFilter, dryRun, resourceGroup, userAssignedIdentityClientID, &zonesClient, &recordSetsClient), nil
return newAzureProvider(domainFilter, zoneNameFilter, zoneIDFilter, dryRun, resourceGroup, userAssignedIdentityClientID, &zonesClient, &recordSetsClient), nil
}
func newAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zonesClient ZonesClient, recordsClient RecordSetsClient) *AzureProvider {
func newAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, zonesClient ZonesClient, recordsClient RecordSetsClient) *AzureProvider {
return &AzureProvider{
domainFilter: domainFilter,
zoneNameFilter: zoneNameFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
resourceGroup: resourceGroup,
@ -257,7 +258,7 @@ func validateAzureEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expect
}
func TestAzureRecord(t *testing.T) {
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), endpoint.NewDomainFilter([]string{}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
&[]dns.Zone{
createMockZone("example.com", "/dnszones/example.com"),
},
@ -294,7 +295,7 @@ func TestAzureRecord(t *testing.T) {
}
func TestAzureMultiRecord(t *testing.T) {
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), endpoint.NewDomainFilter([]string{}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
&[]dns.Zone{
createMockZone("example.com", "/dnszones/example.com"),
},
@ -393,6 +394,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC
}
provider := newAzureProvider(
endpoint.NewDomainFilter([]string{""}),
endpoint.NewDomainFilter([]string{""}),
provider.NewZoneIDFilter([]string{""}),
dryRun,
@ -496,3 +498,138 @@ func TestAzureGetAccessToken(t *testing.T) {
t.Fatalf("expect the clientID of the token is SPNClientID, but got token %s", string(innerToken))
}
}
func TestAzureNameFilter(t *testing.T) {
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"nginx.example.com"}), endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
&[]dns.Zone{
createMockZone("example.com", "/dnszones/example.com"),
},
&[]dns.RecordSet{
createMockRecordSet("@", "NS", "ns1-03.azure-dns.com."),
createMockRecordSet("@", "SOA", "Email: azuredns-hostmaster.microsoft.com"),
createMockRecordSet("@", endpoint.RecordTypeA, "123.123.123.122"),
createMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
createMockRecordSetWithTTL("test.nginx", endpoint.RecordTypeA, "123.123.123.123", 3600),
createMockRecordSetWithTTL("nginx", endpoint.RecordTypeA, "123.123.123.123", 3600),
createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
})
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
actual, err := provider.Records(ctx)
if err != nil {
t.Fatal(err)
}
expected := []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("test.nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"),
endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"),
endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"),
}
validateAzureEndpoints(t, actual, expected)
}
func TestAzureApplyChangesZoneName(t *testing.T) {
recordsClient := mockRecordSetsClient{}
testAzureApplyChangesInternalZoneName(t, false, &recordsClient)
validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{
endpoint.NewEndpoint("old.foo.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("oldcname.foo.example.com", endpoint.RecordTypeCNAME, ""),
endpoint.NewEndpoint("deleted.foo.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("deletedcname.foo.example.com", endpoint.RecordTypeCNAME, ""),
})
validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{
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("new.foo.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"),
endpoint.NewEndpointWithTTL("newcname.foo.example.com", endpoint.RecordTypeCNAME, 10, "other.com"),
})
}
func testAzureApplyChangesInternalZoneName(t *testing.T, dryRun bool, client RecordSetsClient) {
zlr := dns.ZoneListResult{
Value: &[]dns.Zone{
createMockZone("example.com", "/dnszones/example.com"),
},
}
results := []dns.ZoneListResult{
zlr,
}
mockZoneListResultPage := dns.NewZoneListResultPage(func(ctxParam context.Context, zlrParam dns.ZoneListResult) (dns.ZoneListResult, error) {
if len(results) > 0 {
result := results[0]
results = nil
return result, nil
}
return dns.ZoneListResult{}, nil
})
mockZoneClientIterator := dns.NewZoneListResultIterator(mockZoneListResultPage)
zonesClient := mockZonesClient{
mockZonesClientIterator: &mockZoneClientIterator,
}
provider := newAzureProvider(
endpoint.NewDomainFilter([]string{"foo.example.com"}),
endpoint.NewDomainFilter([]string{"example.com"}),
provider.NewZoneIDFilter([]string{""}),
dryRun,
"group",
"",
&zonesClient,
client,
)
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.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"),
endpoint.NewEndpoint("other.com", endpoint.RecordTypeA, "5.6.7.8"),
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("old.foo.example.com", endpoint.RecordTypeA, "121.212.121.212"),
endpoint.NewEndpoint("oldcname.foo.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("old.nope.example.com", endpoint.RecordTypeA, "121.212.121.212"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"),
endpoint.NewEndpointWithTTL("newcname.foo.example.com", endpoint.RecordTypeCNAME, 10, "other.com"),
endpoint.NewEndpoint("new.nope.example.com", endpoint.RecordTypeA, "222.111.222.111"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("deleted.foo.example.com", endpoint.RecordTypeA, "111.222.111.222"),
endpoint.NewEndpoint("deletedcname.foo.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("deleted.nope.example.com", endpoint.RecordTypeA, "222.111.222.111"),
}
changes := &plan.Changes{
Create: createRecords,
UpdateNew: updatedRecords,
UpdateOld: currentRecords,
Delete: deleteRecords,
}
if err := provider.ApplyChanges(context.Background(), changes); err != nil {
t.Fatal(err)
}
}

129
provider/azure/config.go Normal file
View File

@ -0,0 +1,129 @@
/*
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 azure
import (
"fmt"
"io/ioutil"
"strings"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
// config represents common config items for Azure DNS and Azure Private DNS
type config struct {
Cloud string `json:"cloud" yaml:"cloud"`
Environment azure.Environment `json:"-" yaml:"-"`
TenantID string `json:"tenantId" yaml:"tenantId"`
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
Location string `json:"location" yaml:"location"`
ClientID string `json:"aadClientId" yaml:"aadClientId"`
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
}
func getConfig(configFile, resourceGroup, userAssignedIdentityClientID string) (*config, error) {
contents, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
cfg := &config{}
err = yaml.Unmarshal(contents, &cfg)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
// If a resource group was given, override what was present in the config file
if resourceGroup != "" {
cfg.ResourceGroup = resourceGroup
}
// If userAssignedIdentityClientID is provided explicitly, override existing one in config file
if userAssignedIdentityClientID != "" {
cfg.UserAssignedIdentityID = userAssignedIdentityClientID
}
var environment azure.Environment
if cfg.Cloud == "" {
environment = azure.PublicCloud
} else {
environment, err = azure.EnvironmentFromName(cfg.Cloud)
if err != nil {
return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err)
}
}
cfg.Environment = environment
return cfg, nil
}
// getAccessToken retrieves Azure API access token.
func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) {
// Try to retrieve token with service principal credentials.
// Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true`
// and service principal exists. In this case, we still want to use service principal to authenticate.
if len(cfg.ClientID) > 0 &&
len(cfg.ClientSecret) > 0 &&
// due to some historical reason, for pure MSI cluster,
// they will use "msi" as placeholder in azure.json.
// In this case, we shouldn't try to use SPN to authenticate.
!strings.EqualFold(cfg.ClientID, "msi") &&
!strings.EqualFold(cfg.ClientSecret, "msi") {
log.Info("Using client_id+client_secret to retrieve access token for Azure API.")
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
}
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create service principal token: %v", err)
}
return token, nil
}
// Try to retrieve token with MSI.
if cfg.UseManagedIdentityExtension {
log.Info("Using managed identity extension to retrieve access token for Azure API.")
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err)
}
if cfg.UserAssignedIdentityID != "" {
log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID)
token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
log.Info("Resolving to system assigned identity.")
token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
return nil, fmt.Errorf("no credentials provided for Azure API")
}

View File

@ -0,0 +1,67 @@
/*
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 azure
import (
"fmt"
"github.com/Azure/go-autorest/autorest/azure"
"io/ioutil"
"os"
"reflect"
"testing"
)
func TestGetAzureEnvironmentConfig(t *testing.T) {
tmp, err := ioutil.TempFile("", "azureconf")
if err != nil {
t.Errorf("couldn't write temp file %v", err)
}
defer os.Remove(tmp.Name())
tests := map[string]struct {
cloud string
err error
}{
"AzureChinaCloud": {"AzureChinaCloud", nil},
"AzureGermanCloud": {"AzureGermanCloud", nil},
"AzurePublicCloud": {"", nil},
"AzureUSGovernment": {"AzureUSGovernmentCloud", nil},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
_, _ = tmp.Seek(0, 0)
_, _ = tmp.Write([]byte(fmt.Sprintf(`{"cloud": "%s"}`, test.cloud)))
got, err := getConfig(tmp.Name(), "", "")
if err != nil {
t.Errorf("got unexpected err %v", err)
}
if test.cloud == "" {
test.cloud = "AzurePublicCloud"
}
want, err := azure.EnvironmentFromName(test.cloud)
if err != nil {
t.Errorf("couldn't get azure environment from provided name %v", err)
}
if !reflect.DeepEqual(want, got.Environment) {
t.Errorf("got %v, want %v", got.Environment, want)
}
})
}
}

View File

@ -157,7 +157,7 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
p.PaginationOptions.Page = 1
// if there is a zoneIDfilter configured
// && if the filter isnt just a blank string (used in tests)
// && if the filter isn't just a blank string (used in tests)
if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" {
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
@ -331,6 +331,18 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
return nil
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider
func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
adjustedEndpoints := []*endpoint.Endpoint{}
for _, e := range endpoints {
if shouldBeProxied(e, p.proxiedByDefault) {
e.RecordTTL = 0
}
adjustedEndpoints = append(adjustedEndpoints, e)
}
return adjustedEndpoints
}
// changesByZone separates a multi-zone change into a single change per zone.
func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
changes := make(map[string][]*cloudFlareChange)

View File

@ -229,7 +229,7 @@ func (m *mockCloudFlareClient) ZoneDetails(zoneID string) (cloudflare.Zone, erro
return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
}
func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, args ...interface{}) {
func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, managedRecords []string, args ...interface{}) {
t.Helper()
var client *mockCloudFlareClient
@ -250,16 +250,17 @@ func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endp
}
plan := &plan.Plan{
Current: records,
Desired: endpoints,
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
Current: records,
Desired: endpoints,
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
ManagedRecords: managedRecords,
}
changes := plan.Calculate().Changes
// Records other than A and CNAME are not supported by planner, just create them
// Records other than A, CNAME and NS are not supported by planner, just create them
for _, endpoint := range endpoints {
if endpoint.RecordType != "A" && endpoint.RecordType != "CNAME" {
if endpoint.RecordType != "A" && endpoint.RecordType != "CNAME" && endpoint.RecordType != "NS" {
changes.Create = append(changes.Create, endpoint)
}
}
@ -305,7 +306,10 @@ func TestCloudflareA(t *testing.T) {
Proxied: false,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareCname(t *testing.T) {
@ -340,7 +344,9 @@ func TestCloudflareCname(t *testing.T) {
Proxied: false,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareCustomTTL(t *testing.T) {
@ -365,7 +371,9 @@ func TestCloudflareCustomTTL(t *testing.T) {
Proxied: false,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareProxiedDefault(t *testing.T) {
@ -389,7 +397,9 @@ func TestCloudflareProxiedDefault(t *testing.T) {
Proxied: true,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareProxiedOverrideTrue(t *testing.T) {
@ -419,7 +429,9 @@ func TestCloudflareProxiedOverrideTrue(t *testing.T) {
Proxied: true,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareProxiedOverrideFalse(t *testing.T) {
@ -449,7 +461,9 @@ func TestCloudflareProxiedOverrideFalse(t *testing.T) {
Proxied: false,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
@ -479,7 +493,9 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
Proxied: true,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareSetProxied(t *testing.T) {
@ -525,7 +541,7 @@ func TestCloudflareSetProxied(t *testing.T) {
Proxied: testCase.proxiable,
},
},
}, testCase.recordType+" record on "+testCase.domain)
}, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, testCase.recordType+" record on "+testCase.domain)
}
}
@ -1038,6 +1054,7 @@ func TestProviderPropertiesIdempotency(t *testing.T) {
Current: current,
Desired: desired,
PropertyComparator: provider.PropertyValuesEqual,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
plan = *plan.Calculate()
@ -1091,7 +1108,8 @@ func TestCloudflareComplexUpdate(t *testing.T) {
},
},
},
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
planned := plan.Calculate()
@ -1133,3 +1151,61 @@ func TestCloudflareComplexUpdate(t *testing.T) {
},
})
}
func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
"001": []cloudflare.DNSRecord{
{
ID: "1234567890",
ZoneID: "001",
Name: "foobar.bar.com",
Type: endpoint.RecordTypeA,
TTL: 1,
Content: "1.2.3.4",
Proxied: true,
},
},
})
provider := &CloudFlareProvider{
Client: client,
}
records, err := provider.Records(context.Background())
if err != nil {
t.Errorf("should not fail, %s", err)
}
endpoints := []*endpoint.Endpoint{
{
DNSName: "foobar.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 300,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied",
Value: "true",
},
},
},
}
provider.AdjustEndpoints(endpoints)
plan := &plan.Plan{
Current: records,
Desired: endpoints,
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
planned := plan.Calculate()
assert.Equal(t, 0, len(planned.Changes.Create), "no new changes should be here")
assert.Equal(t, 0, len(planned.Changes.UpdateNew), "no new changes should be here")
assert.Equal(t, 0, len(planned.Changes.UpdateOld), "no new changes should be here")
assert.Equal(t, 0, len(planned.Changes.Delete), "no new changes should be here")
}

View File

@ -158,7 +158,7 @@ func (c etcdClient) DeleteService(key string) error {
return err
}
// loads TLS artifacts and builds tls.Clonfig object
// loads TLS artifacts and builds tls.Config object
func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) (*tls.Config, error) {
if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
return nil, errors.New("either both cert and key or none must be provided")

View File

@ -27,6 +27,7 @@ import (
"golang.org/x/oauth2"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
@ -83,7 +84,10 @@ func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFi
oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: token,
}))
client := godo.NewClient(oauthClient)
client, err := godo.New(oauthClient, godo.SetUserAgent("ExternalDNS/"+externaldns.Version))
if err != nil {
return nil, err
}
p := &DigitalOceanProvider{
Client: client.Domains,

View File

@ -195,7 +195,7 @@ func fixMissingTTL(ttl endpoint.TTL, minTTLSeconds int) string {
return strconv.Itoa(i)
}
// merge produces a singe list of records that can be used as a replacement.
// merge produces a single list of records that can be used as a replacement.
// Dyn allows to replace all records with a single call
// Invariant: the result contains only elements from the updateNew parameter
func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint {
@ -625,7 +625,7 @@ func (d *dynProviderState) Records(ctx context.Context) ([]*endpoint.Endpoint, e
// 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(ctx context.Context, changes *plan.Changes) error {
log.Debugf("Processing chages: %+v", changes)
log.Debugf("Processing changes: %+v", changes)
if d.DryRun {
log.Infof("Will NOT delete these records: %+v", changes.Delete)

View File

@ -117,14 +117,40 @@ func (p *HetznerProvider) submitChanges(ctx context.Context, changes []*HetznerC
for _, changes := range zoneChanges {
for _, change := range changes {
// Prepare record name
recordName := strings.TrimSuffix(change.ResourceRecordSet.Name, "."+change.ZoneName)
if recordName == change.ZoneName {
recordName = "@"
}
if change.ResourceRecordSet.RecordType == hclouddns.CNAME && !strings.HasSuffix(change.ResourceRecordSet.Value, ".") {
change.ResourceRecordSet.Value += "."
}
change.ResourceRecordSet.Name = recordName
// Get ID of record if not create operation
if change.Action != hetznerCreate {
allRecords, err := p.Client.GetRecords(hclouddns.HCloudGetRecordsParams{ZoneID: change.ZoneID})
if err != nil {
return err
}
for _, record := range allRecords.Records {
if record.Name == change.ResourceRecordSet.Name && record.RecordType == change.ResourceRecordSet.RecordType {
change.ResourceRecordSet.ID = record.ID
break
}
}
}
log.WithFields(log.Fields{
"id": change.ResourceRecordSet.ID,
"record": change.ResourceRecordSet.Name,
"type": change.ResourceRecordSet.RecordType,
"value": change.ResourceRecordSet.Value,
"ttl": change.ResourceRecordSet.TTL,
"action": change.Action,
"zone": change.ZoneName,
"zone_id": change.ZoneID,
}).Info("Changing record.")
}).Info("Changing record")
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, "."+change.ZoneName)
if change.ResourceRecordSet.Name == change.ZoneName {
@ -143,13 +169,24 @@ func (p *HetznerProvider) submitChanges(ctx context.Context, changes []*HetznerC
Value: change.ResourceRecordSet.Value,
TTL: change.ResourceRecordSet.TTL,
}
_, err := p.Client.CreateRecord(record)
answer, err := p.Client.CreateRecord(record)
if err != nil {
log.WithFields(log.Fields{
"Code": answer.Error.Code,
"Message": answer.Error.Message,
"Record name": answer.Record.Name,
"Record type": answer.Record.RecordType,
"Record value": answer.Record.Value,
}).Warning("Create problem")
return err
}
case hetznerDelete:
_, err := p.Client.DeleteRecord(change.ResourceRecordSet.ID)
answer, err := p.Client.DeleteRecord(change.ResourceRecordSet.ID)
if err != nil {
log.WithFields(log.Fields{
"Code": answer.Error.Code,
"Message": answer.Error.Message,
}).Warning("Delete problem")
return err
}
case hetznerUpdate:
@ -159,9 +196,17 @@ func (p *HetznerProvider) submitChanges(ctx context.Context, changes []*HetznerC
Name: change.ResourceRecordSet.Name,
Value: change.ResourceRecordSet.Value,
TTL: change.ResourceRecordSet.TTL,
ID: change.ResourceRecordSet.ID,
}
_, err := p.Client.UpdateRecord(record)
answer, err := p.Client.UpdateRecord(record)
if err != nil {
log.WithFields(log.Fields{
"Code": answer.Error.Code,
"Message": answer.Error.Message,
"Record name": answer.Record.Name,
"Record type": answer.Record.RecordType,
"Record value": answer.Record.Value,
}).Warning("Update problem")
return err
}
}

View File

@ -36,6 +36,7 @@ type mockHCloudClientAdapter interface {
ImportZoneString(zoneID string, zonePlainText string) (hclouddns.HCloudAnswerGetZone, error)
ExportZoneToString(zoneID string) (hclouddns.HCloudAnswerGetZonePlainText, error)
ValidateZoneString(zonePlainText string) (hclouddns.HCloudAnswerZoneValidate, error)
GetRecord(ID string) (hclouddns.HCloudAnswerGetRecord, error)
GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error)
UpdateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error)
DeleteRecord(ID string) (hclouddns.HCloudAnswerDeleteRecord, error)
@ -95,6 +96,11 @@ func (m *mockHCloudClient) ValidateZoneString(zonePlainText string) (hclouddns.H
}
// records
func (m *mockHCloudClient) GetRecord(ID string) (hclouddns.HCloudAnswerGetRecord, error) {
return hclouddns.HCloudAnswerGetRecord{}, nil
}
func (m *mockHCloudClient) GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error) {
return hclouddns.HCloudAnswerGetRecords{
Records: []hclouddns.HCloudRecord{

View File

@ -21,6 +21,7 @@ import (
"fmt"
"net/http"
"os"
"sort"
"strconv"
"strings"
@ -157,7 +158,24 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E
return nil, fmt.Errorf("could not fetch A records from zone '%s': %s", zone.Fqdn, err)
}
for _, res := range resA {
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, res.Ipv4Addr))
newEndpoint := endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, res.Ipv4Addr)
// Check if endpoint already exists and add to existing endpoint if it does
foundExisting := false
for _, ep := range endpoints {
if ep.DNSName == newEndpoint.DNSName && ep.RecordType == newEndpoint.RecordType {
logrus.Debugf("Adding target '%s' to existing A record '%s'", newEndpoint.Targets[0], ep.DNSName)
ep.Targets = append(ep.Targets, newEndpoint.Targets[0])
foundExisting = true
break
}
}
if !foundExisting {
endpoints = append(endpoints, newEndpoint)
}
}
// sort targets so that they are always in same order, as infoblox might return them in different order
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
// Include Host records since they should be treated synonymously with A records
@ -232,7 +250,11 @@ func (p *InfobloxProvider) ApplyChanges(ctx context.Context, changes *plan.Chang
func (p *InfobloxProvider) zones() ([]ibclient.ZoneAuth, error) {
var res, result []ibclient.ZoneAuth
obj := ibclient.NewZoneAuth(ibclient.ZoneAuth{})
obj := ibclient.NewZoneAuth(
ibclient.ZoneAuth{
View: p.view,
},
)
err := p.client.GetObject(obj, "", &res)
if err != nil {
@ -305,14 +327,14 @@ func (p *InfobloxProvider) findZone(zones []ibclient.ZoneAuth, name string) *ibc
return result
}
func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (recordSet infobloxRecordSet, err error) {
func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool, targetIndex int) (recordSet infobloxRecordSet, err error) {
switch ep.RecordType {
case endpoint.RecordTypeA:
var res []ibclient.RecordA
obj := ibclient.NewRecordA(
ibclient.RecordA{
Name: ep.DNSName,
Ipv4Addr: ep.Targets[0],
Ipv4Addr: ep.Targets[targetIndex],
View: p.view,
},
)
@ -395,28 +417,30 @@ func (p *InfobloxProvider) createRecords(created infobloxChangeMap) {
zone,
)
recordSet, err := p.recordSet(ep, false)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
err,
)
continue
}
_, err = p.client.CreateObject(recordSet.obj)
if err != nil {
logrus.Errorf(
"Failed to create %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
err,
)
for targetIndex := range ep.Targets {
recordSet, err := p.recordSet(ep, false, targetIndex)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
continue
}
_, err = p.client.CreateObject(recordSet.obj)
if err != nil {
logrus.Errorf(
"Failed to create %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
}
}
}
}
@ -430,41 +454,43 @@ func (p *InfobloxProvider) deleteRecords(deleted infobloxChangeMap) {
logrus.Infof("Would delete %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
} else {
logrus.Infof("Deleting %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
recordSet, err := p.recordSet(ep, true)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
err,
)
continue
}
switch ep.RecordType {
case endpoint.RecordTypeA:
for _, record := range *recordSet.res.(*[]ibclient.RecordA) {
_, err = p.client.DeleteObject(record.Ref)
for targetIndex := range ep.Targets {
recordSet, err := p.recordSet(ep, true, targetIndex)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
continue
}
case endpoint.RecordTypeCNAME:
for _, record := range *recordSet.res.(*[]ibclient.RecordCNAME) {
_, err = p.client.DeleteObject(record.Ref)
switch ep.RecordType {
case endpoint.RecordTypeA:
for _, record := range *recordSet.res.(*[]ibclient.RecordA) {
_, err = p.client.DeleteObject(record.Ref)
}
case endpoint.RecordTypeCNAME:
for _, record := range *recordSet.res.(*[]ibclient.RecordCNAME) {
_, err = p.client.DeleteObject(record.Ref)
}
case endpoint.RecordTypeTXT:
for _, record := range *recordSet.res.(*[]ibclient.RecordTXT) {
_, err = p.client.DeleteObject(record.Ref)
}
}
case endpoint.RecordTypeTXT:
for _, record := range *recordSet.res.(*[]ibclient.RecordTXT) {
_, err = p.client.DeleteObject(record.Ref)
if err != nil {
logrus.Errorf(
"Failed to delete %s record named '%s' for Infoblox DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
zone,
err,
)
}
}
if err != nil {
logrus.Errorf(
"Failed to delete %s record named '%s' for Infoblox DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
zone,
err,
)
}
}
}
}

View File

@ -327,6 +327,18 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject
Text: value,
},
)
case "HOST":
return ibclient.NewHostRecord(
ibclient.HostRecord{
Ref: ref,
Name: name,
Ipv4Addrs: []ibclient.HostRecordIpv4Addr{
{
Ipv4Addr: value,
},
},
},
)
}
return nil
}
@ -344,6 +356,7 @@ func TestInfobloxRecords(t *testing.T) {
client := mockIBConnector{
mockInfobloxZones: &[]ibclient.ZoneAuth{
createMockInfobloxZone("example.com"),
createMockInfobloxZone("other.com"),
},
mockInfobloxObjects: &[]ibclient.IBObject{
createMockInfobloxObject("example.com", endpoint.RecordTypeA, "123.123.123.122"),
@ -353,6 +366,13 @@ func TestInfobloxRecords(t *testing.T) {
createMockInfobloxObject("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124"),
createMockInfobloxObject("whitespace.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=white space"),
createMockInfobloxObject("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"),
createMockInfobloxObject("multiple.example.com", endpoint.RecordTypeA, "123.123.123.122"),
createMockInfobloxObject("multiple.example.com", endpoint.RecordTypeA, "123.123.123.121"),
createMockInfobloxObject("multiple.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
createMockInfobloxObject("existing.example.com", endpoint.RecordTypeA, "124.1.1.1"),
createMockInfobloxObject("existing.example.com", endpoint.RecordTypeA, "124.1.1.2"),
createMockInfobloxObject("existing.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=existing"),
createMockInfobloxObject("host.example.com", "HOST", "125.1.1.1"),
},
}
@ -370,6 +390,11 @@ func TestInfobloxRecords(t *testing.T) {
endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124"),
endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=white space\""),
endpoint.NewEndpoint("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeA, "123.123.123.122", "123.123.123.121"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=default\""),
endpoint.NewEndpoint("existing.example.com", endpoint.RecordTypeA, "124.1.1.1", "124.1.1.2"),
endpoint.NewEndpoint("existing.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=existing\""),
endpoint.NewEndpoint("host.example.com", endpoint.RecordTypeA, "125.1.1.1"),
}
validateEndpoints(t, actual, expected)
}
@ -390,12 +415,15 @@ func TestInfobloxApplyChanges(t *testing.T) {
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("new.example.com", endpoint.RecordTypeA, "111.222.111.222"),
endpoint.NewEndpoint("newcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeA, "1.2.3.4,3.4.5.6,8.9.10.11"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeTXT, "tag-multiple-A-records"),
})
validateEndpoints(t, client.deletedEndpoints, []*endpoint.Endpoint{
endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeTXT, ""),
endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""),
})
@ -423,6 +451,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
}
client.(*mockIBConnector).mockInfobloxObjects = &[]ibclient.IBObject{
createMockInfobloxObject("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"),
createMockInfobloxObject("deleted.example.com", endpoint.RecordTypeTXT, "test-deleting-txt"),
createMockInfobloxObject("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
createMockInfobloxObject("old.example.com", endpoint.RecordTypeA, "121.212.121.212"),
createMockInfobloxObject("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
@ -446,6 +475,8 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeA, "1.2.3.4,3.4.5.6,8.9.10.11"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeTXT, "tag-multiple-A-records"),
}
updateOldRecords := []*endpoint.Endpoint{
@ -462,6 +493,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeTXT, "test-deleting-txt"),
endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
}

View File

@ -216,7 +216,7 @@ func (p *OCIProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
// ApplyChanges applies a given set of changes to a given zone.
func (p *OCIProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
log.Debugf("Processing chages: %+v", changes)
log.Debugf("Processing changes: %+v", changes)
ops := []dns.RecordOperation{}
ops = append(ops, p.newFilteredRecordOperations(changes.Create, dns.RecordOperationOperationAdd)...)

View File

@ -93,7 +93,7 @@ func TestOvhZoneRecords(t *testing.T) {
zones, records, err := provider.zonesRecords(context.TODO())
assert.NoError(err)
assert.ElementsMatch(zones, []string{"example.org"})
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}})
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
client.AssertExpectations(t)
// Error on getting zones list
@ -140,8 +140,8 @@ func TestOvhRecords(t *testing.T) {
endpoints, err := provider.Records(context.TODO())
assert.NoError(err)
// Little fix for multi targets endpoint
for _, endoint := range endpoints {
sort.Strings(endoint.Targets)
for _, endpoint := range endpoints {
sort.Strings(endpoint.Targets)
}
assert.ElementsMatch(endpoints, []*endpoint.Endpoint{
{DNSName: "example.org", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42"}},

View File

@ -30,11 +30,16 @@ type Provider interface {
Records(ctx context.Context) ([]*endpoint.Endpoint, error)
ApplyChanges(ctx context.Context, changes *plan.Changes) error
PropertyValuesEqual(name string, previous string, current string) bool
AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint
}
type BaseProvider struct {
}
func (b BaseProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return endpoints
}
func (b BaseProvider) PropertyValuesEqual(name, previous, current string) bool {
return previous == current
}

View File

@ -71,7 +71,7 @@ func TestRcodeZeroProvider_Records(t *testing.T) {
if err != nil {
t.Errorf("should not fail, %s", err)
}
require.Equal(t, 6, len(endpoints))
require.Equal(t, 10, len(endpoints))
mockRRSetService.TestErrorReturned = true

View File

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

View File

@ -269,8 +269,13 @@ func endpointToScalewayRecords(zoneName string, ep *endpoint.Endpoint) []*domain
records := []*domain.Record{}
for _, target := range ep.Targets {
finalTargetName := target
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
finalTargetName = provider.EnsureTrailingDot(target)
}
records = append(records, &domain.Record{
Data: target,
Data: finalTargetName,
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
Priority: priority,
TTL: ttl,
@ -285,9 +290,14 @@ func endpointToScalewayRecordsChangeDelete(zoneName string, ep *endpoint.Endpoin
records := []*domain.RecordChange{}
for _, target := range ep.Targets {
finalTargetName := target
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
finalTargetName = provider.EnsureTrailingDot(target)
}
records = append(records, &domain.RecordChange{
Delete: &domain.RecordChangeDelete{
Data: target,
Data: finalTargetName,
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
Type: domain.RecordType(ep.RecordType),
},

View File

@ -93,7 +93,7 @@ func (m *mockScalewayDomain) ListDNSZoneRecords(req *domain.ListDNSZoneRecordsRe
Type: domain.RecordTypeA,
},
{
Data: "test.example.com",
Data: "test.example.com.",
Name: "two",
TTL: 600,
Priority: 30,
@ -330,7 +330,7 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) {
Add: &domain.RecordChangeAdd{
Records: []*domain.Record{
{
Data: "example.com",
Data: "example.com.",
Name: "",
TTL: 600,
Type: domain.RecordTypeCNAME,

View File

@ -263,7 +263,7 @@ func (p *TransIPProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, er
}
// endpointNameForRecord returns "www.example.org" for DNSEntry with Name "www" and
// Doman with Name "example.org"
// Domain 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.

View File

@ -231,7 +231,7 @@ func TestUltraDNSProvider_ApplyChangesCNAME(t *testing.T) {
assert.NotNil(t, err)
}
// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be avaialble "kubernetes-ultradns-provider-test.com"
// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be available "kubernetes-ultradns-provider-test.com"
func TestUltraDNSProvider_ApplyChanges_Integration(t *testing.T) {
_, ok := os.LookupEnv("ULTRADNS_INTEGRATION")
@ -311,7 +311,7 @@ func TestUltraDNSProvider_ApplyChanges_Integration(t *testing.T) {
}
// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be avaialble "kubernetes-ultradns-provider-test.com" for multiple target
// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be available "kubernetes-ultradns-provider-test.com" for multiple target
func TestUltraDNSProvider_ApplyChanges_MultipleTarget_integeration(t *testing.T) {
_, ok := os.LookupEnv("ULTRADNS_INTEGRATION")
if !ok {
@ -652,7 +652,7 @@ func TestUltraDNSProvider_PoolConversionCase(t *testing.T) {
resp, _ := provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{})
assert.Equal(t, resp.Status, "200 OK")
//Coverting to RD Pool
//Converting to RD Pool
_ = os.Setenv("ULTRADNS_POOL_TYPE", "rdpool")
provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false)
changes = &plan.Changes{}
@ -662,7 +662,7 @@ func TestUltraDNSProvider_PoolConversionCase(t *testing.T) {
resp, _ = provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{})
assert.Equal(t, resp.Status, "200 OK")
//Coverting back to SB Pool
//Converting back to SB Pool
_ = os.Setenv("ULTRADNS_POOL_TYPE", "sbpool")
provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false)
changes = &plan.Changes{}

View File

@ -91,3 +91,8 @@ func (sdr *AWSSDRegistry) updateLabels(endpoints []*endpoint.Endpoint) {
func (sdr *AWSSDRegistry) PropertyValuesEqual(name string, previous string, current string) bool {
return sdr.provider.PropertyValuesEqual(name, previous, current)
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider
func (sdr *AWSSDRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return sdr.provider.AdjustEndpoints(endpoints)
}

View File

@ -50,3 +50,8 @@ func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
func (im *NoopRegistry) PropertyValuesEqual(attribute string, previous string, current string) bool {
return im.provider.PropertyValuesEqual(attribute, previous, current)
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider
func (im *NoopRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return im.provider.AdjustEndpoints(endpoints)
}

View File

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

View File

@ -40,10 +40,15 @@ type TXTRegistry struct {
recordsCache []*endpoint.Endpoint
recordsCacheRefreshTime time.Time
cacheInterval time.Duration
// optional string to use to replace the asterisk in wildcard entries - without using this,
// registry TXT records corresponding to wildcard records will be invalid (and rejected by most providers), due to
// having a '*' appear (not as the first character) - see https://tools.ietf.org/html/rfc1034#section-4.3.3
wildcardReplacement string
}
// NewTXTRegistry returns new TXTRegistry object
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration) (*TXTRegistry, error) {
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string) (*TXTRegistry, error) {
if ownerID == "" {
return nil, errors.New("owner id cannot be empty")
}
@ -52,13 +57,14 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
return nil, errors.New("txt-prefix and txt-suffix are mutual exclusive")
}
mapper := newaffixNameMapper(txtPrefix, txtSuffix)
mapper := newaffixNameMapper(txtPrefix, txtSuffix, txtWildcardReplacement)
return &TXTRegistry{
provider: provider,
ownerID: ownerID,
mapper: mapper,
cacheInterval: cacheInterval,
provider: provider,
ownerID: ownerID,
mapper: mapper,
cacheInterval: cacheInterval,
wildcardReplacement: txtWildcardReplacement,
}, nil
}
@ -107,7 +113,13 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
if ep.Labels == nil {
ep.Labels = endpoint.NewLabels()
}
key := fmt.Sprintf("%s::%s", ep.DNSName, ep.SetIdentifier)
dnsNameSplit := strings.Split(ep.DNSName, ".")
// If specified, replace a leading asterisk in the generated txt record name with some other string
if im.wildcardReplacement != "" && dnsNameSplit[0] == "*" {
dnsNameSplit[0] = im.wildcardReplacement
}
dnsName := strings.Join(dnsNameSplit, ".")
key := fmt.Sprintf("%s::%s", dnsName, ep.SetIdentifier)
if labels, ok := labelMap[key]; ok {
for k, v := range labels {
ep.Labels[k] = v
@ -196,6 +208,11 @@ func (im *TXTRegistry) PropertyValuesEqual(name string, previous string, current
return im.provider.PropertyValuesEqual(name, previous, current)
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider
func (im *TXTRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return im.provider.AdjustEndpoints(endpoints)
}
/**
TXT registry specific private methods
*/
@ -211,14 +228,15 @@ type nameMapper interface {
}
type affixNameMapper struct {
prefix string
suffix string
prefix string
suffix string
wildcardReplacement string
}
var _ nameMapper = affixNameMapper{}
func newaffixNameMapper(prefix string, suffix string) affixNameMapper {
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix)}
func newaffixNameMapper(prefix string, suffix string, wildcardReplacement string) affixNameMapper {
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix), wildcardReplacement: strings.ToLower(wildcardReplacement)}
}
func (pr affixNameMapper) toEndpointName(txtDNSName string) string {
@ -238,6 +256,12 @@ func (pr affixNameMapper) toEndpointName(txtDNSName string) string {
func (pr affixNameMapper) toTXTName(endpointDNSName string) string {
DNSName := strings.SplitN(endpointDNSName, ".", 2)
// If specified, replace a leading asterisk in the generated txt record name with some other string
if pr.wildcardReplacement != "" && DNSName[0] == "*" {
DNSName[0] = pr.wildcardReplacement
}
if len(DNSName) < 2 {
return pr.prefix + DNSName[0] + pr.suffix
}

View File

@ -44,20 +44,20 @@ func TestTXTRegistry(t *testing.T) {
func testTXTRegistryNew(t *testing.T) {
p := inmemory.NewInMemoryProvider()
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour)
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "")
require.Error(t, err)
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour)
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "")
require.Error(t, err)
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour)
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "")
require.NoError(t, err)
assert.Equal(t, p, r.provider)
r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour)
r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "")
require.NoError(t, err)
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour)
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "")
require.Error(t, err)
_, ok := r.mapper.(affixNameMapper)
@ -65,7 +65,7 @@ func testTXTRegistryNew(t *testing.T) {
assert.Equal(t, "owner", r.ownerID)
assert.Equal(t, p, r.provider)
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour)
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "")
require.NoError(t, err)
_, ok = r.mapper.(affixNameMapper)
@ -97,6 +97,8 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("*.wildcard.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("txt.wc.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
},
})
expectedRecords := []*endpoint.Endpoint{
@ -169,15 +171,23 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "*.wildcard.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner",
},
},
}
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc")
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour)
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "")
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
@ -276,13 +286,13 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "")
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour)
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "")
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
@ -357,7 +367,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "")
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -394,7 +404,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
})
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "")
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
@ -488,13 +498,14 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) {
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
})
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard")
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
@ -517,6 +528,8 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) {
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"),
newEndpointWithOwner("example-txt", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"),
newEndpointWithOwner("wildcard-txt.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
@ -578,7 +591,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
},
})
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "")
changes := &plan.Changes{
Create: []*endpoint.Endpoint{

View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e
sed -i -e "s/newTag: .*/newTag: $1/g" kustomize/kustomization.yaml
git add kustomize/kustomization.yaml
git commit -sm "updates kustomize with newly released version"

43
scripts/releaser.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash
set -e
current_tag="${GITHUB_REF#refs/tags/}"
start_ref="HEAD"
function generate_changelog {
# Find the previous release on the same branch, skipping prereleases if the
# current tag is a full release
previous_tag=""
while [[ -z $previous_tag || ( $previous_tag == *-* && $current_tag != *-* ) ]]; do
previous_tag="$(git describe --tags "$start_ref"^ --abbrev=0)"
start_ref="$previous_tag"
done
git log "$previous_tag".. --reverse --merges --oneline --grep='Merge pull request #' | \
while read -r sha title; do
pr_num="$(grep -o '#[[:digit:]]\+' <<<"$title")"
pr_desc="$(git show -s --format=%b "$sha" | sed -n '1,/^$/p' | tr $'\n' ' ')"
pr_author="$(gh pr view "$pr_num" | grep author | awk '{ print $2 }' | tr $'\n' ' ')"
printf "* %s (%s) @%s\n\n" "$pr_desc" "$pr_num" "$pr_author"
done
git log "$previous_tag".. --reverse --oneline --grep='(#' | \
while read -r sha title; do
pr_num="$(grep -o '#[[:digit:]]\+' <<<"$title")"
pr_desc="$(git show -s --format=%s "$sha")"
pr_author="$(gh pr view "$pr_num" | grep author | awk '{ print $2 }' | tr $'\n' ' ')"
printf "* %s (%s) @%s\n\n" "$pr_desc" "$pr_num" "$pr_author"
done
}
function create_release {
generate_changelog | gh release create "$1" -t "$1" -F -
}
if [ $# -ne 1 ]; then
echo "$0: usage: releaser [release number]"
echo "example: ./releaser.sh v0.7.5"
exit 1
fi
create_release "$1"

283
source/ambassador_host.go Normal file
View File

@ -0,0 +1,283 @@
/*
Copyright 2020 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 (
"context"
"fmt"
"sort"
"strings"
"time"
ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/cache"
api "k8s.io/kubernetes/pkg/apis/core"
"sigs.k8s.io/external-dns/endpoint"
)
// ambHostAnnotation is the annotation in the Host that maps to a Service
const ambHostAnnotation = "external-dns.ambassador-service"
// groupName is the group name for the Ambassador API
const groupName = "getambassador.io"
var schemeGroupVersion = schema.GroupVersion{Group: groupName, Version: "v2"}
var ambHostGVR = schemeGroupVersion.WithResource("hosts")
// ambassadorHostSource is an implementation of Source for Ambassador Host objects.
// The IngressRoute implementation uses the spec.virtualHost.fqdn value for the hostname.
// Use targetAnnotationKey to explicitly set Endpoint.
type ambassadorHostSource struct {
dynamicKubeClient dynamic.Interface
kubeClient kubernetes.Interface
namespace string
ambassadorHostInformer informers.GenericInformer
unstructuredConverter *unstructuredConverter
}
// NewAmbassadorHostSource creates a new ambassadorHostSource with the given config.
func NewAmbassadorHostSource(
dynamicKubeClient dynamic.Interface,
kubeClient kubernetes.Interface,
namespace string) (Source, error) {
var err error
// Use shared informer to listen for add/update/delete of Host in the specified namespace.
// Set resync period to 0, to prevent processing when nothing has changed.
informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil)
ambassadorHostInformer := informerFactory.ForResource(ambHostGVR)
// Add default resource event handlers to properly initialize informer.
ambassadorHostInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
},
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
// wait for the local cache to be populated.
err = poll(time.Second, 60*time.Second, func() (bool, error) {
return ambassadorHostInformer.Informer().HasSynced(), nil
})
if err != nil {
return nil, errors.Wrapf(err, "failed to sync cache")
}
uc, err := newUnstructuredConverter()
if err != nil {
return nil, errors.Wrapf(err, "failed to setup Unstructured Converter")
}
return &ambassadorHostSource{
dynamicKubeClient: dynamicKubeClient,
kubeClient: kubeClient,
namespace: namespace,
ambassadorHostInformer: ambassadorHostInformer,
unstructuredConverter: uc,
}, nil
}
// Endpoints returns endpoint objects for each host-target combination that should be processed.
// Retrieves all Hosts in the source's namespace(s).
func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
hosts, err := sc.ambassadorHostInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything())
if err != nil {
return nil, err
}
endpoints := []*endpoint.Endpoint{}
for _, hostObj := range hosts {
unstructuredHost, ok := hostObj.(*unstructured.Unstructured)
if !ok {
return nil, errors.New("could not convert")
}
host := &ambassador.Host{}
err := sc.unstructuredConverter.scheme.Convert(unstructuredHost, host, nil)
if err != nil {
return nil, err
}
fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name)
// look for the "exernal-dns.ambassador-service" annotation. If it is not there then just ignore this `Host`
service, found := host.Annotations[ambHostAnnotation]
if !found {
log.Debugf("Host %s ignored: no annotation %q found", fullname, ambHostAnnotation)
continue
}
targets, err := sc.targetsFromAmbassadorLoadBalancer(ctx, service)
if err != nil {
return nil, err
}
hostEndpoints, err := sc.endpointsFromHost(ctx, host, targets)
if err != nil {
return nil, err
}
if len(hostEndpoints) == 0 {
log.Debugf("No endpoints could be generated from Host %s", fullname)
continue
}
log.Debugf("Endpoints generated from Host: %s: %v", fullname, hostEndpoints)
endpoints = append(endpoints, hostEndpoints...)
}
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
return endpoints, nil
}
// endpointsFromHost extracts the endpoints from a Host object
func (sc *ambassadorHostSource) endpointsFromHost(ctx context.Context, host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
providerSpecific := endpoint.ProviderSpecific{}
setIdentifier := ""
annotations := host.Annotations
ttl, err := getTTLFromAnnotations(annotations)
if err != nil {
return nil, err
}
if host.Spec != nil {
hostname := host.Spec.Hostname
if hostname != "" {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
}
}
return endpoints, nil
}
func (sc *ambassadorHostSource) targetsFromAmbassadorLoadBalancer(ctx context.Context, service string) (targets endpoint.Targets, err error) {
lbNamespace, lbName, err := parseAmbLoadBalancerService(service)
if err != nil {
return nil, err
}
svc, err := sc.kubeClient.CoreV1().Services(lbNamespace).Get(ctx, lbName, metav1.GetOptions{})
if err != nil {
return nil, err
}
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
return
}
// parseAmbLoadBalancerService returns a name/namespace tuple from the annotation in
// an Ambassador Host CRD
//
// This is a thing because Ambassador has historically supported cross-namespace
// references using a name.namespace syntax, but here we want to also support
// namespace/name.
//
// Returns namespace, name, error.
func parseAmbLoadBalancerService(service string) (namespace, name string, err error) {
// Start by assuming that we have namespace/name.
parts := strings.Split(service, "/")
if len(parts) == 1 {
// No "/" at all, so let's try for name.namespace. To be consistent with the
// rest of Ambassador, use SplitN to limit this to one split, so that e.g.
// svc.foo.bar uses service "svc" in namespace "foo.bar".
parts = strings.SplitN(service, ".", 2)
if len(parts) == 2 {
// We got a namespace, great.
name := parts[0]
namespace := parts[1]
return namespace, name, nil
}
// If here, we have no separator, so the whole string is the service, and
// we can assume the default namespace.
name := service
namespace := api.NamespaceDefault
return namespace, name, nil
} else if len(parts) == 2 {
// This is "namespace/name". Note that the name could be qualified,
// which is fine.
namespace := parts[0]
name := parts[1]
return namespace, name, nil
}
// If we got here, this string is simply ill-formatted. Return an error.
return "", "", errors.New(fmt.Sprintf("invalid external-dns service: %s", service))
}
func (sc *ambassadorHostSource) AddEventHandler(ctx context.Context, handler func()) {
}
// unstructuredConverter handles conversions between unstructured.Unstructured and Ambassador types
type unstructuredConverter struct {
// scheme holds an initializer for converting Unstructured to a type
scheme *runtime.Scheme
}
// newUnstructuredConverter returns a new unstructuredConverter initialized
func newUnstructuredConverter() (*unstructuredConverter, error) {
uc := &unstructuredConverter{
scheme: runtime.NewScheme(),
}
// Setup converter to understand custom CRD types
ambassador.AddToScheme(uc.scheme)
// Add the core types we need
if err := scheme.AddToScheme(uc.scheme); err != nil {
return nil, err
}
return uc, nil
}

View File

@ -0,0 +1,78 @@
/*
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 AmbassadorSuite struct {
suite.Suite
}
func TestAmbassadorSource(t *testing.T) {
suite.Run(t, new(AmbassadorSuite))
t.Run("Interface", testAmbassadorSourceImplementsSource)
}
// testAmbassadorSourceImplementsSource tests that ambassadorHostSource is a valid Source.
func testAmbassadorSourceImplementsSource(t *testing.T) {
require.Implements(t, (*Source)(nil), new(ambassadorHostSource))
}
// TestParseAmbLoadBalancerService tests our parsing of Ambassador service info.
func TestParseAmbLoadBalancerService(t *testing.T) {
vectors := []struct {
input string
ns string
svc string
errstr string
}{
{"svc", "default", "svc", ""},
{"ns/svc", "ns", "svc", ""},
{"svc.ns", "ns", "svc", ""},
{"svc.ns.foo.bar", "ns.foo.bar", "svc", ""},
{"ns/svc/foo/bar", "", "", "invalid external-dns service: ns/svc/foo/bar"},
{"ns/svc/foo.bar", "", "", "invalid external-dns service: ns/svc/foo.bar"},
{"ns.foo/svc/bar", "", "", "invalid external-dns service: ns.foo/svc/bar"},
}
for _, v := range vectors {
ns, svc, err := parseAmbLoadBalancerService(v.input)
errstr := ""
if err != nil {
errstr = err.Error()
}
if v.ns != ns {
t.Errorf("%s: got ns \"%s\", wanted \"%s\"", v.input, ns, v.ns)
}
if v.svc != svc {
t.Errorf("%s: got svc \"%s\", wanted \"%s\"", v.input, svc, v.svc)
}
if v.errstr != errstr {
t.Errorf("%s: got err \"%s\", wanted \"%s\"", v.input, errstr, v.errstr)
}
}
}

View File

@ -43,6 +43,7 @@ type crdSource struct {
crdResource string
codec runtime.ParameterCodec
annotationFilter string
labelFilter string
}
func addKnownTypes(scheme *runtime.Scheme, groupVersion schema.GroupVersion) error {
@ -102,11 +103,12 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiS
}
// NewCRDSource creates a new crdSource with the given config.
func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFilter string, scheme *runtime.Scheme) (Source, error) {
func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFilter string, labelFilter string, scheme *runtime.Scheme) (Source, error) {
return &crdSource{
crdResource: strings.ToLower(kind) + "s",
namespace: namespace,
annotationFilter: annotationFilter,
labelFilter: labelFilter,
crdClient: crdClient,
codec: runtime.NewParameterCodec(scheme),
}, nil
@ -119,12 +121,22 @@ func (cs *crdSource) AddEventHandler(ctx context.Context, handler func()) {
func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
endpoints := []*endpoint.Endpoint{}
result, err := cs.List(ctx, &metav1.ListOptions{})
var (
result *endpoint.DNSEndpointList
err error
)
if cs.labelFilter != "" {
result, err = cs.List(ctx, &metav1.ListOptions{LabelSelector: cs.labelFilter})
} else {
result, err = cs.List(ctx, &metav1.ListOptions{})
}
if err != nil {
return nil, err
}
result, err = cs.filterByAnnotations(result)
if err != nil {
return nil, err
}

View File

@ -57,7 +57,7 @@ func objBody(codec runtime.Encoder, 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, annotations map[string]string, t *testing.T) rest.Interface {
func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string, annotations map[string]string, labels map[string]string, t *testing.T) rest.Interface {
groupVersion, _ := schema.ParseGroupVersion(apiVersion)
scheme := runtime.NewScheme()
addKnownTypes(scheme, groupVersion)
@ -72,6 +72,7 @@ func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, ki
Name: name,
Namespace: namespace,
Annotations: annotations,
Labels: labels,
Generation: 1,
},
Spec: endpoint.DNSEndpointSpec{
@ -139,7 +140,9 @@ func testCRDSourceEndpoints(t *testing.T) {
expectEndpoints bool
expectError bool
annotationFilter string
labelFilter string
annotations map[string]string
labels map[string]string
}{
{
title: "invalid crd api version",
@ -308,16 +311,76 @@ func testCRDSourceEndpoints(t *testing.T) {
expectEndpoints: true,
expectError: false,
},
{
title: "valid crd gvk with label and non matching label filter",
registeredAPIVersion: "test.k8s.io/v1alpha1",
apiVersion: "test.k8s.io/v1alpha1",
registeredKind: "DNSEndpoint",
kind: "DNSEndpoint",
namespace: "foo",
registeredNamespace: "foo",
labels: map[string]string{"test": "that"},
labelFilter: "test=filter_something_else",
endpoints: []*endpoint.Endpoint{
{DNSName: "abc.example.org",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 180,
},
},
expectEndpoints: false,
expectError: false,
},
{
title: "valid crd gvk with label and matching label filter",
registeredAPIVersion: "test.k8s.io/v1alpha1",
apiVersion: "test.k8s.io/v1alpha1",
registeredKind: "DNSEndpoint",
kind: "DNSEndpoint",
namespace: "foo",
registeredNamespace: "foo",
labels: map[string]string{"test": "that"},
labelFilter: "test=that",
endpoints: []*endpoint.Endpoint{
{DNSName: "abc.example.org",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 180,
},
},
expectEndpoints: true,
expectError: false,
},
{
title: "Create NS record",
registeredAPIVersion: "test.k8s.io/v1alpha1",
apiVersion: "test.k8s.io/v1alpha1",
registeredKind: "DNSEndpoint",
kind: "DNSEndpoint",
namespace: "foo",
registeredNamespace: "foo",
labels: map[string]string{"test": "that"},
labelFilter: "test=that",
endpoints: []*endpoint.Endpoint{
{DNSName: "abc.example.org",
Targets: endpoint.Targets{"ns1.k8s.io", "ns2.k8s.io"},
RecordType: endpoint.RecordTypeNS,
RecordTTL: 180,
},
},
expectEndpoints: true,
expectError: false,
},
} {
t.Run(ti.title, func(t *testing.T) {
restClient := startCRDServerToServeTargets(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "test", ti.annotations, t)
restClient := startCRDServerToServeTargets(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "test", ti.annotations, ti.labels, t)
groupVersion, err := schema.ParseGroupVersion(ti.apiVersion)
require.NoError(t, err)
scheme := runtime.NewScheme()
addKnownTypes(scheme, groupVersion)
cs, _ := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, scheme)
cs, _ := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, ti.labelFilter, scheme)
receivedEndpoints, err := cs.Endpoints(context.Background())
if ti.expectError {

View File

@ -128,13 +128,13 @@ func (sc *httpProxySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint,
// Convert to []*projectcontour.HTTPProxy
var httpProxies []*projectcontour.HTTPProxy
for _, hp := range hps {
unstrucuredHP, ok := hp.(*unstructured.Unstructured)
unstructuredHP, ok := hp.(*unstructured.Unstructured)
if !ok {
return nil, errors.New("could not convert")
}
hpConverted := &projectcontour.HTTPProxy{}
err := sc.unstructuredConverter.scheme.Convert(unstrucuredHP, hpConverted, nil)
err := sc.unstructuredConverter.scheme.Convert(unstructuredHP, hpConverted, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to convert to HTTPProxy")
}

View File

@ -56,10 +56,11 @@ type ingressSource struct {
combineFQDNAnnotation bool
ignoreHostnameAnnotation bool
ingressInformer extinformers.IngressInformer
ignoreIngressTLSSpec bool
}
// NewIngressSource creates a new ingressSource with the given config.
func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool) (Source, error) {
func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool) (Source, error) {
var (
tmpl *template.Template
err error
@ -105,6 +106,7 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt
combineFQDNAnnotation: combineFqdnAnnotation,
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
ingressInformer: ingressInformer,
ignoreIngressTLSSpec: ignoreIngressTLSSpec,
}
return sc, nil
}
@ -132,7 +134,7 @@ func (sc *ingressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
continue
}
ingEndpoints := endpointsFromIngress(ing, sc.ignoreHostnameAnnotation)
ingEndpoints := endpointsFromIngress(ing, sc.ignoreHostnameAnnotation, sc.ignoreIngressTLSSpec)
// apply template if host is missing on ingress
if (sc.combineFQDNAnnotation || len(ingEndpoints) == 0) && sc.fqdnTemplate != nil {
@ -240,7 +242,7 @@ func (sc *ingressSource) setDualstackLabel(ingress *v1beta1.Ingress, endpoints [
}
// endpointsFromIngress extracts the endpoints from ingress object
func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) []*endpoint.Endpoint {
func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
ttl, err := getTTLFromAnnotations(ing.Annotations)
@ -263,12 +265,15 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [
endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...)
}
for _, tls := range ing.Spec.TLS {
for _, host := range tls.Hosts {
if host == "" {
continue
// Skip endpoints if we do not want entries from tls spec section
if !ignoreIngressTLSSpec {
for _, tls := range ing.Spec.TLS {
for _, host := range tls.Hosts {
if host == "" {
continue
}
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...)
}
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...)
}
}

View File

@ -52,6 +52,7 @@ func (suite *IngressSuite) SetupTest() {
"{{.Name}}",
false,
false,
false,
)
suite.NoError(err, "should initialize ingress source")
@ -134,6 +135,7 @@ func TestNewIngressSource(t *testing.T) {
ti.fqdnTemplate,
ti.combineFQDNAndAnnotation,
false,
false,
)
if ti.expectError {
assert.Error(t, err)
@ -221,7 +223,7 @@ func testEndpointsFromIngress(t *testing.T) {
} {
t.Run(ti.title, func(t *testing.T) {
realIngress := ti.ingress.Ingress()
validateEndpoints(t, endpointsFromIngress(realIngress, false), ti.expected)
validateEndpoints(t, endpointsFromIngress(realIngress, false, false), ti.expected)
})
}
}
@ -238,6 +240,7 @@ func testIngressEndpoints(t *testing.T) {
fqdnTemplate string
combineFQDNAndAnnotation bool
ignoreHostnameAnnotation bool
ignoreIngressTLSSpec bool
}{
{
title: "no ingress",
@ -993,6 +996,39 @@ func testIngressEndpoints(t *testing.T) {
},
},
},
{
title: "ignore tls section",
targetNamespace: "",
ignoreIngressTLSSpec: true,
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
tlsdnsnames: [][]string{{"example.org"}},
ips: []string{"1.2.3.4"},
},
},
expected: []*endpoint.Endpoint{},
},
{
title: "reading tls section",
targetNamespace: "",
ignoreIngressTLSSpec: false,
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
tlsdnsnames: [][]string{{"example.org"}},
ips: []string{"1.2.3.4"},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Targets: endpoint.Targets{"1.2.3.4"},
},
},
},
} {
t.Run(ti.title, func(t *testing.T) {
ingresses := make([]*v1beta1.Ingress, 0)
@ -1008,6 +1044,7 @@ func testIngressEndpoints(t *testing.T) {
ti.fqdnTemplate,
ti.combineFQDNAndAnnotation,
ti.ignoreHostnameAnnotation,
ti.ignoreIngressTLSSpec,
)
for _, ingress := range ingresses {
_, err := fakeClient.ExtensionsV1beta1().Ingresses(ingress.Namespace).Create(context.Background(), ingress, metav1.CreateOptions{})

View File

@ -217,7 +217,7 @@ func NewRouteGroupSource(timeout time.Duration, token, tokenPath, apiServerURL,
}
apiServer := u.String()
// strip port if well known port, because of TLS certifcate match
// strip port if well known port, because of TLS certificate match
if u.Scheme == "https" && u.Port() == "443" {
apiServer = "https://" + u.Hostname()
}

View File

@ -214,6 +214,35 @@ func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
endpoints = append(endpoints, svcEndpoints...)
}
// this sorting is required to make merging work.
// after we merge endpoints that have same DNS, we want to ensure that we end up with the same service being an "owner"
// of all those records, as otherwise each time we update, we will end up with a different service that gets data merged in
// and that will cause external-dns to recreate dns record due to different service owner in TXT record.
// if new service is added or old one removed, that might cause existing record to get re-created due to potentially new
// owner being selected. Which is fine, since it shouldn't happen often and shouldn't cause any disruption.
if len(endpoints) > 1 {
sort.Slice(endpoints, func(i, j int) bool {
return endpoints[i].Labels[endpoint.ResourceLabelKey] < endpoints[j].Labels[endpoint.ResourceLabelKey]
})
// Use stable sort to not disrupt the order of services
sort.SliceStable(endpoints, func(i, j int) bool {
return endpoints[i].DNSName < endpoints[j].DNSName
})
mergedEndpoints := []*endpoint.Endpoint{}
mergedEndpoints = append(mergedEndpoints, endpoints[0])
for i := 1; i < len(endpoints); i++ {
lastMergedEndpoint := len(mergedEndpoints) - 1
if mergedEndpoints[lastMergedEndpoint].DNSName == endpoints[i].DNSName &&
mergedEndpoints[lastMergedEndpoint].RecordType == endpoints[i].RecordType &&
mergedEndpoints[lastMergedEndpoint].RecordTTL == endpoints[i].RecordTTL {
mergedEndpoints[lastMergedEndpoint].Targets = append(mergedEndpoints[lastMergedEndpoint].Targets, endpoints[i].Targets[0])
} else {
mergedEndpoints = append(mergedEndpoints, endpoints[i])
}
}
endpoints = mergedEndpoints
}
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
@ -333,7 +362,7 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.End
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",")
for _, hostname := range hostnameList {
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier)...)
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, false)...)
}
return endpoints, nil
@ -345,9 +374,17 @@ func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint {
// Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation {
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
hostnameList := getHostnamesFromAnnotations(svc.Annotations)
var hostnameList []string
var internalHostnameList []string
hostnameList = getHostnamesFromAnnotations(svc.Annotations)
for _, hostname := range hostnameList {
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier)...)
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, false)...)
}
internalHostnameList = getInternalHostnamesFromAnnotations(svc.Annotations)
for _, hostname := range internalHostnameList {
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, true)...)
}
}
return endpoints
@ -403,7 +440,7 @@ func (sc *serviceSource) setResourceLabel(service *v1.Service, endpoints []*endp
}
}
func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, providerSpecific endpoint.ProviderSpecific, setIdentifier string) []*endpoint.Endpoint {
func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, providerSpecific endpoint.ProviderSpecific, setIdentifier string, useClusterIP bool) []*endpoint.Endpoint {
hostname = strings.TrimSuffix(hostname, ".")
ttl, err := getTTLFromAnnotations(svc.Annotations)
if err != nil {
@ -431,7 +468,11 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
switch svc.Spec.Type {
case v1.ServiceTypeLoadBalancer:
targets = append(targets, extractLoadBalancerTargets(svc)...)
if useClusterIP {
targets = append(targets, extractServiceIps(svc)...)
} else {
targets = append(targets, extractLoadBalancerTargets(svc)...)
}
case v1.ServiceTypeClusterIP:
if sc.publishInternal {
targets = append(targets, extractServiceIps(svc)...)
@ -486,7 +527,10 @@ func extractServiceExternalName(svc *v1.Service) endpoint.Targets {
}
func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets {
var targets endpoint.Targets
var (
targets endpoint.Targets
externalIPs endpoint.Targets
)
// Create a corresponding endpoint for each configured external entrypoint.
for _, lb := range svc.Status.LoadBalancer.Ingress {
@ -498,6 +542,16 @@ func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets {
}
}
if svc.Spec.ExternalIPs != nil {
for _, ext := range svc.Spec.ExternalIPs {
externalIPs = append(externalIPs, ext)
}
}
if len(externalIPs) > 0 {
return externalIPs
}
return targets
}
@ -556,10 +610,16 @@ func (sc *serviceSource) extractNodePortTargets(svc *v1.Service) (endpoint.Targe
}
}
access := getAccessFromAnnotations(svc.Annotations)
if access == "public" {
return externalIPs, nil
}
if access == "private" {
return internalIPs, nil
}
if len(externalIPs) > 0 {
return externalIPs, nil
}
return internalIPs, nil
}
@ -568,14 +628,17 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets e
for _, port := range svc.Spec.Ports {
if port.NodePort > 0 {
// following the RFC 2782, SRV record must have a following format
// _service._proto.name. TTL class SRV priority weight port
// see https://en.wikipedia.org/wiki/SRV_record
// build a target with a priority of 0, weight of 0, and pointing the given port on the given host
target := fmt.Sprintf("0 50 %d %s", port.NodePort, hostname)
// figure out the portname
portName := port.Name
if portName == "" {
portName = fmt.Sprintf("%d", port.NodePort)
}
// take the service name from the K8s Service object
// it is safe to use since it is DNS compatible
// see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
serviceName := svc.ObjectMeta.Name
// figure out the protocol
protocol := strings.ToLower(string(port.Protocol))
@ -583,7 +646,7 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets e
protocol = "tcp"
}
recordName := fmt.Sprintf("_%s._%s.%s", portName, protocol, hostname)
recordName := fmt.Sprintf("_%s._%s.%s", serviceName, protocol, hostname)
var ep *endpoint.Endpoint
if ttl.IsConfigured() {

View File

@ -19,6 +19,8 @@ package source
import (
"context"
"net"
"sort"
"strings"
"testing"
"time"
@ -93,6 +95,7 @@ func TestServiceSource(t *testing.T) {
t.Run("Interface", testServiceSourceImplementsSource)
t.Run("NewServiceSource", testServiceSourceNewServiceSource)
t.Run("Endpoints", testServiceSourceEndpoints)
t.Run("MultipleServices", testMultipleServicesEndpoints)
}
// testServiceSourceImplementsSource tests that serviceSource is a valid Source.
@ -174,6 +177,7 @@ func testServiceSourceEndpoints(t *testing.T) {
labels map[string]string
annotations map[string]string
clusterIP string
externalIPs []string
lbs []string
serviceTypesFilter []string
expected []*endpoint.Endpoint
@ -193,13 +197,14 @@ func testServiceSourceEndpoints(t *testing.T) {
map[string]string{},
map[string]string{},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
false,
},
{
"no annotated services return no endpoints when ignoreing annotations",
"no annotated services return no endpoints when ignoring annotations",
"",
"",
"testing",
@ -212,6 +217,7 @@ func testServiceSourceEndpoints(t *testing.T) {
map[string]string{},
map[string]string{},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
@ -233,6 +239,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -256,6 +263,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
@ -279,6 +287,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"1.2.3.4",
[]string{},
[]string{},
[]string{},
[]*endpoint.Endpoint{},
false,
},
@ -296,6 +305,7 @@ func testServiceSourceEndpoints(t *testing.T) {
map[string]string{},
map[string]string{},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -305,7 +315,7 @@ func testServiceSourceEndpoints(t *testing.T) {
false,
},
{
"FQDN template with multiple hostnames return an endpoint with target IP when ignoreing annotations",
"FQDN template with multiple hostnames return an endpoint with target IP when ignoring annotations",
"",
"",
"testing",
@ -318,6 +328,7 @@ func testServiceSourceEndpoints(t *testing.T) {
map[string]string{},
map[string]string{},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -342,6 +353,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -368,6 +380,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -392,6 +405,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -416,6 +430,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org, bar.example.org",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -440,6 +455,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"lb.example.com"}, // Kubernetes omits the trailing dot
[]string{},
[]*endpoint.Endpoint{
@ -463,6 +479,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted
},
"",
[]string{},
[]string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot
[]string{},
[]*endpoint.Endpoint{
@ -488,6 +505,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -512,6 +530,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
@ -533,6 +552,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -556,6 +576,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
@ -577,6 +598,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -601,6 +623,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -625,6 +648,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"service.beta.kubernetes.io/external-traffic": "SomethingElse",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
@ -647,6 +671,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
@ -669,6 +694,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"service.beta.kubernetes.io/external-traffic": "Global",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -693,6 +719,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
@ -716,9 +743,34 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{},
[]string{},
[]string{},
[]*endpoint.Endpoint{},
false,
},
{
"annotated service with externalIPs returns a single endpoint with multiple targets",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{"10.2.3.4", "11.2.3.4"},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}},
},
false,
},
{
"multiple external entrypoints return a single endpoint with multiple targets",
"",
@ -735,6 +787,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4", "8.8.8.8"},
[]string{},
[]*endpoint.Endpoint{
@ -758,6 +811,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"zalando.org/dnsname": "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
@ -779,6 +833,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"zalando.org/dnsname": "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -804,6 +859,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"domainName": "foo.example.org., bar.example.org",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -826,6 +882,7 @@ func testServiceSourceEndpoints(t *testing.T) {
map[string]string{},
map[string]string{},
"",
[]string{},
[]string{"1.2.3.4", "elb.com"},
[]string{},
[]*endpoint.Endpoint{
@ -850,6 +907,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4", "elb.com"},
[]string{},
[]*endpoint.Endpoint{
@ -874,6 +932,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"zalando.org/dnsname": "mate.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -895,6 +954,7 @@ func testServiceSourceEndpoints(t *testing.T) {
map[string]string{},
map[string]string{},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{},
@ -916,6 +976,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -940,6 +1001,7 @@ func testServiceSourceEndpoints(t *testing.T) {
ttlAnnotationKey: "foo",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -964,6 +1026,7 @@ func testServiceSourceEndpoints(t *testing.T) {
ttlAnnotationKey: "10",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -988,6 +1051,7 @@ func testServiceSourceEndpoints(t *testing.T) {
ttlAnnotationKey: "1m",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -1012,6 +1076,7 @@ func testServiceSourceEndpoints(t *testing.T) {
ttlAnnotationKey: "-10",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
@ -1035,6 +1100,7 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{string(v1.ServiceTypeLoadBalancer)},
[]*endpoint.Endpoint{
@ -1058,11 +1124,62 @@ func testServiceSourceEndpoints(t *testing.T) {
hostnameAnnotationKey: "foo.example.org.",
},
"",
[]string{},
[]string{"1.2.3.4"},
[]string{string(v1.ServiceTypeLoadBalancer)},
[]*endpoint.Endpoint{},
false,
},
{
"internal-host annotated services return an endpoint with Cluster IP",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
map[string]string{
internalHostnameAnnotationKey: "foo.internal.example.org.",
},
"1.1.1.1",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.internal.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
},
false,
},
{
"internal-host annotated and host annotated services return an endpoint with Cluster IP and an endpoint with lb IP",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
internalHostnameAnnotationKey: "foo.internal.example.org.",
},
"1.1.1.1",
[]string{},
[]string{"1.2.3.4"},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.internal.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
} {
t.Run(tc.title, func(t *testing.T) {
// Create a Kubernetes testing client
@ -1080,8 +1197,9 @@ func testServiceSourceEndpoints(t *testing.T) {
service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ClusterIP: tc.clusterIP,
Type: tc.svcType,
ClusterIP: tc.clusterIP,
ExternalIPs: tc.externalIPs,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
@ -1139,6 +1257,191 @@ func testServiceSourceEndpoints(t *testing.T) {
}
}
// testMultipleServicesEndpoints tests that multiple services generate correct merged endpoints
func testMultipleServicesEndpoints(t *testing.T) {
for _, tc := range []struct {
title string
targetNamespace string
annotationFilter string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
combineFQDNAndAnnotation bool
ignoreHostnameAnnotation bool
labels map[string]string
clusterIP string
hostnames map[string]string
serviceTypesFilter []string
expected []*endpoint.Endpoint
expectError bool
}{
{
"test service returns a correct end point",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
"",
map[string]string{
"1.2.3.4": "foo.example.org",
},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo1.2.3.4"}},
},
false,
},
{
"multiple services that share same DNS should be merged into one endpoint",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
"",
map[string]string{
"1.2.3.4": "foo.example.org",
"1.2.3.5": "foo.example.org",
"1.2.3.6": "foo.example.org",
},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5", "1.2.3.6"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo1.2.3.4"}},
},
false,
},
{
"test that services with different hostnames do not get merged together",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
"",
map[string]string{
"1.2.3.5": "foo.example.org",
"10.1.1.3": "bar.example.org",
"10.1.1.1": "bar.example.org",
"1.2.3.4": "foo.example.org",
"10.1.1.2": "bar.example.org",
"20.1.1.1": "foobar.example.org",
"1.2.3.6": "foo.example.org",
},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5", "1.2.3.6"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo1.2.3.4"}},
{DNSName: "bar.example.org", Targets: endpoint.Targets{"10.1.1.1", "10.1.1.2", "10.1.1.3"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo10.1.1.1"}},
{DNSName: "foobar.example.org", Targets: endpoint.Targets{"20.1.1.1"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo20.1.1.1"}},
},
false,
},
} {
t.Run(tc.title, func(t *testing.T) {
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()
// Create services to test against
for serviceip, hostname := range tc.hostnames {
ingresses := []v1.LoadBalancerIngress{}
ingresses = append(ingresses, v1.LoadBalancerIngress{IP: serviceip})
annotations := make(map[string]string)
annotations[hostnameAnnotationKey] = hostname
service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ClusterIP: tc.clusterIP,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName + serviceip,
Labels: tc.labels,
Annotations: annotations,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: ingresses,
},
},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{})
require.NoError(t, err)
}
// Create our object under test and get the endpoints.
client, err := NewServiceSource(
kubernetes,
tc.targetNamespace,
tc.annotationFilter,
tc.fqdnTemplate,
tc.combineFQDNAndAnnotation,
tc.compatibility,
false,
false,
false,
tc.serviceTypesFilter,
tc.ignoreHostnameAnnotation,
)
require.NoError(t, err)
var res []*endpoint.Endpoint
// wait up to a few seconds for new resources to appear in informer cache.
err = poll(time.Second, 3*time.Second, func() (bool, error) {
res, err = client.Endpoints(context.Background())
if err != nil {
// stop waiting if we get an error
return true, err
}
return len(res) >= len(tc.expected), nil
})
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Validate returned endpoints against desired endpoints.
validateEndpoints(t, res, tc.expected)
// Test that endpoint resourceLabelKey matches desired endpoint
sort.SliceStable(res, func(i, j int) bool {
return strings.Compare(res[i].DNSName, res[j].DNSName) < 0
})
sort.SliceStable(tc.expected, func(i, j int) bool {
return strings.Compare(tc.expected[i].DNSName, tc.expected[j].DNSName) < 0
})
for i := range res {
if res[i].Labels[endpoint.ResourceLabelKey] != tc.expected[i].Labels[endpoint.ResourceLabelKey] {
t.Errorf("expected %s, got %s", tc.expected[i].Labels[endpoint.ResourceLabelKey], res[i].Labels[endpoint.ResourceLabelKey])
}
}
})
}
}
// testServiceSourceEndpoints tests that various services generate the correct endpoints.
func TestClusterIpServices(t *testing.T) {
for _, tc := range []struct {
@ -1339,7 +1642,7 @@ func TestNodePortServices(t *testing.T) {
},
nil,
[]*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
},
false,
@ -1426,7 +1729,7 @@ func TestNodePortServices(t *testing.T) {
map[string]string{},
nil,
[]*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "_foo._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
},
false,
@ -1472,7 +1775,7 @@ func TestNodePortServices(t *testing.T) {
},
nil,
[]*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
},
false,
@ -1516,7 +1819,7 @@ func TestNodePortServices(t *testing.T) {
},
nil,
[]*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
},
false,
@ -1562,7 +1865,7 @@ func TestNodePortServices(t *testing.T) {
},
nil,
[]*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
},
false,
@ -1591,6 +1894,100 @@ func TestNodePortServices(t *testing.T) {
[]int{1, 1},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
},
{
"access=private annotation NodePort services return an endpoint with private IP addresses of the cluster's nodes",
"",
"",
"testing",
"foo",
v1.ServiceTypeNodePort,
v1.ServiceExternalTrafficPolicyTypeCluster,
"",
"",
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
accessAnnotationKey: "private",
},
nil,
[]*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
},
false,
[]*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
},
},
}},
[]string{},
[]int{},
[]v1.PodPhase{},
},
{
"access=public annotation NodePort services return an endpoint with public IP addresses of the cluster's nodes",
"",
"",
"testing",
"foo",
v1.ServiceTypeNodePort,
v1.ServiceExternalTrafficPolicyTypeCluster,
"",
"",
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
accessAnnotationKey: "public",
},
nil,
[]*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
},
false,
[]*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
},
},
}},
[]string{},
[]int{},
[]v1.PodPhase{},
},
} {
t.Run(tc.title, func(t *testing.T) {
// Create a Kubernetes testing client

View File

@ -38,6 +38,8 @@ const (
controllerAnnotationKey = "external-dns.alpha.kubernetes.io/controller"
// The annotation used for defining the desired hostname
hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname"
// The annotation used for specifying whether the public or private interface address is used
accessAnnotationKey = "external-dns.alpha.kubernetes.io/access"
// The annotation used for defining the desired ingress target
targetAnnotationKey = "external-dns.alpha.kubernetes.io/target"
// The annotation used for defining the desired DNS record TTL
@ -46,6 +48,8 @@ const (
aliasAnnotationKey = "external-dns.alpha.kubernetes.io/alias"
// The value of the controller annotation so that we feel responsible
controllerAnnotationValue = "dns-controller"
// The annotation used for defining the desired hostname
internalHostnameAnnotationKey = "external-dns.alpha.kubernetes.io/internal-hostname"
)
// Provider-specific annotations
@ -107,6 +111,18 @@ func getHostnamesFromAnnotations(annotations map[string]string) []string {
return strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")
}
func getAccessFromAnnotations(annotations map[string]string) string {
return annotations[accessAnnotationKey]
}
func getInternalHostnamesFromAnnotations(annotations map[string]string) []string {
internalHostnameAnnotation, exists := annotations[internalHostnameAnnotationKey]
if !exists {
return nil
}
return strings.Split(strings.Replace(internalHostnameAnnotation, " ", "", -1), ",")
}
func getAliasFromAnnotations(annotations map[string]string) bool {
aliasAnnotation, exists := annotations[aliasAnnotationKey]
return exists && aliasAnnotation == "true"

View File

@ -75,7 +75,7 @@ func TestGetTTLFromAnnotations(t *testing.T) {
expectedErr: nil,
},
{
title: "TTL annotation value is set correcly using duration (fractional)",
title: "TTL annotation value is set correctly using duration (fractional)",
annotations: map[string]string{ttlAnnotationKey: "20.5s"},
expectedTTL: endpoint.TTL(20),
expectedErr: nil,

View File

@ -42,9 +42,11 @@ var ErrSourceNotFound = errors.New("source not found")
type Config struct {
Namespace string
AnnotationFilter string
LabelFilter string
FQDNTemplate string
CombineFQDNAndAnnotation bool
IgnoreHostnameAnnotation bool
IgnoreIngressTLSSpec bool
Compatibility string
PublishInternal bool
PublishHostIP bool
@ -81,12 +83,12 @@ type SingletonClientGenerator struct {
kubeClient kubernetes.Interface
istioClient *istioclient.Clientset
cfClient *cfclient.Client
contourClient dynamic.Interface
dynKubeClient dynamic.Interface
openshiftClient openshift.Interface
kubeOnce sync.Once
istioOnce sync.Once
cfOnce sync.Once
contourOnce sync.Once
dynCliOnce sync.Once
openshiftOnce sync.Once
}
@ -132,13 +134,13 @@ func NewCFClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*c
return client, nil
}
// DynamicKubernetesClient generates a contour client if it was not created before
// DynamicKubernetesClient generates a dynamic client if it was not created before
func (p *SingletonClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) {
var err error
p.contourOnce.Do(func() {
p.contourClient, err = NewDynamicKubernetesClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
p.dynCliOnce.Do(func() {
p.dynKubeClient, err = NewDynamicKubernetesClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
})
return p.contourClient, err
return p.dynKubeClient, err
}
// OpenShiftClient generates an openshift client if it was not created before
@ -184,7 +186,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil {
return nil, err
}
return NewIngressSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
return NewIngressSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec)
case "istio-gateway":
kubernetesClient, err := p.KubeClient()
if err != nil {
@ -211,6 +213,16 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
return nil, err
}
return NewCloudFoundrySource(cfClient)
case "ambassador-host":
kubernetesClient, err := p.KubeClient()
if err != nil {
return nil, err
}
dynamicClient, err := p.DynamicKubernetesClient()
if err != nil {
return nil, err
}
return NewAmbassadorHostSource(dynamicClient, kubernetesClient, cfg.Namespace)
case "contour-ingressroute":
kubernetesClient, err := p.KubeClient()
if err != nil {
@ -246,7 +258,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil {
return nil, err
}
return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, scheme)
return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme)
case "skipper-routegroup":
apiServerURL := cfg.APIServerURL
tokenPath := ""

View File

@ -157,5 +157,5 @@ func TestByNames(t *testing.T) {
}
var minimalConfig = &Config{
ContourLoadBalancerService: "heptio-contour/contour",
ContourLoadBalancerService: "heptio-contour/contour",
}