diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21b20e7f5..f3d36d5d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Set up Go 1.x - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: 1.19 id: go diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4682abf01..112048477 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,13 +18,13 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: "3.10" cache: "pip" cache-dependency-path: "./docs/scripts/requirements.txt" - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: ^1.19 diff --git a/.github/workflows/json-yaml-validate.yml b/.github/workflows/json-yaml-validate.yml new file mode 100644 index 000000000..1eb833f63 --- /dev/null +++ b/.github/workflows/json-yaml-validate.yml @@ -0,0 +1,23 @@ +name: json-yaml-validate +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write # enable write permissions for pull requests + +jobs: + json-yaml-validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: json-yaml-validate + uses: GrantBirki/json-yaml-validate@v1.2.0 + with: + comment: "true" # enable comment mode + yaml_exclude_regex: "(charts/external-dns/templates.*|mkdocs.yml)" diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b4dd91739..41b9880fa 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -21,7 +21,7 @@ jobs: steps: - name: Set up Go 1.x - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: 1.19 id: go diff --git a/.github/workflows/staging-image-tester.yml b/.github/workflows/staging-image-tester.yml new file mode 100644 index 000000000..30c5eda9f --- /dev/null +++ b/.github/workflows/staging-image-tester.yml @@ -0,0 +1,37 @@ +name: Build all images + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + + build: + permissions: + contents: read # to fetch code (actions/checkout) + checks: write # to create a new check based on the results (shogo82148/actions-goveralls) + + name: Build + runs-on: ubuntu-latest + steps: + + - name: Set up Go 1.x + uses: actions/setup-go@v4 + with: + go-version: 1.19 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Install CI + run: | + go get -v -t -d ./... + + - name: Test + run: make build.image/multiarch diff --git a/Dockerfile b/Dockerfile index 12af3f2ab..db7d8dcca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,12 +24,10 @@ COPY go.sum . RUN go mod download COPY . . -RUN make test build.$ARCH -# final image -FROM $ARCH/alpine:3.17 +FROM alpine:3.18 -RUN apk update && apk add "libcrypto3>=3.0.8-r0" "libssl3>=3.0.8-r0" && rm -rf /var/cache/apt/* +RUN apk update && apk add "libcrypto3>=3.0.8-r4" "libssl3>=3.0.8-r4" && rm -rf /var/cache/apt/* 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 diff --git a/Makefile b/Makefile index 1e00099d8..500fea06a 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ cover: go get github.com/wadey/gocovmerge $(eval PKGS := $(shell go list ./... | grep -v /vendor/)) - $(eval PKGS_DELIM := $(shell echo $(PKGS) | sed -e 's/ /,/g')) + $(eval PKGS_DELIM := $(shell echo $(PKGS) | tr / -')) go list -f '{{if or (len .TestGoFiles) (len .XTestGoFiles)}}go test -test.v -test.timeout=120s -covermode=count -coverprofile={{.Name}}_{{len .Imports}}_{{len .Deps}}.coverprofile -coverpkg $(PKGS_DELIM) {{.ImportPath}}{{end}}' $(PKGS) | xargs -0 sh -c gocovmerge `ls *.coverprofile` > cover.out rm *.coverprofile @@ -90,8 +90,11 @@ IMAGE ?= us.gcr.io/k8s-artifacts-prod/external-dns/$(BINARY) VERSION ?= $(shell git describe --tags --always --dirty) BUILD_FLAGS ?= -v LDFLAGS ?= -X sigs.k8s.io/external-dns/pkg/apis/externaldns.Version=$(VERSION) -w -s -ARCHS = amd64 arm64v8 arm32v7 -SHELL = /bin/bash +ARCHS = amd64 arm64 arm/v7 +ARCH ?= amd64 +DEFAULT_ARCH = amd64 +SHELL = /bin/bash +OUTPUT_TYPE ?= docker build: build/$(BINARY) @@ -99,37 +102,65 @@ build: build/$(BINARY) build/$(BINARY): $(SOURCES) CGO_ENABLED=0 go build -o build/$(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" . -build.push/multiarch: +build.push/multiarch: $(addprefix build.push-,$(ARCHS)) 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.17 ;\ - docker pull golang:1.19 ;\ - DOCKER_BUILDKIT=1 docker build --rm --tag $${image} --build-arg VERSION="$(VERSION)" --build-arg ARCH="$${arch}" . ;\ - docker push $${image} ;\ - arch_specific_tags+=( "--amend $${image}" ) ;\ + image="$(IMAGE):$(VERSION)-$$(echo $$arch | tr / -)" ;\ + arch_specific_tags+=( " $${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)" \ + echo $${arch_specific_tags[@]} ;\ + DOCKER_CLI_EXPERIMENTAL=enabled docker buildx imagetools create --tag "$(IMAGE):$(VERSION)" $${arch_specific_tags[@]} ;\ -build.push: build.docker - docker push "$(IMAGE):$(VERSION)" +build.image/multiarch: $(addprefix build.image-,$(ARCHS)) -build.arm64v8: +build.image: + $(MAKE) ARCH=$(ARCH) OUTPUT_TYPE=docker build.docker + +build.image-amd64: + $(MAKE) ARCH=amd64 build.image + +build.image-arm64: + $(MAKE) ARCH=arm64 build.image + +build.image-arm/v7: + $(MAKE) ARCH=arm/v7 build.image + +build.push: + $(MAKE) ARCH=$(ARCH) OUTPUT_TYPE=registry build.docker + +build.push-amd64: + $(MAKE) ARCH=amd64 build.push + +build.push-arm64: + $(MAKE) ARCH=arm64 build.push + +build.push-arm/v7: + $(MAKE) ARCH=arm/v7 build.push + +build.arm64: 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: +build.arm/v7: CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o build/$(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" . -build.docker: - docker build --rm --tag "$(IMAGE):$(VERSION)" --build-arg VERSION="$(VERSION)" --build-arg ARCH="amd64" . +build.setup: + docker buildx inspect img-builder > /dev/null || docker buildx create --name img-builder --use + +build.docker: build.setup build.$(ARCH) + docker build --rm --tag "$(IMAGE):$(VERSION)" --build-arg VERSION="$(VERSION)" --build-arg ARCH="$(ARCH)" . + image="$(IMAGE):$(VERSION)-$(subst /,-,$(ARCH))"; \ + docker buildx build \ + --pull \ + --provenance=false \ + --sbom=false \ + --output=type=$(OUTPUT_TYPE) \ + --platform linux/$(ARCH) \ + --build-arg ARCH="$(ARCH)" \ + --build-arg VERSION="$(VERSION)" \ + --tag $${image} . build.mini: docker build --rm --tag "$(IMAGE):$(VERSION)-mini" --build-arg VERSION="$(VERSION)" -f Dockerfile.mini . @@ -140,8 +171,8 @@ clean: # Builds and push container images to the staging bucket. .PHONY: release.staging -release.staging: +release.staging: test IMAGE=$(IMAGE_STAGING) $(MAKE) build.push/multiarch -release.prod: +release.prod: test $(MAKE) build.push/multiarch diff --git a/README.md b/README.md index a5041afbe..88957a849 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ hide: ---

- ExternalDNS + ExternalDNS

# ExternalDNS @@ -176,6 +176,7 @@ The following tutorials are provided: * [Nginx Ingress Controller](docs/tutorials/nginx-ingress.md) * [NS1](docs/tutorials/ns1.md) * [NS Record Creation with CRD Source](docs/tutorials/ns-record.md) +* [MX Record Creation with CRD Source](docs/tutorials/mx-record.md) * [OpenStack Designate](docs/tutorials/designate.md) * [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md) * [PowerDNS](docs/tutorials/pdns.md) diff --git a/charts/external-dns/CHANGELOG.md b/charts/external-dns/CHANGELOG.md index 8e30bd0e2..5c2d88be3 100644 --- a/charts/external-dns/CHANGELOG.md +++ b/charts/external-dns/CHANGELOG.md @@ -7,15 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- - + +## [v1.12.2] - UNRELEASED + +### All Changes + +- Added support for ServiceMonitor relabelling. ([#3366](https://github.com/kubernetes-sigs/external-dns/pull/3366)) [@jkroepke](https://github.com/jkroepke) +- Updated chart icon path. ([#3492](https://github.com/kubernetes-sigs/external-dns/pull/3494)) [kundan2707](https://github.com/kundan2707) +- Added RBAC for Gateway-API resources to ClusterRole. ([#3499](https://github.com/kubernetes-sigs/external-dns/pull/3499)) [@michaelvl](https://github.com/MichaelVL) +- Added RBAC for F5 VirtualServer to ClusterRole. ([#3503](https://github.com/kubernetes-sigs/external-dns/pull/3503)) [@mikejoh](https://github.com/mikejoh) +- Added support for running ExternalDNS with namespaced scope. ([#3403](https://github.com/kubernetes-sigs/external-dns/pull/3403)) [@jkroepke](https://github.com/jkroepke) +- Updated _ExternalDNS_ version to [v0.13.4](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.13.4). ([#3516](https://github.com/kubernetes-sigs/external-dns/pull/3516)) [@stevehipwell](https://github.com/stevehipwell) ## [v1.12.1] - 2023-02-06 diff --git a/charts/external-dns/Chart.yaml b/charts/external-dns/Chart.yaml index 9dbbda380..0e4e2cd9b 100644 --- a/charts/external-dns/Chart.yaml +++ b/charts/external-dns/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: external-dns description: ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers. type: application -version: 1.12.1 -appVersion: 0.13.2 +version: 1.12.2 +appVersion: 0.13.4 keywords: - kubernetes - externaldns @@ -12,7 +12,7 @@ keywords: - service - ingress home: https://github.com/kubernetes-sigs/external-dns/ -icon: https://github.com/kubernetes-sigs/external-dns/raw/master/img/external-dns.png +icon: https://github.com/kubernetes-sigs/external-dns/raw/master/docs/img/external-dns.png sources: - https://github.com/kubernetes-sigs/external-dns/ maintainers: @@ -20,9 +20,15 @@ maintainers: email: steve.hipwell@gmail.com annotations: artifacthub.io/changes: | - - kind: changed - description: "Updated ExternalDNS version to v0.13.2." - kind: added - description: "Added secretConfiguration.subPath to mount specific files from secret as a sub-path." + description: "Added support for ServiceMonitor relabelling." - kind: changed - description: "Changed to use registry.k8s.io instead of k8s.gcr.io." + description: "Updated chart icon path." + - kind: added + description: "Added RBAC for Gateway-API resources to ClusterRole." + - kind: added + description: "Added RBAC for F5 VirtualServer to ClusterRole." + - kind: added + description: "Added support for running ExternalDNS with namespaced scope." + - kind: changed + description: "Updated _ExternalDNS_ version to [v0.13.4](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.13.4)." diff --git a/charts/external-dns/README.md b/charts/external-dns/README.md index 67ab1f5df..e3433227b 100644 --- a/charts/external-dns/README.md +++ b/charts/external-dns/README.md @@ -69,6 +69,7 @@ The following table lists the configurable parameters of the _ExternalDNS_ chart | `logFormat` | Formats of the logs, available values are: `text`, `json`. | `text` | | `interval` | The interval for DNS updates. | `1m` | | `triggerLoopOnEvent` | When enabled, triggers run loop on create/update/delete events in addition of regular interval. | `false` | +| `namespaced` | When enabled, external-dns runs on namespace scope. Additionally, Role and Rolebinding will be namespaced, too. | `false` | | `sources` | K8s resources type to be observed for new DNS entries. | See _values.yaml_ | | `policy` | How DNS records are synchronized between sources and providers, available values are: `sync`, `upsert-only`. | `upsert-only` | | `registry` | Registry Type, available types are: `txt`, `noop`. | `txt` | @@ -82,3 +83,36 @@ The following table lists the configurable parameters of the _ExternalDNS_ chart | `secretConfiguration.mountPath` | Mount path of secret configuration secret (this can be templated). | `""` | | `secretConfiguration.data` | Secret configuration secret data. Could be used to store DNS provider credentials. | `{}` | | `secretConfiguration.subPath` | Sub-path of secret configuration secret (this can be templated). | `""` | + +## Namespaced scoped installation + +external-dns supports running on a namespaced only scope, too. +If `namespaced=true` is defined, the helm chart will setup `Roles` and `RoleBindings` instead `ClusterRoles` and `ClusterRoleBindings`. + +### Limited supported +Not all sources are supported in namespaced scope, since some sources depends on cluster-wide resources. +For example: Source `node` isn't supported, since `kind: Node` has scope `Cluster`. +Sources like `istio-virtualservice` only work, if all resources like `Gateway` and `VirtualService` are present in the same +namespaces as `external-dns`. + +The annotation `external-dns.alpha.kubernetes.io/endpoints-type: NodeExternalIP` is not supported. + +If `namespaced` is set to `true`, please ensure that `sources` my only contains supported sources (Default: `service,ingress`. + +### Support matrix + +| Source | Supported | Infos | +|------------------------|-----------|------------------------| +| `ingress` | ✅ | | +| `istio-gateway` | ✅ | | +| `istio-virtualservice` | ✅ | | +| `contour-ingressroute` | ✅ | | +| `crd` | ✅ | | +| `kong-tcpingress` | ✅ | | +| `openshift-route` | ✅ | | +| `skipper-routegroup` | ✅ | | +| `gloo-proxy` | ✅ | | +| `contour-httpproxy` | ✅ | | +| `service` | ⚠️️ | NodePort not supported | +| `node` | ❌ | | +| `pod` | ❌ | | diff --git a/charts/external-dns/templates/clusterrole.yaml b/charts/external-dns/templates/clusterrole.yaml index 8fcc15dce..4f33c1146 100644 --- a/charts/external-dns/templates/clusterrole.yaml +++ b/charts/external-dns/templates/clusterrole.yaml @@ -1,12 +1,12 @@ {{- if .Values.rbac.create -}} apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: {{ .Values.namespaced | ternary "Role" "ClusterRole" }} metadata: name: {{ template "external-dns.fullname" . }} labels: {{- include "external-dns.labels" . | nindent 4 }} rules: -{{- if or (has "node" .Values.sources) (has "pod" .Values.sources) (has "service" .Values.sources) (has "contour-httpproxy" .Values.sources) (has "gloo-proxy" .Values.sources) (has "openshift-route" .Values.sources) (has "skipper-routegroup" .Values.sources) }} +{{- if and (not .Values.namespaced) (or (has "node" .Values.sources) (has "pod" .Values.sources) (has "service" .Values.sources) (has "contour-httpproxy" .Values.sources) (has "gloo-proxy" .Values.sources) (has "openshift-route" .Values.sources) (has "skipper-routegroup" .Values.sources)) }} - apiGroups: [""] resources: ["nodes"] verbs: ["list","watch"] @@ -60,6 +60,41 @@ rules: resources: ["dnsendpoints/status"] verbs: ["*"] {{- end }} +{{- if or (has "gateway-httproute" .Values.sources) (has "gateway-grpcroute" .Values.sources) (has "gateway-tlsroute" .Values.sources) (has "gateway-tcproute" .Values.sources) (has "gateway-udproute" .Values.sources) }} + - apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] +{{- end }} +{{- if has "gateway-httproute" .Values.sources }} + - apiGroups: ["gateway.networking.k8s.io"] + resources: ["httproutes"] + verbs: ["get","watch","list"] +{{- end }} +{{- if has "gateway-httproute" .Values.sources }} + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get","watch","list"] +{{- end }} +{{- if has "gateway-grpcroute" .Values.sources }} + - apiGroups: ["gateway.networking.k8s.io"] + resources: ["grpcroutes"] + verbs: ["get","watch","list"] +{{- end }} +{{- if has "gateway-tlsroute" .Values.sources }} + - apiGroups: ["gateway.networking.k8s.io"] + resources: ["tlsroutes"] + verbs: ["get","watch","list"] +{{- end }} +{{- if has "gateway-tcproute" .Values.sources }} + - apiGroups: ["gateway.networking.k8s.io"] + resources: ["tcproutes"] + verbs: ["get","watch","list"] +{{- end }} +{{- if has "gateway-udproute" .Values.sources }} + - apiGroups: ["gateway.networking.k8s.io"] + resources: ["udproutes"] + verbs: ["get","watch","list"] +{{- end }} {{- if has "gloo-proxy" .Values.sources }} - apiGroups: ["gloo.solo.io","gateway.solo.io"] resources: ["proxies","virtualservices"] @@ -83,6 +118,11 @@ rules: resources: ["routegroups/status"] verbs: ["patch","update"] {{- end }} +{{- if has "f5-virtualserver" .Values.sources }} + - apiGroups: ["cis.f5.com"] + resources: ["virtualservers"] + verbs: ["get","watch","list"] +{{- end }} {{- with .Values.rbac.additionalPermissions }} {{- toYaml . | nindent 2 }} {{- end }} diff --git a/charts/external-dns/templates/clusterrolebinding.yaml b/charts/external-dns/templates/clusterrolebinding.yaml index 9028c6f96..74a51476f 100644 --- a/charts/external-dns/templates/clusterrolebinding.yaml +++ b/charts/external-dns/templates/clusterrolebinding.yaml @@ -1,13 +1,13 @@ {{- if .Values.rbac.create -}} apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding +kind: {{ .Values.namespaced | ternary "RoleBinding" "ClusterRoleBinding" }} metadata: name: {{ printf "%s-viewer" (include "external-dns.fullname" .) }} labels: {{- include "external-dns.labels" . | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io - kind: ClusterRole + kind: {{ .Values.namespaced | ternary "Role" "ClusterRole" }} name: {{ template "external-dns.fullname" . }} subjects: - kind: ServiceAccount diff --git a/charts/external-dns/templates/deployment.yaml b/charts/external-dns/templates/deployment.yaml index 70952675d..5c3e1128f 100644 --- a/charts/external-dns/templates/deployment.yaml +++ b/charts/external-dns/templates/deployment.yaml @@ -89,6 +89,9 @@ spec: - --txt-suffix={{ .Values.txtSuffix }} {{- end }} {{- end }} + {{- if .Values.namespaced }} + - --namespace={{ .Release.Namespace }} + {{- end }} {{- range .Values.domainFilters }} - --domain-filter={{ . }} {{- end }} diff --git a/charts/external-dns/values.yaml b/charts/external-dns/values.yaml index 5b30a9c0e..6e0f80265 100644 --- a/charts/external-dns/values.yaml +++ b/charts/external-dns/values.yaml @@ -151,6 +151,8 @@ logFormat: text interval: 1m triggerLoopOnEvent: false +namespaced: false + sources: - service - ingress diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 52576cc5b..8d2f51943 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -2,15 +2,22 @@ timeout: 5000s options: substitution_option: ALLOW_LOOSE + machineType: 'N1_HIGHCPU_8' steps: - - name: "gcr.io/k8s-staging-test-infra/gcb-docker-gcloud:v20211118-2f2d816b90" - entrypoint: make + - name: "gcr.io/k8s-staging-test-infra/gcb-docker-gcloud:v20230206-8160eea68e" + entrypoint: bash env: - DOCKER_CLI_EXPERIMENTAL=enabled - VERSION=$_GIT_TAG - PULL_BASE_REF=$_PULL_BASE_REF + - HOME=/root args: - - release.staging + - -c + - | + gcloud auth configure-docker + /buildx-entrypoint version + apk add musl-dev gcc + make release.staging substitutions: # _GIT_TAG will be filled with a git-based tag for the image, of the form vYYYYMMDD-hash, and # can be used as a substitution diff --git a/controller/controller.go b/controller/controller.go index 528d870fa..0012ac088 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -102,6 +102,14 @@ var ( Help: "Number of Registry A records.", }, ) + registryAAAARecords = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: "external_dns", + Subsystem: "registry", + Name: "aaaa_records", + Help: "Number of Registry AAAA records.", + }, + ) sourceARecords = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "external_dns", @@ -110,6 +118,14 @@ var ( Help: "Number of Source A records.", }, ) + sourceAAAARecords = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: "external_dns", + Subsystem: "source", + Name: "aaaa_records", + Help: "Number of Source AAAA records.", + }, + ) verifiedARecords = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "external_dns", @@ -118,6 +134,14 @@ var ( Help: "Number of DNS A-records that exists both in source and registry.", }, ) + verifiedAAAARecords = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: "external_dns", + Subsystem: "controller", + Name: "verified_aaaa_records", + Help: "Number of DNS AAAA-records that exists both in source and registry.", + }, + ) ) func init() { @@ -130,8 +154,11 @@ func init() { prometheus.MustRegister(deprecatedSourceErrors) prometheus.MustRegister(controllerNoChangesTotal) prometheus.MustRegister(registryARecords) + prometheus.MustRegister(registryAAAARecords) prometheus.MustRegister(sourceARecords) + prometheus.MustRegister(sourceAAAARecords) prometheus.MustRegister(verifiedARecords) + prometheus.MustRegister(verifiedAAAARecords) } // Controller is responsible for orchestrating the different components. @@ -171,8 +198,9 @@ func (c *Controller) RunOnce(ctx context.Context) error { missingRecords := c.Registry.MissingRecords() registryEndpointsTotal.Set(float64(len(records))) - regARecords := filterARecords(records) - registryARecords.Set(float64(len(regARecords))) + regARecords, regAAAARecords := countAddressRecords(records) + registryARecords.Set(float64(regARecords)) + registryAAAARecords.Set(float64(regAAAARecords)) ctx = context.WithValue(ctx, provider.RecordsContextKey, records) endpoints, err := c.Source.Endpoints(ctx) @@ -182,10 +210,12 @@ func (c *Controller) RunOnce(ctx context.Context) error { return err } sourceEndpointsTotal.Set(float64(len(endpoints))) - srcARecords := filterARecords(endpoints) - sourceARecords.Set(float64(len(srcARecords))) - vRecords := fetchMatchingARecords(endpoints, records) - verifiedARecords.Set(float64(len(vRecords))) + srcARecords, srcAAAARecords := countAddressRecords(endpoints) + sourceARecords.Set(float64(srcARecords)) + sourceAAAARecords.Set(float64(srcAAAARecords)) + vARecords, vAAAARecords := countMatchingAddressRecords(endpoints, records) + verifiedARecords.Set(float64(vARecords)) + verifiedAAAARecords.Set(float64(vAAAARecords)) endpoints = c.Registry.AdjustEndpoints(endpoints) if len(missingRecords) > 0 { @@ -238,30 +268,44 @@ func (c *Controller) RunOnce(ctx context.Context) error { return nil } -// Checks and returns the intersection of A records in endpoint and registry. -func fetchMatchingARecords(endpoints []*endpoint.Endpoint, registryRecords []*endpoint.Endpoint) []string { - aRecords := filterARecords(endpoints) - recordsMap := make(map[string]struct{}) +// Counts the intersections of A and AAAA records in endpoint and registry. +func countMatchingAddressRecords(endpoints []*endpoint.Endpoint, registryRecords []*endpoint.Endpoint) (int, int) { + recordsMap := make(map[string]map[string]struct{}) for _, regRecord := range registryRecords { - recordsMap[regRecord.DNSName] = struct{}{} + if _, found := recordsMap[regRecord.DNSName]; !found { + recordsMap[regRecord.DNSName] = make(map[string]struct{}) + } + recordsMap[regRecord.DNSName][regRecord.RecordType] = struct{}{} } - var cm []string - for _, sourceRecord := range aRecords { - if _, found := recordsMap[sourceRecord]; found { - cm = append(cm, sourceRecord) + aCount := 0 + aaaaCount := 0 + for _, sourceRecord := range endpoints { + if _, found := recordsMap[sourceRecord.DNSName]; found { + if _, found := recordsMap[sourceRecord.DNSName][sourceRecord.RecordType]; found { + switch sourceRecord.RecordType { + case endpoint.RecordTypeA: + aCount++ + case endpoint.RecordTypeAAAA: + aaaaCount++ + } + } } } - return cm + return aCount, aaaaCount } -func filterARecords(endpoints []*endpoint.Endpoint) []string { - var aRecords []string +func countAddressRecords(endpoints []*endpoint.Endpoint) (int, int) { + aCount := 0 + aaaaCount := 0 for _, endPoint := range endpoints { - if endPoint.RecordType == endpoint.RecordTypeA { - aRecords = append(aRecords, endPoint.DNSName) + switch endPoint.RecordType { + case endpoint.RecordTypeA: + aCount++ + case endpoint.RecordTypeAAAA: + aaaaCount++ } } - return aRecords + return aCount, aaaaCount } // ScheduleRunOnce makes sure execution happens at most once per interval. @@ -292,7 +336,7 @@ func (c *Controller) Run(ctx context.Context) { for { if c.ShouldRunOnce(time.Now()) { if err := c.RunOnce(ctx); err != nil { - log.Error(err) + log.Fatal(err) } } select { diff --git a/controller/controller_test.go b/controller/controller_test.go index a56a0b3ac..f6b03d433 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -21,6 +21,7 @@ import ( "errors" "math" "reflect" + "sort" "testing" "time" @@ -83,32 +84,20 @@ func (p *errorMockProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, // ApplyChanges validates that the passed in changes satisfy the assumptions. func (p *mockProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - if len(changes.Create) != len(p.ExpectChanges.Create) { - return errors.New("number of created records is wrong") + if err := verifyEndpoints(changes.Create, p.ExpectChanges.Create); err != nil { + return err } - for i := range changes.Create { - if changes.Create[i].DNSName != p.ExpectChanges.Create[i].DNSName || !changes.Create[i].Targets.Same(p.ExpectChanges.Create[i].Targets) { - return errors.New("created record is wrong") - } + if err := verifyEndpoints(changes.UpdateNew, p.ExpectChanges.UpdateNew); err != nil { + return err } - for i := range changes.UpdateNew { - if changes.UpdateNew[i].DNSName != p.ExpectChanges.UpdateNew[i].DNSName || !changes.UpdateNew[i].Targets.Same(p.ExpectChanges.UpdateNew[i].Targets) { - return errors.New("delete record is wrong") - } + if err := verifyEndpoints(changes.UpdateOld, p.ExpectChanges.UpdateOld); err != nil { + return err } - for i := range changes.UpdateOld { - if changes.UpdateOld[i].DNSName != p.ExpectChanges.UpdateOld[i].DNSName || !changes.UpdateOld[i].Targets.Same(p.ExpectChanges.UpdateOld[i].Targets) { - return errors.New("delete record is wrong") - } - } - - for i := range changes.Delete { - if changes.Delete[i].DNSName != p.ExpectChanges.Delete[i].DNSName || !changes.Delete[i].Targets.Same(p.ExpectChanges.Delete[i].Targets) { - return errors.New("delete record is wrong") - } + if err := verifyEndpoints(changes.Delete, p.ExpectChanges.Delete); err != nil { + return err } if !reflect.DeepEqual(ctx.Value(provider.RecordsContextKey), p.RecordsStore) { @@ -117,6 +106,21 @@ func (p *mockProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) return nil } +func verifyEndpoints(actual, expected []*endpoint.Endpoint) error { + if len(actual) != len(expected) { + return errors.New("number of records is wrong") + } + sort.Slice(actual, func(i, j int) bool { + return actual[i].DNSName < actual[j].DNSName + }) + for i := range actual { + if actual[i].DNSName != expected[i].DNSName || !actual[i].Targets.Same(expected[i].Targets) { + return errors.New("record is wrong") + } + } + return nil +} + // newMockProvider creates a new mockProvider returning the given endpoints and validating the desired changes. func newMockProvider(endpoints []*endpoint.Endpoint, changes *plan.Changes) provider.Provider { dnsProvider := &mockProvider{ @@ -132,7 +136,7 @@ func TestRunOnce(t *testing.T) { // Fake some desired endpoints coming from our source. source := new(testutils.MockSource) cfg := externaldns.NewConfig() - cfg.ManagedDNSRecordTypes = []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME} + cfg.ManagedDNSRecordTypes = []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME} source.On("Endpoints").Return([]*endpoint.Endpoint{ { DNSName: "create-record", @@ -144,6 +148,16 @@ func TestRunOnce(t *testing.T) { RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.4.4"}, }, + { + DNSName: "create-aaaa-record", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::1"}, + }, + { + DNSName: "update-aaaa-record", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::2"}, + }, }, nil) // Fake some existing records in our DNS provider and validate some desired changes. @@ -159,18 +173,32 @@ func TestRunOnce(t *testing.T) { RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"4.3.2.1"}, }, + { + DNSName: "update-aaaa-record", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::3"}, + }, + { + DNSName: "delete-aaaa-record", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::4"}, + }, }, &plan.Changes{ Create: []*endpoint.Endpoint{ + {DNSName: "create-aaaa-record", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}}, {DNSName: "create-record", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, UpdateNew: []*endpoint.Endpoint{ + {DNSName: "update-aaaa-record", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::2"}}, {DNSName: "update-record", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.4.4"}}, }, UpdateOld: []*endpoint.Endpoint{ + {DNSName: "update-aaaa-record", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::3"}}, {DNSName: "update-record", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}}, }, Delete: []*endpoint.Endpoint{ + {DNSName: "delete-aaaa-record", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::4"}}, {DNSName: "delete-record", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"4.3.2.1"}}, }, }, @@ -193,6 +221,7 @@ func TestRunOnce(t *testing.T) { source.AssertExpectations(t) // check the verified records assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedARecords)) + assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedAAAARecords)) } func valueFromMetric(metric prometheus.Gauge) uint64 { @@ -253,7 +282,7 @@ func TestShouldRunOnce(t *testing.T) { func testControllerFiltersDomains(t *testing.T, configuredEndpoints []*endpoint.Endpoint, domainFilter endpoint.DomainFilterInterface, providerEndpoints []*endpoint.Endpoint, expectedChanges []*plan.Changes) { t.Helper() cfg := externaldns.NewConfig() - cfg.ManagedDNSRecordTypes = []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME} + cfg.ManagedDNSRecordTypes = []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME} source := new(testutils.MockSource) source.On("Endpoints").Return(configuredEndpoints, nil) @@ -526,6 +555,85 @@ func TestVerifyARecords(t *testing.T) { }}, ) assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedARecords)) + assert.Equal(t, math.Float64bits(0), valueFromMetric(verifiedAAAARecords)) +} + +func TestVerifyAAAARecords(t *testing.T) { + testControllerFiltersDomains( + t, + []*endpoint.Endpoint{ + { + DNSName: "create-record.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::1"}, + }, + { + DNSName: "some-record.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::2"}, + }, + }, + endpoint.NewDomainFilter([]string{"used.tld"}), + []*endpoint.Endpoint{ + { + DNSName: "some-record.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::2"}, + }, + { + DNSName: "create-record.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::1"}, + }, + }, + []*plan.Changes{}, + ) + assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedAAAARecords)) + + testControllerFiltersDomains( + t, + []*endpoint.Endpoint{ + { + DNSName: "some-record.1.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::1"}, + }, + { + DNSName: "some-record.2.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::2"}, + }, + { + DNSName: "some-record.3.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::3"}, + }, + }, + endpoint.NewDomainFilter([]string{"used.tld"}), + []*endpoint.Endpoint{ + { + DNSName: "some-record.1.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::1"}, + }, + { + DNSName: "some-record.2.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::2"}, + }, + }, + []*plan.Changes{{ + Create: []*endpoint.Endpoint{ + { + DNSName: "some-record.3.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::3"}, + }, + }, + }}, + ) + assert.Equal(t, math.Float64bits(0), valueFromMetric(verifiedARecords)) + assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedAAAARecords)) } func TestARecords(t *testing.T) { @@ -628,3 +736,50 @@ func TestMissingRecordsApply(t *testing.T) { }, }) } + +func TestAAAARecords(t *testing.T) { + testControllerFiltersDomains( + t, + []*endpoint.Endpoint{ + { + DNSName: "record1.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::1"}, + }, + { + DNSName: "record2.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::2"}, + }, + { + DNSName: "_mysql-svc._tcp.mysql.used.tld", + RecordType: endpoint.RecordTypeSRV, + Targets: endpoint.Targets{"0 50 30007 mysql.used.tld"}, + }, + }, + endpoint.NewDomainFilter([]string{"used.tld"}), + []*endpoint.Endpoint{ + { + DNSName: "record1.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::1"}, + }, + { + DNSName: "_mysql-svc._tcp.mysql.used.tld", + RecordType: endpoint.RecordTypeSRV, + Targets: endpoint.Targets{"0 50 30007 mysql.used.tld"}, + }, + }, + []*plan.Changes{{ + Create: []*endpoint.Endpoint{ + { + DNSName: "record2.used.tld", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::2"}, + }, + }, + }}, + ) + assert.Equal(t, math.Float64bits(2), valueFromMetric(sourceAAAARecords)) + assert.Equal(t, math.Float64bits(1), valueFromMetric(registryAAAARecords)) +} diff --git a/docs/contributing/crd-source/dnsendpoint-aws-example.yaml b/docs/contributing/crd-source/dnsendpoint-aws-example.yaml new file mode 100644 index 000000000..e437f0f04 --- /dev/null +++ b/docs/contributing/crd-source/dnsendpoint-aws-example.yaml @@ -0,0 +1,18 @@ +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: examplednsrecord +spec: + endpoints: + - dnsName: subdomain.foo.bar.com + providerSpecific: + - name: "aws/failover" + value: "PRIMARY" + - name: "aws/health-check-id" + value: "asdf1234-as12-as12-as12-asdf12345678" + - name: "aws/evaluate-target-health" + value: "true" + recordType: CNAME + setIdentifier: some-unique-id + targets: + - other-subdomain.foo.bar.com diff --git a/docs/contributing/crd-source/dnsendpoint-example.yaml b/docs/contributing/crd-source/dnsendpoint-example.yaml index 894301d8a..2ed7d7fa0 100644 --- a/docs/contributing/crd-source/dnsendpoint-example.yaml +++ b/docs/contributing/crd-source/dnsendpoint-example.yaml @@ -9,3 +9,7 @@ spec: recordType: A targets: - 192.168.99.216 + # Provider specific configurations are set like an annotation would on other sources + providerSpecific: + - name: external-dns.alpha.kubernetes.io/cloudflare-proxied + value: "true" diff --git a/docs/faq.md b/docs/faq.md index 217951aaa..6da020c5d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -178,16 +178,18 @@ You can use the host label in the metric to figure out if the request was agains Here is the full list of available metrics provided by ExternalDNS: -| Name | Description | Type | -| --------------------------------------------------- | ------------------------------------------------------- | ------- | -| external_dns_controller_last_sync_timestamp_seconds | Timestamp of last successful sync with the DNS provider | Gauge | -| external_dns_registry_endpoints_total | Number of Endpoints in all sources | Gauge | -| external_dns_registry_errors_total | Number of Registry errors | Counter | -| external_dns_source_endpoints_total | Number of Endpoints in the registry | Gauge | -| external_dns_source_errors_total | Number of Source errors | Counter | -| external_dns_controller_verified_records | Number of DNS A-records that exists both in | Gauge | -| | source & registry | | +| Name | Description | Type | +| --------------------------------------------------- | ------------------------------------------------------------------ | ------- | +| external_dns_controller_last_sync_timestamp_seconds | Timestamp of last successful sync with the DNS provider | Gauge | +| external_dns_registry_endpoints_total | Number of Endpoints in all sources | Gauge | +| external_dns_registry_errors_total | Number of Registry errors | Counter | +| external_dns_source_endpoints_total | Number of Endpoints in the registry | Gauge | +| external_dns_source_errors_total | Number of Source errors | Counter | +| external_dns_controller_verified_aaaa_records | Number of DNS AAAA-records that exists both in source and registry | Gauge | +| external_dns_controller_verified_a_records | Number of DNS A-records that exists both in source and registry | Gauge | +| external_dns_registry_aaaa_records | Number of AAAA records in registry | Gauge | | external_dns_registry_a_records | Number of A records in registry | Gauge | +| external_dns_source_aaaa_records | Number of AAAA records in source | Gauge | | external_dns_source_a_records | Number of A records in source | Gauge | ### How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects? @@ -205,7 +207,7 @@ $ docker run \ -e EXTERNAL_DNS_SOURCE=$'service\ningress' \ -e EXTERNAL_DNS_PROVIDER=google \ -e EXTERNAL_DNS_DOMAIN_FILTER=$'foo.com\nbar.com' \ - registry.k8s.io/external-dns/external-dns:v0.13.2 + registry.k8s.io/external-dns/external-dns:v0.13.5 time="2017-08-08T14:10:26Z" level=info msg="config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ... ``` @@ -255,24 +257,33 @@ spec: ### Running an internal and external dns service Sometimes you need to run an internal and an external dns service. -The internal one should provision hostnames used on the internal network (perhaps inside a VPC), and the external -one to expose DNS to the internet. +The internal one should provision hostnames used on the internal network (perhaps inside a VPC), and the external one to expose DNS to the internet. -To do this with ExternalDNS you can use the `--annotation-filter` to specifically tie an instance of ExternalDNS to -an instance of an ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external` -then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-internal)` -and one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-external)`. +To do this with ExternalDNS you can use the `--ingress-class` flag to specifically tie an instance of ExternalDNS to an instance of a ingress controller. +Let's assume you have two ingress controllers, `internal` and `external`. +You can then start two ExternalDNS providers, one with `--ingress-class=internal` and one with `--ingress-class=external`. -If you need to search for multiple values of said annotation, you can provide a comma separated list, like so: -`--annotation-filter=kubernetes.io/ingress.class in (nginx-internal, alb-ingress-internal)`. +If you need to search for multiple ingress classes, you can specify the flag multiple times, like so: +`--ingress-class=internal --ingress-class=external`. -Beware when using multiple sources, e.g. `--source=service --source=ingress`, `--annotation-filter` will filter every given source objects. -If you need to filter only one specific source you have to run a separated external dns service containing only the wanted `--source` and `--annotation-filter`. +The `--ingress-class` flag will check both the `spec.ingressClassName` field and the deprecated `kubernetes.io/ingress.class` annotation. +The `spec.ingressClassName` tasks precedence over the annotation if both are supplied. -**Note:** Filtering based on annotation means that the external-dns controller will receive all resources of that kind and then filter on the client-side. -In larger clusters with many resources which change frequently this can cause performance issues. If only some resources need to be managed by an instance -of external-dns then label filtering can be used instead of annotation filtering. This means that only those resources which match the selector specified -in `--label-filter` will be passed to the controller. +**Backward compatibility** + +The previous `--annotation-filter` flag can still be used to restrict which objects ExternalDNS considers; for example, `--annotation-filter=kubernetes.io/ingress.class in (public,dmz)`. + +However, beware when using annotation filters with multiple sources, e.g. `--source=service --source=ingress`, since `--annotation-filter` will filter every given source object. +If you need to use annotation filters against a specific source you have to run a separated external dns service containing only the wanted `--source` and `--annotation-filter`. + +Note: the `--ingress-class` flag cannot be used at the same time as the `--annotation-filter=kubernetes.io/ingress.class in (...)` flag; if you do this an error will be raised. + +**Performance considerations** + +Filtering based on ingress class name or annotations means that the external-dns controller will receive all resources of that kind and then filter on the client-side. +In larger clusters with many resources which change frequently this can cause performance issues. +If only some resources need to be managed by an instance of external-dns then label filtering can be used instead of ingress class filtering (or legacy annotation filtering). +This means that only those resources which match the selector specified in `--label-filter` will be passed to the controller. ### 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? diff --git a/docs/registry.md b/docs/registry.md index 615ba305a..3ff20b93a 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -13,3 +13,68 @@ The controller will try to create the "new format" TXT records if they are not p Later on, the old format will be dropped and only the new format will be kept (-). Cleanup will be done by controller itself. + +### Encryption of TXT Records +TXT records may contain sensitive information, such as the internal ingress name or namespace, which attackers could exploit to gather information about your infrastructure. +By encrypting TXT records, you can protect this information from unauthorized access. It is strongly recommended to encrypt all TXT records to prevent potential security breaches. + +To enable encryption of TXT records, you can use the following parameters: +- `--txt-encrypt-enabled=true` +- `--txt-encrypt-aes-key=32bytesKey` (used for AES-256-GCM encryption and should be exactly 32 bytes) + +Note that the key used for encryption should be a secure key and properly managed to ensure the security of your TXT records. + +### Generating TXT encryption AES key +Python +```python +python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())' +``` + +Bash +```shell +dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d -- '\n' | tr -- '+/' '-_'; echo +``` + +OpenSSL +```shell +openssl rand -base64 32 | tr -- '+/' '-_' +``` + +PowerShell +```powershell +# Add System.Web assembly to session, just in case +Add-Type -AssemblyName System.Web +[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Web.Security.Membership]::GeneratePassword(32,4))).Replace("+","-").Replace("/","_") +``` + +Terraform +```hcl +resource "random_password" "txt_key" { + length = 32 + override_special = "-_" +} +``` + +### Manually Encrypt/Decrypt TXT Records + +In some cases, you may need to edit labels generated by External-DNS, and in such cases, you can use simple Golang code to do that. + +```go +package main + +import ( + "fmt" + "sigs.k8s.io/external-dns/endpoint" +) + +func main() { + key := []byte("testtesttesttesttesttesttesttest") + encrypted, _ := endpoint.EncryptText( + "heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example", + key, + nil, + ) + decrypted, _, _ := endpoint.DecryptText(encrypted, key) + fmt.Println(decrypted) +} +``` diff --git a/docs/tutorials/ANS_Group_SafeDNS.md b/docs/tutorials/ANS_Group_SafeDNS.md index 9f636b310..92450495b 100644 --- a/docs/tutorials/ANS_Group_SafeDNS.md +++ b/docs/tutorials/ANS_Group_SafeDNS.md @@ -48,7 +48,7 @@ spec: - name: external-dns # You will need to check what the latest version is yourself: # https://github.com/kubernetes-sigs/external-dns/releases - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible # (optional) limit to only example.com domains; change to match the @@ -114,7 +114,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible # (optional) limit to only example.com domains; change to match the diff --git a/docs/tutorials/akamai-edgedns.md b/docs/tutorials/akamai-edgedns.md index beda1096a..4c8893423 100644 --- a/docs/tutorials/akamai-edgedns.md +++ b/docs/tutorials/akamai-edgedns.md @@ -57,7 +57,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # or ingress or both - --provider=akamai @@ -143,7 +143,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # or ingress or both - --provider=akamai diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md index 3f24f71fc..379e13b4c 100644 --- a/docs/tutorials/alibabacloud.md +++ b/docs/tutorials/alibabacloud.md @@ -113,7 +113,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -187,7 +187,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -233,9 +233,8 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: foo - annotations: - kubernetes.io/ingress.class: "nginx" # use the one that corresponds to your ingress controller. spec: + ingressClassName: nginx # use the one that corresponds to your ingress controller. rules: - host: foo.external-dns-test.com http: diff --git a/docs/tutorials/aws-load-balancer-controller.md b/docs/tutorials/aws-load-balancer-controller.md index 9e66b1e6d..98bc5da69 100644 --- a/docs/tutorials/aws-load-balancer-controller.md +++ b/docs/tutorials/aws-load-balancer-controller.md @@ -24,7 +24,7 @@ as Kubernetes does with the AWS cloud provider. In the examples that follow, it is assumed that you configured the ALB Ingress Controller with the `ingress-class=alb` argument (not to be confused with the same argument to ExternalDNS) so that the controller will only respect Ingress -objects with the `kubernetes.io/ingress.class` annotation set to "alb". +objects with the `ingressClassName` field set to "alb". ## Deploy an example application @@ -80,7 +80,6 @@ kind: Ingress metadata: annotations: alb.ingress.kubernetes.io/scheme: internet-facing - kubernetes.io/ingress.class: alb name: echoserver spec: ingressClassName: alb @@ -120,7 +119,6 @@ metadata: annotations: alb.ingress.kubernetes.io/scheme: internet-facing external-dns.alpha.kubernetes.io/hostname: echoserver.mycluster.example.org, echoserver.example.org - kubernetes.io/ingress.class: alb name: echoserver spec: ingressClassName: alb @@ -159,7 +157,6 @@ metadata: annotations: alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/ip-address-type: dualstack - kubernetes.io/ingress.class: alb name: echoserver spec: ingressClassName: alb diff --git a/docs/tutorials/aws-sd.md b/docs/tutorials/aws-sd.md index 321928a73..c540fc7da 100644 --- a/docs/tutorials/aws-sd.md +++ b/docs/tutorials/aws-sd.md @@ -81,7 +81,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 env: - name: AWS_REGION value: us-east-1 # put your CloudMap NameSpace region @@ -148,7 +148,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 env: - name: AWS_REGION value: us-east-1 # put your CloudMap NameSpace region diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md index 6b073b574..fb96ec3e1 100644 --- a/docs/tutorials/aws.md +++ b/docs/tutorials/aws.md @@ -413,7 +413,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -508,7 +508,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -739,9 +739,8 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx - annotations: - kubernetes.io/ingress.class: "nginx" # use the one that corresponds to your ingress controller. spec: + ingressClassName: nginx rules: - host: server.example.com http: @@ -936,7 +935,7 @@ Running several fast polling ExternalDNS instances in a given account can easily * `--source=ingress --source=service` - specify multiple times for multiple sources * `--namespace=my-app` * `--label-filter=app in (my-app)` - * `--annotation-filter=kubernetes.io/ingress.class in (nginx-external)` - note that this filter would apply to services too.. + * `--ingress-class=nginx-external` * Limit services watched by type (not applicable to ingress or other types) * `--service-type-filter=LoadBalancer` default `all` * Limit the hosted zones considered @@ -962,7 +961,7 @@ A simple way to implement randomised startup is with an init container: spec: initContainers: - name: init-jitter - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 command: - /bin/sh - -c diff --git a/docs/tutorials/azure-private-dns.md b/docs/tutorials/azure-private-dns.md index 2e8dab0e4..218b11dff 100644 --- a/docs/tutorials/azure-private-dns.md +++ b/docs/tutorials/azure-private-dns.md @@ -130,7 +130,7 @@ spec: spec: containers: - name: externaldns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -201,7 +201,7 @@ spec: serviceAccountName: externaldns containers: - name: externaldns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -272,7 +272,7 @@ spec: serviceAccountName: externaldns containers: - name: externaldns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -416,9 +416,8 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx - annotations: - kubernetes.io/ingress.class: nginx spec: + ingressClassName: nginx rules: - host: server.example.com http: diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md index 47974a1c1..330c970e1 100644 --- a/docs/tutorials/azure.md +++ b/docs/tutorials/azure.md @@ -356,7 +356,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -424,7 +424,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -495,7 +495,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -560,9 +560,8 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx - annotations: - kubernetes.io/ingress.class: nginx spec: + ingressClassName: nginx rules: - host: server.example.com http: @@ -649,3 +648,9 @@ resource group: ```bash $ az group delete --name "MyDnsResourceGroup" ``` + +## More tutorials + +A video explanantion is available here: https://www.youtube.com/watch?v=VSn6DPKIhM8&list=PLpbcUe4chE79sB7Jg7B4z3HytqUUEwcNE + +![image](https://user-images.githubusercontent.com/6548359/235437721-87611869-75f2-4f32-bb35-9da585e46299.png) diff --git a/docs/tutorials/bluecat.md b/docs/tutorials/bluecat.md index f0cd4295e..5d386aab8 100644 --- a/docs/tutorials/bluecat.md +++ b/docs/tutorials/bluecat.md @@ -46,7 +46,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --log-level=debug - --source=service @@ -136,7 +136,7 @@ spec: secretName: bluecatconfig containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 volumeMounts: - name: bluecatconfig mountPath: "/etc/external-dns/" diff --git a/docs/tutorials/civo.md b/docs/tutorials/civo.md index 52cd8dfe7..32166a1e8 100644 --- a/docs/tutorials/civo.md +++ b/docs/tutorials/civo.md @@ -41,7 +41,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -59,7 +59,7 @@ kind: ServiceAccount metadata: name: external-dns --- -apiVersion: rbac.authorization.k8s.io/v1beta1 +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns @@ -74,7 +74,7 @@ rules: resources: ["nodes"] verbs: ["list"] --- -apiVersion: rbac.authorization.k8s.io/v1beta1 +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer @@ -105,7 +105,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index e8483d94c..869483e89 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -20,6 +20,8 @@ Snippet from [Cloudflare - Getting Started](https://api.cloudflare.com/#getting- API Token will be preferred for authentication if `CF_API_TOKEN` environment variable is set. Otherwise `CF_API_KEY` and `CF_API_EMAIL` should be set to run ExternalDNS with Cloudflare. +You may provide the Cloudflare API token through a file by setting the +`CF_API_TOKEN="file:/path/to/token"`. When using API Token authentication, the token should be granted Zone `Read`, DNS `Edit` privileges, and access to `All zones`. @@ -54,7 +56,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -123,7 +125,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/contour.md b/docs/tutorials/contour.md index 649fddd09..cdb30c52e 100644 --- a/docs/tutorials/contour.md +++ b/docs/tutorials/contour.md @@ -26,7 +26,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -102,7 +102,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md index 0f6e2d3ea..3ef2caf31 100644 --- a/docs/tutorials/coredns.md +++ b/docs/tutorials/coredns.md @@ -108,7 +108,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=ingress - --provider=coredns @@ -175,7 +175,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=ingress - --provider=coredns @@ -198,9 +198,8 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx - annotations: - kubernetes.io/ingress.class: "nginx" spec: + ingressClassName: nginx rules: - host: nginx.example.org http: diff --git a/docs/tutorials/designate.md b/docs/tutorials/designate.md index c2317e720..7ed8b24b4 100644 --- a/docs/tutorials/designate.md +++ b/docs/tutorials/designate.md @@ -59,7 +59,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -136,7 +136,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/digitalocean.md b/docs/tutorials/digitalocean.md index 83431b7aa..a6874326f 100644 --- a/docs/tutorials/digitalocean.md +++ b/docs/tutorials/digitalocean.md @@ -43,7 +43,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -107,7 +107,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/dnsimple.md b/docs/tutorials/dnsimple.md index 1363f05fa..3bdce6838 100644 --- a/docs/tutorials/dnsimple.md +++ b/docs/tutorials/dnsimple.md @@ -35,7 +35,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple. @@ -100,7 +100,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple. diff --git a/docs/tutorials/dyn.md b/docs/tutorials/dyn.md index 5285dbe93..40f5c87ea 100644 --- a/docs/tutorials/dyn.md +++ b/docs/tutorials/dyn.md @@ -43,7 +43,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=ingress - --txt-prefix=_d diff --git a/docs/tutorials/exoscale.md b/docs/tutorials/exoscale.md index 0347cbf78..6f7dd3fbc 100644 --- a/docs/tutorials/exoscale.md +++ b/docs/tutorials/exoscale.md @@ -41,7 +41,7 @@ spec: # serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=ingress # or service or both - --provider=exoscale @@ -109,9 +109,9 @@ kind: Ingress metadata: name: nginx annotations: - kubernetes.io/ingress.class: nginx external-dns.alpha.kubernetes.io/target: {{ Elastic-IP-address }} spec: + ingressClassName: nginx rules: - host: via-ingress.example.com http: diff --git a/docs/tutorials/externalname.md b/docs/tutorials/externalname.md index 0bf6aaff2..3604823ed 100644 --- a/docs/tutorials/externalname.md +++ b/docs/tutorials/externalname.md @@ -27,7 +27,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --log-level=debug - --source=service diff --git a/docs/tutorials/gandi.md b/docs/tutorials/gandi.md index 52132f047..a51ad0abd 100644 --- a/docs/tutorials/gandi.md +++ b/docs/tutorials/gandi.md @@ -39,7 +39,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -56,7 +56,7 @@ kind: ServiceAccount metadata: name: external-dns --- -apiVersion: rbac.authorization.k8s.io/v1beta1 +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns @@ -71,7 +71,7 @@ rules: resources: ["nodes"] verbs: ["list","watch"] --- -apiVersion: rbac.authorization.k8s.io/v1beta1 +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer @@ -103,7 +103,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/gateway-api.md b/docs/tutorials/gateway-api.md index 617258560..f1f39ca69 100644 --- a/docs/tutorials/gateway-api.md +++ b/docs/tutorials/gateway-api.md @@ -72,7 +72,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: # Add desired Gateway API Route sources. - --source=gateway-httproute diff --git a/docs/tutorials/gke.md b/docs/tutorials/gke.md index 4f3246cf7..e0308f9e7 100644 --- a/docs/tutorials/gke.md +++ b/docs/tutorials/gke.md @@ -319,7 +319,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress diff --git a/docs/tutorials/gloo-proxy.md b/docs/tutorials/gloo-proxy.md index 0ec63da80..7a9994eca 100644 --- a/docs/tutorials/gloo-proxy.md +++ b/docs/tutorials/gloo-proxy.md @@ -22,7 +22,7 @@ spec: containers: - name: external-dns # update this to the desired external-dns version - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=gloo-proxy - --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system) @@ -90,7 +90,7 @@ spec: containers: - name: external-dns # update this to the desired external-dns version - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=gloo-proxy - --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system) diff --git a/docs/tutorials/godaddy.md b/docs/tutorials/godaddy.md index 02e22a14f..0dddfdf81 100644 --- a/docs/tutorials/godaddy.md +++ b/docs/tutorials/godaddy.md @@ -44,7 +44,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -115,7 +115,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/hostport.md b/docs/tutorials/hostport.md index 34f94a310..904fe3667 100644 --- a/docs/tutorials/hostport.md +++ b/docs/tutorials/hostport.md @@ -31,7 +31,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --log-level=debug - --source=service @@ -96,7 +96,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --log-level=debug - --source=service @@ -114,7 +114,7 @@ spec: First lets deploy a Kafka Stateful set, a simple example(a lot of stuff is missing) with a headless service called `ksvc` ```yaml -apiVersion: apps/v1beta1 +apiVersion: apps/v1 kind: StatefulSet metadata: name: kafka diff --git a/docs/tutorials/ibmcloud.md b/docs/tutorials/ibmcloud.md index 93784c29c..b6a164d0f 100644 --- a/docs/tutorials/ibmcloud.md +++ b/docs/tutorials/ibmcloud.md @@ -69,7 +69,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -142,7 +142,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/infoblox.md b/docs/tutorials/infoblox.md index 7e713e036..143067d38 100644 --- a/docs/tutorials/infoblox.md +++ b/docs/tutorials/infoblox.md @@ -69,7 +69,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains. @@ -150,7 +150,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains. diff --git a/docs/tutorials/istio.md b/docs/tutorials/istio.md index a4e597657..0a519a46b 100644 --- a/docs/tutorials/istio.md +++ b/docs/tutorials/istio.md @@ -28,7 +28,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -98,7 +98,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress diff --git a/docs/tutorials/kong.md b/docs/tutorials/kong.md index 49a456f69..1c5bd6db3 100644 --- a/docs/tutorials/kong.md +++ b/docs/tutorials/kong.md @@ -22,7 +22,7 @@ spec: containers: - name: external-dns # update this to the desired external-dns version - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=kong-tcpingress - --provider=aws @@ -86,7 +86,7 @@ spec: containers: - name: external-dns # update this to the desired external-dns version - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=kong-tcpingress - --provider=aws diff --git a/docs/tutorials/kube-ingress-aws.md b/docs/tutorials/kube-ingress-aws.md index bea5e56ac..5cf37d4ec 100644 --- a/docs/tutorials/kube-ingress-aws.md +++ b/docs/tutorials/kube-ingress-aws.md @@ -141,8 +141,6 @@ Create the following Ingress to expose the echoserver application to the Interne apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - annotations: - kubernetes.io/ingress.class: skipper name: echoserver spec: ingressClassName: skipper @@ -181,7 +179,6 @@ kind: Ingress metadata: annotations: external-dns.alpha.kubernetes.io/hostname: echoserver.mycluster.example.org, echoserver.example.org - kubernetes.io/ingress.class: skipper name: echoserver spec: ingressClassName: skipper @@ -218,7 +215,6 @@ kind: Ingress metadata: annotations: alb.ingress.kubernetes.io/ip-address-type: dualstack - kubernetes.io/ingress.class: skipper name: echoserver spec: ingressClassName: skipper @@ -256,7 +252,6 @@ kind: Ingress metadata: annotations: zalando.org/aws-load-balancer-type: nlb - kubernetes.io/ingress.class: skipper name: echoserver spec: ingressClassName: skipper diff --git a/docs/tutorials/linode.md b/docs/tutorials/linode.md index 9505a6eea..101a1be3f 100644 --- a/docs/tutorials/linode.md +++ b/docs/tutorials/linode.md @@ -41,7 +41,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -105,7 +105,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/mx-record.md b/docs/tutorials/mx-record.md new file mode 100644 index 000000000..5b5a0eb0b --- /dev/null +++ b/docs/tutorials/mx-record.md @@ -0,0 +1,28 @@ +# Creating MX record with CRD source + +You can create and manage MX records with the help of [CRD source](/docs/contributing/crd-source.md) +and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, and `google` providers. + +In order to start managing MX records you need to set the `--managed-record-types MX` flag. + +```console +external-dns --source crd --provider {aws|azure|google} --managed-record-types A --managed-record-types CNAME --managed-record-types MX +``` + +Targets within the CRD need to be specified according to the RFC 1034 (section 3.6.1). Below is an example of +`example.com` DNS MX record which specifies two separate targets with distinct priorities. + +```yaml +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: examplemxrecord +spec: + endpoints: + - dnsName: example.com + recordTTL: 180 + recordType: MX + targets: + - 10 mailhost1.example.com + - 20 mailhost2.example.com +``` diff --git a/docs/tutorials/nginx-ingress.md b/docs/tutorials/nginx-ingress.md index bf491ea51..fb663d554 100644 --- a/docs/tutorials/nginx-ingress.md +++ b/docs/tutorials/nginx-ingress.md @@ -273,7 +273,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=ingress - --domain-filter=external-dns-test.gcp.zalan.do @@ -294,8 +294,6 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx - annotations: - kubernetes.io/ingress.class: nginx spec: ingressClassName: nginx rules: @@ -570,7 +568,7 @@ spec: - --google-project=zalando-external-dns-test - --registry=txt - --txt-owner-id=my-identifier - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 name: external-dns securityContext: fsGroup: 65534 @@ -595,8 +593,6 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx - annotations: - kubernetes.io/ingress.class: nginx spec: ingressClassName: nginx rules: diff --git a/docs/tutorials/nodes.md b/docs/tutorials/nodes.md index a4a1e1f15..09507180e 100644 --- a/docs/tutorials/nodes.md +++ b/docs/tutorials/nodes.md @@ -3,8 +3,9 @@ This tutorial describes how to configure ExternalDNS to use the cluster nodes as source. Using nodes (`--source=node`) as source is possible to synchronize a DNS zone with the nodes of a cluster. -The node source adds an `A` record per each node `externalIP` (if not found, node's `internalIP` is used). -The TTL record can be set with the `external-dns.alpha.kubernetes.io/ttl` node annotation. +The node source adds an `A` record per each node `externalIP` (if not found, any IPv4 `internalIP` is used instead). +It also adds an `AAAA` record per each node IPv6 `internalIP`. +The TTL of the records can be set with the `external-dns.alpha.kubernetes.io/ttl` node annotation. ## Manifest (for cluster without RBAC enabled) @@ -28,7 +29,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=node # will use nodes as source - --provider=aws @@ -99,7 +100,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=node # will use nodes as source - --provider=aws diff --git a/docs/tutorials/ns1.md b/docs/tutorials/ns1.md index d286497f8..faccd6b1c 100644 --- a/docs/tutorials/ns1.md +++ b/docs/tutorials/ns1.md @@ -61,7 +61,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -125,7 +125,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/openshift.md b/docs/tutorials/openshift.md index 6cdda8d3b..b8297ee4f 100644 --- a/docs/tutorials/openshift.md +++ b/docs/tutorials/openshift.md @@ -66,7 +66,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=openshift-route - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones @@ -133,7 +133,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=openshift-route - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones diff --git a/docs/tutorials/oracle.md b/docs/tutorials/oracle.md index 745947972..39d804d80 100644 --- a/docs/tutorials/oracle.md +++ b/docs/tutorials/oracle.md @@ -6,16 +6,25 @@ Make sure to use the latest version of ExternalDNS for this tutorial. ## Creating an OCI DNS Zone -Create a DNS zone which will contain the managed DNS records. Let's use `example.com` as an reference here. +Create a DNS zone which will contain the managed DNS records. Let's use +`example.com` as a reference here. Make note of the OCID of the compartment +in which you created the zone; you'll need to provide that later. For more information about OCI DNS see the documentation [here][1]. ## Deploy ExternalDNS Connect your `kubectl` client to the cluster you want to test ExternalDNS with. +The OCI provider supports two authentication options: key-based and instance +principals. + +### Key-based + We first need to create a config file containing the information needed to connect with the OCI API. -Create a new file (oci.yaml) and modify the contents to match the example below. Be sure to adjust the values to match your own credentials: +Create a new file (oci.yaml) and modify the contents to match the example +below. Be sure to adjust the values to match your own credentials, and the OCID +of the compartment containing the zone: ```yaml auth: @@ -37,7 +46,29 @@ Create a secret using the config file above: $ kubectl create secret generic external-dns-config --from-file=oci.yaml ``` -### Manifest (for clusters with RBAC enabled) +### OCI IAM Instance Principal + +If you're running ExternalDNS within OCI, you can use OCI IAM instance +principals to authenticate with OCI. This obviates the need to create the +secret with your credentials. You'll need to ensure an OCI IAM policy exists +with a statement granting the `manage dns` permission on zones and records in +the target compartment to the dynamic group covering your instance running +ExternalDNS. +E.g.: + +``` +Allow dynamic-group to manage dns in compartment id +``` + +You'll also need to add the `--oci-auth-instance-principal` flag to enable +this type of authentication. Finally, you'll need to add the +`--oci-compartment-ocid=ocid1.compartment.oc1...` flag to provide the OCID of +the compartment containing the zone to be managed. + +For more information about OCI IAM instance principals, see the documentation [here][2]. +For more information about OCI IAM policy details for the DNS service, see the documentation [here][3]. + +## Manifest (for clusters with RBAC enabled) Apply the following manifest to deploy ExternalDNS. @@ -93,7 +124,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -159,3 +190,6 @@ $ kubectl apply -f nginx.yaml ``` [1]: https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm +[2]: https://docs.cloud.oracle.com/iaas/Content/Identity/Reference/dnspolicyreference.htm +[3]: https://docs.cloud.oracle.com/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm + diff --git a/docs/tutorials/ovh.md b/docs/tutorials/ovh.md index bdf531520..5bd5078e5 100644 --- a/docs/tutorials/ovh.md +++ b/docs/tutorials/ovh.md @@ -86,7 +86,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -160,7 +160,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/pdns.md b/docs/tutorials/pdns.md index 5cd492e3b..90efd6f04 100644 --- a/docs/tutorials/pdns.md +++ b/docs/tutorials/pdns.md @@ -42,7 +42,7 @@ spec: # serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # or ingress or both - --provider=pdns diff --git a/docs/tutorials/pihole.md b/docs/tutorials/pihole.md index a55c8589b..29f1403f2 100644 --- a/docs/tutorials/pihole.md +++ b/docs/tutorials/pihole.md @@ -78,7 +78,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 # If authentication is disabled and/or you didn't create # a secret, you can remove this block. envFrom: diff --git a/docs/tutorials/plural.md b/docs/tutorials/plural.md index c6c140424..98aaf2079 100644 --- a/docs/tutorials/plural.md +++ b/docs/tutorials/plural.md @@ -35,7 +35,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -105,7 +105,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/public-private-route53.md b/docs/tutorials/public-private-route53.md index 284da28a4..970ad00b5 100644 --- a/docs/tutorials/public-private-route53.md +++ b/docs/tutorials/public-private-route53.md @@ -213,10 +213,10 @@ spec: Consult [AWS ExternalDNS setup docs](aws.md) for installation guidelines. -In ExternalDNS containers args, make sure to specify `annotation-filter` and `aws-zone-type`: +In ExternalDNS containers args, make sure to specify `aws-zone-type` and `ingress-class`: ```yaml -apiVersion: apps/v1beta2 +apiVersion: apps/v1 kind: Deployment metadata: labels: @@ -241,9 +241,9 @@ spec: - --provider=aws - --registry=txt - --txt-owner-id=external-dns - - --annotation-filter=kubernetes.io/ingress.class in (external-ingress) + - --ingress-class=external-ingress - --aws-zone-type=public - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 name: external-dns-public ``` @@ -251,10 +251,10 @@ spec: Consult [AWS ExternalDNS setup docs](aws.md) for installation guidelines. -In ExternalDNS containers args, make sure to specify `annotation-filter` and `aws-zone-type`: +In ExternalDNS containers args, make sure to specify `aws-zone-type` and `ingress-class`: ```yaml -apiVersion: apps/v1beta2 +apiVersion: apps/v1 kind: Deployment metadata: labels: @@ -279,28 +279,27 @@ spec: - --provider=aws - --registry=txt - --txt-owner-id=dev.k8s.nexus - - --annotation-filter=kubernetes.io/ingress.class in (internal-ingress) + - --ingress-class=internal-ingress - --aws-zone-type=private - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 name: external-dns-private ``` ## Create application Service definitions -For this setup to work, you've to create two Service definitions for your application. +For this setup to work, you need to create two Ingress definitions for your application. -At first, create public Service definition: +At first, create a public Ingress definition: ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - annotations: - kubernetes.io/ingress.class: "external-ingress" labels: app: app name: app-public spec: + ingressClassName: external-ingress rules: - host: app.domain.com http: @@ -313,18 +312,17 @@ spec: pathType: Prefix ``` -Then create private Service definition: +Then create a private Ingress definition: ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - annotations: - kubernetes.io/ingress.class: "internal-ingress" labels: app: app name: app-private spec: + ingressClassName: internal-ingress rules: - host: app.domain.com http: @@ -347,12 +345,12 @@ metadata: certmanager.k8s.io/acme-challenge-type: "dns01" certmanager.k8s.io/acme-dns01-provider: "route53" certmanager.k8s.io/cluster-issuer: "letsencrypt-production" - kubernetes.io/ingress.class: "external-ingress" kubernetes.io/tls-acme: "true" labels: app: app name: app-public spec: + ingressClassName: "external-ingress" rules: - host: app.domain.com http: @@ -375,12 +373,11 @@ And reuse the requested certificate in private Service definition: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - annotations: - kubernetes.io/ingress.class: "internal-ingress" labels: app: app name: app-private spec: + ingressClassName: "internal-ingress" rules: - host: app.domain.com http: diff --git a/docs/tutorials/rcodezero.md b/docs/tutorials/rcodezero.md index 95a6115e2..72ee1b32f 100644 --- a/docs/tutorials/rcodezero.md +++ b/docs/tutorials/rcodezero.md @@ -53,7 +53,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -120,7 +120,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/rdns.md b/docs/tutorials/rdns.md index b6f9a3b89..62653a508 100644 --- a/docs/tutorials/rdns.md +++ b/docs/tutorials/rdns.md @@ -54,7 +54,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=ingress - --provider=rdns @@ -123,7 +123,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=ingress - --provider=rdns @@ -142,9 +142,8 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx - annotations: - kubernetes.io/ingress.class: "nginx" spec: + ingressClassName: nginx rules: - host: nginx.lb.rancher.cloud http: diff --git a/docs/tutorials/rfc2136.md b/docs/tutorials/rfc2136.md index 980df16af..144179c08 100644 --- a/docs/tutorials/rfc2136.md +++ b/docs/tutorials/rfc2136.md @@ -218,7 +218,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --registry=txt - --txt-prefix=external-dns- @@ -260,7 +260,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --registry=txt - --txt-prefix=external-dns- diff --git a/docs/tutorials/scaleway.md b/docs/tutorials/scaleway.md index ae903c6cb..2bd185530 100644 --- a/docs/tutorials/scaleway.md +++ b/docs/tutorials/scaleway.md @@ -53,7 +53,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -121,7 +121,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/docs/tutorials/security-context.md b/docs/tutorials/security-context.md index 785f38b0e..107033778 100644 --- a/docs/tutorials/security-context.md +++ b/docs/tutorials/security-context.md @@ -20,7 +20,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - ... # your arguments here securityContext: diff --git a/docs/tutorials/tencentcloud.md b/docs/tutorials/tencentcloud.md index 902ae87be..ea567f382 100644 --- a/docs/tutorials/tencentcloud.md +++ b/docs/tutorials/tencentcloud.md @@ -129,7 +129,7 @@ spec: - --policy=sync # set `upsert-only` would prevent ExternalDNS from deleting any records - --tencent-cloud-zone-type=private # only look at private hosted zones. set `public` to use the public dns service. - --tencent-cloud-config-file=/etc/kubernetes/tencent-cloud.json - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 imagePullPolicy: Always name: external-dns resources: {} diff --git a/docs/tutorials/transip.md b/docs/tutorials/transip.md index e9b445e7f..d2a7aa3f6 100644 --- a/docs/tutorials/transip.md +++ b/docs/tutorials/transip.md @@ -36,7 +36,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains @@ -107,7 +107,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains diff --git a/docs/tutorials/ultradns.md b/docs/tutorials/ultradns.md index c112185ab..50d98cf6c 100644 --- a/docs/tutorials/ultradns.md +++ b/docs/tutorials/ultradns.md @@ -44,7 +44,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress # ingress is also possible @@ -116,7 +116,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress diff --git a/docs/tutorials/vinyldns.md b/docs/tutorials/vinyldns.md index bf15795cc..1005b63c3 100644 --- a/docs/tutorials/vinyldns.md +++ b/docs/tutorials/vinyldns.md @@ -66,7 +66,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --provider=vinyldns - --source=service @@ -137,7 +137,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --provider=vinyldns - --source=service diff --git a/docs/tutorials/vultr.md b/docs/tutorials/vultr.md index 81fa6060a..0c491c0f6 100644 --- a/docs/tutorials/vultr.md +++ b/docs/tutorials/vultr.md @@ -42,7 +42,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. @@ -106,7 +106,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.2 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. diff --git a/endpoint/crypto.go b/endpoint/crypto.go new file mode 100644 index 000000000..1d6ebd1dd --- /dev/null +++ b/endpoint/crypto.go @@ -0,0 +1,134 @@ +/* +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 endpoint + +import ( + "bytes" + "compress/gzip" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + + log "github.com/sirupsen/logrus" +) + +// EncryptText gzip input data and encrypts it using the supplied AES key +func EncryptText(text string, aesKey []byte, nonceEncoded []byte) (string, error) { + block, err := aes.NewCipher(aesKey) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, gcm.NonceSize()) + if nonceEncoded == nil { + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + } else { + if _, err = base64.StdEncoding.Decode(nonce, nonceEncoded); err != nil { + return "", err + } + } + + data, err := compressData([]byte(text)) + if err != nil { + return "", err + } + + cipherData := gcm.Seal(nonce, nonce, data, nil) + return base64.StdEncoding.EncodeToString(cipherData), nil +} + +// DecryptText decrypt gziped data using a supplied AES encryption key ang ungzip it +// in case of decryption failed, will return original input and decryption error +func DecryptText(text string, aesKey []byte) (decryptResult string, encryptNonce string, err error) { + block, err := aes.NewCipher(aesKey) + if err != nil { + return "", "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", "", err + } + nonceSize := gcm.NonceSize() + data, err := base64.StdEncoding.DecodeString(text) + if err != nil { + return "", "", err + } + if len(data) <= nonceSize { + return "", "", fmt.Errorf("the encoded data from text %#v is shorter than %#v bytes and can't be decoded", text, nonceSize) + } + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaindata, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", "", err + } + plaindata, err = decompressData(plaindata) + if err != nil { + log.Debugf("Failed to decompress data based on the base64 encoded text %#v. Got error %#v.", text, err) + return "", "", err + } + + return string(plaindata), base64.StdEncoding.EncodeToString(nonce), nil +} + +// decompressData gzip compressed data +func decompressData(data []byte) (resData []byte, err error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, err + } + defer gz.Close() + var b bytes.Buffer + if _, err = b.ReadFrom(gz); err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +// compressData by gzip, for minify data stored in registry +func compressData(data []byte) (compressedData []byte, err error) { + var b bytes.Buffer + gz, err := gzip.NewWriterLevel(&b, gzip.BestCompression) + if err != nil { + return nil, err + } + + defer gz.Close() + if _, err = gz.Write(data); err != nil { + return nil, err + } + + if err = gz.Flush(); err != nil { + return nil, err + } + + if err = gz.Close(); err != nil { + return nil, err + } + + return b.Bytes(), nil +} diff --git a/endpoint/crypto_test.go b/endpoint/crypto_test.go new file mode 100644 index 000000000..880afcce3 --- /dev/null +++ b/endpoint/crypto_test.go @@ -0,0 +1,58 @@ +/* +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 endpoint + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEncrypt(t *testing.T) { + // Verify that text encryption and decryption works + aesKey := []byte("s%zF`.*'5`9.AhI2!B,.~hmbs^.*TL?;") + plaintext := "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." + encryptedtext, err := EncryptText(plaintext, aesKey, nil) + require.NoError(t, err) + decryptedtext, _, err := DecryptText(encryptedtext, aesKey) + require.NoError(t, err) + if plaintext != decryptedtext { + t.Errorf("Original plain text %#v differs from the resulting decrypted text %#v", plaintext, decryptedtext) + } + + // Verify that decrypt returns an error and empty data if wrong AES encryption key is used + decryptedtext, _, err = DecryptText(encryptedtext, []byte("s'J!jD`].LC?g&Oa11AgTub,j48ts/96")) + require.Error(t, err) + if decryptedtext != "" { + t.Error("Data decryption failed, empty string should be as result") + } + + // Verify that decrypt returns an error and empty data if unencrypted input is is supplied + decryptedtext, _, err = DecryptText(plaintext, aesKey) + require.Error(t, err) + if decryptedtext != "" { + t.Errorf("Data decryption failed, empty string should be as result") + } + + // Verify that a known encrypted text is decrypted to what is expected + encryptedtext = "0Mfzf6wsN8llrfX0ucDZ6nlc2+QiQfKKedjPPLu5atb2I35L9nUZeJcCnuLVW7CVW3K0h94vSuBLdXnMrj8Vcm0M09shxaoF48IcCpD03XtQbKXqk2hPbsW6+JybvplHIQGr16/PcjUSObGmR9yjf38+qEltApkKvrPjsyw43BX4eE10rL0Bln33UJD7/w+zazRDPFlAcbGtkt0ETKHnvyB3/aCddLipvrhjCXj2ZY/ktRF6h716kJRgXU10dCIQHFYU45MIdxI+k10HK3yZqhI2V0Gp2xjrFV/LRQ7/OS9SFee4asPWUYxbCEsnOzp8qc0dCPFSo1dtADzWnUZnsAcbnjtudT4milfLJc5CxDk1v3ykqQ/ajejwHjWQ7b8U6AsTErbezfdcqrb5IzkLgHb5TosnfrdDmNc9GcKfpsrCHbVY8KgNwMVdtwavLv7d9WM6sooUlZ3t0sABGkzagXQmPRvwLnkSOlie5XrnzWo8/8/4UByLga29CaXO" + decryptedtext, _, err = DecryptText(encryptedtext, aesKey) + require.NoError(t, err) + if decryptedtext != plaintext { + t.Error("Decryption of text didn't result in expected plaintext result.") + } +} diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 428a7961b..736f1e574 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -30,6 +30,8 @@ import ( const ( // RecordTypeA is a RecordType enum value RecordTypeA = "A" + // RecordTypeAAAA is a RecordType enum value + RecordTypeAAAA = "AAAA" // RecordTypeCNAME is a RecordType enum value RecordTypeCNAME = "CNAME" // RecordTypeTXT is a RecordType enum value @@ -40,6 +42,8 @@ const ( RecordTypeNS = "NS" // RecordTypePTR is a RecordType enum value RecordTypePTR = "PTR" + // RecordTypeMX is a RecordType enum value + RecordTypeMX = "MX" ) // TTL is a structure defining the TTL of a DNS record @@ -164,7 +168,7 @@ type Endpoint struct { DNSName string `json:"dnsName,omitempty"` // The targets the DNS record points to Targets Targets `json:"targets,omitempty"` - // RecordType type of record, e.g. CNAME, A, SRV, TXT etc + // RecordType type of record, e.g. CNAME, A, AAAA, SRV, TXT etc RecordType string `json:"recordType,omitempty"` // Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple') SetIdentifier string `json:"setIdentifier,omitempty"` diff --git a/endpoint/labels.go b/endpoint/labels.go index 797190db4..c65333dc6 100644 --- a/endpoint/labels.go +++ b/endpoint/labels.go @@ -17,6 +17,8 @@ limitations under the License. package endpoint import ( + log "github.com/sirupsen/logrus" + "errors" "fmt" "sort" @@ -41,6 +43,9 @@ const ( // DualstackLabelKey is the name of the label that identifies dualstack endpoints DualstackLabelKey = "dualstack" + + // txtEncryptionNonce label for keep same nonce for same txt records, for prevent different result of encryption for same txt record, it can cause issues for some providers + txtEncryptionNonce = "txt-encryption-nonce" ) // Labels store metadata related to the endpoint @@ -55,7 +60,7 @@ func NewLabels() Labels { // NewLabelsFromString constructs endpoints labels from a provided format string // if heritage set to another value is found then error is returned // no heritage automatically assumes is not owned by external-dns and returns invalidHeritage error -func NewLabelsFromString(labelText string) (Labels, error) { +func NewLabelsFromStringPlain(labelText string) (Labels, error) { endpointLabels := map[string]string{} labelText = strings.Trim(labelText, "\"") // drop quotes tokens := strings.Split(labelText, ",") @@ -85,9 +90,26 @@ func NewLabelsFromString(labelText string) (Labels, error) { return endpointLabels, nil } -// Serialize transforms endpoints labels into a external-dns recognizable format string +func NewLabelsFromString(labelText string, aesKey []byte) (Labels, error) { + if len(aesKey) != 0 { + decryptedText, encryptionNonce, err := DecryptText(strings.Trim(labelText, "\""), aesKey) + //in case if we have decryption error, just try process original text + //decryption errors should be ignored here, because we can already have plain-text labels in registry + if err == nil { + labels, err := NewLabelsFromStringPlain(decryptedText) + if err == nil { + labels[txtEncryptionNonce] = encryptionNonce + } + + return labels, err + } + } + return NewLabelsFromStringPlain(labelText) +} + +// SerializePlain transforms endpoints labels into a external-dns recognizable format string // withQuotes adds additional quotes -func (l Labels) Serialize(withQuotes bool) string { +func (l Labels) SerializePlain(withQuotes bool) string { var tokens []string tokens = append(tokens, fmt.Sprintf("heritage=%s", heritage)) var keys []string @@ -104,3 +126,31 @@ func (l Labels) Serialize(withQuotes bool) string { } return strings.Join(tokens, ",") } + +// Serialize same to SerializePlain, but encrypt data, if encryption enabled +func (l Labels) Serialize(withQuotes bool, txtEncryptEnabled bool, aesKey []byte) string { + if !txtEncryptEnabled { + return l.SerializePlain(withQuotes) + } + + var encryptionNonce []byte = nil + if extractedNonce, nonceExists := l[txtEncryptionNonce]; nonceExists { + encryptionNonce = []byte(extractedNonce) + delete(l, txtEncryptionNonce) + } + + text := l.SerializePlain(false) + log.Debugf("Encrypt the serialized text %#v before returning it.", text) + var err error + text, err = EncryptText(text, aesKey, encryptionNonce) + + if err != nil { + log.Fatalf("Failed to encrypt the text %#v using the encryption key %#v. Got error %#v.", text, aesKey, err) + } + + if withQuotes { + text = fmt.Sprintf("\"%s\"", text) + } + log.Debugf("Serialized text after encryption is %#v.", text) + return text +} diff --git a/endpoint/labels_test.go b/endpoint/labels_test.go index 9386a23e6..394635b71 100644 --- a/endpoint/labels_test.go +++ b/endpoint/labels_test.go @@ -25,14 +25,18 @@ import ( type LabelsSuite struct { suite.Suite - foo Labels - fooAsText string - fooAsTextWithQuotes string - barText string - barTextAsMap Labels - noHeritageText string - wrongHeritageText string - multipleHeritageText string // considered invalid + aesKey []byte + foo Labels + fooAsText string + fooAsTextWithQuotes string + fooAsTextEncrypted string + fooAsTextWithQuotesEncrypted string + barText string + barTextEncrypted string + barTextAsMap Labels + noHeritageText string + wrongHeritageText string + multipleHeritageText string // considered invalid } func (suite *LabelsSuite) SetupTest() { @@ -40,48 +44,79 @@ func (suite *LabelsSuite) SetupTest() { "owner": "foo-owner", "resource": "foo-resource", } + suite.aesKey = []byte(")K_Fy|?Z.64#UuHm`}[d!GC%WJM_fs{_") suite.fooAsText = "heritage=external-dns,external-dns/owner=foo-owner,external-dns/resource=foo-resource" suite.fooAsTextWithQuotes = fmt.Sprintf(`"%s"`, suite.fooAsText) - + suite.fooAsTextEncrypted = `+lvP8q9KHJ6BS6O81i2Q6DLNdf2JSKy8j/gbZKviTZlGYj7q+yDoYMgkQ1hPn6urtGllM5bfFMcaaHto52otQtiOYrX8990J3kQqg4s47m3bH3Ejl8RSxSSuWJM3HJtPghQzYg0/LSOsdQ0=` + suite.fooAsTextWithQuotesEncrypted = fmt.Sprintf(`"%s"`, suite.fooAsTextEncrypted) suite.barTextAsMap = map[string]string{ "owner": "bar-owner", "resource": "bar-resource", "new-key": "bar-new-key", } suite.barText = "heritage=external-dns,,external-dns/owner=bar-owner,external-dns/resource=bar-resource,external-dns/new-key=bar-new-key,random=stuff,no-equal-sign,," // also has some random gibberish - + suite.barTextEncrypted = "yi6vVATlgYN0enXBIupVK2atNUKtajofWMroWtvZjUanFZXlWvqjJPpjmMd91kv86bZj+syQEP0uR3TK6eFVV7oKFh/NxYyh238FjZ+25zlXW9TgbLoMalUNOkhKFdfXkLeeaqJjePB59t+kQBYX+ZEryK652asPs6M+xTIvtg07N7WWZ6SjJujm0RRISg==" suite.noHeritageText = "external-dns/owner=random-owner" suite.wrongHeritageText = "heritage=mate,external-dns/owner=random-owner" suite.multipleHeritageText = "heritage=mate,heritage=external-dns,external-dns/owner=random-owner" } func (suite *LabelsSuite) TestSerialize() { - suite.Equal(suite.fooAsText, suite.foo.Serialize(false), "should serializeLabel") - suite.Equal(suite.fooAsTextWithQuotes, suite.foo.Serialize(true), "should serializeLabel") + suite.Equal(suite.fooAsText, suite.foo.SerializePlain(false), "should serializeLabel") + suite.Equal(suite.fooAsTextWithQuotes, suite.foo.SerializePlain(true), "should serializeLabel") + suite.Equal(suite.fooAsText, suite.foo.Serialize(false, false, nil), "should serializeLabel") + suite.Equal(suite.fooAsTextWithQuotes, suite.foo.Serialize(true, false, nil), "should serializeLabel") + suite.Equal(suite.fooAsText, suite.foo.Serialize(false, false, suite.aesKey), "should serializeLabel") + suite.Equal(suite.fooAsTextWithQuotes, suite.foo.Serialize(true, false, suite.aesKey), "should serializeLabel") + suite.NotEqual(suite.fooAsText, suite.foo.Serialize(false, true, suite.aesKey), "should serializeLabel and encrypt") + suite.NotEqual(suite.fooAsTextWithQuotes, suite.foo.Serialize(true, true, suite.aesKey), "should serializeLabel and encrypt") +} + +func (suite *LabelsSuite) TestEncryptionNonceReUsage() { + foo, err := NewLabelsFromString(suite.fooAsTextEncrypted, suite.aesKey) + suite.NoError(err, "should succeed for valid label text") + serialized := foo.Serialize(false, true, suite.aesKey) + suite.Equal(serialized, suite.fooAsTextEncrypted, "serialized result should be equal") } func (suite *LabelsSuite) TestDeserialize() { - foo, err := NewLabelsFromString(suite.fooAsText) + foo, err := NewLabelsFromStringPlain(suite.fooAsText) suite.NoError(err, "should succeed for valid label text") suite.Equal(suite.foo, foo, "should reconstruct original label map") - foo, err = NewLabelsFromString(suite.fooAsTextWithQuotes) + foo, err = NewLabelsFromStringPlain(suite.fooAsTextWithQuotes) suite.NoError(err, "should succeed for valid label text") suite.Equal(suite.foo, foo, "should reconstruct original label map") - bar, err := NewLabelsFromString(suite.barText) + foo, err = NewLabelsFromString(suite.fooAsTextEncrypted, suite.aesKey) + suite.NoError(err, "should succeed for valid encrypted label text") + for key, val := range suite.foo { + suite.Equal(val, foo[key], "should contains all keys from original label map") + } + + foo, err = NewLabelsFromString(suite.fooAsTextWithQuotesEncrypted, suite.aesKey) + suite.NoError(err, "should succeed for valid encrypted label text") + for key, val := range suite.foo { + suite.Equal(val, foo[key], "should contains all keys from original label map") + } + + bar, err := NewLabelsFromStringPlain(suite.barText) suite.NoError(err, "should succeed for valid label text") suite.Equal(suite.barTextAsMap, bar, "should reconstruct original label map") - noHeritage, err := NewLabelsFromString(suite.noHeritageText) + bar, err = NewLabelsFromString(suite.barText, suite.aesKey) + suite.NoError(err, "should succeed for valid encrypted label text") + suite.Equal(suite.barTextAsMap, bar, "should reconstruct original label map") + + noHeritage, err := NewLabelsFromStringPlain(suite.noHeritageText) suite.Equal(ErrInvalidHeritage, err, "should fail if no heritage is found") suite.Nil(noHeritage, "should return nil") - wrongHeritage, err := NewLabelsFromString(suite.wrongHeritageText) + wrongHeritage, err := NewLabelsFromStringPlain(suite.wrongHeritageText) suite.Equal(ErrInvalidHeritage, err, "should fail if wrong heritage is found") suite.Nil(wrongHeritage, "if error should return nil") - multipleHeritage, err := NewLabelsFromString(suite.multipleHeritageText) + multipleHeritage, err := NewLabelsFromStringPlain(suite.multipleHeritageText) suite.Equal(ErrInvalidHeritage, err, "should fail if multiple heritage is found") suite.Nil(multipleHeritage, "if error should return nil") } diff --git a/go.mod b/go.mod index de66742d3..d7b624ae4 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/onsi/ginkgo v1.16.5 github.com/openshift/api v0.0.0-20210315202829-4b79815405ec github.com/openshift/client-go v0.0.0-20210112165513-ebc401615f47 - github.com/oracle/oci-go-sdk v24.3.0+incompatible + github.com/oracle/oci-go-sdk/v65 v65.35.0 github.com/ovh/go-ovh v1.1.0 github.com/pkg/errors v0.9.1 github.com/pluralsh/gqlclient v1.1.6 @@ -58,8 +58,8 @@ require ( github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556 github.com/vultr/govultr/v2 v2.17.2 - go.etcd.io/etcd/api/v3 v3.5.5 - go.etcd.io/etcd/client/v3 v3.5.5 + go.etcd.io/etcd/api/v3 v3.5.8 + go.etcd.io/etcd/client/v3 v3.5.8 go.uber.org/ratelimit v0.2.0 golang.org/x/net v0.7.0 golang.org/x/oauth2 v0.5.0 @@ -113,6 +113,7 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect github.com/go-stack/stack v1.8.0 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect @@ -164,11 +165,12 @@ require ( github.com/schollz/progressbar/v3 v3.8.6 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/smartystreets/gunit v1.3.4 // indirect + github.com/sony/gobreaker v0.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/terra-farm/udnssdk v1.3.5 // indirect github.com/vektah/gqlparser/v2 v2.5.0 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.5 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.8 // indirect go.mongodb.org/mongo-driver v1.5.1 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.9.0 // indirect @@ -176,7 +178,7 @@ require ( go.uber.org/zap v1.19.1 // indirect golang.org/x/crypto v0.5.0 // indirect golang.org/x/mod v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/sys v0.6.0 // indirect golang.org/x/term v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect diff --git a/go.sum b/go.sum index 8786ee822..69d3d71f0 100644 --- a/go.sum +++ b/go.sum @@ -153,7 +153,6 @@ github.com/ans-group/go-durationstring v1.2.0 h1:UJIuQATkp0t1rBvZsHRwki33YHV9E+U github.com/ans-group/go-durationstring v1.2.0/go.mod h1:QGF9Mdpq9058QXaut8r55QWu6lcHX6i/GvF1PZVkV6o= github.com/ans-group/sdk-go v1.10.4 h1:wZzojt99wtVIEHs8zNQzp1Xhqme5tD5NqMM1VLmG6xQ= github.com/ans-group/sdk-go v1.10.4/go.mod h1:XSKXEDfKobnDtZoyia5DhJxxaDMcCjr76e1KJ9dU/xc= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aokoli/goutils v1.1.0/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -218,8 +217,6 @@ github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/cncf/udpa v0.0.0-20200324003616-bae28a880fdb/go.mod h1:HNVadOiXCy7Jk3R2knJ+qm++zkncJxxBMpjdGgJ+UJc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200324003616-bae28a880fdb/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= @@ -314,9 +311,6 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java.0.20200609174644-bd816e4522c1/go.mod h1:bjmEhrMDubXDd0uKxnWwRmgSsiEv2CkJliIHnj6ETm8= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -484,6 +478,8 @@ github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= @@ -623,7 +619,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= @@ -950,8 +945,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU= -github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= +github.com/oracle/oci-go-sdk/v65 v65.35.0 h1:zvDsEuGs0qf6hPZVbrDnnfPJYQP7CwAgidTr4Pch6E4= +github.com/oracle/oci-go-sdk/v65 v65.35.0/go.mod h1:MXMLMzHnnd9wlpgadPkdlkZ9YrwQmCOmbX5kjVEJodw= github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk= github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= @@ -995,7 +990,6 @@ github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeD github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= @@ -1039,7 +1033,6 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1089,6 +1082,8 @@ github.com/smartystreets/gunit v1.3.4 h1:iHc8Rfhb/uCOc9a3KGuD3ut22L+hLIVaqR1o5fS github.com/smartystreets/gunit v1.3.4/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+zMHZXV9/bvak= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -1201,12 +1196,12 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.etcd.io/etcd/api/v3 v3.5.5 h1:BX4JIbQ7hl7+jL+g+2j5UAr0o1bctCm6/Ct+ArBGkf0= -go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8= -go.etcd.io/etcd/client/pkg/v3 v3.5.5 h1:9S0JUVvmrVl7wCF39iTQthdaaNIiAaQbmK75ogO6GU8= -go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ= -go.etcd.io/etcd/client/v3 v3.5.5 h1:q++2WTJbUgpQu4B6hCuT7VkdwaTP7Qz6Daak3WzbrlI= -go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c= +go.etcd.io/etcd/api/v3 v3.5.8 h1:Zf44zJszoU7zRV0X/nStPenegNXoFDWcB/MwrJbA+L4= +go.etcd.io/etcd/api/v3 v3.5.8/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= +go.etcd.io/etcd/client/pkg/v3 v3.5.8 h1:tPp9YRn/UBFAHdhOQUII9eUs7aOK35eulpMhX4YBd+M= +go.etcd.io/etcd/client/pkg/v3 v3.5.8/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= +go.etcd.io/etcd/client/v3 v3.5.8 h1:B6ngTKZSWWowHEoaucOKHQR/AtZKaoHLiUpWxOLG4l4= +go.etcd.io/etcd/client/v3 v3.5.8/go.mod h1:idZYIPVkttBJBiRigkB5EM0MmEyx8jcl18zCV3F5noc= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= @@ -1221,7 +1216,6 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -1239,7 +1233,6 @@ go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6m go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1298,7 +1291,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -1467,7 +1459,6 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1487,8 +1478,8 @@ golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1587,7 +1578,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= @@ -1651,7 +1641,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -1660,7 +1649,6 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio= google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -1682,11 +1670,7 @@ google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -1745,7 +1729,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/kustomize/kustomization.yaml b/kustomize/kustomization.yaml index 03d30833d..5a6cb4b7e 100644 --- a/kustomize/kustomization.yaml +++ b/kustomize/kustomization.yaml @@ -3,7 +3,7 @@ kind: Kustomization images: - name: registry.k8s.io/external-dns/external-dns - newTag: v0.13.2 + newTag: v0.13.5 resources: - ./external-dns-deployment.yaml diff --git a/main.go b/main.go index ece1b0290..f8cd7896f 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ package main import ( "context" + "fmt" "net/http" "os" "os/signal" @@ -113,6 +114,7 @@ func main() { Namespace: cfg.Namespace, AnnotationFilter: cfg.AnnotationFilter, LabelFilter: labelSelector, + IngressClassNames: cfg.IngressClassNames, FQDNTemplate: cfg.FQDNTemplate, CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation, IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation, @@ -139,6 +141,8 @@ func main() { RequestTimeout: cfg.RequestTimeout, DefaultTargets: cfg.DefaultTargets, OCPRouterName: cfg.OCPRouterName, + UpdateEvents: cfg.UpdateEvents, + ResolveLoadBalancerHostname: cfg.ResolveServiceLoadBalancerHostname, } // Lookup all the selected sources by names and pass them the desired configuration. @@ -312,7 +316,19 @@ func main() { ) case "oci": var config *oci.OCIConfig - config, err = oci.LoadOCIConfig(cfg.OCIConfigFile) + // if the instance-principals flag was set, and a compartment OCID was provided, then ignore the + // OCI config file, and provide a config that uses instance principal authentication. + if cfg.OCIAuthInstancePrincipal { + if len(cfg.OCICompartmentOCID) == 0 { + err = fmt.Errorf("instance principal authentication requested, but no compartment OCID provided") + } else { + authConfig := oci.OCIAuthConfig{UseInstancePrincipal: true} + config = &oci.OCIConfig{Auth: authConfig, CompartmentID: cfg.OCICompartmentOCID} + } + } else { + config, err = oci.LoadOCIConfig(cfg.OCIConfigFile) + } + if err == nil { p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun) } @@ -367,7 +383,7 @@ func main() { case "noop": r, err = registry.NewNoopRegistry(p) case "txt": - r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes) + r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey)) case "aws-sd": r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID) default: diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 688dc0e98..253638e3a 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -43,162 +43,168 @@ var Version = "unknown" // Config is a project-wide configuration type Config struct { - APIServerURL string - KubeConfig string - RequestTimeout time.Duration - DefaultTargets []string - ContourLoadBalancerService string - GlooNamespace string - SkipperRouteGroupVersion string - Sources []string - Namespace string - AnnotationFilter string - LabelFilter string - FQDNTemplate string - CombineFQDNAndAnnotation bool - IgnoreHostnameAnnotation bool - IgnoreIngressTLSSpec bool - IgnoreIngressRulesSpec bool - GatewayNamespace string - GatewayLabelFilter string - Compatibility string - PublishInternal bool - PublishHostIP bool - AlwaysPublishNotReadyAddresses bool - ConnectorSourceServer string - Provider string - GoogleProject string - GoogleBatchChangeSize int - GoogleBatchChangeInterval time.Duration - GoogleZoneVisibility string - DomainFilter []string - ExcludeDomains []string - RegexDomainFilter *regexp.Regexp - RegexDomainExclusion *regexp.Regexp - ZoneNameFilter []string - ZoneIDFilter []string - TargetNetFilter []string - ExcludeTargetNets []string - AlibabaCloudConfigFile string - AlibabaCloudZoneType string - AWSZoneType string - AWSZoneTagFilter []string - AWSAssumeRole string - AWSAssumeRoleExternalID string - AWSBatchChangeSize int - AWSBatchChangeInterval time.Duration - AWSEvaluateTargetHealth bool - AWSAPIRetries int - AWSPreferCNAME bool - AWSZoneCacheDuration time.Duration - AWSSDServiceCleanup bool - AzureConfigFile string - AzureResourceGroup string - AzureSubscriptionID string - AzureUserAssignedIdentityClientID string - BluecatDNSConfiguration string - BluecatConfigFile string - BluecatDNSView string - BluecatGatewayHost string - BluecatRootZone string - BluecatDNSServerName string - BluecatDNSDeployType string - BluecatSkipTLSVerify bool - CloudflareProxied bool - CloudflareDNSRecordsPerPage int - CoreDNSPrefix string - RcodezeroTXTEncrypt bool - AkamaiServiceConsumerDomain string - AkamaiClientToken string - AkamaiClientSecret string - AkamaiAccessToken string - AkamaiEdgercPath string - AkamaiEdgercSection string - InfobloxGridHost string - InfobloxWapiPort int - InfobloxWapiUsername string - InfobloxWapiPassword string `secure:"yes"` - InfobloxWapiVersion string - InfobloxSSLVerify bool - InfobloxView string - InfobloxMaxResults int - InfobloxFQDNRegEx string - InfobloxNameRegEx string - InfobloxCreatePTR bool - InfobloxCacheDuration int - DynCustomerName string - DynUsername string - DynPassword string `secure:"yes"` - DynMinTTLSeconds int - OCIConfigFile string - InMemoryZones []string - OVHEndpoint string - OVHApiRateLimit int - PDNSServer string - PDNSAPIKey string `secure:"yes"` - PDNSTLSEnabled bool - TLSCA string - TLSClientCert string - TLSClientCertKey string - Policy string - Registry string - TXTOwnerID string - TXTPrefix string - TXTSuffix string - Interval time.Duration - MinEventSyncInterval time.Duration - Once bool - DryRun bool - UpdateEvents bool - LogFormat string - MetricsAddress string - LogLevel string - TXTCacheInterval time.Duration - TXTWildcardReplacement string - ExoscaleEndpoint string - ExoscaleAPIKey string `secure:"yes"` - ExoscaleAPISecret string `secure:"yes"` - CRDSourceAPIVersion string - CRDSourceKind string - ServiceTypeFilter []string - CFAPIEndpoint string - CFUsername string - CFPassword string - RFC2136Host string - RFC2136Port int - RFC2136Zone string - RFC2136Insecure bool - RFC2136GSSTSIG bool - RFC2136KerberosRealm string - RFC2136KerberosUsername string - RFC2136KerberosPassword string `secure:"yes"` - RFC2136TSIGKeyName string - RFC2136TSIGSecret string `secure:"yes"` - RFC2136TSIGSecretAlg string - RFC2136TAXFR bool - RFC2136MinTTL time.Duration - RFC2136BatchChangeSize int - NS1Endpoint string - NS1IgnoreSSL bool - NS1MinTTLSeconds int - TransIPAccountName string - TransIPPrivateKeyFile string - DigitalOceanAPIPageSize int - ManagedDNSRecordTypes []string - GoDaddyAPIKey string `secure:"yes"` - GoDaddySecretKey string `secure:"yes"` - GoDaddyTTL int64 - GoDaddyOTE bool - OCPRouterName string - IBMCloudProxied bool - IBMCloudConfigFile string - TencentCloudConfigFile string - TencentCloudZoneType string - PiholeServer string - PiholePassword string `secure:"yes"` - PiholeTLSInsecureSkipVerify bool - PluralCluster string - PluralProvider string + APIServerURL string + KubeConfig string + RequestTimeout time.Duration + DefaultTargets []string + ContourLoadBalancerService string + GlooNamespace string + SkipperRouteGroupVersion string + Sources []string + Namespace string + AnnotationFilter string + LabelFilter string + IngressClassNames []string + FQDNTemplate string + CombineFQDNAndAnnotation bool + IgnoreHostnameAnnotation bool + IgnoreIngressTLSSpec bool + IgnoreIngressRulesSpec bool + GatewayNamespace string + GatewayLabelFilter string + Compatibility string + PublishInternal bool + PublishHostIP bool + AlwaysPublishNotReadyAddresses bool + ConnectorSourceServer string + Provider string + GoogleProject string + GoogleBatchChangeSize int + GoogleBatchChangeInterval time.Duration + GoogleZoneVisibility string + DomainFilter []string + ExcludeDomains []string + RegexDomainFilter *regexp.Regexp + RegexDomainExclusion *regexp.Regexp + ZoneNameFilter []string + ZoneIDFilter []string + TargetNetFilter []string + ExcludeTargetNets []string + AlibabaCloudConfigFile string + AlibabaCloudZoneType string + AWSZoneType string + AWSZoneTagFilter []string + AWSAssumeRole string + AWSAssumeRoleExternalID string + AWSBatchChangeSize int + AWSBatchChangeInterval time.Duration + AWSEvaluateTargetHealth bool + AWSAPIRetries int + AWSPreferCNAME bool + AWSZoneCacheDuration time.Duration + AWSSDServiceCleanup bool + AzureConfigFile string + AzureResourceGroup string + AzureSubscriptionID string + AzureUserAssignedIdentityClientID string + BluecatDNSConfiguration string + BluecatConfigFile string + BluecatDNSView string + BluecatGatewayHost string + BluecatRootZone string + BluecatDNSServerName string + BluecatDNSDeployType string + BluecatSkipTLSVerify bool + CloudflareProxied bool + CloudflareDNSRecordsPerPage int + CoreDNSPrefix string + RcodezeroTXTEncrypt bool + AkamaiServiceConsumerDomain string + AkamaiClientToken string + AkamaiClientSecret string + AkamaiAccessToken string + AkamaiEdgercPath string + AkamaiEdgercSection string + InfobloxGridHost string + InfobloxWapiPort int + InfobloxWapiUsername string + InfobloxWapiPassword string `secure:"yes"` + InfobloxWapiVersion string + InfobloxSSLVerify bool + InfobloxView string + InfobloxMaxResults int + InfobloxFQDNRegEx string + InfobloxNameRegEx string + InfobloxCreatePTR bool + InfobloxCacheDuration int + DynCustomerName string + DynUsername string + DynPassword string `secure:"yes"` + DynMinTTLSeconds int + OCIConfigFile string + OCICompartmentOCID string + OCIAuthInstancePrincipal bool + InMemoryZones []string + OVHEndpoint string + OVHApiRateLimit int + PDNSServer string + PDNSAPIKey string `secure:"yes"` + PDNSTLSEnabled bool + TLSCA string + TLSClientCert string + TLSClientCertKey string + Policy string + Registry string + TXTOwnerID string + TXTPrefix string + TXTSuffix string + TXTEncryptEnabled bool + TXTEncryptAESKey string + Interval time.Duration + MinEventSyncInterval time.Duration + Once bool + DryRun bool + UpdateEvents bool + LogFormat string + MetricsAddress string + LogLevel string + TXTCacheInterval time.Duration + TXTWildcardReplacement string + ExoscaleEndpoint string + ExoscaleAPIKey string `secure:"yes"` + ExoscaleAPISecret string `secure:"yes"` + CRDSourceAPIVersion string + CRDSourceKind string + ServiceTypeFilter []string + CFAPIEndpoint string + CFUsername string + CFPassword string + ResolveServiceLoadBalancerHostname bool + RFC2136Host string + RFC2136Port int + RFC2136Zone string + RFC2136Insecure bool + RFC2136GSSTSIG bool + RFC2136KerberosRealm string + RFC2136KerberosUsername string + RFC2136KerberosPassword string `secure:"yes"` + RFC2136TSIGKeyName string + RFC2136TSIGSecret string `secure:"yes"` + RFC2136TSIGSecretAlg string + RFC2136TAXFR bool + RFC2136MinTTL time.Duration + RFC2136BatchChangeSize int + NS1Endpoint string + NS1IgnoreSSL bool + NS1MinTTLSeconds int + TransIPAccountName string + TransIPPrivateKeyFile string + DigitalOceanAPIPageSize int + ManagedDNSRecordTypes []string + GoDaddyAPIKey string `secure:"yes"` + GoDaddySecretKey string `secure:"yes"` + GoDaddyTTL int64 + GoDaddyOTE bool + OCPRouterName string + IBMCloudProxied bool + IBMCloudConfigFile string + TencentCloudConfigFile string + TencentCloudZoneType string + PiholeServer string + PiholePassword string `secure:"yes"` + PiholeTLSInsecureSkipVerify bool + PluralCluster string + PluralProvider string } var defaultConfig = &Config{ @@ -213,6 +219,7 @@ var defaultConfig = &Config{ Namespace: "", AnnotationFilter: "", LabelFilter: labels.Everything().String(), + IngressClassNames: nil, FQDNTemplate: "", CombineFQDNAndAnnotation: false, IgnoreHostnameAnnotation: false, @@ -292,6 +299,8 @@ var defaultConfig = &Config{ TXTCacheInterval: 0, TXTWildcardReplacement: "", MinEventSyncInterval: 5 * time.Second, + TXTEncryptEnabled: false, + TXTEncryptAESKey: "", Interval: time.Minute, Once: false, DryRun: false, @@ -327,7 +336,7 @@ var defaultConfig = &Config{ TransIPAccountName: "", TransIPPrivateKeyFile: "", DigitalOceanAPIPageSize: 50, - ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, + ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, GoDaddyAPIKey: "", GoDaddySecretKey: "", GoDaddyTTL: 600, @@ -388,6 +397,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("server", "The Kubernetes API server to connect to (default: auto-detect)").Default(defaultConfig.APIServerURL).StringVar(&cfg.APIServerURL) app.Flag("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)").Default(defaultConfig.KubeConfig).StringVar(&cfg.KubeConfig) app.Flag("request-timeout", "Request timeout when calling Kubernetes APIs. 0s means no timeout").Default(defaultConfig.RequestTimeout.String()).DurationVar(&cfg.RequestTimeout) + app.Flag("resolve-service-load-balancer-hostname", "Resolve the hostname of LoadBalancer-type Service object to IP addresses in order to create DNS A/AAAA records instead of CNAMEs").BoolVar(&cfg.ResolveServiceLoadBalancerHostname) // Flags related to cloud foundry app.Flag("cf-api-endpoint", "The fully-qualified domain name of the cloud foundry instance you are targeting").Default(defaultConfig.CFAPIEndpoint).StringVar(&cfg.CFAPIEndpoint) @@ -409,6 +419,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace) app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter) app.Flag("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently supported by source types CRD, ingress, service and openshift-route").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter) + app.Flag("ingress-class", "Require an ingress to have this class name (defaults to any class; specify multiple times to allow more than one class)").StringsVar(&cfg.IngressClassNames) app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate) app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation) app.Flag("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when using fqdn-template is set (optional, default: false)").BoolVar(&cfg.IgnoreHostnameAnnotation) @@ -424,7 +435,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion) app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind) app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter) - app.Flag("managed-record-types", "Comma separated list of record types to manage (default: A, CNAME) (supported records: CNAME, A, NS").Default("A", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes) + app.Flag("managed-record-types", "Record types to manage; specify multiple times to include many; (default: A, AAAA, CNAME) (supported records: CNAME, A, AAAA, NS").Default("A", "AAAA", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes) app.Flag("default-targets", "Set globally default IP address that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets) app.Flag("target-net-filter", "Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.TargetNetFilter) app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets) @@ -498,6 +509,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("dyn-password", "When using the Dyn provider, specify the password").Default("").StringVar(&cfg.DynPassword) app.Flag("dyn-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.DynMinTTLSeconds) app.Flag("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile) + app.Flag("oci-compartment-ocid", "When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records. Required when using OCI IAM instance principal authentication.").StringVar(&cfg.OCICompartmentOCID) + app.Flag("oci-auth-instance-principal", "When using the OCI provider, specify whether OCI IAM instance principal authentication should be used (instead of key-based auth via the OCI config file).").Default(strconv.FormatBool(defaultConfig.OCIAuthInstancePrincipal)).BoolVar(&cfg.OCIAuthInstancePrincipal) app.Flag("rcodezero-txt-encrypt", "When using the Rcodezero provider with txt registry option, set if TXT rrs are encrypted (default: false)").Default(strconv.FormatBool(defaultConfig.RcodezeroTXTEncrypt)).BoolVar(&cfg.RcodezeroTXTEncrypt) app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones) app.Flag("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)").Default(defaultConfig.OVHEndpoint).StringVar(&cfg.OVHEndpoint) @@ -564,6 +577,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Could contain record type template like '%{record_type}-prefix-'. 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). Could contain record type template like '-%{record_type}-suffix'. 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) + app.Flag("txt-encrypt-enabled", "When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled)").BoolVar(&cfg.TXTEncryptEnabled) + app.Flag("txt-encrypt-aes-key", "When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true)").Default(defaultConfig.TXTEncryptAESKey).StringVar(&cfg.TXTEncryptAESKey) // 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) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 87647ee21..d9c68480c 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -122,7 +122,7 @@ var ( TransIPAccountName: "", TransIPPrivateKeyFile: "", DigitalOceanAPIPageSize: 50, - ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, + ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, RFC2136BatchChangeSize: 50, OCPRouterName: "default", IBMCloudProxied: false, @@ -233,7 +233,7 @@ var ( TransIPAccountName: "transip", TransIPPrivateKeyFile: "/path/to/transip.key", DigitalOceanAPIPageSize: 100, - ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, + ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, RFC2136BatchChangeSize: 100, IBMCloudProxied: true, IBMCloudConfigFile: "ibmcloud.json", @@ -372,6 +372,7 @@ func TestParseFlags(t *testing.T) { "--transip-keyfile=/path/to/transip.key", "--digitalocean-api-page-size=100", "--managed-record-types=A", + "--managed-record-types=AAAA", "--managed-record-types=CNAME", "--managed-record-types=NS", "--rfc2136-batch-change-size=100", @@ -488,7 +489,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip", "EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key", "EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100", - "EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nCNAME\nNS", + "EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nAAAA\nCNAME\nNS", "EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100", "EXTERNAL_DNS_IBMCLOUD_PROXIED": "1", "EXTERNAL_DNS_IBMCLOUD_CONFIG_FILE": "ibmcloud.json", diff --git a/plan/plan.go b/plan/plan.go index d7bac7090..1e8a38f1e 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -64,8 +64,15 @@ type Changes struct { Delete []*endpoint.Endpoint } +// planKey is a key for a row in `planTable`. +type planKey struct { + dnsName string + setIdentifier string + recordType string +} + // planTable is a supplementary struct for Plan -// each row correspond to a dnsName -> (current record + all desired records) +// each row correspond to a planKey -> (current record + all desired records) /* planTable: (-> = target) -------------------------------------------------------- @@ -78,12 +85,12 @@ bar.com | | [->191.1.1.1, ->190.1.1.1] | = create (bar.com -> 1 "=", i.e. result of calculation relies on supplied ConflictResolver */ type planTable struct { - rows map[string]map[string]*planTableRow + rows map[planKey]*planTableRow resolver ConflictResolver } func newPlanTable() planTable { // TODO: make resolver configurable - return planTable{map[string]map[string]*planTableRow{}, PerResource{}} + return planTable{map[planKey]*planTableRow{}, PerResource{}} } // planTableRow @@ -99,25 +106,25 @@ func (t planTableRow) String() string { } func (t planTable) addCurrent(e *endpoint.Endpoint) { - dnsName := normalizeDNSName(e.DNSName) - if _, ok := t.rows[dnsName]; !ok { - t.rows[dnsName] = make(map[string]*planTableRow) - } - if _, ok := t.rows[dnsName][e.SetIdentifier]; !ok { - t.rows[dnsName][e.SetIdentifier] = &planTableRow{} - } - t.rows[dnsName][e.SetIdentifier].current = e + key := t.newPlanKey(e) + t.rows[key].current = e } func (t planTable) addCandidate(e *endpoint.Endpoint) { - dnsName := normalizeDNSName(e.DNSName) - if _, ok := t.rows[dnsName]; !ok { - t.rows[dnsName] = make(map[string]*planTableRow) + key := t.newPlanKey(e) + t.rows[key].candidates = append(t.rows[key].candidates, e) +} + +func (t *planTable) newPlanKey(e *endpoint.Endpoint) planKey { + key := planKey{ + dnsName: normalizeDNSName(e.DNSName), + setIdentifier: e.SetIdentifier, + recordType: e.RecordType, } - if _, ok := t.rows[dnsName][e.SetIdentifier]; !ok { - t.rows[dnsName][e.SetIdentifier] = &planTableRow{} + if _, ok := t.rows[key]; !ok { + t.rows[key] = &planTableRow{} } - t.rows[dnsName][e.SetIdentifier].candidates = append(t.rows[dnsName][e.SetIdentifier].candidates, e) + return key } func (c *Changes) HasChanges() bool { @@ -146,26 +153,24 @@ func (p *Plan) Calculate() *Plan { changes := &Changes{} - for _, topRow := range t.rows { - for _, row := range topRow { - if row.current == nil { // dns name not taken - changes.Create = append(changes.Create, t.resolver.ResolveCreate(row.candidates)) - } - if row.current != nil && len(row.candidates) == 0 { - changes.Delete = append(changes.Delete, row.current) - } + for _, row := range t.rows { + if row.current == nil { // dns name not taken + changes.Create = append(changes.Create, t.resolver.ResolveCreate(row.candidates)) + } + if row.current != nil && len(row.candidates) == 0 { + changes.Delete = append(changes.Delete, row.current) + } - // TODO: allows record type change, which might not be supported by all dns providers - if row.current != nil && len(row.candidates) > 0 { // dns name is taken - update := t.resolver.ResolveUpdate(row.current, row.candidates) - // compare "update" to "current" to figure out if actual update is required - if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || p.shouldUpdateProviderSpecific(update, row.current) { - inheritOwner(row.current, update) - changes.UpdateNew = append(changes.UpdateNew, update) - changes.UpdateOld = append(changes.UpdateOld, row.current) - } - continue + // TODO: allows record type change, which might not be supported by all dns providers + if row.current != nil && len(row.candidates) > 0 { // dns name is taken + update := t.resolver.ResolveUpdate(row.current, row.candidates) + // compare "update" to "current" to figure out if actual update is required + if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || p.shouldUpdateProviderSpecific(update, row.current) { + inheritOwner(row.current, update) + changes.UpdateNew = append(changes.UpdateNew, update) + changes.UpdateOld = append(changes.UpdateOld, row.current) } + continue } } for _, pol := range p.Policies { @@ -181,7 +186,7 @@ func (p *Plan) Calculate() *Plan { Current: p.Current, Desired: p.Desired, Changes: changes, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, + ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, } return plan diff --git a/plan/plan_test.go b/plan/plan_test.go index d34c93249..2b107d0e8 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -35,6 +35,9 @@ type PlanTestSuite struct { fooV2CnameNoLabel *endpoint.Endpoint fooV3CnameSameResource *endpoint.Endpoint fooA5 *endpoint.Endpoint + fooAAAA *endpoint.Endpoint + dsA *endpoint.Endpoint + dsAAAA *endpoint.Endpoint bar127A *endpoint.Endpoint bar127AWithTTL *endpoint.Endpoint bar127AWithProviderSpecificTrue *endpoint.Endpoint @@ -106,6 +109,30 @@ func (suite *PlanTestSuite) SetupTest() { endpoint.ResourceLabelKey: "ingress/default/foo-5", }, } + suite.fooAAAA = &endpoint.Endpoint{ + DNSName: "foo", + Targets: endpoint.Targets{"2001:DB8::1"}, + RecordType: "AAAA", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/foo-AAAA", + }, + } + suite.dsA = &endpoint.Endpoint{ + DNSName: "ds", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: "A", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/ds", + }, + } + suite.dsAAAA = &endpoint.Endpoint{ + DNSName: "ds", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: "AAAA", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/ds-AAAAA", + }, + } suite.bar127A = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"127.0.0.1"}, @@ -438,9 +465,9 @@ func (suite *PlanTestSuite) TestIdempotency() { func (suite *PlanTestSuite) TestDifferentTypes() { current := []*endpoint.Endpoint{suite.fooV1Cname} desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooA5} - expectedCreate := []*endpoint.Endpoint{} + expectedCreate := []*endpoint.Endpoint{suite.fooA5} expectedUpdateOld := []*endpoint.Endpoint{suite.fooV1Cname} - expectedUpdateNew := []*endpoint.Endpoint{suite.fooA5} + expectedUpdateNew := []*endpoint.Endpoint{suite.fooV2Cname} expectedDelete := []*endpoint.Endpoint{} p := &Plan{ @@ -544,52 +571,6 @@ func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() { validateEntries(suite.T(), changes.Delete, expectedDelete) } -// TODO: remove once multiple-target per endpoint is supported -func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() { - current := []*endpoint.Endpoint{suite.fooV3CnameSameResource, suite.bar192A} - desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource} - expectedCreate := []*endpoint.Endpoint{} - expectedUpdateOld := []*endpoint.Endpoint{suite.fooV3CnameSameResource} - expectedUpdateNew := []*endpoint.Endpoint{suite.fooV1Cname} - expectedDelete := []*endpoint.Endpoint{suite.bar192A} - - p := &Plan{ - Policies: []Policy{&SyncPolicy{}}, - Current: current, - Desired: desired, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, - } - - changes := p.Calculate().Changes - validateEntries(suite.T(), changes.Create, expectedCreate) - validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) - validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) - validateEntries(suite.T(), changes.Delete, expectedDelete) -} - -// TODO: remove once multiple-target per endpoint is supported -func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() { - current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A} - desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource} - expectedCreate := []*endpoint.Endpoint{} - expectedUpdateOld := []*endpoint.Endpoint{} - expectedUpdateNew := []*endpoint.Endpoint{} - expectedDelete := []*endpoint.Endpoint{suite.bar192A} - - p := &Plan{ - Policies: []Policy{&SyncPolicy{}}, - Current: current, - Desired: desired, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, - } - - changes := p.Calculate().Changes - validateEntries(suite.T(), changes.Create, expectedCreate) - validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) - validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) - validateEntries(suite.T(), changes.Delete, expectedDelete) -} - func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier() { current := []*endpoint.Endpoint{suite.multiple1} desired := []*endpoint.Endpoint{suite.multiple2, suite.multiple3} @@ -695,6 +676,39 @@ func (suite *PlanTestSuite) TestMissing() { validateEntries(suite.T(), changes.Create, expectedCreate) } +func (suite *PlanTestSuite) TestAAAARecords() { + + current := []*endpoint.Endpoint{} + desired := []*endpoint.Endpoint{suite.fooAAAA} + expectedCreate := []*endpoint.Endpoint{suite.fooAAAA} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + ManagedRecords: []string{endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) +} + +func (suite *PlanTestSuite) TestDualStackRecords() { + current := []*endpoint.Endpoint{} + desired := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA} + expectedCreate := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) +} + func TestPlan(t *testing.T) { suite.Run(t, new(PlanTestSuite)) } diff --git a/provider/aws/aws.go b/provider/aws/aws.go index 5f7457420..e8fcd06b1 100644 --- a/provider/aws/aws.go +++ b/provider/aws/aws.go @@ -86,6 +86,7 @@ var canonicalHostedZones = map[string]string{ "eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4", "eu-north-1.elb.amazonaws.com": "Z23TAZ6LKFMNIO", "eu-south-1.elb.amazonaws.com": "Z3ULH7SSC9OV64", + "eu-south-2.elb.amazonaws.com": "Z0956581394HF5D5LXGAP", "sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU", "cn-north-1.elb.amazonaws.com.cn": "Z1GDH35T77C1KE", "cn-northwest-1.elb.amazonaws.com.cn": "ZM7IZAIOVVDZF", @@ -115,6 +116,7 @@ var canonicalHostedZones = map[string]string{ "elb.eu-west-3.amazonaws.com": "Z1CMS0P5QUZ6D5", "elb.eu-north-1.amazonaws.com": "Z1UDT6IFJ4EJM", "elb.eu-south-1.amazonaws.com": "Z23146JA1KNAFP", + "elb.eu-south-2.amazonaws.com": "Z1011216NVTVYADP1SSV", "elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU", "elb.cn-north-1.amazonaws.com.cn": "Z3QFB96KMJ7ED6", "elb.cn-northwest-1.amazonaws.com.cn": "ZQEIKTCZ8352D", @@ -373,7 +375,7 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos for _, r := range resp.ResourceRecordSets { newEndpoints := make([]*endpoint.Endpoint, 0) - if !provider.SupportedRecordType(aws.StringValue(r.Type)) { + if !p.SupportedRecordType(aws.StringValue(r.Type)) { continue } @@ -1059,3 +1061,12 @@ func canonicalHostedZone(hostname string) string { func cleanZoneID(id string) string { return strings.TrimPrefix(id, "/hostedzone/") } + +func (p *AWSProvider) SupportedRecordType(recordType string) bool { + switch recordType { + case "MX": + return true + default: + return provider.SupportedRecordType(recordType) + } +} diff --git a/provider/aws/aws_test.go b/provider/aws/aws_test.go index 362ebb989..7382b54f3 100644 --- a/provider/aws/aws_test.go +++ b/provider/aws/aws_test.go @@ -304,7 +304,7 @@ func TestAWSZones(t *testing.T) { {"zone id filter", provider.NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{}), privateZones}, {"tag filter", provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), provider.NewZoneTagFilter([]string{"zone=3"}), privateZones}, } { - provider, _ := newAWSProviderWithTagFilter(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, _ := newAWSProviderWithTagFilter(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, nil) zones, err := provider.Zones(context.Background()) require.NoError(t, err) @@ -337,25 +337,158 @@ func TestAWSRecordsFilter(t *testing.T) { } func TestAWSRecords(t *testing.T) { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), - endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), - endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), - endpoint.NewEndpoint("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), - endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), - endpoint.NewEndpointWithTTL("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "random"), - endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10"), - endpoint.NewEndpointWithTTL("weight-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"), - endpoint.NewEndpointWithTTL("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificRegion, "us-east-1"), - endpoint.NewEndpointWithTTL("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificFailover, "PRIMARY"), - endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""), - endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"), - endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"), - endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"), - 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"), + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, []*route53.ResourceRecordSet{ + { + Name: aws.String("list-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + }, + { + Name: aws.String("list-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), + EvaluateTargetHealth: aws.Bool(false), + HostedZoneId: aws.String("Z215JYRZR1TBD5"), + }, + }, + { + Name: aws.String("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), + EvaluateTargetHealth: aws.Bool(false), + HostedZoneId: aws.String("Z215JYRZR1TBD5"), + }, + }, + { + Name: aws.String("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), + EvaluateTargetHealth: aws.Bool(true), + HostedZoneId: aws.String("Z215JYRZR1TBD5"), + }, + }, + { + Name: aws.String("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeTxt), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("random")}}, + }, + { + Name: aws.String("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("test-set-1"), + Weight: aws.Int64(10), + }, + { + Name: aws.String("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}}, + SetIdentifier: aws.String("test-set-2"), + Weight: aws.Int64(20), + }, + { + Name: aws.String("latency-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("test-set"), + Region: aws.String("us-east-1"), + }, + { + Name: aws.String("failover-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("test-set"), + Failover: aws.String("PRIMARY"), + }, + { + Name: aws.String("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("test-set"), + MultiValueAnswer: aws.Bool(true), + }, + { + Name: aws.String("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("test-set-1"), + GeoLocation: &route53.GeoLocation{ + ContinentCode: aws.String("EU"), + }, + }, + { + Name: aws.String("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}}, + SetIdentifier: aws.String("test-set-2"), + GeoLocation: &route53.GeoLocation{ + CountryCode: aws.String("DE"), + }, + }, + { + Name: aws.String("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("test-set-1"), + GeoLocation: &route53.GeoLocation{ + SubdivisionCode: aws.String("NY"), + }, + }, + { + Name: aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.example.com")}}, + SetIdentifier: aws.String("test-set-1"), + HealthCheckId: aws.String("foo-bar-healthcheck-id"), + Weight: aws.Int64(10), + }, + { + Name: aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}}, + SetIdentifier: aws.String("test-set-2"), + HealthCheckId: aws.String("abc-def-healthcheck-id"), + Weight: aws.Int64(20), + }, + { + Name: aws.String("mail.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.example.com")}, {Value: aws.String("20 mailhost2.example.com")}}, + }, }) records, err := provider.Records(context.Background()) @@ -380,11 +513,12 @@ func TestAWSRecords(t *testing.T) { endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"), 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"), + endpoint.NewEndpointWithTTL("mail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 mailhost1.example.com", "20 mailhost2.example.com"), }) } func TestAWSAdjustEndpoints(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, nil) records := []*endpoint.Endpoint{ endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), @@ -409,7 +543,7 @@ func TestAWSAdjustEndpoints(t *testing.T) { func TestAWSCreateRecords(t *testing.T) { customTTL := endpoint.TTL(60) - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), 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, nil) records := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), @@ -419,31 +553,109 @@ func TestAWSCreateRecords(t *testing.T) { endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true"), 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-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.example.com", "20 mailhost2.example.com"), } require.NoError(t, provider.CreateRecords(context.Background(), records)) - records, err := provider.Records(context.Background()) - require.NoError(t, err) - - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), - endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("create-test-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"), - endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.example.com"), - endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"), - endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"), - 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"), + validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ + { + Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + }, + { + Name: aws.String("create-test-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(60), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("172.17.0.1")}}, + }, + { + Name: aws.String("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.example.com")}}, + }, + { + Name: aws.String("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), + EvaluateTargetHealth: aws.Bool(true), + HostedZoneId: aws.String("Z215JYRZR1TBD5"), + }, + }, + { + Name: aws.String("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.example.com")}, {Value: aws.String("20 mailhost2.example.com")}}, + }, + }) + validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ + { + Name: aws.String("create-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("create-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("alias-target.zone-2.ext-dns-test-2.teapot.zalan.do."), + EvaluateTargetHealth: aws.Bool(true), + HostedZoneId: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."), + }, + }, }) } func TestAWSUpdateRecords(t *testing.T) { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), - endpoint.NewEndpointWithTTL("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.1.1.1"), - endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*route53.ResourceRecordSet{ + { + Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.1.1.1")}}, + }, + { + Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}}, + }, + { + Name: aws.String("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}}, + }, }) currentRecords := []*endpoint.Endpoint{ @@ -452,6 +664,7 @@ func TestAWSUpdateRecords(t *testing.T) { endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), + endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), @@ -459,24 +672,115 @@ func TestAWSUpdateRecords(t *testing.T) { endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), + endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com", "20 mailhost2.foo.elb.amazonaws.com"), } require.NoError(t, provider.UpdateRecords(context.Background(), updatedRecords, currentRecords)) - records, err := provider.Records(context.Background()) - require.NoError(t, err) - - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), - endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"), - endpoint.NewEndpointWithTTL("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"), + validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ + { + Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + }, + { + Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}}, + }, + { + Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, + }, + { + Name: aws.String("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, + }, + { + Name: aws.String("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}, {Value: aws.String("20 mailhost2.foo.elb.amazonaws.com")}}, + }, + }) + validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ + { + Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}}, + }, }) } func TestAWSDeleteRecords(t *testing.T) { - originalEndpoints := []*endpoint.Endpoint{ + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, []*route53.ResourceRecordSet{ + { + Name: aws.String("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("baz.elb.amazonaws.com")}}, + }, + { + Name: aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), + EvaluateTargetHealth: aws.Bool(false), + HostedZoneId: aws.String("Z215JYRZR1TBD5"), + }, + }, + { + Name: aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), + EvaluateTargetHealth: aws.Bool(true), + HostedZoneId: aws.String("Z215JYRZR1TBD5"), + }, + }, + { + Name: aws.String("delete-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + EvaluateTargetHealth: aws.Bool(true), + HostedZoneId: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."), + }, + }, + { + Name: aws.String("delete-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("delete-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}, {Value: aws.String("20 mailhost2.foo.elb.amazonaws.com")}}, + }, + }) + + require.NoError(t, provider.DeleteRecords(context.Background(), []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"), @@ -484,17 +788,11 @@ func TestAWSDeleteRecords(t *testing.T) { endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpoint("delete-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "delete-test.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificTargetHostedZone, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), endpoint.NewEndpointWithTTL("delete-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), - } + endpoint.NewEndpoint("delete-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com", "20 mailhost2.foo.elb.amazonaws.com"), + })) - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, false, originalEndpoints) - - require.NoError(t, provider.DeleteRecords(context.Background(), originalEndpoints)) - - records, err := provider.Records(context.Background()) - - require.NoError(t, err) - - validateEndpoints(t, records, []*endpoint.Endpoint{}) + validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{}) + validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{}) } func TestAWSApplyChanges(t *testing.T) { @@ -513,24 +811,132 @@ func TestAWSApplyChanges(t *testing.T) { } for _, tt := range tests { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), - 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").WithProviderSpecific(providerSpecificAlias, "true"), - endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), - endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"), - endpoint.NewEndpointWithTTL("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("weighted-to-simple").WithProviderSpecific(providerSpecificWeight, "10"), - endpoint.NewEndpointWithTTL("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), - endpoint.NewEndpointWithTTL("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificWeight, "10"), - endpoint.NewEndpointWithTTL("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("before").WithProviderSpecific(providerSpecificWeight, "10"), - endpoint.NewEndpointWithTTL("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("no-change").WithProviderSpecific(providerSpecificWeight, "10"), + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*route53.ResourceRecordSet{ + { + Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.1.1.1")}}, + }, + { + Name: aws.String("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."), + EvaluateTargetHealth: aws.Bool(true), + HostedZoneId: aws.String("Z215JYRZR1TBD5"), + }, + }, + { + Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, + }, + { + Name: aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, + }, + { + Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, + }, + { + Name: aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, + }, + { + Name: aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, + }, + { + Name: aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("weighted-to-simple"), + Weight: aws.Int64(10), + }, + { + Name: aws.String("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + }, + { + Name: aws.String("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("policy-change"), + Weight: aws.Int64(10), + }, + { + Name: aws.String("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("before"), + Weight: aws.Int64(10), + }, + { + Name: aws.String("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("no-change"), + Weight: aws.Int64(10), + }, + { + Name: aws.String("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost2.bar.elb.amazonaws.com")}}, + }, + { + Name: aws.String("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("30 mailhost1.foo.elb.amazonaws.com")}}, + }, }) createRecords := []*endpoint.Endpoint{ @@ -539,6 +945,7 @@ func TestAWSApplyChanges(t *testing.T) { endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), + endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com"), } currentRecords := []*endpoint.Endpoint{ @@ -554,6 +961,7 @@ func TestAWSApplyChanges(t *testing.T) { endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificWeight, "10"), endpoint.NewEndpoint("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("before").WithProviderSpecific(providerSpecificWeight, "10"), endpoint.NewEndpoint("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("no-change").WithProviderSpecific(providerSpecificWeight, "10"), + endpoint.NewEndpoint("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost2.bar.elb.amazonaws.com"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), @@ -568,6 +976,7 @@ func TestAWSApplyChanges(t *testing.T) { endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificRegion, "us-east-1"), endpoint.NewEndpoint("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("after").WithProviderSpecific(providerSpecificWeight, "10"), endpoint.NewEndpoint("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("no-change").WithProviderSpecific(providerSpecificWeight, "20"), + endpoint.NewEndpoint("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "20 mailhost3.foo.elb.amazonaws.com"), } deleteRecords := []*endpoint.Endpoint{ @@ -576,6 +985,7 @@ func TestAWSApplyChanges(t *testing.T) { endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), + endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mailhost1.foo.elb.amazonaws.com"), } changes := &plan.Changes{ @@ -595,47 +1005,221 @@ func TestAWSApplyChanges(t *testing.T) { assert.Equal(t, 1, counter.calls["ListHostedZonesPages"], tt.name) assert.Equal(t, tt.listRRSets, counter.calls["ListResourceRecordSetsPages"], tt.name) - records, err := provider.Records(ctx) - require.NoError(t, err, tt.name) - - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), - endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), - endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"), - endpoint.NewEndpointWithTTL("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"), - endpoint.NewEndpointWithTTL("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "my-internal-host.example.com"), - endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), - endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"), - endpoint.NewEndpointWithTTL("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), - endpoint.NewEndpointWithTTL("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("simple-to-weighted").WithProviderSpecific(providerSpecificWeight, "10"), - endpoint.NewEndpointWithTTL("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificRegion, "us-east-1"), - endpoint.NewEndpointWithTTL("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("after").WithProviderSpecific(providerSpecificWeight, "10"), - endpoint.NewEndpointWithTTL("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("no-change").WithProviderSpecific(providerSpecificWeight, "20"), + validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ + { + Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + }, + { + Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + AliasTarget: &route53.AliasTarget{ + DNSName: aws.String("foo.elb.amazonaws.com."), + EvaluateTargetHealth: aws.Bool(true), + HostedZoneId: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), + }, + }, + { + Name: aws.String("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("my-internal-host.example.com")}}, + }, + { + Name: aws.String("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}}, + }, + { + Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("baz.elb.amazonaws.com")}}, + }, + { + Name: aws.String("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("foo.elb.amazonaws.com")}}, + }, + { + Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("baz.elb.amazonaws.com")}}, + }, + { + Name: aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + }, + { + Name: aws.String("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("simple-to-weighted"), + Weight: aws.Int64(10), + }, + { + Name: aws.String("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("policy-change"), + Region: aws.String("us-east-1"), + }, + { + Name: aws.String("set-identifier-change.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("after"), + Weight: aws.Int64(10), + }, + { + Name: aws.String("set-identifier-no-change.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("no-change"), + Weight: aws.Int64(20), + }, + { + Name: aws.String("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}}, + }, + }) + validateRecords(t, listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []*route53.ResourceRecordSet{ + { + Name: aws.String("create-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("4.3.2.1")}}, + }, + { + Name: aws.String("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, + }, + { + Name: aws.String("update-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("20 mailhost3.foo.elb.amazonaws.com")}}, + }, }) } } func TestAWSApplyChangesDryRun(t *testing.T) { - originalEndpoints := []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), - endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), - endpoint.NewEndpointWithTTL("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.1.1.1"), - endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), - endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"), + originalRecords := []*route53.ResourceRecordSet{ + { + Name: aws.String("update-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + }, + { + Name: aws.String("update-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.1.1.1")}}, + }, + { + Name: aws.String("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, + }, + { + Name: aws.String("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, + }, + { + Name: aws.String("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("bar.elb.amazonaws.com")}}, + }, + { + Name: aws.String("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeCname), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("qux.elb.amazonaws.com")}}, + }, + { + Name: aws.String("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("8.8.8.8")}, {Value: aws.String("8.8.4.4")}}, + }, + { + Name: aws.String("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("1.2.3.4")}, {Value: aws.String("4.3.2.1")}}, + }, + { + Name: aws.String("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("20 mail.foo.elb.amazonaws.com")}}, + }, + { + Name: aws.String("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: aws.String(route53.RRTypeMx), + TTL: aws.Int64(recordTTL), + ResourceRecords: []*route53.ResourceRecord{{Value: aws.String("10 mail.bar.elb.amazonaws.com")}}, + }, } - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalEndpoints) + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalRecords) createRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), @@ -643,6 +1227,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) { endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), + endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mail.foo.elb.amazonaws.com"), } currentRecords := []*endpoint.Endpoint{ @@ -652,6 +1237,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) { endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), + endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "20 mail.foo.elb.amazonaws.com"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), @@ -660,6 +1246,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) { endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), + endpoint.NewEndpoint("update-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mail.bar.elb.amazonaws.com"), } deleteRecords := []*endpoint.Endpoint{ @@ -668,6 +1255,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) { endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), + endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mail.bar.elb.amazonaws.com"), } changes := &plan.Changes{ @@ -681,10 +1269,11 @@ func TestAWSApplyChangesDryRun(t *testing.T) { require.NoError(t, provider.ApplyChanges(ctx, changes)) - records, err := provider.Records(ctx) - require.NoError(t, err) - - validateEndpoints(t, records, originalEndpoints) + validateRecords(t, + append( + listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), + listAWSRecords(t, provider.client, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.")...), + originalRecords) } func TestAWSChangesByZones(t *testing.T) { @@ -805,7 +1394,7 @@ func TestAWSChangesByZones(t *testing.T) { } func TestAWSsubmitChanges(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, nil) const subnets = 16 const hosts = defaultBatchChangeSize / subnets @@ -834,7 +1423,7 @@ func TestAWSsubmitChanges(t *testing.T) { } func TestAWSsubmitChangesError(t *testing.T) { - provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) clientStub.MockMethod("ChangeResourceRecordSets", mock.Anything).Return(nil, fmt.Errorf("Mock route53 failure")) ctx := context.Background() @@ -848,7 +1437,7 @@ func TestAWSsubmitChangesError(t *testing.T) { } func TestAWSsubmitChangesRetryOnError(t *testing.T) { - provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, clientStub := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) ctx := context.Background() zones, err := provider.Zones(ctx) @@ -1043,7 +1632,7 @@ func validateAWSChangeRecord(t *testing.T, record *Route53Change, expected *Rout } func TestAWSCreateRecordsWithCNAME(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, nil) records := []*endpoint.Endpoint{ {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.example.org"}, RecordType: endpoint.RecordTypeCNAME}, @@ -1073,7 +1662,7 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { "false": false, "": false, } { - 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, nil) // Test dualstack and ipv4 load balancer targets records := []*endpoint.Endpoint{ @@ -1289,22 +1878,33 @@ func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone } } -func setupAWSRecords(t *testing.T, provider *AWSProvider, endpoints []*endpoint.Endpoint) { - clearAWSRecords(t, provider, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") - clearAWSRecords(t, provider, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.") - clearAWSRecords(t, provider, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.") +func setAWSRecords(t *testing.T, provider *AWSProvider, records []*route53.ResourceRecordSet) { + dryRun := provider.dryRun + provider.dryRun = false + defer func() { + provider.dryRun = dryRun + }() ctx := context.Background() - records, err := provider.Records(ctx) + endpoints, err := provider.Records(ctx) require.NoError(t, err) - validateEndpoints(t, records, []*endpoint.Endpoint{}) + validateEndpoints(t, endpoints, []*endpoint.Endpoint{}) - require.NoError(t, provider.CreateRecords(context.Background(), endpoints)) + var changes Route53Changes + for _, record := range records { + changes = append(changes, &Route53Change{ + Change: route53.Change{ + Action: aws.String(route53.ChangeActionCreate), + ResourceRecordSet: record, + }, + }) + } - escapeAWSRecords(t, provider, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") - escapeAWSRecords(t, provider, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.") - escapeAWSRecords(t, provider, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.") + zones, err := provider.Zones(ctx) + require.NoError(t, err) + err = provider.submitChanges(ctx, changes, zones) + require.NoError(t, err) _, err = provider.Records(ctx) require.NoError(t, err) @@ -1323,28 +1923,6 @@ func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.Res return recordSets } -func clearAWSRecords(t *testing.T, provider *AWSProvider, zone string) { - recordSets := listAWSRecords(t, provider.client, zone) - - changes := make([]*route53.Change, 0, len(recordSets)) - for _, recordSet := range recordSets { - changes = append(changes, &route53.Change{ - Action: aws.String(route53.ChangeActionDelete), - ResourceRecordSet: recordSet, - }) - } - - if len(changes) != 0 { - _, err := provider.client.ChangeResourceRecordSetsWithContext(context.Background(), &route53.ChangeResourceRecordSetsInput{ - HostedZoneId: aws.String(zone), - ChangeBatch: &route53.ChangeBatch{ - Changes: changes, - }, - }) - require.NoError(t, err) - } -} - // Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) { recordSets := listAWSRecords(t, provider.client, zone) @@ -1368,11 +1946,11 @@ func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) { } } -func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { +func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*route53.ResourceRecordSet) (*AWSProvider, *Route53APIStub) { return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, provider.NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records) } -func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, zoneTagFilter provider.ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { +func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, zoneTagFilter provider.ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*route53.ResourceRecordSet) (*AWSProvider, *Route53APIStub) { client := NewRoute53APIStub(t) provider := &AWSProvider{ @@ -1416,7 +1994,7 @@ func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilte setupZoneTags(provider.client.(*Route53APIStub)) - setupAWSRecords(t, provider, records) + setAWSRecords(t, provider, records) provider.dryRun = dryRun @@ -1471,7 +2049,7 @@ func containsRecordWithDNSName(records []*endpoint.Endpoint, dnsName string) boo } func TestRequiresDeleteCreate(t *testing.T) { - provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"foo.bar."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"foo.bar."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil) oldRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8") newRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar") diff --git a/provider/awssd/aws_sd.go b/provider/awssd/aws_sd.go index ecdb7efc5..97cca81cc 100644 --- a/provider/awssd/aws_sd.go +++ b/provider/awssd/aws_sd.go @@ -509,7 +509,7 @@ func (p *AWSSDProvider) DeleteService(service *sd.Service) error { // convert ownerID string to service description format label := endpoint.NewLabels() label[endpoint.OwnerLabelKey] = p.ownerID - label[endpoint.AWSSDDescriptionLabel] = label.Serialize(false) + label[endpoint.AWSSDDescriptionLabel] = label.SerializePlain(false) if strings.HasPrefix(aws.StringValue(service.Description), label[endpoint.AWSSDDescriptionLabel]) { log.Infof("Deleting service \"%s\"", *service.Name) diff --git a/provider/azure/azure.go b/provider/azure/azure.go index dfab96eff..bca9c841d 100644 --- a/provider/azure/azure.go +++ b/provider/azure/azure.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// nolint:staticcheck +//nolint:staticcheck // Required due to the current dependency on a deprecated version of azure-sdk-for-go package azure import ( @@ -109,7 +109,7 @@ func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp return true } recordType := strings.TrimPrefix(*recordSet.Type, "Microsoft.Network/dnszones/") - if !provider.SupportedRecordType(recordType) { + if !p.SupportedRecordType(recordType) { return true } name := formatAzureDNSName(*recordSet.Name, *zone.Name) @@ -190,6 +190,15 @@ func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) { return zones, nil } +func (p *AzureProvider) SupportedRecordType(recordType string) bool { + switch recordType { + case "MX": + return true + default: + return provider.SupportedRecordType(recordType) + } +} + func (p *AzureProvider) iterateRecords(ctx context.Context, zoneName string, callback func(dns.RecordSet) bool) error { log.Debugf("Retrieving Azure DNS records for zone '%s'.", zoneName) @@ -241,10 +250,6 @@ func (p *AzureProvider) mapChanges(zones []dns.Zone, changes *plan.Changes) (azu mapChange(deleted, change) } - for _, change := range changes.UpdateOld { - mapChange(deleted, change) - } - for _, change := range changes.Create { mapChange(updated, change) } @@ -377,6 +382,21 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet }, }, }, nil + case dns.MX: + mxRecords := make([]dns.MxRecord, len(endpoint.Targets)) + for i, target := range endpoint.Targets { + mxRecord, err := parseMxTarget[dns.MxRecord](target) + if err != nil { + return dns.RecordSet{}, err + } + mxRecords[i] = mxRecord + } + return dns.RecordSet{ + RecordSetProperties: &dns.RecordSetProperties{ + TTL: to.Int64Ptr(ttl), + MxRecords: &mxRecords, + }, + }, nil case dns.TXT: return dns.RecordSet{ RecordSetProperties: &dns.RecordSetProperties{ @@ -425,6 +445,16 @@ func extractAzureTargets(recordSet *dns.RecordSet) []string { return []string{*cnameRecord.Cname} } + // Check for MX records + mxRecords := properties.MxRecords + if mxRecords != nil && len(*mxRecords) > 0 && (*mxRecords)[0].Exchange != nil { + targets := make([]string, len(*mxRecords)) + for i, mxRecord := range *mxRecords { + targets[i] = fmt.Sprintf("%d %s", *mxRecord.Preference, *mxRecord.Exchange) + } + return targets + } + // Check for TXT records txtRecords := properties.TxtRecords if txtRecords != nil && len(*txtRecords) > 0 && (*txtRecords)[0].Value != nil { diff --git a/provider/azure/azure_private_dns.go b/provider/azure/azure_private_dns.go index 374b5e5d6..ee2e478bd 100644 --- a/provider/azure/azure_private_dns.go +++ b/provider/azure/azure_private_dns.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// nolint:staticcheck +//nolint:staticcheck // Required due to the current dependency on a deprecated version of azure-sdk-for-go package azure import ( @@ -237,10 +237,6 @@ func (p *AzurePrivateDNSProvider) mapChanges(zones []privatedns.PrivateZone, cha mapChange(deleted, change) } - for _, change := range changes.UpdateOld { - mapChange(deleted, change) - } - for _, change := range changes.Create { mapChange(updated, change) } @@ -367,6 +363,21 @@ func (p *AzurePrivateDNSProvider) newRecordSet(endpoint *endpoint.Endpoint) (pri }, }, }, nil + case privatedns.MX: + mxRecords := make([]privatedns.MxRecord, len(endpoint.Targets)) + for i, target := range endpoint.Targets { + mxRecord, err := parseMxTarget[privatedns.MxRecord](target) + if err != nil { + return privatedns.RecordSet{}, err + } + mxRecords[i] = mxRecord + } + return privatedns.RecordSet{ + RecordSetProperties: &privatedns.RecordSetProperties{ + TTL: to.Int64Ptr(ttl), + MxRecords: &mxRecords, + }, + }, nil case privatedns.TXT: return privatedns.RecordSet{ RecordSetProperties: &privatedns.RecordSetProperties{ @@ -407,6 +418,16 @@ func extractAzurePrivateDNSTargets(recordSet *privatedns.RecordSet) []string { return []string{*cnameRecord.Cname} } + // Check for MX records + mxRecords := properties.MxRecords + if mxRecords != nil && len(*mxRecords) > 0 && (*mxRecords)[0].Exchange != nil { + targets := make([]string, len(*mxRecords)) + for i, mxRecord := range *mxRecords { + targets[i] = fmt.Sprintf("%d %s", *mxRecord.Preference, *mxRecord.Exchange) + } + return targets + } + // Check for TXT records txtRecords := properties.TxtRecords if txtRecords != nil && len(*txtRecords) > 0 && (*txtRecords)[0].Value != nil { diff --git a/provider/azure/azure_privatedns_test.go b/provider/azure/azure_privatedns_test.go index f35720154..061b0f4d6 100644 --- a/provider/azure/azure_privatedns_test.go +++ b/provider/azure/azure_privatedns_test.go @@ -123,6 +123,17 @@ func privateCNameRecordSetPropertiesGetter(values []string, ttl int64) *privated } } +func privateMXRecordSetPropertiesGetter(values []string, ttl int64) *privatedns.RecordSetProperties { + mxRecords := make([]privatedns.MxRecord, len(values)) + for i, target := range values { + mxRecords[i], _ = parseMxTarget[privatedns.MxRecord](target) + } + return &privatedns.RecordSetProperties{ + TTL: to.Int64Ptr(ttl), + MxRecords: &mxRecords, + } +} + func privateTxtRecordSetPropertiesGetter(values []string, ttl int64) *privatedns.RecordSetProperties { return &privatedns.RecordSetProperties{ TTL: to.Int64Ptr(ttl), @@ -156,6 +167,8 @@ func createPrivateMockRecordSetMultiWithTTL(name, recordType string, ttl int64, getterFunc = privateARecordSetPropertiesGetter case endpoint.RecordTypeCNAME: getterFunc = privateCNameRecordSetPropertiesGetter + case endpoint.RecordTypeMX: + getterFunc = privateMXRecordSetPropertiesGetter case endpoint.RecordTypeTXT: getterFunc = privateTxtRecordSetPropertiesGetter default: @@ -266,6 +279,7 @@ func TestAzurePrivateDNSRecord(t *testing.T) { createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeA, "123.123.123.123", 3600), createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL), createPrivateMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), + createPrivateMockRecordSetWithTTL("mail", endpoint.RecordTypeMX, "10 example.com", 4000), }) if err != nil { t.Fatal(err) @@ -281,6 +295,7 @@ func TestAzurePrivateDNSRecord(t *testing.T) { 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"), endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com"), } validateAzureEndpoints(t, actual, expected) @@ -299,6 +314,7 @@ func TestAzurePrivateDNSMultiRecord(t *testing.T) { createPrivateMockRecordSetMultiWithTTL("nginx", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"), createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL), createPrivateMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), + createPrivateMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"), }) if err != nil { t.Fatal(err) @@ -314,6 +330,7 @@ func TestAzurePrivateDNSMultiRecord(t *testing.T) { endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"), endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"), endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"), } validateAzureEndpoints(t, actual, expected) @@ -325,8 +342,6 @@ func TestAzurePrivateDNSApplyChanges(t *testing.T) { testAzurePrivateDNSApplyChangesInternal(t, false, &recordsClient) validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{ - endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""), - endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""), endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""), endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""), }) @@ -342,6 +357,9 @@ func TestAzurePrivateDNSApplyChanges(t *testing.T) { endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), + endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 other.com"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), }) } @@ -401,17 +419,21 @@ func testAzurePrivateDNSApplyChangesInternal(t *testing.T, dryRun bool, client P endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"), endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"), endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeMX, "10 other.com"), + endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeTXT, "tag"), } currentRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, "121.212.121.212"), endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"), endpoint.NewEndpoint("old.nope.com", endpoint.RecordTypeA, "121.212.121.212"), + endpoint.NewEndpoint("oldmail.example.com", endpoint.RecordTypeMX, "20 foo.other.com"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), endpoint.NewEndpoint("new.nope.com", endpoint.RecordTypeA, "222.111.222.111"), + endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"), } deleteRecords := []*endpoint.Endpoint{ diff --git a/provider/azure/azure_test.go b/provider/azure/azure_test.go index 0598dd4f3..ba831586c 100644 --- a/provider/azure/azure_test.go +++ b/provider/azure/azure_test.go @@ -122,6 +122,17 @@ func cNameRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetPr } } +func mxRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties { + mxRecords := make([]dns.MxRecord, len(values)) + for i, target := range values { + mxRecords[i], _ = parseMxTarget[dns.MxRecord](target) + } + return &dns.RecordSetProperties{ + TTL: to.Int64Ptr(ttl), + MxRecords: &mxRecords, + } +} + func txtRecordSetPropertiesGetter(values []string, ttl int64) *dns.RecordSetProperties { return &dns.RecordSetProperties{ TTL: to.Int64Ptr(ttl), @@ -155,6 +166,8 @@ func createMockRecordSetMultiWithTTL(name, recordType string, ttl int64, values getterFunc = aRecordSetPropertiesGetter case endpoint.RecordTypeCNAME: getterFunc = cNameRecordSetPropertiesGetter + case endpoint.RecordTypeMX: + getterFunc = mxRecordSetPropertiesGetter case endpoint.RecordTypeTXT: getterFunc = txtRecordSetPropertiesGetter default: @@ -271,6 +284,7 @@ func TestAzureRecord(t *testing.T) { 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), + createMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com"), }) if err != nil { t.Fatal(err) @@ -287,6 +301,7 @@ func TestAzureRecord(t *testing.T) { 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"), endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com"), } validateAzureEndpoints(t, actual, expected) @@ -305,6 +320,7 @@ func TestAzureMultiRecord(t *testing.T) { createMockRecordSetMultiWithTTL("nginx", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"), createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL), createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), + createMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"), }) if err != nil { t.Fatal(err) @@ -321,6 +337,7 @@ func TestAzureMultiRecord(t *testing.T) { endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"), endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"), endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"), } validateAzureEndpoints(t, actual, expected) @@ -332,8 +349,6 @@ func TestAzureApplyChanges(t *testing.T) { testAzureApplyChangesInternal(t, false, &recordsClient) validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{ - endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""), - endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""), endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""), endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""), }) @@ -349,6 +364,9 @@ func TestAzureApplyChanges(t *testing.T) { endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), + endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 other.com"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), }) } @@ -410,17 +428,21 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"), endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"), endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeMX, "10 other.com"), + endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeTXT, "tag"), } currentRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, "121.212.121.212"), endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"), endpoint.NewEndpoint("old.nope.com", endpoint.RecordTypeA, "121.212.121.212"), + endpoint.NewEndpoint("oldmail.example.com", endpoint.RecordTypeMX, "20 foo.other.com"), } updatedRecords := []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), endpoint.NewEndpoint("new.nope.com", endpoint.RecordTypeA, "222.111.222.111"), + endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"), } deleteRecords := []*endpoint.Endpoint{ @@ -455,6 +477,7 @@ func TestAzureNameFilter(t *testing.T) { 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("mail.nginx", endpoint.RecordTypeMX, "20 example.com", recordTTL), createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), }) if err != nil { @@ -470,6 +493,7 @@ func TestAzureNameFilter(t *testing.T) { 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"), + endpoint.NewEndpointWithTTL("mail.nginx.example.com", endpoint.RecordTypeMX, recordTTL, "20 example.com"), } validateAzureEndpoints(t, actual, expected) @@ -481,8 +505,6 @@ func TestAzureApplyChangesZoneName(t *testing.T) { 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, ""), }) diff --git a/provider/azure/common.go b/provider/azure/common.go new file mode 100644 index 000000000..95fd1a851 --- /dev/null +++ b/provider/azure/common.go @@ -0,0 +1,47 @@ +/* +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. +*/ + +//nolint:staticcheck // Required due to the current dependency on a deprecated version of azure-sdk-for-go +package azure + +import ( + "fmt" + "strconv" + "strings" + + "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns" + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + "github.com/Azure/go-autorest/autorest/to" +) + +// Helper function (shared with test code) +func parseMxTarget[T dns.MxRecord | privatedns.MxRecord](mxTarget string) (T, error) { + targetParts := strings.SplitN(mxTarget, " ", 2) + if len(targetParts) != 2 { + return T{}, fmt.Errorf("mx target needs to be of form '10 example.com'") + } + + preferenceRaw, exchange := targetParts[0], targetParts[1] + preference, err := strconv.ParseInt(preferenceRaw, 10, 32) + if err != nil { + return T{}, fmt.Errorf("invalid preference specified") + } + + return T{ + Preference: to.Int32Ptr(int32(preference)), + Exchange: to.StringPtr(exchange), + }, nil +} diff --git a/provider/azure/common_test.go b/provider/azure/common_test.go new file mode 100644 index 000000000..1009594c2 --- /dev/null +++ b/provider/azure/common_test.go @@ -0,0 +1,87 @@ +/* +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" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns" + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + "github.com/Azure/go-autorest/autorest/to" + + "github.com/stretchr/testify/assert" +) + +func Test_parseMxTarget(t *testing.T) { + type testCase[T interface { + dns.MxRecord | privatedns.MxRecord + }] struct { + name string + args string + want T + wantErr assert.ErrorAssertionFunc + } + + tests := []testCase[dns.MxRecord]{ + { + name: "valid mx target", + args: "10 example.com", + want: dns.MxRecord{ + Preference: to.Int32Ptr(int32(10)), + Exchange: to.StringPtr("example.com"), + }, + wantErr: assert.NoError, + }, + { + name: "valid mx target with a subdomain", + args: "99 foo-bar.example.com", + want: dns.MxRecord{ + Preference: to.Int32Ptr(int32(99)), + Exchange: to.StringPtr("foo-bar.example.com"), + }, + wantErr: assert.NoError, + }, + { + name: "invalid mx target with misplaced preference and exchange", + args: "example.com 10", + want: dns.MxRecord{}, + wantErr: assert.Error, + }, + { + name: "invalid mx target without preference", + args: "example.com", + want: dns.MxRecord{}, + wantErr: assert.Error, + }, + { + name: "invalid mx target with non numeric preference", + args: "aa example.com", + want: dns.MxRecord{}, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseMxTarget[dns.MxRecord](tt.args) + if !tt.wantErr(t, err, fmt.Sprintf("parseMxTarget(%v)", tt.args)) { + return + } + assert.Equalf(t, tt.want, got, "parseMxTarget(%v)", tt.args) + }) + } +} diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 0f7bdf0cc..0623ca2a0 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "strconv" + "strings" cloudflare "github.com/cloudflare/cloudflare-go" log "github.com/sirupsen/logrus" @@ -155,7 +156,15 @@ func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter prov err error ) if os.Getenv("CF_API_TOKEN") != "" { - config, err = cloudflare.NewWithAPIToken(os.Getenv("CF_API_TOKEN")) + token := os.Getenv("CF_API_TOKEN") + if strings.HasPrefix(token, "file:") { + tokenBytes, err := os.ReadFile(strings.TrimPrefix(token, "file:")) + if err != nil { + return nil, fmt.Errorf("failed to read CF_API_TOKEN from file: %w", err) + } + token = string(tokenBytes) + } + config, err = cloudflare.NewWithAPIToken(token) } else { config, err = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL")) } diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go index 3755c8c9b..6d1d47b40 100644 --- a/provider/cloudflare/cloudflare_test.go +++ b/provider/cloudflare/cloudflare_test.go @@ -677,6 +677,23 @@ func TestCloudflareProvider(t *testing.T) { if err != nil { t.Errorf("should not fail, %s", err) } + + _ = os.Unsetenv("CF_API_TOKEN") + tokenFile := "/tmp/cf_api_token" + if err := os.WriteFile(tokenFile, []byte("abc123def"), 0644); err != nil { + t.Errorf("failed to write token file, %s", err) + } + _ = os.Setenv("CF_API_TOKEN", tokenFile) + _, err = NewCloudFlareProvider( + endpoint.NewDomainFilter([]string{"bar.com"}), + provider.NewZoneIDFilter([]string{""}), + false, + true, + 5000) + if err != nil { + t.Errorf("should not fail, %s", err) + } + _ = os.Unsetenv("CF_API_TOKEN") _ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx") _ = os.Setenv("CF_API_EMAIL", "test@test.com") @@ -689,6 +706,7 @@ func TestCloudflareProvider(t *testing.T) { if err != nil { t.Errorf("should not fail, %s", err) } + _ = os.Unsetenv("CF_API_KEY") _ = os.Unsetenv("CF_API_EMAIL") _, err = NewCloudFlareProvider( diff --git a/provider/designate/designate.go b/provider/designate/designate.go index 15bfc7829..ac9afa796 100644 --- a/provider/designate/designate.go +++ b/provider/designate/designate.go @@ -322,13 +322,13 @@ func (p designateProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, e if recordSet.Type != endpoint.RecordTypeA && recordSet.Type != endpoint.RecordTypeTXT && recordSet.Type != endpoint.RecordTypeCNAME { return nil } - for _, record := range recordSet.Records { - ep := endpoint.NewEndpoint(recordSet.Name, recordSet.Type, record) - ep.Labels[designateRecordSetID] = recordSet.ID - ep.Labels[designateZoneID] = recordSet.ZoneID - ep.Labels[designateOriginalRecords] = strings.Join(recordSet.Records, "\000") - result = append(result, ep) - } + + ep := endpoint.NewEndpoint(recordSet.Name, recordSet.Type, recordSet.Records...) + ep.Labels[designateRecordSetID] = recordSet.ID + ep.Labels[designateZoneID] = recordSet.ZoneID + ep.Labels[designateOriginalRecords] = strings.Join(recordSet.Records, "\000") + result = append(result, ep) + return nil }, ) @@ -358,7 +358,7 @@ type recordSet struct { } // adds endpoint into recordset aggregation, loading original values from endpoint labels first -func addEndpoint(ep *endpoint.Endpoint, recordSets map[string]*recordSet, delete bool) { +func addEndpoint(ep *endpoint.Endpoint, recordSets map[string]*recordSet, oldEndpoints []*endpoint.Endpoint, delete bool) { key := fmt.Sprintf("%s/%s", ep.DNSName, ep.RecordType) rs := recordSets[key] if rs == nil { @@ -368,6 +368,9 @@ func addEndpoint(ep *endpoint.Endpoint, recordSets map[string]*recordSet, delete names: make(map[string]bool), } } + + addDesignateIDLabelsFromExistingEndpoints(oldEndpoints, ep) + if rs.zoneID == "" { rs.zoneID = ep.Labels[designateZoneID] } @@ -389,25 +392,55 @@ func addEndpoint(ep *endpoint.Endpoint, recordSets map[string]*recordSet, delete recordSets[key] = rs } +// addDesignateIDLabelsFromExistingEndpoints adds the labels identified by the constants designateZoneID and designateRecordSetID +// to an Endpoint. Therefore, it searches all given existing endpoints for an endpoint with the same record type and record +// value. If the given Endpoint already has the labels set, they are left untouched. This fixes an issue with the +// TXTRegistry which generates new TXT entries instead of updating the old ones. +func addDesignateIDLabelsFromExistingEndpoints(existingEndpoints []*endpoint.Endpoint, ep *endpoint.Endpoint) { + _, hasZoneIDLabel := ep.Labels[designateZoneID] + _, hasRecordSetIDLabel := ep.Labels[designateRecordSetID] + if hasZoneIDLabel && hasRecordSetIDLabel { + return + } + for _, oep := range existingEndpoints { + if ep.RecordType == oep.RecordType && ep.DNSName == oep.DNSName { + if !hasZoneIDLabel { + ep.Labels[designateZoneID] = oep.Labels[designateZoneID] + } + if !hasRecordSetIDLabel { + ep.Labels[designateRecordSetID] = oep.Labels[designateRecordSetID] + } + return + } + } +} + // ApplyChanges applies a given set of changes in a given zone. func (p designateProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { managedZones, err := p.getZones() if err != nil { return err } + + endpoints, err := p.Records(ctx) + if err != nil { + return fmt.Errorf("failed to fetch active records: %w", err) + } + recordSets := map[string]*recordSet{} for _, ep := range changes.Create { - addEndpoint(ep, recordSets, false) - } - for _, ep := range changes.UpdateNew { - addEndpoint(ep, recordSets, false) + addEndpoint(ep, recordSets, endpoints, false) } for _, ep := range changes.UpdateOld { - addEndpoint(ep, recordSets, true) + addEndpoint(ep, recordSets, endpoints, true) + } + for _, ep := range changes.UpdateNew { + addEndpoint(ep, recordSets, endpoints, false) } for _, ep := range changes.Delete { - addEndpoint(ep, recordSets, true) + addEndpoint(ep, recordSets, endpoints, true) } + for _, rs := range recordSets { if err2 := p.upsertRecordSet(rs, managedZones); err == nil { err = err2 diff --git a/provider/designate/designate_test.go b/provider/designate/designate_test.go index d0790cf6f..80106489a 100644 --- a/provider/designate/designate_test.go +++ b/provider/designate/designate_test.go @@ -274,17 +274,7 @@ func TestDesignateRecords(t *testing.T) { { DNSName: "srv.test.net", RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.2.1.1"}, - Labels: map[string]string{ - designateRecordSetID: rs21ID, - designateZoneID: zone2ID, - designateOriginalRecords: "10.2.1.1\00010.2.1.2", - }, - }, - { - DNSName: "srv.test.net", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.2.1.2"}, + Targets: endpoint.Targets{"10.2.1.1", "10.2.1.2"}, Labels: map[string]string{ designateRecordSetID: rs21ID, designateZoneID: zone2ID, @@ -336,6 +326,19 @@ func testDesignateCreateRecords(t *testing.T, client *fakeDesignateClient) []*re Status: "ACTIVE", }) } + + _, err := client.CreateRecordSet("zone-1", recordsets.CreateOpts{ + Name: "www.example.com.", + Description: "", + Records: []string{"foo"}, + TTL: 60, + Type: endpoint.RecordTypeTXT, + }) + + if err != nil { + t.Fatal("failed to prefil records") + } + endpoints := []*endpoint.Endpoint{ { DNSName: "www.example.com", @@ -409,7 +412,7 @@ func testDesignateCreateRecords(t *testing.T, client *fakeDesignateClient) []*re expectedCopy := make([]*recordsets.RecordSet, len(expected)) copy(expectedCopy, expected) - err := client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{Create: endpoints}) + err = client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{Create: endpoints}) if err != nil { t.Fatal(err) } diff --git a/provider/gandi/gandi.go b/provider/gandi/gandi.go index 378b166e8..3746469ea 100644 --- a/provider/gandi/gandi.go +++ b/provider/gandi/gandi.go @@ -16,7 +16,6 @@ package gandi import ( "context" "errors" - "fmt" "os" "strings" @@ -121,11 +120,17 @@ func (p *GandiProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, erro name = zone } - if len(r.RrsetValues) > 1 { - return nil, fmt.Errorf("can't handle multiple values for rrset %s", name) - } + for _, v := range r.RrsetValues { + log.WithFields(log.Fields{ + "record": r.RrsetName, + "type": r.RrsetType, + "value": v, + "ttl": r.RrsetTTL, + "zone": zone, + }).Debug("Returning endpoint record") - endpoints = append(endpoints, endpoint.NewEndpoint(name, r.RrsetType, r.RrsetValues[0])) + endpoints = append(endpoints, endpoint.NewEndpoint(name, r.RrsetType, v)) + } } } } diff --git a/provider/gandi/gandi_test.go b/provider/gandi/gandi_test.go index cd9529f05..bd32d5b6c 100644 --- a/provider/gandi/gandi_test.go +++ b/provider/gandi/gandi_test.go @@ -18,7 +18,6 @@ import ( "fmt" "os" "reflect" - "strings" "testing" "github.com/go-gandi/go-gandi/domain" @@ -342,7 +341,7 @@ func TestGandiProvider_RecordsAppliesDomainFilter(t *testing.T) { td.Cmp(t, expectedActions, mockedClient.Actions) } -func TestGandiProvider_RecordsErrorOnMultipleValues(t *testing.T) { +func TestGandiProvider_RecordsWithMultipleValues(t *testing.T) { mockedClient := mockGandiClientNewWithRecords([]livedns.DomainRecord{ { RrsetValues: []string{"foo", "bar"}, @@ -355,23 +354,11 @@ func TestGandiProvider_RecordsErrorOnMultipleValues(t *testing.T) { LiveDNSClient: mockedClient, } - expectedActions := []MockAction{ - { - Name: "ListDomains", - }, - { - Name: "GetDomainRecords", - FQDN: "example.com", - }, - } - endpoints, err := mockedProvider.Records(context.Background()) - if err == nil { - t.Errorf("expected to fail") + if err != nil { + t.Errorf("should not fail, %s", err) } - assert.Equal(t, 0, len(endpoints)) - assert.True(t, strings.HasPrefix(err.Error(), "can't handle multiple values for rrset")) - td.Cmp(t, expectedActions, mockedClient.Actions) + assert.Equal(t, 2, len(endpoints)) } func TestGandiProvider_ApplyChangesEmpty(t *testing.T) { diff --git a/provider/google/google.go b/provider/google/google.go index d6c53ef6a..5fce653f5 100644 --- a/provider/google/google.go +++ b/provider/google/google.go @@ -213,7 +213,7 @@ func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.End f := func(resp *dns.ResourceRecordSetsListResponse) error { for _, r := range resp.Rrsets { - if !provider.SupportedRecordType(r.Type) { + if !p.SupportedRecordType(r.Type) { continue } endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), r.Rrdatas...)) @@ -273,6 +273,16 @@ func (p *GoogleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes return p.submitChange(ctx, change) } +// SupportedRecordType returns true if the record type is supported by the provider +func (p *GoogleProvider) SupportedRecordType(recordType string) bool { + switch recordType { + case "MX": + return true + default: + return provider.SupportedRecordType(recordType) + } +} + // newFilteredRecords returns a collection of RecordSets based on the given endpoints and domainFilter. func (p *GoogleProvider) newFilteredRecords(endpoints []*endpoint.Endpoint) []*dns.ResourceRecordSet { records := []*dns.ResourceRecordSet{} @@ -447,6 +457,12 @@ func newRecord(ep *endpoint.Endpoint) *dns.ResourceRecordSet { targets[0] = provider.EnsureTrailingDot(targets[0]) } + if ep.RecordType == endpoint.RecordTypeMX { + for i, mxRecord := range ep.Targets { + targets[i] = provider.EnsureTrailingDot(mxRecord) + } + } + // no annotation results in a Ttl of 0, default to 300 for backwards-compatibility var ttl int64 = googleRecordTTL if ep.RecordTTL.IsConfigured() { diff --git a/provider/google/google_test.go b/provider/google/google_test.go index 3fe610091..bc321200b 100644 --- a/provider/google/google_test.go +++ b/provider/google/google_test.go @@ -512,6 +512,7 @@ func TestNewFilteredRecords(t *testing.T) { endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, 4000, "bar.elb.amazonaws.com"), // test fallback to Ttl:300 when Ttl==0 : endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 0, "8.8.8.8"), + endpoint.NewEndpointWithTTL("update-test-mx.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeMX, 6000, "10 mail.elb.amazonaws.com"), endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), }) @@ -521,6 +522,7 @@ func TestNewFilteredRecords(t *testing.T) { {Name: "delete-test.zone-2.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"8.8.4.4"}, Type: "A", Ttl: 120}, {Name: "update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"bar.elb.amazonaws.com."}, Type: "CNAME", Ttl: 4000}, {Name: "update-test.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"8.8.8.8"}, Type: "A", Ttl: 300}, + {Name: "update-test-mx.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"10 mail.elb.amazonaws.com."}, Type: "MX", Ttl: 6000}, {Name: "delete-test.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"8.8.8.8"}, Type: "A", Ttl: 300}, {Name: "delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do.", Rrdatas: []string{"qux.elb.amazonaws.com."}, Type: "CNAME", Ttl: 300}, }) diff --git a/provider/infoblox/infoblox.go b/provider/infoblox/infoblox.go index 306d6672d..c15e9dc89 100644 --- a/provider/infoblox/infoblox.go +++ b/provider/infoblox/infoblox.go @@ -192,11 +192,24 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End for _, zone := range zones { logrus.Debugf("fetch records from zone '%s'", zone.Fqdn) + + view := p.view + if view == "" { + view = "default" + } + searchParams := ibclient.NewQueryParams( + false, + map[string]string{ + "zone": zone.Fqdn, + "view": view, + }, + ) + var resA []ibclient.RecordA objA := ibclient.NewEmptyRecordA() objA.View = p.view objA.Zone = zone.Fqdn - err = p.client.GetObject(objA, "", nil, &resA) + err = p.client.GetObject(objA, "", searchParams, &resA) if err != nil && !isNotFoundError(err) { return nil, fmt.Errorf("could not fetch A records from zone '%s': %s", zone.Fqdn, err) } @@ -242,7 +255,7 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End objH := ibclient.NewEmptyHostRecord() objH.View = p.view objH.Zone = zone.Fqdn - err = p.client.GetObject(objH, "", nil, &resH) + err = p.client.GetObject(objH, "", searchParams, &resH) if err != nil && !isNotFoundError(err) { return nil, fmt.Errorf("could not fetch host records from zone '%s': %s", zone.Fqdn, err) } @@ -264,7 +277,7 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End objC := ibclient.NewEmptyRecordCNAME() objC.View = p.view objC.Zone = zone.Fqdn - err = p.client.GetObject(objC, "", nil, &resC) + err = p.client.GetObject(objC, "", searchParams, &resC) if err != nil && !isNotFoundError(err) { return nil, fmt.Errorf("could not fetch CNAME records from zone '%s': %s", zone.Fqdn, err) } @@ -283,7 +296,7 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End objP := ibclient.NewEmptyRecordPTR() objP.Zone = arpaZone objP.View = p.view - err = p.client.GetObject(objP, "", nil, &resP) + err = p.client.GetObject(objP, "", searchParams, &resP) if err != nil && !isNotFoundError(err) { return nil, fmt.Errorf("could not fetch PTR records from zone '%s': %s", zone.Fqdn, err) } @@ -300,7 +313,7 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End View: p.view, }, ) - err = p.client.GetObject(objT, "", nil, &resT) + err = p.client.GetObject(objT, "", searchParams, &resT) if err != nil && !isNotFoundError(err) { return nil, fmt.Errorf("could not fetch TXT records from zone '%s': %s", zone.Fqdn, err) } diff --git a/provider/oci/oci.go b/provider/oci/oci.go index f9a47a48b..26d8f691f 100644 --- a/provider/oci/oci.go +++ b/provider/oci/oci.go @@ -21,8 +21,9 @@ import ( "os" "strings" - "github.com/oracle/oci-go-sdk/common" - "github.com/oracle/oci-go-sdk/dns" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/common/auth" + "github.com/oracle/oci-go-sdk/v65/dns" "github.com/pkg/errors" log "github.com/sirupsen/logrus" yaml "gopkg.in/yaml.v2" @@ -36,12 +37,13 @@ const ociRecordTTL = 300 // OCIAuthConfig holds connection parameters for the OCI API. type OCIAuthConfig struct { - Region string `yaml:"region"` - TenancyID string `yaml:"tenancy"` - UserID string `yaml:"user"` - PrivateKey string `yaml:"key"` - Fingerprint string `yaml:"fingerprint"` - Passphrase string `yaml:"passphrase"` + Region string `yaml:"region"` + TenancyID string `yaml:"tenancy"` + UserID string `yaml:"user"` + PrivateKey string `yaml:"key"` + Fingerprint string `yaml:"fingerprint"` + Passphrase string `yaml:"passphrase"` + UseInstancePrincipal bool `yaml:"useInstancePrincipal"` } // OCIConfig holds the configuration for the OCI Provider. @@ -87,14 +89,25 @@ func LoadOCIConfig(path string) (*OCIConfig, error) { // NewOCIProvider initializes a new OCI DNS based Provider. func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (*OCIProvider, error) { var client ociDNSClient - client, err := dns.NewDnsClientWithConfigurationProvider(common.NewRawConfigurationProvider( - cfg.Auth.TenancyID, - cfg.Auth.UserID, - cfg.Auth.Region, - cfg.Auth.Fingerprint, - cfg.Auth.PrivateKey, - &cfg.Auth.Passphrase, - )) + var err error + var configProvider common.ConfigurationProvider + if cfg.Auth.UseInstancePrincipal { + configProvider, err = auth.InstancePrincipalConfigurationProvider() + if err != nil { + return nil, errors.Wrap(err, "error creating OCI instance principal config provider") + } + } else { + configProvider = common.NewRawConfigurationProvider( + cfg.Auth.TenancyID, + cfg.Auth.UserID, + cfg.Auth.Region, + cfg.Auth.Fingerprint, + cfg.Auth.PrivateKey, + &cfg.Auth.Passphrase, + ) + } + + client, err = dns.NewDnsClientWithConfigurationProvider(configProvider) if err != nil { return nil, errors.Wrap(err, "initializing OCI DNS API client") } diff --git a/provider/oci/oci_test.go b/provider/oci/oci_test.go index 01bed1d97..c11781765 100644 --- a/provider/oci/oci_test.go +++ b/provider/oci/oci_test.go @@ -19,10 +19,11 @@ package oci import ( "context" "sort" + "strings" "testing" - "github.com/oracle/oci-go-sdk/common" - "github.com/oracle/oci-go-sdk/dns" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/dns" "github.com/pkg/errors" "github.com/stretchr/testify/require" @@ -167,6 +168,17 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K }, }, }, + "instance-principal": { + // testing the InstancePrincipalConfigurationProvider is tricky outside of an OCI context, because it tries + // to request a token from the internal OCI systems; this test-case just confirms that the expected error is + // observed, confirming that the instance-principal provider was instantiated. + config: OCIConfig{ + Auth: OCIAuthConfig{ + UseInstancePrincipal: true, + }, + }, + err: errors.New("error creating OCI instance principal config provider: failed to create a new key provider for instance principal"), + }, "invalid": { config: OCIConfig{ Auth: OCIAuthConfig{ @@ -192,7 +204,8 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K if err == nil { require.NoError(t, err) } else { - require.Equal(t, tc.err.Error(), err.Error()) + // have to use prefix testing because the expected instance-principal error strings vary after a known prefix + require.Truef(t, strings.HasPrefix(err.Error(), tc.err.Error()), "observed: %s", err.Error()) } }) } diff --git a/provider/pdns/pdns.go b/provider/pdns/pdns.go index 6f9ca0266..ff8611dc3 100644 --- a/provider/pdns/pdns.go +++ b/provider/pdns/pdns.go @@ -315,7 +315,6 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet if ep.RecordType == "CNAME" { t = provider.EnsureTrailingDot(t) } - records = append(records, pgo.Record{Content: t}) } rrset := pgo.RrSet{ diff --git a/provider/recordfilter.go b/provider/recordfilter.go index 245d47570..97d4f11e8 100644 --- a/provider/recordfilter.go +++ b/provider/recordfilter.go @@ -17,10 +17,10 @@ limitations under the License. package provider // SupportedRecordType returns true only for supported record types. -// Currently A, CNAME, SRV, TXT and NS record types are supported. +// Currently A, AAAA, CNAME, SRV, TXT and NS record types are supported. func SupportedRecordType(recordType string) bool { switch recordType { - case "A", "CNAME", "SRV", "TXT", "NS": + case "A", "AAAA", "CNAME", "SRV", "TXT", "NS": return true default: return false diff --git a/provider/recordfilter_test.go b/provider/recordfilter_test.go index d55e07deb..f482dd1cd 100644 --- a/provider/recordfilter_test.go +++ b/provider/recordfilter_test.go @@ -27,6 +27,10 @@ func TestRecordTypeFilter(t *testing.T) { "A", true, }, + { + "AAAA", + true, + }, { "CNAME", true, diff --git a/provider/transip/transip_test.go b/provider/transip/transip_test.go index a75695282..31d13cb4a 100644 --- a/provider/transip/transip_test.go +++ b/provider/transip/transip_test.go @@ -256,7 +256,7 @@ func TestProviderRecords(t *testing.T) { endpoints, err := p.Records(context.TODO()) if assert.NoError(t, err) { - if assert.Equal(t, 2, len(endpoints)) { + if assert.Equal(t, 4, len(endpoints)) { assert.Equal(t, "www.example.org", endpoints[0].DNSName) assert.EqualValues(t, "@", endpoints[0].Targets[0]) assert.Equal(t, "CNAME", endpoints[0].RecordType) diff --git a/registry/aws_sd_registry.go b/registry/aws_sd_registry.go index a2ff3350d..f0fef584d 100644 --- a/registry/aws_sd_registry.go +++ b/registry/aws_sd_registry.go @@ -55,7 +55,7 @@ func (sdr *AWSSDRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, er } for _, record := range records { - labels, err := endpoint.NewLabelsFromString(record.Labels[endpoint.AWSSDDescriptionLabel]) + labels, err := endpoint.NewLabelsFromStringPlain(record.Labels[endpoint.AWSSDDescriptionLabel]) if err != nil { // if we fail to parse the output then simply assume the endpoint is not managed by any instance of External DNS record.Labels = endpoint.NewLabels() @@ -96,7 +96,7 @@ func (sdr *AWSSDRegistry) updateLabels(endpoints []*endpoint.Endpoint) { ep.Labels = make(map[string]string) } ep.Labels[endpoint.OwnerLabelKey] = sdr.ownerID - ep.Labels[endpoint.AWSSDDescriptionLabel] = ep.Labels.Serialize(false) + ep.Labels[endpoint.AWSSDDescriptionLabel] = ep.Labels.SerializePlain(false) } } diff --git a/registry/txt.go b/registry/txt.go index faa3b7453..2c7cccbdf 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -52,13 +52,27 @@ type TXTRegistry struct { // missingTXTRecords stores TXT records which are missing after the migration to the new format missingTXTRecords []*endpoint.Endpoint + + // encrypt text records + txtEncryptEnabled bool + txtEncryptAESKey []byte } +const keySuffixAAAA = ":AAAA" + // NewTXTRegistry returns new TXTRegistry object -func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string, managedRecordTypes []string) (*TXTRegistry, error) { +func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string, managedRecordTypes []string, txtEncryptEnabled bool, txtEncryptAESKey []byte) (*TXTRegistry, error) { if ownerID == "" { return nil, errors.New("owner id cannot be empty") } + if len(txtEncryptAESKey) == 0 { + txtEncryptAESKey = nil + } else if len(txtEncryptAESKey) != 32 { + return nil, errors.New("the AES Encryption key must have a length of 32 bytes") + } + if txtEncryptEnabled && txtEncryptAESKey == nil { + return nil, errors.New("the AES Encryption key must be set when TXT record encryption is enabled") + } if len(txtPrefix) > 0 && len(txtSuffix) > 0 { return nil, errors.New("txt-prefix and txt-suffix are mutual exclusive") @@ -73,11 +87,13 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st cacheInterval: cacheInterval, wildcardReplacement: txtWildcardReplacement, managedRecordTypes: managedRecordTypes, + txtEncryptEnabled: txtEncryptEnabled, + txtEncryptAESKey: txtEncryptAESKey, }, nil } func getSupportedTypes() []string { - return []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS} + return []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS} } func (im *TXTRegistry) GetDomainFilter() endpoint.DomainFilterInterface { @@ -112,7 +128,7 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error continue } // We simply assume that TXT records for the registry will always have only one target. - labels, err := endpoint.NewLabelsFromString(record.Targets[0]) + labels, err := endpoint.NewLabelsFromString(record.Targets[0], im.txtEncryptAESKey) if err == endpoint.ErrInvalidHeritage { // if no heritage is found or it is invalid // case when value of txt record cannot be identified @@ -123,7 +139,11 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error if err != nil { return nil, err } - key := fmt.Sprintf("%s::%s", im.mapper.toEndpointName(record.DNSName), record.SetIdentifier) + endpointName, isAAAA := im.mapper.toEndpointName(record.DNSName) + key := fmt.Sprintf("%s::%s", endpointName, record.SetIdentifier) + if isAAAA { + key += keySuffixAAAA + } labelMap[key] = labels txtRecordsMap[record.DNSName] = struct{}{} } @@ -139,6 +159,9 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error } dnsName := strings.Join(dnsNameSplit, ".") key := fmt.Sprintf("%s::%s", dnsName, ep.SetIdentifier) + if ep.RecordType == endpoint.RecordTypeAAAA { + key += keySuffixAAAA + } if labels, ok := labelMap[key]; ok { for k, v := range labels { ep.Labels[k] = v @@ -194,17 +217,18 @@ func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpo endpoints := make([]*endpoint.Endpoint, 0) - // old TXT record format - txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)) - if txt != nil { - txt.WithSetIdentifier(r.SetIdentifier) - txt.Labels[endpoint.OwnedRecordLabelKey] = r.DNSName - txt.ProviderSpecific = r.ProviderSpecific - endpoints = append(endpoints, txt) + if r.RecordType != endpoint.RecordTypeAAAA { + // old TXT record format + txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true, im.txtEncryptEnabled, im.txtEncryptAESKey)) + if txt != nil { + txt.WithSetIdentifier(r.SetIdentifier) + txt.Labels[endpoint.OwnedRecordLabelKey] = r.DNSName + txt.ProviderSpecific = r.ProviderSpecific + endpoints = append(endpoints, txt) + } } - // new TXT record format (containing record type) - txtNew := endpoint.NewEndpoint(im.mapper.toNewTXTName(r.DNSName, r.RecordType), endpoint.RecordTypeTXT, r.Labels.Serialize(true)) + txtNew := endpoint.NewEndpoint(im.mapper.toNewTXTName(r.DNSName, r.RecordType), endpoint.RecordTypeTXT, r.Labels.Serialize(true, im.txtEncryptEnabled, im.txtEncryptAESKey)) if txtNew != nil { txtNew.WithSetIdentifier(r.SetIdentifier) txtNew.Labels[endpoint.OwnedRecordLabelKey] = r.DNSName @@ -290,12 +314,12 @@ func (im *TXTRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoi */ /** - nameMapper defines interface which maps the dns name defined for the source - to the dns name which TXT record will be created with + nameMapper is the interface for mapping between the endpoint for the source + and the endpoint for the TXT record. */ type nameMapper interface { - toEndpointName(string) string + toEndpointName(string) (endpointName string, isAAAA bool) toTXTName(string) string toNewTXTName(string, string) string } @@ -312,14 +336,14 @@ func newaffixNameMapper(prefix, suffix, wildcardReplacement string) affixNameMap return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix), wildcardReplacement: strings.ToLower(wildcardReplacement)} } -func dropRecordType(name string) string { +func extractRecordType(name string) (baseName, recordType string) { nameS := strings.Split(name, "-") for _, t := range getSupportedTypes() { if nameS[0] == strings.ToLower(t) { - return strings.TrimPrefix(name, nameS[0]+"-") + return strings.TrimPrefix(name, nameS[0]+"-"), t } } - return name + return name, "" } // dropAffix strips TXT record to find an endpoint name it manages @@ -362,20 +386,20 @@ func (pr affixNameMapper) isSuffix() bool { return len(pr.prefix) == 0 && len(pr.suffix) > 0 } -func (pr affixNameMapper) toEndpointName(txtDNSName string) string { - lowerDNSName := dropRecordType(strings.ToLower(txtDNSName)) +func (pr affixNameMapper) toEndpointName(txtDNSName string) (endpointName string, isAAAA bool) { + lowerDNSName, recordType := extractRecordType(strings.ToLower(txtDNSName)) // drop prefix if strings.HasPrefix(lowerDNSName, pr.prefix) && pr.isPrefix() { - return pr.dropAffix(lowerDNSName) + return pr.dropAffix(lowerDNSName), recordType == endpoint.RecordTypeAAAA } // drop suffix if pr.isSuffix() { DNSName := strings.SplitN(lowerDNSName, ".", 2) - return pr.dropAffix(DNSName[0]) + "." + DNSName[1] + return pr.dropAffix(DNSName[0]) + "." + DNSName[1], recordType == endpoint.RecordTypeAAAA } - return "" + return "", false } func (pr affixNameMapper) toTXTName(endpointDNSName string) string { diff --git a/registry/txt_test.go b/registry/txt_test.go index 5e8168045..80075c81d 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -46,20 +46,20 @@ func TestTXTRegistry(t *testing.T) { func testTXTRegistryNew(t *testing.T) { p := inmemory.NewInMemoryProvider() - _, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}) + _, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}, false, nil) require.Error(t, err) - _, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "", []string{}) + _, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "", []string{}, false, nil) require.Error(t, err) - r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}) + r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}, false, nil) require.NoError(t, err) assert.Equal(t, p, r.provider) - r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "", []string{}) + r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "", []string{}, false, nil) require.NoError(t, err) - _, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "", []string{}) + _, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "", []string{}, false, nil) require.Error(t, err) _, ok := r.mapper.(affixNameMapper) @@ -67,7 +67,17 @@ func testTXTRegistryNew(t *testing.T) { assert.Equal(t, "owner", r.ownerID) assert.Equal(t, p, r.provider) - r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) + aesKey := []byte(";k&l)nUC/33:{?d{3)54+,AD?]SX%yh^") + _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, false, nil) + require.NoError(t, err) + + _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, false, aesKey) + require.NoError(t, err) + + _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, true, nil) + require.Error(t, err) + + r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, true, aesKey) require.NoError(t, err) _, ok = r.mapper.(affixNameMapper) @@ -101,6 +111,10 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) { 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, ""), + newEndpointWithOwner("dualstack.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""), + newEndpointWithOwner("txt.dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""), + newEndpointWithOwner("aaaa-txt.dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ @@ -181,15 +195,31 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) { endpoint.OwnerLabelKey: "owner", }, }, + { + DNSName: "dualstack.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "dualstack.test-zone.example.org", + Targets: endpoint.Targets{"2001:DB8::1"}, + RecordType: endpoint.RecordTypeAAAA, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner-2", + }, + }, } - r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}) + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, false, nil) records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) // Ensure prefix is case-insensitive - r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "", []string{}) + r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "", []string{}, false, nil) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) @@ -214,6 +244,10 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) { newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"), newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"), newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"), + newEndpointWithOwner("dualstack.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""), + newEndpointWithOwner("dualstack-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""), + newEndpointWithOwner("aaaa-dualstack-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ @@ -286,15 +320,31 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) { endpoint.OwnerLabelKey: "", }, }, + { + DNSName: "dualstack.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "dualstack.test-zone.example.org", + Targets: endpoint.Targets{"2001:DB8::1"}, + RecordType: endpoint.RecordTypeAAAA, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner-2", + }, + }, } - r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}) + r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, false, nil) records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) // Ensure prefix is case-insensitive - r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "", []string{}) + r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "", []string{}, false, nil) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) @@ -315,6 +365,10 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("dualstack.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""), + newEndpointWithOwner("dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("dualstack.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, ""), + newEndpointWithOwner("aaaa-dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ @@ -367,9 +421,25 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { endpoint.OwnerLabelKey: "owner", }, }, + { + DNSName: "dualstack.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "dualstack.test-zone.example.org", + Targets: endpoint.Targets{"2001:DB8::1"}, + RecordType: endpoint.RecordTypeAAAA, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner-2", + }, + }, } - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, false, nil) records, _ := r.Records(ctx) assert.True(t, testutils.SameEndpoints(records, expectedRecords)) @@ -412,7 +482,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { newEndpointWithOwner("txt.cname-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, "", []string{}) + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, false, nil) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -501,7 +571,7 @@ func testTXTRegistryApplyChangesWithTemplatedPrefix(t *testing.T) { p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{}, }) - r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "", []string{}) + r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "", []string{}, false, nil) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), @@ -545,7 +615,7 @@ func testTXTRegistryApplyChangesWithTemplatedSuffix(t *testing.T) { p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) } - r, _ := NewTXTRegistry(p, "", "-%{record_type}suffix", "owner", time.Hour, "", []string{}) + r, _ := NewTXTRegistry(p, "", "-%{record_type}suffix", "owner", time.Hour, "", []string{}, false, nil) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), @@ -611,7 +681,7 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) { newEndpointWithOwner("cname-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, "wildcard", []string{}) + r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard", []string{}, false, nil) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -715,7 +785,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, false, nil) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -881,7 +951,7 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, false, nil) records, _ := r.Records(ctx) missingRecords := r.MissingRecords() @@ -985,7 +1055,7 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { }, } - r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}) + r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, false, nil) records, _ := r.Records(ctx) missingRecords := r.MissingRecords() @@ -1012,6 +1082,7 @@ func TestCacheMethods(t *testing.T) { newEndpointWithOwner("thing2.com", "1.2.3.4", "CNAME", "owner"), newEndpointWithOwner("thing3.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"), + newEndpointWithOwner("thing4.com", "2001:DB8::1", "AAAA", "owner"), newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"), } @@ -1022,6 +1093,7 @@ func TestCacheMethods(t *testing.T) { newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"), newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"), newEndpointWithOwner("thing.com", "1.2.3.6", "A", "owner2"), + newEndpointWithOwner("thing4.com", "2001:DB8::2", "AAAA", "owner"), } expectedCacheAfterDelete := []*endpoint.Endpoint{ @@ -1032,6 +1104,7 @@ func TestCacheMethods(t *testing.T) { newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"), } // test add cache + registry.addToCache(newEndpointWithOwner("thing4.com", "2001:DB8::1", "AAAA", "owner")) registry.addToCache(newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner")) if !reflect.DeepEqual(expectedCacheAfterAdd, registry.recordsCache) { @@ -1041,6 +1114,8 @@ func TestCacheMethods(t *testing.T) { // test update cache registry.removeFromCache(newEndpointWithOwner("thing.com", "1.2.3.4", "A", "owner")) registry.addToCache(newEndpointWithOwner("thing.com", "1.2.3.6", "A", "owner2")) + registry.removeFromCache(newEndpointWithOwner("thing4.com", "2001:DB8::1", "AAAA", "owner")) + registry.addToCache(newEndpointWithOwner("thing4.com", "2001:DB8::2", "AAAA", "owner")) // ensure it was updated if !reflect.DeepEqual(expectedCacheAfterUpdate, registry.recordsCache) { t.Fatalf("expected endpoints should match endpoints from cache: expected %v, but got %v", expectedCacheAfterUpdate, registry.recordsCache) @@ -1048,6 +1123,7 @@ func TestCacheMethods(t *testing.T) { // test deleting a record registry.removeFromCache(newEndpointWithOwner("thing.com", "1.2.3.6", "A", "owner2")) + registry.removeFromCache(newEndpointWithOwner("thing4.com", "2001:DB8::2", "AAAA", "owner")) // ensure it was deleted if !reflect.DeepEqual(expectedCacheAfterDelete, registry.recordsCache) { t.Fatalf("expected endpoints should match endpoints from cache: expected %v, but got %v", expectedCacheAfterDelete, registry.recordsCache) @@ -1075,11 +1151,40 @@ func TestDropSuffix(t *testing.T) { assert.Equal(t, expectedARecord, actualARecord) } -func TestDropRecordType(t *testing.T) { - r := "ns-zone.example.com" - expectedRecord := "zone.example.com" - actualRecord := dropRecordType(r) - assert.Equal(t, expectedRecord, actualRecord) +func TestExtractRecordType(t *testing.T) { + tests := []struct { + input string + expectedName string + expectedType string + }{ + { + input: "ns-zone.example.com", + expectedName: "zone.example.com", + expectedType: "NS", + }, + { + input: "aaaa-zone.example.com", + expectedName: "zone.example.com", + expectedType: "AAAA", + }, + { + input: "ptr-zone.example.com", + expectedName: "ptr-zone.example.com", + expectedType: "", + }, + { + input: "zone.example.com", + expectedName: "zone.example.com", + expectedType: "", + }, + } + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + actualName, actualType := extractRecordType(tc.input) + assert.Equal(t, tc.expectedName, actualName) + assert.Equal(t, tc.expectedType, actualType) + }) + } } func TestNewTXTScheme(t *testing.T) { @@ -1104,7 +1209,7 @@ func TestNewTXTScheme(t *testing.T) { newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, false, nil) changes := &plan.Changes{ Create: []*endpoint.Endpoint{ @@ -1180,7 +1285,26 @@ func TestGenerateTXT(t *testing.T) { } p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, false, nil) + gotTXT := r.generateTXTRecord(record) + assert.Equal(t, expectedTXT, gotTXT) +} + +func TestGenerateTXTForAAAA(t *testing.T) { + record := newEndpointWithOwner("foo.test-zone.example.org", "2001:DB8::1", endpoint.RecordTypeAAAA, "owner") + expectedTXT := []*endpoint.Endpoint{ + { + DNSName: "aaaa-foo.test-zone.example.org", + Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, + RecordType: endpoint.RecordTypeTXT, + Labels: map[string]string{ + endpoint.OwnedRecordLabelKey: "foo.test-zone.example.org", + }, + }, + } + p := inmemory.NewInMemoryProvider() + p.CreateZone(testZone) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, false, nil) gotTXT := r.generateTXTRecord(record) assert.Equal(t, expectedTXT, gotTXT) } @@ -1197,7 +1321,7 @@ func TestFailGenerateTXT(t *testing.T) { expectedTXT := []*endpoint.Endpoint{} p := inmemory.NewInMemoryProvider() p.CreateZone(testZone) - r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}) + r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, false, nil) gotTXT := r.generateTXTRecord(cnameRecord) assert.Equal(t, expectedTXT, gotTXT) } diff --git a/source/compatibility.go b/source/compatibility.go index bc6e19abf..1953b76ca 100644 --- a/source/compatibility.go +++ b/source/compatibility.go @@ -157,11 +157,13 @@ func legacyEndpointsFromDNSControllerNodePortService(svc *v1.Service, sc *servic continue } for _, address := range node.Status.Addresses { - if address.Type == v1.NodeExternalIP && isExternal { - endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, address.Address)) + recordType := suitableType(address.Address) + // IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well. + if isExternal && (address.Type == v1.NodeExternalIP || (address.Type == v1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA)) { + endpoints = append(endpoints, endpoint.NewEndpoint(hostname, recordType, address.Address)) } - if address.Type == v1.NodeInternalIP && isInternal { - endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, address.Address)) + if isInternal && address.Type == v1.NodeInternalIP { + endpoints = append(endpoints, endpoint.NewEndpoint(hostname, recordType, address.Address)) } } } diff --git a/source/contour_httpproxy_test.go b/source/contour_httpproxy_test.go index 1905d0eee..e90bd059a 100644 --- a/source/contour_httpproxy_test.go +++ b/source/contour_httpproxy_test.go @@ -220,8 +220,9 @@ func testEndpointsFromHTTPProxy(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -235,8 +236,9 @@ func testEndpointsFromHTTPProxy(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -251,12 +253,14 @@ func testEndpointsFromHTTPProxy(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, }, { - DNSName: "foo.bar", - Targets: endpoint.Targets{"elb.com", "alb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"elb.com", "alb.com"}, }, }, }, @@ -342,20 +346,24 @@ func testHTTPProxyEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -380,20 +388,24 @@ func testHTTPProxyEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -418,12 +430,14 @@ func testHTTPProxyEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -446,8 +460,9 @@ func testHTTPProxyEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -509,8 +524,9 @@ func testHTTPProxyEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -551,8 +567,9 @@ func testHTTPProxyEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -593,12 +610,14 @@ func testHTTPProxyEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "fake1.ext-dns.test.com", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "fake1.ext-dns.test.com", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "fake1.ext-dns.test.com", - Targets: endpoint.Targets{"elb.com"}, + DNSName: "fake1.ext-dns.test.com", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"elb.com"}, }, }, fqdnTemplate: "{{.Name}}.ext-dns.test.com", @@ -882,19 +901,22 @@ func testHTTPProxyEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"httpproxy-target.com"}, - RecordTTL: endpoint.TTL(6), + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"httpproxy-target.com"}, + RecordTTL: endpoint.TTL(6), }, { - DNSName: "example2.org", - Targets: endpoint.Targets{"httpproxy-target.com"}, - RecordTTL: endpoint.TTL(1), + DNSName: "example2.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"httpproxy-target.com"}, + RecordTTL: endpoint.TTL(1), }, { - DNSName: "example3.org", - Targets: endpoint.Targets{"httpproxy-target.com"}, - RecordTTL: endpoint.TTL(10), + DNSName: "example3.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"httpproxy-target.com"}, + RecordTTL: endpoint.TTL(10), }, }, }, @@ -997,20 +1019,24 @@ func testHTTPProxyEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, ignoreHostnameAnnotation: true, diff --git a/source/crd.go b/source/crd.go index 705d770bc..b32b93e6e 100644 --- a/source/crd.go +++ b/source/crd.go @@ -22,6 +22,10 @@ import ( "os" "strings" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/tools/cache" + log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -44,6 +48,7 @@ type crdSource struct { codec runtime.ParameterCodec annotationFilter string labelSelector labels.Selector + informer *cache.SharedInformer } func addKnownTypes(scheme *runtime.Scheme, groupVersion schema.GroupVersion) error { @@ -103,18 +108,55 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiS } // NewCRDSource creates a new crdSource with the given config. -func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFilter string, labelSelector labels.Selector, scheme *runtime.Scheme) (Source, error) { - return &crdSource{ +func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFilter string, labelSelector labels.Selector, scheme *runtime.Scheme, startInformer bool) (Source, error) { + sourceCrd := crdSource{ crdResource: strings.ToLower(kind) + "s", namespace: namespace, annotationFilter: annotationFilter, labelSelector: labelSelector, crdClient: crdClient, codec: runtime.NewParameterCodec(scheme), - }, nil + } + if startInformer { + // external-dns already runs its sync-handler periodically (controlled by `--interval` flag) to ensure any + // missed or dropped events are handled. specify a resync period 0 to avoid unnecessary sync handler invocations. + informer := cache.NewSharedInformer( + &cache.ListWatch{ + ListFunc: func(lo metav1.ListOptions) (result runtime.Object, err error) { + return sourceCrd.List(context.TODO(), &lo) + }, + WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { + return sourceCrd.watch(context.TODO(), &lo) + }, + }, + &endpoint.DNSEndpoint{}, + 0) + sourceCrd.informer = &informer + go informer.Run(wait.NeverStop) + } + return &sourceCrd, nil } func (cs *crdSource) AddEventHandler(ctx context.Context, handler func()) { + if cs.informer != nil { + log.Debug("Adding event handler for CRD") + // Right now there is no way to remove event handler from informer, see: + // https://github.com/kubernetes/kubernetes/issues/79610 + informer := *cs.informer + informer.AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + handler() + }, + UpdateFunc: func(old interface{}, new interface{}) { + handler() + }, + DeleteFunc: func(obj interface{}) { + handler() + }, + }, + ) + } } // Endpoints returns endpoint objects. @@ -189,6 +231,15 @@ func (cs *crdSource) setResourceLabel(crd *endpoint.DNSEndpoint, endpoints []*en } } +func (cs *crdSource) watch(ctx context.Context, opts *metav1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return cs.crdClient.Get(). + Namespace(cs.namespace). + Resource(cs.crdResource). + VersionedParams(opts, cs.codec). + Watch(ctx) +} + func (cs *crdSource) List(ctx context.Context, opts *metav1.ListOptions) (result *endpoint.DNSEndpointList, err error) { result = &endpoint.DNSEndpointList{} err = cs.crdClient.Get(). diff --git a/source/crd_test.go b/source/crd_test.go index aaada1802..4f26aacfa 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -398,7 +398,12 @@ func testCRDSourceEndpoints(t *testing.T) { labelSelector, err := labels.Parse(ti.labelFilter) require.NoError(t, err) - cs, err := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, labelSelector, scheme) + // At present, client-go's fake.RESTClient (used by crd_test.go) is known to cause race conditions when used + // with informers: https://github.com/kubernetes/kubernetes/issues/95372 + // So don't start the informer during testing. + startInformer := false + + cs, err := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, labelSelector, scheme, startInformer) require.NoError(t, err) receivedEndpoints, err := cs.Endpoints(context.Background()) diff --git a/source/ingress.go b/source/ingress.go index 76d5429e7..af7a9dc99 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -18,6 +18,7 @@ package source import ( "context" + "errors" "fmt" "sort" "strings" @@ -26,6 +27,7 @@ import ( log "github.com/sirupsen/logrus" networkv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" kubeinformers "k8s.io/client-go/informers" netinformers "k8s.io/client-go/informers/networking/v1" "k8s.io/client-go/kubernetes" @@ -43,6 +45,8 @@ const ( // Possible values for the ingress-hostname-source annotation IngressHostnameSourceAnnotationOnlyValue = "annotation-only" IngressHostnameSourceDefinedHostsOnlyValue = "defined-hosts-only" + + IngressClassAnnotationKey = "kubernetes.io/ingress.class" ) // ingressSource is an implementation of Source for Kubernetes ingress objects. @@ -53,6 +57,7 @@ type ingressSource struct { client kubernetes.Interface namespace string annotationFilter string + ingressClassNames []string fqdnTemplate *template.Template combineFQDNAnnotation bool ignoreHostnameAnnotation bool @@ -63,12 +68,27 @@ type ingressSource struct { } // NewIngressSource creates a new ingressSource with the given config. -func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector) (Source, error) { +func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector, ingressClassNames []string) (Source, error) { tmpl, err := parseTemplate(fqdnTemplate) if err != nil { return nil, err } + // ensure that ingress class is only set in either the ingressClassNames or + // annotationFilter but not both + if ingressClassNames != nil && annotationFilter != "" { + selector, err := getLabelSelector(annotationFilter) + if err != nil { + return nil, err + } + + requirements, _ := selector.Requirements() + for _, requirement := range requirements { + if requirement.Key() == "kubernetes.io/ingress.class" { + return nil, errors.New("--ingress-class is mutually exclusive with the kubernetes.io/ingress.class annotation filter") + } + } + } // Use shared informer to listen for add/update/delete of ingresses in the specified namespace. // Set resync period to 0, to prevent processing when nothing has changed. informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace)) @@ -93,6 +113,7 @@ func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, name client: kubeClient, namespace: namespace, annotationFilter: annotationFilter, + ingressClassNames: ingressClassNames, fqdnTemplate: tmpl, combineFQDNAnnotation: combineFqdnAnnotation, ignoreHostnameAnnotation: ignoreHostnameAnnotation, @@ -116,6 +137,11 @@ func (sc *ingressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e return nil, err } + ingresses, err = sc.filterByIngressClass(ingresses) + if err != nil { + return nil, err + } + endpoints := []*endpoint.Endpoint{} for _, ing := range ingresses { @@ -210,6 +236,50 @@ func (sc *ingressSource) filterByAnnotations(ingresses []*networkv1.Ingress) ([] return filteredList, nil } +// filterByIngressClass filters a list of ingresses based on a required ingress +// class +func (sc *ingressSource) filterByIngressClass(ingresses []*networkv1.Ingress) ([]*networkv1.Ingress, error) { + // if no class filter is specified then there's nothing to do + if len(sc.ingressClassNames) == 0 { + return ingresses, nil + } + + classNameReq, err := labels.NewRequirement(IngressClassAnnotationKey, selection.In, sc.ingressClassNames) + if err != nil { + return nil, err + } + + selector := labels.NewSelector() + selector = selector.Add(*classNameReq) + + filteredList := []*networkv1.Ingress{} + + for _, ingress := range ingresses { + var matched = false + + for _, nameFilter := range sc.ingressClassNames { + if ingress.Spec.IngressClassName != nil && len(*ingress.Spec.IngressClassName) > 0 { + if nameFilter == *ingress.Spec.IngressClassName { + matched = true + } + } else if matchLabelSelector(selector, ingress.Annotations) { + matched = true + } + + if matched { + filteredList = append(filteredList, ingress) + break + } + } + + if !matched { + log.Debugf("Discarding ingress %s/%s because it does not match required ingress classes %v", ingress.Namespace, ingress.Name, sc.ingressClassNames) + } + } + + return filteredList, nil +} + func (sc *ingressSource) setResourceLabel(ingress *networkv1.Ingress, endpoints []*endpoint.Endpoint) { for _, ep := range endpoints { ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingress/%s/%s", ingress.Namespace, ingress.Name) diff --git a/source/ingress_test.go b/source/ingress_test.go index 6c345d09b..2be3eddfd 100644 --- a/source/ingress_test.go +++ b/source/ingress_test.go @@ -65,6 +65,7 @@ func (suite *IngressSuite) SetupTest() { false, false, labels.Everything(), + []string{}, ) suite.NoError(err, "should initialize ingress source") } @@ -101,6 +102,7 @@ func TestNewIngressSource(t *testing.T) { fqdnTemplate string combineFQDNAndAnnotation bool expectError bool + ingressClassNames []string }{ { title: "invalid template", @@ -132,6 +134,17 @@ func TestNewIngressSource(t *testing.T) { expectError: false, annotationFilter: "kubernetes.io/ingress.class=nginx", }, + { + title: "non-empty ingress class name list", + expectError: false, + ingressClassNames: []string{"internal", "external"}, + }, + { + title: "ingress class name and annotation filter jointly specified", + expectError: true, + ingressClassNames: []string{"internal", "external"}, + annotationFilter: "kubernetes.io/ingress.class=nginx", + }, } { ti := ti t.Run(ti.title, func(t *testing.T) { @@ -148,6 +161,7 @@ func TestNewIngressSource(t *testing.T) { false, false, labels.Everything(), + ti.ingressClassNames, ) if ti.expectError { assert.Error(t, err) @@ -177,8 +191,9 @@ func testEndpointsFromIngress(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -190,8 +205,9 @@ func testEndpointsFromIngress(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -204,12 +220,14 @@ func testEndpointsFromIngress(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, }, { - DNSName: "foo.bar", - Targets: endpoint.Targets{"elb.com", "alb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"elb.com", "alb.com"}, }, }, }, @@ -270,12 +288,14 @@ func testEndpointsFromIngressHostnameSourceAnnotation(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "foo.baz", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.baz", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -287,8 +307,9 @@ func testEndpointsFromIngressHostnameSourceAnnotation(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -301,12 +322,14 @@ func testEndpointsFromIngressHostnameSourceAnnotation(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "foo.baz", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.baz", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -319,8 +342,9 @@ func testEndpointsFromIngressHostnameSourceAnnotation(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -333,8 +357,9 @@ func testEndpointsFromIngressHostnameSourceAnnotation(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.baz", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.baz", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -363,6 +388,7 @@ func testIngressEndpoints(t *testing.T) { ignoreIngressTLSSpec bool ignoreIngressRulesSpec bool ingressLabelSelector labels.Selector + ingressClassNames []string }{ { title: "no ingress", @@ -387,12 +413,33 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "ipv6 ingress", + targetNamespace: "", + ingressItems: []fakeIngress{ + { + name: "fake1", + namespace: namespace, + dnsnames: []string{"example.org"}, + ips: []string{"2001:DB8::1"}, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + RecordType: endpoint.RecordTypeAAAA, + Targets: endpoint.Targets{"2001:DB8::1"}, }, }, }, @@ -435,12 +482,14 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -463,8 +512,9 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -485,8 +535,9 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -542,8 +593,9 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -580,8 +632,9 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -618,12 +671,14 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "fake1.ext-dns.test.com", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "fake1.ext-dns.test.com", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "fake1.ext-dns.test.com", - Targets: endpoint.Targets{"elb.com"}, + DNSName: "fake1.ext-dns.test.com", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"elb.com"}, }, }, fqdnTemplate: "{{.Name}}.ext-dns.test.com", @@ -973,19 +1028,22 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"ingress-target.com"}, - RecordTTL: endpoint.TTL(6), + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"ingress-target.com"}, + RecordTTL: endpoint.TTL(6), }, { - DNSName: "example2.org", - Targets: endpoint.Targets{"ingress-target.com"}, - RecordTTL: endpoint.TTL(1), + DNSName: "example2.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"ingress-target.com"}, + RecordTTL: endpoint.TTL(1), }, { - DNSName: "example3.org", - Targets: endpoint.Targets{"ingress-target.com"}, - RecordTTL: endpoint.TTL(10), + DNSName: "example3.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"ingress-target.com"}, + RecordTTL: endpoint.TTL(10), }, }, }, @@ -1132,12 +1190,14 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -1169,8 +1229,119 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"1.2.3.4"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"1.2.3.4"}, + }, + }, + }, + { + title: "ingressClassName filtering", + targetNamespace: "", + ingressClassNames: []string{"public", "dmz"}, + ingressItems: []fakeIngress{ + { + name: "none", + namespace: namespace, + tlsdnsnames: [][]string{{"none.example.org"}}, + ips: []string{"1.0.0.0"}, + }, + { + name: "fake-public", + namespace: namespace, + tlsdnsnames: [][]string{{"example.org"}}, + ips: []string{"1.2.3.4"}, + ingressClassName: "public", // match + }, + { + name: "fake-internal", + namespace: namespace, + tlsdnsnames: [][]string{{"int.example.org"}}, + ips: []string{"2.3.4.5"}, + ingressClassName: "internal", + }, + { + name: "fake-dmz", + namespace: namespace, + tlsdnsnames: [][]string{{"dmz.example.org"}}, + ips: []string{"3.4.5.6"}, + ingressClassName: "dmz", // match + }, + { + name: "annotated-dmz", + namespace: namespace, + tlsdnsnames: [][]string{{"annodmz.example.org"}}, + ips: []string{"4.5.6.7"}, + annotations: map[string]string{ + "kubernetes.io/ingress.class": "dmz", // match + }, + }, + { + name: "fake-internal-annotated-dmz", + namespace: namespace, + tlsdnsnames: [][]string{{"int-annodmz.example.org"}}, + ips: []string{"5.6.7.8"}, + annotations: map[string]string{ + "kubernetes.io/ingress.class": "dmz", // match but ignored (non-empty ingressClassName) + }, + ingressClassName: "internal", + }, + { + name: "fake-dmz-annotated-internal", + namespace: namespace, + tlsdnsnames: [][]string{{"dmz-annoint.example.org"}}, + ips: []string{"6.7.8.9"}, + annotations: map[string]string{ + "kubernetes.io/ingress.class": "internal", + }, + ingressClassName: "dmz", // match + }, + { + name: "empty-annotated-dmz", + namespace: namespace, + tlsdnsnames: [][]string{{"empty-annotdmz.example.org"}}, + ips: []string{"7.8.9.0"}, + annotations: map[string]string{ + "kubernetes.io/ingress.class": "dmz", // match (empty ingressClassName) + }, + ingressClassName: "", + }, + { + name: "empty-annotated-internal", + namespace: namespace, + tlsdnsnames: [][]string{{"empty-annotint.example.org"}}, + ips: []string{"8.9.0.1"}, + annotations: map[string]string{ + "kubernetes.io/ingress.class": "internal", + }, + ingressClassName: "", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"1.2.3.4"}, + }, + { + DNSName: "dmz.example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"3.4.5.6"}, + }, + { + DNSName: "annodmz.example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"4.5.6.7"}, + }, + { + DNSName: "dmz-annoint.example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"6.7.8.9"}, + }, + { + DNSName: "empty-annotdmz.example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"7.8.9.0"}, }, }, }, @@ -1189,8 +1360,9 @@ func testIngressEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -1236,6 +1408,7 @@ func testIngressEndpoints(t *testing.T) { ti.ignoreIngressTLSSpec, ti.ignoreIngressRulesSpec, ti.ingressLabelSelector, + ti.ingressClassNames, ) // Informer cache has all of the ingresses. Retrieve and validate their endpoints. res, err := source.Endpoints(context.Background()) @@ -1251,14 +1424,15 @@ func testIngressEndpoints(t *testing.T) { // ingress specific helper functions type fakeIngress struct { - dnsnames []string - tlsdnsnames [][]string - ips []string - hostnames []string - namespace string - name string - annotations map[string]string - labels map[string]string + dnsnames []string + tlsdnsnames [][]string + ips []string + hostnames []string + namespace string + name string + annotations map[string]string + labels map[string]string + ingressClassName string } func (ing fakeIngress) Ingress() *networkv1.Ingress { @@ -1270,7 +1444,8 @@ func (ing fakeIngress) Ingress() *networkv1.Ingress { Labels: ing.labels, }, Spec: networkv1.IngressSpec{ - Rules: []networkv1.IngressRule{}, + Rules: []networkv1.IngressRule{}, + IngressClassName: &ing.ingressClassName, }, Status: networkv1.IngressStatus{ LoadBalancer: networkv1.IngressLoadBalancerStatus{ diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go index fefa46c53..535972a34 100644 --- a/source/istio_gateway_test.go +++ b/source/istio_gateway_test.go @@ -184,8 +184,9 @@ func testEndpointsFromGatewayConfig(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -203,8 +204,9 @@ func testEndpointsFromGatewayConfig(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -222,8 +224,9 @@ func testEndpointsFromGatewayConfig(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -242,12 +245,14 @@ func testEndpointsFromGatewayConfig(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, }, { - DNSName: "foo.bar", - Targets: endpoint.Targets{"elb.com", "alb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"elb.com", "alb.com"}, }, }, }, @@ -310,8 +315,9 @@ func testEndpointsFromGatewayConfig(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com", "lb2.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com", "lb2.com"}, }, }, }, @@ -376,20 +382,24 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -416,20 +426,24 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -452,12 +466,14 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -482,8 +498,9 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -551,8 +568,9 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -597,8 +615,9 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -643,12 +662,14 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "fake1.ext-dns.test.com", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "fake1.ext-dns.test.com", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "fake1.ext-dns.test.com", - Targets: endpoint.Targets{"elb.com"}, + DNSName: "fake1.ext-dns.test.com", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"elb.com"}, }, }, fqdnTemplate: "{{.Name}}.ext-dns.test.com", @@ -948,19 +969,22 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"gateway-target.com"}, - RecordTTL: endpoint.TTL(6), + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"gateway-target.com"}, + RecordTTL: endpoint.TTL(6), }, { - DNSName: "example2.org", - Targets: endpoint.Targets{"gateway-target.com"}, - RecordTTL: endpoint.TTL(1), + DNSName: "example2.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"gateway-target.com"}, + RecordTTL: endpoint.TTL(1), }, { - DNSName: "example3.org", - Targets: endpoint.Targets{"gateway-target.com"}, - RecordTTL: endpoint.TTL(10), + DNSName: "example3.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"gateway-target.com"}, + RecordTTL: endpoint.TTL(10), }, }, }, @@ -1069,20 +1093,24 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, ignoreHostnameAnnotation: true, @@ -1137,12 +1165,14 @@ func testGatewayEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "fake1.dns-through-hostname.com", - Targets: endpoint.Targets{"1.2.3.4"}, + DNSName: "fake1.dns-through-hostname.com", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"1.2.3.4"}, }, { - DNSName: "fake2.dns-through-hostname.com", - Targets: endpoint.Targets{"1.2.3.4"}, + DNSName: "fake2.dns-through-hostname.com", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"1.2.3.4"}, }, }, }, diff --git a/source/istio_virtualservice_test.go b/source/istio_virtualservice_test.go index 8f1aa6d81..a510094e7 100644 --- a/source/istio_virtualservice_test.go +++ b/source/istio_virtualservice_test.go @@ -394,8 +394,9 @@ func testEndpointsFromVirtualServiceConfig(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -416,8 +417,9 @@ func testEndpointsFromVirtualServiceConfig(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -439,12 +441,14 @@ func testEndpointsFromVirtualServiceConfig(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, }, { - DNSName: "foo.bar", - Targets: endpoint.Targets{"elb.com", "alb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"elb.com", "alb.com"}, }, }, }, @@ -543,8 +547,9 @@ func testEndpointsFromVirtualServiceConfig(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "foo.bar", - Targets: endpoint.Targets{"elb.com", "alb.com"}, + DNSName: "foo.bar", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"elb.com", "alb.com"}, }, }, }, @@ -618,20 +623,24 @@ func testVirtualServiceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -665,8 +674,9 @@ func testVirtualServiceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -702,20 +712,24 @@ func testVirtualServiceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -752,12 +766,14 @@ func testVirtualServiceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, }, @@ -790,8 +806,9 @@ func testVirtualServiceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -858,8 +875,9 @@ func testVirtualServiceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, @@ -917,12 +935,14 @@ func testVirtualServiceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "vs1.ext-dns.test.com", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "vs1.ext-dns.test.com", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "vs1.ext-dns.test.com", - Targets: endpoint.Targets{"elb.com"}, + DNSName: "vs1.ext-dns.test.com", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"elb.com"}, }, }, fqdnTemplate: "{{.Name}}.ext-dns.test.com", @@ -1206,14 +1226,16 @@ func testVirtualServiceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, - RecordTTL: endpoint.TTL(6), + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + RecordTTL: endpoint.TTL(6), }, { - DNSName: "example2.org", - Targets: endpoint.Targets{"8.8.8.8"}, - RecordTTL: endpoint.TTL(1), + DNSName: "example2.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + RecordTTL: endpoint.TTL(1), }, }, }, @@ -1322,20 +1344,24 @@ func testVirtualServiceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "example.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "example.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "example.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "new.org", - Targets: endpoint.Targets{"lb.com"}, + DNSName: "new.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, }, }, ignoreHostnameAnnotation: true, diff --git a/source/node.go b/source/node.go index b0e672d73..5e287e9a0 100644 --- a/source/node.go +++ b/source/node.go @@ -88,7 +88,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro return nil, err } - endpoints := map[string]*endpoint.Endpoint{} + endpoints := map[endpointKey]*endpoint.Endpoint{} // create endpoints for all nodes for _, node := range nodes { @@ -109,8 +109,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro // create new endpoint with the information we already have ep := &endpoint.Endpoint{ - RecordType: "A", // hardcoded DNS record type - RecordTTL: ttl, + RecordTTL: ttl, } if ns.fqdnTemplate != nil { @@ -134,14 +133,19 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro return nil, fmt.Errorf("failed to get node address from %s: %s", node.Name, err.Error()) } - ep.Targets = endpoint.Targets(addrs) ep.Labels = endpoint.NewLabels() - - log.Debugf("adding endpoint %s", ep) - if _, ok := endpoints[ep.DNSName]; ok { - endpoints[ep.DNSName].Targets = append(endpoints[ep.DNSName].Targets, ep.Targets...) - } else { - endpoints[ep.DNSName] = ep + for _, addr := range addrs { + log.Debugf("adding endpoint %s target %s", ep, addr) + key := endpointKey{ + dnsName: ep.DNSName, + recordType: suitableType(addr), + } + if _, ok := endpoints[key]; !ok { + epCopy := *ep + epCopy.RecordType = key.recordType + endpoints[key] = &epCopy + } + endpoints[key].Targets = append(endpoints[key].Targets, addr) } } @@ -163,13 +167,18 @@ func (ns *nodeSource) nodeAddresses(node *v1.Node) ([]string, error) { v1.NodeExternalIP: {}, v1.NodeInternalIP: {}, } + var ipv6Addresses []string for _, addr := range node.Status.Addresses { addresses[addr.Type] = append(addresses[addr.Type], addr.Address) + // IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well. + if addr.Type == v1.NodeInternalIP && suitableType(addr.Address) == endpoint.RecordTypeAAAA { + ipv6Addresses = append(ipv6Addresses, addr.Address) + } } if len(addresses[v1.NodeExternalIP]) > 0 { - return addresses[v1.NodeExternalIP], nil + return append(addresses[v1.NodeExternalIP], ipv6Addresses...), nil } if len(addresses[v1.NodeInternalIP]) > 0 { diff --git a/source/node_test.go b/source/node_test.go index 901c1baa1..885d9f54e 100644 --- a/source/node_test.go +++ b/source/node_test.go @@ -127,6 +127,19 @@ func testNodeSourceEndpoints(t *testing.T) { }, false, }, + { + "ipv6 node with fqdn returns one endpoint", + "", + "", + "node1.example.org", + []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, + map[string]string{}, + map[string]string{}, + []*endpoint.Endpoint{ + {RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}}, + }, + false, + }, { "node with fqdn template returns endpoint with expanded hostname", "", @@ -166,6 +179,20 @@ func testNodeSourceEndpoints(t *testing.T) { }, false, }, + { + "node with fqdn template returns two endpoints with dual-stack IP addresses and expanded hostname", + "", + "{{.Name}}.example.org", + "node1", + []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, + map[string]string{}, + map[string]string{}, + []*endpoint.Endpoint{ + {RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}}, + }, + false, + }, { "node with both external and internal IP returns an endpoint with external IP", "", @@ -179,6 +206,20 @@ func testNodeSourceEndpoints(t *testing.T) { }, false, }, + { + "node with both external, internal, and IPv6 IP returns endpoints with external IPs", + "", + "", + "node1", + []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, + map[string]string{}, + map[string]string{}, + []*endpoint.Endpoint{ + {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}}, + {RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}}, + }, + false, + }, { "node with only internal IP returns an endpoint with internal IP", "", @@ -192,6 +233,20 @@ func testNodeSourceEndpoints(t *testing.T) { }, false, }, + { + "node with only internal IPs returns endpoints with internal IPs", + "", + "", + "node1", + []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, + map[string]string{}, + map[string]string{}, + []*endpoint.Endpoint{ + {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"2.3.4.5"}}, + {RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}}, + }, + false, + }, { "node with neither external nor internal IP returns no endpoints", "", @@ -318,7 +373,7 @@ func testNodeSourceEndpoints(t *testing.T) { false, }, { - "node with nil Lables returns valid endpoint", + "node with nil Labels returns valid endpoint", "", "", "node1", diff --git a/source/openshift_route_test.go b/source/openshift_route_test.go index af6d9fc57..f9f7d31d7 100644 --- a/source/openshift_route_test.go +++ b/source/openshift_route_test.go @@ -197,7 +197,8 @@ func testOcpRouteSourceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "my-domain.com", + DNSName: "my-domain.com", + RecordType: endpoint.RecordTypeCNAME, Targets: []string{ "apps.my-domain.com", }, @@ -230,7 +231,8 @@ func testOcpRouteSourceEndpoints(t *testing.T) { ocpRouterName: "default", expected: []*endpoint.Endpoint{ { - DNSName: "my-domain.com", + DNSName: "my-domain.com", + RecordType: endpoint.RecordTypeCNAME, Targets: []string{ "router-default.my-domain.com", }, @@ -274,7 +276,8 @@ func testOcpRouteSourceEndpoints(t *testing.T) { ocpRouterName: "default", expected: []*endpoint.Endpoint{ { - DNSName: "my-domain.com", + DNSName: "my-domain.com", + RecordType: endpoint.RecordTypeCNAME, Targets: []string{ "router-default.my-domain.com", }, @@ -393,8 +396,11 @@ func testOcpRouteSourceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "my-domain.com", - Targets: []string{"router-test.my-domain.com"}, + DNSName: "my-domain.com", + RecordType: endpoint.RecordTypeCNAME, + Targets: []string{ + "router-test.my-domain.com", + }, }, }, }, @@ -439,7 +445,8 @@ func testOcpRouteSourceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "my-annotation-domain.com", + DNSName: "my-annotation-domain.com", + RecordType: endpoint.RecordTypeCNAME, Targets: []string{ "my.site.foo.com", }, @@ -479,7 +486,8 @@ func testOcpRouteSourceEndpoints(t *testing.T) { }, expected: []*endpoint.Endpoint{ { - DNSName: "my-annotation-domain.com", + DNSName: "my-annotation-domain.com", + RecordType: endpoint.RecordTypeCNAME, Targets: []string{ "my.site.foo.com", }, diff --git a/source/pod.go b/source/pod.go index 36e6ffe50..e399a8906 100644 --- a/source/pod.go +++ b/source/pod.go @@ -82,58 +82,71 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error return nil, err } - domains := make(map[string][]string) + endpointMap := make(map[endpointKey][]string) for _, pod := range pods { if !pod.Spec.HostNetwork { log.Debugf("skipping pod %s. hostNetwork=false", pod.Name) continue } - if domain, ok := pod.Annotations[internalHostnameAnnotationKey]; ok { - if _, ok := domains[domain]; !ok { - domains[domain] = []string{} + if domainAnnotation, ok := pod.Annotations[internalHostnameAnnotationKey]; ok { + domainList := splitHostnameAnnotation(domainAnnotation) + for _, domain := range domainList { + addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP) } - domains[domain] = append(domains[domain], pod.Status.PodIP) } - if domain, ok := pod.Annotations[hostnameAnnotationKey]; ok { - if _, ok := domains[domain]; !ok { - domains[domain] = []string{} - } - - node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName) - for _, address := range node.Status.Addresses { - if address.Type == corev1.NodeExternalIP { - domains[domain] = append(domains[domain], address.Address) + if domainAnnotation, ok := pod.Annotations[hostnameAnnotationKey]; ok { + domainList := splitHostnameAnnotation(domainAnnotation) + for _, domain := range domainList { + node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName) + for _, address := range node.Status.Addresses { + recordType := suitableType(address.Address) + // IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well. + if address.Type == corev1.NodeExternalIP || (address.Type == corev1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA) { + addToEndpointMap(endpointMap, domain, recordType, address.Address) + } } } } if ps.compatibility == "kops-dns-controller" { - if domain, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok { - if _, ok := domains[domain]; !ok { - domains[domain] = []string{} + if domainAnnotation, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok { + domainList := splitHostnameAnnotation(domainAnnotation) + for _, domain := range domainList { + addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP) } - domains[domain] = append(domains[domain], pod.Status.PodIP) } - if domain, ok := pod.Annotations[kopsDNSControllerHostnameAnnotationKey]; ok { - if _, ok := domains[domain]; !ok { - domains[domain] = []string{} - } - - node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName) - for _, address := range node.Status.Addresses { - if address.Type == corev1.NodeExternalIP { - domains[domain] = append(domains[domain], address.Address) + if domainAnnotation, ok := pod.Annotations[kopsDNSControllerHostnameAnnotationKey]; ok { + domainList := splitHostnameAnnotation(domainAnnotation) + for _, domain := range domainList { + node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName) + for _, address := range node.Status.Addresses { + recordType := suitableType(address.Address) + // IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well. + if address.Type == corev1.NodeExternalIP || (address.Type == corev1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA) { + addToEndpointMap(endpointMap, domain, recordType, address.Address) + } } } } } } endpoints := []*endpoint.Endpoint{} - for domain, targets := range domains { - endpoints = append(endpoints, endpoint.NewEndpoint(domain, endpoint.RecordTypeA, targets...)) + for key, targets := range endpointMap { + endpoints = append(endpoints, endpoint.NewEndpoint(key.dnsName, key.recordType, targets...)) } return endpoints, nil } + +func addToEndpointMap(endpointMap map[endpointKey][]string, domain string, recordType string, address string) { + key := endpointKey{ + dnsName: domain, + recordType: recordType, + } + if _, ok := endpointMap[key]; !ok { + endpointMap[key] = []string{} + } + endpointMap[key] = append(endpointMap[key], address) +} diff --git a/source/pod_test.go b/source/pod_test.go index c138aaf5f..549a9ebf9 100644 --- a/source/pod_test.go +++ b/source/pod_test.go @@ -41,7 +41,7 @@ func TestPodSource(t *testing.T) { pods []*corev1.Pod }{ { - "create records based on pod's external and internal IPs", + "create IPv4 records based on pod's external and internal IPs", "", "", []*endpoint.Endpoint{ @@ -111,7 +111,7 @@ func TestPodSource(t *testing.T) { }, }, { - "create records based on pod's external and internal IPs using DNS Controller annotations", + "create IPv4 records based on pod's external and internal IPs using DNS Controller annotations", "", "kops-dns-controller", []*endpoint.Endpoint{ @@ -180,12 +180,149 @@ func TestPodSource(t *testing.T) { }, }, }, + { + "create IPv6 records based on pod's external and internal IPs", + "", + "", + []*endpoint.Endpoint{ + {DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, + {DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, + }, + false, + []*corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node1", + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + {Type: corev1.NodeInternalIP, Address: "2001:DB8::1"}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node2", + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + {Type: corev1.NodeInternalIP, Address: "2001:DB8::2"}, + }, + }, + }, + }, + []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pod1", + Namespace: "kube-system", + Annotations: map[string]string{ + internalHostnameAnnotationKey: "internal.a.foo.example.org", + hostnameAnnotationKey: "a.foo.example.org", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + NodeName: "my-node1", + }, + Status: corev1.PodStatus{ + PodIP: "2001:DB8::1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pod2", + Namespace: "kube-system", + Annotations: map[string]string{ + internalHostnameAnnotationKey: "internal.a.foo.example.org", + hostnameAnnotationKey: "a.foo.example.org", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + NodeName: "my-node2", + }, + Status: corev1.PodStatus{ + PodIP: "2001:DB8::2", + }, + }, + }, + }, + { + "create IPv6 records based on pod's external and internal IPs using DNS Controller annotations", + "", + "kops-dns-controller", + []*endpoint.Endpoint{ + {DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, + {DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, + }, + false, + []*corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node1", + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + {Type: corev1.NodeInternalIP, Address: "2001:DB8::1"}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node2", + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + {Type: corev1.NodeInternalIP, Address: "2001:DB8::2"}, + }, + }, + }, + }, + []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pod1", + Namespace: "kube-system", + Annotations: map[string]string{ + kopsDNSControllerInternalHostnameAnnotationKey: "internal.a.foo.example.org", + kopsDNSControllerHostnameAnnotationKey: "a.foo.example.org", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + NodeName: "my-node1", + }, + Status: corev1.PodStatus{ + PodIP: "2001:DB8::1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pod2", + Namespace: "kube-system", + Annotations: map[string]string{ + kopsDNSControllerInternalHostnameAnnotationKey: "internal.a.foo.example.org", + kopsDNSControllerHostnameAnnotationKey: "a.foo.example.org", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + NodeName: "my-node2", + }, + Status: corev1.PodStatus{ + PodIP: "2001:DB8::2", + }, + }, + }, + }, { "create multiple records", "", "", []*endpoint.Endpoint{ {DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA}, + {DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1"}, RecordType: endpoint.RecordTypeAAAA}, {DNSName: "b.foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, }, false, @@ -197,6 +334,7 @@ func TestPodSource(t *testing.T) { Status: corev1.NodeStatus{ Addresses: []corev1.NodeAddress{ {Type: corev1.NodeExternalIP, Address: "54.10.11.1"}, + {Type: corev1.NodeInternalIP, Address: "2001:DB8::1"}, {Type: corev1.NodeInternalIP, Address: "10.0.1.1"}, }, }, @@ -388,6 +526,46 @@ func TestPodSource(t *testing.T) { }, }, }, + { + "split record for internal hostname annotation", + "", + "", + []*endpoint.Endpoint{ + {DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}, RecordType: endpoint.RecordTypeA}, + {DNSName: "internal.b.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}, RecordType: endpoint.RecordTypeA}, + }, + false, + []*corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node1", + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + {Type: corev1.NodeInternalIP, Address: "10.0.1.1"}, + }, + }, + }, + }, + []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pod1", + Namespace: "kube-system", + Annotations: map[string]string{ + internalHostnameAnnotationKey: "internal.a.foo.example.org,internal.b.foo.example.org", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + NodeName: "my-node1", + }, + Status: corev1.PodStatus{ + PodIP: "10.0.1.1", + }, + }, + }, + }, } { tc := tc t.Run(tc.title, func(t *testing.T) { diff --git a/source/service.go b/source/service.go index 9c47579dd..845430831 100644 --- a/source/service.go +++ b/source/service.go @@ -19,6 +19,7 @@ package source import ( "context" "fmt" + "net" "sort" "strings" "text/template" @@ -57,6 +58,7 @@ type serviceSource struct { publishInternal bool publishHostIP bool alwaysPublishNotReadyAddresses bool + resolveLoadBalancerHostname bool serviceInformer coreinformers.ServiceInformer endpointsInformer coreinformers.EndpointsInformer podInformer coreinformers.PodInformer @@ -66,7 +68,7 @@ type serviceSource struct { } // NewServiceSource creates a new serviceSource with the given config. -func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool, labelSelector labels.Selector) (Source, error) { +func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool, labelSelector labels.Selector, resolveLoadBalancerHostname bool) (Source, error) { tmpl, err := parseTemplate(fqdnTemplate) if err != nil { return nil, err @@ -137,6 +139,7 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name nodeInformer: nodeInformer, serviceTypeFilter: serviceTypes, labelSelector: labelSelector, + resolveLoadBalancerHostname: resolveLoadBalancerHostname, }, nil } @@ -213,7 +216,10 @@ func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e }) // 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 + if endpoints[i].DNSName != endpoints[j].DNSName { + return endpoints[i].DNSName < endpoints[j].DNSName + } + return endpoints[i].RecordType < endpoints[j].RecordType }) mergedEndpoints := []*endpoint.Endpoint{} mergedEndpoints = append(mergedEndpoints, endpoints[0]) @@ -265,7 +271,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri endpointsType := getEndpointsTypeFromAnnotations(svc.Annotations) - targetsByHeadlessDomain := make(map[string]endpoint.Targets) + targetsByHeadlessDomainAndType := make(map[endpointKey]endpoint.Targets) for _, subset := range endpointsObject.Subsets { addresses := subset.Addresses if svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses { @@ -305,8 +311,8 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri return endpoints } for _, address := range node.Status.Addresses { - if address.Type == v1.NodeExternalIP { - targets = endpoint.Targets{address.Address} + if address.Type == v1.NodeExternalIP || (address.Type == v1.NodeInternalIP && suitableType(address.Address) == endpoint.RecordTypeAAAA) { + targets = append(targets, address.Address) log.Debugf("Generating matching endpoint %s with NodeExternalIP %s", headlessDomain, address.Address) } } @@ -318,18 +324,29 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri log.Debugf("Generating matching endpoint %s with EndpointAddress IP %s", headlessDomain, address.IP) } } - targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], targets...) + for _, target := range targets { + key := endpointKey{ + dnsName: headlessDomain, + recordType: suitableType(target), + } + targetsByHeadlessDomainAndType[key] = append(targetsByHeadlessDomainAndType[key], target) + } } } } - headlessDomains := []string{} - for headlessDomain := range targetsByHeadlessDomain { - headlessDomains = append(headlessDomains, headlessDomain) + headlessKeys := []endpointKey{} + for headlessKey := range targetsByHeadlessDomainAndType { + headlessKeys = append(headlessKeys, headlessKey) } - sort.Strings(headlessDomains) - for _, headlessDomain := range headlessDomains { - allTargets := targetsByHeadlessDomain[headlessDomain] + sort.Slice(headlessKeys, func(i, j int) bool { + if headlessKeys[i].dnsName != headlessKeys[j].dnsName { + return headlessKeys[i].dnsName < headlessKeys[j].dnsName + } + return headlessKeys[i].recordType < headlessKeys[j].recordType + }) + for _, headlessKey := range headlessKeys { + allTargets := targetsByHeadlessDomainAndType[headlessKey] targets := []string{} deduppedTargets := map[string]struct{}{} @@ -344,9 +361,9 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri } if ttl.IsConfigured() { - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessDomain, endpoint.RecordTypeA, ttl, targets...)) + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessKey.dnsName, headlessKey.recordType, ttl, targets...)) } else { - endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, targets...)) + endpoints = append(endpoints, endpoint.NewEndpoint(headlessKey.dnsName, headlessKey.recordType, targets...)) } } @@ -456,6 +473,14 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro DNSName: hostname, } + epAAAA := &endpoint.Endpoint{ + RecordTTL: ttl, + RecordType: endpoint.RecordTypeAAAA, + Labels: endpoint.NewLabels(), + Targets: make(endpoint.Targets, 0, defaultTargetsCapacity), + DNSName: hostname, + } + epCNAME := &endpoint.Endpoint{ RecordTTL: ttl, RecordType: endpoint.RecordTypeCNAME, @@ -467,37 +492,43 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro var endpoints []*endpoint.Endpoint var targets endpoint.Targets - switch svc.Spec.Type { - case v1.ServiceTypeLoadBalancer: - if useClusterIP { - targets = append(targets, extractServiceIps(svc)...) - } else { - targets = append(targets, extractLoadBalancerTargets(svc)...) + targets = getTargetsFromTargetAnnotation(svc.Annotations) + + if len(targets) == 0 { + switch svc.Spec.Type { + case v1.ServiceTypeLoadBalancer: + if useClusterIP { + targets = append(targets, extractServiceIps(svc)...) + } else { + targets = append(targets, extractLoadBalancerTargets(svc, sc.resolveLoadBalancerHostname)...) + } + case v1.ServiceTypeClusterIP: + if sc.publishInternal { + targets = append(targets, extractServiceIps(svc)...) + } + if svc.Spec.ClusterIP == v1.ClusterIPNone { + endpoints = append(endpoints, sc.extractHeadlessEndpoints(svc, hostname, ttl)...) + } + case v1.ServiceTypeNodePort: + // add the nodeTargets and extract an SRV endpoint + targets, err = sc.extractNodePortTargets(svc) + if err != nil { + log.Errorf("Unable to extract targets from service %s/%s error: %v", svc.Namespace, svc.Name, err) + return endpoints + } + endpoints = append(endpoints, sc.extractNodePortEndpoints(svc, hostname, ttl)...) + case v1.ServiceTypeExternalName: + targets = append(targets, extractServiceExternalName(svc)...) } - case v1.ServiceTypeClusterIP: - if sc.publishInternal { - targets = append(targets, extractServiceIps(svc)...) - } - if svc.Spec.ClusterIP == v1.ClusterIPNone { - endpoints = append(endpoints, sc.extractHeadlessEndpoints(svc, hostname, ttl)...) - } - case v1.ServiceTypeNodePort: - // add the nodeTargets and extract an SRV endpoint - targets, err = sc.extractNodePortTargets(svc) - if err != nil { - log.Errorf("Unable to extract targets from service %s/%s error: %v", svc.Namespace, svc.Name, err) - return endpoints - } - endpoints = append(endpoints, sc.extractNodePortEndpoints(svc, targets, hostname, ttl)...) - case v1.ServiceTypeExternalName: - targets = append(targets, extractServiceExternalName(svc)...) } for _, t := range targets { - if suitableType(t) == endpoint.RecordTypeA { + switch suitableType(t) { + case endpoint.RecordTypeA: epA.Targets = append(epA.Targets, t) - } - if suitableType(t) == endpoint.RecordTypeCNAME { + case endpoint.RecordTypeAAAA: + epAAAA.Targets = append(epAAAA.Targets, t) + case endpoint.RecordTypeCNAME: epCNAME.Targets = append(epCNAME.Targets, t) } } @@ -505,6 +536,9 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro if len(epA.Targets) > 0 { endpoints = append(endpoints, epA) } + if len(epAAAA.Targets) > 0 { + endpoints = append(endpoints, epAAAA) + } if len(epCNAME.Targets) > 0 { endpoints = append(endpoints, epCNAME) } @@ -527,7 +561,7 @@ func extractServiceExternalName(svc *v1.Service) endpoint.Targets { return endpoint.Targets{svc.Spec.ExternalName} } -func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets { +func extractLoadBalancerTargets(svc *v1.Service, resolveLoadBalancerHostname bool) endpoint.Targets { var ( targets endpoint.Targets externalIPs endpoint.Targets @@ -539,7 +573,18 @@ func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets { targets = append(targets, lb.IP) } if lb.Hostname != "" { - targets = append(targets, lb.Hostname) + if resolveLoadBalancerHostname { + ips, err := net.LookupIP(lb.Hostname) + if err != nil { + log.Errorf("Unable to resolve %q: %v", lb.Hostname, err) + continue + } + for _, ip := range ips { + targets = append(targets, ip.String()) + } + } else { + targets = append(targets, lb.Hostname) + } } } @@ -560,6 +605,7 @@ func (sc *serviceSource) extractNodePortTargets(svc *v1.Service) (endpoint.Targe var ( internalIPs endpoint.Targets externalIPs endpoint.Targets + ipv6IPs endpoint.Targets nodes []*v1.Node err error ) @@ -607,24 +653,27 @@ func (sc *serviceSource) extractNodePortTargets(svc *v1.Service) (endpoint.Targe externalIPs = append(externalIPs, address.Address) case v1.NodeInternalIP: internalIPs = append(internalIPs, address.Address) + if suitableType(address.Address) == endpoint.RecordTypeAAAA { + ipv6IPs = append(ipv6IPs, address.Address) + } } } } access := getAccessFromAnnotations(svc.Annotations) if access == "public" { - return externalIPs, nil + return append(externalIPs, ipv6IPs...), nil } if access == "private" { return internalIPs, nil } if len(externalIPs) > 0 { - return externalIPs, nil + return append(externalIPs, ipv6IPs...), nil } return internalIPs, nil } -func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets endpoint.Targets, hostname string, ttl endpoint.TTL) []*endpoint.Endpoint { +func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, hostname string, ttl endpoint.TTL) []*endpoint.Endpoint { var endpoints []*endpoint.Endpoint for _, port := range svc.Spec.Ports { diff --git a/source/service_test.go b/source/service_test.go index a16994b2b..a12bd2752 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -78,6 +78,7 @@ func (suite *ServiceSuite) SetupTest() { []string{}, false, labels.Everything(), + false, ) suite.NoError(err, "should initialize service source") } @@ -158,6 +159,7 @@ func testServiceSourceNewServiceSource(t *testing.T) { ti.serviceTypesFilter, false, labels.Everything(), + false, ) if ti.expectError { @@ -174,25 +176,26 @@ func testServiceSourceEndpoints(t *testing.T) { t.Parallel() 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 - annotations map[string]string - clusterIP string - externalIPs []string - lbs []string - serviceTypesFilter []string - expected []*endpoint.Endpoint - expectError bool - serviceLabelSelector string + 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 + annotations map[string]string + clusterIP string + externalIPs []string + lbs []string + serviceTypesFilter []string + expected []*endpoint.Endpoint + expectError bool + serviceLabelSelector string + resolveLoadBalancerHostname bool }{ { title: "no annotated services return no endpoints", @@ -232,7 +235,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -277,8 +280,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.fqdn.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -294,8 +297,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.fqdn.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -313,10 +316,10 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.fqdn.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -335,8 +338,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.fqdn.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -352,8 +355,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -369,8 +372,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -386,7 +389,25 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"lb.example.com"}, // Kubernetes omits the trailing dot serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"lb.example.com"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}}, + }, + }, + { + title: "annotated services return an endpoint with hostname then resolve hostname", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + }, + externalIPs: []string{}, + lbs: []string{"example.com"}, // Use a resolvable hostname for testing. + serviceTypesFilter: []string{}, + resolveLoadBalancerHostname: true, + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"93.184.216.34"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2606:2800:220:1:248:1893:25c8:1946"}}, }, }, { @@ -402,8 +423,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "foo.example.org", Targets: endpoint.Targets{"lb.example.com"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}}, }, }, { @@ -420,7 +441,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -453,7 +474,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -484,7 +505,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -502,7 +523,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -553,7 +574,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -599,7 +620,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}}, }, }, { @@ -615,7 +636,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4", "8.8.8.8"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4", "8.8.8.8"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4", "8.8.8.8"}}, }, }, { @@ -646,7 +667,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -665,8 +686,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -683,8 +704,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4", "lb.example.com"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"lb.example.com"}}, + {DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}}, }, }, { @@ -702,10 +723,10 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "internal.bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -720,8 +741,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4", "elb.com"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"elb.com"}}, + {DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"elb.com"}}, }, }, { @@ -738,8 +759,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4", "elb.com"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "foo.example.org", Targets: endpoint.Targets{"elb.com"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"elb.com"}}, }, }, { @@ -757,7 +778,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "mate.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "mate.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -787,7 +808,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, }, { @@ -804,7 +825,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, }, { @@ -821,7 +842,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(10)}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(10)}, }, }, { @@ -838,7 +859,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(60)}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(60)}, }, }, { @@ -855,7 +876,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, }, { @@ -871,7 +892,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer)}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -902,7 +923,7 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*endpoint.Endpoint{ - {DNSName: "foo.internal.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo.internal.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, }, }, { @@ -920,8 +941,8 @@ func testServiceSourceEndpoints(t *testing.T) { lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, expected: []*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"}}, + {DNSName: "foo.internal.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -939,7 +960,7 @@ func testServiceSourceEndpoints(t *testing.T) { serviceLabelSelector: "app=web-external", fqdnTemplate: "{{.Name}}.bar.example.com", expected: []*endpoint.Endpoint{ - {DNSName: "fqdn.bar.example.com", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "fqdn.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -957,7 +978,7 @@ func testServiceSourceEndpoints(t *testing.T) { serviceLabelSelector: "app=web-external", annotations: map[string]string{hostnameAnnotationKey: "annotation.bar.example.com"}, expected: []*endpoint.Endpoint{ - {DNSName: "annotation.bar.example.com", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "annotation.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, { @@ -992,6 +1013,37 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{hostnameAnnotationKey: "annotation.bar.example.com"}, expected: []*endpoint.Endpoint{}, }, + { + title: "dual-stack load-balancer service gets both addresses", + svcNamespace: "testing", + svcName: "foobar", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + clusterIP: "1.1.1.2,2001:db8::2", + externalIPs: []string{}, + lbs: []string{"1.1.1.1", "2001:db8::1"}, + serviceTypesFilter: []string{}, + annotations: map[string]string{hostnameAnnotationKey: "foobar.example.org"}, + expected: []*endpoint.Endpoint{ + {DNSName: "foobar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foobar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}}, + }, + }, + { + title: "IPv6-only load-balancer service gets IPv6 endpoint", + svcNamespace: "testing", + svcName: "foobar-v6", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + clusterIP: "2001:db8::1", + externalIPs: []string{}, + lbs: []string{"2001:db8::2"}, + serviceTypesFilter: []string{}, + annotations: map[string]string{hostnameAnnotationKey: "foobar-v6.example.org"}, + expected: []*endpoint.Endpoint{ + {DNSName: "foobar-v6.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}}, + }, + }, } { tc := tc t.Run(tc.title, func(t *testing.T) { @@ -1055,6 +1107,7 @@ func testServiceSourceEndpoints(t *testing.T) { tc.serviceTypesFilter, tc.ignoreHostnameAnnotation, sourceLabel, + tc.resolveLoadBalancerHostname, ) require.NoError(t, err) @@ -1112,7 +1165,7 @@ func testMultipleServicesEndpoints(t *testing.T) { }, []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"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo1.2.3.4"}}, }, false, }, @@ -1136,7 +1189,7 @@ func testMultipleServicesEndpoints(t *testing.T) { }, []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: "foo.example.org", RecordType: endpoint.RecordTypeA, 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, }, @@ -1164,9 +1217,9 @@ func testMultipleServicesEndpoints(t *testing.T) { }, []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"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, 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", RecordType: endpoint.RecordTypeA, 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", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"20.1.1.1"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo20.1.1.1"}}, }, false, }, @@ -1189,8 +1242,8 @@ func testMultipleServicesEndpoints(t *testing.T) { }, []string{}, []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"a.elb.com"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/fooa.elb.com"}, SetIdentifier: "a"}, - {DNSName: "foo.example.org", Targets: endpoint.Targets{"b.elb.com"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foob.elb.com"}, SetIdentifier: "b"}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"a.elb.com"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/fooa.elb.com"}, SetIdentifier: "a"}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"b.elb.com"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foob.elb.com"}, SetIdentifier: "b"}, }, false, }, @@ -1244,6 +1297,7 @@ func testMultipleServicesEndpoints(t *testing.T) { tc.serviceTypesFilter, tc.ignoreHostnameAnnotation, labels.Everything(), + false, ) require.NoError(t, err) @@ -1295,7 +1349,7 @@ func TestClusterIpServices(t *testing.T) { labelSelector string }{ { - title: "annotated ClusterIp services return an endpoint with Cluster IP", + title: "hostname annotated ClusterIp services return an endpoint with Cluster IP", svcNamespace: "testing", svcName: "foo", svcType: v1.ServiceTypeClusterIP, @@ -1304,7 +1358,78 @@ func TestClusterIpServices(t *testing.T) { }, clusterIP: "1.2.3.4", expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + }, + }, + { + title: "target annotated ClusterIp services return an endpoint with the specified A", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + targetAnnotationKey: "4.3.2.1", + }, + clusterIP: "1.2.3.4", + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"4.3.2.1"}}, + }, + }, + { + title: "target annotated ClusterIp services return an endpoint with the specified CNAME", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + targetAnnotationKey: "bar.example.org.", + }, + clusterIP: "1.2.3.4", + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org"}}, + }, + }, + { + title: "target annotated ClusterIp services return an endpoint with the specified AAAA", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + targetAnnotationKey: "2001:DB8::1", + }, + clusterIP: "1.2.3.4", + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}}, + }, + }, + { + title: "multiple target annotated ClusterIp services return an endpoint with the specified CNAMES", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + targetAnnotationKey: "bar.example.org.,baz.example.org.", + }, + clusterIP: "1.2.3.4", + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org", "baz.example.org"}}, + }, + }, + { + title: "multiple target annotated ClusterIp services return two endpoints with the specified CNAMES and AAAA", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + targetAnnotationKey: "bar.example.org.,baz.example.org.,2001:DB8::1", + }, + clusterIP: "1.2.3.4", + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org", "baz.example.org"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}}, }, }, { @@ -1319,6 +1444,33 @@ func TestClusterIpServices(t *testing.T) { clusterIP: "1.2.3.4", expected: []*endpoint.Endpoint{}, }, + { + title: "hostname and target annotated ClusterIp services are ignored", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + ignoreHostnameAnnotation: true, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + targetAnnotationKey: "bar.example.org.", + }, + clusterIP: "1.2.3.4", + expected: []*endpoint.Endpoint{}, + }, + { + title: "hostname and target annotated ClusterIp services return an endpoint with the specified CNAME", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + targetAnnotationKey: "bar.example.org.", + }, + clusterIP: "1.2.3.4", + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org"}}, + }, + }, { title: "non-annotated ClusterIp services with set fqdnTemplate return an endpoint with target IP", svcNamespace: "testing", @@ -1327,7 +1479,7 @@ func TestClusterIpServices(t *testing.T) { fqdnTemplate: "{{.Name}}.bar.example.com", clusterIP: "4.5.6.7", expected: []*endpoint.Endpoint{ - {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"4.5.6.7"}}, + {DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"4.5.6.7"}}, }, }, { @@ -1338,6 +1490,20 @@ func TestClusterIpServices(t *testing.T) { clusterIP: v1.ClusterIPNone, expected: []*endpoint.Endpoint{}, }, + { + title: "Headless services generate endpoints when target is specified", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + targetAnnotationKey: "bar.example.org.", + }, + clusterIP: v1.ClusterIPNone, + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org"}}, + }, + }, { title: "ClusterIP service with matching label generates an endpoint", svcNamespace: "testing", @@ -1347,7 +1513,21 @@ func TestClusterIpServices(t *testing.T) { labels: map[string]string{"app": "web-internal"}, clusterIP: "4.5.6.7", expected: []*endpoint.Endpoint{ - {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"4.5.6.7"}}, + {DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"4.5.6.7"}}, + }, + labelSelector: "app=web-internal", + }, + { + title: "ClusterIP service with matching label and target generates a CNAME endpoint", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + fqdnTemplate: "{{.Name}}.bar.example.com", + labels: map[string]string{"app": "web-internal"}, + annotations: map[string]string{targetAnnotationKey: "bar.example.com."}, + clusterIP: "4.5.6.7", + expected: []*endpoint.Endpoint{ + {DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.com"}}, }, labelSelector: "app=web-internal", }, @@ -1409,6 +1589,7 @@ func TestClusterIpServices(t *testing.T) { []string{}, tc.ignoreHostnameAnnotation, labelSelector, + false, ) require.NoError(t, err) @@ -1463,6 +1644,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*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}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1472,6 +1654,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1482,6 +1665,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1504,6 +1688,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1514,6 +1699,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1529,6 +1715,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*endpoint.Endpoint{ {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{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1538,6 +1725,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1548,6 +1736,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1564,6 +1753,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*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}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1572,6 +1762,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Status: v1.NodeStatus{ Addresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1581,6 +1772,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Status: v1.NodeStatus{ Addresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1597,6 +1789,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*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.2"}, RecordType: endpoint.RecordTypeA}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1606,6 +1799,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1616,6 +1810,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1636,6 +1831,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*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.2"}, RecordType: endpoint.RecordTypeA}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1645,6 +1841,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1655,6 +1852,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1676,6 +1874,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*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}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1685,6 +1884,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1695,6 +1895,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1713,6 +1914,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*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}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1722,6 +1924,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1732,6 +1935,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1748,8 +1952,10 @@ func TestServiceSourceNodePortServices(t *testing.T) { kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org", }, expected: []*endpoint.Endpoint{ - {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}}, - {DNSName: "internal.bar.example.org", Targets: endpoint.Targets{"10.0.1.1"}}, + {DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1"}}, + {DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}}, + {DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1"}}, + {DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1762,6 +1968,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1775,6 +1982,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1790,8 +1998,10 @@ func TestServiceSourceNodePortServices(t *testing.T) { kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org", }, expected: []*endpoint.Endpoint{ - {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}}, - {DNSName: "internal.bar.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}}, + {DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}}, + {DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}}, + {DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}}, + {DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1804,6 +2014,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1817,6 +2028,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1832,8 +2044,10 @@ func TestServiceSourceNodePortServices(t *testing.T) { kopsDNSControllerHostnameAnnotationKey: "foo.example.org., bar.example.org", }, expected: []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}}, - {DNSName: "bar.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}}, + {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}}, + {DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}}, + {DNSName: "bar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1846,6 +2060,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1859,6 +2074,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1887,6 +2103,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, }, }, }, { @@ -1900,6 +2117,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }}, @@ -1979,6 +2197,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { []string{}, tc.ignoreHostnameAnnotation, labels.Everything(), + false, ) require.NoError(t, err) @@ -2025,7 +2244,7 @@ func TestHeadlessServices(t *testing.T) { expectError bool }{ { - "annotated Headless services return endpoints for each selected Pod", + "annotated Headless services return IPv4 endpoints for each selected Pod", "", "testing", "foo", @@ -2051,9 +2270,42 @@ func TestHeadlessServices(t *testing.T) { false, []v1.Node{}, []*endpoint.Endpoint{ - {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + }, + false, + }, + { + "annotated Headless services return IPv6 endpoints for each selected Pod", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + []string{"", ""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}}, }, false, }, @@ -2087,7 +2339,7 @@ func TestHeadlessServices(t *testing.T) { false, }, { - "annotated Headless services return endpoints with TTL for each selected Pod", + "annotated Headless services return IPv4 endpoints with TTL for each selected Pod", "", "testing", "foo", @@ -2114,9 +2366,43 @@ func TestHeadlessServices(t *testing.T) { false, []v1.Node{}, []*endpoint.Endpoint{ - {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, + }, + false, + }, + { + "annotated Headless services return IPv6 endpoints with TTL for each selected Pod", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + ttlAnnotationKey: "1", + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + []string{"", ""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}, RecordTTL: endpoint.TTL(1)}, }, false, }, @@ -2147,8 +2433,8 @@ func TestHeadlessServices(t *testing.T) { false, []v1.Node{}, []*endpoint.Endpoint{ - {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, }, false, }, @@ -2179,9 +2465,9 @@ func TestHeadlessServices(t *testing.T) { true, []v1.Node{}, []*endpoint.Endpoint{ - {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, false, }, @@ -2212,12 +2498,12 @@ func TestHeadlessServices(t *testing.T) { false, []v1.Node{}, []*endpoint.Endpoint{ - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, false, }, { - "annotated Headless services return only a unique set of targets", + "annotated Headless services return only a unique set of IPv4 targets", "", "testing", "foo", @@ -2243,12 +2529,43 @@ func TestHeadlessServices(t *testing.T) { false, []v1.Node{}, []*endpoint.Endpoint{ - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, false, }, { - "annotated Headless services return targets from pod annotation", + "annotated Headless services return only a unique set of IPv6 targets", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::1", "2001:db8::2"}, + []string{"", "", ""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1", "foo-3"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}}, + }, + false, + }, + { + "annotated Headless services return IPv4 targets from pod annotation", "", "testing", "foo", @@ -2276,12 +2593,45 @@ func TestHeadlessServices(t *testing.T) { false, []v1.Node{}, []*endpoint.Endpoint{ - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, false, }, { - "annotated Headless services return targets from node external IP if endpoints-type annotation is set", + "annotated Headless services return IPv6 targets from pod annotation", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + map[string]string{ + targetAnnotationKey: "2001:db8::4", + }, + v1.ClusterIPNone, + []string{"2001:db8::1"}, + []string{""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}}, + }, + false, + }, + { + "annotated Headless services return IPv4 targets from node external IP if endpoints-type annotation is set", "", "testing", "foo", @@ -2319,12 +2669,103 @@ func TestHeadlessServices(t *testing.T) { }, }, []*endpoint.Endpoint{ - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, false, }, { - "annotated Headless services return targets from hostIP if endpoints-type annotation is set", + "annotated Headless services return IPv6 targets from node external IP if endpoints-type annotation is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP, + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1"}, + []string{""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{ + { + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "2001:db8::4", + }, + }, + }, + }, + }, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}}, + }, + false, + }, + { + "annotated Headless services return dual-stack targets from node external IP if endpoints-type annotation is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP, + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"1.1.1.1"}, + []string{""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{ + { + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "1.2.3.4", + }, + { + Type: v1.NodeInternalIP, + Address: "2001:db8::4", + }, + }, + }, + }, + }, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}}, + }, + false, + }, + { + "annotated Headless services return IPv4 targets from hostIP if endpoints-type annotation is set", "", "testing", "foo", @@ -2351,7 +2792,39 @@ func TestHeadlessServices(t *testing.T) { false, []v1.Node{}, []*endpoint.Endpoint{ - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, + }, + false, + }, + { + "annotated Headless services return IPv6 targets from hostIP if endpoints-type annotation is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + endpointsTypeAnnotationKey: EndpointsTypeHostIP, + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1"}, + []string{"2001:db8::4"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}}, }, false, }, @@ -2452,6 +2925,7 @@ func TestHeadlessServices(t *testing.T) { []string{}, tc.ignoreHostnameAnnotation, labels.Everything(), + false, ) require.NoError(t, err) @@ -2496,7 +2970,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { expectError bool }{ { - "annotated Headless services return endpoints for each selected Pod", + "annotated Headless services return IPv4 endpoints for each selected Pod", "", "testing", "foo", @@ -2523,9 +2997,43 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, false, []*endpoint.Endpoint{ - {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + }, + false, + }, + { + "annotated Headless services return IPv6 endpoints for each selected Pod", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, true}, + []*v1.ObjectReference{ + {APIVersion: "", Kind: "Pod", Name: "foo-0"}, + {APIVersion: "", Kind: "Pod", Name: "foo-1"}, + }, + false, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}}, }, false, }, @@ -2560,7 +3068,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { false, }, { - "annotated Headless services return endpoints with TTL for each selected Pod", + "annotated Headless services return IPv4 endpoints with TTL for each selected Pod", "", "testing", "foo", @@ -2588,9 +3096,44 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, false, []*endpoint.Endpoint{ - {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, + }, + false, + }, + { + "annotated Headless services return IPv6 endpoints with TTL for each selected Pod", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + ttlAnnotationKey: "1", + }, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, true}, + []*v1.ObjectReference{ + {APIVersion: "", Kind: "Pod", Name: "foo-0"}, + {APIVersion: "", Kind: "Pod", Name: "foo-1"}, + }, + false, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}, RecordTTL: endpoint.TTL(1)}, }, false, }, @@ -2622,8 +3165,8 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, false, []*endpoint.Endpoint{ - {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, }, false, }, @@ -2655,14 +3198,14 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, true, []*endpoint.Endpoint{ - {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, false, }, { - "annotated Headless services return endpoints for pods missing hostname", + "annotated Headless services return IPv4 endpoints for pods missing hostname", "", "testing", "foo", @@ -2689,7 +3232,39 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, false, []*endpoint.Endpoint{ - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + }, + false, + }, + { + "annotated Headless services return IPv6 endpoints for pods missing hostname", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"", ""}, + []bool{true, true}, + []*v1.ObjectReference{ + {APIVersion: "", Kind: "Pod", Name: "foo-0"}, + {APIVersion: "", Kind: "Pod", Name: "foo-1"}, + }, + false, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}}, }, false, }, @@ -2809,6 +3384,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { []string{}, tc.ignoreHostnameAnnotation, labels.Everything(), + false, ) require.NoError(t, err) @@ -2845,7 +3421,7 @@ func TestExternalServices(t *testing.T) { expectError bool }{ { - "external services return an A endpoint for the external name that is an IP address", + "external services return an A endpoint for the external name that is an IPv4 address", "", "testing", "foo", @@ -2863,6 +3439,25 @@ func TestExternalServices(t *testing.T) { }, false, }, + { + "external services return an AAAA endpoint for the external name that is an IPv6 address", + "", + "testing", + "foo", + v1.ServiceTypeExternalName, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + "2001:db8::111", + []*endpoint.Endpoint{ + {DNSName: "service.example.org", Targets: endpoint.Targets{"2001:db8::111"}, RecordType: endpoint.RecordTypeAAAA}, + }, + false, + }, { "external services return a CNAME endpoint for the external name that is a domain", "", @@ -2921,6 +3516,7 @@ func TestExternalServices(t *testing.T) { []string{}, tc.ignoreHostnameAnnotation, labels.Everything(), + false, ) require.NoError(t, err) @@ -2975,6 +3571,7 @@ func BenchmarkServiceEndpoints(b *testing.B) { []string{}, false, labels.Everything(), + false, ) require.NoError(b, err) diff --git a/source/shared_test.go b/source/shared_test.go index 7114b0dd7..11828dbe2 100644 --- a/source/shared_test.go +++ b/source/shared_test.go @@ -29,11 +29,14 @@ func sortEndpoints(endpoints []*endpoint.Endpoint) { sort.Strings([]string(ep.Targets)) } sort.Slice(endpoints, func(i, k int) bool { - // Sort by DNSName and Targets + // Sort by DNSName, RecordType, and Targets ei, ek := endpoints[i], endpoints[k] if ei.DNSName != ek.DNSName { return ei.DNSName < ek.DNSName } + if ei.RecordType != ek.RecordType { + return ei.RecordType < ek.RecordType + } // Targets are sorted ahead of time. for j, ti := range ei.Targets { if j >= len(ek.Targets) { @@ -79,7 +82,7 @@ func validateEndpoint(t *testing.T, endpoint, expected *endpoint.Endpoint) { } // if non-empty record type is expected, check that it matches. - if expected.RecordType != "" && endpoint.RecordType != expected.RecordType { + if endpoint.RecordType != expected.RecordType { t.Errorf("RecordType expected %q, got %q", expected.RecordType, endpoint.RecordType) } diff --git a/source/skipper_routegroup_test.go b/source/skipper_routegroup_test.go index 40cf65c1b..9b4c325a5 100644 --- a/source/skipper_routegroup_test.go +++ b/source/skipper_routegroup_test.go @@ -90,8 +90,9 @@ func TestEndpointsFromRouteGroups(t *testing.T) { }), want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -113,12 +114,14 @@ func TestEndpointsFromRouteGroups(t *testing.T) { ), want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, { - DNSName: "my.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "my.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -140,8 +143,9 @@ func TestEndpointsFromRouteGroups(t *testing.T) { ), want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -163,9 +167,10 @@ func TestEndpointsFromRouteGroups(t *testing.T) { ), want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), - RecordTTL: endpoint.TTL(2189), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), + RecordTTL: endpoint.TTL(2189), }, }, }, @@ -185,8 +190,9 @@ func TestEndpointsFromRouteGroups(t *testing.T) { ), want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"1.5.1.4"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets([]string{"1.5.1.4"}), }, }, }, @@ -207,12 +213,14 @@ func TestEndpointsFromRouteGroups(t *testing.T) { ), want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"1.5.1.4"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets([]string{"1.5.1.4"}), }, { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -278,8 +286,9 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -308,12 +317,14 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, { - DNSName: "rg1.namespace1.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.namespace1.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -341,8 +352,9 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.namespace1.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.namespace1.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -370,8 +382,9 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -400,9 +413,10 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), - RecordTTL: endpoint.TTL(2189), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), + RecordTTL: endpoint.TTL(2189), }, }, }, @@ -430,12 +444,14 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"1.5.1.4"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets([]string{"1.5.1.4"}), }, { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -495,20 +511,24 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, { - DNSName: "rg2.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg2.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, { - DNSName: "rg3.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg3.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, { - DNSName: "rg.k8s.example", - Targets: endpoint.Targets([]string{"lb2.example.org"}), + DNSName: "rg.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb2.example.org"}), }, }, }, @@ -575,8 +595,9 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -643,12 +664,14 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, { - DNSName: "rg2.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg2.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, @@ -701,12 +724,14 @@ func TestRouteGroupsEndpoints(t *testing.T) { }, want: []*endpoint.Endpoint{ { - DNSName: "rg1.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg1.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, { - DNSName: "rg3.k8s.example", - Targets: endpoint.Targets([]string{"lb.example.org"}), + DNSName: "rg3.k8s.example", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets([]string{"lb.example.org"}), }, }, }, diff --git a/source/source.go b/source/source.go index da9909923..5e3cdcf78 100644 --- a/source/source.go +++ b/source/source.go @@ -46,7 +46,7 @@ const ( accessAnnotationKey = "external-dns.alpha.kubernetes.io/access" // The annotation used for specifying the type of endpoints to use for headless services endpointsTypeAnnotationKey = "external-dns.alpha.kubernetes.io/endpoints-type" - // The annotation used for defining the desired ingress target + // The annotation used for defining the desired ingress/service target targetAnnotationKey = "external-dns.alpha.kubernetes.io/target" // The annotation used for defining the desired DNS record TTL ttlAnnotationKey = "external-dns.alpha.kubernetes.io/ttl" @@ -86,6 +86,12 @@ type Source interface { AddEventHandler(context.Context, func()) } +// endpointKey is the type of a map key for separating endpoints or targets. +type endpointKey struct { + dnsName string + recordType string +} + func getTTLFromAnnotations(annotations map[string]string) (endpoint.TTL, error) { ttlNotConfigured := endpoint.TTL(0) ttlAnnotation, exists := annotations[ttlAnnotationKey] @@ -151,7 +157,7 @@ func getHostnamesFromAnnotations(annotations map[string]string) []string { if !exists { return nil } - return strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",") + return splitHostnameAnnotation(hostnameAnnotation) } func getAccessFromAnnotations(annotations map[string]string) string { @@ -167,7 +173,11 @@ func getInternalHostnamesFromAnnotations(annotations map[string]string) []string if !exists { return nil } - return strings.Split(strings.Replace(internalHostnameAnnotation, " ", "", -1), ",") + return splitHostnameAnnotation(internalHostnameAnnotation) +} + +func splitHostnameAnnotation(annotation string) []string { + return strings.Split(strings.Replace(annotation, " ", "", -1), ",") } func getAliasFromAnnotations(annotations map[string]string) bool { @@ -239,8 +249,10 @@ func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targ // suitableType returns the DNS resource record type suitable for the target. // In this case type A for IPs and type CNAME for everything else. func suitableType(target string) string { - if net.ParseIP(target) != nil { + if net.ParseIP(target) != nil && net.ParseIP(target).To4() != nil { return endpoint.RecordTypeA + } else if net.ParseIP(target) != nil && net.ParseIP(target).To16() != nil { + return endpoint.RecordTypeAAAA } return endpoint.RecordTypeCNAME } @@ -250,12 +262,21 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin var endpoints []*endpoint.Endpoint var aTargets endpoint.Targets + var aaaaTargets endpoint.Targets var cnameTargets endpoint.Targets for _, t := range targets { switch suitableType(t) { case endpoint.RecordTypeA: + if isIPv6String(t) { + continue + } aTargets = append(aTargets, t) + case endpoint.RecordTypeAAAA: + if !isIPv6String(t) { + continue + } + aaaaTargets = append(aaaaTargets, t) default: cnameTargets = append(cnameTargets, t) } @@ -274,6 +295,19 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin endpoints = append(endpoints, epA) } + if len(aaaaTargets) > 0 { + epAAAA := &endpoint.Endpoint{ + DNSName: strings.TrimSuffix(hostname, "."), + Targets: aaaaTargets, + RecordTTL: ttl, + RecordType: endpoint.RecordTypeAAAA, + Labels: endpoint.NewLabels(), + ProviderSpecific: providerSpecific, + SetIdentifier: setIdentifier, + } + endpoints = append(endpoints, epAAAA) + } + if len(cnameTargets) > 0 { epCNAME := &endpoint.Endpoint{ DNSName: strings.TrimSuffix(hostname, "."), @@ -286,7 +320,6 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin } endpoints = append(endpoints, epCNAME) } - return endpoints } @@ -348,3 +381,9 @@ func waitForDynamicCacheSync(ctx context.Context, factory dynamicInformerFactory } return nil } + +// isIPv6String returns if ip is IPv6. +func isIPv6String(ip string) bool { + netIP := net.ParseIP(ip) + return netIP != nil && netIP.To4() == nil +} diff --git a/source/source_test.go b/source/source_test.go index 0c31b93ba..d6befe804 100644 --- a/source/source_test.go +++ b/source/source_test.go @@ -94,6 +94,7 @@ func TestSuitableType(t *testing.T) { target, recordType, expected string }{ {"8.8.8.8", "", "A"}, + {"2001:db8::1", "", "AAAA"}, {"foo.example.org", "", "CNAME"}, {"bar.eu-central-1.elb.amazonaws.com", "", "CNAME"}, } { diff --git a/source/store.go b/source/store.go index 52d0dbf93..fc3dc35d1 100644 --- a/source/store.go +++ b/source/store.go @@ -46,6 +46,7 @@ type Config struct { Namespace string AnnotationFilter string LabelFilter labels.Selector + IngressClassNames []string FQDNTemplate string CombineFQDNAndAnnotation bool IgnoreHostnameAnnotation bool @@ -72,6 +73,8 @@ type Config struct { RequestTimeout time.Duration DefaultTargets []string OCPRouterName string + UpdateEvents bool + ResolveLoadBalancerHostname bool } // ClientGenerator provides clients @@ -214,13 +217,13 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg if err != nil { return nil, err } - return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter) + return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname) case "ingress": client, err := p.KubeClient() if err != nil { return nil, err } - return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter) + return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames) case "pod": client, err := p.KubeClient() if err != nil { @@ -318,7 +321,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg if err != nil { return nil, err } - return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme) + return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme, cfg.UpdateEvents) case "skipper-routegroup": apiServerURL := cfg.APIServerURL tokenPath := ""