mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-07 10:06:57 +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"
|
name: "❓Support Request"
|
||||||
about: Support request or question relating to external-dns
|
about: Support request or question relating to external-dns
|
||||||
title: ''
|
title: ''
|
||||||
labels: triage/support
|
labels: kind/support
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: ^1.14
|
go-version: ^1.15
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
@ -30,6 +30,12 @@ jobs:
|
|||||||
dep ensure
|
dep ensure
|
||||||
fi
|
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
|
- name: Lint
|
||||||
run: |
|
run: |
|
||||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0
|
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
|
uses: shogo82148/actions-goveralls@v1
|
||||||
with:
|
with:
|
||||||
path-to-profile: profile.cov
|
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 dir
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
|
profile.cov
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
- Update contributing section in README (#1760) @seanmalloy
|
- Update contributing section in README (#1760) @seanmalloy
|
||||||
- Option to cache AWS zones list @bpineau
|
- Option to cache AWS zones list @bpineau
|
||||||
- Oracle OCI provider: add support for instance principal authentication (#1700) @ericrrath
|
- 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
|
## v0.7.3 - 2020-08-05
|
||||||
|
|
||||||
|
15
Dockerfile
15
Dockerfile
@ -13,22 +13,19 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
# builder image
|
# builder image
|
||||||
FROM golang:1.14 as builder
|
ARG ARCH
|
||||||
|
FROM golang:1.15 as builder
|
||||||
|
ARG ARCH
|
||||||
|
|
||||||
WORKDIR /sigs.k8s.io/external-dns
|
WORKDIR /sigs.k8s.io/external-dns
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go mod vendor && \
|
RUN make test && make build.$ARCH
|
||||||
make test && \
|
|
||||||
make build
|
|
||||||
|
|
||||||
# final image
|
# final image
|
||||||
FROM alpine:3.12
|
FROM $ARCH/alpine:3.12
|
||||||
LABEL maintainer="Team Teapot @ Zalando SE <team-teapot@zalando.de>"
|
|
||||||
|
|
||||||
RUN apk add --update --no-cache ca-certificates && \
|
|
||||||
update-ca-certificates
|
|
||||||
|
|
||||||
|
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
|
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:
|
# 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
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
FROM golang:1.14 as builder
|
FROM golang:1.15 as builder
|
||||||
|
|
||||||
WORKDIR /sigs.k8s.io/external-dns
|
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)
|
VERSION ?= $(shell git describe --tags --always --dirty)
|
||||||
BUILD_FLAGS ?= -v
|
BUILD_FLAGS ?= -v
|
||||||
LDFLAGS ?= -X sigs.k8s.io/external-dns/pkg/apis/externaldns.Version=$(VERSION) -w -s
|
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: build/$(BINARY)
|
||||||
|
|
||||||
build/$(BINARY): $(SOURCES)
|
build/$(BINARY): $(SOURCES)
|
||||||
CGO_ENABLED=0 go build -o build/$(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" .
|
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
|
build.push: build.docker
|
||||||
docker push "$(IMAGE):$(VERSION)"
|
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:
|
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:
|
build.mini:
|
||||||
docker build --rm --tag "$(IMAGE):$(VERSION)-mini" --build-arg VERSION="$(VERSION)" -f Dockerfile.mini .
|
docker build --rm --tag "$(IMAGE):$(VERSION)-mini" --build-arg VERSION="$(VERSION)" -f Dockerfile.mini .
|
||||||
@ -90,7 +118,7 @@ clean:
|
|||||||
.PHONY: release.staging
|
.PHONY: release.staging
|
||||||
|
|
||||||
release.staging:
|
release.staging:
|
||||||
IMAGE=$(IMAGE_STAGING) $(MAKE) build.docker build.push
|
IMAGE=$(IMAGE_STAGING) $(MAKE) build.push/multiarch
|
||||||
|
|
||||||
release.prod:
|
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/)
|
* [NS1](https://ns1.com/)
|
||||||
* [TransIP](https://www.transip.eu/domain-name/)
|
* [TransIP](https://www.transip.eu/domain-name/)
|
||||||
* [VinylDNS](https://www.vinyldns.io)
|
* [VinylDNS](https://www.vinyldns.io)
|
||||||
|
* [Vultr](https://www.vultr.com)
|
||||||
* [OVH](https://www.ovh.com)
|
* [OVH](https://www.ovh.com)
|
||||||
* [Scaleway](https://www.scaleway.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.
|
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 | |
|
| Google Cloud DNS | Stable | |
|
||||||
| AWS Route 53 | Stable | |
|
| AWS Route 53 | Stable | |
|
||||||
| AWS Cloud Map | Beta | |
|
| AWS Cloud Map | Beta | |
|
||||||
|
| Akamai Edge DNS | Beta | |
|
||||||
| AzureDNS | Beta | |
|
| AzureDNS | Beta | |
|
||||||
| CloudFlare | Beta | |
|
| CloudFlare | Beta | |
|
||||||
| RcodeZero | Alpha | |
|
| RcodeZero | Alpha | |
|
||||||
@ -96,7 +99,6 @@ The following table clarifies the current status of the providers according to t
|
|||||||
| TransIP | Alpha | |
|
| TransIP | Alpha | |
|
||||||
| VinylDNS | Alpha | |
|
| VinylDNS | Alpha | |
|
||||||
| RancherDNS | Alpha | |
|
| RancherDNS | Alpha | |
|
||||||
| Akamai FastDNS | Alpha | |
|
|
||||||
| OVH | Alpha | |
|
| OVH | Alpha | |
|
||||||
| Scaleway DNS | Alpha | @Sh4d1 |
|
| Scaleway DNS | Alpha | @Sh4d1 |
|
||||||
| Vultr | Alpha | |
|
| Vultr | Alpha | |
|
||||||
@ -140,6 +142,7 @@ The following tutorials are provided:
|
|||||||
* [Linode](docs/tutorials/linode.md)
|
* [Linode](docs/tutorials/linode.md)
|
||||||
* [Nginx Ingress Controller](docs/tutorials/nginx-ingress.md)
|
* [Nginx Ingress Controller](docs/tutorials/nginx-ingress.md)
|
||||||
* [NS1](docs/tutorials/ns1.md)
|
* [NS1](docs/tutorials/ns1.md)
|
||||||
|
* [NS Record Creation with CRD Source](docs/tutorials/ns-record.md)
|
||||||
* [OpenStack Designate](docs/tutorials/designate.md)
|
* [OpenStack Designate](docs/tutorials/designate.md)
|
||||||
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
|
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
|
||||||
* [PowerDNS](docs/tutorials/pdns.md)
|
* [PowerDNS](docs/tutorials/pdns.md)
|
||||||
@ -163,8 +166,8 @@ from source.
|
|||||||
Next, run an application and expose it via a Kubernetes Service:
|
Next, run an application and expose it via a Kubernetes Service:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ kubectl run nginx --image=nginx --replicas=1 --port=80
|
$ kubectl run nginx --image=nginx --port=80
|
||||||
$ kubectl expose deployment nginx --port=80 --target-port=80 --type=LoadBalancer
|
$ 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.
|
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).
|
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.
|
Locally run a single sync loop of ExternalDNS.
|
||||||
|
|
||||||
```console
|
```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 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
|
# 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.
|
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
|
# See https://cloud.google.com/cloud-build/docs/build-config
|
||||||
timeout: 1200s
|
timeout: 3000s
|
||||||
options:
|
options:
|
||||||
substitution_option: ALLOW_LOOSE
|
substitution_option: ALLOW_LOOSE
|
||||||
steps:
|
steps:
|
||||||
- name: "gcr.io/k8s-testimages/gcb-docker-gcloud:v20190906-745fed4"
|
- name: "gcr.io/k8s-testimages/gcb-docker-gcloud:v20200824-5d057db"
|
||||||
entrypoint: make
|
entrypoint: make
|
||||||
env:
|
env:
|
||||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
|
@ -103,7 +103,7 @@ func init() {
|
|||||||
// * Ask the DNS provider for current list of endpoints.
|
// * Ask the DNS provider for current list of endpoints.
|
||||||
// * Ask the Source for the desired 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.
|
// * 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 {
|
type Controller struct {
|
||||||
Source source.Source
|
Source source.Source
|
||||||
Registry registry.Registry
|
Registry registry.Registry
|
||||||
@ -117,6 +117,8 @@ type Controller struct {
|
|||||||
nextRunAt time.Time
|
nextRunAt time.Time
|
||||||
// The nextRunAtMux is for atomic updating of nextRunAt
|
// The nextRunAtMux is for atomic updating of nextRunAt
|
||||||
nextRunAtMux sync.Mutex
|
nextRunAtMux sync.Mutex
|
||||||
|
// DNS record types that will be considered for management
|
||||||
|
ManagedRecordTypes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunOnce runs a single iteration of a reconciliation loop.
|
// 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)))
|
sourceEndpointsTotal.Set(float64(len(endpoints)))
|
||||||
|
|
||||||
|
endpoints = c.Registry.AdjustEndpoints(endpoints)
|
||||||
|
|
||||||
plan := &plan.Plan{
|
plan := &plan.Plan{
|
||||||
Policies: []plan.Policy{c.Policy},
|
Policies: []plan.Policy{c.Policy},
|
||||||
Current: records,
|
Current: records,
|
||||||
Desired: endpoints,
|
Desired: endpoints,
|
||||||
DomainFilter: c.DomainFilter,
|
DomainFilter: c.DomainFilter,
|
||||||
PropertyComparator: c.Registry.PropertyValuesEqual,
|
PropertyComparator: c.Registry.PropertyValuesEqual,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
plan = plan.Calculate()
|
plan = plan.Calculate()
|
||||||
@ -163,7 +168,7 @@ func (c *Controller) RunOnce(ctx context.Context) error {
|
|||||||
// MinInterval is used as window for batching events
|
// MinInterval is used as window for batching events
|
||||||
const MinInterval = 5 * time.Second
|
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) {
|
func (c *Controller) ScheduleRunOnce(now time.Time) {
|
||||||
c.nextRunAtMux.Lock()
|
c.nextRunAtMux.Lock()
|
||||||
defer c.nextRunAtMux.Unlock()
|
defer c.nextRunAtMux.Unlock()
|
||||||
|
@ -45,7 +45,7 @@ func (p *mockProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error
|
|||||||
return p.RecordsStore, nil
|
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 {
|
func (p *mockProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||||
if len(changes.Create) != len(p.ExpectChanges.Create) {
|
if len(changes.Create) != len(p.ExpectChanges.Create) {
|
||||||
return errors.New("number of created records is wrong")
|
return errors.New("number of created records is wrong")
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
## Summary
|
## 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.
|
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)
|
* 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.
|
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.
|
* 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.
|
* 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/).
|
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:
|
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 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.
|
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:
|
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.
|
* 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
|
# Quick Start
|
||||||
|
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [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)
|
- [Go modules](https://github.com/golang/go/wiki/Modules)
|
||||||
- [golangci-lint](https://github.com/golangci/golangci-lint)
|
- [golangci-lint](https://github.com/golangci/golangci-lint)
|
||||||
- [Docker](https://docs.docker.com/install/)
|
- [Docker](https://docs.docker.com/install/)
|
||||||
- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl)
|
- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl)
|
||||||
|
|
||||||
Compile and run locally against a remote k8s cluster.
|
Compile and run locally against a remote k8s cluster.
|
||||||
```
|
```shell
|
||||||
git clone https://github.com/kubernetes-sigs/external-dns.git && cd external-dns
|
git clone https://github.com/kubernetes-sigs/external-dns.git && cd external-dns
|
||||||
make build
|
make build
|
||||||
# login to remote k8s cluster
|
# login to remote k8s cluster
|
||||||
@ -16,14 +16,14 @@ make build
|
|||||||
```
|
```
|
||||||
|
|
||||||
Run linting, unit tests, and coverage report.
|
Run linting, unit tests, and coverage report.
|
||||||
```
|
```shell
|
||||||
make lint
|
make lint
|
||||||
make test
|
make test
|
||||||
make cover-html
|
make cover-html
|
||||||
```
|
```
|
||||||
|
|
||||||
Build container image.
|
Build container image.
|
||||||
```
|
```shell
|
||||||
make build.docker
|
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.
|
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`.
|
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`.
|
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.
|
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.
|
* `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.
|
* `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.
|
* `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.
|
* `EmptySource`: returns an empty list of Endpoint objects for the purpose of testing and cleaning out entries.
|
||||||
|
|
||||||
### Providers
|
### 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:
|
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.
|
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.
|
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`.
|
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?
|
### 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
|
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.
|
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?
|
### Why am I seeing time out errors even though I have connectivity to my cluster?
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Background
|
## 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)
|
[Initial discussion](https://docs.google.com/document/d/1ML_q3OppUtQKXan6Q42xIq2jelSoIivuXI8zExbc6ec/edit#heading=h.1pgkuagjhm4p)
|
||||||
|
|
||||||
|
@ -2,10 +2,22 @@
|
|||||||
|
|
||||||
## Release cycle
|
## 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
|
## 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`
|
* `external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code`
|
||||||
* Multi-value answer:`external-dns.alpha.kubernetes.io/aws-multi-value-answer`
|
* 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
|
## Clean up
|
||||||
|
|
||||||
Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly.
|
Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly.
|
||||||
|
@ -34,17 +34,19 @@ This is crucial as ExternalDNS reads those endpoints records when creating DNS-R
|
|||||||
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.
|
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.
|
Verify the correct propagation of the loadbalancer's ip by listing the ingresses.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ kubectl get ingress
|
$ kubectl get ingress
|
||||||
```
|
```
|
||||||
|
|
||||||
The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information.
|
The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information.
|
||||||
|
|
||||||
```
|
```
|
||||||
NAME HOSTS ADDRESS PORTS AGE
|
NAME HOSTS ADDRESS PORTS AGE
|
||||||
nginx1 sample1.aks.com 52.167.195.110 80 6d22h
|
nginx1 sample1.aks.com 52.167.195.110 80 6d22h
|
||||||
nginx2 sample2.aks.com 52.167.195.110 80 6d21h
|
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:
|
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/).
|
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).
|
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.
|
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.
|
The credentials of the service principal are provided to ExternalDNS as environment-variables.
|
||||||
@ -175,13 +179,14 @@ spec:
|
|||||||
- --provider=azure-private-dns
|
- --provider=azure-private-dns
|
||||||
- --azure-resource-group=externaldns
|
- --azure-resource-group=externaldns
|
||||||
- --azure-subscription-id=<use the id of your subscription>
|
- --azure-subscription-id=<use the id of your subscription>
|
||||||
env:
|
volumeMounts:
|
||||||
- name: AZURE_TENANT_ID
|
- name: azure-config-file
|
||||||
value: "<use the tenantId discovered during creation of service principal>"
|
mountPath: /etc/kubernetes
|
||||||
- name: AZURE_CLIENT_ID
|
readOnly: true
|
||||||
value: "<use the aadClientId discovered during creation of service principal>"
|
volumes:
|
||||||
- name: AZURE_CLIENT_SECRET
|
- name: azure-config-file
|
||||||
value: "<use the aadClientSecret discovered during creation of service principal>"
|
secret:
|
||||||
|
secretName: azure-config-file
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manifest (for clusters with RBAC enabled, cluster access)
|
### Manifest (for clusters with RBAC enabled, cluster access)
|
||||||
@ -245,13 +250,14 @@ spec:
|
|||||||
- --provider=azure-private-dns
|
- --provider=azure-private-dns
|
||||||
- --azure-resource-group=externaldns
|
- --azure-resource-group=externaldns
|
||||||
- --azure-subscription-id=<use the id of your subscription>
|
- --azure-subscription-id=<use the id of your subscription>
|
||||||
env:
|
volumeMounts:
|
||||||
- name: AZURE_TENANT_ID
|
- name: azure-config-file
|
||||||
value: "<use the tenantId discovered during creation of service principal>"
|
mountPath: /etc/kubernetes
|
||||||
- name: AZURE_CLIENT_ID
|
readOnly: true
|
||||||
value: "<use the aadClientId discovered during creation of service principal>"
|
volumes:
|
||||||
- name: AZURE_CLIENT_SECRET
|
- name: azure-config-file
|
||||||
value: "<use the aadClientSecret discovered during creation of service principal>"
|
secret:
|
||||||
|
secretName: azure-config-file
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manifest (for clusters with RBAC enabled, namespace access)
|
### Manifest (for clusters with RBAC enabled, namespace access)
|
||||||
@ -315,13 +321,14 @@ spec:
|
|||||||
- --provider=azure-private-dns
|
- --provider=azure-private-dns
|
||||||
- --azure-resource-group=externaldns
|
- --azure-resource-group=externaldns
|
||||||
- --azure-subscription-id=<use the id of your subscription>
|
- --azure-subscription-id=<use the id of your subscription>
|
||||||
env:
|
volumeMounts:
|
||||||
- name: AZURE_TENANT_ID
|
- name: azure-config-file
|
||||||
value: "<use the tenantId discovered during creation of service principal>"
|
mountPath: /etc/kubernetes
|
||||||
- name: AZURE_CLIENT_ID
|
readOnly: true
|
||||||
value: "<use the aadClientId discovered during creation of service principal>"
|
volumes:
|
||||||
- name: AZURE_CLIENT_SECRET
|
- name: azure-config-file
|
||||||
value: "<use the aadClientSecret discovered during creation of service principal>"
|
secret:
|
||||||
|
secretName: azure-config-file
|
||||||
```
|
```
|
||||||
|
|
||||||
Create the deployment for ExternalDNS:
|
Create the deployment for ExternalDNS:
|
||||||
|
@ -85,7 +85,7 @@ rules:
|
|||||||
verbs: ["get","watch","list"]
|
verbs: ["get","watch","list"]
|
||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: ["nodes"]
|
resources: ["nodes"]
|
||||||
verbs: ["list"]
|
verbs: ["list", "watch"]
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
kind: ClusterRoleBinding
|
kind: ClusterRoleBinding
|
||||||
|
@ -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
|
You need to edit/patch the file with below diff
|
||||||
```
|
```diff
|
||||||
diff --git a/values.yaml b/values.yaml
|
diff --git a/values.yaml b/values.yaml
|
||||||
index 964e72b..e2fa934 100644
|
index 964e72b..e2fa934 100644
|
||||||
--- a/values.yaml
|
--- a/values.yaml
|
||||||
|
@ -188,7 +188,7 @@ kafka-1.example.org
|
|||||||
kafka-2.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
|
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:
|
you would get the service name inside the generated A records:
|
||||||
|
|
||||||
|
@ -157,6 +157,7 @@ apiVersion: networking.istio.io/v1alpha3
|
|||||||
kind: Gateway
|
kind: Gateway
|
||||||
metadata:
|
metadata:
|
||||||
name: httpbin-gateway
|
name: httpbin-gateway
|
||||||
|
namespace: istio-system
|
||||||
spec:
|
spec:
|
||||||
selector:
|
selector:
|
||||||
istio: ingressgateway # use Istio default gateway implementation
|
istio: ingressgateway # use Istio default gateway implementation
|
||||||
|
@ -78,7 +78,7 @@ rules:
|
|||||||
See also current RBAC yaml files:
|
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)
|
- [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)
|
- [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
|
[3]: https://opensource.zalando.com/skipper/kubernetes/routegroups/#routegroups
|
||||||
[4]: https://opensource.zalando.com/skipper
|
[4]: https://opensource.zalando.com/skipper
|
||||||
@ -269,7 +269,7 @@ status:
|
|||||||
```
|
```
|
||||||
|
|
||||||
ExternalDNS will create a A-records `echoserver.example.org`, that
|
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)
|
## 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: [""]
|
- apiGroups: [""]
|
||||||
resources: ["nodes"]
|
resources: ["nodes"]
|
||||||
verbs: ["list"]
|
verbs: ["list"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["endpoints"]
|
||||||
|
verbs: ["get","watch","list"]
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
kind: ClusterRoleBinding
|
kind: ClusterRoleBinding
|
||||||
|
@ -155,7 +155,7 @@ $ kubectl get services echo
|
|||||||
$ kubectl get endpoints 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.
|
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:
|
Once that's done, wait about 30s-1m (interval for external-dns to kick in), then do:
|
||||||
|
@ -166,6 +166,7 @@ rules:
|
|||||||
- services
|
- services
|
||||||
- endpoints
|
- endpoints
|
||||||
- pods
|
- pods
|
||||||
|
- nodes
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
- watch
|
- watch
|
||||||
@ -289,9 +290,9 @@ You'll want to configure `external-dns` similarly to the following:
|
|||||||
```text
|
```text
|
||||||
...
|
...
|
||||||
- --provider=rfc2136
|
- --provider=rfc2136
|
||||||
- --rfc2136-host=123.123.123.123
|
- --rfc2136-host=192.168.0.1
|
||||||
- --rfc2136-port=53
|
- --rfc2136-port=53
|
||||||
- --rfc2136-zone=your-domain.com
|
- --rfc2136-zone=k8s.example.org
|
||||||
- --rfc2136-insecure
|
- --rfc2136-insecure
|
||||||
- --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
|
- --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.
|
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.
|
**Warning**: Scaleway DNS is currently in Public Beta and may not be suited for production usage.
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: external-dns
|
- 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:
|
args:
|
||||||
- --source=service # ingress is also possible
|
- --source=service # ingress is also possible
|
||||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||||
@ -121,7 +121,7 @@ spec:
|
|||||||
serviceAccountName: external-dns
|
serviceAccountName: external-dns
|
||||||
containers:
|
containers:
|
||||||
- name: external-dns
|
- 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:
|
args:
|
||||||
- --source=service # ingress is also possible
|
- --source=service # ingress is also possible
|
||||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
- --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.
|
- 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".
|
- 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
|
```console
|
||||||
$ kubectl delete -f apple-banana-echo.yaml
|
$ kubectl delete -f apple-banana-echo.yaml
|
||||||
$ kubectl delete -f expose-apple-banana-app.yaml
|
$ kubectl delete -f expose-apple-banana-app.yaml
|
||||||
|
@ -33,6 +33,8 @@ const (
|
|||||||
RecordTypeTXT = "TXT"
|
RecordTypeTXT = "TXT"
|
||||||
// RecordTypeSRV is a RecordType enum value
|
// RecordTypeSRV is a RecordType enum value
|
||||||
RecordTypeSRV = "SRV"
|
RecordTypeSRV = "SRV"
|
||||||
|
// RecordTypeNS is a RecordType enum value
|
||||||
|
RecordTypeNS = "NS"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TTL is a structure defining the TTL of a DNS record
|
// TTL is a structure defining the TTL of a DNS record
|
||||||
@ -85,7 +87,7 @@ func (t Targets) Same(o Targets) bool {
|
|||||||
return true
|
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'
|
// 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.
|
// 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'
|
// 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
|
module sigs.k8s.io/external-dns
|
||||||
|
|
||||||
go 1.14
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.50.0
|
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/azure-sdk-for-go v45.1.0+incompatible
|
||||||
github.com/Azure/go-autorest/autorest v0.11.4
|
github.com/Azure/go-autorest/autorest v0.11.10
|
||||||
github.com/Azure/go-autorest/autorest/adal v0.9.2
|
github.com/Azure/go-autorest/autorest/adal v0.9.5
|
||||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.1
|
|
||||||
github.com/Azure/go-autorest/autorest/to v0.4.0
|
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/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
|
||||||
github.com/alecthomas/colour v0.1.0 // indirect
|
github.com/alecthomas/colour v0.1.0 // indirect
|
||||||
github.com/alecthomas/kingpin v2.2.5+incompatible
|
github.com/alecthomas/kingpin v2.2.5+incompatible
|
||||||
@ -19,6 +18,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go v1.31.4
|
github.com/aws/aws-sdk-go v1.31.4
|
||||||
github.com/cloudflare/cloudflare-go v0.10.1
|
github.com/cloudflare/cloudflare-go v0.10.1
|
||||||
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
|
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
|
||||||
|
github.com/datawire/ambassador v1.6.0
|
||||||
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
|
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
|
||||||
github.com/digitalocean/godo v1.36.0
|
github.com/digitalocean/godo v1.36.0
|
||||||
github.com/dnsimple/dnsimple-go v0.60.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/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200623155123-84df6c4b5301
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200623155123-84df6c4b5301
|
||||||
github.com/sirupsen/logrus v1.6.0
|
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/terra-farm/udnssdk v1.3.5 // indirect
|
||||||
github.com/transip/gotransip v5.8.2+incompatible
|
github.com/transip/gotransip v5.8.2+incompatible
|
||||||
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
|
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
|
||||||
@ -54,16 +55,19 @@ require (
|
|||||||
github.com/vultr/govultr v0.4.2
|
github.com/vultr/govultr v0.4.2
|
||||||
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875
|
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875
|
||||||
go.uber.org/ratelimit v0.1.0
|
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/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||||
|
golang.org/x/tools v0.0.0-20200708003708-134513de8882 // indirect
|
||||||
google.golang.org/api v0.15.0
|
google.golang.org/api v0.15.0
|
||||||
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
|
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
|
||||||
gopkg.in/yaml.v2 v2.2.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/api v0.0.0-20200529165953-72dad51d4ffc
|
||||||
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751
|
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751
|
||||||
k8s.io/api v0.18.8
|
k8s.io/api v0.18.8
|
||||||
k8s.io/apimachinery v0.18.8
|
k8s.io/apimachinery v0.18.8
|
||||||
k8s.io/client-go v0.18.8
|
k8s.io/client-go v0.18.8
|
||||||
|
k8s.io/kubernetes v1.13.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
|
@ -3,7 +3,7 @@ kind: Kustomization
|
|||||||
|
|
||||||
images:
|
images:
|
||||||
- name: k8s.gcr.io/external-dns/external-dns
|
- name: k8s.gcr.io/external-dns/external-dns
|
||||||
newTag: v0.7.3
|
newTag: v0.7.6
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- ./external-dns-deployment.yaml
|
- ./external-dns-deployment.yaml
|
||||||
|
17
main.go
17
main.go
@ -101,9 +101,11 @@ func main() {
|
|||||||
sourceCfg := &source.Config{
|
sourceCfg := &source.Config{
|
||||||
Namespace: cfg.Namespace,
|
Namespace: cfg.Namespace,
|
||||||
AnnotationFilter: cfg.AnnotationFilter,
|
AnnotationFilter: cfg.AnnotationFilter,
|
||||||
|
LabelFilter: cfg.LabelFilter,
|
||||||
FQDNTemplate: cfg.FQDNTemplate,
|
FQDNTemplate: cfg.FQDNTemplate,
|
||||||
CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
|
CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
|
||||||
IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation,
|
IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation,
|
||||||
|
IgnoreIngressTLSSpec: cfg.IgnoreIngressTLSSpec,
|
||||||
Compatibility: cfg.Compatibility,
|
Compatibility: cfg.Compatibility,
|
||||||
PublishInternal: cfg.PublishInternal,
|
PublishInternal: cfg.PublishInternal,
|
||||||
PublishHostIP: cfg.PublishHostIP,
|
PublishHostIP: cfg.PublishHostIP,
|
||||||
@ -142,6 +144,7 @@ func main() {
|
|||||||
endpointsSource := source.NewDedupSource(source.NewMultiSource(sources))
|
endpointsSource := source.NewDedupSource(source.NewMultiSource(sources))
|
||||||
|
|
||||||
domainFilter := endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
|
domainFilter := endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
|
||||||
|
zoneNameFilter := endpoint.NewDomainFilter(cfg.ZoneNameFilter)
|
||||||
zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
|
zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
|
||||||
zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
|
zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
|
||||||
zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
|
zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
|
||||||
@ -149,7 +152,7 @@ func main() {
|
|||||||
var p provider.Provider
|
var p provider.Provider
|
||||||
switch cfg.Provider {
|
switch cfg.Provider {
|
||||||
case "akamai":
|
case "akamai":
|
||||||
p = akamai.NewAkamaiProvider(
|
p, err = akamai.NewAkamaiProvider(
|
||||||
akamai.AkamaiConfig{
|
akamai.AkamaiConfig{
|
||||||
DomainFilter: domainFilter,
|
DomainFilter: domainFilter,
|
||||||
ZoneIDFilter: zoneIDFilter,
|
ZoneIDFilter: zoneIDFilter,
|
||||||
@ -157,9 +160,10 @@ func main() {
|
|||||||
ClientToken: cfg.AkamaiClientToken,
|
ClientToken: cfg.AkamaiClientToken,
|
||||||
ClientSecret: cfg.AkamaiClientSecret,
|
ClientSecret: cfg.AkamaiClientSecret,
|
||||||
AccessToken: cfg.AkamaiAccessToken,
|
AccessToken: cfg.AkamaiAccessToken,
|
||||||
|
EdgercPath: cfg.AkamaiEdgercPath,
|
||||||
|
EdgercSection: cfg.AkamaiEdgercSection,
|
||||||
DryRun: cfg.DryRun,
|
DryRun: cfg.DryRun,
|
||||||
},
|
}, nil)
|
||||||
)
|
|
||||||
case "alibabacloud":
|
case "alibabacloud":
|
||||||
p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
|
p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
|
||||||
case "aws":
|
case "aws":
|
||||||
@ -187,9 +191,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun)
|
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun)
|
||||||
case "azure-dns", "azure":
|
case "azure-dns", "azure":
|
||||||
p, err = 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":
|
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":
|
case "vinyldns":
|
||||||
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
|
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
|
||||||
case "vultr":
|
case "vultr":
|
||||||
@ -319,7 +323,7 @@ func main() {
|
|||||||
case "noop":
|
case "noop":
|
||||||
r, err = registry.NewNoopRegistry(p)
|
r, err = registry.NewNoopRegistry(p)
|
||||||
case "txt":
|
case "txt":
|
||||||
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval)
|
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement)
|
||||||
case "aws-sd":
|
case "aws-sd":
|
||||||
r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID)
|
r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID)
|
||||||
default:
|
default:
|
||||||
@ -341,6 +345,7 @@ func main() {
|
|||||||
Policy: policy,
|
Policy: policy,
|
||||||
Interval: cfg.Interval,
|
Interval: cfg.Interval,
|
||||||
DomainFilter: domainFilter,
|
DomainFilter: domainFilter,
|
||||||
|
ManagedRecordTypes: cfg.ManagedDNSRecordTypes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Once {
|
if cfg.Once {
|
||||||
|
@ -22,6 +22,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
|
|
||||||
"github.com/alecthomas/kingpin"
|
"github.com/alecthomas/kingpin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@ -47,9 +49,11 @@ type Config struct {
|
|||||||
Sources []string
|
Sources []string
|
||||||
Namespace string
|
Namespace string
|
||||||
AnnotationFilter string
|
AnnotationFilter string
|
||||||
|
LabelFilter string
|
||||||
FQDNTemplate string
|
FQDNTemplate string
|
||||||
CombineFQDNAndAnnotation bool
|
CombineFQDNAndAnnotation bool
|
||||||
IgnoreHostnameAnnotation bool
|
IgnoreHostnameAnnotation bool
|
||||||
|
IgnoreIngressTLSSpec bool
|
||||||
Compatibility string
|
Compatibility string
|
||||||
PublishInternal bool
|
PublishInternal bool
|
||||||
PublishHostIP bool
|
PublishHostIP bool
|
||||||
@ -61,6 +65,7 @@ type Config struct {
|
|||||||
GoogleBatchChangeInterval time.Duration
|
GoogleBatchChangeInterval time.Duration
|
||||||
DomainFilter []string
|
DomainFilter []string
|
||||||
ExcludeDomains []string
|
ExcludeDomains []string
|
||||||
|
ZoneNameFilter []string
|
||||||
ZoneIDFilter []string
|
ZoneIDFilter []string
|
||||||
AlibabaCloudConfigFile string
|
AlibabaCloudConfigFile string
|
||||||
AlibabaCloudZoneType string
|
AlibabaCloudZoneType string
|
||||||
@ -85,6 +90,8 @@ type Config struct {
|
|||||||
AkamaiClientToken string
|
AkamaiClientToken string
|
||||||
AkamaiClientSecret string
|
AkamaiClientSecret string
|
||||||
AkamaiAccessToken string
|
AkamaiAccessToken string
|
||||||
|
AkamaiEdgercPath string
|
||||||
|
AkamaiEdgercSection string
|
||||||
InfobloxGridHost string
|
InfobloxGridHost string
|
||||||
InfobloxWapiPort int
|
InfobloxWapiPort int
|
||||||
InfobloxWapiUsername string
|
InfobloxWapiUsername string
|
||||||
@ -122,6 +129,7 @@ type Config struct {
|
|||||||
MetricsAddress string
|
MetricsAddress string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
TXTCacheInterval time.Duration
|
TXTCacheInterval time.Duration
|
||||||
|
TXTWildcardReplacement string
|
||||||
ExoscaleEndpoint string
|
ExoscaleEndpoint string
|
||||||
ExoscaleAPIKey string `secure:"yes"`
|
ExoscaleAPIKey string `secure:"yes"`
|
||||||
ExoscaleAPISecret string `secure:"yes"`
|
ExoscaleAPISecret string `secure:"yes"`
|
||||||
@ -146,6 +154,7 @@ type Config struct {
|
|||||||
TransIPAccountName string
|
TransIPAccountName string
|
||||||
TransIPPrivateKeyFile string
|
TransIPPrivateKeyFile string
|
||||||
DigitalOceanAPIPageSize int
|
DigitalOceanAPIPageSize int
|
||||||
|
ManagedDNSRecordTypes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultConfig = &Config{
|
var defaultConfig = &Config{
|
||||||
@ -157,9 +166,11 @@ var defaultConfig = &Config{
|
|||||||
Sources: nil,
|
Sources: nil,
|
||||||
Namespace: "",
|
Namespace: "",
|
||||||
AnnotationFilter: "",
|
AnnotationFilter: "",
|
||||||
|
LabelFilter: "",
|
||||||
FQDNTemplate: "",
|
FQDNTemplate: "",
|
||||||
CombineFQDNAndAnnotation: false,
|
CombineFQDNAndAnnotation: false,
|
||||||
IgnoreHostnameAnnotation: false,
|
IgnoreHostnameAnnotation: false,
|
||||||
|
IgnoreIngressTLSSpec: false,
|
||||||
Compatibility: "",
|
Compatibility: "",
|
||||||
PublishInternal: false,
|
PublishInternal: false,
|
||||||
PublishHostIP: false,
|
PublishHostIP: false,
|
||||||
@ -191,6 +202,8 @@ var defaultConfig = &Config{
|
|||||||
AkamaiClientToken: "",
|
AkamaiClientToken: "",
|
||||||
AkamaiClientSecret: "",
|
AkamaiClientSecret: "",
|
||||||
AkamaiAccessToken: "",
|
AkamaiAccessToken: "",
|
||||||
|
AkamaiEdgercSection: "",
|
||||||
|
AkamaiEdgercPath: "",
|
||||||
InfobloxGridHost: "",
|
InfobloxGridHost: "",
|
||||||
InfobloxWapiPort: 443,
|
InfobloxWapiPort: 443,
|
||||||
InfobloxWapiUsername: "admin",
|
InfobloxWapiUsername: "admin",
|
||||||
@ -215,6 +228,7 @@ var defaultConfig = &Config{
|
|||||||
TXTPrefix: "",
|
TXTPrefix: "",
|
||||||
TXTSuffix: "",
|
TXTSuffix: "",
|
||||||
TXTCacheInterval: 0,
|
TXTCacheInterval: 0,
|
||||||
|
TXTWildcardReplacement: "",
|
||||||
Interval: time.Minute,
|
Interval: time.Minute,
|
||||||
Once: false,
|
Once: false,
|
||||||
DryRun: false,
|
DryRun: false,
|
||||||
@ -245,6 +259,7 @@ var defaultConfig = &Config{
|
|||||||
TransIPAccountName: "",
|
TransIPAccountName: "",
|
||||||
TransIPPrivateKeyFile: "",
|
TransIPPrivateKeyFile: "",
|
||||||
DigitalOceanAPIPageSize: 50,
|
DigitalOceanAPIPageSize: 50,
|
||||||
|
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig returns new Config object
|
// 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)
|
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
|
||||||
|
|
||||||
// Flags related to processing sources
|
// 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("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("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("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("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-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("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-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)
|
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-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("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("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
|
// 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("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("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("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("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-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)
|
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-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("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("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-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)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
|
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)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret)
|
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)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
|
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-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-port", "When using the Infoblox provider, specify the WAPI port (default: 443)").Default(strconv.Itoa(defaultConfig.InfobloxWapiPort)).IntVar(&cfg.InfobloxWapiPort)
|
||||||
app.Flag("infoblox-wapi-username", "When using the Infoblox provider, specify the WAPI username (default: admin)").Default(defaultConfig.InfobloxWapiUsername).StringVar(&cfg.InfobloxWapiUsername)
|
app.Flag("infoblox-wapi-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("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-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-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("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-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)
|
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-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-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-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
|
// Flags related to the main control loop
|
||||||
app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)
|
app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)
|
||||||
|
@ -22,6 +22,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -44,6 +46,7 @@ var (
|
|||||||
GoogleBatchChangeInterval: time.Second,
|
GoogleBatchChangeInterval: time.Second,
|
||||||
DomainFilter: []string{""},
|
DomainFilter: []string{""},
|
||||||
ExcludeDomains: []string{""},
|
ExcludeDomains: []string{""},
|
||||||
|
ZoneNameFilter: []string{""},
|
||||||
ZoneIDFilter: []string{""},
|
ZoneIDFilter: []string{""},
|
||||||
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
|
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
|
||||||
AWSZoneType: "",
|
AWSZoneType: "",
|
||||||
@ -65,6 +68,8 @@ var (
|
|||||||
AkamaiClientToken: "",
|
AkamaiClientToken: "",
|
||||||
AkamaiClientSecret: "",
|
AkamaiClientSecret: "",
|
||||||
AkamaiAccessToken: "",
|
AkamaiAccessToken: "",
|
||||||
|
AkamaiEdgercPath: "",
|
||||||
|
AkamaiEdgercSection: "",
|
||||||
InfobloxGridHost: "",
|
InfobloxGridHost: "",
|
||||||
InfobloxWapiPort: 443,
|
InfobloxWapiPort: 443,
|
||||||
InfobloxWapiUsername: "admin",
|
InfobloxWapiUsername: "admin",
|
||||||
@ -101,6 +106,7 @@ var (
|
|||||||
TransIPAccountName: "",
|
TransIPAccountName: "",
|
||||||
TransIPPrivateKeyFile: "",
|
TransIPPrivateKeyFile: "",
|
||||||
DigitalOceanAPIPageSize: 50,
|
DigitalOceanAPIPageSize: 50,
|
||||||
|
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
overriddenConfig = &Config{
|
overriddenConfig = &Config{
|
||||||
@ -112,6 +118,7 @@ var (
|
|||||||
Sources: []string{"service", "ingress", "connector"},
|
Sources: []string{"service", "ingress", "connector"},
|
||||||
Namespace: "namespace",
|
Namespace: "namespace",
|
||||||
IgnoreHostnameAnnotation: true,
|
IgnoreHostnameAnnotation: true,
|
||||||
|
IgnoreIngressTLSSpec: true,
|
||||||
FQDNTemplate: "{{.Name}}.service.example.com",
|
FQDNTemplate: "{{.Name}}.service.example.com",
|
||||||
Compatibility: "mate",
|
Compatibility: "mate",
|
||||||
Provider: "google",
|
Provider: "google",
|
||||||
@ -120,6 +127,7 @@ var (
|
|||||||
GoogleBatchChangeInterval: time.Second * 2,
|
GoogleBatchChangeInterval: time.Second * 2,
|
||||||
DomainFilter: []string{"example.org", "company.com"},
|
DomainFilter: []string{"example.org", "company.com"},
|
||||||
ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"},
|
ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"},
|
||||||
|
ZoneNameFilter: []string{"yapi.example.org", "yapi.company.com"},
|
||||||
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
|
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
|
||||||
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
|
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
|
||||||
AWSZoneType: "private",
|
AWSZoneType: "private",
|
||||||
@ -141,6 +149,8 @@ var (
|
|||||||
AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46",
|
AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46",
|
||||||
AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46",
|
AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46",
|
||||||
AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46",
|
AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46",
|
||||||
|
AkamaiEdgercPath: "/home/test/.edgerc",
|
||||||
|
AkamaiEdgercSection: "default",
|
||||||
InfobloxGridHost: "127.0.0.1",
|
InfobloxGridHost: "127.0.0.1",
|
||||||
InfobloxWapiPort: 8443,
|
InfobloxWapiPort: 8443,
|
||||||
InfobloxWapiUsername: "infoblox",
|
InfobloxWapiUsername: "infoblox",
|
||||||
@ -183,6 +193,7 @@ var (
|
|||||||
TransIPAccountName: "transip",
|
TransIPAccountName: "transip",
|
||||||
TransIPPrivateKeyFile: "/path/to/transip.key",
|
TransIPPrivateKeyFile: "/path/to/transip.key",
|
||||||
DigitalOceanAPIPageSize: 100,
|
DigitalOceanAPIPageSize: 100,
|
||||||
|
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -216,6 +227,7 @@ func TestParseFlags(t *testing.T) {
|
|||||||
"--namespace=namespace",
|
"--namespace=namespace",
|
||||||
"--fqdn-template={{.Name}}.service.example.com",
|
"--fqdn-template={{.Name}}.service.example.com",
|
||||||
"--ignore-hostname-annotation",
|
"--ignore-hostname-annotation",
|
||||||
|
"--ignore-ingress-tls-spec",
|
||||||
"--compatibility=mate",
|
"--compatibility=mate",
|
||||||
"--provider=google",
|
"--provider=google",
|
||||||
"--google-project=project",
|
"--google-project=project",
|
||||||
@ -231,6 +243,8 @@ func TestParseFlags(t *testing.T) {
|
|||||||
"--akamai-client-token=o184671d5307a388180fbf7f11dbdf46",
|
"--akamai-client-token=o184671d5307a388180fbf7f11dbdf46",
|
||||||
"--akamai-client-secret=o184671d5307a388180fbf7f11dbdf46",
|
"--akamai-client-secret=o184671d5307a388180fbf7f11dbdf46",
|
||||||
"--akamai-access-token=o184671d5307a388180fbf7f11dbdf46",
|
"--akamai-access-token=o184671d5307a388180fbf7f11dbdf46",
|
||||||
|
"--akamai-edgerc-path=/home/test/.edgerc",
|
||||||
|
"--akamai-edgerc-section=default",
|
||||||
"--infoblox-grid-host=127.0.0.1",
|
"--infoblox-grid-host=127.0.0.1",
|
||||||
"--infoblox-wapi-port=8443",
|
"--infoblox-wapi-port=8443",
|
||||||
"--infoblox-wapi-username=infoblox",
|
"--infoblox-wapi-username=infoblox",
|
||||||
@ -254,6 +268,8 @@ func TestParseFlags(t *testing.T) {
|
|||||||
"--domain-filter=company.com",
|
"--domain-filter=company.com",
|
||||||
"--exclude-domains=xapi.example.org",
|
"--exclude-domains=xapi.example.org",
|
||||||
"--exclude-domains=xapi.company.com",
|
"--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/ZTST1",
|
||||||
"--zone-id-filter=/hostedzone/ZTST2",
|
"--zone-id-filter=/hostedzone/ZTST2",
|
||||||
"--aws-zone-type=private",
|
"--aws-zone-type=private",
|
||||||
@ -306,6 +322,7 @@ func TestParseFlags(t *testing.T) {
|
|||||||
"EXTERNAL_DNS_NAMESPACE": "namespace",
|
"EXTERNAL_DNS_NAMESPACE": "namespace",
|
||||||
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
|
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
|
||||||
"EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1",
|
"EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1",
|
||||||
|
"EXTERNAL_DNS_IGNORE_INGRESS_TLS_SPEC": "1",
|
||||||
"EXTERNAL_DNS_COMPATIBILITY": "mate",
|
"EXTERNAL_DNS_COMPATIBILITY": "mate",
|
||||||
"EXTERNAL_DNS_PROVIDER": "google",
|
"EXTERNAL_DNS_PROVIDER": "google",
|
||||||
"EXTERNAL_DNS_GOOGLE_PROJECT": "project",
|
"EXTERNAL_DNS_GOOGLE_PROJECT": "project",
|
||||||
@ -321,6 +338,8 @@ func TestParseFlags(t *testing.T) {
|
|||||||
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
|
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
|
||||||
"EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46",
|
"EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46",
|
||||||
"EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "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_GRID_HOST": "127.0.0.1",
|
||||||
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
|
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
|
||||||
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",
|
"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_CA": "/path/to/ca.crt",
|
||||||
"EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem",
|
"EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem",
|
||||||
"EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.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_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
|
||||||
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
|
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
|
||||||
"EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo",
|
"EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo",
|
||||||
|
@ -45,16 +45,16 @@ func ValidateConfig(cfg *externaldns.Config) error {
|
|||||||
|
|
||||||
// Akamai provider specific validations
|
// Akamai provider specific validations
|
||||||
if cfg.Provider == "akamai" {
|
if cfg.Provider == "akamai" {
|
||||||
if cfg.AkamaiServiceConsumerDomain == "" {
|
if cfg.AkamaiServiceConsumerDomain == "" && cfg.AkamaiEdgercPath != "" {
|
||||||
return errors.New("no Akamai ServiceConsumerDomain specified")
|
return errors.New("no Akamai ServiceConsumerDomain specified")
|
||||||
}
|
}
|
||||||
if cfg.AkamaiClientToken == "" {
|
if cfg.AkamaiClientToken == "" && cfg.AkamaiEdgercPath != "" {
|
||||||
return errors.New("no Akamai client token specified")
|
return errors.New("no Akamai client token specified")
|
||||||
}
|
}
|
||||||
if cfg.AkamaiClientSecret == "" {
|
if cfg.AkamaiClientSecret == "" && cfg.AkamaiEdgercPath != "" {
|
||||||
return errors.New("no Akamai client secret specified")
|
return errors.New("no Akamai client secret specified")
|
||||||
}
|
}
|
||||||
if cfg.AkamaiAccessToken == "" {
|
if cfg.AkamaiAccessToken == "" && cfg.AkamaiEdgercPath != "" {
|
||||||
return errors.New("no Akamai access token specified")
|
return errors.New("no Akamai access token specified")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
plan/plan.go
32
plan/plan.go
@ -43,6 +43,8 @@ type Plan struct {
|
|||||||
DomainFilter endpoint.DomainFilter
|
DomainFilter endpoint.DomainFilter
|
||||||
// Property comparator compares custom properties of providers
|
// Property comparator compares custom properties of providers
|
||||||
PropertyComparator PropertyComparator
|
PropertyComparator PropertyComparator
|
||||||
|
// DNS record types that will be considered for management
|
||||||
|
ManagedRecords []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changes holds lists of actions to be executed by dns providers
|
// 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 {
|
func (p *Plan) Calculate() *Plan {
|
||||||
t := newPlanTable()
|
t := newPlanTable()
|
||||||
|
|
||||||
for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter) {
|
for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter, p.ManagedRecords) {
|
||||||
t.addCurrent(current)
|
t.addCurrent(current)
|
||||||
}
|
}
|
||||||
for _, desired := range filterRecordsForPlan(p.Desired, p.DomainFilter) {
|
for _, desired := range filterRecordsForPlan(p.Desired, p.DomainFilter, p.ManagedRecords) {
|
||||||
t.addCandidate(desired)
|
t.addCandidate(desired)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +160,7 @@ func (p *Plan) Calculate() *Plan {
|
|||||||
Current: p.Current,
|
Current: p.Current,
|
||||||
Desired: p.Desired,
|
Desired: p.Desired,
|
||||||
Changes: changes,
|
Changes: changes,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan
|
return plan
|
||||||
@ -194,12 +197,6 @@ func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint)
|
|||||||
}
|
}
|
||||||
if current.ProviderSpecific != nil {
|
if current.ProviderSpecific != nil {
|
||||||
for _, c := range current.ProviderSpecific {
|
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 d, ok := desiredProperties[c.Name]; ok {
|
||||||
if p.PropertyComparator != nil {
|
if p.PropertyComparator != nil {
|
||||||
if !p.PropertyComparator(c.Name, c.Value, d.Value) {
|
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
|
// 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
|
// only record with this property. The behavior of the planner may need to be
|
||||||
// made more sophisticated to codify this.
|
// 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{}
|
filtered := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
@ -238,14 +235,8 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Do
|
|||||||
if !domainFilter.Match(record.DNSName) {
|
if !domainFilter.Match(record.DNSName) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if isManagedRecord(record.RecordType, managedRecords) {
|
||||||
// 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:
|
|
||||||
filtered = append(filtered, record)
|
filtered = append(filtered, record)
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,3 +277,12 @@ func CompareBoolean(defaultValue bool, name, current, previous string) bool {
|
|||||||
|
|
||||||
return v1 == v2
|
return v1 == v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isManagedRecord(record string, managedRecords []string) bool {
|
||||||
|
for _, r := range managedRecords {
|
||||||
|
if record == r {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -208,6 +208,7 @@ func (suite *PlanTestSuite) TestSyncFirstRound() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -229,6 +230,7 @@ func (suite *PlanTestSuite) TestSyncSecondRound() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -250,6 +252,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundMigration() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -271,6 +274,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithTTLChange() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -292,6 +296,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -316,6 +321,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefaultFalse(
|
|||||||
PropertyComparator: func(name, previous, current string) bool {
|
PropertyComparator: func(name, previous, current string) bool {
|
||||||
return CompareBoolean(false, name, previous, current)
|
return CompareBoolean(false, name, previous, current)
|
||||||
},
|
},
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -371,6 +377,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -413,6 +420,7 @@ func (suite *PlanTestSuite) TestDifferentTypes() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -434,6 +442,7 @@ func (suite *PlanTestSuite) TestIgnoreTXT() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -455,6 +464,7 @@ func (suite *PlanTestSuite) TestRemoveEndpoint() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -476,6 +486,7 @@ func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() {
|
|||||||
Policies: []Policy{&UpsertOnlyPolicy{}},
|
Policies: []Policy{&UpsertOnlyPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -498,6 +509,7 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -521,6 +533,7 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -543,6 +556,7 @@ func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier()
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -565,6 +579,7 @@ func (suite *PlanTestSuite) TestSetIdentifierUpdateCreatesAndDeletes() {
|
|||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -588,6 +603,7 @@ func (suite *PlanTestSuite) TestDomainFiltersInitial() {
|
|||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
|
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -611,6 +627,7 @@ func (suite *PlanTestSuite) TestDomainFiltersUpdate() {
|
|||||||
Current: current,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
|
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := p.Calculate().Changes
|
changes := p.Calculate().Changes
|
||||||
@ -686,3 +703,133 @@ func TestNormalizeDNSName(t *testing.T) {
|
|||||||
assert.Equal(t, r.expect, gotName)
|
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
|
package akamai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"os"
|
||||||
"net/http"
|
"strconv"
|
||||||
"strings"
|
"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"
|
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@ -34,22 +32,23 @@ import (
|
|||||||
"sigs.k8s.io/external-dns/provider"
|
"sigs.k8s.io/external-dns/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
type akamaiClient interface {
|
const (
|
||||||
NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error)
|
// Default Record TTL
|
||||||
Do(config edgegrid.Config, req *http.Request) (*http.Response, error)
|
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 {
|
type AkamaiConfig struct {
|
||||||
DomainFilter endpoint.DomainFilter
|
DomainFilter endpoint.DomainFilter
|
||||||
ZoneIDFilter provider.ZoneIDFilter
|
ZoneIDFilter provider.ZoneIDFilter
|
||||||
@ -57,17 +56,25 @@ type AkamaiConfig struct {
|
|||||||
ClientToken string
|
ClientToken string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
AccessToken string
|
AccessToken string
|
||||||
|
EdgercPath string
|
||||||
|
EdgercSection string
|
||||||
|
MaxBody int
|
||||||
|
AccountKey string
|
||||||
DryRun bool
|
DryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// AkamaiProvider implements the DNS provider for Akamai.
|
// AkamaiProvider implements the DNS provider for Akamai.
|
||||||
type AkamaiProvider struct {
|
type AkamaiProvider struct {
|
||||||
provider.BaseProvider
|
provider.BaseProvider
|
||||||
|
// Edgedns zones to filter on
|
||||||
domainFilter endpoint.DomainFilter
|
domainFilter endpoint.DomainFilter
|
||||||
|
// Contract Ids to filter on
|
||||||
zoneIDFilter provider.ZoneIDFilter
|
zoneIDFilter provider.ZoneIDFilter
|
||||||
config edgegrid.Config
|
// Edgegrid library configuration
|
||||||
|
config *edgegrid.Config
|
||||||
dryRun bool
|
dryRun bool
|
||||||
client akamaiClient
|
// Defines client. Allows for mocking.
|
||||||
|
client AkamaiDNSService
|
||||||
}
|
}
|
||||||
|
|
||||||
type akamaiZones struct {
|
type akamaiZones struct {
|
||||||
@ -79,84 +86,124 @@ type akamaiZone struct {
|
|||||||
Zone string `json:"zone"`
|
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.
|
// NewAkamaiProvider initializes a new Akamai DNS based Provider.
|
||||||
func NewAkamaiProvider(akamaiConfig AkamaiConfig) *AkamaiProvider {
|
func NewAkamaiProvider(akamaiConfig AkamaiConfig, akaService AkamaiDNSService) (provider.Provider, error) {
|
||||||
edgeGridConfig := edgegrid.Config{
|
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,
|
Host: akamaiConfig.ServiceConsumerDomain,
|
||||||
ClientToken: akamaiConfig.ClientToken,
|
ClientToken: akamaiConfig.ClientToken,
|
||||||
ClientSecret: akamaiConfig.ClientSecret,
|
ClientSecret: akamaiConfig.ClientSecret,
|
||||||
AccessToken: akamaiConfig.AccessToken,
|
AccessToken: akamaiConfig.AccessToken,
|
||||||
MaxBody: 1024,
|
MaxBody: 131072, // same default val as used by Edgegrid
|
||||||
HeaderToSign: []string{
|
HeaderToSign: []string{
|
||||||
"X-External-DNS",
|
"X-External-DNS",
|
||||||
},
|
},
|
||||||
Debug: false,
|
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{
|
provider := &AkamaiProvider{
|
||||||
domainFilter: akamaiConfig.DomainFilter,
|
domainFilter: akamaiConfig.DomainFilter,
|
||||||
zoneIDFilter: akamaiConfig.ZoneIDFilter,
|
zoneIDFilter: akamaiConfig.ZoneIDFilter,
|
||||||
config: edgeGridConfig,
|
config: &edgeGridConfig,
|
||||||
dryRun: akamaiConfig.DryRun,
|
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) {
|
func (p AkamaiProvider) ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) {
|
||||||
req, err := p.client.NewRequest(p.config, method, fmt.Sprintf("https://%s/%s", p.config.Host, path), body)
|
return dns.ListZones(queryArgs)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Look here for endpoint documentation -> https://developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html#getzones
|
func (p AkamaiProvider) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) {
|
||||||
func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) {
|
return dns.GetRecordsets(zone, queryArgs)
|
||||||
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) 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 {
|
if err != nil {
|
||||||
log.Errorf("Failed to fetch zones from Akamai")
|
log.Errorf("Failed to fetch zones from Akamai")
|
||||||
return zones, err
|
return filteredZones, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&zones)
|
for _, zone := range resp.Zones {
|
||||||
if err != nil {
|
if p.domainFilter.Match(zone.Zone) || !p.domainFilter.IsConfigured() {
|
||||||
log.Errorf("Could not decode json response from Akamai on zone request")
|
filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractId, Zone: zone.Zone})
|
||||||
return zones, err
|
log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractId)
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
|
||||||
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)
|
lenFilteredZones := len(filteredZones.Zones)
|
||||||
if lenFilteredZones == 0 {
|
if lenFilteredZones == 0 {
|
||||||
@ -168,53 +215,39 @@ func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) {
|
|||||||
return filteredZones, nil
|
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.
|
//Records returns the list of records in a given zone.
|
||||||
func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) {
|
func (p AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) {
|
||||||
zones, err := p.fetchZones()
|
zones, err := p.fetchZones() // returns a filtered set of zones
|
||||||
if err != nil {
|
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
|
return endpoints, err
|
||||||
}
|
}
|
||||||
for _, zone := range zones.Zones {
|
for _, zone := range zones.Zones {
|
||||||
records, err := p.fetchRecordSet(zone.Zone)
|
recordsets, err := p.client.GetRecordsets(zone.Zone, dns.RecordsetQueryArgs{})
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
if len(recordsets.Recordsets) == 0 {
|
||||||
for _, record := range records.Recordsets {
|
log.Warnf("Zone %s contains no recordsets", zone.Zone)
|
||||||
rdata := make([]string, len(record.Rdata))
|
|
||||||
|
|
||||||
for i, v := range record.Rdata {
|
|
||||||
rdata[i] = v.(string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.domainFilter.Match(record.Name) {
|
for _, recordset := range recordsets.Recordsets {
|
||||||
log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", record.Name, record.Type)
|
if !provider.SupportedRecordType(recordset.Type) {
|
||||||
|
log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s'. Record type not supported.", recordset.Name, recordset.Type)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !p.domainFilter.Match(recordset.Name) {
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint(record.Name, record.Type, rdata...))
|
log.Debugf("Skipping endpoint. Record name %s doesn't match containing zone %s.", recordset.Name, zone)
|
||||||
log.Debugf("Fetched endpoint DNSName: '%s' RecordType: '%s' Rdata: '%s')", record.Name, record.Type, rdata)
|
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)
|
lenEndpoints := len(endpoints)
|
||||||
@ -222,161 +255,237 @@ func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoin
|
|||||||
log.Warnf("No endpoints could be fetched")
|
log.Warnf("No endpoints could be fetched")
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("Fetched '%d' endpoints from Akamai", lenEndpoints)
|
log.Debugf("Fetched '%d' endpoints from Akamai", lenEndpoints)
|
||||||
|
log.Debugf("Endpoints [%v]", endpoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpoints, nil
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyChanges applies a given set of changes in a given zone.
|
// ApplyChanges applies a given set of changes in a given zone.
|
||||||
func (p *AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
func (p AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||||
zoneNameIDMapper := provider.ZoneIDName{}
|
zoneNameIDMapper := provider.ZoneIDName{}
|
||||||
zones, err := p.fetchZones()
|
zones, err := p.fetchZones()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("No zones to fetch endpoints from!")
|
log.Errorf("Failed to fetch zones from Akamai")
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, z := range zones.Zones {
|
for _, z := range zones.Zones {
|
||||||
zoneNameIDMapper[z.Zone] = z.Zone
|
zoneNameIDMapper[z.Zone] = z.Zone
|
||||||
}
|
}
|
||||||
|
log.Debugf("Processing zones: [%v]", zoneNameIDMapper)
|
||||||
|
|
||||||
_, cf := p.createRecords(zoneNameIDMapper, changes.Create)
|
// Create recordsets
|
||||||
if !p.dryRun {
|
log.Debugf("Create Changes requested [%v]", changes.Create)
|
||||||
if len(cf) > 0 {
|
if err := p.createRecordsets(zoneNameIDMapper, changes.Create); err != nil {
|
||||||
log.Warnf("Not all desired endpoints could be created, retrying next iteration")
|
return err
|
||||||
for _, f := range cf {
|
}
|
||||||
log.Warnf("Not created was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if !found {
|
||||||
|
log.Warnf("UpdateOld endpoint '%s' is not accounted for in UpdateNew|Delete endpoint list", rec.DNSName)
|
||||||
_, 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AkamaiProvider) newAkamaiRecord(dnsName, recordType string, targets ...string) *akamaiRecord {
|
// Create DNS Recordset
|
||||||
cleanTargets := make([]interface{}, len(targets))
|
func newAkamaiRecordset(dnsName, recordType string, ttl int, targets []string) dns.Recordset {
|
||||||
for idx, target := range targets {
|
return dns.Recordset{
|
||||||
cleanTargets[idx] = strings.TrimSuffix(target, ".")
|
|
||||||
}
|
|
||||||
return &akamaiRecord{
|
|
||||||
Name: strings.TrimSuffix(dnsName, "."),
|
Name: strings.TrimSuffix(dnsName, "."),
|
||||||
Rdata: cleanTargets,
|
Rdata: targets,
|
||||||
Type: recordType,
|
Type: recordType,
|
||||||
TTL: 300,
|
TTL: ttl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AkamaiProvider) createRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (created []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
|
// cleanTargets preps recordset rdata if necessary for EdgeDNS
|
||||||
for _, endpoint := range endpoints {
|
func cleanTargets(rtype string, targets ...string) []string {
|
||||||
if !p.domainFilter.Match(endpoint.DNSName) {
|
log.Debugf("Targets to clean: [%v]", targets)
|
||||||
log.Debugf("Skipping creation at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
|
if rtype == "CNAME" || rtype == "SRV" {
|
||||||
continue
|
for idx, target := range targets {
|
||||||
|
targets[idx] = strings.TrimSuffix(target, ".")
|
||||||
}
|
}
|
||||||
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
|
} else if rtype == "TXT" {
|
||||||
akamaiRecord := p.newAkamaiRecord(endpoint.DNSName, endpoint.RecordType, endpoint.Targets...)
|
for idx, target := range targets {
|
||||||
body, _ := json.MarshalIndent(akamaiRecord, "", " ")
|
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)
|
||||||
|
|
||||||
log.Infof("Create new Endpoint at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.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 {
|
if p.dryRun {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err := p.request("POST", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body))
|
// Create recordsets all at once
|
||||||
|
err := p.client.CreateRecordsets(recordsets, zone, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to create Akamai endpoint DNSName: '%s' RecordType: '%s' for zone: '%s'", endpoint.DNSName, endpoint.RecordType, zoneName)
|
log.Errorf("Failed to create endpoints for DNS zone %s. Error: %s", zone, err.Error())
|
||||||
failed = append(failed, endpoint)
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
for _, endpoint := range endpoints {
|
||||||
if !p.domainFilter.Match(endpoint.DNSName) {
|
zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName)
|
||||||
log.Debugf("Skipping deletion at Akamai of endpoint: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
|
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
|
continue
|
||||||
}
|
}
|
||||||
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
|
log.Infof("Akamai Edge DNS recordset deletion- Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
|
||||||
log.Infof("Deletion at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
|
|
||||||
|
|
||||||
if p.dryRun {
|
if p.dryRun {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := p.request("DELETE", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, nil)
|
recName := strings.TrimSuffix(endpoint.DNSName, ".")
|
||||||
|
rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to delete Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName)
|
if _, ok := err.(*dns.RecordError); !ok {
|
||||||
failed = append(failed, endpoint)
|
return fmt.Errorf("endpoint deletion. record validation failed. error: %s", err.Error())
|
||||||
|
}
|
||||||
|
log.Infof("Endpoint deletion. Record doesn't exist. Name: %s, Type: %s", recName, endpoint.RecordType)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
deleted = append(deleted, endpoint)
|
if err := p.client.DeleteRecord(rec, zoneName, true); err != nil {
|
||||||
} else {
|
log.Errorf("edge dns recordset deletion failed. error: %s", err.Error())
|
||||||
log.Warnf("No matching zone for endpoint deletion DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
|
return err
|
||||||
failed = append(failed, endpoint)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deleted, failed
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AkamaiProvider) updateNewRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (updated []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
|
// Update endpoint recordsets
|
||||||
|
func (p AkamaiProvider) updateNewRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if !p.domainFilter.Match(endpoint.DNSName) {
|
zoneName, _ := zoneNameIDMapper.FindZone(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 zoneName == "" {
|
||||||
|
log.Debugf("Skipping Akamai Edge DNS endpoint update: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
|
log.Infof("Akamai Edge DNS recordset update - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
|
||||||
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 {
|
if p.dryRun {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := p.request("PUT", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body))
|
recName := strings.TrimSuffix(endpoint.DNSName, ".")
|
||||||
|
rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to update Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName)
|
log.Errorf("Endpoint update. Record validation failed. Error: %s", err.Error())
|
||||||
failed = append(failed, endpoint)
|
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
|
continue
|
||||||
}
|
}
|
||||||
updated = append(updated, endpoint)
|
log.Debugf("Skipping Akamai Edge DNS creation of endpoint: '%s' type: '%s', it does not match against Domain filters", ep.DNSName, ep.RecordType)
|
||||||
} else {
|
|
||||||
log.Warnf("No matching zone for endpoint update DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
|
|
||||||
failed = append(failed, endpoint)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return updated, failed
|
return createsByZone
|
||||||
}
|
}
|
||||||
|
@ -17,148 +17,206 @@ limitations under the License.
|
|||||||
package akamai
|
package akamai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
log "github.com/sirupsen/logrus"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"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/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"sigs.k8s.io/external-dns/endpoint"
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
"sigs.k8s.io/external-dns/plan"
|
"sigs.k8s.io/external-dns/plan"
|
||||||
"sigs.k8s.io/external-dns/provider"
|
"sigs.k8s.io/external-dns/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockAkamaiClient struct {
|
type edgednsStubData struct {
|
||||||
mock.Mock
|
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) {
|
type edgednsStub struct {
|
||||||
switch {
|
stubData map[string]edgednsStubData
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockAkamaiClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) {
|
func newStub() *edgednsStub {
|
||||||
handler := func(w http.ResponseWriter, r *http.Request) (isError bool) {
|
return &edgednsStub{
|
||||||
b, _ := ioutil.ReadAll(r.Body)
|
stubData: make(map[string]edgednsStubData),
|
||||||
io.WriteString(w, string(b))
|
|
||||||
return string(b) == "{\"status\": 404 }"
|
|
||||||
}
|
}
|
||||||
w := httptest.NewRecorder()
|
}
|
||||||
err := handler(w, req)
|
|
||||||
resp := w.Result()
|
|
||||||
|
|
||||||
if err == true {
|
func createAkamaiStubProvider(stub *edgednsStub, domfilter endpoint.DomainFilter, idfilter provider.ZoneIDFilter) (*AkamaiProvider, error) {
|
||||||
resp.StatusCode = 400
|
|
||||||
|
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
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRequestError(t *testing.T) {
|
func (r *edgednsStub) CreateRecordsets(recordsets *dns.Recordsets, zone string, reclock bool) error {
|
||||||
config := AkamaiConfig{}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
return nil
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchZonesZoneIDFilter(t *testing.T) {
|
func (r *edgednsStub) GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error) {
|
||||||
config := AkamaiConfig{
|
|
||||||
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Test"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
resp := &dns.RecordBody{}
|
||||||
c := NewAkamaiProvider(config)
|
|
||||||
c.client = client
|
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()
|
x, _ := c.fetchZones()
|
||||||
y, _ := json.Marshal(x)
|
y, _ := json.Marshal(x)
|
||||||
if assert.NotNil(t, y) {
|
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) {
|
func TestFetchZonesEmpty(t *testing.T) {
|
||||||
config := AkamaiConfig{
|
|
||||||
DomainFilter: endpoint.NewDomainFilter([]string{"Nonexistent"}),
|
|
||||||
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
stub := newStub()
|
||||||
c := NewAkamaiProvider(config)
|
domfilter := endpoint.NewDomainFilter([]string{"Nonexistent"})
|
||||||
c.client = client
|
idfilter := provider.NewZoneIDFilter([]string{"Nonexistent"})
|
||||||
|
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
stub.setOutput("zone", []interface{}{})
|
||||||
|
|
||||||
x, _ := c.fetchZones()
|
x, _ := c.fetchZones()
|
||||||
y, _ := json.Marshal(x)
|
y, _ := json.Marshal(x)
|
||||||
if assert.NotNil(t, y) {
|
if assert.NotNil(t, y) {
|
||||||
assert.Equal(t, "{\"zones\":null}", string(y))
|
assert.Equal(t, "{\"zones\":[]}", 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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAkamaiRecords tests record endpoint
|
||||||
func TestAkamaiRecords(t *testing.T) {
|
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 := make([]*endpoint.Endpoint, 0)
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||||
@ -171,28 +229,40 @@ func TestAkamaiRecords(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAkamaiRecordsEmpty(t *testing.T) {
|
func TestAkamaiRecordsEmpty(t *testing.T) {
|
||||||
config := AkamaiConfig{
|
|
||||||
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
stub := newStub()
|
||||||
c := NewAkamaiProvider(config)
|
domfilter := endpoint.DomainFilter{}
|
||||||
c.client = client
|
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())
|
x, _ := c.Records(context.Background())
|
||||||
assert.Nil(t, x)
|
assert.Nil(t, x)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAkamaiRecordsFilters(t *testing.T) {
|
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 := make([]*endpoint.Endpoint, 0)
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "192.168.0.1", "192.168.0.2"))
|
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) {
|
func TestCreateRecords(t *testing.T) {
|
||||||
config := AkamaiConfig{}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
stub := newStub()
|
||||||
c := NewAkamaiProvider(config)
|
domfilter := endpoint.DomainFilter{}
|
||||||
c.client = client
|
idfilter := provider.ZoneIDFilter{}
|
||||||
|
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||||
endpoints := make([]*endpoint.Endpoint, 0)
|
endpoints := make([]*endpoint.Endpoint, 0)
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||||
|
|
||||||
x, _ := c.createRecords(zoneNameIDMapper, endpoints)
|
err = c.createRecordsets(zoneNameIDMapper, endpoints)
|
||||||
if assert.NotNil(t, x) {
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, endpoints, x)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateRecordsDomainFilter(t *testing.T) {
|
func TestCreateRecordsDomainFilter(t *testing.T) {
|
||||||
config := AkamaiConfig{
|
|
||||||
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
stub := newStub()
|
||||||
c := NewAkamaiProvider(config)
|
domfilter := endpoint.DomainFilter{}
|
||||||
c.client = client
|
idfilter := provider.ZoneIDFilter{}
|
||||||
|
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||||
endpoints := make([]*endpoint.Endpoint, 0)
|
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"))
|
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"))
|
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||||
|
|
||||||
x, _ := c.createRecords(zoneNameIDMapper, exclude)
|
err = c.createRecordsets(zoneNameIDMapper, exclude)
|
||||||
if assert.NotNil(t, x) {
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, endpoints, x)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDeleteRecords validate delete
|
||||||
func TestDeleteRecords(t *testing.T) {
|
func TestDeleteRecords(t *testing.T) {
|
||||||
config := AkamaiConfig{}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
stub := newStub()
|
||||||
c := NewAkamaiProvider(config)
|
domfilter := endpoint.DomainFilter{}
|
||||||
c.client = client
|
idfilter := provider.ZoneIDFilter{}
|
||||||
|
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||||
endpoints := make([]*endpoint.Endpoint, 0)
|
endpoints := make([]*endpoint.Endpoint, 0)
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||||
|
|
||||||
x, _ := c.deleteRecords(zoneNameIDMapper, endpoints)
|
err = c.deleteRecordsets(zoneNameIDMapper, endpoints)
|
||||||
if assert.NotNil(t, x) {
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, endpoints, x)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
func TestDeleteRecordsDomainFilter(t *testing.T) {
|
func TestDeleteRecordsDomainFilter(t *testing.T) {
|
||||||
config := AkamaiConfig{
|
|
||||||
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
stub := newStub()
|
||||||
c := NewAkamaiProvider(config)
|
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
|
||||||
c.client = client
|
idfilter := provider.ZoneIDFilter{}
|
||||||
|
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||||
endpoints := make([]*endpoint.Endpoint, 0)
|
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"))
|
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"))
|
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||||
|
|
||||||
x, _ := c.deleteRecords(zoneNameIDMapper, exclude)
|
err = c.deleteRecordsets(zoneNameIDMapper, exclude)
|
||||||
if assert.NotNil(t, x) {
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, endpoints, x)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test record update func
|
||||||
func TestUpdateRecords(t *testing.T) {
|
func TestUpdateRecords(t *testing.T) {
|
||||||
config := AkamaiConfig{}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
stub := newStub()
|
||||||
c := NewAkamaiProvider(config)
|
domfilter := endpoint.DomainFilter{}
|
||||||
c.client = client
|
idfilter := provider.ZoneIDFilter{}
|
||||||
|
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||||
endpoints := make([]*endpoint.Endpoint, 0)
|
endpoints := make([]*endpoint.Endpoint, 0)
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||||
|
|
||||||
x, _ := c.updateNewRecords(zoneNameIDMapper, endpoints)
|
err = c.updateNewRecordsets(zoneNameIDMapper, endpoints)
|
||||||
if assert.NotNil(t, x) {
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, endpoints, x)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
func TestUpdateRecordsDomainFilter(t *testing.T) {
|
func TestUpdateRecordsDomainFilter(t *testing.T) {
|
||||||
config := AkamaiConfig{
|
|
||||||
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
stub := newStub()
|
||||||
c := NewAkamaiProvider(config)
|
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
|
||||||
c.client = client
|
idfilter := provider.ZoneIDFilter{}
|
||||||
|
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||||
endpoints := make([]*endpoint.Endpoint, 0)
|
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"))
|
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"))
|
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||||
|
|
||||||
x, _ := c.updateNewRecords(zoneNameIDMapper, exclude)
|
err = c.updateNewRecordsets(zoneNameIDMapper, exclude)
|
||||||
if assert.NotNil(t, x) {
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, endpoints, x)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAkamaiApplyChanges(t *testing.T) {
|
func TestAkamaiApplyChanges(t *testing.T) {
|
||||||
config := AkamaiConfig{}
|
|
||||||
|
|
||||||
client := &mockAkamaiClient{}
|
stub := newStub()
|
||||||
c := NewAkamaiProvider(config)
|
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
|
||||||
c.client = client
|
idfilter := provider.ZoneIDFilter{}
|
||||||
|
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
stub.setOutput("zone", []interface{}{"example.com"})
|
||||||
changes := &plan.Changes{}
|
changes := &plan.Changes{}
|
||||||
changes.Create = []*endpoint.Endpoint{
|
changes.Create = []*endpoint.Endpoint{
|
||||||
{DNSName: "www.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}, RecordTTL: 300},
|
{DNSName: "www.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}, RecordTTL: 300},
|
||||||
|
@ -50,6 +50,7 @@ const (
|
|||||||
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
|
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
|
||||||
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
|
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
|
||||||
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
|
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
|
||||||
|
providerSpecificHealthCheckID = "aws/health-check-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -79,6 +80,7 @@ var (
|
|||||||
"us-gov-west-1.elb.amazonaws.com": "Z33AYJ8TM3BH4J",
|
"us-gov-west-1.elb.amazonaws.com": "Z33AYJ8TM3BH4J",
|
||||||
"us-gov-east-1.elb.amazonaws.com": "Z166TLBEWOO7G0",
|
"us-gov-east-1.elb.amazonaws.com": "Z166TLBEWOO7G0",
|
||||||
"me-south-1.elb.amazonaws.com": "ZS929ML54UICD",
|
"me-south-1.elb.amazonaws.com": "ZS929ML54UICD",
|
||||||
|
"af-south-1.elb.amazonaws.com": "Z268VQBMOI5EKX",
|
||||||
// Network Load Balancers
|
// Network Load Balancers
|
||||||
"elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP",
|
"elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP",
|
||||||
"elb.us-east-1.amazonaws.com": "Z26RNL4JYFTOTI",
|
"elb.us-east-1.amazonaws.com": "Z26RNL4JYFTOTI",
|
||||||
@ -102,6 +104,7 @@ var (
|
|||||||
"elb.us-gov-west-1.amazonaws.com": "ZMG1MZ2THAWF1",
|
"elb.us-gov-west-1.amazonaws.com": "ZMG1MZ2THAWF1",
|
||||||
"elb.us-gov-east-1.amazonaws.com": "Z1ZSMQQ6Q24QQ8",
|
"elb.us-gov-east-1.amazonaws.com": "Z1ZSMQQ6Q24QQ8",
|
||||||
"elb.me-south-1.amazonaws.com": "Z3QSRYVP46NYYV",
|
"elb.me-south-1.amazonaws.com": "Z3QSRYVP46NYYV",
|
||||||
|
"elb.af-south-1.amazonaws.com": "Z203XCE67M25HM",
|
||||||
// Global Accelerator
|
// Global Accelerator
|
||||||
"awsglobalaccelerator.com": "Z2BJ6XQ5FK7U4H",
|
"awsglobalaccelerator.com": "Z2BJ6XQ5FK7U4H",
|
||||||
}
|
}
|
||||||
@ -202,6 +205,13 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
|
|||||||
return provider, nil
|
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.
|
// Zones returns the list of hosted zones.
|
||||||
func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone, error) {
|
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 {
|
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
|
// 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)
|
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)
|
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.
|
// DeleteRecords deletes a given set of DNS records in a given zone.
|
||||||
func (p *AWSProvider) DeleteRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
|
func (p *AWSProvider) DeleteRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
|
||||||
return p.doRecords(ctx, route53.ChangeActionDelete, endpoints)
|
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)
|
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.
|
// ApplyChanges applies a given set of changes in a given zone.
|
||||||
func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||||
zones, err := p.Zones(ctx)
|
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.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, p.newChanges(route53.ChangeActionDelete, changes.Delete, records, zones)...)
|
||||||
|
combinedChanges = append(combinedChanges, updateChanges...)
|
||||||
|
|
||||||
return p.submitChanges(ctx, combinedChanges, zones)
|
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
|
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("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), "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("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())
|
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("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), "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("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{
|
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{
|
||||||
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
|
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
|
||||||
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
|
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
|
||||||
|
endpoint.NewEndpointWithTTL("update-test-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("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"),
|
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{
|
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-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.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("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"),
|
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{
|
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-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.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("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"),
|
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{
|
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-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.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("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"),
|
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("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
|
||||||
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
|
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
|
||||||
endpoint.NewEndpointWithTTL("delete-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("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("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
|
||||||
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
|
endpoint.NewEndpointWithTTL("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{
|
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-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.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.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-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"),
|
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{
|
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-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.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.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-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"),
|
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("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("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.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("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("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
|
||||||
endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
|
endpoint.NewEndpointWithTTL("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("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
|
||||||
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
|
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
|
||||||
endpoint.NewEndpointWithTTL("delete-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("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("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
|
||||||
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
|
endpoint.NewEndpointWithTTL("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{
|
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-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.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.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-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"),
|
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{
|
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-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.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.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-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"),
|
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.sa-east-1.elb.amazonaws.com", "Z2P70J7HTTTPLU"},
|
||||||
{"foo.cn-north-1.elb.amazonaws.com.cn", "Z1GDH35T77C1KE"},
|
{"foo.cn-north-1.elb.amazonaws.com.cn", "Z1GDH35T77C1KE"},
|
||||||
{"foo.cn-northwest-1.elb.amazonaws.com.cn", "ZM7IZAIOVVDZF"},
|
{"foo.cn-northwest-1.elb.amazonaws.com.cn", "ZM7IZAIOVVDZF"},
|
||||||
|
{"foo.af-south-1.elb.amazonaws.com", "Z268VQBMOI5EKX"},
|
||||||
// Network Load Balancers
|
// Network Load Balancers
|
||||||
{"foo.elb.us-east-2.amazonaws.com", "ZLMOA37VPKANP"},
|
{"foo.elb.us-east-2.amazonaws.com", "ZLMOA37VPKANP"},
|
||||||
{"foo.elb.us-east-1.amazonaws.com", "Z26RNL4JYFTOTI"},
|
{"foo.elb.us-east-1.amazonaws.com", "Z26RNL4JYFTOTI"},
|
||||||
@ -1046,6 +1066,7 @@ func TestAWSCanonicalHostedZone(t *testing.T) {
|
|||||||
{"foo.elb.sa-east-1.amazonaws.com", "ZTK26PT1VY4CU"},
|
{"foo.elb.sa-east-1.amazonaws.com", "ZTK26PT1VY4CU"},
|
||||||
{"foo.elb.cn-north-1.amazonaws.com.cn", "Z3QFB96KMJ7ED6"},
|
{"foo.elb.cn-north-1.amazonaws.com.cn", "Z3QFB96KMJ7ED6"},
|
||||||
{"foo.elb.cn-northwest-1.amazonaws.com.cn", "ZQEIKTCZ8352D"},
|
{"foo.elb.cn-northwest-1.amazonaws.com.cn", "ZQEIKTCZ8352D"},
|
||||||
|
{"foo.elb.af-south-1.amazonaws.com", "Z203XCE67M25HM"},
|
||||||
// No Load Balancer
|
// No Load Balancer
|
||||||
{"foo.example.org", ""},
|
{"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) {
|
func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) {
|
||||||
params := &route53.CreateHostedZoneInput{
|
params := &route53.CreateHostedZoneInput{
|
||||||
CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"),
|
CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"),
|
||||||
|
@ -19,17 +19,12 @@ package azure
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
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/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
|
||||||
"github.com/Azure/go-autorest/autorest"
|
"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"
|
"github.com/Azure/go-autorest/autorest/to"
|
||||||
|
|
||||||
"sigs.k8s.io/external-dns/endpoint"
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
@ -41,18 +36,6 @@ const (
|
|||||||
azureRecordTTL = 300
|
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.
|
// ZonesClient is an interface of dns.ZoneClient that can be stubbed for testing.
|
||||||
type ZonesClient interface {
|
type ZonesClient interface {
|
||||||
ListByResourceGroupComplete(ctx context.Context, resourceGroupName string, top *int32) (result dns.ZoneListResultIterator, err error)
|
ListByResourceGroupComplete(ctx context.Context, resourceGroupName string, top *int32) (result dns.ZoneListResultIterator, err error)
|
||||||
@ -69,6 +52,7 @@ type RecordSetsClient interface {
|
|||||||
type AzureProvider struct {
|
type AzureProvider struct {
|
||||||
provider.BaseProvider
|
provider.BaseProvider
|
||||||
domainFilter endpoint.DomainFilter
|
domainFilter endpoint.DomainFilter
|
||||||
|
zoneNameFilter endpoint.DomainFilter
|
||||||
zoneIDFilter provider.ZoneIDFilter
|
zoneIDFilter provider.ZoneIDFilter
|
||||||
dryRun bool
|
dryRun bool
|
||||||
resourceGroup string
|
resourceGroup string
|
||||||
@ -80,109 +64,32 @@ type AzureProvider struct {
|
|||||||
// NewAzureProvider creates a new Azure provider.
|
// NewAzureProvider creates a new Azure provider.
|
||||||
//
|
//
|
||||||
// Returns the provider or an error if a provider could not be created.
|
// Returns the provider or an error if a provider could not be created.
|
||||||
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) {
|
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) {
|
||||||
contents, err := ioutil.ReadFile(configFile)
|
cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
|
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a resource group was given, override what was present in the config file
|
token, err := getAccessToken(*cfg, cfg.Environment)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get token: %v", err)
|
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)
|
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)
|
recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
|
||||||
|
|
||||||
provider := &AzureProvider{
|
return &AzureProvider{
|
||||||
domainFilter: domainFilter,
|
domainFilter: domainFilter,
|
||||||
|
zoneNameFilter: zoneNameFilter,
|
||||||
zoneIDFilter: zoneIDFilter,
|
zoneIDFilter: zoneIDFilter,
|
||||||
dryRun: dryRun,
|
dryRun: dryRun,
|
||||||
resourceGroup: cfg.ResourceGroup,
|
resourceGroup: cfg.ResourceGroup,
|
||||||
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
|
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
|
||||||
zonesClient: zonesClient,
|
zonesClient: zonesClient,
|
||||||
recordSetsClient: recordSetsClient,
|
recordSetsClient: recordSetsClient,
|
||||||
}
|
}, nil
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Records gets the current records.
|
// Records gets the current records.
|
||||||
@ -205,6 +112,11 @@ func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
name := formatAzureDNSName(*recordSet.Name, *zone.Name)
|
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)
|
targets := extractAzureTargets(&recordSet)
|
||||||
if len(targets) == 0 {
|
if len(targets) == 0 {
|
||||||
log.Errorf("Failed to extract targets for '%s' with type '%s'.", name, recordType)
|
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) {
|
if zone.Name != nil && p.domainFilter.Match(*zone.Name) && p.zoneIDFilter.Match(*zone.ID) {
|
||||||
zones = append(zones, zone)
|
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)
|
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) {
|
func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMap) {
|
||||||
// Delete records first
|
// Delete records first
|
||||||
for zone, endpoints := range deleted {
|
for zone, endpoints := range deleted {
|
||||||
for _, endpoint := range endpoints {
|
for _, ep := range endpoints {
|
||||||
name := p.recordSetNameForZone(zone, endpoint)
|
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 {
|
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 {
|
} else {
|
||||||
log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone)
|
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(endpoint.RecordType), ""); err != nil {
|
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(ep.RecordType), ""); err != nil {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
"Failed to delete %s record named '%s' for Azure DNS zone '%s': %v",
|
"Failed to delete %s record named '%s' for Azure DNS zone '%s': %v",
|
||||||
endpoint.RecordType,
|
ep.RecordType,
|
||||||
name,
|
name,
|
||||||
zone,
|
zone,
|
||||||
err,
|
err,
|
||||||
@ -364,14 +283,18 @@ func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMa
|
|||||||
|
|
||||||
func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMap) {
|
func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMap) {
|
||||||
for zone, endpoints := range updated {
|
for zone, endpoints := range updated {
|
||||||
for _, endpoint := range endpoints {
|
for _, ep := range endpoints {
|
||||||
name := p.recordSetNameForZone(zone, endpoint)
|
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 {
|
if p.dryRun {
|
||||||
log.Infof(
|
log.Infof(
|
||||||
"Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.",
|
"Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.",
|
||||||
endpoint.RecordType,
|
ep.RecordType,
|
||||||
name,
|
name,
|
||||||
endpoint.Targets,
|
ep.Targets,
|
||||||
zone,
|
zone,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@ -379,20 +302,20 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
|
|||||||
|
|
||||||
log.Infof(
|
log.Infof(
|
||||||
"Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.",
|
"Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.",
|
||||||
endpoint.RecordType,
|
ep.RecordType,
|
||||||
name,
|
name,
|
||||||
endpoint.Targets,
|
ep.Targets,
|
||||||
zone,
|
zone,
|
||||||
)
|
)
|
||||||
|
|
||||||
recordSet, err := p.newRecordSet(endpoint)
|
recordSet, err := p.newRecordSet(ep)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_, err = p.recordSetsClient.CreateOrUpdate(
|
_, err = p.recordSetsClient.CreateOrUpdate(
|
||||||
ctx,
|
ctx,
|
||||||
p.resourceGroup,
|
p.resourceGroup,
|
||||||
zone,
|
zone,
|
||||||
name,
|
name,
|
||||||
dns.RecordType(endpoint.RecordType),
|
dns.RecordType(ep.RecordType),
|
||||||
recordSet,
|
recordSet,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
@ -401,9 +324,9 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
"Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v",
|
"Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v",
|
||||||
endpoint.RecordType,
|
ep.RecordType,
|
||||||
name,
|
name,
|
||||||
endpoint.Targets,
|
ep.Targets,
|
||||||
zone,
|
zone,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
@ -21,9 +21,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"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"
|
||||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
|
||||||
"github.com/Azure/go-autorest/autorest/to"
|
"github.com/Azure/go-autorest/autorest/to"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@ -50,8 +49,8 @@ type AzurePrivateDNSProvider struct {
|
|||||||
domainFilter endpoint.DomainFilter
|
domainFilter endpoint.DomainFilter
|
||||||
zoneIDFilter provider.ZoneIDFilter
|
zoneIDFilter provider.ZoneIDFilter
|
||||||
dryRun bool
|
dryRun bool
|
||||||
subscriptionID string
|
|
||||||
resourceGroup string
|
resourceGroup string
|
||||||
|
userAssignedIdentityClientID string
|
||||||
zonesClient PrivateZonesClient
|
zonesClient PrivateZonesClient
|
||||||
recordSetsClient PrivateRecordSetsClient
|
recordSetsClient PrivateRecordSetsClient
|
||||||
}
|
}
|
||||||
@ -59,32 +58,31 @@ type AzurePrivateDNSProvider struct {
|
|||||||
// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
|
// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
|
||||||
//
|
//
|
||||||
// Returns the provider or an error if a provider could not be created.
|
// Returns the provider or an error if a provider could not be created.
|
||||||
func NewAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, subscriptionID string, dryRun bool) (*AzurePrivateDNSProvider, error) {
|
func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup, userAssignedIdentityClientID string, dryRun bool) (*AzurePrivateDNSProvider, error) {
|
||||||
authorizer, err := auth.NewAuthorizerFromEnvironment()
|
cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to get token: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID)
|
zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
|
||||||
zonesClient.Authorizer = authorizer
|
zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
|
||||||
recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID)
|
recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
|
||||||
recordSetsClient.Authorizer = authorizer
|
recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
|
||||||
|
|
||||||
provider := &AzurePrivateDNSProvider{
|
return &AzurePrivateDNSProvider{
|
||||||
domainFilter: domainFilter,
|
domainFilter: domainFilter,
|
||||||
zoneIDFilter: zoneIDFilter,
|
zoneIDFilter: zoneIDFilter,
|
||||||
dryRun: dryRun,
|
dryRun: dryRun,
|
||||||
subscriptionID: subscriptionID,
|
resourceGroup: cfg.ResourceGroup,
|
||||||
resourceGroup: resourceGroup,
|
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
|
||||||
zonesClient: zonesClient,
|
zonesClient: zonesClient,
|
||||||
recordSetsClient: recordSetsClient,
|
recordSetsClient: recordSetsClient,
|
||||||
}
|
}, nil
|
||||||
return provider, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Records gets the current records.
|
// 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))
|
log.Debugf("Records to be deleted: %d", len(deleted))
|
||||||
// Delete records first
|
// Delete records first
|
||||||
for zone, endpoints := range deleted {
|
for zone, endpoints := range deleted {
|
||||||
for _, endpoint := range endpoints {
|
for _, ep := range endpoints {
|
||||||
name := p.recordSetNameForZone(zone, endpoint)
|
name := p.recordSetNameForZone(zone, ep)
|
||||||
if p.dryRun {
|
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 {
|
} else {
|
||||||
log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone)
|
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(endpoint.RecordType), name, ""); err != nil {
|
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(ep.RecordType), name, ""); err != nil {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
"Failed to delete %s record named '%s' for Azure Private DNS zone '%s': %v",
|
"Failed to delete %s record named '%s' for Azure Private DNS zone '%s': %v",
|
||||||
endpoint.RecordType,
|
ep.RecordType,
|
||||||
name,
|
name,
|
||||||
zone,
|
zone,
|
||||||
err,
|
err,
|
||||||
@ -279,14 +277,14 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu
|
|||||||
func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azurePrivateDNSChangeMap) {
|
func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azurePrivateDNSChangeMap) {
|
||||||
log.Debugf("Records to be updated: %d", len(updated))
|
log.Debugf("Records to be updated: %d", len(updated))
|
||||||
for zone, endpoints := range updated {
|
for zone, endpoints := range updated {
|
||||||
for _, endpoint := range endpoints {
|
for _, ep := range endpoints {
|
||||||
name := p.recordSetNameForZone(zone, endpoint)
|
name := p.recordSetNameForZone(zone, ep)
|
||||||
if p.dryRun {
|
if p.dryRun {
|
||||||
log.Infof(
|
log.Infof(
|
||||||
"Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
|
"Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
|
||||||
endpoint.RecordType,
|
ep.RecordType,
|
||||||
name,
|
name,
|
||||||
endpoint.Targets,
|
ep.Targets,
|
||||||
zone,
|
zone,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@ -294,19 +292,19 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
|
|||||||
|
|
||||||
log.Infof(
|
log.Infof(
|
||||||
"Updating %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
|
"Updating %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
|
||||||
endpoint.RecordType,
|
ep.RecordType,
|
||||||
name,
|
name,
|
||||||
endpoint.Targets,
|
ep.Targets,
|
||||||
zone,
|
zone,
|
||||||
)
|
)
|
||||||
|
|
||||||
recordSet, err := p.newRecordSet(endpoint)
|
recordSet, err := p.newRecordSet(ep)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_, err = p.recordSetsClient.CreateOrUpdate(
|
_, err = p.recordSetsClient.CreateOrUpdate(
|
||||||
ctx,
|
ctx,
|
||||||
p.resourceGroup,
|
p.resourceGroup,
|
||||||
zone,
|
zone,
|
||||||
privatedns.RecordType(endpoint.RecordType),
|
privatedns.RecordType(ep.RecordType),
|
||||||
name,
|
name,
|
||||||
recordSet,
|
recordSet,
|
||||||
"",
|
"",
|
||||||
@ -316,9 +314,9 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
"Failed to update %s record named '%s' to '%s' for Azure Private DNS zone '%s': %v",
|
"Failed to update %s record named '%s' to '%s' for Azure Private DNS zone '%s': %v",
|
||||||
endpoint.RecordType,
|
ep.RecordType,
|
||||||
name,
|
name,
|
||||||
endpoint.Targets,
|
ep.Targets,
|
||||||
zone,
|
zone,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
@ -18,16 +18,11 @@ package azure
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/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"
|
||||||
"github.com/Azure/go-autorest/autorest/azure"
|
|
||||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
|
||||||
"github.com/Azure/go-autorest/autorest/to"
|
"github.com/Azure/go-autorest/autorest/to"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"sigs.k8s.io/external-dns/endpoint"
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
"sigs.k8s.io/external-dns/plan"
|
"sigs.k8s.io/external-dns/plan"
|
||||||
"sigs.k8s.io/external-dns/provider"
|
"sigs.k8s.io/external-dns/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) {
|
func TestAzurePrivateDNSRecord(t *testing.T) {
|
||||||
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s",
|
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s",
|
||||||
&[]privatedns.PrivateZone{
|
&[]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
|
// 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
|
// init zone-related parts of the mock-client
|
||||||
pageIterator := mockZoneListResultPageIterator{
|
pageIterator := mockZoneListResultPageIterator{
|
||||||
results: []dns.ZoneListResult{
|
results: []dns.ZoneListResult{
|
||||||
@ -237,12 +237,13 @@ func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneIDFilter pro
|
|||||||
mockRecordSetListIterator: &mockRecordSetListIterator,
|
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{
|
return &AzureProvider{
|
||||||
domainFilter: domainFilter,
|
domainFilter: domainFilter,
|
||||||
|
zoneNameFilter: zoneNameFilter,
|
||||||
zoneIDFilter: zoneIDFilter,
|
zoneIDFilter: zoneIDFilter,
|
||||||
dryRun: dryRun,
|
dryRun: dryRun,
|
||||||
resourceGroup: resourceGroup,
|
resourceGroup: resourceGroup,
|
||||||
@ -257,7 +258,7 @@ func validateAzureEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expect
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAzureRecord(t *testing.T) {
|
func TestAzureRecord(t *testing.T) {
|
||||||
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
|
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"example.com"}), endpoint.NewDomainFilter([]string{}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
|
||||||
&[]dns.Zone{
|
&[]dns.Zone{
|
||||||
createMockZone("example.com", "/dnszones/example.com"),
|
createMockZone("example.com", "/dnszones/example.com"),
|
||||||
},
|
},
|
||||||
@ -294,7 +295,7 @@ func TestAzureRecord(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAzureMultiRecord(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{
|
&[]dns.Zone{
|
||||||
createMockZone("example.com", "/dnszones/example.com"),
|
createMockZone("example.com", "/dnszones/example.com"),
|
||||||
},
|
},
|
||||||
@ -393,6 +394,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC
|
|||||||
}
|
}
|
||||||
|
|
||||||
provider := newAzureProvider(
|
provider := newAzureProvider(
|
||||||
|
endpoint.NewDomainFilter([]string{""}),
|
||||||
endpoint.NewDomainFilter([]string{""}),
|
endpoint.NewDomainFilter([]string{""}),
|
||||||
provider.NewZoneIDFilter([]string{""}),
|
provider.NewZoneIDFilter([]string{""}),
|
||||||
dryRun,
|
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))
|
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
|
p.PaginationOptions.Page = 1
|
||||||
|
|
||||||
// if there is a zoneIDfilter configured
|
// 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] != "" {
|
if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" {
|
||||||
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
|
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
|
||||||
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
|
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
|
||||||
@ -331,6 +331,18 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
|
|||||||
return nil
|
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.
|
// changesByZone separates a multi-zone change into a single change per zone.
|
||||||
func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
|
func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
|
||||||
changes := make(map[string][]*cloudFlareChange)
|
changes := make(map[string][]*cloudFlareChange)
|
||||||
|
@ -229,7 +229,7 @@ func (m *mockCloudFlareClient) ZoneDetails(zoneID string) (cloudflare.Zone, erro
|
|||||||
return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
|
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()
|
t.Helper()
|
||||||
|
|
||||||
var client *mockCloudFlareClient
|
var client *mockCloudFlareClient
|
||||||
@ -253,13 +253,14 @@ func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endp
|
|||||||
Current: records,
|
Current: records,
|
||||||
Desired: endpoints,
|
Desired: endpoints,
|
||||||
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
|
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
|
||||||
|
ManagedRecords: managedRecords,
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := plan.Calculate().Changes
|
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 {
|
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)
|
changes.Create = append(changes.Create, endpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -305,7 +306,10 @@ func TestCloudflareA(t *testing.T) {
|
|||||||
Proxied: false,
|
Proxied: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudflareCname(t *testing.T) {
|
func TestCloudflareCname(t *testing.T) {
|
||||||
@ -340,7 +344,9 @@ func TestCloudflareCname(t *testing.T) {
|
|||||||
Proxied: false,
|
Proxied: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudflareCustomTTL(t *testing.T) {
|
func TestCloudflareCustomTTL(t *testing.T) {
|
||||||
@ -365,7 +371,9 @@ func TestCloudflareCustomTTL(t *testing.T) {
|
|||||||
Proxied: false,
|
Proxied: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudflareProxiedDefault(t *testing.T) {
|
func TestCloudflareProxiedDefault(t *testing.T) {
|
||||||
@ -389,7 +397,9 @@ func TestCloudflareProxiedDefault(t *testing.T) {
|
|||||||
Proxied: true,
|
Proxied: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudflareProxiedOverrideTrue(t *testing.T) {
|
func TestCloudflareProxiedOverrideTrue(t *testing.T) {
|
||||||
@ -419,7 +429,9 @@ func TestCloudflareProxiedOverrideTrue(t *testing.T) {
|
|||||||
Proxied: true,
|
Proxied: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudflareProxiedOverrideFalse(t *testing.T) {
|
func TestCloudflareProxiedOverrideFalse(t *testing.T) {
|
||||||
@ -449,7 +461,9 @@ func TestCloudflareProxiedOverrideFalse(t *testing.T) {
|
|||||||
Proxied: false,
|
Proxied: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
|
func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
|
||||||
@ -479,7 +493,9 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
|
|||||||
Proxied: true,
|
Proxied: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudflareSetProxied(t *testing.T) {
|
func TestCloudflareSetProxied(t *testing.T) {
|
||||||
@ -525,7 +541,7 @@ func TestCloudflareSetProxied(t *testing.T) {
|
|||||||
Proxied: testCase.proxiable,
|
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,
|
Current: current,
|
||||||
Desired: desired,
|
Desired: desired,
|
||||||
PropertyComparator: provider.PropertyValuesEqual,
|
PropertyComparator: provider.PropertyValuesEqual,
|
||||||
|
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||||
}
|
}
|
||||||
|
|
||||||
plan = *plan.Calculate()
|
plan = *plan.Calculate()
|
||||||
@ -1092,6 +1109,7 @@ 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()
|
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
|
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) {
|
func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) (*tls.Config, error) {
|
||||||
if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
|
if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
|
||||||
return nil, errors.New("either both cert and key or none must be provided")
|
return nil, errors.New("either both cert and key or none must be provided")
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"sigs.k8s.io/external-dns/endpoint"
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
|
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
|
||||||
"sigs.k8s.io/external-dns/plan"
|
"sigs.k8s.io/external-dns/plan"
|
||||||
"sigs.k8s.io/external-dns/provider"
|
"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{
|
oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
|
||||||
AccessToken: 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{
|
p := &DigitalOceanProvider{
|
||||||
Client: client.Domains,
|
Client: client.Domains,
|
||||||
|
@ -195,7 +195,7 @@ func fixMissingTTL(ttl endpoint.TTL, minTTLSeconds int) string {
|
|||||||
return strconv.Itoa(i)
|
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
|
// Dyn allows to replace all records with a single call
|
||||||
// Invariant: the result contains only elements from the updateNew parameter
|
// Invariant: the result contains only elements from the updateNew parameter
|
||||||
func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint {
|
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
|
// this method does C + 2*Z requests: C=total number of changes, Z = number of
|
||||||
// affected zones (1 login + 1 commit)
|
// affected zones (1 login + 1 commit)
|
||||||
func (d *dynProviderState) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
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 {
|
if d.DryRun {
|
||||||
log.Infof("Will NOT delete these records: %+v", changes.Delete)
|
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 _, changes := range zoneChanges {
|
||||||
for _, change := range changes {
|
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{
|
log.WithFields(log.Fields{
|
||||||
|
"id": change.ResourceRecordSet.ID,
|
||||||
"record": change.ResourceRecordSet.Name,
|
"record": change.ResourceRecordSet.Name,
|
||||||
"type": change.ResourceRecordSet.RecordType,
|
"type": change.ResourceRecordSet.RecordType,
|
||||||
|
"value": change.ResourceRecordSet.Value,
|
||||||
"ttl": change.ResourceRecordSet.TTL,
|
"ttl": change.ResourceRecordSet.TTL,
|
||||||
"action": change.Action,
|
"action": change.Action,
|
||||||
"zone": change.ZoneName,
|
"zone": change.ZoneName,
|
||||||
"zone_id": change.ZoneID,
|
"zone_id": change.ZoneID,
|
||||||
}).Info("Changing record.")
|
}).Info("Changing record")
|
||||||
|
|
||||||
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, "."+change.ZoneName)
|
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, "."+change.ZoneName)
|
||||||
if 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,
|
Value: change.ResourceRecordSet.Value,
|
||||||
TTL: change.ResourceRecordSet.TTL,
|
TTL: change.ResourceRecordSet.TTL,
|
||||||
}
|
}
|
||||||
_, err := p.Client.CreateRecord(record)
|
answer, err := p.Client.CreateRecord(record)
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
case hetznerDelete:
|
case hetznerDelete:
|
||||||
_, err := p.Client.DeleteRecord(change.ResourceRecordSet.ID)
|
answer, err := p.Client.DeleteRecord(change.ResourceRecordSet.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"Code": answer.Error.Code,
|
||||||
|
"Message": answer.Error.Message,
|
||||||
|
}).Warning("Delete problem")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case hetznerUpdate:
|
case hetznerUpdate:
|
||||||
@ -159,9 +196,17 @@ func (p *HetznerProvider) submitChanges(ctx context.Context, changes []*HetznerC
|
|||||||
Name: change.ResourceRecordSet.Name,
|
Name: change.ResourceRecordSet.Name,
|
||||||
Value: change.ResourceRecordSet.Value,
|
Value: change.ResourceRecordSet.Value,
|
||||||
TTL: change.ResourceRecordSet.TTL,
|
TTL: change.ResourceRecordSet.TTL,
|
||||||
|
ID: change.ResourceRecordSet.ID,
|
||||||
}
|
}
|
||||||
_, err := p.Client.UpdateRecord(record)
|
answer, err := p.Client.UpdateRecord(record)
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ type mockHCloudClientAdapter interface {
|
|||||||
ImportZoneString(zoneID string, zonePlainText string) (hclouddns.HCloudAnswerGetZone, error)
|
ImportZoneString(zoneID string, zonePlainText string) (hclouddns.HCloudAnswerGetZone, error)
|
||||||
ExportZoneToString(zoneID string) (hclouddns.HCloudAnswerGetZonePlainText, error)
|
ExportZoneToString(zoneID string) (hclouddns.HCloudAnswerGetZonePlainText, error)
|
||||||
ValidateZoneString(zonePlainText string) (hclouddns.HCloudAnswerZoneValidate, error)
|
ValidateZoneString(zonePlainText string) (hclouddns.HCloudAnswerZoneValidate, error)
|
||||||
|
GetRecord(ID string) (hclouddns.HCloudAnswerGetRecord, error)
|
||||||
GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error)
|
GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error)
|
||||||
UpdateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error)
|
UpdateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error)
|
||||||
DeleteRecord(ID string) (hclouddns.HCloudAnswerDeleteRecord, error)
|
DeleteRecord(ID string) (hclouddns.HCloudAnswerDeleteRecord, error)
|
||||||
@ -95,6 +96,11 @@ func (m *mockHCloudClient) ValidateZoneString(zonePlainText string) (hclouddns.H
|
|||||||
}
|
}
|
||||||
|
|
||||||
// records
|
// records
|
||||||
|
|
||||||
|
func (m *mockHCloudClient) GetRecord(ID string) (hclouddns.HCloudAnswerGetRecord, error) {
|
||||||
|
return hclouddns.HCloudAnswerGetRecord{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockHCloudClient) GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error) {
|
func (m *mockHCloudClient) GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error) {
|
||||||
return hclouddns.HCloudAnswerGetRecords{
|
return hclouddns.HCloudAnswerGetRecords{
|
||||||
Records: []hclouddns.HCloudRecord{
|
Records: []hclouddns.HCloudRecord{
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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)
|
return nil, fmt.Errorf("could not fetch A records from zone '%s': %s", zone.Fqdn, err)
|
||||||
}
|
}
|
||||||
for _, res := range resA {
|
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
|
// 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) {
|
func (p *InfobloxProvider) zones() ([]ibclient.ZoneAuth, error) {
|
||||||
var res, result []ibclient.ZoneAuth
|
var res, result []ibclient.ZoneAuth
|
||||||
obj := ibclient.NewZoneAuth(ibclient.ZoneAuth{})
|
obj := ibclient.NewZoneAuth(
|
||||||
|
ibclient.ZoneAuth{
|
||||||
|
View: p.view,
|
||||||
|
},
|
||||||
|
)
|
||||||
err := p.client.GetObject(obj, "", &res)
|
err := p.client.GetObject(obj, "", &res)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -305,14 +327,14 @@ func (p *InfobloxProvider) findZone(zones []ibclient.ZoneAuth, name string) *ibc
|
|||||||
return result
|
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 {
|
switch ep.RecordType {
|
||||||
case endpoint.RecordTypeA:
|
case endpoint.RecordTypeA:
|
||||||
var res []ibclient.RecordA
|
var res []ibclient.RecordA
|
||||||
obj := ibclient.NewRecordA(
|
obj := ibclient.NewRecordA(
|
||||||
ibclient.RecordA{
|
ibclient.RecordA{
|
||||||
Name: ep.DNSName,
|
Name: ep.DNSName,
|
||||||
Ipv4Addr: ep.Targets[0],
|
Ipv4Addr: ep.Targets[targetIndex],
|
||||||
View: p.view,
|
View: p.view,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -395,13 +417,14 @@ func (p *InfobloxProvider) createRecords(created infobloxChangeMap) {
|
|||||||
zone,
|
zone,
|
||||||
)
|
)
|
||||||
|
|
||||||
recordSet, err := p.recordSet(ep, false)
|
for targetIndex := range ep.Targets {
|
||||||
|
recordSet, err := p.recordSet(ep, false, targetIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf(
|
logrus.Errorf(
|
||||||
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
|
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
|
||||||
ep.RecordType,
|
ep.RecordType,
|
||||||
ep.DNSName,
|
ep.DNSName,
|
||||||
ep.Targets,
|
ep.Targets[targetIndex],
|
||||||
zone,
|
zone,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
@ -413,13 +436,14 @@ func (p *InfobloxProvider) createRecords(created infobloxChangeMap) {
|
|||||||
"Failed to create %s record named '%s' to '%s' for DNS zone '%s': %v",
|
"Failed to create %s record named '%s' to '%s' for DNS zone '%s': %v",
|
||||||
ep.RecordType,
|
ep.RecordType,
|
||||||
ep.DNSName,
|
ep.DNSName,
|
||||||
ep.Targets,
|
ep.Targets[targetIndex],
|
||||||
zone,
|
zone,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *InfobloxProvider) deleteRecords(deleted infobloxChangeMap) {
|
func (p *InfobloxProvider) deleteRecords(deleted infobloxChangeMap) {
|
||||||
@ -430,13 +454,14 @@ func (p *InfobloxProvider) deleteRecords(deleted infobloxChangeMap) {
|
|||||||
logrus.Infof("Would delete %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
|
logrus.Infof("Would delete %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
|
||||||
} else {
|
} else {
|
||||||
logrus.Infof("Deleting %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
|
logrus.Infof("Deleting %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
|
||||||
recordSet, err := p.recordSet(ep, true)
|
for targetIndex := range ep.Targets {
|
||||||
|
recordSet, err := p.recordSet(ep, true, targetIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf(
|
logrus.Errorf(
|
||||||
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
|
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
|
||||||
ep.RecordType,
|
ep.RecordType,
|
||||||
ep.DNSName,
|
ep.DNSName,
|
||||||
ep.Targets,
|
ep.Targets[targetIndex],
|
||||||
zone,
|
zone,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
@ -468,6 +493,7 @@ func (p *InfobloxProvider) deleteRecords(deleted infobloxChangeMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupEnvAtoi(key string, fallback int) (i int) {
|
func lookupEnvAtoi(key string, fallback int) (i int) {
|
||||||
|
@ -327,6 +327,18 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject
|
|||||||
Text: value,
|
Text: value,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
case "HOST":
|
||||||
|
return ibclient.NewHostRecord(
|
||||||
|
ibclient.HostRecord{
|
||||||
|
Ref: ref,
|
||||||
|
Name: name,
|
||||||
|
Ipv4Addrs: []ibclient.HostRecordIpv4Addr{
|
||||||
|
{
|
||||||
|
Ipv4Addr: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -344,6 +356,7 @@ func TestInfobloxRecords(t *testing.T) {
|
|||||||
client := mockIBConnector{
|
client := mockIBConnector{
|
||||||
mockInfobloxZones: &[]ibclient.ZoneAuth{
|
mockInfobloxZones: &[]ibclient.ZoneAuth{
|
||||||
createMockInfobloxZone("example.com"),
|
createMockInfobloxZone("example.com"),
|
||||||
|
createMockInfobloxZone("other.com"),
|
||||||
},
|
},
|
||||||
mockInfobloxObjects: &[]ibclient.IBObject{
|
mockInfobloxObjects: &[]ibclient.IBObject{
|
||||||
createMockInfobloxObject("example.com", endpoint.RecordTypeA, "123.123.123.122"),
|
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.RecordTypeA, "123.123.123.124"),
|
||||||
createMockInfobloxObject("whitespace.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=white space"),
|
createMockInfobloxObject("whitespace.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=white space"),
|
||||||
createMockInfobloxObject("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"),
|
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.RecordTypeA, "123.123.123.124"),
|
||||||
endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=white space\""),
|
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("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)
|
validateEndpoints(t, actual, expected)
|
||||||
}
|
}
|
||||||
@ -390,12 +415,15 @@ func TestInfobloxApplyChanges(t *testing.T) {
|
|||||||
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
|
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
|
||||||
endpoint.NewEndpoint("new.example.com", endpoint.RecordTypeA, "111.222.111.222"),
|
endpoint.NewEndpoint("new.example.com", endpoint.RecordTypeA, "111.222.111.222"),
|
||||||
endpoint.NewEndpoint("newcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
|
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{
|
validateEndpoints(t, client.deletedEndpoints, []*endpoint.Endpoint{
|
||||||
endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""),
|
endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""),
|
||||||
endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""),
|
endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""),
|
||||||
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""),
|
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""),
|
||||||
|
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeTXT, ""),
|
||||||
endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""),
|
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{
|
client.(*mockIBConnector).mockInfobloxObjects = &[]ibclient.IBObject{
|
||||||
createMockInfobloxObject("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"),
|
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("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
|
||||||
createMockInfobloxObject("old.example.com", endpoint.RecordTypeA, "121.212.121.212"),
|
createMockInfobloxObject("old.example.com", endpoint.RecordTypeA, "121.212.121.212"),
|
||||||
createMockInfobloxObject("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
|
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("other.com", endpoint.RecordTypeTXT, "tag"),
|
||||||
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
|
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
|
||||||
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
|
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{
|
updateOldRecords := []*endpoint.Endpoint{
|
||||||
@ -462,6 +493,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
|
|||||||
|
|
||||||
deleteRecords := []*endpoint.Endpoint{
|
deleteRecords := []*endpoint.Endpoint{
|
||||||
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"),
|
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("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
|
||||||
endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
|
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.
|
// ApplyChanges applies a given set of changes to a given zone.
|
||||||
func (p *OCIProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
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 := []dns.RecordOperation{}
|
||||||
ops = append(ops, p.newFilteredRecordOperations(changes.Create, dns.RecordOperationOperationAdd)...)
|
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())
|
zones, records, err := provider.zonesRecords(context.TODO())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.ElementsMatch(zones, []string{"example.org"})
|
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)
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
// Error on getting zones list
|
// Error on getting zones list
|
||||||
@ -140,8 +140,8 @@ func TestOvhRecords(t *testing.T) {
|
|||||||
endpoints, err := provider.Records(context.TODO())
|
endpoints, err := provider.Records(context.TODO())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
// Little fix for multi targets endpoint
|
// Little fix for multi targets endpoint
|
||||||
for _, endoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
sort.Strings(endoint.Targets)
|
sort.Strings(endpoint.Targets)
|
||||||
}
|
}
|
||||||
assert.ElementsMatch(endpoints, []*endpoint.Endpoint{
|
assert.ElementsMatch(endpoints, []*endpoint.Endpoint{
|
||||||
{DNSName: "example.org", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42"}},
|
{DNSName: "example.org", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42"}},
|
||||||
|
@ -30,11 +30,16 @@ type Provider interface {
|
|||||||
Records(ctx context.Context) ([]*endpoint.Endpoint, error)
|
Records(ctx context.Context) ([]*endpoint.Endpoint, error)
|
||||||
ApplyChanges(ctx context.Context, changes *plan.Changes) error
|
ApplyChanges(ctx context.Context, changes *plan.Changes) error
|
||||||
PropertyValuesEqual(name string, previous string, current string) bool
|
PropertyValuesEqual(name string, previous string, current string) bool
|
||||||
|
AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseProvider struct {
|
type BaseProvider struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b BaseProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
|
||||||
|
return endpoints
|
||||||
|
}
|
||||||
|
|
||||||
func (b BaseProvider) PropertyValuesEqual(name, previous, current string) bool {
|
func (b BaseProvider) PropertyValuesEqual(name, previous, current string) bool {
|
||||||
return previous == current
|
return previous == current
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ func TestRcodeZeroProvider_Records(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("should not fail, %s", err)
|
t.Errorf("should not fail, %s", err)
|
||||||
}
|
}
|
||||||
require.Equal(t, 6, len(endpoints))
|
require.Equal(t, 10, len(endpoints))
|
||||||
|
|
||||||
mockRRSetService.TestErrorReturned = true
|
mockRRSetService.TestErrorReturned = true
|
||||||
|
|
||||||
|
@ -17,10 +17,10 @@ limitations under the License.
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
// SupportedRecordType returns true only for supported record types.
|
// SupportedRecordType returns true only for supported record types.
|
||||||
// Currently A, CNAME, SRV, and TXT record types are supported.
|
// Currently A, CNAME, SRV, TXT and NS record types are supported.
|
||||||
func SupportedRecordType(recordType string) bool {
|
func SupportedRecordType(recordType string) bool {
|
||||||
switch recordType {
|
switch recordType {
|
||||||
case "A", "CNAME", "SRV", "TXT":
|
case "A", "CNAME", "SRV", "TXT", "NS":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -269,8 +269,13 @@ func endpointToScalewayRecords(zoneName string, ep *endpoint.Endpoint) []*domain
|
|||||||
records := []*domain.Record{}
|
records := []*domain.Record{}
|
||||||
|
|
||||||
for _, target := range ep.Targets {
|
for _, target := range ep.Targets {
|
||||||
|
finalTargetName := target
|
||||||
|
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
|
||||||
|
finalTargetName = provider.EnsureTrailingDot(target)
|
||||||
|
}
|
||||||
|
|
||||||
records = append(records, &domain.Record{
|
records = append(records, &domain.Record{
|
||||||
Data: target,
|
Data: finalTargetName,
|
||||||
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
|
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
TTL: ttl,
|
TTL: ttl,
|
||||||
@ -285,9 +290,14 @@ func endpointToScalewayRecordsChangeDelete(zoneName string, ep *endpoint.Endpoin
|
|||||||
records := []*domain.RecordChange{}
|
records := []*domain.RecordChange{}
|
||||||
|
|
||||||
for _, target := range ep.Targets {
|
for _, target := range ep.Targets {
|
||||||
|
finalTargetName := target
|
||||||
|
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
|
||||||
|
finalTargetName = provider.EnsureTrailingDot(target)
|
||||||
|
}
|
||||||
|
|
||||||
records = append(records, &domain.RecordChange{
|
records = append(records, &domain.RecordChange{
|
||||||
Delete: &domain.RecordChangeDelete{
|
Delete: &domain.RecordChangeDelete{
|
||||||
Data: target,
|
Data: finalTargetName,
|
||||||
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
|
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
|
||||||
Type: domain.RecordType(ep.RecordType),
|
Type: domain.RecordType(ep.RecordType),
|
||||||
},
|
},
|
||||||
|
@ -93,7 +93,7 @@ func (m *mockScalewayDomain) ListDNSZoneRecords(req *domain.ListDNSZoneRecordsRe
|
|||||||
Type: domain.RecordTypeA,
|
Type: domain.RecordTypeA,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Data: "test.example.com",
|
Data: "test.example.com.",
|
||||||
Name: "two",
|
Name: "two",
|
||||||
TTL: 600,
|
TTL: 600,
|
||||||
Priority: 30,
|
Priority: 30,
|
||||||
@ -330,7 +330,7 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) {
|
|||||||
Add: &domain.RecordChangeAdd{
|
Add: &domain.RecordChangeAdd{
|
||||||
Records: []*domain.Record{
|
Records: []*domain.Record{
|
||||||
{
|
{
|
||||||
Data: "example.com",
|
Data: "example.com.",
|
||||||
Name: "",
|
Name: "",
|
||||||
TTL: 600,
|
TTL: 600,
|
||||||
Type: domain.RecordTypeCNAME,
|
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
|
// 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 {
|
func (p *TransIPProvider) endpointNameForRecord(r transip.DNSEntry, d transip.Domain) string {
|
||||||
// root name is identified by "@" and should be translated to domain name for
|
// root name is identified by "@" and should be translated to domain name for
|
||||||
// the endpoint entry.
|
// the endpoint entry.
|
||||||
|
@ -231,7 +231,7 @@ func TestUltraDNSProvider_ApplyChangesCNAME(t *testing.T) {
|
|||||||
assert.NotNil(t, err)
|
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) {
|
func TestUltraDNSProvider_ApplyChanges_Integration(t *testing.T) {
|
||||||
|
|
||||||
_, ok := os.LookupEnv("ULTRADNS_INTEGRATION")
|
_, 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) {
|
func TestUltraDNSProvider_ApplyChanges_MultipleTarget_integeration(t *testing.T) {
|
||||||
_, ok := os.LookupEnv("ULTRADNS_INTEGRATION")
|
_, ok := os.LookupEnv("ULTRADNS_INTEGRATION")
|
||||||
if !ok {
|
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{})
|
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")
|
assert.Equal(t, resp.Status, "200 OK")
|
||||||
|
|
||||||
//Coverting to RD Pool
|
//Converting to RD Pool
|
||||||
_ = os.Setenv("ULTRADNS_POOL_TYPE", "rdpool")
|
_ = os.Setenv("ULTRADNS_POOL_TYPE", "rdpool")
|
||||||
provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false)
|
provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false)
|
||||||
changes = &plan.Changes{}
|
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{})
|
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")
|
assert.Equal(t, resp.Status, "200 OK")
|
||||||
|
|
||||||
//Coverting back to SB Pool
|
//Converting back to SB Pool
|
||||||
_ = os.Setenv("ULTRADNS_POOL_TYPE", "sbpool")
|
_ = os.Setenv("ULTRADNS_POOL_TYPE", "sbpool")
|
||||||
provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false)
|
provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false)
|
||||||
changes = &plan.Changes{}
|
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 {
|
func (sdr *AWSSDRegistry) PropertyValuesEqual(name string, previous string, current string) bool {
|
||||||
return sdr.provider.PropertyValuesEqual(name, previous, current)
|
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 {
|
func (im *NoopRegistry) PropertyValuesEqual(attribute string, previous string, current string) bool {
|
||||||
return im.provider.PropertyValuesEqual(attribute, previous, current)
|
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)
|
Records(ctx context.Context) ([]*endpoint.Endpoint, error)
|
||||||
ApplyChanges(ctx context.Context, changes *plan.Changes) error
|
ApplyChanges(ctx context.Context, changes *plan.Changes) error
|
||||||
PropertyValuesEqual(attribute string, previous string, current string) bool
|
PropertyValuesEqual(attribute string, previous string, current string) bool
|
||||||
|
AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO(ideahitme): consider moving this to Plan
|
//TODO(ideahitme): consider moving this to Plan
|
||||||
|
@ -40,10 +40,15 @@ type TXTRegistry struct {
|
|||||||
recordsCache []*endpoint.Endpoint
|
recordsCache []*endpoint.Endpoint
|
||||||
recordsCacheRefreshTime time.Time
|
recordsCacheRefreshTime time.Time
|
||||||
cacheInterval time.Duration
|
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
|
// 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 == "" {
|
if ownerID == "" {
|
||||||
return nil, errors.New("owner id cannot be empty")
|
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")
|
return nil, errors.New("txt-prefix and txt-suffix are mutual exclusive")
|
||||||
}
|
}
|
||||||
|
|
||||||
mapper := newaffixNameMapper(txtPrefix, txtSuffix)
|
mapper := newaffixNameMapper(txtPrefix, txtSuffix, txtWildcardReplacement)
|
||||||
|
|
||||||
return &TXTRegistry{
|
return &TXTRegistry{
|
||||||
provider: provider,
|
provider: provider,
|
||||||
ownerID: ownerID,
|
ownerID: ownerID,
|
||||||
mapper: mapper,
|
mapper: mapper,
|
||||||
cacheInterval: cacheInterval,
|
cacheInterval: cacheInterval,
|
||||||
|
wildcardReplacement: txtWildcardReplacement,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +113,13 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
|
|||||||
if ep.Labels == nil {
|
if ep.Labels == nil {
|
||||||
ep.Labels = endpoint.NewLabels()
|
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 {
|
if labels, ok := labelMap[key]; ok {
|
||||||
for k, v := range labels {
|
for k, v := range labels {
|
||||||
ep.Labels[k] = v
|
ep.Labels[k] = v
|
||||||
@ -196,6 +208,11 @@ func (im *TXTRegistry) PropertyValuesEqual(name string, previous string, current
|
|||||||
return im.provider.PropertyValuesEqual(name, previous, 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
|
TXT registry specific private methods
|
||||||
*/
|
*/
|
||||||
@ -213,12 +230,13 @@ type nameMapper interface {
|
|||||||
type affixNameMapper struct {
|
type affixNameMapper struct {
|
||||||
prefix string
|
prefix string
|
||||||
suffix string
|
suffix string
|
||||||
|
wildcardReplacement string
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ nameMapper = affixNameMapper{}
|
var _ nameMapper = affixNameMapper{}
|
||||||
|
|
||||||
func newaffixNameMapper(prefix string, suffix string) affixNameMapper {
|
func newaffixNameMapper(prefix string, suffix string, wildcardReplacement string) affixNameMapper {
|
||||||
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix)}
|
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix), wildcardReplacement: strings.ToLower(wildcardReplacement)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pr affixNameMapper) toEndpointName(txtDNSName string) string {
|
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 {
|
func (pr affixNameMapper) toTXTName(endpointDNSName string) string {
|
||||||
DNSName := strings.SplitN(endpointDNSName, ".", 2)
|
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 {
|
if len(DNSName) < 2 {
|
||||||
return pr.prefix + DNSName[0] + pr.suffix
|
return pr.prefix + DNSName[0] + pr.suffix
|
||||||
}
|
}
|
||||||
|
@ -44,20 +44,20 @@ func TestTXTRegistry(t *testing.T) {
|
|||||||
|
|
||||||
func testTXTRegistryNew(t *testing.T) {
|
func testTXTRegistryNew(t *testing.T) {
|
||||||
p := inmemory.NewInMemoryProvider()
|
p := inmemory.NewInMemoryProvider()
|
||||||
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour)
|
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour)
|
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour)
|
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, p, r.provider)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour)
|
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
_, ok := r.mapper.(affixNameMapper)
|
_, ok := r.mapper.(affixNameMapper)
|
||||||
@ -65,7 +65,7 @@ func testTXTRegistryNew(t *testing.T) {
|
|||||||
assert.Equal(t, "owner", r.ownerID)
|
assert.Equal(t, "owner", r.ownerID)
|
||||||
assert.Equal(t, p, r.provider)
|
assert.Equal(t, p, r.provider)
|
||||||
|
|
||||||
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour)
|
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, ok = r.mapper.(affixNameMapper)
|
_, 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", "\"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", "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("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{
|
expectedRecords := []*endpoint.Endpoint{
|
||||||
@ -169,15 +171,23 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
|
|||||||
endpoint.OwnerLabelKey: "",
|
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)
|
records, _ := r.Records(ctx)
|
||||||
|
|
||||||
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
|
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
|
||||||
|
|
||||||
// Ensure prefix is case-insensitive
|
// Ensure prefix is case-insensitive
|
||||||
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour)
|
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "")
|
||||||
records, _ = r.Records(ctx)
|
records, _ = r.Records(ctx)
|
||||||
|
|
||||||
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
|
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)
|
records, _ := r.Records(ctx)
|
||||||
|
|
||||||
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
|
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
|
||||||
|
|
||||||
// Ensure prefix is case-insensitive
|
// Ensure prefix is case-insensitive
|
||||||
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour)
|
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "")
|
||||||
records, _ = r.Records(ctx)
|
records, _ = r.Records(ctx)
|
||||||
|
|
||||||
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
|
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)
|
records, _ := r.Records(ctx)
|
||||||
|
|
||||||
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
|
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"),
|
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour)
|
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "")
|
||||||
|
|
||||||
changes := &plan.Changes{
|
changes := &plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
@ -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"),
|
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{
|
changes := &plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
|
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("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("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{
|
Delete: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
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"),
|
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"),
|
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, ""),
|
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{
|
Delete: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
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, ""),
|
newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour)
|
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "")
|
||||||
|
|
||||||
changes := &plan.Changes{
|
changes := &plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
|
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
|
crdResource string
|
||||||
codec runtime.ParameterCodec
|
codec runtime.ParameterCodec
|
||||||
annotationFilter string
|
annotationFilter string
|
||||||
|
labelFilter string
|
||||||
}
|
}
|
||||||
|
|
||||||
func addKnownTypes(scheme *runtime.Scheme, groupVersion schema.GroupVersion) error {
|
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.
|
// 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{
|
return &crdSource{
|
||||||
crdResource: strings.ToLower(kind) + "s",
|
crdResource: strings.ToLower(kind) + "s",
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
annotationFilter: annotationFilter,
|
annotationFilter: annotationFilter,
|
||||||
|
labelFilter: labelFilter,
|
||||||
crdClient: crdClient,
|
crdClient: crdClient,
|
||||||
codec: runtime.NewParameterCodec(scheme),
|
codec: runtime.NewParameterCodec(scheme),
|
||||||
}, nil
|
}, nil
|
||||||
@ -119,12 +121,22 @@ func (cs *crdSource) AddEventHandler(ctx context.Context, handler func()) {
|
|||||||
func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||||
endpoints := []*endpoint.Endpoint{}
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err = cs.filterByAnnotations(result)
|
result, err = cs.filterByAnnotations(result)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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))))
|
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)
|
groupVersion, _ := schema.ParseGroupVersion(apiVersion)
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
addKnownTypes(scheme, groupVersion)
|
addKnownTypes(scheme, groupVersion)
|
||||||
@ -72,6 +72,7 @@ func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, ki
|
|||||||
Name: name,
|
Name: name,
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Annotations: annotations,
|
Annotations: annotations,
|
||||||
|
Labels: labels,
|
||||||
Generation: 1,
|
Generation: 1,
|
||||||
},
|
},
|
||||||
Spec: endpoint.DNSEndpointSpec{
|
Spec: endpoint.DNSEndpointSpec{
|
||||||
@ -139,7 +140,9 @@ func testCRDSourceEndpoints(t *testing.T) {
|
|||||||
expectEndpoints bool
|
expectEndpoints bool
|
||||||
expectError bool
|
expectError bool
|
||||||
annotationFilter string
|
annotationFilter string
|
||||||
|
labelFilter string
|
||||||
annotations map[string]string
|
annotations map[string]string
|
||||||
|
labels map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
title: "invalid crd api version",
|
title: "invalid crd api version",
|
||||||
@ -308,16 +311,76 @@ func testCRDSourceEndpoints(t *testing.T) {
|
|||||||
expectEndpoints: true,
|
expectEndpoints: true,
|
||||||
expectError: false,
|
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) {
|
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)
|
groupVersion, err := schema.ParseGroupVersion(ti.apiVersion)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
addKnownTypes(scheme, groupVersion)
|
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())
|
receivedEndpoints, err := cs.Endpoints(context.Background())
|
||||||
if ti.expectError {
|
if ti.expectError {
|
||||||
|
@ -128,13 +128,13 @@ func (sc *httpProxySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint,
|
|||||||
// Convert to []*projectcontour.HTTPProxy
|
// Convert to []*projectcontour.HTTPProxy
|
||||||
var httpProxies []*projectcontour.HTTPProxy
|
var httpProxies []*projectcontour.HTTPProxy
|
||||||
for _, hp := range hps {
|
for _, hp := range hps {
|
||||||
unstrucuredHP, ok := hp.(*unstructured.Unstructured)
|
unstructuredHP, ok := hp.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("could not convert")
|
return nil, errors.New("could not convert")
|
||||||
}
|
}
|
||||||
|
|
||||||
hpConverted := &projectcontour.HTTPProxy{}
|
hpConverted := &projectcontour.HTTPProxy{}
|
||||||
err := sc.unstructuredConverter.scheme.Convert(unstrucuredHP, hpConverted, nil)
|
err := sc.unstructuredConverter.scheme.Convert(unstructuredHP, hpConverted, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to convert to HTTPProxy")
|
return nil, errors.Wrap(err, "failed to convert to HTTPProxy")
|
||||||
}
|
}
|
||||||
|
@ -56,10 +56,11 @@ type ingressSource struct {
|
|||||||
combineFQDNAnnotation bool
|
combineFQDNAnnotation bool
|
||||||
ignoreHostnameAnnotation bool
|
ignoreHostnameAnnotation bool
|
||||||
ingressInformer extinformers.IngressInformer
|
ingressInformer extinformers.IngressInformer
|
||||||
|
ignoreIngressTLSSpec bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIngressSource creates a new ingressSource with the given config.
|
// 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 (
|
var (
|
||||||
tmpl *template.Template
|
tmpl *template.Template
|
||||||
err error
|
err error
|
||||||
@ -105,6 +106,7 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt
|
|||||||
combineFQDNAnnotation: combineFqdnAnnotation,
|
combineFQDNAnnotation: combineFqdnAnnotation,
|
||||||
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
|
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
|
||||||
ingressInformer: ingressInformer,
|
ingressInformer: ingressInformer,
|
||||||
|
ignoreIngressTLSSpec: ignoreIngressTLSSpec,
|
||||||
}
|
}
|
||||||
return sc, nil
|
return sc, nil
|
||||||
}
|
}
|
||||||
@ -132,7 +134,7 @@ func (sc *ingressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ingEndpoints := endpointsFromIngress(ing, sc.ignoreHostnameAnnotation)
|
ingEndpoints := endpointsFromIngress(ing, sc.ignoreHostnameAnnotation, sc.ignoreIngressTLSSpec)
|
||||||
|
|
||||||
// apply template if host is missing on ingress
|
// apply template if host is missing on ingress
|
||||||
if (sc.combineFQDNAnnotation || len(ingEndpoints) == 0) && sc.fqdnTemplate != nil {
|
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
|
// 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
|
var endpoints []*endpoint.Endpoint
|
||||||
|
|
||||||
ttl, err := getTTLFromAnnotations(ing.Annotations)
|
ttl, err := getTTLFromAnnotations(ing.Annotations)
|
||||||
@ -263,6 +265,8 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [
|
|||||||
endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...)
|
endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip endpoints if we do not want entries from tls spec section
|
||||||
|
if !ignoreIngressTLSSpec {
|
||||||
for _, tls := range ing.Spec.TLS {
|
for _, tls := range ing.Spec.TLS {
|
||||||
for _, host := range tls.Hosts {
|
for _, host := range tls.Hosts {
|
||||||
if host == "" {
|
if host == "" {
|
||||||
@ -271,6 +275,7 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [
|
|||||||
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...)
|
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Skip endpoints if we do not want entries from annotations
|
// Skip endpoints if we do not want entries from annotations
|
||||||
if !ignoreHostnameAnnotation {
|
if !ignoreHostnameAnnotation {
|
||||||
|
@ -52,6 +52,7 @@ func (suite *IngressSuite) SetupTest() {
|
|||||||
"{{.Name}}",
|
"{{.Name}}",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
suite.NoError(err, "should initialize ingress source")
|
suite.NoError(err, "should initialize ingress source")
|
||||||
|
|
||||||
@ -134,6 +135,7 @@ func TestNewIngressSource(t *testing.T) {
|
|||||||
ti.fqdnTemplate,
|
ti.fqdnTemplate,
|
||||||
ti.combineFQDNAndAnnotation,
|
ti.combineFQDNAndAnnotation,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
if ti.expectError {
|
if ti.expectError {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@ -221,7 +223,7 @@ func testEndpointsFromIngress(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(ti.title, func(t *testing.T) {
|
t.Run(ti.title, func(t *testing.T) {
|
||||||
realIngress := ti.ingress.Ingress()
|
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
|
fqdnTemplate string
|
||||||
combineFQDNAndAnnotation bool
|
combineFQDNAndAnnotation bool
|
||||||
ignoreHostnameAnnotation bool
|
ignoreHostnameAnnotation bool
|
||||||
|
ignoreIngressTLSSpec bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
title: "no ingress",
|
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) {
|
t.Run(ti.title, func(t *testing.T) {
|
||||||
ingresses := make([]*v1beta1.Ingress, 0)
|
ingresses := make([]*v1beta1.Ingress, 0)
|
||||||
@ -1008,6 +1044,7 @@ func testIngressEndpoints(t *testing.T) {
|
|||||||
ti.fqdnTemplate,
|
ti.fqdnTemplate,
|
||||||
ti.combineFQDNAndAnnotation,
|
ti.combineFQDNAndAnnotation,
|
||||||
ti.ignoreHostnameAnnotation,
|
ti.ignoreHostnameAnnotation,
|
||||||
|
ti.ignoreIngressTLSSpec,
|
||||||
)
|
)
|
||||||
for _, ingress := range ingresses {
|
for _, ingress := range ingresses {
|
||||||
_, err := fakeClient.ExtensionsV1beta1().Ingresses(ingress.Namespace).Create(context.Background(), ingress, metav1.CreateOptions{})
|
_, 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()
|
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" {
|
if u.Scheme == "https" && u.Port() == "443" {
|
||||||
apiServer = "https://" + u.Hostname()
|
apiServer = "https://" + u.Hostname()
|
||||||
}
|
}
|
||||||
|
@ -214,6 +214,35 @@ func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
|
|||||||
endpoints = append(endpoints, svcEndpoints...)
|
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 {
|
for _, ep := range endpoints {
|
||||||
sort.Sort(ep.Targets)
|
sort.Sort(ep.Targets)
|
||||||
}
|
}
|
||||||
@ -333,7 +362,7 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.End
|
|||||||
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
|
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
|
||||||
hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",")
|
hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",")
|
||||||
for _, hostname := range hostnameList {
|
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
|
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
|
// Skip endpoints if we do not want entries from annotations
|
||||||
if !sc.ignoreHostnameAnnotation {
|
if !sc.ignoreHostnameAnnotation {
|
||||||
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
|
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
|
||||||
hostnameList := getHostnamesFromAnnotations(svc.Annotations)
|
var hostnameList []string
|
||||||
|
var internalHostnameList []string
|
||||||
|
|
||||||
|
hostnameList = getHostnamesFromAnnotations(svc.Annotations)
|
||||||
for _, hostname := range hostnameList {
|
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
|
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, ".")
|
hostname = strings.TrimSuffix(hostname, ".")
|
||||||
ttl, err := getTTLFromAnnotations(svc.Annotations)
|
ttl, err := getTTLFromAnnotations(svc.Annotations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -431,7 +468,11 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
|
|||||||
|
|
||||||
switch svc.Spec.Type {
|
switch svc.Spec.Type {
|
||||||
case v1.ServiceTypeLoadBalancer:
|
case v1.ServiceTypeLoadBalancer:
|
||||||
|
if useClusterIP {
|
||||||
|
targets = append(targets, extractServiceIps(svc)...)
|
||||||
|
} else {
|
||||||
targets = append(targets, extractLoadBalancerTargets(svc)...)
|
targets = append(targets, extractLoadBalancerTargets(svc)...)
|
||||||
|
}
|
||||||
case v1.ServiceTypeClusterIP:
|
case v1.ServiceTypeClusterIP:
|
||||||
if sc.publishInternal {
|
if sc.publishInternal {
|
||||||
targets = append(targets, extractServiceIps(svc)...)
|
targets = append(targets, extractServiceIps(svc)...)
|
||||||
@ -486,7 +527,10 @@ func extractServiceExternalName(svc *v1.Service) endpoint.Targets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractLoadBalancerTargets(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.
|
// Create a corresponding endpoint for each configured external entrypoint.
|
||||||
for _, lb := range svc.Status.LoadBalancer.Ingress {
|
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
|
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 {
|
if len(externalIPs) > 0 {
|
||||||
return externalIPs, nil
|
return externalIPs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return internalIPs, nil
|
return internalIPs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -568,14 +628,17 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets e
|
|||||||
|
|
||||||
for _, port := range svc.Spec.Ports {
|
for _, port := range svc.Spec.Ports {
|
||||||
if port.NodePort > 0 {
|
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
|
// 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)
|
target := fmt.Sprintf("0 50 %d %s", port.NodePort, hostname)
|
||||||
|
|
||||||
// figure out the portname
|
// take the service name from the K8s Service object
|
||||||
portName := port.Name
|
// it is safe to use since it is DNS compatible
|
||||||
if portName == "" {
|
// see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
|
||||||
portName = fmt.Sprintf("%d", port.NodePort)
|
serviceName := svc.ObjectMeta.Name
|
||||||
}
|
|
||||||
|
|
||||||
// figure out the protocol
|
// figure out the protocol
|
||||||
protocol := strings.ToLower(string(port.Protocol))
|
protocol := strings.ToLower(string(port.Protocol))
|
||||||
@ -583,7 +646,7 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets e
|
|||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
}
|
}
|
||||||
|
|
||||||
recordName := fmt.Sprintf("_%s._%s.%s", portName, protocol, hostname)
|
recordName := fmt.Sprintf("_%s._%s.%s", serviceName, protocol, hostname)
|
||||||
|
|
||||||
var ep *endpoint.Endpoint
|
var ep *endpoint.Endpoint
|
||||||
if ttl.IsConfigured() {
|
if ttl.IsConfigured() {
|
||||||
|
@ -19,6 +19,8 @@ package source
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -93,6 +95,7 @@ func TestServiceSource(t *testing.T) {
|
|||||||
t.Run("Interface", testServiceSourceImplementsSource)
|
t.Run("Interface", testServiceSourceImplementsSource)
|
||||||
t.Run("NewServiceSource", testServiceSourceNewServiceSource)
|
t.Run("NewServiceSource", testServiceSourceNewServiceSource)
|
||||||
t.Run("Endpoints", testServiceSourceEndpoints)
|
t.Run("Endpoints", testServiceSourceEndpoints)
|
||||||
|
t.Run("MultipleServices", testMultipleServicesEndpoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testServiceSourceImplementsSource tests that serviceSource is a valid Source.
|
// testServiceSourceImplementsSource tests that serviceSource is a valid Source.
|
||||||
@ -174,6 +177,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
labels map[string]string
|
labels map[string]string
|
||||||
annotations map[string]string
|
annotations map[string]string
|
||||||
clusterIP string
|
clusterIP string
|
||||||
|
externalIPs []string
|
||||||
lbs []string
|
lbs []string
|
||||||
serviceTypesFilter []string
|
serviceTypesFilter []string
|
||||||
expected []*endpoint.Endpoint
|
expected []*endpoint.Endpoint
|
||||||
@ -193,13 +197,14 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
map[string]string{},
|
map[string]string{},
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"no annotated services return no endpoints when ignoreing annotations",
|
"no annotated services return no endpoints when ignoring annotations",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"testing",
|
"testing",
|
||||||
@ -212,6 +217,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
map[string]string{},
|
map[string]string{},
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
@ -233,6 +239,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -256,6 +263,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
@ -279,6 +287,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"1.2.3.4",
|
"1.2.3.4",
|
||||||
[]string{},
|
[]string{},
|
||||||
[]string{},
|
[]string{},
|
||||||
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
@ -296,6 +305,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
map[string]string{},
|
map[string]string{},
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -305,7 +315,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
false,
|
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",
|
"testing",
|
||||||
@ -318,6 +328,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
map[string]string{},
|
map[string]string{},
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -342,6 +353,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
|
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -368,6 +380,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
|
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -392,6 +405,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
|
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -416,6 +430,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org, bar.example.org",
|
hostnameAnnotationKey: "foo.example.org, bar.example.org",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -440,6 +455,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"lb.example.com"}, // Kubernetes omits the trailing dot
|
[]string{"lb.example.com"}, // Kubernetes omits the trailing dot
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -463,6 +479,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted
|
hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot
|
[]string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -488,6 +505,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -512,6 +530,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
@ -533,6 +552,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -556,6 +576,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
@ -577,6 +598,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -601,6 +623,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
|
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -625,6 +648,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"service.beta.kubernetes.io/external-traffic": "SomethingElse",
|
"service.beta.kubernetes.io/external-traffic": "SomethingElse",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
@ -647,6 +671,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
|
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
@ -669,6 +694,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"service.beta.kubernetes.io/external-traffic": "Global",
|
"service.beta.kubernetes.io/external-traffic": "Global",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -693,6 +719,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
|
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
@ -716,9 +743,34 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"",
|
"",
|
||||||
[]string{},
|
[]string{},
|
||||||
[]string{},
|
[]string{},
|
||||||
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
false,
|
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",
|
"multiple external entrypoints return a single endpoint with multiple targets",
|
||||||
"",
|
"",
|
||||||
@ -735,6 +787,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4", "8.8.8.8"},
|
[]string{"1.2.3.4", "8.8.8.8"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -758,6 +811,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"zalando.org/dnsname": "foo.example.org.",
|
"zalando.org/dnsname": "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
@ -779,6 +833,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"zalando.org/dnsname": "foo.example.org.",
|
"zalando.org/dnsname": "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -804,6 +859,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"domainName": "foo.example.org., bar.example.org",
|
"domainName": "foo.example.org., bar.example.org",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -826,6 +882,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
map[string]string{},
|
map[string]string{},
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4", "elb.com"},
|
[]string{"1.2.3.4", "elb.com"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -850,6 +907,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4", "elb.com"},
|
[]string{"1.2.3.4", "elb.com"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -874,6 +932,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
"zalando.org/dnsname": "mate.example.org.",
|
"zalando.org/dnsname": "mate.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -895,6 +954,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
map[string]string{},
|
map[string]string{},
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
@ -916,6 +976,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -940,6 +1001,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
ttlAnnotationKey: "foo",
|
ttlAnnotationKey: "foo",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -964,6 +1026,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
ttlAnnotationKey: "10",
|
ttlAnnotationKey: "10",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -988,6 +1051,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
ttlAnnotationKey: "1m",
|
ttlAnnotationKey: "1m",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -1012,6 +1076,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
ttlAnnotationKey: "-10",
|
ttlAnnotationKey: "-10",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{},
|
[]string{},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -1035,6 +1100,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{string(v1.ServiceTypeLoadBalancer)},
|
[]string{string(v1.ServiceTypeLoadBalancer)},
|
||||||
[]*endpoint.Endpoint{
|
[]*endpoint.Endpoint{
|
||||||
@ -1058,11 +1124,62 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
hostnameAnnotationKey: "foo.example.org.",
|
hostnameAnnotationKey: "foo.example.org.",
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
|
[]string{},
|
||||||
[]string{"1.2.3.4"},
|
[]string{"1.2.3.4"},
|
||||||
[]string{string(v1.ServiceTypeLoadBalancer)},
|
[]string{string(v1.ServiceTypeLoadBalancer)},
|
||||||
[]*endpoint.Endpoint{},
|
[]*endpoint.Endpoint{},
|
||||||
false,
|
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) {
|
t.Run(tc.title, func(t *testing.T) {
|
||||||
// Create a Kubernetes testing client
|
// Create a Kubernetes testing client
|
||||||
@ -1082,6 +1199,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
|||||||
Spec: v1.ServiceSpec{
|
Spec: v1.ServiceSpec{
|
||||||
Type: tc.svcType,
|
Type: tc.svcType,
|
||||||
ClusterIP: tc.clusterIP,
|
ClusterIP: tc.clusterIP,
|
||||||
|
ExternalIPs: tc.externalIPs,
|
||||||
},
|
},
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Namespace: tc.svcNamespace,
|
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.
|
// testServiceSourceEndpoints tests that various services generate the correct endpoints.
|
||||||
func TestClusterIpServices(t *testing.T) {
|
func TestClusterIpServices(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@ -1339,7 +1642,7 @@ func TestNodePortServices(t *testing.T) {
|
|||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
[]*endpoint.Endpoint{
|
[]*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},
|
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -1426,7 +1729,7 @@ func TestNodePortServices(t *testing.T) {
|
|||||||
map[string]string{},
|
map[string]string{},
|
||||||
nil,
|
nil,
|
||||||
[]*endpoint.Endpoint{
|
[]*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},
|
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -1472,7 +1775,7 @@ func TestNodePortServices(t *testing.T) {
|
|||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
[]*endpoint.Endpoint{
|
[]*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},
|
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -1516,7 +1819,7 @@ func TestNodePortServices(t *testing.T) {
|
|||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
[]*endpoint.Endpoint{
|
[]*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},
|
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -1562,7 +1865,7 @@ func TestNodePortServices(t *testing.T) {
|
|||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
[]*endpoint.Endpoint{
|
[]*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},
|
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -1591,6 +1894,100 @@ func TestNodePortServices(t *testing.T) {
|
|||||||
[]int{1, 1},
|
[]int{1, 1},
|
||||||
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
|
[]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) {
|
t.Run(tc.title, func(t *testing.T) {
|
||||||
// Create a Kubernetes testing client
|
// Create a Kubernetes testing client
|
||||||
|
@ -38,6 +38,8 @@ const (
|
|||||||
controllerAnnotationKey = "external-dns.alpha.kubernetes.io/controller"
|
controllerAnnotationKey = "external-dns.alpha.kubernetes.io/controller"
|
||||||
// The annotation used for defining the desired hostname
|
// The annotation used for defining the desired hostname
|
||||||
hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/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
|
// The annotation used for defining the desired ingress target
|
||||||
targetAnnotationKey = "external-dns.alpha.kubernetes.io/target"
|
targetAnnotationKey = "external-dns.alpha.kubernetes.io/target"
|
||||||
// The annotation used for defining the desired DNS record TTL
|
// The annotation used for defining the desired DNS record TTL
|
||||||
@ -46,6 +48,8 @@ const (
|
|||||||
aliasAnnotationKey = "external-dns.alpha.kubernetes.io/alias"
|
aliasAnnotationKey = "external-dns.alpha.kubernetes.io/alias"
|
||||||
// The value of the controller annotation so that we feel responsible
|
// The value of the controller annotation so that we feel responsible
|
||||||
controllerAnnotationValue = "dns-controller"
|
controllerAnnotationValue = "dns-controller"
|
||||||
|
// The annotation used for defining the desired hostname
|
||||||
|
internalHostnameAnnotationKey = "external-dns.alpha.kubernetes.io/internal-hostname"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider-specific annotations
|
// Provider-specific annotations
|
||||||
@ -107,6 +111,18 @@ func getHostnamesFromAnnotations(annotations map[string]string) []string {
|
|||||||
return strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")
|
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 {
|
func getAliasFromAnnotations(annotations map[string]string) bool {
|
||||||
aliasAnnotation, exists := annotations[aliasAnnotationKey]
|
aliasAnnotation, exists := annotations[aliasAnnotationKey]
|
||||||
return exists && aliasAnnotation == "true"
|
return exists && aliasAnnotation == "true"
|
||||||
|
@ -75,7 +75,7 @@ func TestGetTTLFromAnnotations(t *testing.T) {
|
|||||||
expectedErr: nil,
|
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"},
|
annotations: map[string]string{ttlAnnotationKey: "20.5s"},
|
||||||
expectedTTL: endpoint.TTL(20),
|
expectedTTL: endpoint.TTL(20),
|
||||||
expectedErr: nil,
|
expectedErr: nil,
|
||||||
|
@ -42,9 +42,11 @@ var ErrSourceNotFound = errors.New("source not found")
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Namespace string
|
Namespace string
|
||||||
AnnotationFilter string
|
AnnotationFilter string
|
||||||
|
LabelFilter string
|
||||||
FQDNTemplate string
|
FQDNTemplate string
|
||||||
CombineFQDNAndAnnotation bool
|
CombineFQDNAndAnnotation bool
|
||||||
IgnoreHostnameAnnotation bool
|
IgnoreHostnameAnnotation bool
|
||||||
|
IgnoreIngressTLSSpec bool
|
||||||
Compatibility string
|
Compatibility string
|
||||||
PublishInternal bool
|
PublishInternal bool
|
||||||
PublishHostIP bool
|
PublishHostIP bool
|
||||||
@ -81,12 +83,12 @@ type SingletonClientGenerator struct {
|
|||||||
kubeClient kubernetes.Interface
|
kubeClient kubernetes.Interface
|
||||||
istioClient *istioclient.Clientset
|
istioClient *istioclient.Clientset
|
||||||
cfClient *cfclient.Client
|
cfClient *cfclient.Client
|
||||||
contourClient dynamic.Interface
|
dynKubeClient dynamic.Interface
|
||||||
openshiftClient openshift.Interface
|
openshiftClient openshift.Interface
|
||||||
kubeOnce sync.Once
|
kubeOnce sync.Once
|
||||||
istioOnce sync.Once
|
istioOnce sync.Once
|
||||||
cfOnce sync.Once
|
cfOnce sync.Once
|
||||||
contourOnce sync.Once
|
dynCliOnce sync.Once
|
||||||
openshiftOnce sync.Once
|
openshiftOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,13 +134,13 @@ func NewCFClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*c
|
|||||||
return client, nil
|
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) {
|
func (p *SingletonClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) {
|
||||||
var err error
|
var err error
|
||||||
p.contourOnce.Do(func() {
|
p.dynCliOnce.Do(func() {
|
||||||
p.contourClient, err = NewDynamicKubernetesClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
|
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
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
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":
|
case "istio-gateway":
|
||||||
kubernetesClient, err := p.KubeClient()
|
kubernetesClient, err := p.KubeClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -211,6 +213,16 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewCloudFoundrySource(cfClient)
|
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":
|
case "contour-ingressroute":
|
||||||
kubernetesClient, err := p.KubeClient()
|
kubernetesClient, err := p.KubeClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -246,7 +258,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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":
|
case "skipper-routegroup":
|
||||||
apiServerURL := cfg.APIServerURL
|
apiServerURL := cfg.APIServerURL
|
||||||
tokenPath := ""
|
tokenPath := ""
|
||||||
|
Loading…
Reference in New Issue
Block a user