mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
Merge branch 'master' into oci-auth-instance-principal
This commit is contained in:
commit
e3feec4c8f
2
.github/ISSUE_TEMPLATE/-support-request.md
vendored
2
.github/ISSUE_TEMPLATE/-support-request.md
vendored
@ -2,7 +2,7 @@
|
||||
name: "❓Support Request"
|
||||
about: Support request or question relating to external-dns
|
||||
title: ''
|
||||
labels: triage/support
|
||||
labels: kind/support
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@ -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
2
.gitignore
vendored
@ -46,3 +46,5 @@ external-dns
|
||||
|
||||
# vendor dir
|
||||
vendor/
|
||||
|
||||
profile.cov
|
||||
|
@ -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
|
||||
|
||||
|
15
Dockerfile
15
Dockerfile
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
34
Makefile
34
Makefile
@ -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
|
||||
|
19
README.md
19
README.md
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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 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.
|
||||
* 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 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.
|
||||
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 can’t 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 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.
|
||||
* 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 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.
|
||||
* 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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
19
docs/faq.md
19
docs/faq.md
@ -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?
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 :-)
|
||||
|
251
docs/tutorials/akamai-edgedns.md
Normal file
251
docs/tutorials/akamai-edgedns.md
Normal 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.
|
||||
|
@ -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)
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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#
|
||||
```
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
23
docs/tutorials/ns-record.md
Normal file
23
docs/tutorials/ns-record.md
Normal 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`
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
...
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
22
go.mod
@ -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 (
|
||||
|
@ -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
27
main.go
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
38
plan/plan.go
38
plan/plan.go
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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{
|
||||
|
@ -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
129
provider/azure/config.go
Normal 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")
|
||||
}
|
67
provider/azure/config_test.go
Normal file
67
provider/azure/config_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"),
|
||||
}
|
||||
|
@ -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)...)
|
||||
|
@ -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"}},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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{}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
6
scripts/kustomize-version-udapter.sh
Executable file
6
scripts/kustomize-version-udapter.sh
Executable 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
43
scripts/releaser.sh
Executable 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
283
source/ambassador_host.go
Normal 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
|
||||
}
|
78
source/ambassador_host_test.go
Normal file
78
source/ambassador_host_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)...)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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{})
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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 := ""
|
||||
|
@ -157,5 +157,5 @@ func TestByNames(t *testing.T) {
|
||||
}
|
||||
|
||||
var minimalConfig = &Config{
|
||||
ContourLoadBalancerService: "heptio-contour/contour",
|
||||
ContourLoadBalancerService: "heptio-contour/contour",
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user