diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1897a3ebd3..b537898579 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,11 +9,26 @@ updates: directory: "/" # Location of package manifests schedule: interval: "daily" + groups: + dev-dependencies: + patterns: + - "*" + ignore: + - dependency-name: "github.com/openshift/api" + - dependency-name: "github.com/openshift/client-go" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" + groups: + dev-dependencies: + patterns: + - "*" - package-ecosystem: "docker" # Keep Docker dependencies up to date directory: "/" schedule: interval: "daily" + groups: + dev-dependencies: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3d36d5d53..5194ed0dcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: '1.20' id: go - name: Check out code into the Go module directory @@ -38,7 +38,7 @@ jobs: apt update apt install -y make gcc libc-dev git if: github.actor == 'nektos/act' - + - name: Test run: make test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bf37e27d3a..df29ff6d33 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,9 +28,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Install go version - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: '^1.19' + go-version: '^1.20' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 112048477a..b5a7decd88 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/setup-go@v4 with: - go-version: ^1.19 + go-version: '^1.20' - run: | pip install -r docs/scripts/requirements.txt diff --git a/.github/workflows/gh-workflow-approve.yaml b/.github/workflows/gh-workflow-approve.yaml index c0dc99008d..71b51b0ae0 100644 --- a/.github/workflows/gh-workflow-approve.yaml +++ b/.github/workflows/gh-workflow-approve.yaml @@ -17,11 +17,11 @@ jobs: actions: write steps: - name: Update PR - uses: actions/github-script@d556feaca394842dc55e4734bf3bb9f685482fa0 # v6.3.3 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} - debug: ${{ secrets.ACTIONS_RUNNER_DEBUG }} + debug: ${{ secrets.ACTIONS_RUNNER_DEBUG == 'true' }} script: | const result = await github.rest.actions.listWorkflowRunsForRepo({ owner: context.repo.owner, diff --git a/.github/workflows/json-yaml-validate.yml b/.github/workflows/json-yaml-validate.yml index 1eb833f636..d551229188 100644 --- a/.github/workflows/json-yaml-validate.yml +++ b/.github/workflows/json-yaml-validate.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 - name: json-yaml-validate - uses: GrantBirki/json-yaml-validate@v1.2.0 + uses: GrantBirki/json-yaml-validate@v1.5.0 with: comment: "true" # enable comment mode yaml_exclude_regex: "(charts/external-dns/templates.*|mkdocs.yml)" diff --git a/.github/workflows/lint-test-chart.yaml b/.github/workflows/lint-test-chart.yaml index f8207ef507..2b6b5b13a9 100644 --- a/.github/workflows/lint-test-chart.yaml +++ b/.github/workflows/lint-test-chart.yaml @@ -38,21 +38,21 @@ jobs: python-version: "3.x" - name: Set-up chart-testing - uses: helm/chart-testing-action@afea100a513515fbd68b0e72a7bb0ae34cb62aec + uses: helm/chart-testing-action@e8788873172cb653a90ca2e819d79d65a66d4e76 - name: Run chart-testing (list-changed) id: list-changed run: | changed=$(ct list-changed) if [[ -n "$changed" ]]; then - echo "::set-output name=changed::true" + echo "changed=true" >> $GITHUB_OUTPUT fi - name: Run chart-testing (lint) run: ct lint --check-version-increment=false - name: Set-up Kind cluster - uses: helm/kind-action@d8ccf8fb623ce1bb360ae2f45f323d9d5c5e9f00 + uses: helm/kind-action@fa81e57adff234b2908110485695db0f181f3c67 with: wait: 120s if: steps.list-changed.outputs.changed == 'true' diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 41b9880fa6..b5dd025095 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -23,7 +23,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: '1.20' id: go - name: Check out code into the Go module directory @@ -31,5 +31,5 @@ jobs: - name: Lint run: | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.1 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.3 make lint diff --git a/.github/workflows/release-chart.yaml b/.github/workflows/release-chart.yaml index 95ce0baa85..770e9a04a2 100644 --- a/.github/workflows/release-chart.yaml +++ b/.github/workflows/release-chart.yaml @@ -29,7 +29,7 @@ jobs: run: | set -euo pipefail chart_version="$(grep -Po "(?<=^version: ).+" charts/external-dns/Chart.yaml)" - echo "::set-output name=version::${chart_version}" + echo "version=${chart_version}" >> $GITHUB_OUTPUT - name: Get changelog entry id: changelog_reader diff --git a/.golangci.yml b/.golangci.yml index 20c72e8c2e..da6227d28a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,7 +19,6 @@ linters: # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint disable-all: true enable: - - depguard - dogsled - gofmt - goimports @@ -61,6 +60,14 @@ issues: - unused - varcheck - whitespace + - path: source/ambassador_host.go + linters: [ typecheck ] + - path: source/contour_httpproxy.go + linters: [ typecheck ] + - path: source/f5_virtualserver.go + linters: [ typecheck ] + - path: source/kong_tcpingress.go + linters: [ typecheck ] run: skip-files: diff --git a/Dockerfile b/Dockerfile index 55b72749d3..341df57028 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ # builder image ARG ARCH -FROM golang:1.19 as builder +FROM golang:1.20 as builder ARG ARCH WORKDIR /sigs.k8s.io/external-dns @@ -25,9 +25,9 @@ RUN go mod download COPY . . -FROM alpine:3.17 +FROM alpine:3.18 -RUN apk update && apk add "libcrypto3>=3.0.8-r1" "libssl3>=3.0.8-r1" && 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/Dockerfile.mini b/Dockerfile.mini index 90cba37d9a..2428351aed 100644 --- a/Dockerfile.mini +++ b/Dockerfile.mini @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.19 as builder +FROM golang:1.20 as builder WORKDIR /sigs.k8s.io/external-dns diff --git a/OWNERS b/OWNERS index dfd80b17e9..4ca6d6bfa5 100644 --- a/OWNERS +++ b/OWNERS @@ -8,6 +8,7 @@ approvers: - szuecs reviewers: + - johngmyers - njuettner - raffo - seanmalloy diff --git a/README.md b/README.md index 2426c9a362..fd517b651a 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,10 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz * [Plural](https://www.plural.sh/) * [Pi-hole](https://pi-hole.net/) -From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API. +ExternalDNS is, by default, aware of the records it is managing, therefore it can safely manage non-empty hosted zones. We strongly encourage you to set `--txt-owner-id` to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API. Note that all flags can be replaced with environment variables; for instance, -`--dry-run` could be replaced with `EXTERNAL_DNS_DRY_RUN=1`, or -`--registry txt` could be replaced with `EXTERNAL_DNS_REGISTRY=txt`. +`--dry-run` could be replaced with `EXTERNAL_DNS_DRY_RUN=1`. ## Status of providers @@ -176,6 +175,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) @@ -236,17 +236,17 @@ If the service is not of type Loadbalancer you need the --publish-internal-servi Locally run a single sync loop of ExternalDNS. ```console -external-dns --registry txt --txt-owner-id my-cluster-id --provider google --google-project example-project --source service --once --dry-run +external-dns --txt-owner-id my-cluster-id --provider google --google-project example-project --source service --once --dry-run ``` This should output the DNS records it will modify to match the managed zone with the DNS records you desire. It also assumes you are running in the `default` namespace. See the [FAQ](docs/faq.md) for more information regarding namespaces. -Note: TXT records will have `my-cluster-id` value embedded. Those are used to ensure that ExternalDNS is aware of the records it manages. +Note: TXT records will have the `my-cluster-id` value embedded. Those are used to ensure that ExternalDNS is aware of the records it manages. Once you're satisfied with the result, you can run ExternalDNS like you would run it in your cluster: as a control loop, and **not in dry-run** mode: ```console -external-dns --registry txt --txt-owner-id my-cluster-id --provider google --google-project example-project --source service +external-dns --txt-owner-id my-cluster-id --provider google --google-project example-project --source service ``` Check that ExternalDNS has created the desired DNS record for your Service and that it points to its load balancer's IP. Then try to resolve it: @@ -270,71 +270,6 @@ If using a txt registry and attempting to use a CNAME the `--txt-prefix` must be If `externalIPs` list is defined for a `LoadBalancer` service, this list will be used instead of an assigned load balancer IP to create a DNS record. It's useful when you run bare metal Kubernetes clusters behind NAT or in a similar setup, where a load balancer IP differs from a public IP (e.g. with [MetalLB](https://metallb.universe.tf)). -# Roadmap - -ExternalDNS was built with extensibility in mind. Adding and experimenting with new DNS providers and sources of desired DNS records should be as easy as possible. It should also be possible to modify how ExternalDNS behaves—e.g. whether it should add records but never delete them. - -Here's a rough outline on what is to come (subject to change): - -### v0.1 - -- [x] Support for Google CloudDNS -- [x] Support for Kubernetes Services - -### v0.2 - -- [x] Support for AWS Route 53 -- [x] Support for Kubernetes Ingresses - -### v0.3 - -- [x] Support for AWS Route 53 via ALIAS -- [x] Support for multiple zones -- [x] Ownership System - -### v0.4 - -- [x] Support for AzureDNS -- [x] Support for CloudFlare -- [x] Support for DigitalOcean -- [x] Multiple DNS names per Service - -### v0.5 - -- [x] Support for creating DNS records to multiple targets (for Google and AWS) -- [x] Support for OpenStack Designate -- [x] Support for PowerDNS -- [x] Support for Linode -- [x] Support for RcodeZero -- [x] Support for NS1 -- [x] Support for TransIP -- [x] Support for Azure Private DNS - -### v0.6 - -- [ ] Ability to replace kOps' [DNS Controller](https://github.com/kubernetes/kops/tree/HEAD/dns-controller) (This could also directly become `v1.0`) -- [x] Support for OVH - -### v1.0 - -- [ ] Ability to replace kOps' [DNS Controller](https://github.com/kubernetes/kops/tree/HEAD/dns-controller) - - [x] Add support for pod source - - [x] Add support for DNS Controller annotations for pod and service sources - - [ ] Add support for kOps gossip provider -- [x] Ability to replace Zalando's [Mate](https://github.com/linki/mate) -- [x] Ability to replace Molecule Software's [route53-kubernetes](https://github.com/wearemolecule/route53-kubernetes) - -### Yet to be defined - -* Support for CoreDNS -* Support for record weights -* Support for different behavioral policies -* Support for Services with `type=NodePort` -* Support for CRDs -* Support for more advanced DNS record configurations - -Have a look at [the milestones](https://github.com/kubernetes-sigs/external-dns/milestones) to get an idea of where we currently stand. - ## Contributing Are you interested in contributing to external-dns? We, the maintainers and community, would love your diff --git a/charts/external-dns/CHANGELOG.md b/charts/external-dns/CHANGELOG.md index 5c2d88be39..5c18948e0d 100644 --- a/charts/external-dns/CHANGELOG.md +++ b/charts/external-dns/CHANGELOG.md @@ -11,7 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### All Changes -## [v1.12.2] - UNRELEASED +- Disallowed privilege escalation in container security context and set the seccomp profile type to `RuntimeDefault`. ([#3689](https://github.com/kubernetes-sigs/external-dns/pull/3689)) [@nrvnrvn](https://github.com/nrvnrvn) +- Added RBAC for Traefik to ClusterRole. ([#3325](https://github.com/kubernetes-sigs/external-dns/pull/3325)) [@ThomasK33](https://github.com/thomask33) + +## [v1.13.0] - 2023-03-30 + +### All Changes + +- Updated _ExternalDNS_ version to [v0.13.5](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.13.5). ([#3661](https://github.com/kubernetes-sigs/external-dns/pull/3661)) [@GMartinez-Sisti](https://github.com/GMartinez-Sisti) +- Adding missing gateway-httproute cluster role permission. ([#3541](https://github.com/kubernetes-sigs/external-dns/pull/3541)) [@nicon89](https://github.com/nicon89) + +## [v1.12.2] - 2023-03-30 ### All Changes diff --git a/charts/external-dns/Chart.yaml b/charts/external-dns/Chart.yaml index 0e4e2cd9bb..8498852a7e 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.2 -appVersion: 0.13.4 +version: 1.13.0 +appVersion: 0.13.5 keywords: - kubernetes - externaldns @@ -20,15 +20,7 @@ maintainers: email: steve.hipwell@gmail.com annotations: artifacthub.io/changes: | - - kind: added - description: "Added support for ServiceMonitor relabelling." - kind: changed - 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." + description: "Updated _ExternalDNS_ version to [v0.13.5](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.13.5)." - kind: changed - description: "Updated _ExternalDNS_ version to [v0.13.4](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.13.4)." + description: "Adding missing gateway-httproute cluster role permission." diff --git a/charts/external-dns/ci/ci-values.yaml b/charts/external-dns/ci/ci-values.yaml new file mode 100644 index 0000000000..6c1735cbac --- /dev/null +++ b/charts/external-dns/ci/ci-values.yaml @@ -0,0 +1 @@ +provider: inmemory diff --git a/charts/external-dns/templates/clusterrole.yaml b/charts/external-dns/templates/clusterrole.yaml index 4f33c1146c..11852f1cf4 100644 --- a/charts/external-dns/templates/clusterrole.yaml +++ b/charts/external-dns/templates/clusterrole.yaml @@ -105,6 +105,11 @@ rules: resources: ["tcpingresses"] verbs: ["get","watch","list"] {{- end }} +{{- if has "traefik-proxy" .Values.sources }} + - apiGroups: ["traefik.containo.us", "traefik.io"] + resources: ["ingressroutes", "ingressroutetcps", "ingressrouteudps"] + verbs: ["get","watch","list"] +{{- end }} {{- if has "openshift-route" .Values.sources }} - apiGroups: ["route.openshift.io"] resources: ["routes"] diff --git a/charts/external-dns/values.yaml b/charts/external-dns/values.yaml index 6e0f80265a..fe0f0dd4cb 100644 --- a/charts/external-dns/values.yaml +++ b/charts/external-dns/values.yaml @@ -43,8 +43,11 @@ shareProcessNamespace: false podSecurityContext: fsGroup: 65534 + seccompProfile: + type: RuntimeDefault securityContext: + allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 65534 readOnlyRootFilesystem: true diff --git a/controller/controller.go b/controller/controller.go index b09e0bb4bb..af3e3219a2 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -195,8 +195,6 @@ func (c *Controller) RunOnce(ctx context.Context) error { return err } - missingRecords := c.Registry.MissingRecords() - registryEndpointsTotal.Set(float64(len(records))) regARecords, regAAAARecords := countAddressRecords(records) registryARecords.Set(float64(regARecords)) @@ -218,29 +216,6 @@ func (c *Controller) RunOnce(ctx context.Context) error { verifiedAAAARecords.Set(float64(vAAAARecords)) endpoints = c.Registry.AdjustEndpoints(endpoints) - if len(missingRecords) > 0 { - // Add missing records before the actual plan is applied. - // This prevents the problems when the missing TXT record needs to be - // created and deleted/upserted in the same batch. - missingRecordsPlan := &plan.Plan{ - Policies: []plan.Policy{c.Policy}, - Missing: missingRecords, - DomainFilter: endpoint.MatchAllDomainFilters{c.DomainFilter, c.Registry.GetDomainFilter()}, - PropertyComparator: c.Registry.PropertyValuesEqual, - ManagedRecords: c.ManagedRecordTypes, - } - missingRecordsPlan = missingRecordsPlan.Calculate() - if missingRecordsPlan.Changes.HasChanges() { - err = c.Registry.ApplyChanges(ctx, missingRecordsPlan.Changes) - if err != nil { - registryErrorsTotal.Inc() - deprecatedRegistryErrors.Inc() - return err - } - log.Info("All missing records are created") - } - } - plan := &plan.Plan{ Policies: []plan.Policy{c.Policy}, Current: records, @@ -336,7 +311,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 f6b03d4331..9f37ab2952 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -311,51 +311,6 @@ func testControllerFiltersDomains(t *testing.T, configuredEndpoints []*endpoint. } } -type noopRegistryWithMissing struct { - *registry.NoopRegistry - missingRecords []*endpoint.Endpoint -} - -func (r *noopRegistryWithMissing) MissingRecords() []*endpoint.Endpoint { - return r.missingRecords -} - -func testControllerFiltersDomainsWithMissing(t *testing.T, configuredEndpoints []*endpoint.Endpoint, domainFilter endpoint.DomainFilterInterface, providerEndpoints, missingEndpoints []*endpoint.Endpoint, expectedChanges []*plan.Changes) { - t.Helper() - cfg := externaldns.NewConfig() - cfg.ManagedDNSRecordTypes = []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME} - - source := new(testutils.MockSource) - source.On("Endpoints").Return(configuredEndpoints, nil) - - // Fake some existing records in our DNS provider and validate some desired changes. - provider := &filteredMockProvider{ - RecordsStore: providerEndpoints, - } - noop, err := registry.NewNoopRegistry(provider) - require.NoError(t, err) - - r := &noopRegistryWithMissing{ - NoopRegistry: noop, - missingRecords: missingEndpoints, - } - - ctrl := &Controller{ - Source: source, - Registry: r, - Policy: &plan.SyncPolicy{}, - DomainFilter: domainFilter, - ManagedRecordTypes: cfg.ManagedDNSRecordTypes, - } - - assert.NoError(t, ctrl.RunOnce(context.Background())) - assert.Equal(t, 1, provider.RecordsCallCount) - require.Len(t, provider.ApplyChangesCalls, len(expectedChanges)) - for i, change := range expectedChanges { - assert.Equal(t, *change, *provider.ApplyChangesCalls[i]) - } -} - func TestControllerSkipsEmptyChanges(t *testing.T) { testControllerFiltersDomains( t, @@ -683,60 +638,6 @@ func TestARecords(t *testing.T) { assert.Equal(t, math.Float64bits(1), valueFromMetric(registryARecords)) } -// TestMissingRecordsApply validates that the missing records result in the dedicated plan apply. -func TestMissingRecordsApply(t *testing.T) { - testControllerFiltersDomainsWithMissing( - t, - []*endpoint.Endpoint{ - { - DNSName: "record1.used.tld", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"1.2.3.4"}, - }, - { - DNSName: "record2.used.tld", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"8.8.8.8"}, - }, - }, - endpoint.NewDomainFilter([]string{"used.tld"}), - []*endpoint.Endpoint{ - { - DNSName: "record1.used.tld", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"1.2.3.4"}, - }, - }, - []*endpoint.Endpoint{ - { - DNSName: "a-record1.used.tld", - RecordType: endpoint.RecordTypeTXT, - Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, - }, - }, - []*plan.Changes{ - // Missing record had its own plan applied. - { - Create: []*endpoint.Endpoint{ - { - DNSName: "a-record1.used.tld", - RecordType: endpoint.RecordTypeTXT, - Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, - }, - }, - }, - { - Create: []*endpoint.Endpoint{ - { - DNSName: "record2.used.tld", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"8.8.8.8"}, - }, - }, - }, - }) -} - func TestAAAARecords(t *testing.T) { testControllerFiltersDomains( t, diff --git a/docs/annotations/annotations.md b/docs/annotations/annotations.md new file mode 100644 index 0000000000..79957c1393 --- /dev/null +++ b/docs/annotations/annotations.md @@ -0,0 +1,120 @@ +# Annotations + +ExternalDNS sources support a number of annotations on the Kubernetes resources that they examine. + +The following table documents which sources support which annotations: + +| Source | controller | hostname | internal-hostname | target | ttl | (provider-specific) | +|--------------|------------|----------|-------------------|---------|-----|---------------------| +| Ambassador | | | | | Yes | | +| Connector | | | | | | | +| Contour | Yes | Yes[^1] | | Yes | Yes | Yes | +| CloudFoundry | | | | | | | +| CRD | | | | | | | +| F5 | | | | | Yes | | +| Gateway | Yes | Yes[^1] | | | Yes | Yes | +| Gloo | | | | | Yes | Yes | +| Ingress | Yes | Yes[^1] | | Yes | Yes | Yes | +| Istio | Yes | Yes[^1] | | Yes | Yes | Yes | +| Kong | | Yes | | | Yes | Yes | +| Node | Yes | | | | Yes | | +| OpenShift | Yes | Yes[^1] | | Yes | Yes | Yes | +| Pod | | Yes | Yes | | | | +| Service | Yes | Yes[^1] | Yes[^1][^2] | Yes[^3] | Yes | Yes | +| Skipper | Yes | Yes[^1] | | Yes | Yes | Yes | +| Traefik | | Yes | | Yes | Yes | Yes | + +[^1]: Unless the `--ignore-hostname-annotation` flag is specified. +[^2]: Only behaves differently than `hostname` for `Service`s of type `LoadBalancer`. +[^3]: Also supported on `Pods` referenced from a headless `Service`'s `Endpoints`. + +## external-dns.alpha.kubernetes.io/access + +Specifies which set of node IP addresses to use for a `Service` of type `NodePort`. + +If the value is `public`, use the Nodes' addresses of type `ExternalIP`, plus IPv6 addresses of type `InternalIP`. + +If the value is `private`, use the Nodes' addresses of type `InternalIP`. + +If the annotation is not present and there is at least one address of type `ExternalIP`, +behave as if the value were `public`, otherwise behave as if the value were `private`. + +## external-dns.alpha.kubernetes.io/controller + +If this annotation exists and has a value other than `dns-controller` then the source ignores the resource. + +## external-dns.alpha.kubernetes.io/endpoints-type + +Specifies which set of addresses to use for a headless `Service`. + +If the value is `NodeExternalIP`, use each relevant `Pod`'s `Node`'s address of type `ExternalIP` +plus each IPv6 address of type `InternalIP`. + +Otherwise, if the value is `HostIP` or the `--publish-host-ip` flag is specified, use +each relevant `Pod`'s `Status.HostIP`. + +Otherwise, use the `IP` of each of the `Service`'s `Endpoints`'s `Addresses`. + +## external-dns.alpha.kubernetes.io/hostname + +Specifies the domain for the resource's DNS records. + +## external-dns.alpha.kubernetes.io/ingress-hostname-source + +Specifies where to get the domain for an `Ingress` resource. + +If the value is `defined-hosts-only`, use only the domains from the `Ingress` spec. + +If the value is `annotation-only`, use only the domains from the `Ingress` annotations. + +If the annotation is not present, use the domains from both the spec and annotations. + +## external-dns.alpha.kubernetes.io/internal-hostname + +Specifies the domain for the resource's DNS records that are for use from internal networks. + +For `Services` of type `LoadBalancer`, uses the `Service`'s `ClusterIP`. + +For `Pods`, uses the `Pod`'s `Status.PodIP`. + +## external-dns.alpha.kubernetes.io/target + +Specifies a comma-separated list of values to override the resource's DNS record targets (RDATA). + +Targets that parse as IPv4 addresses are published as A records and +targets that parse as IPv6 addresses are published as AAAA records. All other targets +are published as CNAME records. + +## external-dns.alpha.kubernetes.io/ttl + +Specifies the TTL (time to live) for the resource's DNS records. + +The value may be specified as either a duration or an integer number of seconds. +It must be between 1 and 2,147,483,647 seconds. + +## Provider-specific annotations + +Some providers define their own annotations. Cloud-specific annotations have keys prefixed as follows: + +| Cloud | Annotation prefix | +|------------|------------------------------------------------| +| AWS | `external-dns.alpha.kubernetes.io/aws-` | +| CloudFlare | `external-dns.alpha.kubernetes.io/cloudflare-` | +| IBM Cloud | `external-dns.alpha.kubernetes.io/ibmcloud-` | +| Scaleway | `external-dns.alpha.kubernetes.io/scw-` | + +Additional annotations that are currently implemented only by AWS are: + +### external-dns.alpha.kubernetes.io/alias + +If the value of this annotation is `true`, specifies that CNAME records generated by the +resource should instead be alias records. + +This annotation is only relevant if the `--aws-prefer-cname` flag is specified. + +### external-dns.alpha.kubernetes.io/set-identifier + +Specifies the set identifier for DNS records generated by the resource. + +A set identifier differentiates among multiple DNS record sets that have the same combination of domain and type. +Which record set or sets are returned to queries is then determined by the configured routing policy. diff --git a/docs/contributing/crd-source.md b/docs/contributing/crd-source.md index 816b7344b2..40e3814a5b 100644 --- a/docs/contributing/crd-source.md +++ b/docs/contributing/crd-source.md @@ -15,10 +15,11 @@ Here is typical example of [CRD API type](https://github.com/kubernetes-sigs/ext type TTL int64 type Targets []string type ProviderSpecificProperty struct { - Name string - Value string + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` } type ProviderSpecific []ProviderSpecificProperty +type Labels map[string]string type Endpoint struct { // The hostname of the DNS record diff --git a/docs/contributing/crd-source/dnsendpoint-example.yaml b/docs/contributing/crd-source/dnsendpoint-example.yaml index 894301d8ae..2ed7d7fa01 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/contributing/getting-started.md b/docs/contributing/getting-started.md index 82976ee6d8..7cf42ca09f 100644 --- a/docs/contributing/getting-started.md +++ b/docs/contributing/getting-started.md @@ -1,7 +1,7 @@ # Quick Start - [Git](https://git-scm.com/downloads) -- [Go 1.19+](https://golang.org/dl/) +- [Go 1.20+](https://golang.org/dl/) - [Go modules](https://github.com/golang/go/wiki/Modules) - [golangci-lint](https://github.com/golangci/golangci-lint) - [Docker](https://docs.docker.com/install/) diff --git a/docs/faq.md b/docs/faq.md index 044ee7aa86..6da020c5d8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -207,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.4 + 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: ... ``` @@ -257,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/proposal/registry.md b/docs/proposal/registry.md deleted file mode 100644 index 84f355dc7f..0000000000 --- a/docs/proposal/registry.md +++ /dev/null @@ -1,124 +0,0 @@ -# Registry -#### [Old name: storage] - -Initial discussion - https://github.com/kubernetes-sigs/external-dns/issues/44 - -## Purpose - -One should not be afraid to use external-dns, because it can delete or overwrite the records preexisting in the DNS provider. - -**Why we need it?** - -DNS provider (AWS Route53, Google DNS, etc.) stores dns records which are created via various means. Integration of External-DNS should be safe and should not delete or overwrite the records which it is not responsible for. Moreover, it should certainly be possible for multiple kubernetes clusters to share the same hosted zone within the dns provider, additionally multiple external-dns instances inside the same cluster should be able to co-exist without messing with the same set of records. - -Registry provides a mechanism to ensure the safe management of DNS records - -This proposal introduces multiple possible implementation with the details depending on the setup. - -## Requirements and assumptions - -1. Pre-existing records should not be modified by external-dns -2. External-dns instance only creates/modifies/deletes records which are created by this instance -3. It should be possible to transfer the ownership to another external-dns instance -4. ~~Any integrated DNS provider should provide at least a single way to implement the registry~~ Noop registry can be used to disable ownership -5. Lifetime of the records should not be limited to lifetime of external-dns -6. External-dns should have its identifier for marking the managed records - **`owner-id`** - -## Types of registry - -The following presents two ways to implement the registry, and we are planning to implement both for compatibility purposes. - -### TXT records - -This implementation idea is borrowed from [Mate](https://github.com/linki/mate/) - -Each record created by external-dns is accompanied by the TXT record, which internally stores the external-dns identifier. For example, if external dns with `owner-id="external-dns-1"` record to be created with dns name `foo.zone.org`, external-dns will create a TXT record with the same dns name `-foo.zone.org` and injected value of `"external-dns-1"`. The transfer of ownership can be done by modifying the value of the TXT record. If no TXT record exists for the record or the value does not match its own `owner-id`, then external-dns will simply ignore it. - -#### Goods -1. Easy to guarantee cross-cluster ownership safety -2. Data lifetime is not limited to cluster or external-dns lifetime -3. Supported by major DNS providers -4. TXT record are created alongside other records in a batch request. Hence **eliminating possibility of having inconsistent ownership information and dns provider state** - -#### Bads -1. TXT record cannot co-exist with CNAME records (this can be solved by creating a TXT record with another domain name, e.g. `foo.org->foo.txt.org`) -2. Introduces complexity to the logic -3. Difficult to do the transfer of ownership -4. Too easy to mess up with manual modifications - -### ConfigMap - -**This implementation cannot be considered 100% error free**, hence use with caution [see **Possible failure scenario** below] - -Store the state in the configmap. ConfigMap is created and managed by each external-dns individually, i.e. external-dns with **`owner-id=external-dns-1`** will create and operate on `extern-dns-1-registry` ConfigMap. ConfigMap will store **all** the records present in the DNS provider as serialized JSON. For example: - -``` -kind: ConfigMap -apiVersion: v1 -metadata: - creationTimestamp: 2016-03-09T19:14:38Z - name: external-dns-1-storage - namespace: same-as-external-dns-1 -data: - records: "[{ - \"dnsname\": \"foo.org\", - \"owner\": \"external-dns-1\", - \"target\": \"loadbalancer1.com\", - \"type\": \"A\" -}, { - \"dnsname\": \"bar.org\", - \"owner\": \"external-dns-2\", - \"target\": \"loadbalancer2.com\", - \"type\": \"A\" -}, { - \"dnsname\": \"unmanaged.org\", - \"owner\": \"\", - \"target\": \"loadbalancer2.com\", - \"type\": \"CNAME\" -}]" - -``` - -ConfigMap will be periodically resynced with the dns provider by fetching the dns records and comparing it with the data currently stored and hence rebuilding the ownership information. - -#### Goods -1. Not difficult to implement and easy to do the ownership transfer -2. ConfigMap is a first class citizen in kubernetes world -3. Does not create dependency/restriction on DNS provider -4. Cannot be easily messed with by other parties - -#### Bads -1. ConfigMap might be out of sync with dns provider state -2. LifeTime is obviously limited to the cluster lifetime -3. Not supported in older kubernetes clusters -4. Bloated ConfigMap - cannot be paginated and will grow very big on DNS provider with thousands of records - -#### Failure scenario - -It is possible that the configmap will go out of sync with the dns provider state. In the implementation flow external-dns will first modify records on dns provider side to subsequently update configmap. And if ExternalDNS will crash in-between two operation created records will be left unmanaged and not viable for update/deletion by External DNS. - -## Component integration - -Components: -* Source - all endpoints ( collection of ingress, service[type=LoadBalancer] etc.) -* [Plan](https://github.com/kubernetes-sigs/external-dns/issues/13) - object responsible for the create of change lists in external-dns -* Provider - interface to access the DNS provider API - -Registry will serve as wrapper around `Provider` providing additional information regarding endpoint ownership. Ownership will further taken into account by `Plan` to filter out records to include only records managed by current ExternalDNS instance (having same `owner-id` value) - -A single loop iteration of external-dns operation: - -1. Get all endpoints ( collection ingress, service[type=LoadBalancer] etc.) into collection of `endpoints` -2. Get registry `Records()` (makes the call to DNSProvider and also build ownership information) -3. Pass `Records` (including ownership information) and list of endpoints to `Plan` to do the calculation -4. Call registry `ApplyChanges()` method (which subsequently calls DNS Provider Apply method to update records) -5. If ConfigMap implementation of Registry is used, then ConfigMap needs to be updated separately - -~~In case of configmap, Registry gets updated all the time via `Poll`. `Plan` does not call DNS provider directly. Good value of the `Poll` is to have simple rate limiting mechanism on DNS provider.~~ - -#### Notes: - -1. DNS Provider should use batch operations -2. DNS Provider should be called with CREATE operation (not UPSERT!) when the record does not yet exist! - - diff --git a/docs/registry.md b/docs/registry.md deleted file mode 100644 index 615ba305a6..0000000000 --- a/docs/registry.md +++ /dev/null @@ -1,15 +0,0 @@ -### TXT Registry migration to a new format ### - -In order to support more record types and be able to track ownership without TXT record name clash, a new TXT record is introduced. -It contains record type it manages, e.g.: -* A record foo.example.com will be tracked with classic foo.example.com TXT record -* At the same time a new TXT record will be created a-foo.example.com - -Prefix and suffix are extended with %{record_type} template where the user can control how prefixed/suffixed records should look like. - -In order to maintain compatibility, both records will be maintained for some time, in order to have downgrade possibility. -The controller will try to create the "new format" TXT records if they are not present to ease the migration from the versions < 0.12.0. - -Later on, the old format will be dropped and only the new format will be kept (-). - -Cleanup will be done by controller itself. diff --git a/docs/registry/dynamodb.md b/docs/registry/dynamodb.md new file mode 100644 index 0000000000..9e5f8331fd --- /dev/null +++ b/docs/registry/dynamodb.md @@ -0,0 +1,41 @@ +# The DynamoDB registry + +The DynamoDB registry stores DNS record metadata in an AWS DynamoDB table. + +## The DynamoDB Table + +By default, the DynamoDB registry stores data in the table named `external-dns`. +A different table may be specified using the `--dynamodb-table` flag. +A different region may be specified using the `--dynamodb-region` flag. + +The table must have a partition (hash) key named `k` and string type. +The table must not have a sort (range) key. + +## IAM permissions + +The ExternalDNS Role must be granted the following permissions: + +```json + { + "Effect": "Allow", + "Action": [ + "DynamoDB:DescribeTable", + "DynamoDB:PartiQLDelete", + "DynamoDB:PartiQLInsert", + "DynamoDB:PartiQLUpdate", + "DynamoDB:Scan" + ], + "Resource": [ + "arn:aws:dynamodb:*:*:table/external-dns" + ] + } +``` + +The region and account ID may be specified explicitly specified instead of using wildcards. + +## Caching + +The DynamoDB registry can optionally cache DNS records read from the provider. This can mitigate +rate limits imposed by the provider. + +Caching is enabled by specifying a cache duration with the `--txt-cache-interval` flag. diff --git a/docs/registry/registry.md b/docs/registry/registry.md new file mode 100644 index 0000000000..59a4313ce8 --- /dev/null +++ b/docs/registry/registry.md @@ -0,0 +1,17 @@ +# Registries + +A registry persists metadata pertaining to DNS records. + +The most important metadata is the owning external-dns deployment. +This is specified using the `--txt-owner-id` flag, specifying a value unique to the +deployment of external-dns and which doesn't change for the lifetime of the deployment. +Deployments in different clusters but sharing a DNS zone need to use different owner IDs. + +The registry implementation is specified using the `--registry` flag. + +## Supported registries + +* [txt](txt.md) (default) - Stores metadata in TXT records in the same provider. +* [dynamodb](dynamodb.md) - Stores metadata in an AWS DynamoDB table. +* noop - Passes metadata directly to the provider. For most providers, this means the metadata is not persisted. +* aws-sd - Stores metadata in AWS Service Discovery. Only usable with the `aws-sd` provider. diff --git a/docs/registry/txt.md b/docs/registry/txt.md new file mode 100644 index 0000000000..c6ea26bfbd --- /dev/null +++ b/docs/registry/txt.md @@ -0,0 +1,97 @@ +# The TXT registry + +The TXT registry is the default registry. +It stores DNS record metadata in TXT records, using the same provider. + +## Prefixes and Suffixes + +In order to avoid having the registry TXT records collide with +TXT or CNAME records created from sources, you can configure a fixed prefix or suffix +to be added to the first component of the domain of all registry TXT records. + +The prefix or suffix may not be changed after initial deployment, +lest the registry records be orphaned and the metadata be lost. + +The prefix or suffix may contain the substring `%{record_type}`, which is replaced with +the record type of the DNS record for which it is storing metadata. + +The prefix is specified using the `--txt-prefix` flag and the suffix is specified using +the `--txt-suffix` flag. The two flags are mutually exclusive. + +## Wildcard Replacement + +The `--txt-wildcard-replacement` flag specifies a string to use to replace the "*" in +registry TXT records for wildcard domains. Without using this, registry TXT records for +wildcard domains will have invalid domain syntax and be rejected by most providers. + +## Encryption + +Registry TXT records may contain information, such as the internal ingress name or namespace, considered sensitive, , which attackers could exploit to gather information about your infrastructure. +By encrypting TXT records, you can protect this information from unauthorized access. + +Encryption is enabled by using the `--txt-encrypt-enabled` flag. The 32-byte AES-256-GCM encryption +key must be specified in URL-safe base64 form, using the `--txt-encrypt-aes-key` flag. + +Note that the key used for encryption should be a secure key and properly managed to ensure the security of your TXT records. + +### Generating the TXT Encryption 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 Encrypting/Decrypting TXT Records + +In some cases you might need to edit registry TXT records. The following example Go code encrypts and decrypts such records. + +```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) +} +``` + +## Caching + +The TXT registry can optionally cache DNS records read from the provider. This can mitigate +rate limits imposed by the provider. + +Caching is enabled by specifying a cache duration with the `--txt-cache-interval` flag. diff --git a/docs/tutorials/ANS_Group_SafeDNS.md b/docs/tutorials/ANS_Group_SafeDNS.md index c60e65517b..92450495b9 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.4 + 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.4 + 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 dd065daed0..4c88934239 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.4 + 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.4 + 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 ee0477edea..379e13b4c5 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.4 + 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.4 + 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 9e66b1e6da..98bc5da693 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 970af7940a..c540fc7daf 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.4 + 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.4 + 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 d5e7ad92b4..be05cad6e4 100644 --- a/docs/tutorials/aws.md +++ b/docs/tutorials/aws.md @@ -29,7 +29,8 @@ Hosted Zone IDs. "Effect": "Allow", "Action": [ "route53:ListHostedZones", - "route53:ListResourceRecordSets" + "route53:ListResourceRecordSets", + "route53:ListTagsForResource" ], "Resource": [ "*" @@ -413,7 +414,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.4 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -508,7 +509,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.4 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -739,9 +740,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 +936,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 +962,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.4 + 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 640036e461..218b11dff7 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.4 + 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.4 + 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.4 + 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 186407aaae..386721bcb2 100644 --- a/docs/tutorials/azure.md +++ b/docs/tutorials/azure.md @@ -50,7 +50,7 @@ The following fields are used: * `tenantId` (**required**) - run `az account show --query "tenantId"` or by selecting Azure Active Directory in the Azure Portal and checking the _Directory ID_ under Properties. * `subscriptionId` (**required**) - run `az account show --query "id"` or by selecting Subscriptions in the Azure Portal. * `resourceGroup` (**required**) is the Resource Group created in a previous step that contains the Azure DNS Zone. -* `aadClientID` and `aaClientSecret` are associated with the Service Principal. This is only used with Service Principal method documented in the next section. +* `aadClientID` and `aadClientSecret` are associated with the Service Principal. This is only used with Service Principal method documented in the next section. * `useManagedIdentityExtension` - this is set to `true` if you use either AKS Kubelet Identity or AAD Pod Identities methods documented in the next section. * `userAssignedIdentityID` - this contains the client id from the Managed identitty when using the AAD Pod Identities method documented in the next setion. @@ -356,7 +356,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.4 + 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.4 + 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.4 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress @@ -648,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 4ef9c1b39a..5d386aab87 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.4 + 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.4 + 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 efa5b6fc45..7da6cbce07 100644 --- a/docs/tutorials/civo.md +++ b/docs/tutorials/civo.md @@ -2,7 +2,7 @@ This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Civo DNS Manager. -Make sure to use **>0.12.2** version of ExternalDNS for this tutorial. +Make sure to use **>0.13.5** version of ExternalDNS for this tutorial. ## Managing DNS with Civo @@ -12,8 +12,7 @@ If you want to learn about how to use Civo DNS Manager read the following tutori ## Get Civo Token -Copy the token in the settings fo your account - +Copy the token in the settings for your account The environment variable `CIVO_TOKEN` will be needed to run ExternalDNS with Civo. ## Deploy ExternalDNS @@ -41,7 +40,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.4 + 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 +58,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 +73,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 +104,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.4 + 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 79deffeff7..869483e89e 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -56,7 +56,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.4 + 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.4 + 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 05e392b6f8..cdb30c52e0 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.4 + 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.4 + 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 f2c11b8c2f..3ef2caf317 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.4 + 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.4 + 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 e2c79bfbbf..7ed8b24b45 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.4 + 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.4 + 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 cbad0cfe6d..a6874326f9 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.4 + 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.4 + 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 5af82f6d93..3bdce68381 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.4 + 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.4 + 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 573837bb5b..40f5c87ea2 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.4 + 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 d1e93cb0f0..6f7dd3fbc2 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.4 + 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 c5a5600395..3604823ede 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.4 + 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 7f4f4036b9..a51ad0abdf 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.4 + 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.4 + 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 26ebae312f..f1f39ca690 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.4 + 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 d4d9e8d298..e0308f9e7b 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.4 + 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 be5de4a60a..7a9994eca6 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.4 + 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.4 + 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 cd6ba15285..0dddfdf811 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.4 + 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.4 + 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 2a0d321dc6..904fe36672 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.4 + 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.4 + 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 6f243bd5ef..b6a164d0f0 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.4 + 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.4 + 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 3b25aa5230..143067d388 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.4 + 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.4 + 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 a63d1d815f..0a519a46bc 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.4 + 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.4 + 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 fc4fbf20f1..1c5bd6db3e 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.4 + 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.4 + 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 bea5e56ac1..5cf37d4ec9 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 5884c162e6..101a1be3f7 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.4 + 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.4 + 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 0000000000..5b5a0eb0b2 --- /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 f6c170b4c4..fb663d554a 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.4 + 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.4 + 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 46f21da5d8..09507180ef 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.4 + 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.4 + 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 b96ca85a81..faccd6b1c3 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.4 + 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.4 + 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 e5a995b42c..b8297ee4f7 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.4 + 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.4 + 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 d7d385839e..39d804d803 100644 --- a/docs/tutorials/oracle.md +++ b/docs/tutorials/oracle.md @@ -124,7 +124,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.4 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=service - --source=ingress diff --git a/docs/tutorials/ovh.md b/docs/tutorials/ovh.md index 0f05457f46..5bd5078e55 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.4 + 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.4 + 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 ff0a9874aa..90efd6f04a 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.4 + 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 dfb3137c9f..29f1403f29 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.4 + 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 956087e934..98aaf2079c 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.4 + 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.4 + 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 9a4f7be6f4..970ad00b55 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.4 + 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.4 + 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 7890b8fc91..72ee1b32f2 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.4 + 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.4 + 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 684c7c64d5..62653a5088 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.4 + 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.4 + 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 63e515826f..144179c083 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.4 + 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.4 + 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 b6aa2d0984..a098970b80 100644 --- a/docs/tutorials/scaleway.md +++ b/docs/tutorials/scaleway.md @@ -19,12 +19,10 @@ In this example we will use `example.com` as an example. To use ExternalDNS with Scaleway DNS, you need to create an API token (composed of the Access Key and the Secret Key). You can either use existing ones or you can create a new token, as explained in [How to generate an API token](https://www.scaleway.com/en/docs/generate-an-api-token/) or directly by going to the [credentials page](https://console.scaleway.com/account/organization/credentials). -Note that you will also need to the Organization ID, which can be retrieve on the same page. -Three environment variables are needed to run ExternalDNS with Scaleway DNS: +Two environment variables are needed to run ExternalDNS with Scaleway DNS: - `SCW_ACCESS_KEY` which is the Access Key. - `SCW_SECRET_KEY` which is the Secret Key. -- `SCW_DEFAULT_ORGANIZATION_ID` which is the Default Organization ID. ## Deploy ExternalDNS @@ -53,7 +51,7 @@ spec: spec: containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.4 + 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. @@ -63,8 +61,6 @@ spec: value: "" - name: SCW_SECRET_KEY value: "" - - name: SCW_DEFAULT_ORGANIZATION_ID - value: "" ``` ### Manifest (for clusters with RBAC enabled) @@ -121,7 +117,7 @@ spec: serviceAccountName: external-dns containers: - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.13.4 + 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. @@ -131,8 +127,6 @@ spec: value: "" - name: SCW_SECRET_KEY value: "" - - name: SCW_DEFAULT_ORGANIZATION_ID - value: "" ``` diff --git a/docs/tutorials/security-context.md b/docs/tutorials/security-context.md index 0b5bf39291..107033778b 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.4 + 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 e5a94311df..ea567f3823 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.4 + image: registry.k8s.io/external-dns/external-dns:v0.13.5 imagePullPolicy: Always name: external-dns resources: {} diff --git a/docs/tutorials/traefik-proxy.md b/docs/tutorials/traefik-proxy.md new file mode 100644 index 0000000000..7b83a1a7c9 --- /dev/null +++ b/docs/tutorials/traefik-proxy.md @@ -0,0 +1,96 @@ +# Configuring ExternalDNS to use the Traefik Proxy Source + +This tutorial describes how to configure ExternalDNS to use the Traefik Proxy source. +It is meant to supplement the other provider-specific setup tutorials. + +## Manifest (for clusters without RBAC enabled) + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + # update this to the desired external-dns version + image: registry.k8s.io/external-dns/external-dns:v0.13.3 + args: + - --source=traefik-proxy + - --provider=aws + - --registry=txt + - --txt-owner-id=my-identifier +``` + +## Manifest (for clusters with RBAC enabled) + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services","endpoints","pods"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list","watch"] +- apiGroups: ["traefik.containo.us","traefik.io"] + resources: ["ingressroutes", "ingressroutetcps", "ingressrouteudps"] + verbs: ["get","watch","list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: external-dns-viewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: external-dns +subjects: +- kind: ServiceAccount + name: external-dns + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + # update this to the desired external-dns version + image: registry.k8s.io/external-dns/external-dns:v0.13.3 + args: + - --source=traefik-proxy + - --provider=aws + - --registry=txt + - --txt-owner-id=my-identifier +``` diff --git a/docs/tutorials/transip.md b/docs/tutorials/transip.md index 83dd6ec509..d2a7aa3f65 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.4 + 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.4 + 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 af684bebb0..50d98cf6cb 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.4 + 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.4 + 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 b7166c7776..1005b63c34 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.4 + 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.4 + 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 7eef4a580b..0c491c0f64 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.4 + 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.4 + 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 0000000000..1d6ebd1dd7 --- /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 0000000000..880afcce3a --- /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/domain_filter.go b/endpoint/domain_filter.go index 1fe36aee05..8ddde179b6 100644 --- a/endpoint/domain_filter.go +++ b/endpoint/domain_filter.go @@ -24,41 +24,22 @@ import ( // DomainFilterInterface defines the interface to select matching domains for a specific provider or runtime type DomainFilterInterface interface { Match(domain string) bool - IsConfigured() bool } type MatchAllDomainFilters []DomainFilterInterface func (f MatchAllDomainFilters) Match(domain string) bool { - if !f.IsConfigured() { - return true - } for _, filter := range f { if filter == nil { continue } - if filter.IsConfigured() && !filter.Match(domain) { + if !filter.Match(domain) { return false } } return true } -func (f MatchAllDomainFilters) IsConfigured() bool { - if f == nil { - return false - } - for _, filter := range f { - if filter == nil { - continue - } - if filter.IsConfigured() { - return true - } - } - return len(f) > 0 -} - // DomainFilter holds a lists of valid domain names type DomainFilter struct { // Filters define what domains to match diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 6c22c4fba2..7df3f882f9 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -42,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 @@ -160,6 +162,13 @@ type ProviderSpecificProperty struct { // ProviderSpecific holds configuration which is specific to individual DNS providers type ProviderSpecific []ProviderSpecificProperty +// EndpointKey is the type of a map key for separating endpoints or targets. +type EndpointKey struct { + DNSName string + RecordType string + SetIdentifier string +} + // Endpoint is a high-level way of a connection between a service and an IP type Endpoint struct { // The hostname of the DNS record @@ -220,22 +229,52 @@ func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint { // warrant its own field on the Endpoint object itself. It differs from Labels in the fact that it's // not persisted in the Registry but only kept in memory during a single record synchronization. func (e *Endpoint) WithProviderSpecific(key, value string) *Endpoint { - if e.ProviderSpecific == nil { - e.ProviderSpecific = ProviderSpecific{} + e.SetProviderSpecificProperty(key, value) + return e +} + +// GetProviderSpecificProperty returns the value of a ProviderSpecificProperty if the property exists. +func (e *Endpoint) GetProviderSpecificProperty(key string) (string, bool) { + for _, providerSpecific := range e.ProviderSpecific { + if providerSpecific.Name == key { + return providerSpecific.Value, true + } + } + return "", false +} + +// SetProviderSpecificProperty sets the value of a ProviderSpecificProperty. +func (e *Endpoint) SetProviderSpecificProperty(key string, value string) { + for i, providerSpecific := range e.ProviderSpecific { + if providerSpecific.Name == key { + e.ProviderSpecific[i] = ProviderSpecificProperty{ + Name: key, + Value: value, + } + return + } } e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{Name: key, Value: value}) - return e } -// GetProviderSpecificProperty returns a ProviderSpecificProperty if the property exists. -func (e *Endpoint) GetProviderSpecificProperty(key string) (ProviderSpecificProperty, bool) { - for _, providerSpecific := range e.ProviderSpecific { +// DeleteProviderSpecificProperty deletes any ProviderSpecificProperty of the specified name. +func (e *Endpoint) DeleteProviderSpecificProperty(key string) { + for i, providerSpecific := range e.ProviderSpecific { if providerSpecific.Name == key { - return providerSpecific, true + e.ProviderSpecific = append(e.ProviderSpecific[:i], e.ProviderSpecific[i+1:]...) + return } } - return ProviderSpecificProperty{}, false +} + +// Key returns the EndpointKey of the Endpoint. +func (e *Endpoint) Key() EndpointKey { + return EndpointKey{ + DNSName: e.DNSName, + RecordType: e.RecordType, + SetIdentifier: e.SetIdentifier, + } } func (e *Endpoint) String() string { diff --git a/endpoint/labels.go b/endpoint/labels.go index 797190db41..c65333dc6e 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 9386a23e6a..394635b710 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/endpoint/target_filter.go b/endpoint/target_filter.go index 1d1b90c880..e4e69957fa 100644 --- a/endpoint/target_filter.go +++ b/endpoint/target_filter.go @@ -26,7 +26,6 @@ import ( // TargetFilterInterface defines the interface to select matching targets for a specific provider or runtime type TargetFilterInterface interface { Match(target string) bool - IsConfigured() bool } // TargetNetFilter holds a lists of valid target names @@ -61,11 +60,6 @@ func NewTargetNetFilterWithExclusions(targetFilterNets []string, excludeNets []s return TargetNetFilter{FilterNets: prepareTargetFilters(targetFilterNets), excludeNets: prepareTargetFilters(excludeNets)} } -// NewTargetNetFilter returns a new TargetNetFilter given a comma separated list of targets -func NewTargetNetFilter(targetFilterNets []string) TargetNetFilter { - return TargetNetFilter{FilterNets: prepareTargetFilters(targetFilterNets)} -} - // Match checks whether a target can be found in the TargetNetFilter. func (tf TargetNetFilter) Match(target string) bool { return matchTargetNetFilter(tf.FilterNets, target, true) && !matchTargetNetFilter(tf.excludeNets, target, false) @@ -89,11 +83,3 @@ func matchTargetNetFilter(filters []*net.IPNet, target string, emptyval bool) bo return false } - -// IsConfigured returns true if TargetFilter is configured, false otherwise -func (tf TargetNetFilter) IsConfigured() bool { - if len(tf.FilterNets) == 1 { - return tf.FilterNets[0].Network() != "" - } - return len(tf.FilterNets) > 0 -} diff --git a/endpoint/target_filter_test.go b/endpoint/target_filter_test.go index 4d7380426e..57690f7c1d 100644 --- a/endpoint/target_filter_test.go +++ b/endpoint/target_filter_test.go @@ -68,19 +68,6 @@ var targetFilterTests = []targetFilterTest{ }, } -func TestTargetFilterMatch(t *testing.T) { - for i, tt := range targetFilterTests { - if len(tt.exclusions) > 0 { - t.Logf("NewTargetFilter() doesn't support exclusions - skipping test %+v", tt) - continue - } - targetFilter := NewTargetNetFilter(tt.targetFilter) - for _, target := range tt.targets { - assert.Equal(t, tt.expected, targetFilter.Match(target), "should not fail: %v in test-case #%v", target, i) - } - } -} - func TestTargetFilterWithExclusions(t *testing.T) { for i, tt := range targetFilterTests { if len(tt.exclusions) == 0 { @@ -107,47 +94,3 @@ func TestMatchTargetFilterReturnsProperEmptyVal(t *testing.T) { assert.Equal(t, true, matchFilter(emptyFilters, "sometarget.com", true)) assert.Equal(t, false, matchFilter(emptyFilters, "sometarget.com", false)) } - -func TestTargetFilterIsConfigured(t *testing.T) { - for _, tt := range []struct { - filters []string - exclude []string - expected bool - }{ - { - []string{""}, - []string{""}, - false, - }, - { - []string{" "}, - []string{" "}, - false, - }, - { - []string{"", ""}, - []string{""}, - false, - }, - { - []string{"10/8"}, - []string{" "}, - false, - }, - { - []string{"10.0.0.0/8"}, - []string{" "}, - true, - }, - { - []string{" 10.0.0.0/8 "}, - []string{" ignored "}, - true, - }, - } { - t.Run("test IsConfigured", func(t *testing.T) { - tf := NewTargetNetFilterWithExclusions(tt.filters, tt.exclude) - assert.Equal(t, tt.expected, tf.IsConfigured()) - }) - } -} diff --git a/go.mod b/go.mod index d7b624ae47..df8ec7be7d 100644 --- a/go.mod +++ b/go.mod @@ -1,82 +1,83 @@ module sigs.k8s.io/external-dns -go 1.19 +go 1.20 require ( cloud.google.com/go/compute/metadata v0.2.3 - github.com/Azure/azure-sdk-for-go v66.0.0+incompatible - github.com/Azure/go-autorest/autorest v0.11.28 - github.com/Azure/go-autorest/autorest/adal v0.9.21 + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible + github.com/Azure/go-autorest/autorest v0.11.29 + github.com/Azure/go-autorest/autorest/adal v0.9.23 github.com/Azure/go-autorest/autorest/to v0.4.0 - github.com/F5Networks/k8s-bigip-ctlr/v2 v2.11.1 - github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.0.0 - github.com/IBM/go-sdk-core/v5 v5.8.0 - github.com/IBM/networking-go-sdk v0.36.0 - github.com/StackExchange/dnscontrol/v3 v3.27.1 + github.com/F5Networks/k8s-bigip-ctlr/v2 v2.13.0 + github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.1.0 + github.com/IBM/go-sdk-core/v5 v5.13.4 + github.com/IBM/networking-go-sdk v0.42.0 + github.com/StackExchange/dnscontrol/v3 v3.31.6 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 - github.com/alecthomas/kingpin v2.2.5+incompatible - github.com/aliyun/alibaba-cloud-sdk-go v1.62.4 - github.com/ans-group/sdk-go v1.10.4 - github.com/aws/aws-sdk-go v1.44.136 - github.com/bodgit/tsig v1.2.0 + github.com/alecthomas/kingpin v2.2.6+incompatible + github.com/aliyun/alibaba-cloud-sdk-go v1.62.380 + github.com/ans-group/sdk-go v1.16.5 + github.com/aws/aws-sdk-go v1.44.285 + github.com/bodgit/tsig v1.2.2 github.com/civo/civogo v0.3.14 - github.com/cloudflare/cloudflare-go v0.58.1 + github.com/cloudflare/cloudflare-go v0.69.0 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/datawire/ambassador v1.6.0 github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba - github.com/digitalocean/godo v1.97.0 - github.com/dnsimple/dnsimple-go v1.0.1 - github.com/exoscale/egoscale v0.97.0 + github.com/digitalocean/godo v1.99.0 + github.com/dnsimple/dnsimple-go v1.2.0 + github.com/exoscale/egoscale v1.19.0 github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 github.com/go-gandi/go-gandi v0.6.0 github.com/google/go-cmp v0.5.9 - github.com/gophercloud/gophercloud v0.25.0 + github.com/gophercloud/gophercloud v1.4.0 github.com/hooklift/gowsdl v0.5.0 - github.com/infobloxopen/infoblox-go-client/v2 v2.1.2-0.20220407114022-6f4c71443168 + github.com/infobloxopen/infoblox-go-client/v2 v2.3.0 github.com/linki/instrumented_http v0.3.0 - github.com/linode/linodego v1.9.1 - github.com/maxatome/go-testdeep v1.12.0 - github.com/miekg/dns v1.1.51 + github.com/linode/linodego v1.17.0 + github.com/maxatome/go-testdeep v1.13.0 + github.com/miekg/dns v1.1.55 github.com/nesv/go-dynect v0.6.0 github.com/nic-at/rc0go v1.1.1 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/v65 v65.35.0 - github.com/ovh/go-ovh v1.1.0 + github.com/openshift/api v0.0.0-20230607130528-611114dca681 + github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3 + github.com/oracle/oci-go-sdk/v65 v65.41.0 + github.com/ovh/go-ovh v1.4.1 github.com/pkg/errors v0.9.1 - github.com/pluralsh/gqlclient v1.1.6 - github.com/projectcontour/contour v1.23.2 - github.com/prometheus/client_golang v1.14.0 - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f - github.com/sirupsen/logrus v1.9.0 - github.com/stretchr/testify v1.8.2 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.599 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.344 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.599 - github.com/transip/gotransip/v6 v6.19.0 - github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 - github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556 + github.com/pluralsh/gqlclient v1.3.17 + github.com/projectcontour/contour v1.25.0 + github.com/prometheus/client_golang v1.16.0 + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.4 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.684 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.684 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.684 + github.com/transip/gotransip/v6 v6.20.0 + github.com/ultradns/ultradns-sdk-go v1.3.7 + github.com/vinyldns/go-vinyldns v0.9.16 github.com/vultr/govultr/v2 v2.17.2 - go.etcd.io/etcd/api/v3 v3.5.8 - go.etcd.io/etcd/client/v3 v3.5.8 + go.etcd.io/etcd/api/v3 v3.5.9 + go.etcd.io/etcd/client/v3 v3.5.9 go.uber.org/ratelimit v0.2.0 - golang.org/x/net v0.7.0 - golang.org/x/oauth2 v0.5.0 - golang.org/x/sync v0.1.0 - google.golang.org/api v0.110.0 - gopkg.in/ns1/ns1-go.v2 v2.7.4 + golang.org/x/net v0.11.0 + golang.org/x/oauth2 v0.9.0 + golang.org/x/sync v0.3.0 + golang.org/x/time v0.3.0 + google.golang.org/api v0.128.0 + gopkg.in/ns1/ns1-go.v2 v2.7.6 gopkg.in/yaml.v2 v2.4.0 - istio.io/api v0.0.0-20210128181506-0c4b8e54850f - istio.io/client-go v0.0.0-20210128182905-ee2edd059e02 - k8s.io/api v0.26.0 - k8s.io/apimachinery v0.26.0 - k8s.io/client-go v0.26.0 - sigs.k8s.io/gateway-api v0.6.0 + istio.io/api v0.0.0-20230524015941-fa6c5f7916bf + istio.io/client-go v1.18.0 + k8s.io/api v0.27.3 + k8s.io/apimachinery v0.27.3 + k8s.io/client-go v0.27.3 + sigs.k8s.io/gateway-api v0.7.1 ) require ( - cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute v1.19.3 // indirect code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect @@ -84,67 +85,68 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/Masterminds/semver v1.4.2 // indirect github.com/Yamashou/gqlgenc v0.11.0 // indirect - github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect - github.com/alecthomas/colour v0.1.0 // indirect - github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/ans-group/go-durationstring v1.2.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect - github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/deepmap/oapi-codegen v1.9.1 // indirect - github.com/emicklei/go-restful/v3 v3.9.0 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/emicklei/go-restful/v3 v3.10.2 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/fatih/structs v1.1.0 // indirect + github.com/frankban/quicktest v1.14.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.2.3 // indirect - github.com/go-openapi/errors v0.19.8 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/strfmt v0.20.2 // indirect - github.com/go-openapi/swag v0.19.14 // indirect - github.com/go-playground/locales v0.14.0 // indirect - 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/go-logr/logr v1.2.4 // indirect + github.com/go-openapi/errors v0.20.3 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.1 // indirect + github.com/go-openapi/strfmt v0.21.5 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.13.0 // indirect + github.com/go-resty/resty/v2 v2.7.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 + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic v0.6.9 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/s2a-go v0.1.4 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.7.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect + github.com/googleapis/gax-go/v2 v2.10.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.7.2 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/imdario/mergo v0.3.13 // indirect + github.com/hashicorp/go-retryablehttp v0.7.3 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.15 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect - github.com/jcmturner/gofork v1.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/goidentity/v6 v6.0.1 // indirect - github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.3 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jinzhu/copier v0.3.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.2.1 // indirect - github.com/mailru/easyjson v0.7.6 // indirect + github.com/leodido/go-urn v1.2.3 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -156,52 +158,56 @@ require ( github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/peterhellberg/link v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.43.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect 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/afero v1.9.3 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.15.0 // indirect github.com/stretchr/objx v0.5.0 // indirect + github.com/subosito/gotenv v1.4.2 // 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.8 // indirect - go.mongodb.org/mongo-driver v1.5.1 // indirect + github.com/vektah/gqlparser/v2 v2.5.1 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect + go.mongodb.org/mongo-driver v1.11.3 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - 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.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 - golang.org/x/tools v0.3.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/term v0.9.0 // indirect + golang.org/x/text v0.10.0 // indirect + golang.org/x/tools v0.8.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect - google.golang.org/grpc v1.53.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/grpc v1.55.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/go-playground/validator.v9 v9.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.66.6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/resty.v1 v1.12.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a // indirect - k8s.io/klog/v2 v2.80.1 // indirect - k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect - k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect + k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect + k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect moul.io/http2curl v1.0.0 // indirect - sigs.k8s.io/controller-runtime v0.12.1 // indirect - sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/controller-runtime v0.14.6 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) - -replace k8s.io/klog/v2 => github.com/Raffo/knolog v0.0.0-20211016155154-e4d5e0cc970a diff --git a/go.sum b/go.sum index 69d3d71f0e..ca621a003c 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -14,21 +15,21 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds= +cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -38,41 +39,36 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk= code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= git.lukeshu.com/go/libsystemd v0.5.3/go.mod h1:FfDoP0i92r4p5Vn4NCLxvjkd7rCOe6otPa4L6hZg9WM= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v66.0.0+incompatible h1:bmmC38SlE8/E81nNADlgmVGurPWMHDX2YNXVQMrBpEE= -github.com/Azure/azure-sdk-for-go v66.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM= -github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= +github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= +github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk= -github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= +github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= @@ -81,15 +77,14 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/F5Networks/f5-ipam-controller v0.1.6-0.20211217134627-c2be8b459270/go.mod h1:XBOjYUVRKG8q8atIpNmil/XF6RAGVekqfbeNQludcV4= -github.com/F5Networks/k8s-bigip-ctlr/v2 v2.11.1 h1:/ZCtF9JC9VyTN2bdwtHnlpCoJOWxZON5Sh7QDtFlB1E= -github.com/F5Networks/k8s-bigip-ctlr/v2 v2.11.1/go.mod h1:vdlVHq8oUD8W8yUr53RSZ9hKkOcboFcgvhS6nMKH+qc= -github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.0.0 h1:2gzVSELk4I4ncZNrsaKI6fvZ3to60iYnig+lTFcGCEM= -github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.0.0/go.mod h1:P9YNyJaJazc49fLNFG4uQ61VZVptykWqNU2vWLWcxu0= -github.com/IBM/go-sdk-core/v5 v5.8.0 h1:Bn9BxTaKYKWpd+BDpVsL6XOOJl4QDgxux4gSdWi31vE= -github.com/IBM/go-sdk-core/v5 v5.8.0/go.mod h1:+YbdhrjCHC84ls4MeBp+Hj4NZCni+tDAc0XQUqRO9Jc= -github.com/IBM/networking-go-sdk v0.36.0 h1:ADntTsRM8DMZOxS9TYGTAL6i0zw9V2L7OeLFd9Czntk= -github.com/IBM/networking-go-sdk v0.36.0/go.mod h1:tDJtlySQC/txyejU9KeQ27Amc6xKH0MwHFE/B2+Sn5w= +github.com/F5Networks/k8s-bigip-ctlr/v2 v2.13.0 h1:oHBU4lCjA3SeuF7Q2uZ/YH169N3f+M7fgg2lJ88R6dk= +github.com/F5Networks/k8s-bigip-ctlr/v2 v2.13.0/go.mod h1:GJ5fTJ9GuGe2CzEYd8hk/KinNXDNJ0QYqWluiPdLn/s= +github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.1.0 h1:m2VQ7wYE8k3ZuV0iIuye5QIK/t6QLZa4BqOx+CteBTo= +github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.1.0/go.mod h1:/xwZEX9hm7/YFFEEiFa0Bes2YP5OWmXvgf9D/0o9jic= +github.com/IBM/go-sdk-core/v5 v5.13.4 h1:kJvBNQOwhFRkXCPapjNvKVC7n7n2vd1Nr6uUtDZGcfo= +github.com/IBM/go-sdk-core/v5 v5.13.4/go.mod h1:gKRSB+YyKsGlRQW7v5frlLbue5afulSvrRa4O26o4MM= +github.com/IBM/networking-go-sdk v0.42.0 h1:tgUwkAdKRnbnovAUxRkHHFajnnjVrhbTXv5YLjRHXAo= +github.com/IBM/networking-go-sdk v0.42.0/go.mod h1:lTUZwtUkMANMnrLHFIgRhHrkBfwASY/Iho1fabaPHxo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -104,20 +99,17 @@ github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHS github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Raffo/knolog v0.0.0-20211016155154-e4d5e0cc970a h1:4D87FDUGSrfLp/NJmYxybiC8+OR8s5eULyNgmmxAI9U= -github.com/Raffo/knolog v0.0.0-20211016155154-e4d5e0cc970a/go.mod h1:AsBYmtPY5rgsIyjQZDLO+Dwdx91Jmi6uxu7LO6Xf9iw= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/StackExchange/dnscontrol/v3 v3.27.1 h1:7kU3X/prVVnMA7e7AOu7dpwo70WnNA3mJ5FdPoZTz8A= -github.com/StackExchange/dnscontrol/v3 v3.27.1/go.mod h1:ZpOpcdK5F2vLFL+SbbDXXksewqlB/WIsSfytOCUD64U= +github.com/StackExchange/dnscontrol/v3 v3.31.6 h1:QIfe5mN+PveeW0DeE+M7wvWkYpeTtT3IKJdnUb94qps= +github.com/StackExchange/dnscontrol/v3 v3.31.6/go.mod h1:ySJb55bSINayq5h9sK/BePPp52yLAXXCisd0DTs9Ies= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/Yamashou/gqlgenc v0.11.0 h1:y6I7CDrUdY4JBxfwss9168HTP5k/CdExLV5+YPG/3nY= github.com/Yamashou/gqlgenc v0.11.0/go.mod h1:OeQhghEgvGWvRwzx9XjMeg3FUQOHnTo5/12iuJSJxLg= @@ -125,25 +117,19 @@ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= -github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= -github.com/alecthomas/kingpin v2.2.5+incompatible h1:umWl1NNd72+ZvRti3T9C0SYean2hPZ7ZhxU8bsgc9BQ= -github.com/alecthomas/kingpin v2.2.5+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= -github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c h1:MVVbswUlqicyj8P/JljoocA7AyCo62gzD0O7jfvrhtE= -github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI= +github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4= github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro= -github.com/aliyun/alibaba-cloud-sdk-go v1.62.4 h1:PvdgtaX+aDehbXZvxgQDRRCPGsPnC1LNAXi84Iusykc= -github.com/aliyun/alibaba-cloud-sdk-go v1.62.4/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.380 h1:ycRtdJGvv88OHxj5Pj9U9zHSo8rSFwGx99ba2N2/VOY= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.380/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= @@ -151,8 +137,9 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U= github.com/ans-group/go-durationstring v1.2.0 h1:UJIuQATkp0t1rBvZsHRwki33YHV9E+Ulro+3NbMB7MM= 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/ans-group/sdk-go v1.16.5 h1:FoBcdhM209iS2EfjVfq98GrDJNlJRu2mygZ0Xi8Mb3Q= +github.com/ans-group/sdk-go v1.16.5/go.mod h1:p1vrXBxHPvMOGlS4sFUSgeLeKAl9vIe/lJ6UaExe49A= +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= @@ -165,17 +152,16 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= -github.com/aws/aws-sdk-go v1.44.136 h1:J1KJJssa8pjU8jETYUxwRS37KTcxjACfKd9GK8t+5ZU= -github.com/aws/aws-sdk-go v1.44.136/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.285 h1:rgoWYl+NdmKzRgoi/fZLEtGXOjCkcWIa5jPH02Uahdo= +github.com/aws/aws-sdk-go v1.44.285/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -183,14 +169,13 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bodgit/tsig v1.2.0 h1:wNfc7yTk2OuWh/s7nEFa9h+SkIfTn7e4xlFtf1Sgvr4= -github.com/bodgit/tsig v1.2.0/go.mod h1:bsN2ntwGE/s3EeoawjAoKUcAfO4Fr0nGKC72vNF/cqM= +github.com/bodgit/tsig v1.2.2 h1:RgxTCr8UFUHyU4D8Ygb2UtXtS4niw4B6XYYBpgCjl0k= +github.com/bodgit/tsig v1.2.2/go.mod h1:rIGNOLZOV/UA03fmCUtEFbpWOrIoaOuETkpaeTvnLF4= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= @@ -199,7 +184,6 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= @@ -210,13 +194,20 @@ github.com/civo/civogo v0.3.14 h1:W+o+hFXtEhWyJOmZOm2C5s8OEorSXGP6eyPYOa69NA8= github.com/civo/civogo v0.3.14/go.mod h1:SbS06e0JPgIF27r1sLC97gjU1xWmONQeHgzF1hfLpak= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cloudflare-go v0.58.1 h1:+Tqt4N9nuNEMgSC3tCQOixyifU5jihaq+JfDQidTSgY= -github.com/cloudflare/cloudflare-go v0.58.1/go.mod h1:QaA8x4JI0/gA/tni1nTdyimFuyEGJi8cB7YSGoFhXFo= +github.com/cloudflare/cloudflare-go v0.69.0 h1:DHRjCQyM0p4qd3e0t3wnUNtRso1BQPUe4BtMAxZjKqY= +github.com/cloudflare/cloudflare-go v0.69.0/go.mod h1:A6gZktcMokwEgzoAP4BbVDoUU7VorNFAU7FzfIejVF8= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14= github.com/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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/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= @@ -234,7 +225,6 @@ github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDG github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -242,8 +232,8 @@ github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmf github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -252,8 +242,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/datawire/ambassador v1.6.0 h1:4KduhY/wqtv0jK8sMVQNtENHy9fmoXugsuFp/UrM0Ts= github.com/datawire/ambassador v1.6.0/go.mod h1:mV5EhoG/NnHBsffmLnjrq+x4ZNkYDWFZXW9R+AueUiE= @@ -262,10 +250,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= -github.com/deepmap/oapi-codegen v1.9.1 h1:yHmEnA7jSTUMQgV+uN02WpZtwHnz2CBW3mZRIxr1vtI= -github.com/deepmap/oapi-codegen v1.9.1/go.mod h1:PLqNAhdedP8ttRpBBkzLKU3bp+Fpy+tTgeAMlztR2cw= github.com/deislabs/oras v0.8.1/go.mod h1:Mx0rMSbBNaNfY9hjpccEnxkOqJL6KGjtxNHPLC4G4As= github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4= @@ -273,12 +257,12 @@ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8l github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/digitalocean/godo v1.97.0 h1:p9w1yCcWMZcxFSLPToNGXA96WfUVLXqoHti6GzVomL4= -github.com/digitalocean/godo v1.97.0/go.mod h1:NRpFznZFvhHjBoqZAaOD3khVzsJ3EibzKqFL4R60dmA= -github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= +github.com/digitalocean/godo v1.99.0 h1:gUHO7n9bDaZFWvbzOum4bXE0/09ZuYA9yA8idQHX57E= +github.com/digitalocean/godo v1.99.0/go.mod h1:SsS2oXo2rznfM/nORlZ/6JaUJZFhmKTib1YhopUc8NA= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/dnsimple/dnsimple-go v1.0.1 h1:ueDji5xvz6+hLA4JPNvOXGYGHelex0TT7uXdN5ZEWnQ= -github.com/dnsimple/dnsimple-go v1.0.1/go.mod h1:iw/53UDs5RV4ptHVyNrBBr7GHKnndETsP0J/n/JVnA4= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnsimple/dnsimple-go v1.2.0 h1:ddTGyLVKly5HKb5L65AkLqFqwZlWo3WnR0BlFZlIddM= +github.com/dnsimple/dnsimple-go v1.2.0/go.mod h1:z/cs26v/eiRvUyXsHQBLd8lWF8+cD6GbmkPH84plM4U= github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= @@ -304,48 +288,47 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE= +github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815/go.mod h1:wYFFK4LYXbX7j+76mOq7aiC/EAw2S22CrzPHqgsisPw= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 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.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +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.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +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= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/exoscale/egoscale v0.97.0 h1:9DRSdFxepQrm/BOX/tvMXmfeN7d1row9N/+D9wrFp8E= -github.com/exoscale/egoscale v0.97.0/go.mod h1:BAb9p4rmyU+Wl400CJZO5270H2sXtdsZjLcm5xMKkz4= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exoscale/egoscale v1.19.0 h1:DbSyyzaPyxDBgGOuduCU26rQ4PJb/GP+8srXRabRf5U= +github.com/exoscale/egoscale v1.19.0/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= -github.com/f5devcentral/go-bigip/f5teem v0.0.0-20210918163638-28fdd0579913/go.mod h1:r7o5I22EvO+fps2u10bz4ZUlTlNHopQSWzVcW19hK3U= -github.com/f5devcentral/mockhttpclient v0.0.0-20210630101009-cc12e8b81051/go.mod h1:g2/ykgb7Fzf6ag/pYv0LfcwSH8z46TnjFOF3rWyh01I= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 h1:jmwW6QWvUO2OPe22YfgFvBaaZlSr8Rlrac5lZvG6IdM= github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99/go.mod h1:4mP9w9+vYGw2jUx2+2v03IA+phyQQjNRR4AL3uxlNrs= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-gandi/go-gandi v0.6.0 h1:RgFoevggRRp7hF9XsOmWmtwbUg2axhe2ygEdd6Mtstc= github.com/go-gandi/go-gandi v0.6.0/go.mod h1:9NoYyfWCjFosClPiWjkbbRK5UViaZ4ctpT8/pKSSFlw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -355,18 +338,14 @@ github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3I github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= @@ -378,22 +357,23 @@ github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2 github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/errors v0.19.8 h1:doM+tQdZbUm9gydV9yR+iQNmztbjj7I3sW4sIcAwIzc= -github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= +github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= @@ -407,79 +387,51 @@ github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsd github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= -github.com/go-openapi/strfmt v0.20.2 h1:6XZL+fF4VZYFxKQGLAUB358hOrRh/wS51uWEtlONADE= -github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk= +github.com/go-openapi/strfmt v0.21.5 h1:Z/algjpXIZpbvdN+6KbVTkpO75RuedMrqpn1GN529h4= +github.com/go-openapi/strfmt v0.21.5/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY= -github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ= +github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= -github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= -github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= -github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= -github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= -github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= -github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= -github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= -github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= -github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= -github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= -github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= -github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= -github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= -github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= -github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= -github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= -github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= -github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= -github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= -github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= 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 v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 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= @@ -487,16 +439,13 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= -github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -531,18 +480,18 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= +github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -565,6 +514,7 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -572,28 +522,34 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.4 h1:uGy6JWR/uMIILU8wbf+OkstIrNiMjGpEIyhx8f6W7s4= +github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.10.0 h1:ebSgKfMxynOdxw8QQuFOKMgomqeLGPqNLQox2bo42zg= +github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/color v1.2.3/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= -github.com/gophercloud/gophercloud v0.25.0 h1:C3Oae7y0fUVQGSsBrb3zliAjdX+riCSEh4lNMejFNI4= -github.com/gophercloud/gophercloud v0.25.0/go.mod h1:Q8fZtyi5zZxPS/j9aj3sSxtvj41AdQMDwyo1myduD5c= +github.com/gophercloud/gophercloud v1.4.0 h1:RqEu43vaX0lb0LanZr5BylK5ICVxjpFFoc0sxivyuHU= +github.com/gophercloud/gophercloud v1.4.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= @@ -611,7 +567,6 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -619,11 +574,10 @@ 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= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -640,22 +594,22 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1: github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= -github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.3 h1:5n2R5B7+2YWrtEzIWO2jPPQWseAPLkgIuvUgS1l97KY= +github.com/hashicorp/go-retryablehttp v0.7.3/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -672,26 +626,25 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI= -github.com/infobloxopen/infoblox-go-client/v2 v2.1.2-0.20220407114022-6f4c71443168 h1:EXKtVoP/44ckXpw3v2/vrtMEdKx/PA+YBl+REoV27XQ= -github.com/infobloxopen/infoblox-go-client/v2 v2.1.2-0.20220407114022-6f4c71443168/go.mod h1:+lznx4ASBSUZ2i6qwlgyn0v3eKDxBHNU5aRJzghAFbw= +github.com/infobloxopen/infoblox-go-client/v2 v2.3.0 h1:xLHcstumFvziz8dhrc+8gNRwfAyM2Q0nLSlyfK0+3XU= +github.com/infobloxopen/infoblox-go-client/v2 v2.3.0/go.mod h1:ZR191VH7ccpUZ+dzjALQorTevLPv+1P9N62TGJ87ib0= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= -github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= -github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/gokrb5/v8 v8.4.3 h1:iTonLeSJOn7MVUtyMT+arAn5AKAPrkilzhGw8wE/Tq8= +github.com/jcmturner/gokrb5/v8 v8.4.3/go.mod h1:dqRwJGXznQrzw6cWmyo6kH+E7jksEQG/CyVWsJEsJO0= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -707,14 +660,12 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -723,10 +674,7 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= -github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -734,37 +682,28 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A= -github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= -github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= -github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM= -github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= -github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= -github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= +github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -774,38 +713,33 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linki/instrumented_http v0.3.0 h1:dsN92+mXpfZtjJraartcQ99jnuw7fqsnPDjr85ma2dA= github.com/linki/instrumented_http v0.3.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk= -github.com/linode/linodego v1.9.1 h1:29UpEPpYcGFnbwiJW8mbk/bjBZpgd/pv68io2IKTo34= -github.com/linode/linodego v1.9.1/go.mod h1:h6AuFR/JpqwwM/vkj7s8KV3iGN8/jxn+zc437F8SZ8w= +github.com/linode/linodego v1.17.0 h1:aWS98f0jUoY2lhsEuBxRdVkqyGM0nazPd68AEDF0EvU= +github.com/linode/linodego v1.17.0/go.mod h1:/omzPxie0/YI6S0sTw1q47qDt5IYSlbO/infRR4UG+A= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lyft/protoc-gen-star v0.4.10/go.mod h1:mE8fbna26u7aEA2QCVvvfBU/ZrPgocG1206xAFPcs94= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI= -github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -814,20 +748,17 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM= -github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= -github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxatome/go-testdeep v1.13.0 h1:EBmRelH7MhMfPvA+0kXAeOeJUXn3mzul5NmvjLDcQZI= +github.com/maxatome/go-testdeep v1.13.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.6/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo= -github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= @@ -847,9 +778,6 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/protoc-gen-go-json v0.0.0-20190813154521-ece073100ced/go.mod h1:VhkV06JZBMrulb3fNiUvM5Lmz3yc7ei3omzocl9UtCw= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= -github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -863,7 +791,6 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= @@ -896,26 +823,20 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.5.1 h1:auzK7OI497k6x4OvWq+TKAcpcSAlod0doAH72oIN0Jw= +github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= -github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= -github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -927,12 +848,10 @@ github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5X github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= -github.com/openshift/api v0.0.0-20210105115604-44119421ec6b/go.mod h1:aqU5Cq+kqKKPbDMqxo9FojgDeSpNJI7iuskjXjtojDg= -github.com/openshift/api v0.0.0-20210315202829-4b79815405ec h1:Uoyk1PiylPs99uuDpUiYyqjbk6BkZRj/oCJeQ2f3WSE= -github.com/openshift/api v0.0.0-20210315202829-4b79815405ec/go.mod h1:aqU5Cq+kqKKPbDMqxo9FojgDeSpNJI7iuskjXjtojDg= -github.com/openshift/build-machinery-go v0.0.0-20200917070002-f171684f77ab/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE= -github.com/openshift/client-go v0.0.0-20210112165513-ebc401615f47 h1:+TEY29DK0XhqB7HFC9OfV8qf3wffSyi7MWv3AP28DGQ= -github.com/openshift/client-go v0.0.0-20210112165513-ebc401615f47/go.mod h1:u7NRAjtYVAKokiI9LouzTv4mhds8P4S1TwdVAfbjKSk= +github.com/openshift/api v0.0.0-20230607130528-611114dca681 h1:kSvo4fjZyYRu7z7PVkZlqcYhoS4mZHVFYVUkG3WkvIE= +github.com/openshift/api v0.0.0-20230607130528-611114dca681/go.mod h1:4VWG+W22wrB4HfBL88P40DxLEpSOaiBVxUnfalfJo9k= +github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3 h1:uVCq/Sx2y4UZh+qCsCL1BBUJpc3DULHkN4j7XHHgHtw= +github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3/go.mod h1:M+VUIcqx5IvgzejcbgmQnxETPrXRYlcufHpw2bAgz9Y= github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b h1:it0YPE/evO6/m8t8wxis9KFI2F/aleOKsI6d9uz0cEk= github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b/go.mod h1:tNrEB5k8SI+g5kOlsCmL2ELASfpqEofI0+FLBgBdN08= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= @@ -945,10 +864,10 @@ 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/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/oracle/oci-go-sdk/v65 v65.41.0 h1:BTGVqeTo7gCp3ebVYxlm6ezhSLKl58E9hm3eTGN6M8M= +github.com/oracle/oci-go-sdk/v65 v65.41.0/go.mod h1:MXMLMzHnnd9wlpgadPkdlkZ9YrwQmCOmbX5kjVEJodw= +github.com/ovh/go-ovh v1.4.1 h1:VBGa5wMyQtTP7Zb+w97zRCh9sLtM/2YKRyy+MEJmWaM= +github.com/ovh/go-ovh v1.4.1/go.mod h1:6bL6pPyUT7tBfI0pqOegJgRjgjuO+mOo+MyXd1EEC0M= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= @@ -957,7 +876,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterhellberg/link v1.1.0 h1:s2+RH8EGuI/mI4QwrWGSYQCRz7uNgip9BaM04HKu5kc= @@ -973,14 +893,15 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pluralsh/gqlclient v1.1.6 h1:lj6KVfUET9DPyB95+T+hXtCGqqOm+TXOB3jYzuBX1Zc= -github.com/pluralsh/gqlclient v1.1.6/go.mod h1:qcE4KD7hBGl/JFCoXXy8zgUP0mWHJsAM6bhDAf592AE= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pluralsh/gqlclient v1.3.17 h1:hD/rG+lhxP3kN1UUXrzZd2uN7P76MvNTEJEzYOpERXo= +github.com/pluralsh/gqlclient v1.3.17/go.mod h1:z1qHnvPeqIN/a+5OzFs40e6HI6tDxzh1+yJuEpvqGy4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/projectcontour/contour v1.23.2 h1:L7t1ghQJLjN3SaqJ/iOtp3VRpTebm0Y4XgQoS9ZUArc= -github.com/projectcontour/contour v1.23.2/go.mod h1:+ngF5UpPvS+ZpioG9XBY0hvtst47CjAWhOcLyEcXHHA= +github.com/projectcontour/contour v1.25.0 h1:Vdq//JTZfo52cxCddKVLCC6eWANrcHSg/U1HBRuUFro= +github.com/projectcontour/contour v1.25.0/go.mod h1:Nehnwg9lPUPqSUQEP1HwQt+HikkvTmZMs6AQ0mxHT+I= github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -988,11 +909,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= 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.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= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -1000,8 +918,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -1009,11 +927,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.43.0 h1:iq+BVjvYLei5f27wiuNiB1DN6DYQkp1c8Bx0Vykh5us= +github.com/prometheus/common v0.43.0/go.mod h1:NCvr5cQIh3Y/gy73/RdVtC9r8xxrxwJnB+2lB3BxrFc= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -1022,25 +937,20 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 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= github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3/go.mod h1:rtQlpHw+eR6UrqaS3kX1VYeaCxzCVdimDS7g5Ln4pPc= github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -1048,8 +958,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f h1:WSnaD0/cvbKJgSTYbjAPf4RJXVvNNDAwVm+W8wEmnGE= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 h1:1WuWJu7/e8SqK+uQl7lfk/N/oMZTL2NE/TJsNKRNMc4= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c= github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -1059,25 +969,20 @@ github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= -github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= -github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= github.com/smartystreets/gunit v1.3.4 h1:iHc8Rfhb/uCOc9a3KGuD3ut22L+hLIVaqR1o5fS6zC4= 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= @@ -1087,14 +992,19 @@ github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJ 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= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -1103,7 +1013,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -1123,55 +1034,49 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.599 h1:9rMFA8++HynZHYz32gAluJ2ONtz7NjhlBaiomVHWwdw= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.599/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.344 h1:pdwJ6T3iEjP5nB9Mgi4y/OBO8XNtkGN2/+mjGZ8yCbw= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.344/go.mod h1:CuOaLxOQr477GhMWAQPYQFUJrsZbW+ZqkAgP2uHDZXg= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.599 h1:VQqI9ln8dsCDCOleLvm8bR1/g8q8x2QIiMyzRLRrwWg= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.599/go.mod h1:GexzvtWfywrNN4h0+enS8iZBu2OeL6ArsSdcG1ScrwI= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.684 h1:xnx5G6+hwH/tuVDbi4nWTykMXrU5CopIf/uKESAPiO4= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.684/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.684 h1:0W0kMFSMH1wmHDH2u8E7FCMx9KcNpeqz/CKxvEw0+hU= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.684/go.mod h1:coy6xxRqMqBCeCIvJJykUsVh0MdRP9FhL4tW4FhSEq8= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.684 h1:Ed7JypbR/k9DeV6L4xjcX7LUa8vZlA91G6Tp1eDSe3s= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.684/go.mod h1:+okbYzwXLiIIooY7g5VdAqg3kdjhEcb8XS7QJ7qxK1E= github.com/terra-farm/udnssdk v1.3.5 h1:MNR3adfuuEK/l04+jzo8WW/0fnorY+nW515qb3vEr6I= github.com/terra-farm/udnssdk v1.3.5/go.mod h1:8RnM56yZTR7mYyUIvrDgXzdRaEyFIzqdEi7+um26Sv8= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/transip/gotransip/v6 v6.19.0 h1:L8MLCvebIst+NHRYrezupRda9NfyoINbY81CMlaWUGM= -github.com/transip/gotransip/v6 v6.19.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c= +github.com/transip/gotransip/v6 v6.20.0 h1:AuvwyOZ51f2brzMbTqlRy/wmaM3kF7Vx5Wds8xcDflY= +github.com/transip/gotransip/v6 v6.20.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 h1:n7unetnX8WWTc0U85h/0+dJoLWLqoaJwowXB9RkBdxU= -github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60/go.mod h1:43vmy6GEvRuVMpGEWfJ/JoEM6RIqUQI1/tb8JqZR1zI= +github.com/ultradns/ultradns-sdk-go v1.3.7 h1:P4CaM+npeXIybbLL27ezR316NnyILI1Y8IvfZtNE+Co= +github.com/ultradns/ultradns-sdk-go v1.3.7/go.mod h1:43vmy6GEvRuVMpGEWfJ/JoEM6RIqUQI1/tb8JqZR1zI= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= -github.com/vektah/gqlparser/v2 v2.5.0 h1:GwEwy7AJsqPWrey0bHnn+3JLaHLZVT66wY/+O+Tf9SU= -github.com/vektah/gqlparser/v2 v2.5.0/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= -github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556 h1:UbVjBjgJUYGD8MlobEdOR+yTeNqaNa2Gf1/nskVNCSE= -github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg= +github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4= +github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= +github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= +github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= @@ -1193,20 +1098,19 @@ github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 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.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.etcd.io/etcd/api/v3 v3.5.9 h1:4wSsluwyTbGGmyjJktOf3wFQoTBIURXHnq9n/G/JQHs= +go.etcd.io/etcd/api/v3 v3.5.9/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= +go.etcd.io/etcd/client/pkg/v3 v3.5.9 h1:oidDC4+YEuSIQbsR94rY9gur91UPL6DnxDCIYd2IGsE= +go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= +go.etcd.io/etcd/client/v3 v3.5.9 h1:r5xghnU7CwbUxD/fbUtRyJGaYNfDun8sp/gTr1hew6E= +go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA= 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= -go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI= -go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y= +go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1214,27 +1118,28 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 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.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 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= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= 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.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1243,7 +1148,6 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1253,22 +1157,19 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1291,6 +1192,7 @@ 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-20201208152925-83fdc39ff7b5/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= @@ -1299,12 +1201,12 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1348,38 +1250,40 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= +golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1388,8 +1292,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180425194835-bb9c189858d9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1406,15 +1310,12 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1426,14 +1327,12 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1445,49 +1344,39 @@ golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -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-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1498,18 +1387,17 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1522,18 +1410,13 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1542,7 +1425,6 @@ golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1563,25 +1445,27 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/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= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1605,8 +1489,11 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.128.0 h1:RjPESny5CnQRn9V6siglged+DZCgfu9l6mO9dkX9VOg= +google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1641,16 +1528,27 @@ 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= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 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-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1666,13 +1564,19 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 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.31.1/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.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1686,8 +1590,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1703,7 +1607,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= @@ -1711,15 +1614,13 @@ gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdOD gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= -gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/ns1/ns1-go.v2 v2.7.4 h1:uLb8u8uM9E7Xij/GHPctVIKMwyQDcA8NjSPnScG7gmQ= -gopkg.in/ns1/ns1-go.v2 v2.7.4/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk= +gopkg.in/ns1/ns1-go.v2 v2.7.6 h1:mCPl7q0jbIGACXvGBljAuuApmKZo3rRi4tlRIEbMvjA= +gopkg.in/ns1/ns1-go.v2 v2.7.6/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -1729,6 +1630,7 @@ 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= @@ -1740,12 +1642,9 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= helm.sh/helm/v3 v3.2.4/go.mod h1:ZaXz/vzktgwjyGGFbUWtIQkscfE7WYoRGP2szqAFHR0= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1755,86 +1654,60 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -istio.io/api v0.0.0-20210128181506-0c4b8e54850f h1:zUFsawgPj5oI9p5cf91YCExRlxLIVsEkIunN9ODUSJs= -istio.io/api v0.0.0-20210128181506-0c4b8e54850f/go.mod h1:88HN3o1fSD1jo+Z1WTLlJfMm9biopur6Ct9BFKjiB64= -istio.io/client-go v0.0.0-20210128182905-ee2edd059e02 h1:ZA8Y2gKkKtEeYuKfqlEzIBDfU4IE5uIAdsXDeD41T9w= -istio.io/client-go v0.0.0-20210128182905-ee2edd059e02/go.mod h1:oXMjFUWhxlReUSbg4i3GjKgOhSX1WgD68ZNlHQEcmQg= +istio.io/api v0.0.0-20230524015941-fa6c5f7916bf h1:yY8r1TtMBaOTDaF2mEJ1LzXJbHdmN0HLiSL7TScpFEI= +istio.io/api v0.0.0-20230524015941-fa6c5f7916bf/go.mod h1:dDMe1TsOtrRoUlBzdxqNolWXpXPQjLfbcXvqPMtQ6eo= +istio.io/client-go v1.18.0 h1:T89Foio5RbQQakoJdGLk7e8fIvv5xTqLaRpG9Y3lfRE= +istio.io/client-go v1.18.0/go.mod h1:e/55rKUWBUuNc07GSN0DGYl1x0WflOBx+xVw7giWZhU= istio.io/gogo-genproto v0.0.0-20190904133402-ee07f2785480/go.mod h1:uKtbae4K9k2rjjX4ToV0l6etglbc1i7gqQ94XdkshzY= -istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a h1:w7zILua2dnYo9CxImhpNW4NE/8ZxEoc/wfBfHrhUhrE= -istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a/go.mod h1:OzpAts7jljZceG4Vqi5/zXy/pOg1b209T3jb7Nv5wIs= k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8= -k8s.io/api v0.18.1/go.mod h1:3My4jorQWzSs5a+l7Ge6JBbIxChLnY8HnuT58ZWolss= k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= k8s.io/api v0.18.4/go.mod h1:lOIQAKYgai1+vz9J7YcDZwC26Z0zQewYOGWdyIPUUQ4= -k8s.io/api v0.20.0/go.mod h1:HyLC5l5eoS/ygQYl1BXBgFzWNlkHiAuyNAbevIn+FKg= -k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= -k8s.io/api v0.21.2/go.mod h1:Lv6UGJZ1rlMI1qusN8ruAp9PUBFyBwpEHAdG24vIsiU= -k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= -k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= +k8s.io/api v0.27.3 h1:yR6oQXXnUEBWEWcvPWS0jQL575KoAboQPfJAuKNrw5Y= +k8s.io/api v0.27.3/go.mod h1:C4BNvZnQOF7JA/0Xed2S+aUyJSfTGkGFxLXz9MnpIpg= k8s.io/apiextensions-apiserver v0.18.0/go.mod h1:18Cwn1Xws4xnWQNC00FLq1E350b9lUF+aOdIWDOZxgo= k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY= k8s.io/apiextensions-apiserver v0.18.4/go.mod h1:NYeyeYq4SIpFlPxSAB6jHPIdvu3hL0pc36wuRChybio= -k8s.io/apiextensions-apiserver v0.20.4/go.mod h1:Hzebis/9c6Io5yzHp24Vg4XOkTp1ViMwKP/6gmpsfA4= -k8s.io/apiextensions-apiserver v0.21.2/go.mod h1:+Axoz5/l3AYpGLlhJDfcVQzCerVYq3K3CvDMvw6X1RA= k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= -k8s.io/apimachinery v0.18.1/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= k8s.io/apimachinery v0.18.4/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= -k8s.io/apimachinery v0.20.0/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= -k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= -k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= +k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM= +k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= k8s.io/apiserver v0.18.0/go.mod h1:3S2O6FeBBd6XTo0njUrLxiqk8GNy6wWOftjhJcXYnjw= k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw= k8s.io/apiserver v0.18.4/go.mod h1:q+zoFct5ABNnYkGIaGQ3bcbUNdmPyOCoEBcg51LChY8= -k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= -k8s.io/apiserver v0.21.2/go.mod h1:lN4yBoGyiNT7SC1dmNk0ue6a5Wi6O3SWOIw91TsucQw= k8s.io/cli-runtime v0.18.0/go.mod h1:1eXfmBsIJosjn9LjEBUd2WVPoPAY9XGTqTFcPMIBsUQ= k8s.io/cli-runtime v0.18.4/go.mod h1:9/hS/Cuf7NVzWR5F/5tyS6xsnclxoPLVtwhnkJG1Y4g= k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8= -k8s.io/client-go v0.18.1/go.mod h1:iCikYRiXOj/yRRFE/aWqrpPtDt4P2JVWhtHkmESTcfY= k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= k8s.io/client-go v0.18.4/go.mod h1:f5sXwL4yAZRkAtzOxRWUhA/N8XzGCb+nPZI8PfobZ9g= -k8s.io/client-go v0.20.0/go.mod h1:4KWh/g+Ocd8KkCwKF8vUNnmqgv+EVnQDK4MBF4oB5tY= -k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= -k8s.io/client-go v0.21.2/go.mod h1:HdJ9iknWpbl3vMGtib6T2PyI/VYxiZfq936WNVHBRrA= -k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= -k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= +k8s.io/client-go v0.27.3 h1:7dnEGHZEJld3lYwxvLl7WoehK6lAq7GvgjxpA3nv1E8= +k8s.io/client-go v0.27.3/go.mod h1:2MBEKuTo6V1lbKy3z1euEGnhPfGZLKTS9tiJ2xodM48= k8s.io/code-generator v0.18.0/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= k8s.io/code-generator v0.18.4/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= -k8s.io/code-generator v0.20.0/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= -k8s.io/code-generator v0.20.4/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= -k8s.io/code-generator v0.21.2/go.mod h1:8mXJDCB7HcRo1xiEQstcguZkbxZaqeUOrO9SsicWs3U= k8s.io/component-base v0.18.0/go.mod h1:u3BCg0z1uskkzrnAKFzulmYaEpZF7XC9Pf/uFyb1v2c= k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmDfUM= k8s.io/component-base v0.18.4/go.mod h1:7jr/Ef5PGmKwQhyAz/pjByxJbC58mhKAhiaDu0vXfPk= -k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= -k8s.io/component-base v0.21.2/go.mod h1:9lvmIThzdlrJj5Hp8Z/TOgIkdfsNARQ1pT+3PByuiuc= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/helm v2.16.9+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= -k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= -k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= k8s.io/kubectl v0.18.0/go.mod h1:LOkWx9Z5DXMEg5KtOjHhRiC1fqJPLyCr3KtQgEolCkU= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/metrics v0.18.0/go.mod h1:8aYTW18koXqjLVKL7Ds05RPMX9ipJZI3mywYvBOxXd4= k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200603063816-c1c6865ac451/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs= -k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU= +k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= @@ -1842,21 +1715,17 @@ rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gEORz0efEja7A= -sigs.k8s.io/controller-runtime v0.12.1 h1:4BJY01xe9zKQti8oRjj/NeHKRXthf1YkYJAgLONFFoI= -sigs.k8s.io/controller-runtime v0.12.1/go.mod h1:BKhxlA4l7FPK4AQcsuL4X6vZeWnKDXez/vp1Y8dxTU0= +sigs.k8s.io/controller-runtime v0.14.6 h1:oxstGVvXGNnMvY7TAESYk+lzr6S3V5VFxQ6d92KcwQA= +sigs.k8s.io/controller-runtime v0.14.6/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= sigs.k8s.io/controller-tools v0.3.1-0.20200517180335-820a4a27ea84/go.mod h1:enhtKGfxZD1GFEoMgP8Fdbu+uKQ/cq1/WGJhdVChfvI= -sigs.k8s.io/gateway-api v0.6.0 h1:v2FqrN2ROWZLrSnI2o91taHR8Sj3s+Eh3QU7gLNWIqA= -sigs.k8s.io/gateway-api v0.6.0/go.mod h1:EYJT+jlPWTeNskjV0JTki/03WX1cyAnBhwBJfYHpV/0= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/gateway-api v0.7.1 h1:Tts2jeepVkPA5rVG/iO+S43s9n7Vp7jCDhZDQYtPigQ= +sigs.k8s.io/gateway-api v0.7.1/go.mod h1:Xv0+ZMxX0lu1nSSDIIPEfbVztgNZ+3cfiYrJsa2Ooso= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/internal/testutils/endpoint_test.go b/internal/testutils/endpoint_test.go index ffbb54fb2a..efa472f89e 100644 --- a/internal/testutils/endpoint_test.go +++ b/internal/testutils/endpoint_test.go @@ -19,11 +19,12 @@ package testutils import ( "fmt" "sort" + "testing" "sigs.k8s.io/external-dns/endpoint" ) -func ExampleSameEndpoints() { +func TestExampleSameEndpoints(t *testing.T) { eps := []*endpoint.Endpoint{ { DNSName: "example.org", diff --git a/kustomize/kustomization.yaml b/kustomize/kustomization.yaml index f0c516a640..5a6cb4b7e5 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.4 + newTag: v0.13.5 resources: - ./external-dns-deployment.yaml diff --git a/main.go b/main.go index 960045ce98..d83a4e25d6 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,11 @@ import ( "syscall" "time" + awsSDK "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/route53" + sd "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/prometheus/client_golang/prometheus/promhttp" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/labels" @@ -114,6 +119,7 @@ func main() { Namespace: cfg.Namespace, AnnotationFilter: cfg.AnnotationFilter, LabelFilter: labelSelector, + IngressClassNames: cfg.IngressClassNames, FQDNTemplate: cfg.FQDNTemplate, CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation, IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation, @@ -179,6 +185,20 @@ func main() { zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType) zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter) + var awsSession *session.Session + if cfg.Provider == "aws" || cfg.Provider == "aws-sd" || cfg.Registry == "dynamodb" { + awsSession, err = aws.NewSession( + aws.AWSSessionConfig{ + AssumeRole: cfg.AWSAssumeRole, + AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID, + APIRetries: cfg.AWSAPIRetries, + }, + ) + if err != nil { + log.Fatal(err) + } + } + var p provider.Provider switch cfg.Provider { case "akamai": @@ -206,13 +226,11 @@ func main() { BatchChangeSize: cfg.AWSBatchChangeSize, BatchChangeInterval: cfg.AWSBatchChangeInterval, EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth, - AssumeRole: cfg.AWSAssumeRole, - AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID, - APIRetries: cfg.AWSAPIRetries, PreferCNAME: cfg.AWSPreferCNAME, DryRun: cfg.DryRun, ZoneCacheDuration: cfg.AWSZoneCacheDuration, }, + route53.New(awsSession), ) case "aws-sd": // Check that only compatible Registry is used with AWS-SD @@ -220,7 +238,7 @@ func main() { log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry) cfg.Registry = "aws-sd" } - p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.AWSAssumeRoleExternalID, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID) + p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, sd.New(awsSession)) case "azure-dns", "azure": p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) case "azure-private-dns": @@ -379,10 +397,16 @@ func main() { var r registry.Registry switch cfg.Registry { + case "dynamodb": + config := awsSDK.NewConfig() + if cfg.AWSDynamoDBRegion != "" { + config = config.WithRegion(cfg.AWSDynamoDBRegion) + } + r, err = registry.NewDynamoDBRegistry(p, cfg.TXTOwnerID, dynamodb.New(awsSession, config), cfg.AWSDynamoDBTable, cfg.TXTCacheInterval) 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/mkdocs.yml b/mkdocs.yml index c1b9ed6237..4714287211 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,18 @@ repo_url: https://github.com/kubernetes-sigs/external-dns/ nav: - index.md + - About: + - FAQ: faq.md + - Out of Incubator: 20190708-external-dns-incubator.md + - Code of Conduct: code-of-conduct.md + - License: LICENSE.md - Tutorials: tutorials/ + - Annotations: + - About: annotations/annotations.md + - Registries: + - About: registry/registry.md + - TXT: registry/txt.md + - DynamoDB: registry/dynamodb.md - Advanced Topics: - Initial Design: initial-design.md - TTL: ttl.md @@ -14,11 +25,6 @@ nav: - Kubernetes Contributions: CONTRIBUTING.md - Release: release.md - contributing/* - - About: - - FAQ: faq.md - - Out of Incubator: 20190708-external-dns-incubator.md - - Code of Conduct: code-of-conduct.md - - License: LICENSE.md theme: name: material diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index eefd2b2bf0..0b022ef6e5 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -54,6 +54,7 @@ type Config struct { Namespace string AnnotationFilter string LabelFilter string + IngressClassNames []string FQDNTemplate string CombineFQDNAndAnnotation bool IgnoreHostnameAnnotation bool @@ -92,6 +93,8 @@ type Config struct { AWSPreferCNAME bool AWSZoneCacheDuration time.Duration AWSSDServiceCleanup bool + AWSDynamoDBRegion string + AWSDynamoDBTable string AzureConfigFile string AzureResourceGroup string AzureSubscriptionID string @@ -147,6 +150,8 @@ type Config struct { TXTOwnerID string TXTPrefix string TXTSuffix string + TXTEncryptEnabled bool + TXTEncryptAESKey string Interval time.Duration MinEventSyncInterval time.Duration Once bool @@ -216,6 +221,7 @@ var defaultConfig = &Config{ Namespace: "", AnnotationFilter: "", LabelFilter: labels.Everything().String(), + IngressClassNames: nil, FQDNTemplate: "", CombineFQDNAndAnnotation: false, IgnoreHostnameAnnotation: false, @@ -251,6 +257,8 @@ var defaultConfig = &Config{ AWSPreferCNAME: false, AWSZoneCacheDuration: 0 * time.Second, AWSSDServiceCleanup: false, + AWSDynamoDBRegion: "", + AWSDynamoDBTable: "external-dns", AzureConfigFile: "/etc/kubernetes/azure.json", AzureResourceGroup: "", AzureSubscriptionID: "", @@ -295,6 +303,8 @@ var defaultConfig = &Config{ TXTCacheInterval: 0, TXTWildcardReplacement: "", MinEventSyncInterval: 5 * time.Second, + TXTEncryptEnabled: false, + TXTEncryptAESKey: "", Interval: time.Minute, Once: false, DryRun: false, @@ -408,11 +418,12 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion) // Flags related to processing source - app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver") + app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, traefik-proxy)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver", "traefik-proxy") app.Flag("openshift-router-name", "if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record.").StringVar(&cfg.OCPRouterName) 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) @@ -450,12 +461,12 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private") app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private") app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter) - app.Flag("aws-assume-role", "When using the AWS provider, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole) - app.Flag("aws-assume-role-external-id", "When using the AWS provider and assuming a role then specify this external ID` (optional)").Default(defaultConfig.AWSAssumeRoleExternalID).StringVar(&cfg.AWSAssumeRoleExternalID) + app.Flag("aws-assume-role", "When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole) + app.Flag("aws-assume-role-external-id", "When using the AWS API and assuming a role then specify this external ID` (optional)").Default(defaultConfig.AWSAssumeRoleExternalID).StringVar(&cfg.AWSAssumeRoleExternalID) app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize) app.Flag("aws-batch-change-interval", "When using the AWS provider, set the interval between batch changes.").Default(defaultConfig.AWSBatchChangeInterval.String()).DurationVar(&cfg.AWSBatchChangeInterval) app.Flag("aws-evaluate-target-health", "When using the AWS provider, set whether to evaluate the health of a DNS target (default: enabled, disable with --no-aws-evaluate-target-health)").Default(strconv.FormatBool(defaultConfig.AWSEvaluateTargetHealth)).BoolVar(&cfg.AWSEvaluateTargetHealth) - app.Flag("aws-api-retries", "When using the AWS provider, set the maximum number of retries for API calls before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries) + app.Flag("aws-api-retries", "When using the AWS API, set the maximum number of retries before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries) app.Flag("aws-prefer-cname", "When using the AWS provider, prefer using CNAME instead of ALIAS (default: disabled)").BoolVar(&cfg.AWSPreferCNAME) app.Flag("aws-zones-cache-duration", "When using the AWS provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AWSZoneCacheDuration.String()).DurationVar(&cfg.AWSZoneCacheDuration) app.Flag("aws-sd-service-cleanup", "When using the AWS CloudMap provider, delete empty Services without endpoints (default: disabled)").BoolVar(&cfg.AWSSDServiceCleanup) @@ -565,11 +576,15 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("policy", "Modify how DNS records are synchronized between sources and providers (default: sync, options: sync, upsert-only, create-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only", "create-only") // Flags related to the registry - app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "aws-sd") - app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID) + app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, dynamodb, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "dynamodb", "aws-sd") + app.Flag("txt-owner-id", "When using the TXT or DynamoDB registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID) app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). 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) + app.Flag("dynamodb-region", "When using the DynamoDB registry, the AWS region of the DynamoDB table (optional)").Default(cfg.AWSDynamoDBRegion).StringVar(&cfg.AWSDynamoDBRegion) + app.Flag("dynamodb-table", "When using the DynamoDB registry, the name of the DynamoDB table (default: \"external-dns\")").Default(defaultConfig.AWSDynamoDBTable).StringVar(&cfg.AWSDynamoDBTable) // 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 d9c68480c0..c66ba92929 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -65,6 +65,7 @@ var ( AWSPreferCNAME: false, AWSZoneCacheDuration: 0 * time.Second, AWSSDServiceCleanup: false, + AWSDynamoDBTable: "external-dns", AzureConfigFile: "/etc/kubernetes/azure.json", AzureResourceGroup: "", AzureSubscriptionID: "", @@ -170,6 +171,7 @@ var ( AWSPreferCNAME: true, AWSZoneCacheDuration: 10 * time.Second, AWSSDServiceCleanup: true, + AWSDynamoDBTable: "custom-table", AzureConfigFile: "azure.json", AzureResourceGroup: "arg", AzureSubscriptionID: "arg", @@ -351,6 +353,7 @@ func TestParseFlags(t *testing.T) { "--txt-owner-id=owner-1", "--txt-prefix=associated-txt-record", "--txt-cache-interval=12h", + "--dynamodb-table=custom-table", "--interval=10m", "--min-event-sync-interval=50s", "--once", @@ -464,6 +467,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_AWS_PREFER_CNAME": "true", "EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s", "EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true", + "EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table", "EXTERNAL_DNS_POLICY": "upsert-only", "EXTERNAL_DNS_REGISTRY": "noop", "EXTERNAL_DNS_TXT_OWNER_ID": "owner-1", diff --git a/pkg/tlsutils/tlsconfig.go b/pkg/tlsutils/tlsconfig.go index 3a25334971..5275fad135 100644 --- a/pkg/tlsutils/tlsconfig.go +++ b/pkg/tlsutils/tlsconfig.go @@ -51,7 +51,7 @@ func NewTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool, m if certPath != "" { cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { - return nil, fmt.Errorf("could not load TLS cert: %s", err) + return nil, fmt.Errorf("could not load TLS cert: %w", err) } certificates = append(certificates, cert) } @@ -78,11 +78,11 @@ func loadRoots(caPath string) (*x509.CertPool, error) { roots := x509.NewCertPool() pem, err := os.ReadFile(caPath) if err != nil { - return nil, fmt.Errorf("error reading %s: %s", caPath, err) + return nil, fmt.Errorf("error reading %s: %w", caPath, err) } ok := roots.AppendCertsFromPEM(pem) if !ok { - return nil, fmt.Errorf("could not read root certs: %s", err) + return nil, fmt.Errorf("could not read root certs: %w", err) } return roots, nil } diff --git a/plan/plan.go b/plan/plan.go index 1e8a38f1e1..312bb261b1 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -37,8 +37,6 @@ type Plan struct { Current []*endpoint.Endpoint // List of desired records Desired []*endpoint.Endpoint - // List of missing records to be created, use for the migrations (e.g. old-new TXT format) - Missing []*endpoint.Endpoint // Policies under which the desired changes are calculated Policies []Policy // List of changes necessary to move towards desired state @@ -177,11 +175,6 @@ func (p *Plan) Calculate() *Plan { changes = pol.Apply(changes) } - // Handle the migration of the TXT records created before the new format (introduced in v0.12.0) - if len(p.Missing) > 0 { - changes.Create = append(changes.Create, filterRecordsForPlan(p.Missing, p.DomainFilter, append(p.ManagedRecords, endpoint.RecordTypeTXT))...) - } - plan := &Plan{ Current: p.Current, Desired: p.Desired, diff --git a/plan/plan_test.go b/plan/plan_test.go index 2b107d0e89..cc9e56bd5a 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -51,9 +51,6 @@ type PlanTestSuite struct { domainFilterFiltered2 *endpoint.Endpoint domainFilterFiltered3 *endpoint.Endpoint domainFilterExcluded *endpoint.Endpoint - domainFilterFilteredTXT1 *endpoint.Endpoint - domainFilterFilteredTXT2 *endpoint.Endpoint - domainFilterExcludedTXT *endpoint.Endpoint } func (suite *PlanTestSuite) SetupTest() { @@ -233,21 +230,6 @@ func (suite *PlanTestSuite) SetupTest() { Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A", } - suite.domainFilterFilteredTXT1 = &endpoint.Endpoint{ - DNSName: "a-foo.domain.tld", - Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, - RecordType: "TXT", - } - suite.domainFilterFilteredTXT2 = &endpoint.Endpoint{ - DNSName: "cname-bar.domain.tld", - Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, - RecordType: "TXT", - } - suite.domainFilterExcludedTXT = &endpoint.Endpoint{ - DNSName: "cname-bar.otherdomain.tld", - Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, - RecordType: "TXT", - } } func (suite *PlanTestSuite) TestSyncFirstRound() { @@ -661,21 +643,6 @@ func (suite *PlanTestSuite) TestDomainFiltersUpdate() { validateEntries(suite.T(), changes.Delete, expectedDelete) } -func (suite *PlanTestSuite) TestMissing() { - missing := []*endpoint.Endpoint{suite.domainFilterFilteredTXT1, suite.domainFilterFilteredTXT2, suite.domainFilterExcludedTXT} - expectedCreate := []*endpoint.Endpoint{suite.domainFilterFilteredTXT1, suite.domainFilterFilteredTXT2} - - p := &Plan{ - Policies: []Policy{&SyncPolicy{}}, - Missing: missing, - DomainFilter: endpoint.NewDomainFilter([]string{"domain.tld"}), - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, - } - - changes := p.Calculate().Changes - validateEntries(suite.T(), changes.Create, expectedCreate) -} - func (suite *PlanTestSuite) TestAAAARecords() { current := []*endpoint.Endpoint{} diff --git a/provider/akamai/akamai.go b/provider/akamai/akamai.go index 6547edce08..290b423ae6 100644 --- a/provider/akamai/akamai.go +++ b/provider/akamai/akamai.go @@ -199,7 +199,7 @@ func (p AkamaiProvider) fetchZones() (akamaiZones, error) { } for _, zone := range resp.Zones { - if p.domainFilter.Match(zone.Zone) || !p.domainFilter.IsConfigured() { + if p.domainFilter.Match(zone.Zone) { filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractId, Zone: zone.Zone}) log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractId) } @@ -426,7 +426,7 @@ func (p AkamaiProvider) deleteRecordsets(zoneNameIDMapper provider.ZoneIDName, e rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType) if err != nil { if _, ok := err.(*dns.RecordError); !ok { - return fmt.Errorf("endpoint deletion. record validation failed. error: %s", err.Error()) + return fmt.Errorf("endpoint deletion. record validation failed. error: %w", err) } log.Infof("Endpoint deletion. Record doesn't exist. Name: %s, Type: %s", recName, endpoint.RecordType) continue diff --git a/provider/aws/aws.go b/provider/aws/aws.go index 5f7457420e..235841f4a5 100644 --- a/provider/aws/aws.go +++ b/provider/aws/aws.go @@ -25,11 +25,8 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/route53" - "github.com/linki/instrumented_http" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -47,10 +44,12 @@ const ( // As we are using the standard AWS client, this should already be compliant. // Hence, ifever AWS decides to raise this limit, we will automatically reduce the pressure on rate limits route53PageSize = "300" - // provider specific key that designates whether an AWS ALIAS record has the EvaluateTargetHealth - // field set to true. - providerSpecificAlias = "alias" - providerSpecificTargetHostedZone = "aws/target-hosted-zone" + // providerSpecificAlias specifies whether a CNAME endpoint maps to an AWS ALIAS record. + providerSpecificAlias = "alias" + providerSpecificTargetHostedZone = "aws/target-hosted-zone" + // providerSpecificEvaluateTargetHealth specifies whether an AWS ALIAS record + // has the EvaluateTargetHealth field set to true. Present iff the endpoint + // has a `providerSpecificAlias` value of `true`. providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health" providerSpecificWeight = "aws/weight" providerSpecificRegion = "aws/region" @@ -86,6 +85,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 +115,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", @@ -222,49 +223,15 @@ type AWSConfig struct { BatchChangeSize int BatchChangeInterval time.Duration EvaluateTargetHealth bool - AssumeRole string - AssumeRoleExternalID string - APIRetries int PreferCNAME bool DryRun bool ZoneCacheDuration time.Duration } // NewAWSProvider initializes a new AWS Route53 based Provider. -func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) { - config := aws.NewConfig().WithMaxRetries(awsConfig.APIRetries) - - config.WithHTTPClient( - instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{ - PathProcessor: func(path string) string { - parts := strings.Split(path, "/") - return parts[len(parts)-1] - }, - }), - ) - - session, err := session.NewSessionWithOptions(session.Options{ - Config: *config, - SharedConfigState: session.SharedConfigEnable, - }) - if err != nil { - return nil, errors.Wrap(err, "failed to instantiate AWS session") - } - - if awsConfig.AssumeRole != "" { - if awsConfig.AssumeRoleExternalID != "" { - log.Infof("Assuming role: %s with external id %s", awsConfig.AssumeRole, awsConfig.AssumeRoleExternalID) - session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole, func(p *stscreds.AssumeRoleProvider) { - p.ExternalID = &awsConfig.AssumeRoleExternalID - })) - } else { - log.Infof("Assuming role: %s", awsConfig.AssumeRole) - session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole)) - } - } - +func NewAWSProvider(awsConfig AWSConfig, client Route53API) (*AWSProvider, error) { provider := &AWSProvider{ - client: route53.New(session), + client: client, domainFilter: awsConfig.DomainFilter, zoneIDFilter: awsConfig.ZoneIDFilter, zoneTypeFilter: awsConfig.ZoneTypeFilter, @@ -281,13 +248,6 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) { return provider, nil } -func (p *AWSProvider) PropertyValuesEqual(name string, previous string, current string) bool { - if name == "aws/evaluate-target-health" { - return true - } - return p.BaseProvider.PropertyValuesEqual(name, previous, current) -} - // Zones returns the list of hosted zones. func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone, error) { if p.zonesCache.zones != nil && time.Since(p.zonesCache.age) < p.zonesCache.duration { @@ -373,7 +333,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 } @@ -388,7 +348,11 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos targets[idx] = aws.StringValue(rr.Value) } - newEndpoints = append(newEndpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.Type), ttl, targets...)) + ep := endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.Type), ttl, targets...) + if aws.StringValue(r.Type) == endpoint.RecordTypeCNAME { + ep = ep.WithProviderSpecific(providerSpecificAlias, "false") + } + newEndpoints = append(newEndpoints, ep) } if r.AliasTarget != nil { @@ -456,37 +420,6 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos return endpoints, nil } -// CreateRecords creates a given set of DNS records in the given hosted zone. -func (p *AWSProvider) CreateRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error { - return p.doRecords(ctx, route53.ChangeActionCreate, endpoints) -} - -// DeleteRecords deletes a given set of DNS records in a given zone. -func (p *AWSProvider) DeleteRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error { - return p.doRecords(ctx, route53.ChangeActionDelete, endpoints) -} - -func (p *AWSProvider) doRecords(ctx context.Context, action string, endpoints []*endpoint.Endpoint) error { - zones, err := p.Zones(ctx) - if err != nil { - return errors.Wrapf(err, "failed to list zones, aborting %s doRecords action", action) - } - - p.AdjustEndpoints(endpoints) - - return p.submitChanges(ctx, p.newChanges(action, endpoints), zones) -} - -// UpdateRecords updates a given set of old records to a new set of records in a given hosted zone. -func (p *AWSProvider) UpdateRecords(ctx context.Context, updates, current []*endpoint.Endpoint) error { - zones, err := p.Zones(ctx) - if err != nil { - return errors.Wrapf(err, "failed to list zones, aborting UpdateRecords") - } - - return p.submitChanges(ctx, p.createUpdateChanges(updates, current), zones) -} - // Identify if old and new endpoints require DELETE/CREATE instead of UPDATE. func (p *AWSProvider) requiresDeleteCreate(old *endpoint.Endpoint, new *endpoint.Endpoint) bool { // a change of record type @@ -495,8 +428,12 @@ func (p *AWSProvider) requiresDeleteCreate(old *endpoint.Endpoint, new *endpoint } // an ALIAS record change to/from a CNAME - if old.RecordType == endpoint.RecordTypeCNAME && useAlias(old, p.preferCNAME) != useAlias(new, p.preferCNAME) { - return true + if old.RecordType == endpoint.RecordTypeCNAME { + oldAlias, _ := old.GetProviderSpecificProperty(providerSpecificAlias) + newAlias, _ := new.GetProviderSpecificProperty(providerSpecificAlias) + if oldAlias != newAlias { + return true + } } // a set identifier change @@ -696,23 +633,33 @@ func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint) func (p *AWSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { for _, ep := range endpoints { alias := false - if aliasString, ok := ep.GetProviderSpecificProperty(providerSpecificAlias); ok { - alias = aliasString.Value == "true" - } else if useAlias(ep, p.preferCNAME) { - alias = true - log.Debugf("Modifying endpoint: %v, setting %s=true", ep, providerSpecificAlias) - ep.ProviderSpecific = append(ep.ProviderSpecific, endpoint.ProviderSpecificProperty{ - Name: providerSpecificAlias, - Value: "true", - }) + if ep.RecordType != endpoint.RecordTypeCNAME { + ep.DeleteProviderSpecificProperty(providerSpecificAlias) + } else if aliasString, ok := ep.GetProviderSpecificProperty(providerSpecificAlias); ok { + alias = aliasString == "true" + if !alias && aliasString != "false" { + ep.SetProviderSpecificProperty(providerSpecificAlias, "false") + } + } else { + alias = useAlias(ep, p.preferCNAME) + log.Debugf("Modifying endpoint: %v, setting %s=%v", ep, providerSpecificAlias, alias) + ep.SetProviderSpecificProperty(providerSpecificAlias, strconv.FormatBool(alias)) } - if _, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); alias && !ok { - log.Debugf("Modifying endpoint: %v, setting %s=%t", ep, providerSpecificEvaluateTargetHealth, p.evaluateTargetHealth) - ep.ProviderSpecific = append(ep.ProviderSpecific, endpoint.ProviderSpecificProperty{ - Name: providerSpecificEvaluateTargetHealth, - Value: fmt.Sprintf("%t", p.evaluateTargetHealth), - }) + if alias { + if ep.RecordTTL.IsConfigured() { + log.Debugf("Modifying endpoint: %v, setting ttl=%v", ep, recordTTL) + ep.RecordTTL = recordTTL + } + if prop, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok { + if prop != "true" && prop != "false" { + ep.SetProviderSpecificProperty(providerSpecificEvaluateTargetHealth, "false") + } + } else { + ep.SetProviderSpecificProperty(providerSpecificEvaluateTargetHealth, strconv.FormatBool(p.evaluateTargetHealth)) + } + } else { + ep.DeleteProviderSpecificProperty(providerSpecificEvaluateTargetHealth) } } return endpoints @@ -735,7 +682,7 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C if targetHostedZone := isAWSAlias(ep); targetHostedZone != "" { evalTargetHealth := p.evaluateTargetHealth if prop, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok { - evalTargetHealth = prop.Value == "true" + evalTargetHealth = prop == "true" } // If the endpoint has a Dualstack label, append a change for AAAA record as well. if val, ok := ep.Labels[endpoint.DualstackLabelKey]; ok { @@ -766,18 +713,18 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C if setIdentifier != "" { change.ResourceRecordSet.SetIdentifier = aws.String(setIdentifier) if prop, ok := ep.GetProviderSpecificProperty(providerSpecificWeight); ok { - weight, err := strconv.ParseInt(prop.Value, 10, 64) + weight, err := strconv.ParseInt(prop, 10, 64) if err != nil { - log.Errorf("Failed parsing value of %s: %s: %v; using weight of 0", providerSpecificWeight, prop.Value, err) + log.Errorf("Failed parsing value of %s: %s: %v; using weight of 0", providerSpecificWeight, prop, err) weight = 0 } change.ResourceRecordSet.Weight = aws.Int64(weight) } if prop, ok := ep.GetProviderSpecificProperty(providerSpecificRegion); ok { - change.ResourceRecordSet.Region = aws.String(prop.Value) + change.ResourceRecordSet.Region = aws.String(prop) } if prop, ok := ep.GetProviderSpecificProperty(providerSpecificFailover); ok { - change.ResourceRecordSet.Failover = aws.String(prop.Value) + change.ResourceRecordSet.Failover = aws.String(prop) } if _, ok := ep.GetProviderSpecificProperty(providerSpecificMultiValueAnswer); ok { change.ResourceRecordSet.MultiValueAnswer = aws.Bool(true) @@ -786,15 +733,15 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C geolocation := &route53.GeoLocation{} useGeolocation := false if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationContinentCode); ok { - geolocation.ContinentCode = aws.String(prop.Value) + geolocation.ContinentCode = aws.String(prop) useGeolocation = true } else { if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationCountryCode); ok { - geolocation.CountryCode = aws.String(prop.Value) + geolocation.CountryCode = aws.String(prop) useGeolocation = true } if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationSubdivisionCode); ok { - geolocation.SubdivisionCode = aws.String(prop.Value) + geolocation.SubdivisionCode = aws.String(prop) useGeolocation = true } } @@ -804,7 +751,7 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C } if prop, ok := ep.GetProviderSpecificProperty(providerSpecificHealthCheckID); ok { - change.ResourceRecordSet.HealthCheckId = aws.String(prop.Value) + change.ResourceRecordSet.HealthCheckId = aws.String(prop) } if ownedRecord, ok := ep.Labels[endpoint.OwnedRecordLabelKey]; ok { @@ -1018,13 +965,13 @@ func useAlias(ep *endpoint.Endpoint, preferCNAME bool) bool { // isAWSAlias determines if a given endpoint is supposed to create an AWS Alias record // and (if so) returns the target hosted zone ID func isAWSAlias(ep *endpoint.Endpoint) string { - prop, exists := ep.GetProviderSpecificProperty(providerSpecificAlias) - if exists && prop.Value == "true" && ep.RecordType == endpoint.RecordTypeCNAME && len(ep.Targets) > 0 { + isAlias, exists := ep.GetProviderSpecificProperty(providerSpecificAlias) + if exists && isAlias == "true" && ep.RecordType == endpoint.RecordTypeCNAME && len(ep.Targets) > 0 { // alias records can only point to canonical hosted zones (e.g. to ELBs) or other records in the same zone if hostedZoneID, ok := ep.GetProviderSpecificProperty(providerSpecificTargetHostedZone); ok { // existing Endpoint where we got the target hosted zone from the Route53 data - return hostedZoneID.Value + return hostedZoneID } // check if the target is in a canonical hosted zone @@ -1059,3 +1006,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 362ebb989a..cce21e66a8 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,31 +337,164 @@ 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()) require.NoError(t, err) - validateEndpoints(t, records, []*endpoint.Endpoint{ + validateEndpoints(t, provider, records, []*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"), @@ -378,29 +511,30 @@ func TestAWSRecords(t *testing.T) { 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.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.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id").WithProviderSpecific(providerSpecificAlias, "false"), 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"), endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"), - endpoint.NewEndpoint("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true"), + endpoint.NewEndpointWithTTL("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, 60, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"), endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"), endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health } - provider.AdjustEndpoints(records) + records = provider.AdjustEndpoints(records) - validateEndpoints(t, records, []*endpoint.Endpoint{ + validateEndpoints(t, provider, records, []*endpoint.Endpoint{ endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), - endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"), - endpoint.NewEndpoint("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), + endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com").WithProviderSpecific(providerSpecificAlias, "false"), + endpoint.NewEndpointWithTTL("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, 300, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"), endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health @@ -409,41 +543,121 @@ 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"), endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "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.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"), - 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-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), 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) + require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ + Create: records, + })) - 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 +666,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,42 +674,132 @@ 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) + require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ + UpdateOld: currentRecords, + UpdateNew: updatedRecords, + })) - 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{ - 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"), - endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), - 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"), - } - - 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()) + 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, err) + require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ + Delete: []*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"), + endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"), + 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").WithProviderSpecific(providerSpecificAlias, "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"), + }, + })) - 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 +818,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 +952,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 +968,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 +983,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 +992,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 +1012,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 +1234,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 +1244,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 +1253,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 +1262,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 +1276,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 +1401,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 @@ -830,11 +1426,11 @@ func TestAWSsubmitChanges(t *testing.T) { records, err := provider.Records(ctx) require.NoError(t, err) - validateEndpoints(t, records, endpoints) + validateEndpoints(t, provider, records, endpoints) } 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 +1444,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) @@ -1011,8 +1607,11 @@ func TestAWSBatchChangeSetExceedingNameChange(t *testing.T) { require.Equal(t, 0, len(batchCs)) } -func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { +func validateEndpoints(t *testing.T, provider *AWSProvider, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) { assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %+v:%+v", endpoints, expected) + + normalized := provider.AdjustEndpoints(endpoints) + assert.True(t, testutils.SameEndpoints(normalized, expected), "actual and normalized endpoints don't match. %+v:%+v", endpoints, normalized) } func validateAWSZones(t *testing.T, zones map[string]*route53.HostedZone, expected map[string]*route53.HostedZone) { @@ -1043,13 +1642,15 @@ 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}, } - require.NoError(t, provider.CreateRecords(context.Background(), records)) + require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ + Create: records, + })) recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") @@ -1073,7 +1674,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{ @@ -1082,6 +1683,10 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME, ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: providerSpecificAlias, + Value: "true", + }, endpoint.ProviderSpecificProperty{ Name: providerSpecificEvaluateTargetHealth, Value: key, @@ -1093,6 +1698,10 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { Targets: endpoint.Targets{"bar.eu-central-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME, ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: providerSpecificAlias, + Value: "true", + }, endpoint.ProviderSpecificProperty{ Name: providerSpecificEvaluateTargetHealth, Value: key, @@ -1102,7 +1711,9 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { }, } - require.NoError(t, provider.CreateRecords(context.Background(), records)) + require.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{ + Create: records, + })) recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") @@ -1232,51 +1843,6 @@ func TestAWSSuitableZones(t *testing.T) { } } -func TestAWSHealthTargetAnnotation(tt *testing.T) { - comparator := func(name, previous, current string) bool { - return previous == current - } - for _, test := range []struct { - name string - current *endpoint.Endpoint - desired *endpoint.Endpoint - propertyComparator func(name, previous, current string) bool - shouldUpdate bool - }{ - { - name: "skip AWS target health", - current: &endpoint.Endpoint{ - RecordType: "A", - DNSName: "foo.com", - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "aws/evaluate-target-health", Value: "true"}, - }, - }, - desired: &endpoint.Endpoint{ - DNSName: "foo.com", - RecordType: "A", - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "aws/evaluate-target-health", Value: "false"}, - }, - }, - propertyComparator: comparator, - shouldUpdate: false, - }, - } { - tt.Run(test.name, func(t *testing.T) { - provider := &AWSProvider{} - plan := &plan.Plan{ - Current: []*endpoint.Endpoint{test.current}, - Desired: []*endpoint.Endpoint{test.desired}, - PropertyComparator: provider.PropertyValuesEqual, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, - } - plan = plan.Calculate() - assert.Equal(t, test.shouldUpdate, len(plan.Changes.UpdateNew) == 1) - }) - } -} - func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) { params := &route53.CreateHostedZoneInput{ CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"), @@ -1289,22 +1855,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, provider, 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 +1900,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 +1923,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 +1971,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,16 +2026,16 @@ 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") + newRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar").WithProviderSpecific(providerSpecificAlias, "false") assert.False(t, provider.requiresDeleteCreate(oldRecordType, oldRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, oldRecordType) assert.True(t, provider.requiresDeleteCreate(oldRecordType, newRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, newRecordType) - oldCNAMEAlias := endpoint.NewEndpointWithTTL("CNAMEAlias", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar") - newCNAMEAlias := endpoint.NewEndpointWithTTL("CNAMEAlias", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.us-east-1.elb.amazonaws.com") + oldCNAMEAlias := endpoint.NewEndpointWithTTL("CNAMEAlias", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar").WithProviderSpecific(providerSpecificAlias, "false") + newCNAMEAlias := endpoint.NewEndpointWithTTL("CNAMEAlias", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.us-east-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true") assert.False(t, provider.requiresDeleteCreate(oldCNAMEAlias, oldCNAMEAlias), "actual and expected endpoints don't match. %+v:%+v", oldCNAMEAlias, oldCNAMEAlias.DNSName) assert.True(t, provider.requiresDeleteCreate(oldCNAMEAlias, newCNAMEAlias), "actual and expected endpoints don't match. %+v:%+v", oldCNAMEAlias, newCNAMEAlias) diff --git a/provider/aws/session.go b/provider/aws/session.go new file mode 100644 index 0000000000..822ab4aff2 --- /dev/null +++ b/provider/aws/session.go @@ -0,0 +1,75 @@ +/* +Copyright 2023 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 aws + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/linki/instrumented_http" + "github.com/sirupsen/logrus" + + "sigs.k8s.io/external-dns/pkg/apis/externaldns" +) + +// AWSSessionConfig contains configuration to create a new AWS provider. +type AWSSessionConfig struct { + AssumeRole string + AssumeRoleExternalID string + APIRetries int +} + +func NewSession(awsConfig AWSSessionConfig) (*session.Session, error) { + config := aws.NewConfig().WithMaxRetries(awsConfig.APIRetries) + + config.WithHTTPClient( + instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{ + PathProcessor: func(path string) string { + parts := strings.Split(path, "/") + return parts[len(parts)-1] + }, + }), + ) + + session, err := session.NewSessionWithOptions(session.Options{ + Config: *config, + SharedConfigState: session.SharedConfigEnable, + }) + if err != nil { + return nil, fmt.Errorf("instantiating AWS session: %w", err) + } + + if awsConfig.AssumeRole != "" { + if awsConfig.AssumeRoleExternalID != "" { + logrus.Infof("Assuming role: %s with external id %s", awsConfig.AssumeRole, awsConfig.AssumeRoleExternalID) + session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole, func(p *stscreds.AssumeRoleProvider) { + p.ExternalID = &awsConfig.AssumeRoleExternalID + })) + } else { + logrus.Infof("Assuming role: %s", awsConfig.AssumeRole) + session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole)) + } + } + + session.Handlers.Build.PushBack(request.MakeAddToUserAgentHandler("ExternalDNS", externaldns.Version)) + + return session, nil +} diff --git a/provider/awssd/aws_sd.go b/provider/awssd/aws_sd.go index ecdb7efc58..b161622399 100644 --- a/provider/awssd/aws_sd.go +++ b/provider/awssd/aws_sd.go @@ -25,15 +25,10 @@ import ( "strings" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/aws/session" sd "github.com/aws/aws-sdk-go/service/servicediscovery" - "github.com/linki/instrumented_http" log "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/pkg/apis/externaldns" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" ) @@ -86,42 +81,9 @@ type AWSSDProvider struct { } // NewAWSSDProvider initializes a new AWS Cloud Map based Provider. -func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, assumeRole string, assumeRoleExternalID string, dryRun, cleanEmptyService bool, ownerID string) (*AWSSDProvider, error) { - config := aws.NewConfig() - - config = config.WithHTTPClient( - instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{ - PathProcessor: func(path string) string { - parts := strings.Split(path, "/") - return parts[len(parts)-1] - }, - }), - ) - - sess, err := session.NewSessionWithOptions(session.Options{ - Config: *config, - SharedConfigState: session.SharedConfigEnable, - }) - if err != nil { - return nil, err - } - - if assumeRole != "" { - if assumeRoleExternalID != "" { - log.Infof("Assuming role %q with external ID %q", assumeRole, assumeRoleExternalID) - sess.Config.WithCredentials(stscreds.NewCredentials(sess, assumeRole, func(p *stscreds.AssumeRoleProvider) { - p.ExternalID = &assumeRoleExternalID - })) - } else { - log.Infof("Assuming role: %s", assumeRole) - sess.Config.WithCredentials(stscreds.NewCredentials(sess, assumeRole)) - } - } - - sess.Handlers.Build.PushBack(request.MakeAddToUserAgentHandler("ExternalDNS", externaldns.Version)) - +func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, dryRun, cleanEmptyService bool, ownerID string, client AWSSDClient) (*AWSSDProvider, error) { provider := &AWSSDProvider{ - client: sd.New(sess), + client: client, dryRun: dryRun, namespaceFilter: domainFilter, namespaceTypeFilter: newSdNamespaceFilter(namespaceType), @@ -509,7 +471,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 dfab96effa..bca9c841d6 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 374b5e5d6c..ee2e478bd1 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 f357201548..061b0f4d6b 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 0598dd4f3c..ba831586c2 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 0000000000..95fd1a8515 --- /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 0000000000..1009594c26 --- /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/civo/civo.go b/provider/civo/civo.go index bd61bd98e1..3f6dd9e0e2 100644 --- a/provider/civo/civo.go +++ b/provider/civo/civo.go @@ -46,6 +46,11 @@ type CivoChanges struct { Updates []*CivoChangeUpdate } +// Empty returns true if there are no changes +func (c *CivoChanges) Empty() bool { + return len(c.Creates) == 0 && len(c.Updates) == 0 && len(c.Deletes) == 0 +} + // CivoChangeCreate Civo Domain Record Creates type CivoChangeCreate struct { Domain civogo.DNSDomain @@ -169,6 +174,11 @@ func (p *CivoProvider) fetchZones(ctx context.Context) ([]civogo.DNSDomain, erro // submitChanges takes a zone and a collection of Changes and sends them as a single transaction. func (p *CivoProvider) submitChanges(ctx context.Context, changes CivoChanges) error { + if changes.Empty() { + log.Info("All records are already up to date") + return nil + } + for _, change := range changes.Creates { logFields := log.Fields{ "Type": change.Options.Type, diff --git a/provider/civo/civo_test.go b/provider/civo/civo_test.go index 6d637d0774..6a05a7d8fb 100644 --- a/provider/civo/civo_test.go +++ b/provider/civo/civo_test.go @@ -809,6 +809,154 @@ func TestCivo_submitChangesDelete(t *testing.T) { assert.NoError(t, err) } +func TestCivoChangesEmpty(t *testing.T) { + // Test empty CivoChanges + c := &CivoChanges{} + assert.True(t, c.Empty()) + + // Test CivoChanges with Creates + c = &CivoChanges{ + Creates: []*CivoChangeCreate{ + { + Domain: civogo.DNSDomain{ + ID: "12345", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: civogo.DNSRecordTypeA, + Name: "www", + Value: "192.1.1.1", + Priority: 0, + TTL: 600, + }, + }, + }, + } + assert.False(t, c.Empty()) + + // Test CivoChanges with Updates + c = &CivoChanges{ + Updates: []*CivoChangeUpdate{ + { + Domain: civogo.DNSDomain{ + ID: "12345", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", + AccountID: "1", + DNSDomainID: "12345", + Name: "mail", + Value: "192.168.1.2", + Type: "MX", + Priority: 10, + TTL: 600, + }, + Options: civogo.DNSRecordConfig{ + Type: "MX", + Name: "mail", + Value: "192.168.1.3", + Priority: 10, + TTL: 600, + }, + }, + }, + } + assert.False(t, c.Empty()) + + // Test CivoChanges with Deletes + c = &CivoChanges{ + Deletes: []*CivoChangeDelete{ + { + Domain: civogo.DNSDomain{ + ID: "12345", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", + AccountID: "1", + DNSDomainID: "12345", + Name: "mail", + Value: "192.168.1.3", + Type: "MX", + Priority: 10, + TTL: 600, + }, + }, + }, + } + assert.False(t, c.Empty()) + + // Test CivoChanges with Creates, Updates, and Deletes + c = &CivoChanges{ + Creates: []*CivoChangeCreate{ + { + Domain: civogo.DNSDomain{ + ID: "12345", + AccountID: "1", + Name: "example.com", + }, + Options: &civogo.DNSRecordConfig{ + Type: civogo.DNSRecordTypeA, + Name: "www", + Value: "192.1.1.1", + Priority: 0, + TTL: 600, + }, + }, + }, + Updates: []*CivoChangeUpdate{ + { + Domain: civogo.DNSDomain{ + ID: "12345", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", + AccountID: "1", + DNSDomainID: "12345", + Name: "mail", + Value: "192.168.1.2", + Type: "MX", + Priority: 10, + TTL: 600, + }, + Options: civogo.DNSRecordConfig{ + Type: "MX", + Name: "mail", + Value: "192.168.1.3", + Priority: 10, + TTL: 600, + }, + }, + }, + Deletes: []*CivoChangeDelete{ + { + Domain: civogo.DNSDomain{ + ID: "12345", + AccountID: "1", + Name: "example.com", + }, + DomainRecord: civogo.DNSRecord{ + ID: "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", + AccountID: "1", + DNSDomainID: "12345", + Name: "mail", + Value: "192.168.1.3", + Type: "MX", + Priority: 10, + TTL: 600, + }, + }, + }, + } + assert.False(t, c.Empty()) +} + // This function is an adapted copy of the testify package's ElementsMatch function with the // call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to // other structs. It also ignores ordering when comparing unlike cmp.Equal. diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 0623ca2a05..621b5e0353 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -69,7 +69,7 @@ type cloudFlareDNS interface { ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error) - CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (*cloudflare.DNSRecordResponse, error) + CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error } @@ -90,7 +90,7 @@ func (z zoneService) ZoneIDByName(zoneName string) (string, error) { return z.service.ZoneIDByName(zoneName) } -func (z zoneService) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (*cloudflare.DNSRecordResponse, error) { +func (z zoneService) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) { return z.service.CreateDNSRecord(ctx, rc, rp) } @@ -99,7 +99,8 @@ func (z zoneService) ListDNSRecords(ctx context.Context, rc *cloudflare.Resource } func (z zoneService) UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error { - return z.service.UpdateDNSRecord(ctx, rc, rp) + _, err := z.service.UpdateDNSRecord(ctx, rc, rp) + return err } func (z zoneService) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error { @@ -137,9 +138,20 @@ type RecordParamsTypes interface { cloudflare.UpdateDNSRecordParams | cloudflare.CreateDNSRecordParams } -// getRecordParam is a generic function that returns the appropriate Record Param based on the cloudFlareChange passed in -func getRecordParam[T RecordParamsTypes](cfc cloudFlareChange) T { - return T{ +// getUpdateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in +func getUpdateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams { + return cloudflare.UpdateDNSRecordParams{ + Name: cfc.ResourceRecord.Name, + TTL: cfc.ResourceRecord.TTL, + Proxied: cfc.ResourceRecord.Proxied, + Type: cfc.ResourceRecord.Type, + Content: cfc.ResourceRecord.Content, + } +} + +// getCreateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in +func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordParams { + return cloudflare.CreateDNSRecordParams{ Name: cfc.ResourceRecord.Name, TTL: cfc.ResourceRecord.TTL, Proxied: cfc.ResourceRecord.Proxied, @@ -297,6 +309,7 @@ func (p *CloudFlareProvider) PropertyValuesEqual(name string, previous string, c func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloudFlareChange) error { // return early if there is nothing to change if len(changes) == 0 { + log.Info("All records are already up to date") return nil } @@ -334,7 +347,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord) continue } - recordParam := getRecordParam[cloudflare.UpdateDNSRecordParams](*change) + recordParam := getUpdateDNSRecordParam(*change) recordParam.ID = recordID err := p.Client.UpdateDNSRecord(ctx, resourceContainer, recordParam) if err != nil { @@ -351,7 +364,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud log.WithFields(logFields).Errorf("failed to delete record: %v", err) } } else if change.Action == cloudFlareCreate { - recordParam := getRecordParam[cloudflare.CreateDNSRecordParams](*change) + recordParam := getCreateDNSRecordParam(*change) _, err := p.Client.CreateDNSRecord(ctx, resourceContainer, recordParam) if err != nil { log.WithFields(logFields).Errorf("failed to create record: %v", err) diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go index 6d1d47b400..5040404d23 100644 --- a/provider/cloudflare/cloudflare_test.go +++ b/provider/cloudflare/cloudflare_test.go @@ -130,7 +130,7 @@ func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord { } } -func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (*cloudflare.DNSRecordResponse, error) { +func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) { recordData := getDNSRecordFromRecordParams(rp) m.Actions = append(m.Actions, MockAction{ Name: "Create", @@ -141,7 +141,7 @@ func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudfla if zone, ok := m.Records[rc.Identifier]; ok { zone[rp.ID] = recordData } - return nil, nil + return cloudflare.DNSRecord{}, nil } func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error) { @@ -680,7 +680,7 @@ func TestCloudflareProvider(t *testing.T) { _ = os.Unsetenv("CF_API_TOKEN") tokenFile := "/tmp/cf_api_token" - if err := os.WriteFile(tokenFile, []byte("abc123def"), 0644); err != nil { + if err := os.WriteFile(tokenFile, []byte("abc123def"), 0o644); err != nil { t.Errorf("failed to write token file, %s", err) } _ = os.Setenv("CF_API_TOKEN", tokenFile) diff --git a/provider/coredns/coredns.go b/provider/coredns/coredns.go index 356ffee732..a514ca8659 100644 --- a/provider/coredns/coredns.go +++ b/provider/coredns/coredns.go @@ -37,10 +37,6 @@ import ( "sigs.k8s.io/external-dns/provider" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - const ( priority = 10 // default priority when nothing is set etcdTimeout = 5 * time.Second @@ -112,7 +108,7 @@ func (c etcdClient) GetServices(prefix string) ([]*Service, error) { for _, n := range r.Kvs { svc := new(Service) if err := json.Unmarshal(n.Value, svc); err != nil { - return nil, fmt.Errorf("%s: %s", n.Key, err.Error()) + return nil, fmt.Errorf("%s: %w", n.Key, err) } b := Service{Host: svc.Host, Port: svc.Port, Priority: svc.Priority, Weight: svc.Weight, Text: svc.Text, Key: string(n.Key)} if _, ok := bx[b]; ok { @@ -166,7 +162,7 @@ func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) ( if certPath != "" { cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { - return nil, fmt.Errorf("could not load TLS cert: %s", err) + return nil, fmt.Errorf("could not load TLS cert: %w", err) } certificates = append(certificates, cert) } @@ -192,11 +188,11 @@ func loadRoots(caPath string) (*x509.CertPool, error) { roots := x509.NewCertPool() pem, err := os.ReadFile(caPath) if err != nil { - return nil, fmt.Errorf("error reading %s: %s", caPath, err) + return nil, fmt.Errorf("error reading %s: %w", caPath, err) } ok := roots.AppendCertsFromPEM(pem) if !ok { - return nil, fmt.Errorf("could not read root certs: %s", err) + return nil, fmt.Errorf("could not read root certs: %w", err) } return roots, nil } diff --git a/provider/gandi/gandi.go b/provider/gandi/gandi.go index 378b166e8b..3746469ea1 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 cd9529f05e..bd32d5b6ce 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/godaddy/client.go b/provider/godaddy/client.go index 32f4df62c3..208fe22a4f 100644 --- a/provider/godaddy/client.go +++ b/provider/godaddy/client.go @@ -23,9 +23,13 @@ import ( "errors" "fmt" "io" + "math/rand" "net/http" + "strconv" "time" + "golang.org/x/time/rate" + "sigs.k8s.io/external-dns/pkg/apis/externaldns" ) @@ -71,6 +75,9 @@ type Client struct { // Client is the underlying HTTP client used to run the requests. It may be overloaded but a default one is instanciated in ``NewClient`` by default. Client *http.Client + // GoDaddy limits to 60 requests per minute + Ratelimiter *rate.Limiter + // Logger is used to log HTTP requests and responses. Logger Logger @@ -115,6 +122,7 @@ func NewClient(useOTE bool, apiKey, apiSecret string) (*Client, error) { APISecret: apiSecret, APIEndPoint: endpoint, Client: &http.Client{}, + Ratelimiter: rate.NewLimiter(rate.Every(60*time.Second), 60), Timeout: DefaultTimeout, } @@ -216,7 +224,22 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) { if c.Logger != nil { c.Logger.LogRequest(req) } + + c.Ratelimiter.Wait(req.Context()) resp, err := c.Client.Do(req) + // In case of several clients behind NAT we still can hit rate limit + for i := 1; i < 3 && err == nil && resp.StatusCode == 429; i++ { + retryAfter, _ := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 0) + + jitter := rand.Int63n(retryAfter) + retryAfterSec := retryAfter + jitter/2 + + sleepTime := time.Duration(retryAfterSec) * time.Second + time.Sleep(sleepTime) + + c.Ratelimiter.Wait(req.Context()) + resp, err = c.Client.Do(req) + } if err != nil { return nil, err } diff --git a/provider/godaddy/godaddy.go b/provider/godaddy/godaddy.go index 76b0520766..0b2186ce54 100644 --- a/provider/godaddy/godaddy.go +++ b/provider/godaddy/godaddy.go @@ -5,7 +5,7 @@ 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 + 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, @@ -34,13 +34,13 @@ import ( const ( gdMinimalTTL = 600 gdCreate = 0 - gdUpdate = 1 + gdReplace = 1 gdDelete = 2 ) var actionNames = []string{ "create", - "update", + "replace", "delete", } @@ -82,9 +82,8 @@ type gdRecordField struct { Service *string `json:"service,omitempty"` } -type gdUpdateRecordField struct { +type gdReplaceRecordField struct { Data string `json:"data"` - Name string `json:"name"` TTL int64 `json:"ttl"` Port *int `json:"port,omitempty"` Priority *int `json:"priority,omitempty"` @@ -247,7 +246,7 @@ func (p *GDProvider) records(ctx *context.Context, zone string, all bool) (*gdRe results.records = append(results.records, rec) } else { - log.Infof("GoDaddy: Discard record %s for %s is %+v", rec.Name, zone, rec) + log.Infof("GoDaddy: Ignore record %s for %s is %+v", rec.Name, zone, rec) } } @@ -347,28 +346,20 @@ func (p *GDProvider) changeAllRecords(endpoints []gdEndpoint, zoneRecords []*gdR log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", dnsName) } else { dnsName = strings.TrimSuffix(dnsName, "."+zone) + if dnsName == zone { + dnsName = "" + } if e.endpoint.RecordType == endpoint.RecordTypeA && (len(dnsName) == 0) { dnsName = "@" } - for _, target := range e.endpoint.Targets { - change := gdRecordField{ - Type: e.endpoint.RecordType, - Name: dnsName, - TTL: p.ttl, - Data: target, - } - - if e.endpoint.RecordTTL.IsConfigured() { - change.TTL = maxOf(gdMinimalTTL, int64(e.endpoint.RecordTTL)) - } + e.endpoint.RecordTTL = endpoint.TTL(maxOf(gdMinimalTTL, int64(e.endpoint.RecordTTL))) - if err := zoneRecord.applyChange(e.action, p.client, change, p.DryRun); err != nil { - log.Errorf("Unable to apply change %s on record %s, %v", actionNames[e.action], change, err) + if err := zoneRecord.applyEndpoint(e.action, p.client, *e.endpoint, dnsName, p.DryRun); err != nil { + log.Errorf("Unable to apply change %s on record %s type %s, %v", actionNames[e.action], dnsName, e.endpoint.RecordType, err) - return err - } + return err } } } @@ -393,11 +384,49 @@ func (p *GDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) er changedZoneRecords[i] = &records[i] } - allChanges := make([]gdEndpoint, 0, countTargets(changes)) + var allChanges []gdEndpoint allChanges = p.appendChange(gdDelete, changes.Delete, allChanges) - allChanges = p.appendChange(gdDelete, changes.UpdateOld, allChanges) - allChanges = p.appendChange(gdCreate, changes.UpdateNew, allChanges) + + iOldSkip := make(map[int]bool) + iNewSkip := make(map[int]bool) + + for iOld, recOld := range changes.UpdateOld { + for iNew, recNew := range changes.UpdateNew { + if recOld.DNSName == recNew.DNSName && recOld.RecordType == recNew.RecordType { + ReplaceEndpoints := []*endpoint.Endpoint{recNew} + allChanges = p.appendChange(gdReplace, ReplaceEndpoints, allChanges) + iOldSkip[iOld] = true + iNewSkip[iNew] = true + break + } + } + } + + for iOld, recOld := range changes.UpdateOld { + _, found := iOldSkip[iOld] + if found { + continue + } + for iNew, recNew := range changes.UpdateNew { + _, found := iNewSkip[iNew] + if found { + continue + } + + if recOld.DNSName != recNew.DNSName { + continue + } + + DeleteEndpoints := []*endpoint.Endpoint{recOld} + CreateEndpoints := []*endpoint.Endpoint{recNew} + allChanges = p.appendChange(gdDelete, DeleteEndpoints, allChanges) + allChanges = p.appendChange(gdCreate, CreateEndpoints, allChanges) + + break + } + } + allChanges = p.appendChange(gdCreate, changes.Create, allChanges) log.Infof("GoDaddy: %d changes will be done", len(allChanges)) @@ -409,102 +438,136 @@ func (p *GDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) er return nil } -func (p *gdRecords) addRecord(client gdClient, change gdRecordField, dryRun bool) error { +func (p *gdRecords) addRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error { var response GDErrorResponse + for _, target := range endpoint.Targets { + change := gdRecordField{ + Type: endpoint.RecordType, + Name: dnsName, + TTL: int64(endpoint.RecordTTL), + Data: target, + } - log.Debugf("GoDaddy: Add an entry %s to zone %s", change.String(), p.zone) - - p.records = append(p.records, change) - p.changed = true + p.records = append(p.records, change) + p.changed = true - if dryRun { - log.Infof("[DryRun] - Add record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change)) - } else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records", p.zone), []gdRecordField{change}, &response); err != nil { - log.Errorf("Add record %s.%s of type %s failed: %s", change.Name, p.zone, change.Type, response) + log.Debugf("GoDaddy: Add an entry %s to zone %s", change.String(), p.zone) + if dryRun { + log.Infof("[DryRun] - Add record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change)) + } else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records", p.zone), []gdRecordField{change}, &response); err != nil { + log.Errorf("Add record %s.%s of type %s failed: %s", change.Name, p.zone, change.Type, response) - return err + return err + } } return nil } -func (p *gdRecords) updateRecord(client gdClient, change gdRecordField, dryRun bool) error { - log.Debugf("GoDaddy: Update an entry %s to zone %s", change.String(), p.zone) - - for index, record := range p.records { - if record.Type == change.Type && record.Name == change.Name { - var response GDErrorResponse - - p.records[index] = change - p.changed = true +func (p *gdRecords) replaceRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error { + changed := []gdReplaceRecordField{} + records := []string{} - changed := []gdUpdateRecordField{{ - Data: change.Data, - Name: change.Name, - TTL: change.TTL, - Port: change.Port, - Priority: change.Priority, - Weight: change.Weight, - Protocol: change.Protocol, - Service: change.Service, - }} - - if dryRun { - log.Infof("[DryRun] - Update record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(changed)) - } else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records/%s", p.zone, change.Type), changed, &response); err != nil { - log.Errorf("Update record %s.%s of type %s failed: %v", change.Name, p.zone, change.Type, response) + for _, target := range endpoint.Targets { + change := gdRecordField{ + Type: endpoint.RecordType, + Name: dnsName, + TTL: int64(endpoint.RecordTTL), + Data: target, + } - return err + for index, record := range p.records { + if record.Type == change.Type && record.Name == change.Name { + p.records[index] = change + p.changed = true } } + records = append(records, target) + changed = append(changed, gdReplaceRecordField{ + Data: change.Data, + TTL: change.TTL, + Port: change.Port, + Priority: change.Priority, + Weight: change.Weight, + Protocol: change.Protocol, + Service: change.Service, + }) + } + + var response GDErrorResponse + + if dryRun { + log.Infof("[DryRun] - Replace record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records) + + return nil + } + + log.Debugf("Replace record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records) + if err := client.Put(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, endpoint.RecordType, dnsName), changed, &response); err != nil { + log.Errorf("Replace record %s.%s of type %s failed: %v", dnsName, p.zone, endpoint.RecordType, response) + + return err } return nil } // Remove one record from the record list -func (p *gdRecords) deleteRecord(client gdClient, change gdRecordField, dryRun bool) error { - log.Debugf("GoDaddy: Delete an entry %s to zone %s", change.String(), p.zone) +func (p *gdRecords) deleteRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error { + records := []string{} + + for _, target := range endpoint.Targets { + change := gdRecordField{ + Type: endpoint.RecordType, + Name: dnsName, + TTL: int64(endpoint.RecordTTL), + Data: target, + } + records = append(records, target) - deleteIndex := -1 + log.Debugf("GoDaddy: Delete an entry %s from zone %s", change.String(), p.zone) - for index, record := range p.records { - if record.Type == change.Type && record.Name == change.Name && record.Data == change.Data { - deleteIndex = index - break + deleteIndex := -1 + + for index, record := range p.records { + if record.Type == change.Type && record.Name == change.Name && record.Data == change.Data { + deleteIndex = index + break + } } - } - if deleteIndex >= 0 { - var response GDErrorResponse + if deleteIndex >= 0 { + p.records[deleteIndex] = p.records[len(p.records)-1] - p.records[deleteIndex] = p.records[len(p.records)-1] + p.records = p.records[:len(p.records)-1] + p.changed = true + } + } - p.records = p.records[:len(p.records)-1] - p.changed = true + if dryRun { + log.Infof("[DryRun] - Delete record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records) - if dryRun { - log.Infof("[DryRun] - Delete record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change)) - } else if err := client.Delete(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, change.Type, change.Name), &response); err != nil { - log.Errorf("Delete record %s.%s of type %s failed: %v", change.Name, p.zone, change.Type, response) + return nil + } - return err - } - } else { - log.Warnf("GoDaddy: record in zone %s not found %s to delete", p.zone, change.String()) + var response GDErrorResponse + if err := client.Delete(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, endpoint.RecordType, dnsName), &response); err != nil { + log.Errorf("Delete record %s.%s of type %s failed: %v", dnsName, p.zone, endpoint.RecordType, response) + + return err } return nil } -func (p *gdRecords) applyChange(action int, client gdClient, change gdRecordField, dryRun bool) error { +func (p *gdRecords) applyEndpoint(action int, client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error { switch action { case gdCreate: - return p.addRecord(client, change, dryRun) - case gdUpdate: - return p.updateRecord(client, change, dryRun) + return p.addRecord(client, endpoint, dnsName, dryRun) + case gdReplace: + return p.replaceRecord(client, endpoint, dnsName, dryRun) case gdDelete: - return p.deleteRecord(client, change, dryRun) + return p.deleteRecord(client, endpoint, dnsName, dryRun) } return nil diff --git a/provider/google/google.go b/provider/google/google.go index d6c53ef6ab..5fce653f58 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 3fe6100914..bc321200b7 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/ibmcloud/ibmcloud.go b/provider/ibmcloud/ibmcloud.go index 1340edfd2c..c8560db013 100644 --- a/provider/ibmcloud/ibmcloud.go +++ b/provider/ibmcloud/ibmcloud.go @@ -757,7 +757,7 @@ func (p *IBMCloudProvider) groupPrivateRecords(records []dnssvcsv1.ResourceRecor for _, records := range groups { targets := make([]string, len(records)) for i, record := range records { - data := record.Rdata.(map[string]interface{}) + data := record.Rdata log.Debugf("record data: %v", data) switch *record.Type { case "A": @@ -820,18 +820,18 @@ func (p *IBMCloudProvider) newIBMCloudChange(action string, endpoint *endpoint.E } if p.privateZone { - var rData interface{} + rData := make(map[string]interface{}) switch endpoint.RecordType { case "A": - rData = &dnssvcsv1.ResourceRecordInputRdataRdataARecord{ + rData[dnssvcsv1.CreateResourceRecordOptions_Type_A] = &dnssvcsv1.ResourceRecordInputRdataRdataARecord{ Ip: core.StringPtr(target), } case "CNAME": - rData = &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{ + rData[dnssvcsv1.CreateResourceRecordOptions_Type_Cname] = &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{ Cname: core.StringPtr(target), } case "TXT": - rData = &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{ + rData[dnssvcsv1.CreateResourceRecordOptions_Type_Txt] = &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{ Text: core.StringPtr(target), } } @@ -869,15 +869,15 @@ func (p *IBMCloudProvider) createRecord(ctx context.Context, zoneID string, chan } switch *change.PrivateResourceRecord.Type { case "A": - data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) + data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) aData, _ := p.Client.NewResourceRecordInputRdataRdataARecord(*data.Ip) createResourceRecordOptions.SetRdata(aData) case "CNAME": - data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) + data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) cnameData, _ := p.Client.NewResourceRecordInputRdataRdataCnameRecord(*data.Cname) createResourceRecordOptions.SetRdata(cnameData) case "TXT": - data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) + data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) txtData, _ := p.Client.NewResourceRecordInputRdataRdataTxtRecord(*data.Text) createResourceRecordOptions.SetRdata(txtData) } @@ -910,15 +910,15 @@ func (p *IBMCloudProvider) updateRecord(ctx context.Context, zoneID, recordID st } switch *change.PrivateResourceRecord.Type { case "A": - data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) + data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) aData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataARecord(*data.Ip) updateResourceRecordOptions.SetRdata(aData) case "CNAME": - data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) + data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) cnameData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataCnameRecord(*data.Cname) updateResourceRecordOptions.SetRdata(cnameData) case "TXT": - data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) + data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) txtData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataTxtRecord(*data.Text) updateResourceRecordOptions.SetRdata(txtData) } diff --git a/provider/infoblox/infoblox.go b/provider/infoblox/infoblox.go index 2d88a06b32..fa4a0c8c3b 100644 --- a/provider/infoblox/infoblox.go +++ b/provider/infoblox/infoblox.go @@ -199,7 +199,7 @@ func recordQueryParams(zone string, view string) *ibclient.QueryParams { func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) { zones, err := p.zones() if err != nil { - return nil, fmt.Errorf("could not fetch zones: %s", err) + return nil, fmt.Errorf("could not fetch zones: %w", err) } for _, zone := range zones { @@ -209,7 +209,7 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End objA := ibclient.NewEmptyRecordA() 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) + return nil, fmt.Errorf("could not fetch A records from zone '%s': %w", zone.Fqdn, err) } for _, res := range resA { // Check if endpoint already exists and add to existing endpoint if it does @@ -253,7 +253,7 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End objH := ibclient.NewEmptyHostRecord() 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) + return nil, fmt.Errorf("could not fetch host records from zone '%s': %w", zone.Fqdn, err) } for _, res := range resH { for _, ip := range res.Ipv4Addrs { @@ -273,7 +273,7 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End objC := ibclient.NewEmptyRecordCNAME() 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) + return nil, fmt.Errorf("could not fetch CNAME records from zone '%s': %w", zone.Fqdn, err) } for _, res := range resC { logrus.Debugf("Record='%s' CNAME:'%s'", res.Name, res.Canonical) @@ -290,7 +290,7 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End objP := ibclient.NewEmptyRecordPTR() err = p.client.GetObject(objP, "", recordQueryParams(arpaZone, p.view), &resP) if err != nil && !isNotFoundError(err) { - return nil, fmt.Errorf("could not fetch PTR records from zone '%s': %s", zone.Fqdn, err) + return nil, fmt.Errorf("could not fetch PTR records from zone '%s': %w", zone.Fqdn, err) } for _, res := range resP { endpoints = append(endpoints, endpoint.NewEndpoint(res.PtrdName, endpoint.RecordTypePTR, res.Ipv4Addr)) @@ -299,10 +299,10 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End } var resT []ibclient.RecordTXT - objT := ibclient.NewRecordTXT(ibclient.RecordTXT{}) + objT := ibclient.NewEmptyRecordTXT() 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) + return nil, fmt.Errorf("could not fetch TXT records from zone '%s': %w", zone.Fqdn, err) } for _, res := range resT { // The Infoblox API strips enclosing double quotes from TXT records lacking whitespace. @@ -587,13 +587,10 @@ func (p *ProviderConfig) recordSet(ep *endpoint.Endpoint, getObject bool, target if target, err2 := strconv.Unquote(ep.Targets[0]); err2 == nil && !strings.Contains(ep.Targets[0], " ") { ep.Targets = endpoint.Targets{target} } - obj := ibclient.NewRecordTXT( - ibclient.RecordTXT{ - Name: ep.DNSName, - Text: ep.Targets[0], - View: p.view, - }, - ) + obj := ibclient.NewEmptyRecordTXT() + obj.Name = ep.DNSName + obj.Text = ep.Targets[0] + obj.View = p.view if getObject { queryParams := ibclient.NewQueryParams(false, map[string]string{"name": obj.Name}) err = p.client.GetObject(obj, "", queryParams, &res) diff --git a/provider/infoblox/infoblox_test.go b/provider/infoblox/infoblox_test.go index e3f341271e..1cb56aa27a 100644 --- a/provider/infoblox/infoblox_test.go +++ b/provider/infoblox/infoblox_test.go @@ -330,11 +330,8 @@ func (client *mockIBConnector) DeleteObject(ref string) (refRes string, err erro } case "record:txt": var records []ibclient.RecordTXT - obj := ibclient.NewRecordTXT( - ibclient.RecordTXT{ - Name: result[2], - }, - ) + obj := ibclient.NewEmptyRecordTXT() + obj.Name = result[2] client.GetObject(obj, ref, nil, &records) for _, record := range records { client.deletedEndpoints = append( @@ -431,13 +428,11 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject obj.Canonical = value return obj case endpoint.RecordTypeTXT: - return ibclient.NewRecordTXT( - ibclient.RecordTXT{ - Ref: ref, - Name: name, - Text: value, - }, - ) + obj := ibclient.NewEmptyRecordTXT() + obj.Name = name + obj.Ref = ref + obj.Text = value + return obj case "HOST": obj := ibclient.NewEmptyHostRecord() obj.Name = name diff --git a/provider/inmemory/inmemory.go b/provider/inmemory/inmemory.go index 0abb1c9558..cee6acc246 100644 --- a/provider/inmemory/inmemory.go +++ b/provider/inmemory/inmemory.go @@ -22,6 +22,7 @@ import ( "strings" log "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" @@ -132,11 +133,7 @@ func (im *InMemoryProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, return nil, err } - for _, record := range records { - ep := endpoint.NewEndpoint(record.Name, record.Type, record.Target).WithSetIdentifier(record.SetIdentifier) - ep.Labels = record.Labels - endpoints = append(endpoints, ep) - } + endpoints = append(endpoints, copyEndpoints(records)...) } return endpoints, nil @@ -187,11 +184,11 @@ func (im *InMemoryProvider) ApplyChanges(ctx context.Context, changes *plan.Chan } for zoneID := range perZoneChanges { - change := &inMemoryChange{ - Create: convertToInMemoryRecord(perZoneChanges[zoneID].Create), - UpdateNew: convertToInMemoryRecord(perZoneChanges[zoneID].UpdateNew), - UpdateOld: convertToInMemoryRecord(perZoneChanges[zoneID].UpdateOld), - Delete: convertToInMemoryRecord(perZoneChanges[zoneID].Delete), + change := &plan.Changes{ + Create: perZoneChanges[zoneID].Create, + UpdateNew: perZoneChanges[zoneID].UpdateNew, + UpdateOld: perZoneChanges[zoneID].UpdateOld, + Delete: perZoneChanges[zoneID].Delete, } err := im.client.ApplyChanges(ctx, zoneID, change) if err != nil { @@ -202,16 +199,15 @@ func (im *InMemoryProvider) ApplyChanges(ctx context.Context, changes *plan.Chan return nil } -func convertToInMemoryRecord(endpoints []*endpoint.Endpoint) []*inMemoryRecord { - records := []*inMemoryRecord{} +func copyEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { + records := make([]*endpoint.Endpoint, 0, len(endpoints)) for _, ep := range endpoints { - records = append(records, &inMemoryRecord{ - Type: ep.RecordType, - Name: ep.DNSName, - Target: ep.Targets[0], - SetIdentifier: ep.SetIdentifier, - Labels: ep.Labels, - }) + newEp := endpoint.NewEndpointWithTTL(ep.DNSName, ep.RecordType, ep.RecordTTL, ep.Targets...).WithSetIdentifier(ep.SetIdentifier) + newEp.Labels = endpoint.NewLabels() + for k, v := range ep.Labels { + newEp.Labels[k] = v + } + records = append(records, newEp) } return records } @@ -244,26 +240,7 @@ func (f *filter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]st return matchZoneID } -// inMemoryRecord - record stored in memory -// Type - type of record -// Name - DNS name assigned to the record -// Target - target of the record -type inMemoryRecord struct { - Type string - SetIdentifier string - Name string - Target string - Labels endpoint.Labels -} - -type zone map[string][]*inMemoryRecord - -type inMemoryChange struct { - Create []*inMemoryRecord - UpdateNew []*inMemoryRecord - UpdateOld []*inMemoryRecord - Delete []*inMemoryRecord -} +type zone map[endpoint.EndpointKey]*endpoint.Endpoint type inMemoryClient struct { zones map[string]zone @@ -273,14 +250,14 @@ func newInMemoryClient() *inMemoryClient { return &inMemoryClient{map[string]zone{}} } -func (c *inMemoryClient) Records(zone string) ([]*inMemoryRecord, error) { +func (c *inMemoryClient) Records(zone string) ([]*endpoint.Endpoint, error) { if _, ok := c.zones[zone]; !ok { return nil, ErrZoneNotFound } - records := []*inMemoryRecord{} + var records []*endpoint.Endpoint for _, rec := range c.zones[zone] { - records = append(records, rec...) + records = append(records, rec) } return records, nil } @@ -297,66 +274,44 @@ func (c *inMemoryClient) CreateZone(zone string) error { if _, ok := c.zones[zone]; ok { return ErrZoneAlreadyExists } - c.zones[zone] = map[string][]*inMemoryRecord{} + c.zones[zone] = map[endpoint.EndpointKey]*endpoint.Endpoint{} return nil } -func (c *inMemoryClient) ApplyChanges(ctx context.Context, zoneID string, changes *inMemoryChange) error { +func (c *inMemoryClient) ApplyChanges(ctx context.Context, zoneID string, changes *plan.Changes) error { if err := c.validateChangeBatch(zoneID, changes); err != nil { return err } for _, newEndpoint := range changes.Create { - if _, ok := c.zones[zoneID][newEndpoint.Name]; !ok { - c.zones[zoneID][newEndpoint.Name] = make([]*inMemoryRecord, 0) - } - c.zones[zoneID][newEndpoint.Name] = append(c.zones[zoneID][newEndpoint.Name], newEndpoint) + c.zones[zoneID][newEndpoint.Key()] = newEndpoint } for _, updateEndpoint := range changes.UpdateNew { - for _, rec := range c.zones[zoneID][updateEndpoint.Name] { - if rec.Type == updateEndpoint.Type { - rec.Target = updateEndpoint.Target - break - } - } + c.zones[zoneID][updateEndpoint.Key()] = updateEndpoint } for _, deleteEndpoint := range changes.Delete { - newSet := make([]*inMemoryRecord, 0) - for _, rec := range c.zones[zoneID][deleteEndpoint.Name] { - if rec.Type != deleteEndpoint.Type { - newSet = append(newSet, rec) - } - } - c.zones[zoneID][deleteEndpoint.Name] = newSet + delete(c.zones[zoneID], deleteEndpoint.Key()) } return nil } -func (c *inMemoryClient) updateMesh(mesh map[string]map[string]map[string]bool, record *inMemoryRecord) error { - if _, exists := mesh[record.Name]; exists { - if _, exists := mesh[record.Name][record.Type]; exists { - if mesh[record.Name][record.Type][record.SetIdentifier] { - return ErrDuplicateRecordFound - } - mesh[record.Name][record.Type][record.SetIdentifier] = true - return nil - } - mesh[record.Name][record.Type] = map[string]bool{record.SetIdentifier: true} - return nil +func (c *inMemoryClient) updateMesh(mesh sets.Set[endpoint.EndpointKey], record *endpoint.Endpoint) error { + if mesh.Has(record.Key()) { + return ErrDuplicateRecordFound } - mesh[record.Name] = map[string]map[string]bool{record.Type: {record.SetIdentifier: true}} + mesh.Insert(record.Key()) return nil } // validateChangeBatch validates that the changes passed to InMemory DNS provider is valid -func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChange) error { +func (c *inMemoryClient) validateChangeBatch(zone string, changes *plan.Changes) error { curZone, ok := c.zones[zone] if !ok { return ErrZoneNotFound } - mesh := map[string]map[string]map[string]bool{} + mesh := sets.New[endpoint.EndpointKey]() for _, newEndpoint := range changes.Create { - if c.findByTypeAndSetIdentifier(newEndpoint.Type, newEndpoint.SetIdentifier, curZone[newEndpoint.Name]) != nil { + if _, exists := curZone[newEndpoint.Key()]; exists { return ErrRecordAlreadyExists } if err := c.updateMesh(mesh, newEndpoint); err != nil { @@ -364,7 +319,7 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang } } for _, updateEndpoint := range changes.UpdateNew { - if c.findByTypeAndSetIdentifier(updateEndpoint.Type, updateEndpoint.SetIdentifier, curZone[updateEndpoint.Name]) == nil { + if _, exists := curZone[updateEndpoint.Key()]; !exists { return ErrRecordNotFound } if err := c.updateMesh(mesh, updateEndpoint); err != nil { @@ -372,12 +327,12 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang } } for _, updateOldEndpoint := range changes.UpdateOld { - if rec := c.findByTypeAndSetIdentifier(updateOldEndpoint.Type, updateOldEndpoint.SetIdentifier, curZone[updateOldEndpoint.Name]); rec == nil || rec.Target != updateOldEndpoint.Target { + if rec, exists := curZone[updateOldEndpoint.Key()]; !exists || rec.Targets[0] != updateOldEndpoint.Targets[0] { return ErrRecordNotFound } } for _, deleteEndpoint := range changes.Delete { - if rec := c.findByTypeAndSetIdentifier(deleteEndpoint.Type, deleteEndpoint.SetIdentifier, curZone[deleteEndpoint.Name]); rec == nil || rec.Target != deleteEndpoint.Target { + if rec, exists := curZone[deleteEndpoint.Key()]; !exists || rec.Targets[0] != deleteEndpoint.Targets[0] { return ErrRecordNotFound } if err := c.updateMesh(mesh, deleteEndpoint); err != nil { @@ -386,12 +341,3 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang } return nil } - -func (c *inMemoryClient) findByTypeAndSetIdentifier(recordType, setIdentifier string, records []*inMemoryRecord) *inMemoryRecord { - for _, record := range records { - if record.Type == recordType && record.SetIdentifier == setIdentifier { - return record - } - } - return nil -} diff --git a/provider/inmemory/inmemory_test.go b/provider/inmemory/inmemory_test.go index 2772afed62..275b534101 100644 --- a/provider/inmemory/inmemory_test.go +++ b/provider/inmemory/inmemory_test.go @@ -22,7 +22,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" @@ -32,7 +31,6 @@ import ( var _ provider.Provider = &InMemoryProvider{} func TestInMemoryProvider(t *testing.T) { - t.Run("findByType", testInMemoryFindByType) t.Run("Records", testInMemoryRecords) t.Run("validateChangeBatch", testInMemoryValidateChangeBatch) t.Run("ApplyChanges", testInMemoryApplyChanges) @@ -40,114 +38,6 @@ func TestInMemoryProvider(t *testing.T) { t.Run("CreateZone", testInMemoryCreateZone) } -func testInMemoryFindByType(t *testing.T) { - for _, ti := range []struct { - title string - findType string - findSetIdentifier string - records []*inMemoryRecord - expected *inMemoryRecord - expectedEmpty bool - }{ - { - title: "no records, empty type", - findType: "", - records: nil, - expected: nil, - expectedEmpty: true, - }, - { - title: "no records, non-empty type", - findType: endpoint.RecordTypeA, - records: nil, - expected: nil, - expectedEmpty: true, - }, - { - title: "one record, empty type", - findType: "", - records: []*inMemoryRecord{ - { - Type: endpoint.RecordTypeA, - }, - }, - expected: nil, - expectedEmpty: true, - }, - { - title: "one record, wrong type", - findType: endpoint.RecordTypeCNAME, - records: []*inMemoryRecord{ - { - Type: endpoint.RecordTypeA, - }, - }, - expected: nil, - expectedEmpty: true, - }, - { - title: "one record, right type", - findType: endpoint.RecordTypeA, - records: []*inMemoryRecord{ - { - Type: endpoint.RecordTypeA, - }, - }, - expected: &inMemoryRecord{ - Type: endpoint.RecordTypeA, - }, - }, - { - title: "multiple records, right type", - findType: endpoint.RecordTypeA, - records: []*inMemoryRecord{ - { - Type: endpoint.RecordTypeA, - }, - { - Type: endpoint.RecordTypeTXT, - }, - }, - expected: &inMemoryRecord{ - Type: endpoint.RecordTypeA, - }, - }, - { - title: "multiple records, right type and set identifier", - findType: endpoint.RecordTypeA, - findSetIdentifier: "test-set-1", - records: []*inMemoryRecord{ - { - Type: endpoint.RecordTypeA, - SetIdentifier: "test-set-1", - }, - { - Type: endpoint.RecordTypeA, - SetIdentifier: "test-set-2", - }, - { - Type: endpoint.RecordTypeTXT, - }, - }, - expected: &inMemoryRecord{ - Type: endpoint.RecordTypeA, - SetIdentifier: "test-set-1", - }, - }, - } { - t.Run(ti.title, func(t *testing.T) { - c := newInMemoryClient() - record := c.findByTypeAndSetIdentifier(ti.findType, ti.findSetIdentifier, ti.records) - if ti.expectedEmpty { - assert.Nil(t, record) - } else { - require.NotNil(t, record) - assert.Equal(t, *ti.expected, *record) - } - }) - } -} - func testInMemoryRecords(t *testing.T) { for _, ti := range []struct { title string @@ -175,35 +65,12 @@ func testInMemoryRecords(t *testing.T) { title: "records, zone with records", zone: "org", init: map[string]zone{ - "org": { - "example.org": []*inMemoryRecord{ - { - Name: "example.org", - Target: "8.8.8.8", - Type: endpoint.RecordTypeA, - }, - { - Name: "example.org", - Type: endpoint.RecordTypeTXT, - }, - }, - "foo.org": []*inMemoryRecord{ - { - Name: "foo.org", - Target: "4.4.4.4", - Type: endpoint.RecordTypeCNAME, - }, - }, - }, - "com": { - "example.com": []*inMemoryRecord{ - { - Name: "example.com", - Target: "4.4.4.4", - Type: endpoint.RecordTypeCNAME, - }, - }, - }, + "org": makeZone( + "example.org", "8.8.8.8", endpoint.RecordTypeA, + "example.org", "", endpoint.RecordTypeTXT, + "foo.org", "4.4.4.4", endpoint.RecordTypeCNAME, + ), + "com": makeZone("example.com", "4.4.4.4", endpoint.RecordTypeCNAME), }, expectError: false, expected: []*endpoint.Endpoint{ @@ -246,41 +113,13 @@ func testInMemoryRecords(t *testing.T) { func testInMemoryValidateChangeBatch(t *testing.T) { init := map[string]zone{ - "org": { - "example.org": []*inMemoryRecord{ - { - Name: "example.org", - Target: "8.8.8.8", - Type: endpoint.RecordTypeA, - }, - { - Name: "example.org", - }, - }, - "foo.org": []*inMemoryRecord{ - { - Name: "foo.org", - Target: "bar.org", - Type: endpoint.RecordTypeCNAME, - }, - }, - "foo.bar.org": []*inMemoryRecord{ - { - Name: "foo.bar.org", - Target: "5.5.5.5", - Type: endpoint.RecordTypeA, - }, - }, - }, - "com": { - "example.com": []*inMemoryRecord{ - { - Name: "example.com", - Target: "another-example.com", - Type: endpoint.RecordTypeCNAME, - }, - }, - }, + "org": makeZone( + "example.org", "8.8.8.8", endpoint.RecordTypeA, + "example.org", "", endpoint.RecordTypeTXT, + "foo.org", "bar.org", endpoint.RecordTypeCNAME, + "foo.bar.org", "5.5.5.5", endpoint.RecordTypeA, + ), + "com": makeZone("example.com", "another-example.com", endpoint.RecordTypeCNAME), } for _, ti := range []struct { title string @@ -561,11 +400,11 @@ func testInMemoryValidateChangeBatch(t *testing.T) { t.Run(ti.title, func(t *testing.T) { c := &inMemoryClient{} c.zones = ti.init - ichanges := &inMemoryChange{ - Create: convertToInMemoryRecord(ti.changes.Create), - UpdateNew: convertToInMemoryRecord(ti.changes.UpdateNew), - UpdateOld: convertToInMemoryRecord(ti.changes.UpdateOld), - Delete: convertToInMemoryRecord(ti.changes.Delete), + ichanges := &plan.Changes{ + Create: ti.changes.Create, + UpdateNew: ti.changes.UpdateNew, + UpdateOld: ti.changes.UpdateOld, + Delete: ti.changes.Delete, } err := c.validateChangeBatch(ti.zone, ichanges) if ti.expectError { @@ -579,42 +418,12 @@ func testInMemoryValidateChangeBatch(t *testing.T) { func getInitData() map[string]zone { return map[string]zone{ - "org": { - "example.org": []*inMemoryRecord{ - { - Name: "example.org", - Target: "8.8.8.8", - Type: endpoint.RecordTypeA, - }, - { - Name: "example.org", - Type: endpoint.RecordTypeTXT, - }, - }, - "foo.org": []*inMemoryRecord{ - { - Name: "foo.org", - Target: "4.4.4.4", - Type: endpoint.RecordTypeCNAME, - }, - }, - "foo.bar.org": []*inMemoryRecord{ - { - Name: "foo.bar.org", - Target: "5.5.5.5", - Type: endpoint.RecordTypeA, - }, - }, - }, - "com": { - "example.com": []*inMemoryRecord{ - { - Name: "example.com", - Target: "4.4.4.4", - Type: endpoint.RecordTypeCNAME, - }, - }, - }, + "org": makeZone("example.org", "8.8.8.8", endpoint.RecordTypeA, + "example.org", "", endpoint.RecordTypeTXT, + "foo.org", "4.4.4.4", endpoint.RecordTypeCNAME, + "foo.bar.org", "5.5.5.5", endpoint.RecordTypeA, + ), + "com": makeZone("example.com", "4.4.4.4", endpoint.RecordTypeCNAME), } } @@ -679,36 +488,11 @@ func testInMemoryApplyChanges(t *testing.T) { }, }, expectedZonesState: map[string]zone{ - "org": { - "example.org": []*inMemoryRecord{ - { - Name: "example.org", - Target: "8.8.8.8", - Type: endpoint.RecordTypeA, - }, - { - Name: "example.org", - Type: endpoint.RecordTypeTXT, - }, - }, - "foo.org": []*inMemoryRecord{ - { - Name: "foo.org", - Target: "4.4.4.4", - Type: endpoint.RecordTypeCNAME, - }, - }, - "foo.bar.org": []*inMemoryRecord{}, - }, - "com": { - "example.com": []*inMemoryRecord{ - { - Name: "example.com", - Target: "4.4.4.4", - Type: endpoint.RecordTypeCNAME, - }, - }, - }, + "org": makeZone("example.org", "8.8.8.8", endpoint.RecordTypeA, + "example.org", "", endpoint.RecordTypeTXT, + "foo.org", "4.4.4.4", endpoint.RecordTypeCNAME, + ), + "com": makeZone("example.com", "4.4.4.4", endpoint.RecordTypeCNAME), }, }, { @@ -720,6 +504,7 @@ func testInMemoryApplyChanges(t *testing.T) { DNSName: "foo.bar.new.org", Targets: endpoint.Targets{"4.8.8.9"}, RecordType: endpoint.RecordTypeA, + Labels: endpoint.NewLabels(), }, }, UpdateNew: []*endpoint.Endpoint{ @@ -727,6 +512,7 @@ func testInMemoryApplyChanges(t *testing.T) { DNSName: "foo.bar.org", Targets: endpoint.Targets{"4.8.8.4"}, RecordType: endpoint.RecordTypeA, + Labels: endpoint.NewLabels(), }, }, UpdateOld: []*endpoint.Endpoint{ @@ -734,6 +520,7 @@ func testInMemoryApplyChanges(t *testing.T) { DNSName: "foo.bar.org", Targets: endpoint.Targets{"5.5.5.5"}, RecordType: endpoint.RecordTypeA, + Labels: endpoint.NewLabels(), }, }, Delete: []*endpoint.Endpoint{ @@ -741,48 +528,18 @@ func testInMemoryApplyChanges(t *testing.T) { DNSName: "example.org", Targets: endpoint.Targets{"8.8.8.8"}, RecordType: endpoint.RecordTypeA, + Labels: endpoint.NewLabels(), }, }, }, expectedZonesState: map[string]zone{ - "org": { - "example.org": []*inMemoryRecord{ - { - Name: "example.org", - Type: endpoint.RecordTypeTXT, - }, - }, - "foo.org": []*inMemoryRecord{ - { - Name: "foo.org", - Target: "4.4.4.4", - Type: endpoint.RecordTypeCNAME, - }, - }, - "foo.bar.org": []*inMemoryRecord{ - { - Name: "foo.bar.org", - Target: "4.8.8.4", - Type: endpoint.RecordTypeA, - }, - }, - "foo.bar.new.org": []*inMemoryRecord{ - { - Name: "foo.bar.new.org", - Target: "4.8.8.9", - Type: endpoint.RecordTypeA, - }, - }, - }, - "com": { - "example.com": []*inMemoryRecord{ - { - Name: "example.com", - Target: "4.4.4.4", - Type: endpoint.RecordTypeCNAME, - }, - }, - }, + "org": makeZone( + "example.org", "", endpoint.RecordTypeTXT, + "foo.org", "4.4.4.4", endpoint.RecordTypeCNAME, + "foo.bar.org", "4.8.8.4", endpoint.RecordTypeA, + "foo.bar.new.org", "4.8.8.9", endpoint.RecordTypeA, + ), + "com": makeZone("example.com", "4.4.4.4", endpoint.RecordTypeCNAME), }, }, } { @@ -815,3 +572,17 @@ func testInMemoryCreateZone(t *testing.T) { err = im.CreateZone("zone") assert.EqualError(t, err, ErrZoneAlreadyExists.Error()) } + +func makeZone(s ...string) map[endpoint.EndpointKey]*endpoint.Endpoint { + if len(s)%3 != 0 { + panic("makeZone arguments must be multiple of 3") + } + + output := map[endpoint.EndpointKey]*endpoint.Endpoint{} + for i := 0; i < len(s); i += 3 { + ep := endpoint.NewEndpoint(s[i], s[i+2], s[i+1]) + output[ep.Key()] = ep + } + + return output +} diff --git a/provider/pdns/pdns.go b/provider/pdns/pdns.go index ff8611dc3a..f3e2578858 100644 --- a/provider/pdns/pdns.go +++ b/provider/pdns/pdns.go @@ -258,6 +258,7 @@ func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, err func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) { endpoints = []*endpoint.Endpoint{} targets := []string{} + rrType_ := rr.Type_ for _, record := range rr.Records { // If a record is "Disabled", it's not supposed to be "visible" @@ -265,8 +266,10 @@ func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpo targets = append(targets, record.Content) } } - - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rr.Type_, endpoint.TTL(rr.Ttl), targets...)) + if rr.Type_ == "ALIAS" { + rrType_ = "CNAME" + } + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rrType_, endpoint.TTL(rr.Ttl), targets...)) return endpoints, nil } @@ -311,15 +314,19 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet // per (ep.DNSName, ep.RecordType) tuple, which holds true for // external-dns v5.0.0-alpha onwards records := []pgo.Record{} + RecordType_ := ep.RecordType for _, t := range ep.Targets { - if ep.RecordType == "CNAME" { + if ep.RecordType == "CNAME" || ep.RecordType == "ALIAS" { t = provider.EnsureTrailingDot(t) + if t != zone.Name && !strings.HasSuffix(t, "."+zone.Name) { + RecordType_ = "ALIAS" + } } records = append(records, pgo.Record{Content: t}) } rrset := pgo.RrSet{ Name: dnsname, - Type_: ep.RecordType, + Type_: RecordType_, Records: records, Changetype: string(changetype), } diff --git a/provider/pdns/pdns_test.go b/provider/pdns/pdns_test.go index 5e7eeb365b..b7108e1ed0 100644 --- a/provider/pdns/pdns_test.go +++ b/provider/pdns/pdns_test.go @@ -82,9 +82,19 @@ var ( Type_: "CNAME", Ttl: 300, Records: []pgo.Record{ - {Content: "example.by.any.other.name.com", Disabled: false, SetPtr: false}, + {Content: "example.com.", Disabled: false, SetPtr: false}, }, } + + RRSetALIASRecord = pgo.RrSet{ + Name: "alias.example.com.", + Type_: "ALIAS", + Ttl: 300, + Records: []pgo.Record{ + {Content: "example.by.any.other.name.com.", Disabled: false, SetPtr: false}, + }, + } + RRSetTXTRecord = pgo.RrSet{ Name: "example.com.", Type_: "TXT", @@ -129,9 +139,10 @@ var ( } endpointsMixedRecords = []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("cname.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.by.any.other.name.com"), + endpoint.NewEndpointWithTTL("cname.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.com"), endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "'would smell as sweet'"), endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8", "8.8.4.4", "4.4.4.4"), + endpoint.NewEndpointWithTTL("alias.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.by.any.other.name.com"), } endpointsMultipleZones = []*endpoint.Endpoint{ @@ -215,7 +226,7 @@ var ( Type_: "Zone", Url: "/api/v1/servers/localhost/zones/example.com.", Kind: "Native", - Rrsets: []pgo.RrSet{RRSetCNAMERecord, RRSetTXTRecord, RRSetMultipleRecords}, + Rrsets: []pgo.RrSet{RRSetCNAMERecord, RRSetTXTRecord, RRSetMultipleRecords, RRSetALIASRecord}, } ZoneEmptyToSimplePatch = pgo.Zone{ @@ -909,7 +920,7 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSConvertEndpointsToZones() { for _, z := range zlist { for _, rs := range z.Rrsets { - if "CNAME" == rs.Type_ { + if rs.Type_ == "CNAME" { for _, r := range rs.Records { assert.Equal(suite.T(), uint8(0x2e), r.Content[len(r.Content)-1]) } diff --git a/provider/pihole/client.go b/provider/pihole/client.go index df6fb4c576..4ca6552587 100644 --- a/provider/pihole/client.go +++ b/provider/pihole/client.go @@ -148,7 +148,7 @@ func (p *piholeClient) listRecords(ctx context.Context, rtype string) ([]*endpoi for _, rec := range data { name := rec[0] target := rec[1] - if p.cfg.DomainFilter.IsConfigured() && !p.cfg.DomainFilter.Match(name) { + if !p.cfg.DomainFilter.Match(name) { log.Debugf("Skipping %s that does not match domain filter", name) continue } @@ -195,7 +195,7 @@ type actionResponse struct { } func (p *piholeClient) apply(ctx context.Context, action string, ep *endpoint.Endpoint) error { - if p.cfg.DomainFilter.IsConfigured() && !p.cfg.DomainFilter.Match(ep.DNSName) { + if !p.cfg.DomainFilter.Match(ep.DNSName) { log.Debugf("Skipping %s %s that does not match domain filter", action, ep.DNSName) return nil } diff --git a/provider/provider.go b/provider/provider.go index 06791204dc..3c8d358f17 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -30,6 +30,13 @@ type Provider interface { Records(ctx context.Context) ([]*endpoint.Endpoint, error) ApplyChanges(ctx context.Context, changes *plan.Changes) error PropertyValuesEqual(name string, previous string, current string) bool + // AdjustEndpoints canonicalizes a set of candidate endpoints. + // It is called with a set of candidate endpoints obtained from the various sources. + // It returns a set modified as required by the provider. The provider is responsible for + // adding, removing, and modifying the ProviderSpecific properties to match + // the endpoints that the provider returns in `Records` so that the change plan will not have + // unnecessary (potentially failing) changes. It may also modify other fields, add, or remove + // Endpoints. It is permitted to modify the supplied endpoints. AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint GetDomainFilter() endpoint.DomainFilterInterface } diff --git a/provider/rdns/rdns.go b/provider/rdns/rdns.go index e645be0bc0..10766c8765 100644 --- a/provider/rdns/rdns.go +++ b/provider/rdns/rdns.go @@ -46,7 +46,7 @@ const ( ) func init() { - rand.Seed(time.Now().UnixNano()) + rand.New(rand.NewSource(time.Now().UnixNano())) } // RDNSClient is an interface to work with Rancher DNS(RDNS) records in etcdv3 backend. @@ -295,7 +295,7 @@ func newEtcdv3Client() (RDNSClient, error) { if cert != "" { cert, err := tls.LoadX509KeyPair(cert, key) if err != nil { - return nil, fmt.Errorf("could not load TLS cert: %s", err) + return nil, fmt.Errorf("could not load TLS cert: %w", err) } certificates = append(certificates, cert) } @@ -310,11 +310,11 @@ func newEtcdv3Client() (RDNSClient, error) { roots := x509.NewCertPool() pem, err := os.ReadFile(ca) if err != nil { - return nil, fmt.Errorf("error reading %s: %s", ca, err) + return nil, fmt.Errorf("error reading %s: %w", ca, err) } ok := roots.AppendCertsFromPEM(pem) if !ok { - return nil, fmt.Errorf("could not read root certs: %s", err) + return nil, fmt.Errorf("could not read root certs: %w", err) } config.RootCAs = roots } @@ -347,7 +347,7 @@ func (c etcdv3Client) Get(key string) ([]RDNSRecord, error) { for _, v := range result.Kvs { r := new(RDNSRecord) if err := json.Unmarshal(v.Value, r); err != nil { - return nil, fmt.Errorf("%s: %s", v.Key, err.Error()) + return nil, fmt.Errorf("%s: %w", v.Key, err) } r.Key = string(v.Key) rs = append(rs, *r) @@ -412,7 +412,7 @@ func (c etcdv3Client) aggregationRecords(result *clientv3.GetResponse) ([]RDNSRe for _, n := range result.Kvs { r := new(RDNSRecord) if err := json.Unmarshal(n.Value, r); err != nil { - return nil, fmt.Errorf("%s: %s", n.Key, err.Error()) + return nil, fmt.Errorf("%s: %w", n.Key, err) } r.Key = string(n.Key) diff --git a/provider/rfc2136/rfc2136.go b/provider/rfc2136/rfc2136.go index b46e410337..54a5211a4b 100644 --- a/provider/rfc2136/rfc2136.go +++ b/provider/rfc2136/rfc2136.go @@ -389,7 +389,6 @@ func (r rfc2136Provider) SendMessage(msg *dns.Msg) error { log.Debugf("SendMessage") c := new(dns.Client) - c.SingleInflight = true if !r.insecure { if r.gssTsig { diff --git a/provider/scaleway/scaleway.go b/provider/scaleway/scaleway.go index 29969fb2cb..0aa7606d45 100644 --- a/provider/scaleway/scaleway.go +++ b/provider/scaleway/scaleway.go @@ -19,6 +19,7 @@ package scaleway import ( "context" "fmt" + "os" "strconv" "strings" @@ -55,9 +56,19 @@ type ScalewayChange struct { // NewScalewayProvider initializes a new Scaleway DNS provider func NewScalewayProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*ScalewayProvider, error) { + var err error + defaultPageSize := uint64(1000) + if envPageSize, ok := os.LookupEnv("SCW_DEFAULT_PAGE_SIZE"); ok { + defaultPageSize, err = strconv.ParseUint(envPageSize, 10, 32) + if err != nil { + log.Infof("Ignoring default page size %s, defaulting to 1000", envPageSize) + defaultPageSize = 1000 + } + } scwClient, err := scw.NewClient( scw.WithEnv(), scw.WithUserAgent("ExternalDNS/"+externaldns.Version), + scw.WithDefaultPageSize(uint32(defaultPageSize)), ) if err != nil { return nil, err @@ -256,6 +267,10 @@ func (p *ScalewayProvider) generateApplyRequests(ctx context.Context, changes *p req.Changes = append(req.Changes, &domain.RecordChange{ Add: recordsToAdd[zoneName], }) + // ignore sending empty update requests + if len(req.Changes) == 1 && len(req.Changes[0].Add.Records) == 0 { + continue + } returnedRequests = append(returnedRequests, req) } @@ -278,9 +293,9 @@ func endpointToScalewayRecords(zoneName string, ep *endpoint.Endpoint) []*domain } priority := scalewayDefaultPriority if prop, ok := ep.GetProviderSpecificProperty(scalewayPriorityKey); ok { - prio, err := strconv.ParseUint(prop.Value, 10, 32) + prio, err := strconv.ParseUint(prop, 10, 32) if err != nil { - log.Errorf("Failed parsing value of %s: %s: %v; using priority of %d", scalewayPriorityKey, prop.Value, err, scalewayDefaultPriority) + log.Errorf("Failed parsing value of %s: %s: %v; using priority of %d", scalewayPriorityKey, prop, err, scalewayDefaultPriority) } else { priority = uint32(prio) } diff --git a/provider/tencentcloud/cloudapi/mockapi.go b/provider/tencentcloud/cloudapi/mockapi.go index 424bd6102b..3484ddc120 100644 --- a/provider/tencentcloud/cloudapi/mockapi.go +++ b/provider/tencentcloud/cloudapi/mockapi.go @@ -18,7 +18,6 @@ package cloudapi import ( "math/rand" - "time" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" @@ -34,7 +33,6 @@ type mockAPIService struct { } func NewMockService(privateZones []*privatedns.PrivateZone, privateZoneRecords map[string][]*privatedns.PrivateZoneRecord, dnspodDomains []*dnspod.DomainListItem, dnspodRecords map[string][]*dnspod.RecordListItem) *mockAPIService { - rand.Seed(time.Now().Unix()) return &mockAPIService{ privateZones: privateZones, privateZoneRecords: privateZoneRecords, @@ -137,11 +135,12 @@ func (api *mockAPIService) DescribePrivateZoneRecordList(request *privatedns.Des func (api *mockAPIService) DescribeDomainList(request *dnspod.DescribeDomainListRequest) (response *dnspod.DescribeDomainListResponse, err error) { response = dnspod.NewDescribeDomainListResponse() - response.Response = &struct { - DomainCountInfo *dnspod.DomainCountInfo `json:"DomainCountInfo,omitempty" name:"DomainCountInfo"` - DomainList []*dnspod.DomainListItem `json:"DomainList,omitempty" name:"DomainList"` - RequestId *string `json:"RequestId,omitempty" name:"RequestId"` - }{} + response.Response = &dnspod.DescribeDomainListResponseParams{ + DomainCountInfo: &dnspod.DomainCountInfo{ + AllTotal: common.Uint64Ptr(uint64(len(api.dnspodDomains))), + }, + DomainList: api.dnspodDomains, + } response.Response.DomainList = api.dnspodDomains response.Response.DomainCountInfo = &dnspod.DomainCountInfo{ AllTotal: common.Uint64Ptr(uint64(len(api.dnspodDomains))), @@ -151,11 +150,7 @@ func (api *mockAPIService) DescribeDomainList(request *dnspod.DescribeDomainList func (api *mockAPIService) DescribeRecordList(request *dnspod.DescribeRecordListRequest) (response *dnspod.DescribeRecordListResponse, err error) { response = dnspod.NewDescribeRecordListResponse() - response.Response = &struct { - RecordCountInfo *dnspod.RecordCountInfo `json:"RecordCountInfo,omitempty" name:"RecordCountInfo"` - RecordList []*dnspod.RecordListItem `json:"RecordList,omitempty" name:"RecordList"` - RequestId *string `json:"RequestId,omitempty" name:"RequestId"` - }{} + response.Response = &dnspod.DescribeRecordListResponseParams{} if _, exist := api.dnspodRecords[*request.Domain]; !exist { response.Response.RecordList = make([]*dnspod.RecordListItem, 0) response.Response.RecordCountInfo = &dnspod.RecordCountInfo{ diff --git a/provider/tencentcloud/privatedns.go b/provider/tencentcloud/privatedns.go index ad29418331..7bfc9d326f 100644 --- a/provider/tencentcloud/privatedns.go +++ b/provider/tencentcloud/privatedns.go @@ -119,7 +119,7 @@ func (p *TencentCloudProvider) getPrivateZones() ([]*privatedns.PrivateZone, err privateZonesFilter := make([]*privatedns.PrivateZone, 0) for _, privateZone := range privateZones { - if p.domainFilter.IsConfigured() && !p.domainFilter.Match(*privateZone.Domain) { + if !p.domainFilter.Match(*privateZone.Domain) { continue } privateZonesFilter = append(privateZonesFilter, privateZone) diff --git a/provider/transip/transip.go b/provider/transip/transip.go index 6543da9151..af8992383a 100644 --- a/provider/transip/transip.go +++ b/provider/transip/transip.go @@ -72,7 +72,7 @@ func NewTransIPProvider(accountName, privateKeyFile string, domainFilter endpoin Mode: apiMode, }) if err != nil { - return nil, fmt.Errorf("could not setup TransIP API client: %s", err.Error()) + return nil, fmt.Errorf("could not setup TransIP API client: %w", err) } // return TransIPProvider struct diff --git a/provider/zone_id_filter.go b/provider/zone_id_filter.go index 50a016b2d8..7a7b10621b 100644 --- a/provider/zone_id_filter.go +++ b/provider/zone_id_filter.go @@ -34,6 +34,9 @@ func (f ZoneIDFilter) Match(zoneID string) bool { if len(f.ZoneIDs) == 0 { return true } + if len(f.ZoneIDs) == 1 && f.ZoneIDs[0] == "" { + return true + } for _, id := range f.ZoneIDs { if strings.HasSuffix(zoneID, id) { diff --git a/registry/aws_sd_registry.go b/registry/aws_sd_registry.go index a2ff3350d8..1ecd885463 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() @@ -67,11 +67,6 @@ func (sdr *AWSSDRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, er return records, nil } -// MissingRecords returns nil because there is no missing records for AWSSD registry -func (sdr *AWSSDRegistry) MissingRecords() []*endpoint.Endpoint { - return nil -} - // ApplyChanges filters out records not owned the External-DNS, additionally it adds the required label // inserted in the AWS SD instance as a CreateID field func (sdr *AWSSDRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { @@ -96,7 +91,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/dynamodb.go b/registry/dynamodb.go new file mode 100644 index 0000000000..6507af6507 --- /dev/null +++ b/registry/dynamodb.go @@ -0,0 +1,437 @@ +/* +Copyright 2023 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 registry + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/dynamodb" + log "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/sets" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +// DynamoDBAPI is the subset of the AWS Route53 API that we actually use. Add methods as required. Signatures must match exactly. +type DynamoDBAPI interface { + DescribeTableWithContext(ctx aws.Context, input *dynamodb.DescribeTableInput, opts ...request.Option) (*dynamodb.DescribeTableOutput, error) + ScanPagesWithContext(ctx aws.Context, input *dynamodb.ScanInput, fn func(*dynamodb.ScanOutput, bool) bool, opts ...request.Option) error + BatchExecuteStatementWithContext(aws.Context, *dynamodb.BatchExecuteStatementInput, ...request.Option) (*dynamodb.BatchExecuteStatementOutput, error) +} + +// DynamoDBRegistry implements registry interface with ownership implemented via an AWS DynamoDB table. +type DynamoDBRegistry struct { + provider provider.Provider + ownerID string // refers to the owner id of the current instance + + dynamodbAPI DynamoDBAPI + table string + + // cache the dynamodb records owned by us. + labels map[endpoint.EndpointKey]endpoint.Labels + orphanedLabels sets.Set[endpoint.EndpointKey] + + // cache the records in memory and update on an interval instead. + recordsCache []*endpoint.Endpoint + recordsCacheRefreshTime time.Time + cacheInterval time.Duration +} + +// NewDynamoDBRegistry returns a new DynamoDBRegistry object. +func NewDynamoDBRegistry(provider provider.Provider, ownerID string, dynamodbAPI DynamoDBAPI, table string, cacheInterval time.Duration) (*DynamoDBRegistry, error) { + if ownerID == "" { + return nil, errors.New("owner id cannot be empty") + } + if table == "" { + return nil, errors.New("table cannot be empty") + } + + return &DynamoDBRegistry{ + provider: provider, + ownerID: ownerID, + dynamodbAPI: dynamodbAPI, + table: table, + cacheInterval: cacheInterval, + }, nil +} + +func (im *DynamoDBRegistry) GetDomainFilter() endpoint.DomainFilterInterface { + return im.provider.GetDomainFilter() +} + +// Records returns the current records from the registry. +func (im *DynamoDBRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { + // If we have the zones cached AND we have refreshed the cache since the + // last given interval, then just use the cached results. + if im.recordsCache != nil && time.Since(im.recordsCacheRefreshTime) < im.cacheInterval { + log.Debug("Using cached records.") + return im.recordsCache, nil + } + + if im.labels == nil { + if err := im.readLabels(ctx); err != nil { + return nil, err + } + } + + records, err := im.provider.Records(ctx) + if err != nil { + return nil, err + } + + orphanedLabels := sets.KeySet(im.labels) + endpoints := make([]*endpoint.Endpoint, 0, len(records)) + for _, record := range records { + key := record.Key() + if labels := im.labels[key]; labels != nil { + record.Labels = labels + orphanedLabels.Delete(key) + } else { + record.Labels = endpoint.NewLabels() + } + + endpoints = append(endpoints, record) + } + + im.orphanedLabels = orphanedLabels + + // Update the cache. + if im.cacheInterval > 0 { + im.recordsCache = endpoints + im.recordsCacheRefreshTime = time.Now() + } + + return endpoints, nil +} + +// ApplyChanges updates the DNS provider and DynamoDB table with the changes. +func (im *DynamoDBRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + filteredChanges := &plan.Changes{ + Create: changes.Create, + UpdateNew: filterOwnedRecords(im.ownerID, changes.UpdateNew), + UpdateOld: filterOwnedRecords(im.ownerID, changes.UpdateOld), + Delete: filterOwnedRecords(im.ownerID, changes.Delete), + } + + statements := make([]*dynamodb.BatchStatementRequest, 0, len(filteredChanges.Create)+len(filteredChanges.UpdateNew)) + for _, r := range filteredChanges.Create { + if r.Labels == nil { + r.Labels = make(map[string]string) + } + r.Labels[endpoint.OwnerLabelKey] = im.ownerID + + key := r.Key() + oldLabels := im.labels[key] + if oldLabels == nil { + statements = append(statements, &dynamodb.BatchStatementRequest{ + Statement: aws.String(fmt.Sprintf("INSERT INTO %q VALUE {'k':?, 'o':?, 'l':?}", im.table)), + Parameters: []*dynamodb.AttributeValue{ + toDynamoKey(key), + {S: aws.String(im.ownerID)}, + toDynamoLabels(r.Labels), + }, + ConsistentRead: aws.Bool(true), + }) + } else { + im.orphanedLabels.Delete(key) + statements = im.appendUpdate(statements, key, oldLabels, r.Labels) + } + + im.labels[key] = r.Labels + if im.cacheInterval > 0 { + im.addToCache(r) + } + } + + for _, r := range filteredChanges.Delete { + delete(im.labels, r.Key()) + if im.cacheInterval > 0 { + im.removeFromCache(r) + } + } + + oldLabels := make(map[endpoint.EndpointKey]endpoint.Labels, len(filteredChanges.UpdateOld)) + for _, r := range filteredChanges.UpdateOld { + oldLabels[r.Key()] = r.Labels + + // remove old version of record from cache + if im.cacheInterval > 0 { + im.removeFromCache(r) + } + } + + for _, r := range filteredChanges.UpdateNew { + key := r.Key() + statements = im.appendUpdate(statements, key, oldLabels[key], r.Labels) + + // add new version of record to caches + im.labels[key] = r.Labels + if im.cacheInterval > 0 { + im.addToCache(r) + } + } + + err := im.executeStatements(ctx, statements, func(request *dynamodb.BatchStatementRequest, response *dynamodb.BatchStatementResponse) error { + var context string + if strings.HasPrefix(*request.Statement, "INSERT") { + if aws.StringValue(response.Error.Code) == "DuplicateItem" { + // We lost a race with a different owner or another owner has an orphaned ownership record. + key := fromDynamoKey(request.Parameters[0]) + for i, endpoint := range filteredChanges.Create { + if endpoint.Key() == key { + log.Infof("Skipping endpoint %v because owner does not match", endpoint) + filteredChanges.Create = append(filteredChanges.Create[:i], filteredChanges.Create[i+1:]...) + // The dynamodb insertion failed; remove from our cache. + im.removeFromCache(endpoint) + delete(im.labels, key) + return nil + } + } + } + context = fmt.Sprintf("inserting dynamodb record %q", aws.StringValue(request.Parameters[0].S)) + } else { + context = fmt.Sprintf("updating dynamodb record %q", aws.StringValue(request.Parameters[1].S)) + } + return fmt.Errorf("%s: %s: %s", context, aws.StringValue(response.Error.Code), aws.StringValue(response.Error.Message)) + }) + if err != nil { + im.recordsCache = nil + im.labels = nil + return err + } + + // When caching is enabled, disable the provider from using the cache. + if im.cacheInterval > 0 { + ctx = context.WithValue(ctx, provider.RecordsContextKey, nil) + } + err = im.provider.ApplyChanges(ctx, filteredChanges) + if err != nil { + im.recordsCache = nil + im.labels = nil + return err + } + + statements = make([]*dynamodb.BatchStatementRequest, 0, len(filteredChanges.Delete)+len(im.orphanedLabels)) + for _, r := range filteredChanges.Delete { + statements = im.appendDelete(statements, r.Key()) + } + for r := range im.orphanedLabels { + statements = im.appendDelete(statements, r) + delete(im.labels, r) + } + im.orphanedLabels = nil + return im.executeStatements(ctx, statements, func(request *dynamodb.BatchStatementRequest, response *dynamodb.BatchStatementResponse) error { + im.labels = nil + return fmt.Errorf("deleting dynamodb record %q: %s: %s", aws.StringValue(request.Parameters[0].S), aws.StringValue(response.Error.Code), aws.StringValue(response.Error.Message)) + }) +} + +// PropertyValuesEqual compares two attribute values for equality. +func (im *DynamoDBRegistry) PropertyValuesEqual(name string, previous string, current string) bool { + return im.provider.PropertyValuesEqual(name, previous, current) +} + +// AdjustEndpoints modifies the endpoints as needed by the specific provider. +func (im *DynamoDBRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { + return im.provider.AdjustEndpoints(endpoints) +} + +func (im *DynamoDBRegistry) readLabels(ctx context.Context) error { + table, err := im.dynamodbAPI.DescribeTableWithContext(ctx, &dynamodb.DescribeTableInput{ + TableName: aws.String(im.table), + }) + if err != nil { + return fmt.Errorf("describing table %q: %w", im.table, err) + } + + foundKey := false + for _, def := range table.Table.AttributeDefinitions { + if aws.StringValue(def.AttributeName) == "k" { + if aws.StringValue(def.AttributeType) != "S" { + return fmt.Errorf("table %q attribute \"k\" must have type \"S\"", im.table) + } + foundKey = true + } + } + if !foundKey { + return fmt.Errorf("table %q must have attribute \"k\" of type \"S\"", im.table) + } + + if aws.StringValue(table.Table.KeySchema[0].AttributeName) != "k" { + return fmt.Errorf("table %q must have hash key \"k\"", im.table) + } + if len(table.Table.KeySchema) > 1 { + return fmt.Errorf("table %q must not have a range key", im.table) + } + + labels := map[endpoint.EndpointKey]endpoint.Labels{} + err = im.dynamodbAPI.ScanPagesWithContext(ctx, &dynamodb.ScanInput{ + TableName: aws.String(im.table), + FilterExpression: aws.String("o = :ownerval"), + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":ownerval": {S: aws.String(im.ownerID)}, + }, + ProjectionExpression: aws.String("k,l"), + ConsistentRead: aws.Bool(true), + }, func(output *dynamodb.ScanOutput, last bool) bool { + for _, item := range output.Items { + labels[fromDynamoKey(item["k"])] = fromDynamoLabels(item["l"], im.ownerID) + } + return true + }) + if err != nil { + return fmt.Errorf("querying dynamodb: %w", err) + } + + im.labels = labels + return nil +} + +func fromDynamoKey(key *dynamodb.AttributeValue) endpoint.EndpointKey { + split := strings.SplitN(aws.StringValue(key.S), "#", 3) + return endpoint.EndpointKey{ + DNSName: split[0], + RecordType: split[1], + SetIdentifier: split[2], + } +} + +func toDynamoKey(key endpoint.EndpointKey) *dynamodb.AttributeValue { + return &dynamodb.AttributeValue{ + S: aws.String(fmt.Sprintf("%s#%s#%s", key.DNSName, key.RecordType, key.SetIdentifier)), + } +} + +func fromDynamoLabels(label *dynamodb.AttributeValue, owner string) endpoint.Labels { + labels := endpoint.NewLabels() + for k, v := range label.M { + labels[k] = aws.StringValue(v.S) + } + labels[endpoint.OwnerLabelKey] = owner + return labels +} + +func toDynamoLabels(labels endpoint.Labels) *dynamodb.AttributeValue { + labelMap := make(map[string]*dynamodb.AttributeValue, len(labels)) + for k, v := range labels { + if k == endpoint.OwnerLabelKey { + continue + } + labelMap[k] = &dynamodb.AttributeValue{S: aws.String(v)} + } + return &dynamodb.AttributeValue{M: labelMap} +} + +func (im *DynamoDBRegistry) appendUpdate(statements []*dynamodb.BatchStatementRequest, key endpoint.EndpointKey, old endpoint.Labels, new endpoint.Labels) []*dynamodb.BatchStatementRequest { + if len(old) == len(new) { + equal := true + for k, v := range old { + if newV, exists := new[k]; !exists || v != newV { + equal = false + break + } + } + if equal { + return statements + } + } + + return append(statements, &dynamodb.BatchStatementRequest{ + Statement: aws.String(fmt.Sprintf("UPDATE %q SET \"l\"=? WHERE \"k\"=?", im.table)), + Parameters: []*dynamodb.AttributeValue{ + toDynamoLabels(new), + toDynamoKey(key), + }, + }) +} + +func (im *DynamoDBRegistry) appendDelete(statements []*dynamodb.BatchStatementRequest, key endpoint.EndpointKey) []*dynamodb.BatchStatementRequest { + return append(statements, &dynamodb.BatchStatementRequest{ + Statement: aws.String(fmt.Sprintf("DELETE FROM %q WHERE \"k\"=? AND \"o\"=?", im.table)), + Parameters: []*dynamodb.AttributeValue{ + toDynamoKey(key), + {S: aws.String(im.ownerID)}, + }, + }) +} + +func (im *DynamoDBRegistry) executeStatements(ctx context.Context, statements []*dynamodb.BatchStatementRequest, handleErr func(request *dynamodb.BatchStatementRequest, response *dynamodb.BatchStatementResponse) error) error { + for len(statements) > 0 { + var chunk []*dynamodb.BatchStatementRequest + if len(statements) > 25 { + chunk = chunk[:25] + statements = statements[25:] + } else { + chunk = statements + statements = nil + } + + output, err := im.dynamodbAPI.BatchExecuteStatementWithContext(ctx, &dynamodb.BatchExecuteStatementInput{ + Statements: chunk, + }) + if err != nil { + return err + } + + for i, response := range output.Responses { + request := chunk[i] + if response.Error == nil { + op, _, _ := strings.Cut(*request.Statement, " ") + var key string + if op == "UPDATE" { + key = *request.Parameters[1].S + } else { + key = *request.Parameters[0].S + } + log.Infof("%s dynamodb record %q", op, key) + } else { + if err := handleErr(request, response); err != nil { + return err + } + } + } + } + return nil +} + +func (im *DynamoDBRegistry) addToCache(ep *endpoint.Endpoint) { + if im.recordsCache != nil { + im.recordsCache = append(im.recordsCache, ep) + } +} + +func (im *DynamoDBRegistry) removeFromCache(ep *endpoint.Endpoint) { + if im.recordsCache == nil || ep == nil { + return + } + + for i, e := range im.recordsCache { + if e.DNSName == ep.DNSName && e.RecordType == ep.RecordType && e.SetIdentifier == ep.SetIdentifier && e.Targets.Same(ep.Targets) { + // We found a match; delete the endpoint from the cache. + im.recordsCache = append(im.recordsCache[:i], im.recordsCache[i+1:]...) + return + } + } +} diff --git a/registry/dynamodb_test.go b/registry/dynamodb_test.go new file mode 100644 index 0000000000..2f62ea3bb1 --- /dev/null +++ b/registry/dynamodb_test.go @@ -0,0 +1,995 @@ +/* +Copyright 2023 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 registry + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/internal/testutils" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" + "sigs.k8s.io/external-dns/provider/inmemory" +) + +func TestDynamoDBRegistryNew(t *testing.T) { + api, p := newDynamoDBAPIStub(t, nil) + + _, err := NewDynamoDBRegistry(p, "test-owner", api, "test-table", time.Hour) + require.NoError(t, err) + + _, err = NewDynamoDBRegistry(p, "", api, "test-table", time.Hour) + require.EqualError(t, err, "owner id cannot be empty") + + _, err = NewDynamoDBRegistry(p, "test-owner", api, "", time.Hour) + require.EqualError(t, err, "table cannot be empty") +} + +func TestDynamoDBRegistryRecordsBadTable(t *testing.T) { + for _, tc := range []struct { + name string + setup func(desc *dynamodb.TableDescription) + expected string + }{ + { + name: "missing attribute k", + setup: func(desc *dynamodb.TableDescription) { + desc.AttributeDefinitions[0].AttributeName = aws.String("wrong") + }, + expected: "table \"test-table\" must have attribute \"k\" of type \"S\"", + }, + { + name: "wrong attribute type", + setup: func(desc *dynamodb.TableDescription) { + desc.AttributeDefinitions[0].AttributeType = aws.String("SS") + }, + expected: "table \"test-table\" attribute \"k\" must have type \"S\"", + }, + { + name: "wrong key", + setup: func(desc *dynamodb.TableDescription) { + desc.KeySchema[0].AttributeName = aws.String("wrong") + }, + expected: "table \"test-table\" must have hash key \"k\"", + }, + { + name: "has range key", + setup: func(desc *dynamodb.TableDescription) { + desc.AttributeDefinitions = append(desc.AttributeDefinitions, &dynamodb.AttributeDefinition{ + AttributeName: aws.String("o"), + AttributeType: aws.String("S"), + }) + desc.KeySchema = append(desc.KeySchema, &dynamodb.KeySchemaElement{ + AttributeName: aws.String("o"), + KeyType: aws.String("RANGE"), + }) + }, + expected: "table \"test-table\" must not have a range key", + }, + } { + t.Run(tc.name, func(t *testing.T) { + api, p := newDynamoDBAPIStub(t, nil) + tc.setup(&api.tableDescription) + + r, _ := NewDynamoDBRegistry(p, "test-owner", api, "test-table", time.Hour) + + _, err := r.Records(context.Background()) + assert.EqualError(t, err, tc.expected) + }) + } +} + +func TestDynamoDBRegistryRecords(t *testing.T) { + api, p := newDynamoDBAPIStub(t, nil) + + ctx := context.Background() + expectedRecords := []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + } + + r, _ := NewDynamoDBRegistry(p, "test-owner", api, "test-table", time.Hour) + records, err := r.Records(ctx) + require.Nil(t, err) + + assert.True(t, testutils.SameEndpoints(records, expectedRecords)) +} + +func TestDynamoDBRegistryApplyChanges(t *testing.T) { + for _, tc := range []struct { + name string + stubConfig DynamoDBStubConfig + changes plan.Changes + expectedError string + expectedRecords []*endpoint.Endpoint + }{ + { + name: "create", + changes: plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "new.test-zone.example.org", + Targets: endpoint.Targets{"new.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + SetIdentifier: "set-new", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/new-ingress", + }, + }, + }, + }, + stubConfig: DynamoDBStubConfig{ + ExpectInsert: map[string]map[string]string{ + "new.test-zone.example.org#CNAME#set-new": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"}, + }, + ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"), + }, + expectedRecords: []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + { + DNSName: "new.test-zone.example.org", + Targets: endpoint.Targets{"new.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + SetIdentifier: "set-new", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/new-ingress", + }, + }, + }, + }, + { + name: "create orphaned", + changes: plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "quux.test-zone.example.org", + Targets: endpoint.Targets{"5.5.5.5"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/quux-ingress", + }, + }, + }, + }, + stubConfig: DynamoDBStubConfig{}, + expectedRecords: []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + { + DNSName: "quux.test-zone.example.org", + Targets: endpoint.Targets{"5.5.5.5"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/quux-ingress", + }, + }, + }, + }, + { + name: "create orphaned change", + changes: plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "quux.test-zone.example.org", + Targets: endpoint.Targets{"5.5.5.5"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/new-ingress", + }, + }, + }, + }, + stubConfig: DynamoDBStubConfig{ + ExpectUpdate: map[string]map[string]string{ + "quux.test-zone.example.org#A#set-2": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"}, + }, + }, + expectedRecords: []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + { + DNSName: "quux.test-zone.example.org", + Targets: endpoint.Targets{"5.5.5.5"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/new-ingress", + }, + }, + }, + }, + { + name: "create duplicate", + changes: plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "new.test-zone.example.org", + Targets: endpoint.Targets{"new.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + SetIdentifier: "set-new", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/new-ingress", + }, + }, + }, + }, + stubConfig: DynamoDBStubConfig{ + ExpectInsertError: map[string]string{ + "new.test-zone.example.org#CNAME#set-new": "DuplicateItem", + }, + ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"), + }, + expectedRecords: []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + }, + }, + { + name: "create error", + changes: plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "new.test-zone.example.org", + Targets: endpoint.Targets{"new.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + SetIdentifier: "set-new", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/new-ingress", + }, + }, + }, + }, + stubConfig: DynamoDBStubConfig{ + ExpectInsertError: map[string]string{ + "new.test-zone.example.org#CNAME#set-new": "TestingError", + }, + }, + expectedError: "inserting dynamodb record \"new.test-zone.example.org#CNAME#set-new\": TestingError: testing error", + expectedRecords: []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + }, + }, + { + name: "update", + changes: plan.Changes{ + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"new-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + }, + }, + stubConfig: DynamoDBStubConfig{ + ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"), + }, + expectedRecords: []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"new-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + }, + }, + { + name: "update change", + changes: plan.Changes{ + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"new-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/new-ingress", + }, + }, + }, + }, + stubConfig: DynamoDBStubConfig{ + ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"), + ExpectUpdate: map[string]map[string]string{ + "bar.test-zone.example.org#CNAME#": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"}, + }, + }, + expectedRecords: []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"new-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/new-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + }, + }, + { + name: "update error", + changes: plan.Changes{ + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"new-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/new-ingress", + }, + }, + }, + }, + stubConfig: DynamoDBStubConfig{ + ExpectUpdateError: map[string]string{ + "bar.test-zone.example.org#CNAME#": "TestingError", + }, + }, + expectedError: "updating dynamodb record \"bar.test-zone.example.org#CNAME#\": TestingError: testing error", + expectedRecords: []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + }, + }, + { + name: "delete", + changes: plan.Changes{ + Delete: []*endpoint.Endpoint{ + { + DNSName: "bar.test-zone.example.org", + Targets: endpoint.Targets{"my-domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + }, + }, + stubConfig: DynamoDBStubConfig{ + ExpectDelete: sets.New("bar.test-zone.example.org#CNAME#", "quux.test-zone.example.org#A#set-2"), + }, + expectedRecords: []*endpoint.Endpoint{ + { + DNSName: "foo.test-zone.example.org", + Targets: endpoint.Targets{"foo.loadbalancer.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-1", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/my-ingress", + }, + }, + { + DNSName: "baz.test-zone.example.org", + Targets: endpoint.Targets{"2.2.2.2"}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "set-2", + Labels: map[string]string{ + endpoint.OwnerLabelKey: "test-owner", + endpoint.ResourceLabelKey: "ingress/default/other-ingress", + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + api, p := newDynamoDBAPIStub(t, &tc.stubConfig) + ctx := context.Background() + + r, _ := NewDynamoDBRegistry(p, "test-owner", api, "test-table", time.Hour) + _, err := r.Records(ctx) + require.Nil(t, err) + + err = r.ApplyChanges(ctx, &tc.changes) + if tc.expectedError == "" { + assert.Nil(t, err) + } else { + assert.EqualError(t, err, tc.expectedError) + } + + assert.Empty(t, tc.stubConfig.ExpectInsert, "all expected inserts made") + assert.Empty(t, tc.stubConfig.ExpectDelete, "all expected deletions made") + + records, err := r.Records(ctx) + require.Nil(t, err) + assert.True(t, testutils.SameEndpoints(records, tc.expectedRecords)) + + r.recordsCache = nil + records, err = r.Records(ctx) + require.Nil(t, err) + assert.True(t, testutils.SameEndpoints(records, tc.expectedRecords)) + if tc.expectedError == "" { + assert.Empty(t, r.orphanedLabels) + } + }) + } +} + +// DynamoDBAPIStub is a minimal implementation of DynamoDBAPI, used primarily for unit testing. +type DynamoDBStub struct { + t *testing.T + stubConfig *DynamoDBStubConfig + tableDescription dynamodb.TableDescription + changesApplied bool +} + +type DynamoDBStubConfig struct { + ExpectInsert map[string]map[string]string + ExpectInsertError map[string]string + ExpectUpdate map[string]map[string]string + ExpectUpdateError map[string]string + ExpectDelete sets.Set[string] +} + +type wrappedProvider struct { + provider.Provider + stub *DynamoDBStub +} + +func (w *wrappedProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + assert.False(w.stub.t, w.stub.changesApplied, "ApplyChanges already called") + w.stub.changesApplied = true + return w.Provider.ApplyChanges(ctx, changes) +} + +func newDynamoDBAPIStub(t *testing.T, stubConfig *DynamoDBStubConfig) (*DynamoDBStub, provider.Provider) { + stub := &DynamoDBStub{ + t: t, + stubConfig: stubConfig, + tableDescription: dynamodb.TableDescription{ + AttributeDefinitions: []*dynamodb.AttributeDefinition{ + { + AttributeName: aws.String("k"), + AttributeType: aws.String("S"), + }, + }, + KeySchema: []*dynamodb.KeySchemaElement{ + { + AttributeName: aws.String("k"), + KeyType: aws.String("HASH"), + }, + }, + }, + } + p := inmemory.NewInMemoryProvider() + _ = p.CreateZone(testZone) + _ = p.ApplyChanges(context.Background(), &plan.Changes{ + Create: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo.test-zone.example.org", endpoint.RecordTypeCNAME, "foo.loadbalancer.com"), + endpoint.NewEndpoint("bar.test-zone.example.org", endpoint.RecordTypeCNAME, "my-domain.com"), + endpoint.NewEndpoint("baz.test-zone.example.org", endpoint.RecordTypeA, "1.1.1.1").WithSetIdentifier("set-1"), + endpoint.NewEndpoint("baz.test-zone.example.org", endpoint.RecordTypeA, "2.2.2.2").WithSetIdentifier("set-2"), + }, + }) + return stub, &wrappedProvider{ + Provider: p, + stub: stub, + } +} + +func (r *DynamoDBStub) DescribeTableWithContext(ctx aws.Context, input *dynamodb.DescribeTableInput, opts ...request.Option) (*dynamodb.DescribeTableOutput, error) { + assert.NotNil(r.t, ctx) + assert.Equal(r.t, "test-table", *input.TableName, "table name") + return &dynamodb.DescribeTableOutput{ + Table: &r.tableDescription, + }, nil +} + +func (r *DynamoDBStub) ScanPagesWithContext(ctx aws.Context, input *dynamodb.ScanInput, fn func(*dynamodb.ScanOutput, bool) bool, opts ...request.Option) error { + assert.NotNil(r.t, ctx) + assert.Equal(r.t, "test-table", *input.TableName, "table name") + assert.Equal(r.t, "o = :ownerval", *input.FilterExpression) + assert.Len(r.t, input.ExpressionAttributeValues, 1) + assert.Equal(r.t, "test-owner", *input.ExpressionAttributeValues[":ownerval"].S) + assert.Equal(r.t, "k,l", *input.ProjectionExpression) + assert.True(r.t, *input.ConsistentRead) + fn(&dynamodb.ScanOutput{ + Items: []map[string]*dynamodb.AttributeValue{ + { + "k": &dynamodb.AttributeValue{S: aws.String("bar.test-zone.example.org#CNAME#")}, + "l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{ + endpoint.ResourceLabelKey: {S: aws.String("ingress/default/my-ingress")}, + }}, + }, + { + "k": &dynamodb.AttributeValue{S: aws.String("baz.test-zone.example.org#A#set-1")}, + "l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{ + endpoint.ResourceLabelKey: {S: aws.String("ingress/default/my-ingress")}, + }}, + }, + { + "k": &dynamodb.AttributeValue{S: aws.String("baz.test-zone.example.org#A#set-2")}, + "l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{ + endpoint.ResourceLabelKey: {S: aws.String("ingress/default/other-ingress")}, + }}, + }, + { + "k": &dynamodb.AttributeValue{S: aws.String("quux.test-zone.example.org#A#set-2")}, + "l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{ + endpoint.ResourceLabelKey: {S: aws.String("ingress/default/quux-ingress")}, + }}, + }, + }, + }, true) + return nil +} + +func (r *DynamoDBStub) BatchExecuteStatementWithContext(context aws.Context, input *dynamodb.BatchExecuteStatementInput, option ...request.Option) (*dynamodb.BatchExecuteStatementOutput, error) { + assert.NotNil(r.t, context) + hasDelete := strings.HasPrefix(strings.ToLower(aws.StringValue(input.Statements[0].Statement)), "delete") + assert.Equal(r.t, hasDelete, r.changesApplied, "delete after provider changes, everything else before") + assert.LessOrEqual(r.t, len(input.Statements), 25) + responses := make([]*dynamodb.BatchStatementResponse, 0, len(input.Statements)) + + for _, statement := range input.Statements { + assert.Equal(r.t, hasDelete, strings.HasPrefix(strings.ToLower(aws.StringValue(statement.Statement)), "delete")) + switch aws.StringValue(statement.Statement) { + case "DELETE FROM \"test-table\" WHERE \"k\"=? AND \"o\"=?": + assert.True(r.t, r.changesApplied, "unexpected delete before provider changes") + + key := aws.StringValue(statement.Parameters[0].S) + assert.True(r.t, r.stubConfig.ExpectDelete.Has(key), "unexpected delete for key %q", key) + r.stubConfig.ExpectDelete.Delete(key) + + assert.Equal(r.t, "test-owner", aws.StringValue(statement.Parameters[1].S)) + + responses = append(responses, &dynamodb.BatchStatementResponse{}) + + case "INSERT INTO \"test-table\" VALUE {'k':?, 'o':?, 'l':?}": + assert.False(r.t, r.changesApplied, "unexpected insert after provider changes") + + key := aws.StringValue(statement.Parameters[0].S) + if code, exists := r.stubConfig.ExpectInsertError[key]; exists { + delete(r.stubConfig.ExpectInsertError, key) + responses = append(responses, &dynamodb.BatchStatementResponse{ + Error: &dynamodb.BatchStatementError{ + Code: aws.String(code), + Message: aws.String("testing error"), + }, + }) + break + } + + expectedLabels, found := r.stubConfig.ExpectInsert[key] + assert.True(r.t, found, "unexpected insert for key %q", key) + delete(r.stubConfig.ExpectInsert, key) + + assert.Equal(r.t, "test-owner", aws.StringValue(statement.Parameters[1].S)) + + for label, attribute := range statement.Parameters[2].M { + value := aws.StringValue(attribute.S) + expectedValue, found := expectedLabels[label] + assert.True(r.t, found, "insert for key %q has unexpected label %q", key, label) + delete(expectedLabels, label) + assert.Equal(r.t, expectedValue, value, "insert for key %q label %q value", key, label) + } + + for label := range expectedLabels { + r.t.Errorf("insert for key %q did not get expected label %q", key, label) + } + + responses = append(responses, &dynamodb.BatchStatementResponse{}) + + case "UPDATE \"test-table\" SET \"l\"=? WHERE \"k\"=?": + assert.False(r.t, r.changesApplied, "unexpected update after provider changes") + + key := aws.StringValue(statement.Parameters[1].S) + if code, exists := r.stubConfig.ExpectUpdateError[key]; exists { + delete(r.stubConfig.ExpectInsertError, key) + responses = append(responses, &dynamodb.BatchStatementResponse{ + Error: &dynamodb.BatchStatementError{ + Code: aws.String(code), + Message: aws.String("testing error"), + }, + }) + break + } + + expectedLabels, found := r.stubConfig.ExpectUpdate[key] + assert.True(r.t, found, "unexpected update for key %q", key) + delete(r.stubConfig.ExpectUpdate, key) + + for label, attribute := range statement.Parameters[0].M { + value := aws.StringValue(attribute.S) + expectedValue, found := expectedLabels[label] + assert.True(r.t, found, "update for key %q has unexpected label %q", key, label) + delete(expectedLabels, label) + assert.Equal(r.t, expectedValue, value, "update for key %q label %q value", key, label) + } + + for label := range expectedLabels { + r.t.Errorf("update for key %q did not get expected label %q", key, label) + } + + responses = append(responses, &dynamodb.BatchStatementResponse{}) + + default: + r.t.Errorf("unexpected statement: %s", aws.StringValue(statement.Statement)) + } + } + + return &dynamodb.BatchExecuteStatementOutput{ + Responses: responses, + }, nil +} diff --git a/registry/noop.go b/registry/noop.go index d48cd82fe0..73257730cd 100644 --- a/registry/noop.go +++ b/registry/noop.go @@ -45,11 +45,6 @@ func (im *NoopRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, erro return im.provider.Records(ctx) } -// MissingRecords returns nil because there is no missing records for Noop registry -func (im *NoopRegistry) MissingRecords() []*endpoint.Endpoint { - return nil -} - // ApplyChanges propagates changes to the dns provider func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { return im.provider.ApplyChanges(ctx, changes) diff --git a/registry/registry.go b/registry/registry.go index 7f219a8463..fa39fb8ec0 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -35,7 +35,6 @@ type Registry interface { PropertyValuesEqual(attribute string, previous string, current string) bool AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint GetDomainFilter() endpoint.DomainFilterInterface - MissingRecords() []*endpoint.Endpoint } // TODO(ideahitme): consider moving this to Plan diff --git a/registry/txt.go b/registry/txt.go index 48dd028d5c..3300b5ef1b 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -30,7 +30,10 @@ import ( "sigs.k8s.io/external-dns/provider" ) -const recordTemplate = "%{record_type}" +const ( + recordTemplate = "%{record_type}" + providerSpecificForceUpdate = "txt/force-update" +) // TXTRegistry implements registry interface with ownership implemented via associated TXT records type TXTRegistry struct { @@ -50,17 +53,26 @@ type TXTRegistry struct { managedRecordTypes []string - // 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") @@ -75,6 +87,8 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st cacheInterval: cacheInterval, wildcardReplacement: txtWildcardReplacement, managedRecordTypes: managedRecordTypes, + txtEncryptEnabled: txtEncryptEnabled, + txtEncryptAESKey: txtEncryptAESKey, }, nil } @@ -103,7 +117,6 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error } endpoints := []*endpoint.Endpoint{} - missingEndpoints := []*endpoint.Endpoint{} labelMap := map[string]endpoint.Labels{} txtRecordsMap := map[string]struct{}{} @@ -114,7 +127,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 @@ -160,17 +173,11 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error if plan.IsManagedRecord(ep.RecordType, im.managedRecordTypes) { // Get desired TXT records and detect the missing ones desiredTXTs := im.generateTXTRecord(ep) - missingDesiredTXTs := []*endpoint.Endpoint{} for _, desiredTXT := range desiredTXTs { if _, exists := txtRecordsMap[desiredTXT.DNSName]; !exists { - missingDesiredTXTs = append(missingDesiredTXTs, desiredTXT) + ep.WithProviderSpecific(providerSpecificForceUpdate, "true") } } - if len(desiredTXTs) > len(missingDesiredTXTs) { - // Add missing TXT records only if those are managed (by externaldns) ones. - // The unmanaged record has both of the desired TXT records missing. - missingEndpoints = append(missingEndpoints, missingDesiredTXTs...) - } } } } @@ -181,17 +188,9 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error im.recordsCacheRefreshTime = time.Now() } - im.missingTXTRecords = missingEndpoints - return endpoints, nil } -// MissingRecords returns the TXT record to be created. -// The missing records are collected during the run of Records method. -func (im *TXTRegistry) MissingRecords() []*endpoint.Endpoint { - return im.missingTXTRecords -} - // generateTXTRecord generates both "old" and "new" TXT records. // Once we decide to drop old format we need to drop toTXTName() and rename toNewTXTName func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpoint { @@ -205,7 +204,7 @@ func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpo if r.RecordType != endpoint.RecordTypeAAAA { // old TXT record format - txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)) + 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 @@ -213,9 +212,8 @@ func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpo 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 @@ -296,10 +294,6 @@ func (im *TXTRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoi return im.provider.AdjustEndpoints(endpoints) } -/** - TXT registry specific private methods -*/ - /** nameMapper is the interface for mapping between the endpoint for the source and the endpoint for the TXT record. @@ -454,7 +448,6 @@ func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) { func (im *TXTRegistry) removeFromCache(ep *endpoint.Endpoint) { if im.recordsCache == nil || ep == nil { - // return early. return } diff --git a/registry/txt_test.go b/registry/txt_test.go index 2f7297fb11..71e56b5b75 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) @@ -203,13 +213,13 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) { }, } - 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, "wc", []string{}, false, nil) records, _ = r.Records(ctx) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) @@ -328,13 +338,13 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) { }, } - 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)) @@ -429,7 +439,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { }, } - 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)) @@ -472,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{ @@ -561,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"), @@ -605,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"), @@ -671,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{ @@ -775,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{ @@ -865,6 +875,12 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) { // owner was added from the TXT record's target endpoint.OwnerLabelKey: "owner", }, + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "txt/force-update", + Value: "true", + }, + }, }, { DNSName: "oldformat2.test-zone.example.org", @@ -873,6 +889,12 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) { Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "txt/force-update", + Value: "true", + }, + }, }, { DNSName: "newformat.test-zone.example.org", @@ -921,32 +943,10 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) { }, } - expectedMissingRecords := []*endpoint.Endpoint{ - { - DNSName: "cname-oldformat.test-zone.example.org", - // owner is taken from the source record (A, CNAME, etc.) - Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, - RecordType: endpoint.RecordTypeTXT, - Labels: endpoint.Labels{ - endpoint.OwnedRecordLabelKey: "oldformat.test-zone.example.org", - }, - }, - { - DNSName: "a-oldformat2.test-zone.example.org", - Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, - RecordType: endpoint.RecordTypeTXT, - Labels: endpoint.Labels{ - endpoint.OwnedRecordLabelKey: "oldformat2.test-zone.example.org", - }, - }, - } - - 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() assert.True(t, testutils.SameEndpoints(records, expectedRecords)) - assert.True(t, testutils.SameEndpoints(missingRecords, expectedMissingRecords)) } func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { @@ -978,6 +978,12 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { // owner was added from the TXT record's target endpoint.OwnerLabelKey: "owner", }, + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "txt/force-update", + Value: "true", + }, + }, }, { DNSName: "oldformat2.test-zone.example.org", @@ -986,6 +992,12 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "txt/force-update", + Value: "true", + }, + }, }, { DNSName: "newformat.test-zone.example.org", @@ -1025,32 +1037,10 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { }, } - expectedMissingRecords := []*endpoint.Endpoint{ - { - DNSName: "txt.cname-oldformat.test-zone.example.org", - // owner is taken from the source record (A, CNAME, etc.) - Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, - RecordType: endpoint.RecordTypeTXT, - Labels: endpoint.Labels{ - endpoint.OwnedRecordLabelKey: "oldformat.test-zone.example.org", - }, - }, - { - DNSName: "txt.a-oldformat2.test-zone.example.org", - Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""}, - RecordType: endpoint.RecordTypeTXT, - Labels: endpoint.Labels{ - endpoint.OwnedRecordLabelKey: "oldformat2.test-zone.example.org", - }, - }, - } - - 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() assert.True(t, testutils.SameEndpoints(records, expectedRecords)) - assert.True(t, testutils.SameEndpoints(missingRecords, expectedMissingRecords)) } func TestCacheMethods(t *testing.T) { @@ -1199,7 +1189,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{ @@ -1275,7 +1265,7 @@ 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) } @@ -1294,7 +1284,7 @@ func TestGenerateTXTForAAAA(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) } @@ -1311,7 +1301,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 bc6e19abf9..1953b76ca9 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/crd.go b/source/crd.go index b32b93e6e3..10be3fccea 100644 --- a/source/crd.go +++ b/source/crd.go @@ -79,7 +79,7 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiS } apiResourceList, err := client.Discovery().ServerResourcesForGroupVersion(groupVersion.String()) if err != nil { - return nil, nil, fmt.Errorf("error listing resources in GroupVersion %q: %s", groupVersion.String(), err) + return nil, nil, fmt.Errorf("error listing resources in GroupVersion %q: %w", groupVersion.String(), err) } var crdAPIResource *metav1.APIResource diff --git a/source/fake.go b/source/fake.go index 9173f4d0a1..2041c116e4 100644 --- a/source/fake.go +++ b/source/fake.go @@ -25,7 +25,6 @@ import ( "fmt" "math/rand" "net" - "time" "sigs.k8s.io/external-dns/endpoint" ) @@ -40,10 +39,6 @@ const ( defaultFQDNTemplate = "example.com" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - // NewFakeSource creates a new fakeSource with the given config. func NewFakeSource(fqdnTemplate string) (Source, error) { if fqdnTemplate == "" { diff --git a/source/gloo_proxy.go b/source/gloo_proxy.go index 845ccdee05..09e96ead05 100644 --- a/source/gloo_proxy.go +++ b/source/gloo_proxy.go @@ -64,20 +64,35 @@ type proxySpecHTTPListener struct { } type proxyVirtualHost struct { - Domains []string `json:"domains,omitempty"` - Metadata proxyVirtualHostMetadata `json:"metadata,omitempty"` + Domains []string `json:"domains,omitempty"` + Metadata proxyVirtualHostMetadata `json:"metadata,omitempty"` + MetadataStatic proxyVirtualHostMetadataStatic `json:"metadataStatic,omitempty"` } type proxyVirtualHostMetadata struct { Source []proxyVirtualHostMetadataSource `json:"sources,omitempty"` } +type proxyVirtualHostMetadataStatic struct { + Source []proxyVirtualHostMetadataStaticSource `json:"sources,omitempty"` +} + type proxyVirtualHostMetadataSource struct { Kind string `json:"kind,omitempty"` Name string `json:"name,omitempty"` Namespace string `json:"namespace,omitempty"` } +type proxyVirtualHostMetadataStaticSource struct { + ResourceKind string `json:"resourceKind,omitempty"` + ResourceRef proxyVirtualHostMetadataSourceResourceRef `json:"resourceRef,omitempty"` +} + +type proxyVirtualHostMetadataSourceResourceRef struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + type glooSource struct { dynamicKubeClient dynamic.Interface kubeClient kubernetes.Interface @@ -165,6 +180,18 @@ func (gs *glooSource) annotationsFromProxySource(ctx context.Context, virtualHos } } } + for _, src := range virtualHost.MetadataStatic.Source { + kind := sourceKind(src.ResourceKind) + if kind != nil { + source, err := gs.dynamicKubeClient.Resource(*kind).Namespace(src.ResourceRef.Namespace).Get(ctx, src.ResourceRef.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + for key, value := range source.GetAnnotations() { + annotations[key] = value + } + } + } return annotations, nil } diff --git a/source/gloo_proxy_test.go b/source/gloo_proxy_test.go index c1005fec2b..385d1a9692 100644 --- a/source/gloo_proxy_test.go +++ b/source/gloo_proxy_test.go @@ -211,6 +211,97 @@ var externalProxySource = metav1.PartialObjectMetadata{ }, } +// Proxy with metadata static test +var proxyMetadataStatic = proxy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: proxyGVR.GroupVersion().String(), + Kind: "Proxy", + }, + Metadata: metav1.ObjectMeta{ + Name: "internal-static", + Namespace: defaultGlooNamespace, + }, + Spec: proxySpec{ + Listeners: []proxySpecListener{ + { + HTTPListener: proxySpecHTTPListener{ + VirtualHosts: []proxyVirtualHost{ + { + Domains: []string{"f.test", "g.test"}, + MetadataStatic: proxyVirtualHostMetadataStatic{ + Source: []proxyVirtualHostMetadataStaticSource{ + { + ResourceKind: "*v1.Unknown", + ResourceRef: proxyVirtualHostMetadataSourceResourceRef{ + Name: "my-unknown-svc", + Namespace: "unknown", + }, + }, + }, + }, + }, + { + Domains: []string{"h.test"}, + MetadataStatic: proxyVirtualHostMetadataStatic{ + Source: []proxyVirtualHostMetadataStaticSource{ + { + ResourceKind: "*v1.VirtualService", + ResourceRef: proxyVirtualHostMetadataSourceResourceRef{ + Name: "my-internal-static-svc", + Namespace: "internal-static", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +} + +var proxyMetadataStaticSvc = corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: proxyMetadataStatic.Metadata.Name, + Namespace: proxyMetadataStatic.Metadata.Namespace, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + }, + Status: corev1.ServiceStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: []corev1.LoadBalancerIngress{ + { + IP: "203.0.115.1", + }, + { + IP: "203.0.115.2", + }, + { + IP: "203.0.115.3", + }, + }, + }, + }, +} + +var proxyMetadataStaticSource = metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: virtualServiceGVR.GroupVersion().String(), + Kind: "VirtualService", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: proxyMetadataStatic.Spec.Listeners[0].HTTPListener.VirtualHosts[1].MetadataStatic.Source[0].ResourceRef.Name, + Namespace: proxyMetadataStatic.Spec.Listeners[0].HTTPListener.VirtualHosts[1].MetadataStatic.Source[0].ResourceRef.Namespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/ttl": "420", + "external-dns.alpha.kubernetes.io/aws-geolocation-country-code": "ES", + "external-dns.alpha.kubernetes.io/set-identifier": "identifier", + }, + }, +} + func TestGlooSource(t *testing.T) { t.Parallel() @@ -226,9 +317,11 @@ func TestGlooSource(t *testing.T) { internalProxyUnstructured := unstructured.Unstructured{} externalProxyUnstructured := unstructured.Unstructured{} + proxyMetadataStaticUnstructured := unstructured.Unstructured{} internalProxySourceUnstructured := unstructured.Unstructured{} externalProxySourceUnstructured := unstructured.Unstructured{} + proxyMetadataStaticSourceUnstructured := unstructured.Unstructured{} internalProxyAsJSON, err := json.Marshal(internalProxy) assert.NoError(t, err) @@ -236,39 +329,53 @@ func TestGlooSource(t *testing.T) { externalProxyAsJSON, err := json.Marshal(externalProxy) assert.NoError(t, err) + proxyMetadataStaticAsJSON, err := json.Marshal(proxyMetadataStatic) + assert.NoError(t, err) + internalProxySvcAsJSON, err := json.Marshal(internalProxySource) assert.NoError(t, err) externalProxySvcAsJSON, err := json.Marshal(externalProxySource) assert.NoError(t, err) + proxyMetadataStaticSvcAsJSON, err := json.Marshal(proxyMetadataStaticSource) + assert.NoError(t, err) + assert.NoError(t, internalProxyUnstructured.UnmarshalJSON(internalProxyAsJSON)) assert.NoError(t, externalProxyUnstructured.UnmarshalJSON(externalProxyAsJSON)) + assert.NoError(t, proxyMetadataStaticUnstructured.UnmarshalJSON(proxyMetadataStaticAsJSON)) assert.NoError(t, internalProxySourceUnstructured.UnmarshalJSON(internalProxySvcAsJSON)) assert.NoError(t, externalProxySourceUnstructured.UnmarshalJSON(externalProxySvcAsJSON)) + assert.NoError(t, proxyMetadataStaticSourceUnstructured.UnmarshalJSON(proxyMetadataStaticSvcAsJSON)) // Create proxy resources _, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &internalProxyUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) _, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &externalProxyUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) + _, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &proxyMetadataStaticUnstructured, metav1.CreateOptions{}) + assert.NoError(t, err) // Create proxy source _, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(internalProxySource.Namespace).Create(context.Background(), &internalProxySourceUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) _, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(externalProxySource.Namespace).Create(context.Background(), &externalProxySourceUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) + _, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(proxyMetadataStaticSource.Namespace).Create(context.Background(), &proxyMetadataStaticSourceUnstructured, metav1.CreateOptions{}) + assert.NoError(t, err) // Create proxy service resources _, err = fakeKubernetesClient.CoreV1().Services(internalProxySvc.GetNamespace()).Create(context.Background(), &internalProxySvc, metav1.CreateOptions{}) assert.NoError(t, err) _, err = fakeKubernetesClient.CoreV1().Services(externalProxySvc.GetNamespace()).Create(context.Background(), &externalProxySvc, metav1.CreateOptions{}) assert.NoError(t, err) + _, err = fakeKubernetesClient.CoreV1().Services(proxyMetadataStaticSvc.GetNamespace()).Create(context.Background(), &proxyMetadataStaticSvc, metav1.CreateOptions{}) + assert.NoError(t, err) endpoints, err := source.Endpoints(context.Background()) assert.NoError(t, err) - assert.Len(t, endpoints, 5) + assert.Len(t, endpoints, 8) assert.ElementsMatch(t, endpoints, []*endpoint.Endpoint{ { DNSName: "a.test", @@ -322,5 +429,35 @@ func TestGlooSource(t *testing.T) { }, }, }, + { + DNSName: "f.test", + Targets: []string{proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP}, + RecordType: endpoint.RecordTypeA, + RecordTTL: 0, + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "g.test", + Targets: []string{proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP}, + RecordType: endpoint.RecordTypeA, + RecordTTL: 0, + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "h.test", + Targets: []string{proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP}, + RecordType: endpoint.RecordTypeA, + SetIdentifier: "identifier", + RecordTTL: 420, + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: "aws/geolocation-country-code", + Value: "ES", + }, + }, + }, }) } diff --git a/source/ingress.go b/source/ingress.go index 76d5429e7f..af7a9dc990 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 cdd23505a7..2be3eddfd0 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) @@ -374,6 +388,7 @@ func testIngressEndpoints(t *testing.T) { ignoreIngressTLSSpec bool ignoreIngressRulesSpec bool ingressLabelSelector labels.Selector + ingressClassNames []string }{ { title: "no ingress", @@ -1220,6 +1235,116 @@ func testIngressEndpoints(t *testing.T) { }, }, }, + { + 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"}, + }, + }, + }, { ingressLabelSelector: labels.SelectorFromSet(labels.Set{"app": "web-external"}), title: "ingress with matching labels", @@ -1283,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()) @@ -1298,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 { @@ -1317,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.go b/source/istio_gateway.go index 4f5a68ddf9..76c1094cf4 100644 --- a/source/istio_gateway.go +++ b/source/istio_gateway.go @@ -149,7 +149,7 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e // apply template if host is missing on gateway if (sc.combineFQDNAnnotation || len(gwHostnames) == 0) && sc.fqdnTemplate != nil { - iHostnames, err := execTemplate(sc.fqdnTemplate, &gateway) + iHostnames, err := execTemplate(sc.fqdnTemplate, gateway) if err != nil { return nil, err } @@ -196,7 +196,7 @@ func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) { } // filterByAnnotations filters a list of configs by a given annotation selector. -func (sc *gatewaySource) filterByAnnotations(gateways []networkingv1alpha3.Gateway) ([]networkingv1alpha3.Gateway, error) { +func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gateway) ([]*networkingv1alpha3.Gateway, error) { labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) if err != nil { return nil, err @@ -211,7 +211,7 @@ func (sc *gatewaySource) filterByAnnotations(gateways []networkingv1alpha3.Gatew return gateways, nil } - var filteredList []networkingv1alpha3.Gateway + var filteredList []*networkingv1alpha3.Gateway for _, gw := range gateways { // convert the annotations to an equivalent label selector @@ -226,13 +226,13 @@ func (sc *gatewaySource) filterByAnnotations(gateways []networkingv1alpha3.Gatew return filteredList, nil } -func (sc *gatewaySource) setResourceLabel(gateway networkingv1alpha3.Gateway, endpoints []*endpoint.Endpoint) { +func (sc *gatewaySource) setResourceLabel(gateway *networkingv1alpha3.Gateway, endpoints []*endpoint.Endpoint) { for _, ep := range endpoints { ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("gateway/%s/%s", gateway.Namespace, gateway.Name) } } -func (sc *gatewaySource) targetsFromGateway(gateway networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { +func (sc *gatewaySource) targetsFromGateway(gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { targets = getTargetsFromTargetAnnotation(gateway.Annotations) if len(targets) > 0 { return @@ -262,7 +262,7 @@ func (sc *gatewaySource) targetsFromGateway(gateway networkingv1alpha3.Gateway) } // endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object -func (sc *gatewaySource) endpointsFromGateway(hostnames []string, gateway networkingv1alpha3.Gateway) ([]*endpoint.Endpoint, error) { +func (sc *gatewaySource) endpointsFromGateway(hostnames []string, gateway *networkingv1alpha3.Gateway) ([]*endpoint.Endpoint, error) { var endpoints []*endpoint.Endpoint annotations := gateway.Annotations @@ -289,7 +289,7 @@ func (sc *gatewaySource) endpointsFromGateway(hostnames []string, gateway networ return endpoints, nil } -func (sc *gatewaySource) hostNamesFromGateway(gateway networkingv1alpha3.Gateway) ([]string, error) { +func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1alpha3.Gateway) ([]string, error) { var hostnames []string for _, server := range gateway.Spec.Servers { for _, host := range server.Hosts { diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go index 535972a34b..7ca79623ca 100644 --- a/source/istio_gateway_test.go +++ b/source/istio_gateway_test.go @@ -1192,7 +1192,7 @@ func testGatewayEndpoints(t *testing.T) { fakeIstioClient := istiofake.NewSimpleClientset() for _, config := range ti.configItems { gatewayCfg := config.Config() - _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(ti.targetNamespace).Create(context.Background(), &gatewayCfg, metav1.CreateOptions{}) + _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(ti.targetNamespace).Create(context.Background(), gatewayCfg, metav1.CreateOptions{}) require.NoError(t, err) } @@ -1301,8 +1301,8 @@ type fakeGatewayConfig struct { selector map[string]string } -func (c fakeGatewayConfig) Config() networkingv1alpha3.Gateway { - gw := networkingv1alpha3.Gateway{ +func (c fakeGatewayConfig) Config() *networkingv1alpha3.Gateway { + gw := &networkingv1alpha3.Gateway{ ObjectMeta: metav1.ObjectMeta{ Name: c.name, Namespace: c.namespace, diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go index 934490a803..e6e87e6f5a 100644 --- a/source/istio_virtualservice.go +++ b/source/istio_virtualservice.go @@ -23,12 +23,12 @@ import ( "strings" "text/template" - networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" - log "github.com/sirupsen/logrus" + networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" istioclient "istio.io/client-go/pkg/clientset/versioned" istioinformers "istio.io/client-go/pkg/informers/externalversions" networkingv1alpha3informer "istio.io/client-go/pkg/informers/externalversions/networking/v1alpha3" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" kubeinformers "k8s.io/client-go/informers" @@ -203,7 +203,10 @@ func (sc *virtualServiceSource) getGateway(ctx context.Context, gatewayStr strin } gateway, err := sc.istioClient.NetworkingV1alpha3().Gateways(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { + if errors.IsNotFound(err) { + log.Warnf("VirtualService (%s/%s) references non-existent gateway: %s ", virtualService.Namespace, virtualService.Name, gatewayStr) + return nil, nil + } else if err != nil { log.Errorf("Failed retrieving gateway %s referenced by VirtualService %s/%s: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err) return nil, err } @@ -211,7 +214,6 @@ func (sc *virtualServiceSource) getGateway(ctx context.Context, gatewayStr strin log.Debugf("Gateway %s referenced by VirtualService %s/%s not found: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err) return nil, nil } - return gateway, nil } diff --git a/source/istio_virtualservice_test.go b/source/istio_virtualservice_test.go index a510094e75..551be67061 100644 --- a/source/istio_virtualservice_test.go +++ b/source/istio_virtualservice_test.go @@ -18,19 +18,23 @@ package source import ( "context" + "fmt" "testing" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "istio.io/api/meta/v1alpha1" istionetworking "istio.io/api/networking/v1alpha3" networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" istiofake "istio.io/client-go/pkg/clientset/versioned/fake" + fakenetworking3 "istio.io/client-go/pkg/clientset/versioned/typed/networking/v1alpha3/fake" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" + k8sclienttesting "k8s.io/client-go/testing" "sigs.k8s.io/external-dns/endpoint" ) @@ -42,7 +46,7 @@ type VirtualServiceSuite struct { suite.Suite source Source lbServices []*v1.Service - gwconfig networkingv1alpha3.Gateway + gwconfig *networkingv1alpha3.Gateway vsconfig *networkingv1alpha3.VirtualService } @@ -76,7 +80,7 @@ func (suite *VirtualServiceSuite) SetupTest() { namespace: "istio-system", dnsnames: [][]string{{"*"}}, }).Config() - _, err = fakeIstioClient.NetworkingV1alpha3().Gateways(suite.gwconfig.Namespace).Create(context.Background(), &suite.gwconfig, metav1.CreateOptions{}) + _, err = fakeIstioClient.NetworkingV1alpha3().Gateways(suite.gwconfig.Namespace).Create(context.Background(), suite.gwconfig, metav1.CreateOptions{}) suite.NoError(err, "should succeed") suite.vsconfig = (fakeVirtualServiceConfig{ @@ -362,7 +366,7 @@ func testVirtualServiceBindsToGateway(t *testing.T) { t.Run(ti.title, func(t *testing.T) { vsconfig := ti.vsconfig.Config() gwconfig := ti.gwconfig.Config() - require.Equal(t, ti.expected, virtualServiceBindsToGateway(vsconfig, &gwconfig, ti.vsHost)) + require.Equal(t, ti.expected, virtualServiceBindsToGateway(vsconfig, gwconfig, ti.vsHost)) }) } } @@ -1479,7 +1483,7 @@ func testVirtualServiceEndpoints(t *testing.T) { t.Run(ti.title, func(t *testing.T) { t.Parallel() - var gateways []networkingv1alpha3.Gateway + var gateways []*networkingv1alpha3.Gateway var virtualservices []*networkingv1alpha3.VirtualService for _, gwItem := range ti.gwConfigs { @@ -1500,7 +1504,7 @@ func testVirtualServiceEndpoints(t *testing.T) { fakeIstioClient := istiofake.NewSimpleClientset() for _, gateway := range gateways { - _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gateway.Namespace).Create(context.Background(), &gateway, metav1.CreateOptions{}) + _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gateway.Namespace).Create(context.Background(), gateway, metav1.CreateOptions{}) require.NoError(t, err) } @@ -1579,7 +1583,9 @@ func newTestVirtualServiceSource(loadBalancerList []fakeIngressGatewayService, g for _, gw := range gwList { gwObj := gw.Config() - _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gw.namespace).Create(context.Background(), &gwObj, metav1.CreateOptions{}) + // use create instead of add + // https://github.com/kubernetes/client-go/blob/92512ee2b8cf6696e9909245624175b7f0c971d9/testing/fixture.go#LL336C3-L336C52 + _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gw.namespace).Create(context.Background(), gwObj, metav1.CreateOptions{}) if err != nil { return nil, err } @@ -1631,6 +1637,127 @@ func (c fakeVirtualServiceConfig) Config() *networkingv1alpha3.VirtualService { Namespace: c.namespace, Annotations: c.annotations, }, - Spec: vs, + Spec: *vs.DeepCopy(), + } +} + +func TestVirtualServiceSourceGetGateway(t *testing.T) { + type fields struct { + virtualServiceSource *virtualServiceSource + } + type args struct { + ctx context.Context + gatewayStr string + virtualService *networkingv1alpha3.VirtualService + } + tests := []struct { + name string + fields fields + args args + want *networkingv1alpha3.Gateway + expectedErrStr string + }{ + {name: "EmptyGateway", fields: fields{ + virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(nil, nil); return vs }(), + }, args: args{ + ctx: context.TODO(), + gatewayStr: "", + virtualService: nil, + }, want: nil, expectedErrStr: ""}, + {name: "MeshGateway", fields: fields{ + virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(nil, nil); return vs }(), + }, args: args{ + ctx: context.TODO(), + gatewayStr: IstioMeshGateway, + virtualService: nil, + }, want: nil, expectedErrStr: ""}, + {name: "MissingGateway", fields: fields{ + virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(nil, nil); return vs }(), + }, args: args{ + ctx: context.TODO(), + gatewayStr: "doesnt/exist", + virtualService: &networkingv1alpha3.VirtualService{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: "exist", Namespace: "doesnt"}, + Spec: istionetworking.VirtualService{}, + Status: v1alpha1.IstioStatus{}, + }, + }, want: nil, expectedErrStr: ""}, + {name: "InvalidGatewayStr", fields: fields{ + virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(nil, nil); return vs }(), + }, args: args{ + ctx: context.TODO(), + gatewayStr: "1/2/3/", + virtualService: &networkingv1alpha3.VirtualService{}, + }, want: nil, expectedErrStr: "invalid gateway name (name or namespace/name) found '1/2/3/'"}, + {name: "ExistingGateway", fields: fields{ + virtualServiceSource: func() *virtualServiceSource { + vs, _ := newTestVirtualServiceSource(nil, []fakeGatewayConfig{{ + namespace: "bar", + name: "foo", + }}) + return vs + }(), + }, args: args{ + ctx: context.TODO(), + gatewayStr: "bar/foo", + virtualService: &networkingv1alpha3.VirtualService{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, + Spec: istionetworking.VirtualService{}, + Status: v1alpha1.IstioStatus{}, + }, + }, want: &networkingv1alpha3.Gateway{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, + Spec: istionetworking.Gateway{}, + Status: v1alpha1.IstioStatus{}, + }, expectedErrStr: ""}, + {name: "ErrorGettingGateway", fields: fields{ + virtualServiceSource: func() *virtualServiceSource { + istioFake := istiofake.NewSimpleClientset() + istioFake.NetworkingV1alpha3().(*fakenetworking3.FakeNetworkingV1alpha3).PrependReactor("get", "gateways", func(action k8sclienttesting.Action) (handled bool, ret runtime.Object, err error) { + return true, &networkingv1alpha3.Gateway{}, fmt.Errorf("error getting gateway") + }) + vs, _ := NewIstioVirtualServiceSource( + context.TODO(), + fake.NewSimpleClientset(), + istioFake, + "", + "", + "{{.Name}}", + false, + false, + ) + return vs.(*virtualServiceSource) + }(), + }, args: args{ + ctx: context.TODO(), + gatewayStr: "foo/bar", + virtualService: &networkingv1alpha3.VirtualService{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "error"}, + Spec: istionetworking.VirtualService{}, + Status: v1alpha1.IstioStatus{}, + }, + }, want: nil, expectedErrStr: "error getting gateway"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.fields.virtualServiceSource.getGateway(tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService) + if tt.expectedErrStr != "" { + assert.EqualError(t, err, tt.expectedErrStr, fmt.Sprintf("getGateway(%v, %v, %v)", tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService)) + return + } else { + require.NoError(t, err) + } + if tt.want != nil && got != nil { + tt.want.Spec.ProtoReflect() + tt.want.Status.ProtoReflect() + assert.Equalf(t, tt.want, got, "getGateway(%v, %v, %v)", tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService) + } else { + assert.Equalf(t, tt.want, got, "getGateway(%v, %v, %v)", tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService) + } + }) } } diff --git a/source/node.go b/source/node.go index b0e672d732..223ec2982d 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[endpoint.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 { @@ -131,17 +130,22 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro addrs, err := ns.nodeAddresses(node) if err != nil { - return nil, fmt.Errorf("failed to get node address from %s: %s", node.Name, err.Error()) + return nil, fmt.Errorf("failed to get node address from %s: %w", node.Name, err) } - 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 := endpoint.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 901c1baa11..885d9f54e1 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.go b/source/openshift_route.go index ec061c108b..14e0b632c8 100644 --- a/source/openshift_route.go +++ b/source/openshift_route.go @@ -21,6 +21,7 @@ import ( "fmt" "sort" "text/template" + "time" routev1 "github.com/openshift/api/route/v1" versioned "github.com/openshift/client-go/route/clientset/versioned" @@ -71,7 +72,7 @@ func NewOcpRouteSource( // Use a shared informer to listen for add/update/delete of Routes in the specified namespace. // Set resync period to 0, to prevent processing when nothing has changed. - informerFactory := extInformers.NewSharedInformerFactoryWithOptions(ocpClient, 0, extInformers.WithNamespace(namespace)) + informerFactory := extInformers.NewFilteredSharedInformerFactory(ocpClient, 0*time.Second, namespace, nil) informer := informerFactory.Route().V1().Routes() // Add default resource event handlers to properly initialize informer. diff --git a/source/pod.go b/source/pod.go index 36e6ffe50e..2441d28908 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[endpoint.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[endpoint.EndpointKey][]string, domain string, recordType string, address string) { + key := endpoint.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 c138aaf5fd..549a9ebf90 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 925c327f24..b19ba48825 100644 --- a/source/service.go +++ b/source/service.go @@ -216,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]) @@ -268,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[endpoint.EndpointKey]endpoint.Targets) for _, subset := range endpointsObject.Subsets { addresses := subset.Addresses if svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses { @@ -308,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) } } @@ -321,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 := endpoint.EndpointKey{ + DNSName: headlessDomain, + RecordType: suitableType(target), + } + targetsByHeadlessDomainAndType[key] = append(targetsByHeadlessDomainAndType[key], target) + } } } } - headlessDomains := []string{} - for headlessDomain := range targetsByHeadlessDomain { - headlessDomains = append(headlessDomains, headlessDomain) + headlessKeys := []endpoint.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{}{} @@ -347,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...)) } } @@ -478,30 +492,33 @@ 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, 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 + targets = getTargetsFromTargetAnnotation(svc.Annotations) + + if len(targets) == 0 { + switch svc.Spec.Type { + case v1.ServiceTypeLoadBalancer: + if useClusterIP { + targets = extractServiceIps(svc) + } else { + targets = extractLoadBalancerTargets(svc, sc.resolveLoadBalancerHostname) + } + case v1.ServiceTypeClusterIP: + if svc.Spec.ClusterIP == v1.ClusterIPNone { + endpoints = append(endpoints, sc.extractHeadlessEndpoints(svc, hostname, ttl)...) + } else if sc.publishInternal { + targets = extractServiceIps(svc) + } + 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 = extractServiceExternalName(svc) } - endpoints = append(endpoints, sc.extractNodePortEndpoints(svc, targets, hostname, ttl)...) - case v1.ServiceTypeExternalName: - targets = append(targets, extractServiceExternalName(svc)...) } for _, t := range targets { @@ -544,12 +561,12 @@ func extractServiceExternalName(svc *v1.Service) endpoint.Targets { } func extractLoadBalancerTargets(svc *v1.Service, resolveLoadBalancerHostname bool) endpoint.Targets { - var ( - targets endpoint.Targets - externalIPs endpoint.Targets - ) + if len(svc.Spec.ExternalIPs) > 0 { + return svc.Spec.ExternalIPs + } // Create a corresponding endpoint for each configured external entrypoint. + var targets endpoint.Targets for _, lb := range svc.Status.LoadBalancer.Ingress { if lb.IP != "" { targets = append(targets, lb.IP) @@ -570,16 +587,6 @@ func extractLoadBalancerTargets(svc *v1.Service, resolveLoadBalancerHostname boo } } - if svc.Spec.ExternalIPs != nil { - for _, ext := range svc.Spec.ExternalIPs { - externalIPs = append(externalIPs, ext) - } - } - - if len(externalIPs) > 0 { - return externalIPs - } - return targets } @@ -587,6 +594,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 ) @@ -634,24 +642,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 98b400fca3..a12bd27524 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -1349,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, @@ -1361,6 +1361,77 @@ func TestClusterIpServices(t *testing.T) { {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"}}, + }, + }, { title: "hostname annotated ClusterIp services are ignored", svcNamespace: "testing", @@ -1373,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", @@ -1392,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", @@ -1405,6 +1517,20 @@ func TestClusterIpServices(t *testing.T) { }, 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", + }, { title: "ClusterIP service without matching label generates an endpoint", svcNamespace: "testing", @@ -1518,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{ @@ -1527,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"}, }, }, }, { @@ -1537,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"}, }, }, }}, @@ -1559,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"}, }, }, }, { @@ -1569,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"}, }, }, }}, @@ -1584,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{ @@ -1593,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"}, }, }, }, { @@ -1603,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"}, }, }, }}, @@ -1619,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{ @@ -1627,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"}, }, }, }, { @@ -1636,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"}, }, }, }}, @@ -1652,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{ @@ -1661,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"}, }, }, }, { @@ -1671,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"}, }, }, }}, @@ -1691,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{ @@ -1700,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"}, }, }, }, { @@ -1710,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"}, }, }, }}, @@ -1731,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{ @@ -1740,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"}, }, }, }, { @@ -1750,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"}, }, }, }}, @@ -1768,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{ @@ -1777,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"}, }, }, }, { @@ -1787,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"}, }, }, }}, @@ -1804,7 +1953,9 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, expected: []*endpoint.Endpoint{ {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{ @@ -1817,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"}, }, }, }, { @@ -1830,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"}, }, }, }}, @@ -1846,7 +1999,9 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, expected: []*endpoint.Endpoint{ {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{ @@ -1859,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"}, }, }, }, { @@ -1872,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"}, }, }, }}, @@ -1888,7 +2045,9 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, expected: []*endpoint.Endpoint{ {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{ @@ -1901,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"}, }, }, }, { @@ -1914,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"}, }, }, }}, @@ -1942,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"}, }, }, }, { @@ -1955,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"}, }, }, }}, @@ -2081,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", @@ -2113,6 +2276,39 @@ func TestHeadlessServices(t *testing.T) { }, 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, + }, { "hostname annotated Headless services are ignored", "", @@ -2143,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", @@ -2176,6 +2372,40 @@ func TestHeadlessServices(t *testing.T) { }, 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, + }, { "annotated Headless services return endpoints for each selected Pod, which are in running state", "", @@ -2273,7 +2503,7 @@ func TestHeadlessServices(t *testing.T) { false, }, { - "annotated Headless services return only a unique set of targets", + "annotated Headless services return only a unique set of IPv4 targets", "", "testing", "foo", @@ -2304,7 +2534,38 @@ func TestHeadlessServices(t *testing.T) { 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", @@ -2337,7 +2598,40 @@ func TestHeadlessServices(t *testing.T) { 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", @@ -2380,7 +2674,98 @@ func TestHeadlessServices(t *testing.T) { 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", @@ -2411,6 +2796,38 @@ func TestHeadlessServices(t *testing.T) { }, 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, + }, } { tc := tc t.Run(tc.title, func(t *testing.T) { @@ -2553,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", @@ -2586,6 +3003,40 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, 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, + }, { "hostname annotated Headless services are ignored", "", @@ -2617,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", @@ -2651,6 +3102,41 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, 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, + }, { "annotated Headless services return endpoints for each selected Pod, which are in running state", "", @@ -2719,7 +3205,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { false, }, { - "annotated Headless services return endpoints for pods missing hostname", + "annotated Headless services return IPv4 endpoints for pods missing hostname", "", "testing", "foo", @@ -2750,6 +3236,38 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, 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, + }, { "annotated Headless services without a targetRef has no endpoints", "", @@ -2903,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", @@ -2921,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", "", diff --git a/source/shared_test.go b/source/shared_test.go index 9cdc58d22a..11828dbe2e 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) { diff --git a/source/source.go b/source/source.go index 8573772c37..31e9f8758c 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" @@ -151,7 +151,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 +167,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 { @@ -328,9 +332,9 @@ func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]stri type eventHandlerFunc func() -func (fn eventHandlerFunc) OnAdd(obj interface{}) { fn() } -func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{}) { fn() } -func (fn eventHandlerFunc) OnDelete(obj interface{}) { fn() } +func (fn eventHandlerFunc) OnAdd(obj interface{}, isInInitialList bool) { fn() } +func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{}) { fn() } +func (fn eventHandlerFunc) OnDelete(obj interface{}) { fn() } type informerFactory interface { WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool diff --git a/source/store.go b/source/store.go index d22e16ff87..fc3dc35d1b 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 @@ -222,7 +223,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg 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 { @@ -291,6 +292,16 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg return nil, err } return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespace) + case "traefik-proxy": + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) case "openshift-route": ocpClient, err := p.OpenShiftClient() if err != nil { diff --git a/source/store_test.go b/source/store_test.go index e2d15f3f23..adb70633dd 100644 --- a/source/store_test.go +++ b/source/store_test.go @@ -130,11 +130,41 @@ func (suite *ByNamesTestSuite) TestAllInitialized() { Version: "v1", Resource: "virtualservers", }: "VirtualServersList", + { + Group: "traefik.containo.us", + Version: "v1alpha1", + Resource: "ingressroutes", + }: "IngressRouteList", + { + Group: "traefik.containo.us", + Version: "v1alpha1", + Resource: "ingressroutetcps", + }: "IngressRouteTCPList", + { + Group: "traefik.containo.us", + Version: "v1alpha1", + Resource: "ingressrouteudps", + }: "IngressRouteUDPList", + { + Group: "traefik.io", + Version: "v1alpha1", + Resource: "ingressroutes", + }: "IngressRouteList", + { + Group: "traefik.io", + Version: "v1alpha1", + Resource: "ingressroutetcps", + }: "IngressRouteTCPList", + { + Group: "traefik.io", + Version: "v1alpha1", + Resource: "ingressrouteudps", + }: "IngressRouteUDPList", }), nil) - sources, err := ByNames(context.TODO(), mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-httpproxy", "kong-tcpingress", "f5-virtualserver", "fake"}, minimalConfig) + sources, err := ByNames(context.TODO(), mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-httpproxy", "kong-tcpingress", "f5-virtualserver", "traefik-proxy", "fake"}, minimalConfig) suite.NoError(err, "should not generate errors") - suite.Len(sources, 7, "should generate all seven sources") + suite.Len(sources, 8, "should generate all eight sources") } func (suite *ByNamesTestSuite) TestOnlyFake() { @@ -171,9 +201,6 @@ func (suite *ByNamesTestSuite) TestKubeClientFails() { _, err = ByNames(context.TODO(), mockClientGenerator, []string{"kong-tcpingress"}, minimalConfig) suite.Error(err, "should return an error if kubernetes client cannot be created") - - _, err = ByNames(context.TODO(), mockClientGenerator, []string{"f5-virtualserver"}, minimalConfig) - suite.Error(err, "should return an error if kubernetes client cannot be created") } func (suite *ByNamesTestSuite) TestIstioClientFails() { diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go new file mode 100644 index 0000000000..38bc75b3ee --- /dev/null +++ b/source/traefik_proxy.go @@ -0,0 +1,1125 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "fmt" + "regexp" + "sort" + "strings" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/cache" + + "sigs.k8s.io/external-dns/endpoint" +) + +var ( + ingressrouteGVR = schema.GroupVersionResource{ + Group: "traefik.io", + Version: "v1alpha1", + Resource: "ingressroutes", + } + ingressrouteTCPGVR = schema.GroupVersionResource{ + Group: "traefik.io", + Version: "v1alpha1", + Resource: "ingressroutetcps", + } + ingressrouteUDPGVR = schema.GroupVersionResource{ + Group: "traefik.io", + Version: "v1alpha1", + Resource: "ingressrouteudps", + } + oldIngressrouteGVR = schema.GroupVersionResource{ + Group: "traefik.containo.us", + Version: "v1alpha1", + Resource: "ingressroutes", + } + oldIngressrouteTCPGVR = schema.GroupVersionResource{ + Group: "traefik.containo.us", + Version: "v1alpha1", + Resource: "ingressroutetcps", + } + oldIngressrouteUDPGVR = schema.GroupVersionResource{ + Group: "traefik.containo.us", + Version: "v1alpha1", + Resource: "ingressrouteudps", + } +) + +var ( + traefikHostExtractor = regexp.MustCompile(`(?:HostSNI|HostHeader|Host)\s*\(\s*(\x60.*?\x60)\s*\)`) + traefikValueProcessor = regexp.MustCompile(`\x60([^,\x60]+)\x60`) +) + +type traefikSource struct { + annotationFilter string + dynamicKubeClient dynamic.Interface + ingressRouteInformer informers.GenericInformer + ingressRouteTcpInformer informers.GenericInformer + ingressRouteUdpInformer informers.GenericInformer + oldIngressRouteInformer informers.GenericInformer + oldIngressRouteTcpInformer informers.GenericInformer + oldIngressRouteUdpInformer informers.GenericInformer + kubeClient kubernetes.Interface + namespace string + unstructuredConverter *unstructuredConverter +} + +func NewTraefikSource(ctx context.Context, dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace string, annotationFilter string) (Source, error) { + // Use shared informer to listen for add/update/delete of Host in the specified namespace. + // Set resync period to 0, to prevent processing when nothing has changed. + informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil) + ingressRouteInformer := informerFactory.ForResource(ingressrouteGVR) + ingressRouteTcpInformer := informerFactory.ForResource(ingressrouteTCPGVR) + ingressRouteUdpInformer := informerFactory.ForResource(ingressrouteUDPGVR) + oldIngressRouteInformer := informerFactory.ForResource(oldIngressrouteGVR) + oldIngressRouteTcpInformer := informerFactory.ForResource(oldIngressrouteTCPGVR) + oldIngressRouteUdpInformer := informerFactory.ForResource(oldIngressrouteUDPGVR) + + // Add default resource event handlers to properly initialize informers. + ingressRouteInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + ingressRouteTcpInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + ingressRouteUdpInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + oldIngressRouteInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + oldIngressRouteTcpInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + oldIngressRouteUdpInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + + informerFactory.Start((ctx.Done())) + + // wait for the local cache to be populated. + if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { + return nil, err + } + + uc, err := newTraefikUnstructuredConverter() + if err != nil { + return nil, errors.Wrapf(err, "failed to setup Unstructured Converter") + } + + return &traefikSource{ + annotationFilter: annotationFilter, + dynamicKubeClient: dynamicKubeClient, + ingressRouteInformer: ingressRouteInformer, + ingressRouteTcpInformer: ingressRouteTcpInformer, + ingressRouteUdpInformer: ingressRouteUdpInformer, + oldIngressRouteInformer: oldIngressRouteInformer, + oldIngressRouteTcpInformer: oldIngressRouteTcpInformer, + oldIngressRouteUdpInformer: oldIngressRouteUdpInformer, + kubeClient: kubeClient, + namespace: namespace, + unstructuredConverter: uc, + }, nil +} + +func (ts *traefikSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + ingressRouteEndpoints, err := ts.ingressRouteEndpoints() + if err != nil { + return nil, err + } + oldIngressRouteEndpoints, err := ts.oldIngressRouteEndpoints() + if err != nil { + return nil, err + } + ingressRouteTCPEndpoints, err := ts.ingressRouteTCPEndpoints() + if err != nil { + return nil, err + } + oldIngressRouteTCPEndpoints, err := ts.oldIngressRouteTCPEndpoints() + if err != nil { + return nil, err + } + ingressRouteUDPEndpoints, err := ts.ingressRouteUDPEndpoints() + if err != nil { + return nil, err + } + oldIngressRouteUDPEndpoints, err := ts.oldIngressRouteUDPEndpoints() + if err != nil { + return nil, err + } + + endpoints = append(endpoints, ingressRouteEndpoints...) + endpoints = append(endpoints, ingressRouteTCPEndpoints...) + endpoints = append(endpoints, ingressRouteUDPEndpoints...) + endpoints = append(endpoints, oldIngressRouteEndpoints...) + endpoints = append(endpoints, oldIngressRouteTCPEndpoints...) + endpoints = append(endpoints, oldIngressRouteUDPEndpoints...) + + for _, ep := range endpoints { + sort.Sort(ep.Targets) + } + + return endpoints, nil +} + +// ingressRouteEndpoints extracts endpoints from all IngressRoute objects +func (ts *traefikSource) ingressRouteEndpoints() ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + irs, err := ts.ingressRouteInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var ingressRoutes []*IngressRoute + for _, ingressRouteObj := range irs { + unstructuredHost, ok := ingressRouteObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert IngressRoute object to unstructured") + } + + ingressRoute := &IngressRoute{} + err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil) + if err != nil { + return nil, err + } + ingressRoutes = append(ingressRoutes, ingressRoute) + } + + ingressRoutes, err = ts.filterIngressRouteByAnnotation(ingressRoutes) + if err != nil { + return nil, errors.Wrap(err, "failed to filter IngressRoute") + } + + for _, ingressRoute := range ingressRoutes { + var targets endpoint.Targets + + targets = append(targets, getTargetsFromTargetAnnotation(ingressRoute.Annotations)...) + + fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name) + + ingressEndpoints, err := ts.endpointsFromIngressRoute(ingressRoute, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from IngressRoute: %s: %v", fullname, ingressEndpoints) + ts.setResourceLabelIngressRoute(ingressRoute, ingressEndpoints) + ts.setDualstackLabelIngressRoute(ingressRoute, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + return endpoints, nil +} + +// ingressRouteTCPEndpoints extracts endpoints from all IngressRouteTCP objects +func (ts *traefikSource) ingressRouteTCPEndpoints() ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + irs, err := ts.ingressRouteTcpInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var ingressRouteTCPs []*IngressRouteTCP + for _, ingressRouteTCPObj := range irs { + unstructuredHost, ok := ingressRouteTCPObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert IngressRouteTCP object to unstructured") + } + + ingressRouteTCP := &IngressRouteTCP{} + err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRouteTCP, nil) + if err != nil { + return nil, err + } + ingressRouteTCPs = append(ingressRouteTCPs, ingressRouteTCP) + } + + ingressRouteTCPs, err = ts.filterIngressRouteTcpByAnnotations(ingressRouteTCPs) + if err != nil { + return nil, errors.Wrap(err, "failed to filter IngressRouteTCP") + } + + for _, ingressRouteTCP := range ingressRouteTCPs { + var targets endpoint.Targets + + targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteTCP.Annotations)...) + + fullname := fmt.Sprintf("%s/%s", ingressRouteTCP.Namespace, ingressRouteTCP.Name) + + ingressEndpoints, err := ts.endpointsFromIngressRouteTCP(ingressRouteTCP, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from IngressRouteTCP: %s: %v", fullname, ingressEndpoints) + ts.setResourceLabelIngressRouteTCP(ingressRouteTCP, ingressEndpoints) + ts.setDualstackLabelIngressRouteTCP(ingressRouteTCP, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + return endpoints, nil +} + +// ingressRouteUDPEndpoints extracts endpoints from all IngressRouteUDP objects +func (ts *traefikSource) ingressRouteUDPEndpoints() ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + irs, err := ts.ingressRouteUdpInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var ingressRouteUDPs []*IngressRouteUDP + for _, ingressRouteUDPObj := range irs { + unstructuredHost, ok := ingressRouteUDPObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert IngressRouteUDP object to unstructured") + } + + ingressRoute := &IngressRouteUDP{} + err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil) + if err != nil { + return nil, err + } + ingressRouteUDPs = append(ingressRouteUDPs, ingressRoute) + } + + ingressRouteUDPs, err = ts.filterIngressRouteUdpByAnnotations(ingressRouteUDPs) + if err != nil { + return nil, errors.Wrap(err, "failed to filter IngressRouteUDP") + } + + for _, ingressRouteUDP := range ingressRouteUDPs { + var targets endpoint.Targets + + targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteUDP.Annotations)...) + + fullname := fmt.Sprintf("%s/%s", ingressRouteUDP.Namespace, ingressRouteUDP.Name) + + ingressEndpoints, err := ts.endpointsFromIngressRouteUDP(ingressRouteUDP, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from IngressRouteUDP: %s: %v", fullname, ingressEndpoints) + ts.setResourceLabelIngressRouteUDP(ingressRouteUDP, ingressEndpoints) + ts.setDualstackLabelIngressRouteUDP(ingressRouteUDP, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + return endpoints, nil +} + +// oldIngressRouteEndpoints extracts endpoints from all IngressRoute objects +func (ts *traefikSource) oldIngressRouteEndpoints() ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + irs, err := ts.oldIngressRouteInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var ingressRoutes []*IngressRoute + for _, ingressRouteObj := range irs { + unstructuredHost, ok := ingressRouteObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert IngressRoute object to unstructured") + } + + ingressRoute := &IngressRoute{} + err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil) + if err != nil { + return nil, err + } + ingressRoutes = append(ingressRoutes, ingressRoute) + } + + ingressRoutes, err = ts.filterIngressRouteByAnnotation(ingressRoutes) + if err != nil { + return nil, errors.Wrap(err, "failed to filter IngressRoute") + } + + for _, ingressRoute := range ingressRoutes { + var targets endpoint.Targets + + targets = append(targets, getTargetsFromTargetAnnotation(ingressRoute.Annotations)...) + + fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name) + + ingressEndpoints, err := ts.endpointsFromIngressRoute(ingressRoute, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from IngressRoute: %s: %v", fullname, ingressEndpoints) + ts.setResourceLabelIngressRoute(ingressRoute, ingressEndpoints) + ts.setDualstackLabelIngressRoute(ingressRoute, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + return endpoints, nil +} + +// oldIngressRouteTCPEndpoints extracts endpoints from all IngressRouteTCP objects +func (ts *traefikSource) oldIngressRouteTCPEndpoints() ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + irs, err := ts.oldIngressRouteTcpInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var ingressRouteTCPs []*IngressRouteTCP + for _, ingressRouteTCPObj := range irs { + unstructuredHost, ok := ingressRouteTCPObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert IngressRouteTCP object to unstructured") + } + + ingressRouteTCP := &IngressRouteTCP{} + err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRouteTCP, nil) + if err != nil { + return nil, err + } + ingressRouteTCPs = append(ingressRouteTCPs, ingressRouteTCP) + } + + ingressRouteTCPs, err = ts.filterIngressRouteTcpByAnnotations(ingressRouteTCPs) + if err != nil { + return nil, errors.Wrap(err, "failed to filter IngressRouteTCP") + } + + for _, ingressRouteTCP := range ingressRouteTCPs { + var targets endpoint.Targets + + targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteTCP.Annotations)...) + + fullname := fmt.Sprintf("%s/%s", ingressRouteTCP.Namespace, ingressRouteTCP.Name) + + ingressEndpoints, err := ts.endpointsFromIngressRouteTCP(ingressRouteTCP, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from IngressRouteTCP: %s: %v", fullname, ingressEndpoints) + ts.setResourceLabelIngressRouteTCP(ingressRouteTCP, ingressEndpoints) + ts.setDualstackLabelIngressRouteTCP(ingressRouteTCP, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + return endpoints, nil +} + +// oldIngressRouteUDPEndpoints extracts endpoints from all IngressRouteUDP objects +func (ts *traefikSource) oldIngressRouteUDPEndpoints() ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + irs, err := ts.oldIngressRouteUdpInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var ingressRouteUDPs []*IngressRouteUDP + for _, ingressRouteUDPObj := range irs { + unstructuredHost, ok := ingressRouteUDPObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert IngressRouteUDP object to unstructured") + } + + ingressRoute := &IngressRouteUDP{} + err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil) + if err != nil { + return nil, err + } + ingressRouteUDPs = append(ingressRouteUDPs, ingressRoute) + } + + ingressRouteUDPs, err = ts.filterIngressRouteUdpByAnnotations(ingressRouteUDPs) + if err != nil { + return nil, errors.Wrap(err, "failed to filter IngressRouteUDP") + } + + for _, ingressRouteUDP := range ingressRouteUDPs { + var targets endpoint.Targets + + targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteUDP.Annotations)...) + + fullname := fmt.Sprintf("%s/%s", ingressRouteUDP.Namespace, ingressRouteUDP.Name) + + ingressEndpoints, err := ts.endpointsFromIngressRouteUDP(ingressRouteUDP, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from IngressRouteUDP: %s: %v", fullname, ingressEndpoints) + ts.setResourceLabelIngressRouteUDP(ingressRouteUDP, ingressEndpoints) + ts.setDualstackLabelIngressRouteUDP(ingressRouteUDP, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + return endpoints, nil +} + +// filterIngressRouteByAnnotation filters a list of IngressRoute by a given annotation selector. +func (ts *traefikSource) filterIngressRouteByAnnotation(ingressRoutes []*IngressRoute) ([]*IngressRoute, error) { + labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) + if err != nil { + return nil, err + } + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + + // empty filter returns original list + if selector.Empty() { + return ingressRoutes, nil + } + + filteredList := []*IngressRoute{} + + for _, ingressRoute := range ingressRoutes { + // convert the IngressRoute's annotations to an equivalent label selector + annotations := labels.Set(ingressRoute.Annotations) + + // include IngressRoute if its annotations match the selector + if selector.Matches(annotations) { + filteredList = append(filteredList, ingressRoute) + } + } + + return filteredList, nil +} + +// filterIngressRouteTcpByAnnotations filters a list of IngressRouteTCP by a given annotation selector. +func (ts *traefikSource) filterIngressRouteTcpByAnnotations(ingressRoutes []*IngressRouteTCP) ([]*IngressRouteTCP, error) { + labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) + if err != nil { + return nil, err + } + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + + // empty filter returns original list + if selector.Empty() { + return ingressRoutes, nil + } + + filteredList := []*IngressRouteTCP{} + + for _, ingressRoute := range ingressRoutes { + // convert the IngressRoute's annotations to an equivalent label selector + annotations := labels.Set(ingressRoute.Annotations) + + // include IngressRoute if its annotations match the selector + if selector.Matches(annotations) { + filteredList = append(filteredList, ingressRoute) + } + } + + return filteredList, nil +} + +// filterIngressRouteUdpByAnnotations filters a list of IngressRoute by a given annotation selector. +func (ts *traefikSource) filterIngressRouteUdpByAnnotations(ingressRoutes []*IngressRouteUDP) ([]*IngressRouteUDP, error) { + labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) + if err != nil { + return nil, err + } + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + + // empty filter returns original list + if selector.Empty() { + return ingressRoutes, nil + } + + filteredList := []*IngressRouteUDP{} + + for _, ingressRoute := range ingressRoutes { + // convert the IngressRoute's annotations to an equivalent label selector + annotations := labels.Set(ingressRoute.Annotations) + + // include IngressRoute if its annotations match the selector + if selector.Matches(annotations) { + filteredList = append(filteredList, ingressRoute) + } + } + + return filteredList, nil +} + +func (ts *traefikSource) setResourceLabelIngressRoute(ingressroute *IngressRoute, endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingressroute/%s/%s", ingressroute.Namespace, ingressroute.Name) + } +} +func (ts *traefikSource) setResourceLabelIngressRouteTCP(ingressroute *IngressRouteTCP, endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingressroutetcp/%s/%s", ingressroute.Namespace, ingressroute.Name) + } +} +func (ts *traefikSource) setResourceLabelIngressRouteUDP(ingressroute *IngressRouteUDP, endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingressrouteudp/%s/%s", ingressroute.Namespace, ingressroute.Name) + } +} + +func (ts *traefikSource) setDualstackLabelIngressRoute(ingressRoute *IngressRoute, endpoints []*endpoint.Endpoint) { + val, ok := ingressRoute.Annotations[ALBDualstackAnnotationKey] + if ok && val == ALBDualstackAnnotationValue { + log.Debugf("Adding dualstack label to IngressRoute %s/%s.", ingressRoute.Namespace, ingressRoute.Name) + for _, ep := range endpoints { + ep.Labels[endpoint.DualstackLabelKey] = "true" + } + } +} +func (ts *traefikSource) setDualstackLabelIngressRouteTCP(ingressRoute *IngressRouteTCP, endpoints []*endpoint.Endpoint) { + val, ok := ingressRoute.Annotations[ALBDualstackAnnotationKey] + if ok && val == ALBDualstackAnnotationValue { + log.Debugf("Adding dualstack label to IngressRouteTCP %s/%s.", ingressRoute.Namespace, ingressRoute.Name) + for _, ep := range endpoints { + ep.Labels[endpoint.DualstackLabelKey] = "true" + } + } +} +func (ts *traefikSource) setDualstackLabelIngressRouteUDP(ingressRoute *IngressRouteUDP, endpoints []*endpoint.Endpoint) { + val, ok := ingressRoute.Annotations[ALBDualstackAnnotationKey] + if ok && val == ALBDualstackAnnotationValue { + log.Debugf("Adding dualstack label to IngressRouteUDP %s/%s.", ingressRoute.Namespace, ingressRoute.Name) + for _, ep := range endpoints { + ep.Labels[endpoint.DualstackLabelKey] = "true" + } + } +} + +// endpointsFromIngressRoute extracts the endpoints from a IngressRoute object +func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *IngressRoute, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations) + + ttl, err := getTTLFromAnnotations(ingressRoute.Annotations) + if err != nil { + return nil, err + } + + hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) + for _, hostname := range hostnameList { + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) + } + + for _, route := range ingressRoute.Spec.Routes { + match := route.Match + + for _, hostEntry := range traefikHostExtractor.FindAllString(match, -1) { + for _, host := range traefikValueProcessor.FindAllString(hostEntry, -1) { + host = strings.TrimPrefix(host, "`") + host = strings.TrimSuffix(host, "`") + + // Checking for host = * is required, as Host(`*`) can be set + if host != "*" && host != "" { + endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...) + } + } + } + } + + return endpoints, nil +} + +// endpointsFromIngressRouteTCP extracts the endpoints from a IngressRouteTCP object +func (ts *traefikSource) endpointsFromIngressRouteTCP(ingressRoute *IngressRouteTCP, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations) + + ttl, err := getTTLFromAnnotations(ingressRoute.Annotations) + if err != nil { + return nil, err + } + + hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) + for _, hostname := range hostnameList { + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) + } + + for _, route := range ingressRoute.Spec.Routes { + match := route.Match + + for _, hostEntry := range traefikHostExtractor.FindAllString(match, -1) { + for _, host := range traefikValueProcessor.FindAllString(hostEntry, -1) { + host = strings.TrimPrefix(host, "`") + host = strings.TrimSuffix(host, "`") + + // Checking for host = * is required, as HostSNI(`*`) can be set + // in the case of TLS passthrough + if host != "*" && host != "" { + endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...) + } + } + } + } + + return endpoints, nil +} + +// endpointsFromIngressRouteUDP extracts the endpoints from a IngressRouteUDP object +func (ts *traefikSource) endpointsFromIngressRouteUDP(ingressRoute *IngressRouteUDP, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations) + + ttl, err := getTTLFromAnnotations(ingressRoute.Annotations) + if err != nil { + return nil, err + } + + hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) + for _, hostname := range hostnameList { + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) + } + + return endpoints, nil +} + +func (ts *traefikSource) AddEventHandler(ctx context.Context, handler func()) { + // Right now there is no way to remove event handler from informer, see: + // https://github.com/kubernetes/kubernetes/issues/79610 + log.Debug("Adding event handler for IngressRoute") + ts.ingressRouteInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + ts.oldIngressRouteInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + log.Debug("Adding event handler for IngressRouteTCP") + ts.ingressRouteTcpInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + ts.oldIngressRouteTcpInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + log.Debug("Adding event handler for IngressRouteUDP") + ts.ingressRouteUdpInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + ts.oldIngressRouteUdpInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) +} + +// newTraefikUnstructuredConverter returns a new unstructuredConverter initialized +func newTraefikUnstructuredConverter() (*unstructuredConverter, error) { + uc := &unstructuredConverter{ + scheme: runtime.NewScheme(), + } + + // Add the core types we need + uc.scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + uc.scheme.AddKnownTypes(oldIngressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + uc.scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + uc.scheme.AddKnownTypes(oldIngressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + uc.scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + uc.scheme.AddKnownTypes(oldIngressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + if err := scheme.AddToScheme(uc.scheme); err != nil { + return nil, err + } + + return uc, nil +} + +// Basic redefinition of Traefik 2's CRD: https://github.com/traefik/traefik/tree/v2.8.7/pkg/provider/kubernetes/crd/traefik/v1alpha1 + +// traefikIngressRouteSpec defines the desired state of IngressRoute. +type traefikIngressRouteSpec struct { + // Routes defines the list of routes. + Routes []traefikRoute `json:"routes"` +} + +// traefikRoute holds the HTTP route configuration. +type traefikRoute struct { + // Match defines the router's rule. + // More info: https://doc.traefik.io/traefik/v2.9/routing/routers/#rule + Match string `json:"match"` +} + +// IngressRoute is the CRD implementation of a Traefik HTTP Router. +type IngressRoute struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta `json:"metadata"` + + Spec traefikIngressRouteSpec `json:"spec"` +} + +// IngressRouteList is a collection of IngressRoute. +type IngressRouteList struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ListMeta `json:"metadata"` + + // Items is the list of IngressRoute. + Items []IngressRoute `json:"items"` +} + +// traefikIngressRouteTCPSpec defines the desired state of IngressRouteTCP. +type traefikIngressRouteTCPSpec struct { + Routes []traefikRouteTCP `json:"routes"` +} + +// traefikRouteTCP holds the TCP route configuration. +type traefikRouteTCP struct { + // Match defines the router's rule. + // More info: https://doc.traefik.io/traefik/v2.9/routing/routers/#rule_1 + Match string `json:"match"` +} + +// IngressRouteTCP is the CRD implementation of a Traefik TCP Router. +type IngressRouteTCP struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta `json:"metadata"` + + Spec traefikIngressRouteTCPSpec `json:"spec"` +} + +// IngressRouteTCPList is a collection of IngressRouteTCP. +type IngressRouteTCPList struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ListMeta `json:"metadata"` + + // Items is the list of IngressRouteTCP. + Items []IngressRouteTCP `json:"items"` +} + +// IngressRouteUDP is a CRD implementation of a Traefik UDP Router. +type IngressRouteUDP struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta `json:"metadata"` +} + +// IngressRouteUDPList is a collection of IngressRouteUDP. +type IngressRouteUDPList struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ListMeta `json:"metadata"` + + // Items is the list of IngressRouteUDP. + Items []IngressRouteUDP `json:"items"` +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressRoute) DeepCopyInto(out *IngressRoute) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRoute. +func (in *IngressRoute) DeepCopy() *IngressRoute { + if in == nil { + return nil + } + out := new(IngressRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IngressRoute) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressRouteList) DeepCopyInto(out *IngressRouteList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IngressRoute, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRouteList. +func (in *IngressRouteList) DeepCopy() *IngressRouteList { + if in == nil { + return nil + } + out := new(IngressRouteList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IngressRouteList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *traefikIngressRouteSpec) DeepCopyInto(out *traefikIngressRouteSpec) { + *out = *in + if in.Routes != nil { + in, out := &in.Routes, &out.Routes + *out = make([]traefikRoute, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRouteSpec. +func (in *traefikIngressRouteSpec) DeepCopy() *traefikIngressRouteSpec { + if in == nil { + return nil + } + out := new(traefikIngressRouteSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *traefikRoute) DeepCopyInto(out *traefikRoute) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Route. +func (in *traefikRoute) DeepCopy() *traefikRoute { + if in == nil { + return nil + } + out := new(traefikRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressRouteTCP) DeepCopyInto(out *IngressRouteTCP) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRouteTCP. +func (in *IngressRouteTCP) DeepCopy() *IngressRouteTCP { + if in == nil { + return nil + } + out := new(IngressRouteTCP) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IngressRouteTCP) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressRouteTCPList) DeepCopyInto(out *IngressRouteTCPList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IngressRouteTCP, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRouteTCPList. +func (in *IngressRouteTCPList) DeepCopy() *IngressRouteTCPList { + if in == nil { + return nil + } + out := new(IngressRouteTCPList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IngressRouteTCPList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *traefikIngressRouteTCPSpec) DeepCopyInto(out *traefikIngressRouteTCPSpec) { + *out = *in + if in.Routes != nil { + in, out := &in.Routes, &out.Routes + *out = make([]traefikRouteTCP, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRouteTCPSpec. +func (in *traefikIngressRouteTCPSpec) DeepCopy() *traefikIngressRouteTCPSpec { + if in == nil { + return nil + } + out := new(traefikIngressRouteTCPSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *traefikRouteTCP) DeepCopyInto(out *traefikRouteTCP) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteTCP. +func (in *traefikRouteTCP) DeepCopy() *traefikRouteTCP { + if in == nil { + return nil + } + out := new(traefikRouteTCP) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressRouteUDP) DeepCopyInto(out *IngressRouteUDP) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRouteUDP. +func (in *IngressRouteUDP) DeepCopy() *IngressRouteUDP { + if in == nil { + return nil + } + out := new(IngressRouteUDP) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IngressRouteUDP) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressRouteUDPList) DeepCopyInto(out *IngressRouteUDPList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IngressRouteUDP, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRouteUDPList. +func (in *IngressRouteUDPList) DeepCopy() *IngressRouteUDPList { + if in == nil { + return nil + } + out := new(IngressRouteUDPList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IngressRouteUDPList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/source/traefik_proxy_test.go b/source/traefik_proxy_test.go new file mode 100644 index 0000000000..eb26ed2b31 --- /dev/null +++ b/source/traefik_proxy_test.go @@ -0,0 +1,1330 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + fakeDynamic "k8s.io/client-go/dynamic/fake" + fakeKube "k8s.io/client-go/kubernetes/fake" + "sigs.k8s.io/external-dns/endpoint" +) + +// This is a compile-time validation that traefikSource is a Source. +var _ Source = &traefikSource{} + +const defaultTraefikNamespace = "traefik" + +func TestTraefikProxyIngressRouteEndpoints(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRoute IngressRoute + expected []*endpoint.Endpoint + }{ + { + title: "IngressRoute with hostname annotation", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with host rule", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-host-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "Host(`b.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "b.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with hostheader rule", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-hostheader-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "HostHeader(`c.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "c.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-hostheader-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with multiple host rules", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-multi-host-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "Host(`d.example.com`) || Host(`e.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "d.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "e.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with multiple host rules and annotation", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-multi-host-annotations-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "f.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "Host(`g.example.com`, `h.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "f.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "g.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "h.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute omit wildcard", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-omit-wildcard-host", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "Host(`*`)", + }, + }, + }, + }, + expected: nil, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + scheme.AddKnownTypes(oldIngressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(oldIngressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(oldIngressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + ir := unstructured.Unstructured{} + + ingressRouteAsJSON, err := json.Marshal(ti.ingressRoute) + assert.NoError(t, err) + + assert.NoError(t, ir.UnmarshalJSON(ingressRouteAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(ingressrouteGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(ingressrouteGVR).Namespace(defaultTraefikNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +} + +func TestTraefikProxyIngressRouteTCPEndpoints(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRouteTCP IngressRouteTCP + expected []*endpoint.Endpoint + }{ + { + title: "IngressRouteTCP with hostname annotation", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with host sni rule", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-hostsni-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteTCPSpec{ + Routes: []traefikRouteTCP{ + { + Match: "HostSNI(`b.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "b.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-hostsni-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with multiple host sni rules", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-multi-host-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteTCPSpec{ + Routes: []traefikRouteTCP{ + { + Match: "HostSNI(`d.example.com`) || HostSNI(`e.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "d.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "e.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with multiple host sni rules and annotation", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-multi-host-annotations-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "f.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteTCPSpec{ + Routes: []traefikRouteTCP{ + { + Match: "HostSNI(`g.example.com`, `h.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "f.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "g.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "h.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP omit wildcard host sni", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-omit-wildcard-host", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteTCPSpec{ + Routes: []traefikRouteTCP{ + { + Match: "HostSNI(`*`)", + }, + }, + }, + }, + expected: nil, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + scheme.AddKnownTypes(oldIngressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(oldIngressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(oldIngressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + ir := unstructured.Unstructured{} + + ingressRouteAsJSON, err := json.Marshal(ti.ingressRouteTCP) + assert.NoError(t, err) + + assert.NoError(t, ir.UnmarshalJSON(ingressRouteAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(ingressrouteTCPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(ingressrouteTCPGVR).Namespace(defaultTraefikNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +} + +func TestTraefikProxyIngressRouteUDPEndpoints(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRouteUDP IngressRouteUDP + expected []*endpoint.Endpoint + }{ + { + title: "IngressRouteTCP with hostname annotation", + ingressRouteUDP: IngressRouteUDP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteUDPGVR.GroupVersion().String(), + Kind: "IngressRouteUDP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressrouteudp-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressrouteudp/traefik/ingressrouteudp-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with multiple hostname annotation", + ingressRouteUDP: IngressRouteUDP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteUDPGVR.GroupVersion().String(), + Kind: "IngressRouteUDP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressrouteudp-multi-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com, b.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressrouteudp/traefik/ingressrouteudp-multi-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "b.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressrouteudp/traefik/ingressrouteudp-multi-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + scheme.AddKnownTypes(oldIngressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(oldIngressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(oldIngressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + ir := unstructured.Unstructured{} + + ingressRouteAsJSON, err := json.Marshal(ti.ingressRouteUDP) + assert.NoError(t, err) + + assert.NoError(t, ir.UnmarshalJSON(ingressRouteAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(ingressrouteUDPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(ingressrouteUDPGVR).Namespace(defaultTraefikNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +} + +func TestTraefikProxyOldIngressRouteEndpoints(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRoute IngressRoute + expected []*endpoint.Endpoint + }{ + { + title: "IngressRoute with hostname annotation", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with host rule", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-host-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "Host(`b.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "b.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with hostheader rule", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-hostheader-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "HostHeader(`c.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "c.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-hostheader-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with multiple host rules", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-multi-host-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "Host(`d.example.com`) || Host(`e.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "d.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "e.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with multiple host rules and annotation", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-multi-host-annotations-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "f.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "Host(`g.example.com`, `h.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "f.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "g.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "h.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute omit wildcard", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteGVR.GroupVersion().String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-omit-wildcard-host", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteSpec{ + Routes: []traefikRoute{ + { + Match: "Host(`*`)", + }, + }, + }, + }, + expected: nil, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + scheme.AddKnownTypes(oldIngressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(oldIngressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(oldIngressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + ir := unstructured.Unstructured{} + + ingressRouteAsJSON, err := json.Marshal(ti.ingressRoute) + assert.NoError(t, err) + + assert.NoError(t, ir.UnmarshalJSON(ingressRouteAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(oldIngressrouteGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(oldIngressrouteGVR).Namespace(defaultTraefikNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +} + +func TestTraefikProxyOldIngressRouteTCPEndpoints(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRouteTCP IngressRouteTCP + expected []*endpoint.Endpoint + }{ + { + title: "IngressRouteTCP with hostname annotation", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with host sni rule", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-hostsni-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteTCPSpec{ + Routes: []traefikRouteTCP{ + { + Match: "HostSNI(`b.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "b.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-hostsni-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with multiple host sni rules", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-multi-host-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteTCPSpec{ + Routes: []traefikRouteTCP{ + { + Match: "HostSNI(`d.example.com`) || HostSNI(`e.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "d.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "e.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with multiple host sni rules and annotation", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-multi-host-annotations-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "f.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteTCPSpec{ + Routes: []traefikRouteTCP{ + { + Match: "HostSNI(`g.example.com`, `h.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "f.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "g.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "h.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP omit wildcard host sni", + ingressRouteTCP: IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteTCPGVR.GroupVersion().String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-omit-wildcard-host", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikIngressRouteTCPSpec{ + Routes: []traefikRouteTCP{ + { + Match: "HostSNI(`*`)", + }, + }, + }, + }, + expected: nil, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + scheme.AddKnownTypes(oldIngressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(oldIngressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(oldIngressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + ir := unstructured.Unstructured{} + + ingressRouteAsJSON, err := json.Marshal(ti.ingressRouteTCP) + assert.NoError(t, err) + + assert.NoError(t, ir.UnmarshalJSON(ingressRouteAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(oldIngressrouteTCPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(oldIngressrouteTCPGVR).Namespace(defaultTraefikNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +} + +func TestTraefikProxyOldIngressRouteUDPEndpoints(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRouteUDP IngressRouteUDP + expected []*endpoint.Endpoint + }{ + { + title: "IngressRouteTCP with hostname annotation", + ingressRouteUDP: IngressRouteUDP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteUDPGVR.GroupVersion().String(), + Kind: "IngressRouteUDP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressrouteudp-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressrouteudp/traefik/ingressrouteudp-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with multiple hostname annotation", + ingressRouteUDP: IngressRouteUDP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteUDPGVR.GroupVersion().String(), + Kind: "IngressRouteUDP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressrouteudp-multi-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com, b.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressrouteudp/traefik/ingressrouteudp-multi-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "b.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressrouteudp/traefik/ingressrouteudp-multi-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + scheme.AddKnownTypes(oldIngressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(oldIngressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(oldIngressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + ir := unstructured.Unstructured{} + + ingressRouteAsJSON, err := json.Marshal(ti.ingressRouteUDP) + assert.NoError(t, err) + + assert.NoError(t, ir.UnmarshalJSON(ingressRouteAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(oldIngressrouteUDPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(oldIngressrouteUDPGVR).Namespace(defaultTraefikNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +}