Merge branch 'master' into cloudflare/region

This commit is contained in:
Andrew Hay 2024-11-01 10:39:22 -04:00 committed by GitHub
commit 3b5ef9733b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 2591 additions and 2610 deletions

View File

@ -21,7 +21,7 @@ jobs:
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go 1.x
uses: actions/setup-go@v5

View File

@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install go version
uses: actions/setup-go@v5
with:

View File

@ -15,11 +15,11 @@ jobs:
name: Release Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
cache: "pip"

View File

@ -14,7 +14,7 @@ jobs:
json-yaml-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: json-yaml-validate
uses: GrantBirki/json-yaml-validate@v3.2.1

View File

@ -14,7 +14,7 @@ jobs:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
@ -59,7 +59,7 @@ jobs:
version: latest
- name: Install Python
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
token: ${{ github.token }}
python-version: "3.x"

View File

@ -22,7 +22,7 @@ jobs:
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go 1.x
uses: actions/setup-go@v5

View File

@ -20,7 +20,7 @@ jobs:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0

View File

@ -21,7 +21,7 @@ jobs:
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go 1.x
uses: actions/setup-go@v5

View File

@ -74,20 +74,25 @@ See PR #3063 for all the discussions about it.
Known providers using webhooks:
| Provider | Repo |
| -------- | ----------- |
| Adguard Home Provider | https://github.com/muhlba91/external-dns-provider-adguard |
| Anexia | https://github.com/ProbstenHias/external-dns-anexia-webhook |
| Bizfly Cloud | https://github.com/bizflycloud/external-dns-bizflycloud-webhook |
| Gcore | https://github.com/G-Core/external-dns-gcore-webhook |
| GleSYS | https://github.com/glesys/external-dns-glesys |
| Hetzner | https://github.com/mconfalonieri/external-dns-hetzner-webhook |
| IONOS | https://github.com/ionos-cloud/external-dns-ionos-webhook |
| Infoblox | https://github.com/AbsaOSS/external-dns-infoblox-webhook |
| Netcup | https://github.com/mrueg/external-dns-netcup-webhook |
| RouterOS | https://github.com/benfiola/external-dns-routeros-provider |
| STACKIT | https://github.com/stackitcloud/external-dns-stackit-webhook |
| Unifi | https://github.com/kashalls/external-dns-unifi-webhook |
| Provider | Repo |
|-----------------------|----------------------------------------------------------------------|
| Adguard Home Provider | https://github.com/muhlba91/external-dns-provider-adguard |
| Anexia | https://github.com/ProbstenHias/external-dns-anexia-webhook |
| Bizfly Cloud | https://github.com/bizflycloud/external-dns-bizflycloud-webhook |
| Efficient IP | https://github.com/EfficientIP-Labs/external-dns-efficientip-webhook |
| Gcore | https://github.com/G-Core/external-dns-gcore-webhook |
| GleSYS | https://github.com/glesys/external-dns-glesys |
| Hetzner | https://github.com/mconfalonieri/external-dns-hetzner-webhook |
| Huawei Cloud | https://github.com/setoru/external-dns-huaweicloud-webhook |
| IONOS | https://github.com/ionos-cloud/external-dns-ionos-webhook |
| Infoblox | https://github.com/AbsaOSS/external-dns-infoblox-webhook |
| Mikrotik | https://github.com/mirceanton/external-dns-provider-mikrotik |
| Netcup | https://github.com/mrueg/external-dns-netcup-webhook |
| Netic | https://github.com/neticdk/external-dns-tidydns-webhook |
| RouterOS | https://github.com/benfiola/external-dns-routeros-provider |
| STACKIT | https://github.com/stackitcloud/external-dns-stackit-webhook |
| Unifi | https://github.com/kashalls/external-dns-unifi-webhook |
| Vultr | https://github.com/vultr/external-dns-vultr-webhook |
## Status of in-tree providers
@ -123,7 +128,6 @@ The following table clarifies the current status of the providers according to t
| RFC2136 | Alpha | |
| NS1 | Alpha | |
| TransIP | Alpha | |
| RancherDNS | Alpha | |
| OVH | Alpha | |
| Scaleway DNS | Alpha | @Sh4d1 |
| UltraDNS | Alpha | |
@ -181,10 +185,10 @@ The following tutorials are provided:
* [NS1](docs/tutorials/ns1.md)
* [NS Record Creation with CRD Source](docs/sources/ns-record.md)
* [MX Record Creation with CRD Source](docs/sources/mx-record.md)
* [TXT Record Creation with CRD Source](docs/sources/txt-record.md)
* [OpenStack Designate](docs/tutorials/designate.md)
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
* [PowerDNS](docs/tutorials/pdns.md)
* [RancherDNS (RDNS)](docs/tutorials/rdns.md)
* [RFC2136](docs/tutorials/rfc2136.md)
* [TransIP](docs/tutorials/transip.md)
* [OVH](docs/tutorials/ovh.md)

View File

@ -18,11 +18,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [UNRELEASED]
### Added
- Ability to configure `imagePullSecrets` via helm `global` value ([#4667](https://github.com/kubernetes-sigs/external-dns/pull/4667)) _@jkroepke_
## [v1.15.0] - 2023-09-10
### Changed
- Updated _ExternalDNS_ OCI image version to [v0.15.0](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.15.0). ([#xxxx](https://github.com/kubernetes-sigs/external-dns/pull/xxxx)) _@stevehipwell_
### Fixed
- Fixed `provider.webhook.resources` behavior to correctly leverage resource limits ([#4560](https://github.com/kubernetes-sigs/external-dns/pull/4560))
- Fixed `provider.webhook.imagePullPolicy` behavior to correctly leverage pull policy ([#4643](https://github.com/kubernetes-sigs/external-dns/pull/4643)) _@kimsondrup_
- Add correct webhook metric port to `Service` and `ServiceMonitor` ([#4643](https://github.com/kubernetes-sigs/external-dns/pull/4643)) _@kimsondrup_
- Fixed `provider.webhook.resources` behavior to correctly leverage resource limits. ([#4560](https://github.com/kubernetes-sigs/external-dns/pull/4560)) _@crutonjohn_
- Fixed `provider.webhook.imagePullPolicy` behavior to correctly leverage pull policy. ([#4643](https://github.com/kubernetes-sigs/external-dns/pull/4643)) _@kimsondrup_
- Fixed to add correct webhook metric port to `Service` and `ServiceMonitor`. ([#4643](https://github.com/kubernetes-sigs/external-dns/pull/4643)) _@kimsondrup_
- Fixed to no longer require the unauthenticated webhook provider port to be exposed for health probes. ([#4691](https://github.com/kubernetes-sigs/external-dns/pull/4691)) _@kimsondrup_ & _@hatrx_
## [v1.14.5] - 2023-06-10
@ -193,6 +204,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
RELEASE LINKS
-->
[UNRELEASED]: https://github.com/kubernetes-sigs/external-dns/tree/master/charts/external-dns
[v1.15.0]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.15.0
[v1.14.5]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.14.5
[v1.14.4]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.14.4
[v1.14.3]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.14.3

View File

@ -2,8 +2,8 @@ apiVersion: v2
name: external-dns
description: ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.
type: application
version: 1.14.5
appVersion: 0.14.2
version: 1.15.0
appVersion: 0.15.0
keywords:
- kubernetes
- externaldns
@ -20,15 +20,13 @@ maintainers:
email: steve.hipwell@gmail.com
annotations:
artifacthub.io/changes: |
- kind: added
description: "Added support for `extraContainers` argument."
- kind: added
description: "Added support for setting `excludeDomains` argument."
- kind: changed
description: "Updated _ExternalDNS_ OCI image version to [v0.14.2](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.14.2)."
- kind: changed
description: "Updated `DNSEndpoint` CRD."
- kind: changed
description: "Changed the implementation for `revisionHistoryLimit` to be more generic."
description: "Updated _ExternalDNS_ OCI image version to [v0.15.0](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.15.0)."
- kind: fixed
description: "Fixed the `ServiceMonitor` job name to correctly use the instance label."
description: "Fixed `provider.webhook.resources` behavior to correctly leverage resource limits."
- kind: fixed
description: "Fixed `provider.webhook.imagePullPolicy` behavior to correctly leverage pull policy."
- kind: fixed
description: "Fixed to add correct webhook metric port to `Service` and `ServiceMonitor`."
- kind: fixed
description: "Fixed to no longer require the unauthenticated webhook provider port to be exposed for health probes."

View File

@ -1,6 +1,6 @@
# external-dns
![Version: 1.14.5](https://img.shields.io/badge/Version-1.14.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.14.2](https://img.shields.io/badge/AppVersion-0.14.2-informational?style=flat-square)
![Version: 1.15.0](https://img.shields.io/badge/Version-1.15.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.15.0](https://img.shields.io/badge/AppVersion-0.15.0-informational?style=flat-square)
ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.
@ -27,7 +27,7 @@ helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
After you've installed the repo you can install the chart.
```shell
helm upgrade --install external-dns external-dns/external-dns --version 1.14.5
helm upgrade --install external-dns external-dns/external-dns --version 1.15.0
```
## Providers
@ -105,6 +105,7 @@ If `namespaced` is set to `true`, please ensure that `sources` my only contains
| extraVolumeMounts | list | `[]` | Extra [volume mounts](https://kubernetes.io/docs/concepts/storage/volumes/) for the `external-dns` container. |
| extraVolumes | list | `[]` | Extra [volumes](https://kubernetes.io/docs/concepts/storage/volumes/) for the `Pod`. |
| fullnameOverride | string | `nil` | Override the full name of the chart. |
| global.imagePullSecrets | list | `[]` | Global image pull secrets. |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy for the `external-dns` container. |
| image.repository | string | `"registry.k8s.io/external-dns/external-dns"` | Image repository for the `external-dns` container. |
| image.tag | string | `nil` | Image tag for the `external-dns` container, this will default to `.Chart.AppVersion` if not set. |
@ -133,7 +134,7 @@ If `namespaced` is set to `true`, please ensure that `sources` my only contains
| provider.webhook.readinessProbe | object | See _values.yaml_ | [Readiness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) configuration for the `webhook` container. |
| provider.webhook.resources | object | `{}` | [Resources](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) for the `webhook` container. |
| provider.webhook.securityContext | object | See _values.yaml_ | [Pod security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container) for the `webhook` container. |
| provider.webhook.service.metricsPort | int | `8080` | Webhook metrics port for the service. |
| provider.webhook.service.port | int | `8080` | Webhook exposed HTTP port for the service. |
| provider.webhook.serviceMonitor | object | See _values.yaml_ | Optional [Service Monitor](https://prometheus-operator.dev/docs/operator/design/#servicemonitor) configuration for the `webhook` container. |
| rbac.additionalPermissions | list | `[]` | Additional rules to add to the `ClusterRole`. |
| rbac.create | bool | `true` | If `true`, create a `ClusterRole` & `ClusterRoleBinding` with access to the Kubernetes API. |

View File

@ -40,7 +40,7 @@ spec:
{{- if not (quote .Values.automountServiceAccountToken | empty) }}
automountServiceAccountToken: {{ .Values.automountServiceAccountToken }}
{{- end }}
{{- with .Values.imagePullSecrets }}
{{- with (default .Values.global.imagePullSecrets .Values.imagePullSecrets) }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
@ -158,9 +158,6 @@ spec:
{{- end }}
ports:
- name: http-webhook
protocol: TCP
containerPort: 8888
- name: http-wh-metrics
protocol: TCP
containerPort: 8080
livenessProbe:

View File

@ -28,9 +28,9 @@ spec:
protocol: TCP
{{- if eq $providerName "webhook" }}
{{- with .Values.provider.webhook.service }}
- name: http-wh-metrics
port: {{ .metricsPort }}
targetPort: http-wh-metrics
- name: http-webhook
port: {{ .port }}
targetPort: http-webhook
protocol: TCP
{{- end }}
{{- end }}

View File

@ -51,7 +51,7 @@ spec:
{{- end }}
{{- if eq $providerName "webhook" }}
{{- with .Values.provider.webhook.serviceMonitor }}
- port: http-wh-metrics
- port: http-webhook
path: /metrics
{{- with .interval }}
interval: {{ . }}

View File

@ -2,6 +2,9 @@
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"properties": {
"global": {
"type": "object"
},
"provider": {
"anyOf": [
{

View File

@ -2,6 +2,10 @@
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
global:
# -- Global image pull secrets.
imagePullSecrets: []
image:
# -- Image repository for the `external-dns` container.
repository: registry.k8s.io/external-dns/external-dns
@ -270,8 +274,8 @@ provider:
failureThreshold: 6
successThreshold: 1
service:
# -- Webhook metrics port for the service.
metricsPort: 8080
# -- Webhook exposed HTTP port for the service.
port: 8080
# -- Optional [Service Monitor](https://prometheus-operator.dev/docs/operator/design/#servicemonitor) configuration for the `webhook` container.
# @default -- See _values.yaml_
serviceMonitor:

View File

@ -1,12 +1,12 @@
# MX record with CRD source
You can create and manage MX records with the help of [CRD source](../contributing/crd-source.md)
and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, and `google` providers.
and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, `google` and `digitalocean` 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
external-dns --source crd --provider {aws|azure|google|digitalocean} --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

View File

@ -7,6 +7,9 @@ The node source adds an `A` record per each node `externalIP` (if not found, any
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.
Nodes marked as **Unschedulable** as per [core/v1/NodeSpec](https://pkg.go.dev/k8s.io/api@v0.31.1/core/v1#NodeSpec) are excluded.
This avoid exposing Unhealthy, NotReady or SchedulingDisabled (cordon) nodes.
## Manifest (for cluster without RBAC enabled)
```

View File

@ -0,0 +1,30 @@
# Creating TXT record with CRD source
You can create and manage TXT records with the help of [CRD source](../contributing/crd-source.md)
and `DNSEndpoint` CRD. Currently, this feature is only supported by `digitalocean` providers.
In order to start managing TXT records you need to set the `--managed-record-types TXT` flag.
```console
external-dns --source crd --provider {digitalocean} --managed-record-types A --managed-record-types CNAME --managed-record-types TXT
```
Targets within the CRD need to be specified according to the RFC 1035 (section 3.3.14). Below is an example of
`example.com` DNS TXT two records creation.
**NOTE** Current implementation do not support RFC 6763 (section 6).
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
name: examplemxrecord
spec:
endpoints:
- dnsName: example.com
recordTTL: 180
recordType: TXT
targets:
- SOMETXT
- ANOTHERTXT
```

View File

@ -12,7 +12,7 @@ Learn more about the API in the [AWS Cloud Map API Reference](https://docs.aws.a
## IAM Permissions
To use the AWS Cloud Map API, a user must have permissions to create the DNS namespace. Additionally you need to make sure that your nodes (on which External DNS runs) have an IAM instance profile with the `AWSCloudMapFullAccess` managed policy attached, that provides following permissions:
To use the AWS Cloud Map API, a user must have permissions to create the DNS namespace. You need to make sure that your nodes (on which External DNS runs) have an IAM instance profile with the `AWSCloudMapFullAccess` managed policy attached, that provides following permissions:
```
{
@ -42,6 +42,82 @@ To use the AWS Cloud Map API, a user must have permissions to create the DNS nam
}
```
### IAM Permissions with ABAC
You can use Attribute-based access control(ABAC) for advanced deployments.
You can define AWS tags that are applied to services created by the controller. By doing so, you can have precise control over your IAM policy to limit the scope of the permissions to services managed by the controller, rather than having to grant full permissions on your entire AWS account.
To pass tags to service creation, use either CLI flags or environment variables:
*cli:* `--aws-sd-create-tag=key1=value1 --aws-sd-create-tag=key2=value2`
*environment:* `EXTERNAL_DNS_AWS_SD_CREATE_TAG=key1=value1\nkey2=value2`
Using tags, your `servicediscovery` policy can become:
```
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"servicediscovery:ListNamespaces",
"servicediscovery:ListServices"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"servicediscovery:CreateService",
"servicediscovery:TagResource"
],
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"aws:RequestTag/YOUR_TAG_KEY": "YOUR_TAG_VALUE"
}
}
},
{
"Effect": "Allow",
"Action": [
"servicediscovery:DiscoverInstances"
],
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"servicediscovery:NamespaceName": "YOUR_NAMESPACE_NAME"
}
}
},
{
"Effect": "Allow",
"Action": [
"servicediscovery:RegisterInstance",
"servicediscovery:DeregisterInstance",
"servicediscovery:DeleteService",
"servicediscovery:UpdateService"
],
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"aws:ResourceTag/YOUR_TAG_KEY": "YOUR_TAG_VALUE"
}
}
}
]
}
```
## Set up a namespace
Create a DNS namespace using the AWS Cloud Map API:
@ -228,6 +304,32 @@ spec:
This will set the TTL for the DNS record to 60 seconds.
## IPv6 Support
If your Kubernetes cluster is configured with IPv6 support, such as an [EKS cluster with IPv6 support](https://docs.aws.amazon.com/eks/latest/userguide/deploy-ipv6-cluster.html), ExternalDNS can
also create AAAA DNS records.
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
external-dns.alpha.kubernetes.io/ttl: "60"
spec:
ipFamilies:
- "IPv6"
type: NodePort
ports:
- port: 80
name: http
targetPort: 80
selector:
app: nginx
```
:information_source: The AWS-SD provider does not currently support dualstack load balancers and will only create A records for these at this time. See the AWS provider and the [AWS Load Balancer Controller Tutorial](./aws-load-balancer-controller.md) for dualstack load balancer support.
## Clean up

View File

@ -98,6 +98,10 @@ $ az role assignment create --role "Reader" --assignee <appId GUID> --scope <res
$ az role assignment create --role "Private DNS Zone Contributor" --assignee <appId GUID> --scope <dns zone resource id>
```
## Throttling
When the ExternalDNS managed zones list doesn't change frequently, one can set `--azure-zones-cache-duration` (zones list cache time-to-live). The zones list cache is disabled by default, with a value of 0s.
## Deploy ExternalDNS
Configure `kubectl` to be able to communicate and authenticate with your cluster.
This is per default done through the file `~/.kube/config`.

View File

@ -480,6 +480,10 @@ NOTE: it's also possible to specify (or override) ClientID through `userAssigned
NOTE: make sure the pod is restarted whenever you make a configuration change.
## Throttling
When the ExternalDNS managed zones list doesn't change frequently, one can set `--azure-zones-cache-duration` (zones list cache time-to-live). The zones list cache is disabled by default, with a value of 0s.
## Ingress used with ExternalDNS
This deployment assumes that you will be using nginx-ingress. When using nginx-ingress do not deploy it as a Daemon Set. This causes nginx-ingress to write the Cluster IP of the backend pods in the ingress status.loadbalancer.ip property which then has external-dns write the Cluster IP(s) in DNS vs. the nginx-ingress service external IP.

View File

@ -1,6 +1,6 @@
# GoDaddy
This tutorial describes how to setup ExternalDNS for use within a
This tutorial describes how to set up ExternalDNS for use within a
Kubernetes cluster using GoDaddy DNS.
Make sure to use **>=0.6** version of ExternalDNS for this tutorial.
@ -26,7 +26,7 @@ Connect your `kubectl` client to the cluster with which you want to test Externa
## Using Helm
Create a values.yaml file to configure ExternalDNS to use NS1 as the DNS provider. This file should include the necessary environment variables:
Create a values.yaml file to configure ExternalDNS to use GoDaddy as the DNS provider. This file should include the necessary environment variables:
```shell
provider:
@ -36,7 +36,7 @@ extraArgs:
- --godaddy-api-secret=YOUR_API_SECRET
```
Ensure to replace YOUR_API_KEY and YOUR_API_SECRET with your actual godaddy API key and godaddy API secret.
Be sure to replace YOUR_API_KEY and YOUR_API_SECRET with your actual GoDaddy API key and GoDaddy API secret.
Finally, install the ExternalDNS chart with Helm using the configuration specified in your values.yaml file:

View File

@ -47,6 +47,7 @@ spec:
- --source=service # or ingress or both
- --provider=pdns
- --pdns-server={{ pdns-api-url }}
- --pdns-server-id={{ pdns-server-id }}
- --pdns-api-key={{ pdns-http-api-key }}
- --txt-owner-id={{ owner-id-for-this-external-dns }}
- --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the zones matching provided domain; omit to process all available zones in PowerDNS
@ -172,3 +173,102 @@ Once the API shows the record correctly, you can double check your record using:
```bash
$ dig @${PDNS_FQDN} echo.example.com.
```
## Using CRD source to manage DNS records in PowerDNS
[CRD source](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/contributing/crd-source.md) provides a generic mechanism and declarative way to manage DNS records in PowerDNS using external-dns.
```bash
external-dns --source=crd --provider=pdns \
--pdns-server={{ pdns-api-url }} \
--pdns-api-key={{ pdns-api-key }} \
--domain-filter=example.com \
--managed-record-types=A \
--managed-record-types=CNAME \
--managed-record-types=TXT \
--managed-record-types=MX \
--managed-record-types=SRV
```
Not all the record types are enabled by default so we can enable the required record types using `--managed-record-types`.
* Example for record type `A`
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
name: examplearecord
spec:
endpoints:
- dnsName: example.com
recordTTL: 60
recordType: A
targets:
- 10.0.0.1
```
* Example for record type `CNAME`
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
name: examplecnamerecord
spec:
endpoints:
- dnsName: test-a.example.com
recordTTL: 300
recordType: CNAME
targets:
- example.com
```
* Example for record type `TXT`
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
name: exampletxtrecord
spec:
endpoints:
- dnsName: example.com
recordTTL: 3600
recordType: TXT
targets:
- '"v=spf1 include:spf.protection.example.com include:example.org -all"'
- '"apple-domain-verification=XXXXXXXXXXXXX"'
```
* Example for record type `MX`
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
name: examplemxrecord
spec:
endpoints:
- dnsName: example.com
recordTTL: 3600
recordType: MX
targets:
- "10 mailhost1.example.com"
```
* Example for record type `SRV`
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
name: examplesrvrecord
spec:
endpoints:
- dnsName: _service._tls.example.com
recordTTL: 180
recordType: SRV
targets:
- "100 1 443 service.example.com"
```

View File

@ -1,173 +0,0 @@
# RancherDNS
This tutorial describes how to setup ExternalDNS for usage within a kubernetes cluster that makes use of [RDNS](https://github.com/rancher/rdns-server) and [nginx ingress controller](https://github.com/kubernetes/ingress-nginx).
You need to:
* install RDNS with [etcd](https://github.com/etcd-io/etcd) enabled
* install external-dns with rdns as a provider
## Installing RDNS with etcdv3 backend
### Clone RDNS
```
git clone https://github.com/rancher/rdns-server.git
```
### Installing ETCD
```
cd rdns-server
docker-compose -f deploy/etcdv3/etcd-compose.yaml up -d
```
> ETCD was successfully deployed on `http://172.31.35.77:2379`
### Installing RDNS
```
export ETCD_ENDPOINTS="http://172.31.35.77:2379"
export DOMAIN="lb.rancher.cloud"
./scripts/start etcdv3
```
> RDNS was successfully deployed on `172.31.35.77`
## Installing ExternalDNS
### Install external ExternalDNS
ETCD_URLS is configured to etcd client service address.
RDNS_ROOT_DOMAIN is configured to the same with RDNS DOMAIN environment. e.g. lb.rancher.cloud.
#### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: kube-system
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.15.0
args:
- --source=ingress
- --provider=rdns
- --log-level=debug # debug only
env:
- name: ETCD_URLS
value: http://172.31.35.77:2379
- name: RDNS_ROOT_DOMAIN
value: lb.rancher.cloud
```
#### Manifest (for clusters with RBAC enabled)
```yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["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: kube-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: kube-system
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.15.0
args:
- --source=ingress
- --provider=rdns
- --log-level=debug # debug only
env:
- name: ETCD_URLS
value: http://172.31.35.77:2379
- name: RDNS_ROOT_DOMAIN
value: lb.rancher.cloud
```
## Testing ingress example
```
$ cat ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx
spec:
ingressClassName: nginx
rules:
- host: nginx.lb.rancher.cloud
http:
paths:
- backend:
serviceName: nginx
servicePort: 80
$ kubectl apply -f ingress.yaml
ingress.extensions "nginx" created
```
Wait a moment until DNS has the ingress IP. The RDNS IP in this example is "172.31.35.77".
```
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
nginx nginx.lb.rancher.cloud 172.31.42.211 80 2m
$ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools
If you don't see a command prompt, try pressing enter.
dnstools# dig @172.31.35.77 nginx.lb.rancher.cloud +short
172.31.42.211
dnstools#
```

View File

@ -16,24 +16,32 @@ Providers implementing the HTTP API have to keep in sync with changes to the JSO
The following table represents the methods to implement mapped to their HTTP method and route.
| Provider method | HTTP Method | Route |
| --- | --- | --- |
| Records | GET | /records |
| AdjustEndpoints | POST | /adjustendpoints |
| ApplyChanges | POST | /records |
| K8s probe | GET | /healthz |
### Provider endpoints
| Provider method | HTTP Method | Route | Description |
| --------------- | ----------- | ---------------- | ---------------------------------------- |
| Negotiate | GET | / | Negotiate `DomainFilter` |
| Records | GET | /records | Get records |
| AdjustEndpoints | POST | /adjustendpoints | Provider specific adjustments of records |
| ApplyChanges | POST | /records | Apply record |
ExternalDNS will also make requests to the `/` endpoint for negotiation and for deserialization of the `DomainFilter`.
The server needs to respond to those requests by reading the `Accept` header and responding with a corresponding `Content-Type` header specifying the supported media type format and version.
The default recommended port is 8888, and should listen only on localhost (ie: only accessible for k8s probes and external-dns).
The default recommended port for the provider endpoints is `8888`, and should listen only on `localhost` (ie: only accessible for external-dns).
**NOTE**: only `5xx` responses will be retried and only `20x` will be considered as successful. All status codes different from those will be considered a failure on ExternalDNS's side.
## Metrics support
### Exposed endpoints
The metrics should listen ":8080" on `/metrics` following [Open Metrics](https://github.com/OpenObservability/OpenMetrics) format.
| Provider method | HTTP Method | Route | Description |
| --------------- | ----------- | -------- | -------------------------------------------------------------------------------------------- |
| K8s probe | GET | /healthz | Used by `livenessProbe` and `readinessProbe` |
| Open Metrics | GET | /metrics | Optional endpoint to expose [Open Metrics](https://github.com/OpenObservability/OpenMetrics) |
The default recommended port for the exposed endpoints is `8080`, and it should be bound to all interfaces (`0.0.0.0`)
## Custom Annotations

128
go.mod
View File

@ -3,28 +3,35 @@ module sigs.k8s.io/external-dns
go 1.23
require (
cloud.google.com/go/compute/metadata v0.5.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
cloud.google.com/go/compute/metadata v0.5.2
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.17.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.18.1
github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.5.0
github.com/IBM/go-sdk-core/v5 v5.17.4
github.com/IBM/go-sdk-core/v5 v5.18.1
github.com/IBM/networking-go-sdk v0.49.0
github.com/Yamashou/gqlgenc v0.24.0
github.com/Yamashou/gqlgenc v0.25.3
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/aliyun/alibaba-cloud-sdk-go v1.63.0
github.com/aws/aws-sdk-go v1.55.5
github.com/aliyun/alibaba-cloud-sdk-go v1.63.39
github.com/aws/aws-sdk-go-v2 v1.32.2
github.com/aws/aws-sdk-go-v2/config v1.28.0
github.com/aws/aws-sdk-go-v2/credentials v1.17.41
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.12
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.36.2
github.com/aws/aws-sdk-go-v2/service/route53 v1.45.2
github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.33.2
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2
github.com/bodgit/tsig v1.2.2
github.com/cenkalti/backoff/v4 v4.3.0
github.com/civo/civogo v0.3.73
github.com/cloudflare/cloudflare-go v0.102.0
github.com/civo/civogo v0.3.84
github.com/cloudflare/cloudflare-go v0.108.0
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/datawire/ambassador v1.12.4
github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace
github.com/digitalocean/godo v1.120.0
github.com/digitalocean/godo v1.128.0
github.com/dnsimple/dnsimple-go v1.7.0
github.com/exoscale/egoscale v0.102.3
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
@ -32,59 +39,69 @@ require (
github.com/go-logr/logr v1.4.2
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/gophercloud/gophercloud v1.14.0
github.com/gophercloud/gophercloud v1.14.1
github.com/linki/instrumented_http v0.3.0
github.com/linode/linodego v1.39.0
github.com/linode/linodego v1.42.0
github.com/maxatome/go-testdeep v1.14.0
github.com/miekg/dns v1.1.62
github.com/onsi/ginkgo v1.16.5
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.71.1
github.com/oracle/oci-go-sdk/v65 v65.77.1
github.com/ovh/go-ovh v1.6.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/pluralsh/gqlclient v1.12.2
github.com/projectcontour/contour v1.30.0
github.com/prometheus/client_golang v1.20.0
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.29
github.com/prometheus/client_golang v1.20.5
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.984
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.984
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.984
github.com/transip/gotransip/v6 v6.25.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1030
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1030
github.com/transip/gotransip/v6 v6.26.0
github.com/ultradns/ultradns-sdk-go v1.3.7
go.etcd.io/etcd/api/v3 v3.5.15
go.etcd.io/etcd/client/v3 v3.5.15
go.etcd.io/etcd/client/v3 v3.5.16
go.uber.org/ratelimit v0.3.1
golang.org/x/net v0.28.0
golang.org/x/oauth2 v0.22.0
golang.org/x/net v0.30.0
golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.8.0
golang.org/x/time v0.6.0
google.golang.org/api v0.192.0
gopkg.in/ns1/ns1-go.v2 v2.12.0
golang.org/x/time v0.7.0
google.golang.org/api v0.203.0
gopkg.in/ns1/ns1-go.v2 v2.12.2
gopkg.in/yaml.v2 v2.4.0
istio.io/api v1.23.0
istio.io/client-go v1.23.0
k8s.io/api v0.31.0
k8s.io/apimachinery v0.31.0
k8s.io/client-go v0.31.0
istio.io/api v1.23.3
istio.io/client-go v1.23.3
k8s.io/api v0.31.2
k8s.io/apimachinery v0.31.2
k8s.io/client-go v0.31.2
k8s.io/klog/v2 v2.130.1
sigs.k8s.io/gateway-api v1.1.0
sigs.k8s.io/gateway-api v1.2.0
)
require (
cloud.google.com/go/auth v0.8.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
cloud.google.com/go/auth v0.9.9 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect
github.com/99designs/gqlgen v0.17.44 // indirect
github.com/99designs/gqlgen v0.17.54 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/Masterminds/semver v1.4.2 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect
github.com/aws/smithy-go v1.22.0 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@ -119,7 +136,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@ -163,33 +180,34 @@ require (
github.com/schollz/progressbar/v3 v3.8.6 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sony/gobreaker v0.5.0 // indirect
github.com/sosodev/duration v1.2.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/terra-farm/udnssdk v1.3.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.14 // indirect
github.com/vektah/gqlparser/v2 v2.5.16 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
go.etcd.io/etcd/api/v3 v3.5.16 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect

278
go.sum
View File

@ -2,34 +2,36 @@ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxo
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go/auth v0.8.1 h1:QZW9FjC5lZzN864p13YxvAtGUlQ+KgRL+8Sg45Z6vxo=
cloud.google.com/go/auth v0.8.1/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ=
cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
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/99designs/gqlgen v0.17.44 h1:OS2wLk/67Y+vXM75XHbwRnNYJcbuJd4OBL76RX3NQQA=
github.com/99designs/gqlgen v0.17.44/go.mod h1:UTCu3xpK2mLI5qcMNw+HKDiEL77it/1XtAjisC4sLwM=
github.com/99designs/gqlgen v0.17.54 h1:AsF49k/7RJlwA00RQYsYN0T8cQuaosnV/7G1dHC3Uh8=
github.com/99designs/gqlgen v0.17.54/go.mod h1:77/+pVe6zlTsz++oUg2m8VLgzdUPHxjoAG3BxI5y8Rc=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible h1:KnPIugL51v3N3WwvaSmZbxukD1WuWXOiE9fRdu32f2I=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 h1:9Eih8XcEeQnFD0ntMlUDleKMzfeCeUfa+VbnDCI4AZs=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0/go.mod h1:wGPyTi+aURdqPAGMZDQqnNs9IrShADF8w2WZb6bKeq0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
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/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
@ -39,18 +41,20 @@ github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxB
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
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/k8s-bigip-ctlr/v2 v2.17.1 h1:DsX6HIG2kxYg2bRM7jIVgyBb6PcJXBt9iCsTfLvFwWE=
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.17.1/go.mod h1:mMF9pk71U8aIzMBS+CWq8OL3gLcFCRCiy+wNpE4gDIE=
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.18.1 h1:2JJzYLLONOJR73uudihkDh2ks8jL8/h6AhO7cbYpdIo=
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.18.1/go.mod h1:NZ7znr7KT1/0+MFPA9MHBsa4e0YhVz65ZCioi/m5KiE=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.5.0 h1:+a994rHmNFwlSA609Z6SYhn9xt+lhGFF+dsgjMF75hY=
github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.5.0/go.mod h1:XxWyb5MQDU4GnRBSDZpGgIFwfbcn+GAUbPKS8CR8Bxc=
github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc=
github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns=
github.com/IBM/go-sdk-core/v5 v5.18.1 h1:wdftQO8xejECTWTKF3FGXyW0McKxxDAopH7MKwA187c=
github.com/IBM/go-sdk-core/v5 v5.18.1/go.mod h1:3ywpylZ41WhWPusqtpJZWopYlt2brebcphV7mA2JncU=
github.com/IBM/networking-go-sdk v0.49.0 h1:lPS34u3C0JVrbxH+Ulua76Nwl6Frv8BEfq6LRkyvOv0=
github.com/IBM/networking-go-sdk v0.49.0/go.mod h1:G9CKbmPE8gSLjN+ABh4hIZ1bMx076enl5Eekvj6zQnA=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@ -77,8 +81,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:H
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/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/Yamashou/gqlgenc v0.24.0 h1:Aeufjb2zF0XxkeSTAVQ+DfiHL+ney/M2ovShZozBmHw=
github.com/Yamashou/gqlgenc v0.24.0/go.mod h1:3QQD8ZoeEyVXuzqcMDsl8OfCCCTk+ulaxkvFFQDupIA=
github.com/Yamashou/gqlgenc v0.25.3 h1:mVV8/Ho8EDZUQKQZbQqqXGNq8jc8aQfPpHhZOnTkMNE=
github.com/Yamashou/gqlgenc v0.25.3/go.mod h1:G0g1N81xpIklVdnyboW1zwOHcj/n4hNfhTwfN29Rjig=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
@ -94,8 +98,8 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAu
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.63.0 h1:GIwkDPfeF/IBh5lZ5Mig50r1LXomNXR7t/oKGSMJWns=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.0/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.39 h1:zlenrBGDiSEu7YnpWiAPscKNolgIo9Z6jvM5pcWAEL4=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.39/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
@ -116,9 +120,45 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W
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.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI=
github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ=
github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc=
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8=
github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.12 h1:zYf8E8zaqolHA5nQ+VmX2r3wc4K6xw5i6xKvvMjZBL0=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.12/go.mod h1:vYGIVLASk19Gb0FGwAcwES+qQF/aekD7m2G/X6mBOdQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.36.2 h1:kJqyYcGqhWFmXqjRrtFFD4Oc9FXiskhsll2xnlpe8Do=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.36.2/go.mod h1:+t2Zc5VNOzhaWzpGE+cEYZADsgAAQT5v55AO+fhU+2s=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.2 h1:E7Tuo0ipWpBl0f3uThz8cZsuyD5H8jLCnbtbKR4YL2s=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.2/go.mod h1:txOfweuNPBLhHodsV+C2lvPPRTommVTWbts9SZV6Myc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.2 h1:1G7TTQNPNv5fhCyIQGYk8FOggLgkzKq6c4Y1nOGzAOE=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.2/go.mod h1:+ybYGLXoF7bcD7wIcMcklxyABZQmuBf1cHUhvY6FGIo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0=
github.com/aws/aws-sdk-go-v2/service/route53 v1.45.2 h1:P4ElvGTPph12a87YpxPDIqCvVICeYJFV32UMMS/TIPc=
github.com/aws/aws-sdk-go-v2/service/route53 v1.45.2/go.mod h1:zLKE53MjadFH0VYrDerAx25brxLYiSg4Vk3C+qPY4BQ=
github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.33.2 h1:TVfX2jnpYDxgORh5ozbSBpFa/D0B82Iq28a2+bY62uQ=
github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.33.2/go.mod h1:Qy6c/ZAKohV1Ikot1ZOMm9be4bazUs27RLQjnERG4/U=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo=
github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -147,12 +187,12 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
github.com/civo/civogo v0.3.73 h1:thkNnkziU+xh+MEOChIUwRZI1forN20+SSAPe/VFDME=
github.com/civo/civogo v0.3.73/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM=
github.com/civo/civogo v0.3.84 h1:jf5IT7VJFPaReO6g8B0zqKhsYCIizaGo4PjDLY7Sl6Y=
github.com/civo/civogo v0.3.84/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM=
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.102.0 h1:+0MGbkirM/yzVLOYpWMgW7CDdKzesSbdwA2Y+rABrWI=
github.com/cloudflare/cloudflare-go v0.102.0/go.mod h1:BOB41tXf31ti/qtBO9paYhyapotQbGRDbQoLOAF7pSg=
github.com/cloudflare/cloudflare-go v0.108.0 h1:C4Skfjd8I8X3uEOGmQUT4/iGyZcWdkIU7HwvMoLkEE0=
github.com/cloudflare/cloudflare-go v0.108.0/go.mod h1:m492eNahT/9MsN7Ppnoge8AaI7QhVFtEgVm3I9HJFeU=
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/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@ -214,9 +254,11 @@ github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace h1:1SnCTPFh2AA
github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace/go.mod h1:TK05uvk4XXfK2kdvRwfcZ1NaxjDxmm7H3aQLko0mJxA=
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-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/digitalocean/godo v1.120.0 h1:t2DpzIitSnCDNQM7svSW4+cZd8E4Lv6+r8y33Kym0Xw=
github.com/digitalocean/godo v1.120.0/go.mod h1:WQVH83OHUy6gC4gXpEVQKtxTd4L5oCp+5OialidkPLY=
github.com/digitalocean/godo v1.128.0 h1:cGn/ibMSRZ9+8etbzMv2MnnCEPTTGlEnx3HHTPwdk1U=
github.com/digitalocean/godo v1.128.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY=
github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8=
@ -261,8 +303,8 @@ github.com/exoscale/egoscale v0.102.3/go.mod h1:RPf2Gah6up+6kAEayHTQwqapzXlm93f0
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
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.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@ -487,8 +529,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
@ -497,8 +539,8 @@ github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTV
github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
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 v1.14.0 h1:Bt9zQDhPrbd4qX7EILGmy+i7GP35cc+AAL2+wIJpUE8=
github.com/gophercloud/gophercloud v1.14.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=
github.com/gophercloud/gophercloud v1.14.1/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=
@ -618,6 +660,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
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=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -668,8 +712,8 @@ 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.39.0 h1:gRsj2PXX+HTO3eYQaXEuQGsLeeLFDSBDontC5JL3Nn8=
github.com/linode/linodego v1.39.0/go.mod h1:da8KzAQKSm5obwa06yXk5CZSDFMP9Wb08GA/O+aR9W0=
github.com/linode/linodego v1.42.0 h1:ZSbi4MtvwrfB9Y6bknesorvvueBGGilcmh2D5dq76RM=
github.com/linode/linodego v1.42.0/go.mod h1:2yzmY6pegPBDgx2HDllmt0eIk2IlzqcgK6NR0wFCFRY=
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=
@ -813,8 +857,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/oracle/oci-go-sdk/v65 v65.71.1 h1:t1GpyLYaD/x2OrUoSyxNwBQaDaQP4F084FX8LQMXA/s=
github.com/oracle/oci-go-sdk/v65 v65.71.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
github.com/oracle/oci-go-sdk/v65 v65.77.1 h1:gqjTXIUWvTihkn470AclxSAMcR1JecqjD2IUtp+sDIU=
github.com/oracle/oci-go-sdk/v65 v65.77.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso=
@ -858,8 +902,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.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI=
github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
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=
@ -890,6 +934,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
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/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
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=
@ -908,8 +954,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.29 h1:BkTk4gynLjguayxrYxZoMZjBnAOh7ntQvUkOFmkMqPU=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.29/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8=
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=
@ -938,8 +984,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us=
github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
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=
@ -981,19 +1027,19 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.984 h1:QLSx+ibsV68NXKgzofPuo1gxFwYSWk2++rvxZxNjbVo=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.984/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.984 h1:ABZeSsOOkkBn+gToVp8KkMt4E69hQkBMEFegCD4w15Q=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.984/go.mod h1:r++X8dKvTZWltr4J83TIwqGlyvG5fKaVh7RGC2+BryI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.984 h1:dD0pLtMCJyRNMTystzaZ9WAK+UBb2ymGcdbkhtAub+8=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.984/go.mod h1:hWQvbAt8kqN3JLfVpgxsn2YNxDBLaiUaXZFtF4ymRjU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030 h1:kwiUoCkooUgy7iPyhEEbio7WT21kGJUeZ5JeJfb/dYk=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1030 h1:U1n/Jr0rpKctFrTOsONkZlkbQeNiLqDbi5+MS7fGMO4=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1030/go.mod h1:erWu7r8lbWSjjVuWOlLFziOEY0K674qAOWHVSvdSvrY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1030 h1:reuw5wChMq2nXPYrRDrU7XRXU3kDvXh87kk8GMa80no=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1030/go.mod h1:gPK/7DZzJ0CKMT3Fg8de70rkvg/ClcUUOMvfAkPVZ88=
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/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.25.0 h1:/H+SjMq/9HNZ0/maE1OLhJpxLaCGHsxq0PWaMPJHxK4=
github.com/transip/gotransip/v6 v6.25.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550=
github.com/transip/gotransip/v6 v6.26.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
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=
@ -1013,8 +1059,8 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
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.14 h1:dzLq75BJe03jjQm6n56PdH1oweB8ana42wj7E4jRy70=
github.com/vektah/gqlparser/v2 v2.5.14/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w=
github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@ -1039,12 +1085,12 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK
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/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0=
go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28=
go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q=
go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E=
go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE=
go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50=
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=
@ -1057,14 +1103,14 @@ 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.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
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=
@ -1111,8 +1157,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1138,8 +1184,8 @@ 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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=
@ -1182,14 +1228,14 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
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.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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=
@ -1259,8 +1305,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1268,8 +1314,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1281,8 +1327,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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=
@ -1290,8 +1336,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb
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.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1326,8 +1372,8 @@ golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4f
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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
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=
@ -1340,8 +1386,8 @@ gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZ
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.192.0 h1:PljqpNAfZaaSpS+TnANfnNAXKdzHM/B9bKhwRlo7JP0=
google.golang.org/api v0.192.0/go.mod h1:9VcphjvAxPKLmSxVSzPlSRXy/5ARMEw5bf58WoVXafQ=
google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU=
google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI=
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=
@ -1356,10 +1402,10 @@ google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dT
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f h1:b1Ln/PG8orm0SsBbHZWke8dDp2lrCD4jSmfglFpTZbk=
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
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=
@ -1374,8 +1420,8 @@ 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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
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=
@ -1388,8 +1434,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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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=
@ -1416,8 +1462,8 @@ gopkg.in/ini.v1 v1.51.1/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.12.0 h1:cqdqQoTx17JmTusfxh5m3e2b36jfUzFAZedv89pFX18=
gopkg.in/ns1/ns1-go.v2 v2.12.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
gopkg.in/ns1/ns1-go.v2 v2.12.2 h1:SPM5BTTMJ1zVBhMMiiPFdF7l6Y3fq5o7bKM7jDqsUfM=
gopkg.in/ns1/ns1-go.v2 v2.12.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
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=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@ -1445,23 +1491,23 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
istio.io/api v1.23.0 h1:yqv3lNW6XSYS5XkbEkxsmFROXIQznp4lFWqj7xKEqCA=
istio.io/api v1.23.0/go.mod h1:QPSTGXuIQdnZFEm3myf9NZ5uBMwCdJWUvfj9ZZ+2oBM=
istio.io/client-go v1.23.0 h1://xojbifr84q29WE3eMx74p36hD4lvcejX1KxE3iJvY=
istio.io/client-go v1.23.0/go.mod h1:3qX/KBS5aR47QV4JhphcZl5ysnZ53x78TBjNQLM2TC4=
istio.io/api v1.23.3 h1:+CP0AHz8/+WJ7ZKJLbilHEiqBCi5KLe1Yil9bJI39ow=
istio.io/api v1.23.3/go.mod h1:QPSTGXuIQdnZFEm3myf9NZ5uBMwCdJWUvfj9ZZ+2oBM=
istio.io/client-go v1.23.3 h1:rs+mO4A+NaXVcZgDO0RRZE7KRAlDooq2PSkxl7tevig=
istio.io/client-go v1.23.3/go.mod h1:Lfa3anzx7/kCOpcAciR+JiRMj/SYuzDcbXQDjkThnLg=
k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8=
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.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
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/apimachinery v0.18.0/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.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
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=
@ -1470,8 +1516,8 @@ 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.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.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
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=
@ -1509,8 +1555,8 @@ sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gE
sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw=
sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg=
sigs.k8s.io/controller-tools v0.3.1-0.20200517180335-820a4a27ea84/go.mod h1:enhtKGfxZD1GFEoMgP8Fdbu+uKQ/cq1/WGJhdVChfvI=
sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM=
sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs=
sigs.k8s.io/gateway-api v1.2.0 h1:LrToiFwtqKTKZcZtoQPTuo3FxhrrhTgzQG0Te+YGSo8=
sigs.k8s.io/gateway-api v1.2.0/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0=
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=

40
main.go
View File

@ -25,10 +25,9 @@ import (
"syscall"
"time"
awsSDK "github.com/aws/aws-sdk-go/aws"
"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/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/route53"
sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery"
"github.com/go-logr/logr"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
@ -66,7 +65,6 @@ import (
"sigs.k8s.io/external-dns/provider/pdns"
"sigs.k8s.io/external-dns/provider/pihole"
"sigs.k8s.io/external-dns/provider/plural"
"sigs.k8s.io/external-dns/provider/rdns"
"sigs.k8s.io/external-dns/provider/rfc2136"
"sigs.k8s.io/external-dns/provider/scaleway"
"sigs.k8s.io/external-dns/provider/tencentcloud"
@ -206,10 +204,10 @@ func main() {
case "alibabacloud":
p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
case "aws":
sessions := aws.CreateSessions(cfg)
clients := make(map[string]aws.Route53API, len(sessions))
for profile, session := range sessions {
clients[profile] = route53.New(session)
configs := aws.CreateV2Configs(cfg)
clients := make(map[string]aws.Route53API, len(configs))
for profile, config := range configs {
clients[profile] = route53.NewFromConfig(config)
}
p, err = aws.NewAWSProvider(
@ -236,11 +234,11 @@ 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.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, sd.New(aws.CreateDefaultSession(cfg)))
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, cfg.AWSSDCreateTag, sd.NewFromConfig(aws.CreateDefaultV2Config(cfg)))
case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.DryRun)
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.DryRun)
case "azure-private-dns":
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.DryRun)
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.DryRun)
case "ultradns":
p, err = ultradns.NewUltraDNSProvider(domainFilter, cfg.DryRun)
case "civo":
@ -259,13 +257,6 @@ func main() {
p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "coredns", "skydns":
p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
case "rdns":
p, err = rdns.NewRDNSProvider(
rdns.RDNSConfig{
DomainFilter: domainFilter,
DryRun: cfg.DryRun,
},
)
case "exoscale":
p, err = exoscale.NewExoscaleProvider(
cfg.ExoscaleAPIEnvironment,
@ -287,6 +278,7 @@ func main() {
DomainFilter: domainFilter,
DryRun: cfg.DryRun,
Server: cfg.PDNSServer,
ServerID: cfg.PDNSServerID,
APIKey: cfg.PDNSAPIKey,
TLSConfig: pdns.TLSConfig{
SkipTLSVerify: cfg.PDNSSkipTLSVerify,
@ -383,11 +375,15 @@ func main() {
var r registry.Registry
switch cfg.Registry {
case "dynamodb":
config := awsSDK.NewConfig()
var dynamodbOpts []func(*dynamodb.Options)
if cfg.AWSDynamoDBRegion != "" {
config = config.WithRegion(cfg.AWSDynamoDBRegion)
dynamodbOpts = []func(*dynamodb.Options){
func(opts *dynamodb.Options) {
opts.Region = cfg.AWSDynamoDBRegion
},
}
}
r, err = registry.NewDynamoDBRegistry(p, cfg.TXTOwnerID, dynamodb.New(aws.CreateDefaultSession(cfg), config), cfg.AWSDynamoDBTable, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, []byte(cfg.TXTEncryptAESKey), cfg.TXTCacheInterval)
r, err = registry.NewDynamoDBRegistry(p, cfg.TXTOwnerID, dynamodb.NewFromConfig(aws.CreateDefaultV2Config(cfg), dynamodbOpts...), cfg.AWSDynamoDBTable, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, []byte(cfg.TXTEncryptAESKey), cfg.TXTCacheInterval)
case "noop":
r, err = registry.NewNoopRegistry(p)
case "txt":

View File

@ -86,7 +86,7 @@ type Config struct {
AWSZoneTagFilter []string
AWSAssumeRole string
AWSProfiles []string
AWSAssumeRoleExternalID string
AWSAssumeRoleExternalID string `secure:"yes"`
AWSBatchChangeSize int
AWSBatchChangeSizeBytes int
AWSBatchChangeSizeValues int
@ -96,6 +96,7 @@ type Config struct {
AWSPreferCNAME bool
AWSZoneCacheDuration time.Duration
AWSSDServiceCleanup bool
AWSSDCreateTag map[string]string
AWSZoneMatchParent bool
AWSDynamoDBRegion string
AWSDynamoDBTable string
@ -104,6 +105,7 @@ type Config struct {
AzureSubscriptionID string
AzureUserAssignedIdentityClientID string
AzureActiveDirectoryAuthorityHost string
AzureZonesCacheDuration time.Duration
CloudflareProxied bool
CloudflareDNSRecordsPerPage int
CloudflareRegionKey string
@ -123,6 +125,7 @@ type Config struct {
OVHEndpoint string
OVHApiRateLimit int
PDNSServer string
PDNSServerID string
PDNSAPIKey string `secure:"yes"`
PDNSSkipTLSVerify bool
TLSCA string
@ -256,11 +259,13 @@ var defaultConfig = &Config{
AWSPreferCNAME: false,
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AWSSDCreateTag: map[string]string{},
AWSDynamoDBRegion: "",
AWSDynamoDBTable: "external-dns",
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
AzureZonesCacheDuration: 0 * time.Second,
CloudflareProxied: false,
CloudflareDNSRecordsPerPage: 100,
CloudflareRegionKey: "earth",
@ -278,6 +283,7 @@ var defaultConfig = &Config{
OVHEndpoint: "ovh-eu",
OVHApiRateLimit: 20,
PDNSServer: "http://localhost:8081",
PDNSServerID: "localhost",
PDNSAPIKey: "",
PDNSSkipTLSVerify: false,
TLSCA: "",
@ -357,7 +363,9 @@ var defaultConfig = &Config{
// NewConfig returns new Config object
func NewConfig() *Config {
return &Config{}
return &Config{
AWSSDCreateTag: map[string]string{},
}
}
func (cfg *Config) String() string {
@ -445,7 +453,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("nat64-networks", "Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.NAT64Networks)
// Flags related to providers
providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rdns", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"}
providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"}
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...)
app.Flag("provider-cache-time", "The time to cache the DNS provider record list requests.").Default(defaultConfig.ProviderCacheTime.String()).DurationVar(&cfg.ProviderCacheTime)
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
@ -475,10 +483,12 @@ func (cfg *Config) ParseFlags(args []string) error {
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-zone-match-parent", "Expand limit possible target by sub-domains (default: disabled)").BoolVar(&cfg.AWSZoneMatchParent)
app.Flag("aws-sd-service-cleanup", "When using the AWS CloudMap provider, delete empty Services without endpoints (default: disabled)").BoolVar(&cfg.AWSSDServiceCleanup)
app.Flag("aws-sd-create-tag", "When using the AWS CloudMap provider, add tag to created services. The flag can be used multiple times").StringMapVar(&cfg.AWSSDCreateTag)
app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure)").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile)
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
app.Flag("azure-subscription-id", "When using the Azure provider, override the Azure subscription to use (optional)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID)
app.Flag("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)").Default("").StringVar(&cfg.AzureUserAssignedIdentityClientID)
app.Flag("azure-zones-cache-duration", "When using the Azure provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AzureZonesCacheDuration.String()).DurationVar(&cfg.AzureZonesCacheDuration)
app.Flag("tencent-cloud-config-file", "When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud)").Default(defaultConfig.TencentCloudConfigFile).StringVar(&cfg.TencentCloudConfigFile)
app.Flag("tencent-cloud-zone-type", "When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private)").Default(defaultConfig.TencentCloudZoneType).EnumVar(&cfg.TencentCloudZoneType, "", "public", "private")
@ -501,6 +511,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)").Default(defaultConfig.OVHEndpoint).StringVar(&cfg.OVHEndpoint)
app.Flag("ovh-api-rate-limit", "When using the OVH provider, specify the API request rate limit, X operations by seconds (default: 20)").Default(strconv.Itoa(defaultConfig.OVHApiRateLimit)).IntVar(&cfg.OVHApiRateLimit)
app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)
app.Flag("pdns-server-id", "When using the PowerDNS/PDNS provider, specify the id of the server to retrieve. Should be `localhost` except when the server is behind a proxy (optional when --provider=pdns) (default: localhost)").Default(defaultConfig.PDNSServerID).StringVar(&cfg.PDNSServerID)
app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)
app.Flag("pdns-skip-tls-verify", "When using the PowerDNS/PDNS provider, disable verification of any TLS certificates (optional when --provider=pdns) (default: false)").Default(strconv.FormatBool(defaultConfig.PDNSSkipTLSVerify)).BoolVar(&cfg.PDNSSkipTLSVerify)
app.Flag("ns1-endpoint", "When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/)").Default(defaultConfig.NS1Endpoint).StringVar(&cfg.NS1Endpoint)

View File

@ -68,6 +68,7 @@ var (
AWSProfiles: []string{""},
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AWSSDCreateTag: map[string]string{},
AWSDynamoDBTable: "external-dns",
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
@ -89,6 +90,7 @@ var (
OVHEndpoint: "ovh-eu",
OVHApiRateLimit: 20,
PDNSServer: "http://localhost:8081",
PDNSServerID: "localhost",
PDNSAPIKey: "",
Policy: "sync",
Registry: "txt",
@ -167,6 +169,7 @@ var (
AWSProfiles: []string{"profile1", "profile2"},
AWSZoneCacheDuration: 10 * time.Second,
AWSSDServiceCleanup: true,
AWSSDCreateTag: map[string]string{"key1": "value1", "key2": "value2"},
AWSDynamoDBTable: "custom-table",
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
@ -188,6 +191,7 @@ var (
OVHEndpoint: "ovh-ca",
OVHApiRateLimit: 42,
PDNSServer: "http://ns.example.com:8081",
PDNSServerID: "localhost",
PDNSAPIKey: "some-secret-key",
PDNSSkipTLSVerify: true,
TLSCA: "/path/to/ca.crt",
@ -288,6 +292,7 @@ func TestParseFlags(t *testing.T) {
"--ovh-endpoint=ovh-ca",
"--ovh-api-rate-limit=42",
"--pdns-server=http://ns.example.com:8081",
"--pdns-server-id=localhost",
"--pdns-api-key=some-secret-key",
"--pdns-skip-tls-verify",
"--oci-config-file=oci.yaml",
@ -325,6 +330,8 @@ func TestParseFlags(t *testing.T) {
"--aws-profile=profile2",
"--aws-zones-cache-duration=10s",
"--aws-sd-service-cleanup",
"--aws-sd-create-tag=key1=value1",
"--aws-sd-create-tag=key2=value2",
"--no-aws-evaluate-target-health",
"--policy=upsert-only",
"--registry=noop",
@ -413,6 +420,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_TARGET_NET_FILTER": "10.0.0.0/9\n10.1.0.0/9",
"EXTERNAL_DNS_EXCLUDE_TARGET_NET": "1.0.0.0/9\n1.1.0.0/9",
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
"EXTERNAL_DNS_PDNS_ID": "localhost",
"EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key",
"EXTERNAL_DNS_PDNS_SKIP_TLS_VERIFY": "1",
"EXTERNAL_DNS_RDNS_ROOT_DOMAIN": "lb.rancher.cloud",
@ -436,6 +444,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AWS_PROFILE": "profile1\nprofile2",
"EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s",
"EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true",
"EXTERNAL_DNS_AWS_SD_CREATE_TAG": "key1=value1\nkey2=value2",
"EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
@ -504,8 +513,8 @@ func restoreEnv(t *testing.T, originalEnv map[string]string) {
func TestPasswordsNotLogged(t *testing.T) {
cfg := Config{
PDNSAPIKey: "pdns-api-key",
RFC2136TSIGSecret: "tsig-secret",
PDNSAPIKey: "pdns-api-key",
RFC2136TSIGSecret: "tsig-secret",
}
s := cfg.String()

View File

@ -240,6 +240,10 @@ func (p *Plan) Calculate() *Plan {
if ownersMatch {
changes.Create = append(changes.Create, creates...)
} else if log.GetLevel() == log.DebugLevel {
for _, current := range row.current {
log.Debugf(`Skipping endpoint %v because owner id does not match for one or more items to create, found: "%s", required: "%s"`, current, current.Labels[endpoint.OwnerLabelKey], p.OwnerID)
}
}
}
}

View File

@ -212,16 +212,16 @@ func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(request *pvtz.Describe
response = pvtz.CreateDescribeZoneInfoResponse()
response.ZoneId = m.zone.ZoneId
response.ZoneName = m.zone.ZoneName
response.BindVpcs = pvtz.BindVpcsInDescribeZoneInfo{Vpc: m.zone.Vpcs.Vpc}
response.BindVpcs = pvtz.BindVpcsInDescribeZoneInfo{Vpc: make([]pvtz.VpcInDescribeZoneInfo, len(m.zone.Vpcs.Vpc))}
for idx, vpc := range m.zone.Vpcs.Vpc {
response.BindVpcs.Vpc[idx] = pvtz.VpcInDescribeZoneInfo{VpcName: vpc.VpcName, VpcId: vpc.VpcId, VpcType: vpc.VpcType, RegionName: vpc.RegionName, RegionId: vpc.RegionId}
}
return response, nil
}
func newTestAlibabaCloudProvider(private bool) *AlibabaCloudProvider {
cfg := alibabaCloudConfig{
RegionID: "cn-beijing",
AccessKeyID: "xxxxxx",
AccessKeySecret: "xxxxxx",
VPCID: "vpc-xxxxxx",
VPCID: "vpc-xxxxxx",
}
//
//dnsClient, _ := alidns.NewClientWithAccessKey(

View File

@ -20,15 +20,15 @@ import (
"context"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/route53"
route53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
@ -41,11 +41,11 @@ const (
recordTTL = 300
// From the experiments, it seems that the default MaxItems applied is 100,
// and that, on the server side, there is a hard limit of 300 elements per page.
// After a discussion with AWS representants, clients should accept
// when less items are returned, and still paginate accordingly.
// After a discussion with AWS representatives, clients should accept
// when fewer items are returned, and still paginate accordingly.
// 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"
// Hence, if AWS ever decides to raise this limit, we will automatically reduce the pressure on rate limits
route53PageSize int32 = 300
// providerSpecificAlias specifies whether a CNAME endpoint maps to an AWS ALIAS record.
providerSpecificAlias = "alias"
providerSpecificTargetHostedZone = "aws/target-hosted-zone"
@ -199,16 +199,16 @@ var canonicalHostedZones = map[string]string{
// Route53API is the subset of the AWS Route53 API that we actually use. Add methods as required. Signatures must match exactly.
// mostly taken from: https://github.com/kubernetes/kubernetes/blob/853167624edb6bc0cfdcdfb88e746e178f5db36c/federation/pkg/dnsprovider/providers/aws/route53/stubs/route53api.go
type Route53API interface {
ListResourceRecordSetsPagesWithContext(ctx context.Context, input *route53.ListResourceRecordSetsInput, fn func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error
ChangeResourceRecordSetsWithContext(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, opts ...request.Option) (*route53.ChangeResourceRecordSetsOutput, error)
CreateHostedZoneWithContext(ctx context.Context, input *route53.CreateHostedZoneInput, opts ...request.Option) (*route53.CreateHostedZoneOutput, error)
ListHostedZonesPagesWithContext(ctx context.Context, input *route53.ListHostedZonesInput, fn func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error
ListTagsForResourceWithContext(ctx context.Context, input *route53.ListTagsForResourceInput, opts ...request.Option) (*route53.ListTagsForResourceOutput, error)
ListResourceRecordSets(ctx context.Context, input *route53.ListResourceRecordSetsInput, optFns ...func(options *route53.Options)) (*route53.ListResourceRecordSetsOutput, error)
ChangeResourceRecordSets(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, optFns ...func(options *route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error)
CreateHostedZone(ctx context.Context, input *route53.CreateHostedZoneInput, optFns ...func(*route53.Options)) (*route53.CreateHostedZoneOutput, error)
ListHostedZones(ctx context.Context, input *route53.ListHostedZonesInput, optFns ...func(options *route53.Options)) (*route53.ListHostedZonesOutput, error)
ListTagsForResource(ctx context.Context, input *route53.ListTagsForResourceInput, optFns ...func(options *route53.Options)) (*route53.ListTagsForResourceOutput, error)
}
// wrapper to handle ownership relation throughout the provider implementation
type Route53Change struct {
route53.Change
route53types.Change
OwnedRecord string
sizeBytes int
sizeValues int
@ -218,13 +218,13 @@ type Route53Changes []*Route53Change
type profiledZone struct {
profile string
zone *route53.HostedZone
zone *route53types.HostedZone
}
func (cs Route53Changes) Route53Changes() []*route53.Change {
ret := []*route53.Change{}
func (cs Route53Changes) Route53Changes() []route53types.Change {
ret := []route53types.Change{}
for _, c := range cs {
ret = append(ret, &c.Change)
ret = append(ret, c.Change)
}
return ret
}
@ -253,7 +253,7 @@ type AWSProvider struct {
zoneTypeFilter provider.ZoneTypeFilter
// filter hosted zones by tags
zoneTagFilter provider.ZoneTagFilter
// extend filter for sub-domains in the zone (e.g. first.us-east-1.example.com)
// extend filter for subdomains in the zone (e.g. first.us-east-1.example.com)
zoneMatchParent bool
preferCNAME bool
zonesCache *zonesListCache
@ -302,13 +302,13 @@ func NewAWSProvider(awsConfig AWSConfig, clients map[string]Route53API) (*AWSPro
}
// Zones returns the list of hosted zones.
func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone, error) {
func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53types.HostedZone, error) {
zones, err := p.zones(ctx)
if err != nil {
return nil, err
}
result := make(map[string]*route53.HostedZone, len(zones))
result := make(map[string]*route53types.HostedZone, len(zones))
for id, zone := range zones {
result[id] = zone.zone
}
@ -324,61 +324,57 @@ func (p *AWSProvider) zones(ctx context.Context) (map[string]*profiledZone, erro
log.Debug("Refreshing zones list cache")
zones := make(map[string]*profiledZone)
var profile string
var tagErr error
f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) {
for _, zone := range resp.HostedZones {
if !p.zoneIDFilter.Match(aws.StringValue(zone.Id)) {
continue
}
if !p.zoneTypeFilter.Match(zone) {
continue
}
for profile, client := range p.clients {
var tagErr error
paginator := route53.NewListHostedZonesPaginator(client, &route53.ListHostedZonesInput{})
if !p.domainFilter.Match(aws.StringValue(zone.Name)) {
if !p.zoneMatchParent {
continue
}
if !p.domainFilter.MatchParent(aws.StringValue(zone.Name)) {
continue
}
}
// Only fetch tags if a tag filter was specified
if !p.zoneTagFilter.IsEmpty() {
tags, err := p.tagsForZone(ctx, *zone.Id, profile)
if err != nil {
tagErr = err
return false
}
if !p.zoneTagFilter.Match(tags) {
continue
}
}
zones[aws.StringValue(zone.Id)] = &profiledZone{
profile: profile,
zone: zone,
}
}
return true
}
for p, client := range p.clients {
profile = p
err := client.ListHostedZonesPagesWithContext(ctx, &route53.ListHostedZonesInput{}, f)
if err != nil {
var awsErr awserr.Error
if errors.As(err, &awsErr) {
if awsErr.Code() == route53.ErrCodeThrottlingException {
log.Warnf("Skipping AWS profile %q due to provider side throttling: %v", profile, awsErr.Message())
for paginator.HasMorePages() {
resp, err := paginator.NextPage(ctx)
if err != nil {
var te *route53types.ThrottlingException
if errors.As(err, &te) {
log.Infof("Skipping AWS profile %q due to provider side throttling: %v", profile, te.ErrorMessage())
continue
}
// nothing to do here. Falling through to general error handling
return nil, provider.NewSoftError(fmt.Errorf("failed to list hosted zones: %w", err))
}
for _, zone := range resp.HostedZones {
if !p.zoneIDFilter.Match(*zone.Id) {
continue
}
if !p.zoneTypeFilter.Match(zone) {
continue
}
if !p.domainFilter.Match(*zone.Name) {
if !p.zoneMatchParent {
continue
}
if !p.domainFilter.MatchParent(*zone.Name) {
continue
}
}
// Only fetch tags if a tag filter was specified
if !p.zoneTagFilter.IsEmpty() {
tags, err := p.tagsForZone(ctx, *zone.Id, profile)
if err != nil {
tagErr = err
break
}
if !p.zoneTagFilter.Match(tags) {
continue
}
}
zones[*zone.Id] = &profiledZone{
profile: profile,
zone: &zone,
}
}
return nil, provider.NewSoftError(fmt.Errorf("failed to list hosted zones: %w", err))
}
if tagErr != nil {
return nil, provider.NewSoftError(fmt.Errorf("failed to list zones tags: %w", tagErr))
@ -386,7 +382,7 @@ func (p *AWSProvider) zones(ctx context.Context) (map[string]*profiledZone, erro
}
for _, zone := range zones {
log.Debugf("Considering zone: %s (domain: %s)", aws.StringValue(zone.zone.Id), aws.StringValue(zone.zone.Name))
log.Debugf("Considering zone: %s (domain: %s)", *zone.zone.Id, *zone.zone.Name)
}
if p.zonesCache.duration > time.Duration(0) {
@ -403,6 +399,28 @@ func wildcardUnescape(s string) string {
return strings.Replace(s, "\\052", "*", 1)
}
// See https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html
// convertOctalToAscii decodes inputs that contain octal escape sequences into their original ASCII characters.
// The function returns converted string where any octal escape sequences have been replaced with their corresponding ASCII characters.
func convertOctalToAscii(input string) string {
if !containsOctalSequence(input) {
return input
}
result, err := strconv.Unquote("\"" + input + "\"")
if err != nil {
return input
}
return result
}
// validateDomainName checks if the domain name contains valid octal escape sequences.
func containsOctalSequence(domain string) bool {
// Pattern to match valid octal escape sequences
octalEscapePattern := `\\[0-3][0-7]{2}`
octalEscapeRegex := regexp.MustCompile(octalEscapePattern)
return octalEscapeRegex.MatchString(domain)
}
// Records returns the list of records in a given hosted zone.
func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
zones, err := p.zones(ctx)
@ -415,92 +433,95 @@ func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoi
func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZone) ([]*endpoint.Endpoint, error) {
endpoints := make([]*endpoint.Endpoint, 0)
f := func(resp *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) {
for _, r := range resp.ResourceRecordSets {
newEndpoints := make([]*endpoint.Endpoint, 0)
if !p.SupportedRecordType(aws.StringValue(r.Type)) {
continue
}
var ttl endpoint.TTL
if r.TTL != nil {
ttl = endpoint.TTL(*r.TTL)
}
if len(r.ResourceRecords) > 0 {
targets := make([]string, len(r.ResourceRecords))
for idx, rr := range r.ResourceRecords {
targets[idx] = aws.StringValue(rr.Value)
}
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 {
// Alias records don't have TTLs so provide the default to match the TXT generation
if ttl == 0 {
ttl = recordTTL
}
ep := endpoint.
NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), endpoint.RecordTypeA, ttl, aws.StringValue(r.AliasTarget.DNSName)).
WithProviderSpecific(providerSpecificEvaluateTargetHealth, fmt.Sprintf("%t", aws.BoolValue(r.AliasTarget.EvaluateTargetHealth))).
WithProviderSpecific(providerSpecificAlias, "true")
newEndpoints = append(newEndpoints, ep)
}
for _, ep := range newEndpoints {
if r.SetIdentifier != nil {
ep.SetIdentifier = aws.StringValue(r.SetIdentifier)
switch {
case r.Weight != nil:
ep.WithProviderSpecific(providerSpecificWeight, fmt.Sprintf("%d", aws.Int64Value(r.Weight)))
case r.Region != nil:
ep.WithProviderSpecific(providerSpecificRegion, aws.StringValue(r.Region))
case r.Failover != nil:
ep.WithProviderSpecific(providerSpecificFailover, aws.StringValue(r.Failover))
case r.MultiValueAnswer != nil && aws.BoolValue(r.MultiValueAnswer):
ep.WithProviderSpecific(providerSpecificMultiValueAnswer, "")
case r.GeoLocation != nil:
if r.GeoLocation.ContinentCode != nil {
ep.WithProviderSpecific(providerSpecificGeolocationContinentCode, aws.StringValue(r.GeoLocation.ContinentCode))
} else {
if r.GeoLocation.CountryCode != nil {
ep.WithProviderSpecific(providerSpecificGeolocationCountryCode, aws.StringValue(r.GeoLocation.CountryCode))
}
if r.GeoLocation.SubdivisionCode != nil {
ep.WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, aws.StringValue(r.GeoLocation.SubdivisionCode))
}
}
default:
// one of the above needs to be set, otherwise SetIdentifier doesn't make sense
}
}
if r.HealthCheckId != nil {
ep.WithProviderSpecific(providerSpecificHealthCheckID, aws.StringValue(r.HealthCheckId))
}
endpoints = append(endpoints, ep)
}
}
return true
}
for _, z := range zones {
params := &route53.ListResourceRecordSetsInput{
HostedZoneId: z.zone.Id,
MaxItems: aws.String(route53PageSize),
}
client := p.clients[z.profile]
if err := client.ListResourceRecordSetsPagesWithContext(ctx, params, f); err != nil {
return nil, fmt.Errorf("failed to list resource records sets for zone %s using aws profile %q: %w", *z.zone.Id, z.profile, err)
paginator := route53.NewListResourceRecordSetsPaginator(client, &route53.ListResourceRecordSetsInput{
HostedZoneId: z.zone.Id,
MaxItems: aws.Int32(route53PageSize),
})
for paginator.HasMorePages() {
resp, err := paginator.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list resource records sets for zone %s using aws profile %q: %w", *z.zone.Id, z.profile, err)
}
for _, r := range resp.ResourceRecordSets {
newEndpoints := make([]*endpoint.Endpoint, 0)
if !p.SupportedRecordType(r.Type) {
continue
}
name := convertOctalToAscii(wildcardUnescape(*r.Name))
var ttl endpoint.TTL
if r.TTL != nil {
ttl = endpoint.TTL(*r.TTL)
}
if len(r.ResourceRecords) > 0 {
targets := make([]string, len(r.ResourceRecords))
for idx, rr := range r.ResourceRecords {
targets[idx] = *rr.Value
}
ep := endpoint.NewEndpointWithTTL(name, string(r.Type), ttl, targets...)
if r.Type == endpoint.RecordTypeCNAME {
ep = ep.WithProviderSpecific(providerSpecificAlias, "false")
}
newEndpoints = append(newEndpoints, ep)
}
if r.AliasTarget != nil {
// Alias records don't have TTLs so provide the default to match the TXT generation
if ttl == 0 {
ttl = recordTTL
}
ep := endpoint.
NewEndpointWithTTL(name, endpoint.RecordTypeA, ttl, *r.AliasTarget.DNSName).
WithProviderSpecific(providerSpecificEvaluateTargetHealth, fmt.Sprintf("%t", r.AliasTarget.EvaluateTargetHealth)).
WithProviderSpecific(providerSpecificAlias, "true")
newEndpoints = append(newEndpoints, ep)
}
for _, ep := range newEndpoints {
if r.SetIdentifier != nil {
ep.SetIdentifier = *r.SetIdentifier
switch {
case r.Weight != nil:
ep.WithProviderSpecific(providerSpecificWeight, fmt.Sprintf("%d", *r.Weight))
case r.Region != "":
ep.WithProviderSpecific(providerSpecificRegion, string(r.Region))
case r.Failover != "":
ep.WithProviderSpecific(providerSpecificFailover, string(r.Failover))
case r.MultiValueAnswer != nil && *r.MultiValueAnswer:
ep.WithProviderSpecific(providerSpecificMultiValueAnswer, "")
case r.GeoLocation != nil:
if r.GeoLocation.ContinentCode != nil {
ep.WithProviderSpecific(providerSpecificGeolocationContinentCode, *r.GeoLocation.ContinentCode)
} else {
if r.GeoLocation.CountryCode != nil {
ep.WithProviderSpecific(providerSpecificGeolocationCountryCode, *r.GeoLocation.CountryCode)
}
if r.GeoLocation.SubdivisionCode != nil {
ep.WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, *r.GeoLocation.SubdivisionCode)
}
}
default:
// one of the above needs to be set, otherwise SetIdentifier doesn't make sense
}
}
if r.HealthCheckId != nil {
ep.WithProviderSpecific(providerSpecificHealthCheckID, *r.HealthCheckId)
}
endpoints = append(endpoints, ep)
}
}
}
}
@ -560,9 +581,9 @@ func (p *AWSProvider) createUpdateChanges(newEndpoints, oldEndpoints []*endpoint
}
combined := make(Route53Changes, 0, len(deletes)+len(creates)+len(updates))
combined = append(combined, p.newChanges(route53.ChangeActionCreate, creates)...)
combined = append(combined, p.newChanges(route53.ChangeActionUpsert, updates)...)
combined = append(combined, p.newChanges(route53.ChangeActionDelete, deletes)...)
combined = append(combined, p.newChanges(route53types.ChangeActionCreate, creates)...)
combined = append(combined, p.newChanges(route53types.ChangeActionUpsert, updates)...)
combined = append(combined, p.newChanges(route53types.ChangeActionDelete, deletes)...)
return combined
}
@ -575,7 +596,7 @@ func (p *AWSProvider) GetDomainFilter() endpoint.DomainFilterInterface {
}
zoneNames := []string(nil)
for _, z := range zones {
zoneNames = append(zoneNames, aws.StringValue(z.Name), "."+aws.StringValue(z.Name))
zoneNames = append(zoneNames, *z.Name, "."+*z.Name)
}
log.Infof("Applying provider record filter for domains: %v", zoneNames)
return endpoint.NewDomainFilter(zoneNames)
@ -591,8 +612,8 @@ func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) e
updateChanges := p.createUpdateChanges(changes.UpdateNew, changes.UpdateOld)
combinedChanges := make(Route53Changes, 0, len(changes.Delete)+len(changes.Create)+len(updateChanges))
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionCreate, changes.Create)...)
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionDelete, changes.Delete)...)
combinedChanges = append(combinedChanges, p.newChanges(route53types.ChangeActionCreate, changes.Create)...)
combinedChanges = append(combinedChanges, p.newChanges(route53types.ChangeActionDelete, changes.Delete)...)
combinedChanges = append(combinedChanges, updateChanges...)
return p.submitChanges(ctx, combinedChanges, zones)
@ -615,7 +636,7 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
var failedZones []string
for z, cs := range changesByZone {
log := log.WithFields(log.Fields{
"zoneName": aws.StringValue(zones[z].zone.Name),
"zoneName": *zones[z].zone.Name,
"zoneID": z,
"profile": zones[z].profile,
})
@ -634,13 +655,13 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
}
for _, c := range b {
log.Infof("Desired change: %s %s %s", *c.Action, *c.ResourceRecordSet.Name, *c.ResourceRecordSet.Type)
log.Infof("Desired change: %s %s %s", c.Action, *c.ResourceRecordSet.Name, c.ResourceRecordSet.Type)
}
if !p.dryRun {
params := &route53.ChangeResourceRecordSetsInput{
HostedZoneId: aws.String(z),
ChangeBatch: &route53.ChangeBatch{
ChangeBatch: &route53types.ChangeBatch{
Changes: b.Route53Changes(),
},
}
@ -648,8 +669,8 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
successfulChanges := 0
client := p.clients[zones[z].profile]
if _, err := client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil {
log.Errorf("Failure in zone %s when submitting change batch: %v", aws.StringValue(zones[z].zone.Name), err)
if _, err := client.ChangeResourceRecordSets(ctx, params); err != nil {
log.Errorf("Failure in zone %s when submitting change batch: %v", *zones[z].zone.Name, err)
changesByOwnership := groupChangesByNameAndOwnershipRelation(b)
@ -658,12 +679,12 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
for _, changes := range changesByOwnership {
for _, c := range changes {
log.Debugf("Desired change: %s %s %s", *c.Action, *c.ResourceRecordSet.Name, *c.ResourceRecordSet.Type)
log.Debugf("Desired change: %s %s %s", c.Action, *c.ResourceRecordSet.Name, c.ResourceRecordSet.Type)
}
params.ChangeBatch = &route53.ChangeBatch{
params.ChangeBatch = &route53types.ChangeBatch{
Changes: changes.Route53Changes(),
}
if _, err := client.ChangeResourceRecordSetsWithContext(ctx, params); err != nil {
if _, err := client.ChangeResourceRecordSets(ctx, params); err != nil {
failedUpdate = true
log.Errorf("Failed submitting change (error: %v), it will be retried in a separate change batch in the next iteration", err)
p.failedChangesQueue[z] = append(p.failedChangesQueue[z], changes...)
@ -702,7 +723,7 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
}
// newChanges returns a collection of Changes based on the given records and action.
func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint) Route53Changes {
func (p *AWSProvider) newChanges(action route53types.ChangeAction, endpoints []*endpoint.Endpoint) Route53Changes {
changes := make(Route53Changes, 0, len(endpoints))
for _, endpoint := range endpoints {
@ -711,8 +732,10 @@ func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint)
if dualstack {
// make a copy of change, modify RRS type to AAAA, then add new change
rrs := *change.ResourceRecordSet
change2 := &Route53Change{Change: route53.Change{Action: change.Action, ResourceRecordSet: &rrs}}
change2.ResourceRecordSet.Type = aws.String(route53.RRTypeAaaa)
change2 := &Route53Change{
Change: route53types.Change{Action: change.Action, ResourceRecordSet: &rrs},
}
change2.ResourceRecordSet.Type = route53types.RRTypeAaaa
changes = append(changes, change2)
}
}
@ -774,11 +797,11 @@ func (p *AWSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoi
// returned Change is based on the given record by the given action, e.g.
// action=ChangeActionCreate returns a change for creation of the record and
// action=ChangeActionDelete returns a change for deletion of the record.
func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53Change, bool) {
func (p *AWSProvider) newChange(action route53types.ChangeAction, ep *endpoint.Endpoint) (*Route53Change, bool) {
change := &Route53Change{
Change: route53.Change{
Action: aws.String(action),
ResourceRecordSet: &route53.ResourceRecordSet{
Change: route53types.Change{
Action: action,
ResourceRecordSet: &route53types.ResourceRecordSet{
Name: aws.String(ep.DNSName),
},
},
@ -793,24 +816,24 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C
if val, ok := ep.Labels[endpoint.DualstackLabelKey]; ok {
dualstack = val == "true"
}
change.ResourceRecordSet.Type = aws.String(route53.RRTypeA)
change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{
change.ResourceRecordSet.Type = route53types.RRTypeA
change.ResourceRecordSet.AliasTarget = &route53types.AliasTarget{
DNSName: aws.String(ep.Targets[0]),
HostedZoneId: aws.String(cleanZoneID(targetHostedZone)),
EvaluateTargetHealth: aws.Bool(evalTargetHealth),
EvaluateTargetHealth: evalTargetHealth,
}
change.sizeBytes += len([]byte(ep.Targets[0]))
change.sizeValues += 1
} else {
change.ResourceRecordSet.Type = aws.String(ep.RecordType)
change.ResourceRecordSet.Type = route53types.RRType(ep.RecordType)
if !ep.RecordTTL.IsConfigured() {
change.ResourceRecordSet.TTL = aws.Int64(recordTTL)
} else {
change.ResourceRecordSet.TTL = aws.Int64(int64(ep.RecordTTL))
}
change.ResourceRecordSet.ResourceRecords = make([]*route53.ResourceRecord, len(ep.Targets))
change.ResourceRecordSet.ResourceRecords = make([]route53types.ResourceRecord, len(ep.Targets))
for idx, val := range ep.Targets {
change.ResourceRecordSet.ResourceRecords[idx] = &route53.ResourceRecord{
change.ResourceRecordSet.ResourceRecords[idx] = route53types.ResourceRecord{
Value: aws.String(val),
}
change.sizeBytes += len([]byte(val))
@ -818,7 +841,7 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C
}
}
if action == route53.ChangeActionUpsert {
if action == route53types.ChangeActionUpsert {
// If the value of the Action element is UPSERT, each ResourceRecord element and each character in a Value
// element is counted twice
change.sizeBytes *= 2
@ -837,16 +860,16 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C
change.ResourceRecordSet.Weight = aws.Int64(weight)
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificRegion); ok {
change.ResourceRecordSet.Region = aws.String(prop)
change.ResourceRecordSet.Region = route53types.ResourceRecordSetRegion(prop)
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificFailover); ok {
change.ResourceRecordSet.Failover = aws.String(prop)
change.ResourceRecordSet.Failover = route53types.ResourceRecordSetFailover(prop)
}
if _, ok := ep.GetProviderSpecificProperty(providerSpecificMultiValueAnswer); ok {
change.ResourceRecordSet.MultiValueAnswer = aws.Bool(true)
}
geolocation := &route53.GeoLocation{}
geolocation := &route53types.GeoLocation{}
useGeolocation := false
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationContinentCode); ok {
geolocation.ContinentCode = aws.String(prop)
@ -908,7 +931,7 @@ func groupChangesByNameAndOwnershipRelation(cs Route53Changes) map[string]Route5
for _, v := range cs {
key := v.OwnedRecord
if key == "" {
key = aws.StringValue(v.ResourceRecordSet.Name)
key = *v.ResourceRecordSet.Name
}
changesByOwnership[key] = append(changesByOwnership[key], v)
}
@ -918,8 +941,8 @@ func groupChangesByNameAndOwnershipRelation(cs Route53Changes) map[string]Route5
func (p *AWSProvider) tagsForZone(ctx context.Context, zoneID string, profile string) (map[string]string, error) {
client := p.clients[profile]
response, err := client.ListTagsForResourceWithContext(ctx, &route53.ListTagsForResourceInput{
ResourceType: aws.String("hostedzone"),
response, err := client.ListTagsForResource(ctx, &route53.ListTagsForResourceInput{
ResourceType: route53types.TagResourceTypeHostedzone,
ResourceId: aws.String(zoneID),
})
if err != nil {
@ -1006,10 +1029,10 @@ func batchChangeSet(cs Route53Changes, batchSize int, batchSizeBytes int, batchS
func sortChangesByActionNameType(cs Route53Changes) Route53Changes {
sort.SliceStable(cs, func(i, j int) bool {
if *cs[i].Action > *cs[j].Action {
if cs[i].Action > cs[j].Action {
return true
}
if *cs[i].Action < *cs[j].Action {
if cs[i].Action < cs[j].Action {
return false
}
if *cs[i].ResourceRecordSet.Name < *cs[j].ResourceRecordSet.Name {
@ -1018,7 +1041,7 @@ func sortChangesByActionNameType(cs Route53Changes) Route53Changes {
if *cs[i].ResourceRecordSet.Name > *cs[j].ResourceRecordSet.Name {
return false
}
return *cs[i].ResourceRecordSet.Type < *cs[j].ResourceRecordSet.Type
return cs[i].ResourceRecordSet.Type < cs[j].ResourceRecordSet.Type
})
return cs
@ -1029,34 +1052,34 @@ func changesByZone(zones map[string]*profiledZone, changeSet Route53Changes) map
changes := make(map[string]Route53Changes)
for _, z := range zones {
changes[aws.StringValue(z.zone.Id)] = Route53Changes{}
changes[*z.zone.Id] = Route53Changes{}
}
for _, c := range changeSet {
hostname := provider.EnsureTrailingDot(aws.StringValue(c.ResourceRecordSet.Name))
hostname := provider.EnsureTrailingDot(*c.ResourceRecordSet.Name)
zones := suitableZones(hostname, zones)
if len(zones) == 0 {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.String())
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", *c.ResourceRecordSet.Name)
continue
}
for _, z := range zones {
if c.ResourceRecordSet.AliasTarget != nil && aws.StringValue(c.ResourceRecordSet.AliasTarget.HostedZoneId) == sameZoneAlias {
if c.ResourceRecordSet.AliasTarget != nil && *c.ResourceRecordSet.AliasTarget.HostedZoneId == sameZoneAlias {
// alias record is to be created; target needs to be in the same zone as endpoint
// if it's not, this will fail
rrset := *c.ResourceRecordSet
aliasTarget := *rrset.AliasTarget
aliasTarget.HostedZoneId = aws.String(cleanZoneID(aws.StringValue(z.zone.Id)))
aliasTarget.HostedZoneId = aws.String(cleanZoneID(*z.zone.Id))
rrset.AliasTarget = &aliasTarget
c = &Route53Change{
Change: route53.Change{
Change: route53types.Change{
Action: c.Action,
ResourceRecordSet: &rrset,
},
}
}
changes[aws.StringValue(z.zone.Id)] = append(changes[aws.StringValue(z.zone.Id)], c)
log.Debugf("Adding %s to zone %s [Id: %s]", hostname, aws.StringValue(z.zone.Name), aws.StringValue(z.zone.Id))
changes[*z.zone.Id] = append(changes[*z.zone.Id], c)
log.Debugf("Adding %s to zone %s [Id: %s]", hostname, *z.zone.Name, *z.zone.Id)
}
}
@ -1078,10 +1101,10 @@ func suitableZones(hostname string, zones map[string]*profiledZone) []*profiledZ
var publicZone *profiledZone
for _, z := range zones {
if aws.StringValue(z.zone.Name) == hostname || strings.HasSuffix(hostname, "."+aws.StringValue(z.zone.Name)) {
if z.zone.Config == nil || !aws.BoolValue(z.zone.Config.PrivateZone) {
if *z.zone.Name == hostname || strings.HasSuffix(hostname, "."+*z.zone.Name) {
if z.zone.Config == nil || !z.zone.Config.PrivateZone {
// Only select the best matching public zone
if publicZone == nil || len(aws.StringValue(z.zone.Name)) > len(aws.StringValue(publicZone.zone.Name)) {
if publicZone == nil || len(*z.zone.Name) > len(*publicZone.zone.Name) {
publicZone = z
}
} else {
@ -1156,11 +1179,11 @@ func cleanZoneID(id string) string {
return strings.TrimPrefix(id, "/hostedzone/")
}
func (p *AWSProvider) SupportedRecordType(recordType string) bool {
func (p *AWSProvider) SupportedRecordType(recordType route53types.RRType) bool {
switch recordType {
case "MX":
case route53types.RRTypeMx:
return true
default:
return provider.SupportedRecordType(recordType)
return provider.SupportedRecordType(string(recordType))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -17,13 +17,16 @@ limitations under the License.
package aws
import (
"context"
"fmt"
"net/http"
"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"
awsv2 "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/retry"
"github.com/aws/aws-sdk-go-v2/config"
stscredsv2 "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/linki/instrumented_http"
"github.com/sirupsen/logrus"
@ -38,8 +41,8 @@ type AWSSessionConfig struct {
Profile string
}
func CreateDefaultSession(cfg *externaldns.Config) *session.Session {
result, err := newSession(
func CreateDefaultV2Config(cfg *externaldns.Config) awsv2.Config {
result, err := newV2Config(
AWSSessionConfig{
AssumeRole: cfg.AWSAssumeRole,
AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID,
@ -52,24 +55,14 @@ func CreateDefaultSession(cfg *externaldns.Config) *session.Session {
return result
}
func CreateSessions(cfg *externaldns.Config) map[string]*session.Session {
result := make(map[string]*session.Session)
func CreateV2Configs(cfg *externaldns.Config) map[string]awsv2.Config {
result := make(map[string]awsv2.Config)
if len(cfg.AWSProfiles) == 0 || (len(cfg.AWSProfiles) == 1 && cfg.AWSProfiles[0] == "") {
session, err := newSession(
AWSSessionConfig{
AssumeRole: cfg.AWSAssumeRole,
AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID,
APIRetries: cfg.AWSAPIRetries,
},
)
if err != nil {
logrus.Fatal(err)
}
result[defaultAWSProfile] = session
cfg := CreateDefaultV2Config(cfg)
result[defaultAWSProfile] = cfg
} else {
for _, profile := range cfg.AWSProfiles {
session, err := newSession(
cfg, err := newV2Config(
AWSSessionConfig{
AssumeRole: cfg.AWSAssumeRole,
AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID,
@ -80,46 +73,48 @@ func CreateSessions(cfg *externaldns.Config) map[string]*session.Session {
if err != nil {
logrus.Fatal(err)
}
result[profile] = session
result[profile] = cfg
}
}
return result
}
func newSession(awsConfig AWSSessionConfig) (*session.Session, error) {
config := aws.NewConfig().WithMaxRetries(awsConfig.APIRetries)
config.WithHTTPClient(
instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{
func newV2Config(awsConfig AWSSessionConfig) (awsv2.Config, error) {
defaultOpts := []func(*config.LoadOptions) error{
config.WithRetryer(func() awsv2.Retryer {
return retry.AddWithMaxAttempts(retry.NewStandard(), awsConfig.APIRetries)
}),
config.WithHTTPClient(instrumented_http.NewClient(&http.Client{}, &instrumented_http.Callbacks{
PathProcessor: func(path string) string {
parts := strings.Split(path, "/")
return parts[len(parts)-1]
},
}),
)
})),
config.WithSharedConfigProfile(awsConfig.Profile),
}
session, err := session.NewSessionWithOptions(session.Options{
Config: *config,
SharedConfigState: session.SharedConfigEnable,
Profile: awsConfig.Profile,
})
cfg, err := config.LoadDefaultConfig(context.Background(), defaultOpts...)
if err != nil {
return nil, fmt.Errorf("instantiating AWS session: %w", err)
return awsv2.Config{}, fmt.Errorf("instantiating AWS config: %w", err)
}
if awsConfig.AssumeRole != "" {
stsSvc := sts.NewFromConfig(cfg)
var assumeRoleOpts []func(*stscredsv2.AssumeRoleOptions)
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
}))
logrus.Infof("Assuming role %s with external id", awsConfig.AssumeRole)
logrus.Debugf("External id: %s", awsConfig.AssumeRoleExternalID)
assumeRoleOpts = []func(*stscredsv2.AssumeRoleOptions){
func(opts *stscredsv2.AssumeRoleOptions) {
opts.ExternalID = &awsConfig.AssumeRoleExternalID
},
}
} else {
logrus.Infof("Assuming role: %s", awsConfig.AssumeRole)
session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole))
}
creds := stscredsv2.NewAssumeRoleProvider(stsSvc, awsConfig.AssumeRole, assumeRoleOpts...)
cfg.Credentials = awsv2.NewCredentialsCache(creds)
}
session.Handlers.Build.PushBack(request.MakeAddToUserAgentHandler("ExternalDNS", externaldns.Version))
return session, nil
return cfg, nil
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package aws
import (
"context"
"os"
"testing"
@ -24,7 +25,7 @@ import (
"github.com/stretchr/testify/require"
)
func Test_newSession(t *testing.T) {
func Test_newV2Config(t *testing.T) {
t.Run("should use profile from credentials file", func(t *testing.T) {
// setup
credsFile, err := prepareCredentialsFile(t)
@ -34,9 +35,9 @@ func Test_newSession(t *testing.T) {
defer os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE")
// when
s, err := newSession(AWSSessionConfig{Profile: "profile2"})
cfg, err := newV2Config(AWSSessionConfig{Profile: "profile2"})
require.NoError(t, err)
creds, err := s.Config.Credentials.Get()
creds, err := cfg.Credentials.Retrieve(context.Background())
// then
assert.NoError(t, err)
@ -52,9 +53,9 @@ func Test_newSession(t *testing.T) {
defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
// when
s, err := newSession(AWSSessionConfig{})
cfg, err := newV2Config(AWSSessionConfig{})
require.NoError(t, err)
creds, err := s.Config.Credentials.Get()
creds, err := cfg.Credentials.Retrieve(context.Background())
// then
assert.NoError(t, err)

View File

@ -24,9 +24,9 @@ import (
"regexp"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
sd "github.com/aws/aws-sdk-go/service/servicediscovery"
"github.com/aws/aws-sdk-go-v2/aws"
sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery"
sdtypes "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
@ -41,6 +41,7 @@ const (
sdNamespaceTypePrivate = "private"
sdInstanceAttrIPV4 = "AWS_INSTANCE_IPV4"
sdInstanceAttrIPV6 = "AWS_INSTANCE_IPV6"
sdInstanceAttrCname = "AWS_INSTANCE_CNAME"
sdInstanceAttrAlias = "AWS_ALIAS_DNS_NAME"
)
@ -54,16 +55,16 @@ var (
)
// AWSSDClient is the subset of the AWS Cloud Map API that we actually use. Add methods as required.
// Signatures must match exactly. Taken from https://github.com/aws/aws-sdk-go/blob/HEAD/service/servicediscovery/api.go
// Signatures must match exactly. Taken from https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/servicediscovery
type AWSSDClient interface {
CreateService(input *sd.CreateServiceInput) (*sd.CreateServiceOutput, error)
DeregisterInstance(input *sd.DeregisterInstanceInput) (*sd.DeregisterInstanceOutput, error)
DiscoverInstancesWithContext(ctx aws.Context, input *sd.DiscoverInstancesInput, opts ...request.Option) (*sd.DiscoverInstancesOutput, error)
ListNamespacesPages(input *sd.ListNamespacesInput, fn func(*sd.ListNamespacesOutput, bool) bool) error
ListServicesPages(input *sd.ListServicesInput, fn func(*sd.ListServicesOutput, bool) bool) error
RegisterInstance(input *sd.RegisterInstanceInput) (*sd.RegisterInstanceOutput, error)
UpdateService(input *sd.UpdateServiceInput) (*sd.UpdateServiceOutput, error)
DeleteService(input *sd.DeleteServiceInput) (*sd.DeleteServiceOutput, error)
CreateService(ctx context.Context, params *sd.CreateServiceInput, optFns ...func(*sd.Options)) (*sd.CreateServiceOutput, error)
DeregisterInstance(ctx context.Context, params *sd.DeregisterInstanceInput, optFns ...func(*sd.Options)) (*sd.DeregisterInstanceOutput, error)
DiscoverInstances(ctx context.Context, params *sd.DiscoverInstancesInput, optFns ...func(*sd.Options)) (*sd.DiscoverInstancesOutput, error)
ListNamespaces(ctx context.Context, params *sd.ListNamespacesInput, optFns ...func(*sd.Options)) (*sd.ListNamespacesOutput, error)
ListServices(ctx context.Context, params *sd.ListServicesInput, optFns ...func(*sd.Options)) (*sd.ListServicesOutput, error)
RegisterInstance(ctx context.Context, params *sd.RegisterInstanceInput, optFns ...func(*sd.Options)) (*sd.RegisterInstanceOutput, error)
UpdateService(ctx context.Context, params *sd.UpdateServiceInput, optFns ...func(*sd.Options)) (*sd.UpdateServiceOutput, error)
DeleteService(ctx context.Context, params *sd.DeleteServiceInput, optFns ...func(*sd.Options)) (*sd.DeleteServiceOutput, error)
}
// AWSSDProvider is an implementation of Provider for AWS Cloud Map.
@ -74,60 +75,72 @@ type AWSSDProvider struct {
// only consider namespaces ending in this suffix
namespaceFilter endpoint.DomainFilter
// filter namespace by type (private or public)
namespaceTypeFilter *sd.NamespaceFilter
namespaceTypeFilter sdtypes.NamespaceFilter
// enables service without instances cleanup
cleanEmptyService bool
// filter services for removal
ownerID string
// tags to be added to the service
tags []sdtypes.Tag
}
// NewAWSSDProvider initializes a new AWS Cloud Map based Provider.
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, dryRun, cleanEmptyService bool, ownerID string, client AWSSDClient) (*AWSSDProvider, error) {
provider := &AWSSDProvider{
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, dryRun, cleanEmptyService bool, ownerID string, tags map[string]string, client AWSSDClient) (*AWSSDProvider, error) {
p := &AWSSDProvider{
client: client,
dryRun: dryRun,
namespaceFilter: domainFilter,
namespaceTypeFilter: newSdNamespaceFilter(namespaceType),
cleanEmptyService: cleanEmptyService,
ownerID: ownerID,
tags: awsTags(tags),
}
return provider, nil
return p, nil
}
// newSdNamespaceFilter initialized AWS SD Namespace Filter based on given string config
func newSdNamespaceFilter(namespaceTypeConfig string) *sd.NamespaceFilter {
func newSdNamespaceFilter(namespaceTypeConfig string) sdtypes.NamespaceFilter {
switch namespaceTypeConfig {
case sdNamespaceTypePublic:
return &sd.NamespaceFilter{
Name: aws.String(sd.NamespaceFilterNameType),
Values: []*string{aws.String(sd.NamespaceTypeDnsPublic)},
return sdtypes.NamespaceFilter{
Name: sdtypes.NamespaceFilterNameType,
Values: []string{string(sdtypes.NamespaceTypeDnsPublic)},
}
case sdNamespaceTypePrivate:
return &sd.NamespaceFilter{
Name: aws.String(sd.NamespaceFilterNameType),
Values: []*string{aws.String(sd.NamespaceTypeDnsPrivate)},
return sdtypes.NamespaceFilter{
Name: sdtypes.NamespaceFilterNameType,
Values: []string{string(sdtypes.NamespaceTypeDnsPrivate)},
}
default:
return nil
return sdtypes.NamespaceFilter{}
}
}
// awsTags converts user supplied tags to AWS format
func awsTags(tags map[string]string) []sdtypes.Tag {
awsTags := make([]sdtypes.Tag, 0, len(tags))
for k, v := range tags {
awsTags = append(awsTags, sdtypes.Tag{Key: aws.String(k), Value: aws.String(v)})
}
return awsTags
}
// Records returns list of all endpoints.
func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
namespaces, err := p.ListNamespaces()
namespaces, err := p.ListNamespaces(ctx)
if err != nil {
return nil, err
}
for _, ns := range namespaces {
services, err := p.ListServicesByNamespaceID(ns.Id)
services, err := p.ListServicesByNamespaceID(ctx, ns.Id)
if err != nil {
return nil, err
}
for _, srv := range services {
resp, err := p.client.DiscoverInstancesWithContext(ctx, &sd.DiscoverInstancesInput{
resp, err := p.client.DiscoverInstances(ctx, &sd.DiscoverInstancesInput{
NamespaceName: ns.Name,
ServiceName: srv.Name,
})
@ -136,8 +149,8 @@ func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp
}
if len(resp.Instances) == 0 {
if err := p.DeleteService(srv); err != nil {
log.Errorf("Failed to delete service %q, error: %s", aws.StringValue(srv.Name), err)
if err := p.DeleteService(ctx, srv); err != nil {
log.Errorf("Failed to delete service %q, error: %s", *srv.Name, err)
}
continue
}
@ -149,35 +162,40 @@ func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp
return endpoints, nil
}
func (p *AWSSDProvider) instancesToEndpoint(ns *sd.NamespaceSummary, srv *sd.Service, instances []*sd.HttpInstanceSummary) *endpoint.Endpoint {
func (p *AWSSDProvider) instancesToEndpoint(ns *sdtypes.NamespaceSummary, srv *sdtypes.Service, instances []sdtypes.HttpInstanceSummary) *endpoint.Endpoint {
// DNS name of the record is a concatenation of service and namespace
recordName := *srv.Name + "." + *ns.Name
labels := endpoint.NewLabels()
labels[endpoint.AWSSDDescriptionLabel] = aws.StringValue(srv.Description)
labels[endpoint.AWSSDDescriptionLabel] = *srv.Description
newEndpoint := &endpoint.Endpoint{
DNSName: recordName,
RecordTTL: endpoint.TTL(aws.Int64Value(srv.DnsConfig.DnsRecords[0].TTL)),
RecordTTL: endpoint.TTL(*srv.DnsConfig.DnsRecords[0].TTL),
Targets: make(endpoint.Targets, 0, len(instances)),
Labels: labels,
}
for _, inst := range instances {
// CNAME
if inst.Attributes[sdInstanceAttrCname] != nil && aws.StringValue(srv.DnsConfig.DnsRecords[0].Type) == sd.RecordTypeCname {
if inst.Attributes[sdInstanceAttrCname] != "" && srv.DnsConfig.DnsRecords[0].Type == sdtypes.RecordTypeCname {
newEndpoint.RecordType = endpoint.RecordTypeCNAME
newEndpoint.Targets = append(newEndpoint.Targets, aws.StringValue(inst.Attributes[sdInstanceAttrCname]))
newEndpoint.Targets = append(newEndpoint.Targets, inst.Attributes[sdInstanceAttrCname])
// ALIAS
} else if inst.Attributes[sdInstanceAttrAlias] != nil {
} else if inst.Attributes[sdInstanceAttrAlias] != "" {
newEndpoint.RecordType = endpoint.RecordTypeCNAME
newEndpoint.Targets = append(newEndpoint.Targets, aws.StringValue(inst.Attributes[sdInstanceAttrAlias]))
newEndpoint.Targets = append(newEndpoint.Targets, inst.Attributes[sdInstanceAttrAlias])
// IP-based target
} else if inst.Attributes[sdInstanceAttrIPV4] != nil {
// IPv4-based target
} else if inst.Attributes[sdInstanceAttrIPV4] != "" {
newEndpoint.RecordType = endpoint.RecordTypeA
newEndpoint.Targets = append(newEndpoint.Targets, aws.StringValue(inst.Attributes[sdInstanceAttrIPV4]))
newEndpoint.Targets = append(newEndpoint.Targets, inst.Attributes[sdInstanceAttrIPV4])
// IPv6-based target
} else if inst.Attributes[sdInstanceAttrIPV6] != "" {
newEndpoint.RecordType = endpoint.RecordTypeAAAA
newEndpoint.Targets = append(newEndpoint.Targets, inst.Attributes[sdInstanceAttrIPV6])
} else {
log.Warnf("Invalid instance \"%v\" found in service \"%v\"", inst, srv.Name)
}
@ -199,7 +217,7 @@ func (p *AWSSDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes)
changes.Delete = append(changes.Delete, deletes...)
changes.Create = append(changes.Create, creates...)
namespaces, err := p.ListNamespaces()
namespaces, err := p.ListNamespaces(ctx)
if err != nil {
return err
}
@ -211,12 +229,12 @@ func (p *AWSSDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes)
// creates = [1.2.3.4, 1.2.3.5]
// ```
// then when deletes are executed after creates it will miss the `1.2.3.4` instance.
err = p.submitDeletes(namespaces, changes.Delete)
err = p.submitDeletes(ctx, namespaces, changes.Delete)
if err != nil {
return err
}
err = p.submitCreates(namespaces, changes.Create)
err = p.submitCreates(ctx, namespaces, changes.Create)
if err != nil {
return err
}
@ -245,11 +263,11 @@ func (p *AWSSDProvider) updatesToCreates(changes *plan.Changes) (creates []*endp
return creates, deletes
}
func (p *AWSSDProvider) submitCreates(namespaces []*sd.NamespaceSummary, changes []*endpoint.Endpoint) error {
func (p *AWSSDProvider) submitCreates(ctx context.Context, namespaces []*sdtypes.NamespaceSummary, changes []*endpoint.Endpoint) error {
changesByNamespaceID := p.changesByNamespaceID(namespaces, changes)
for nsID, changeList := range changesByNamespaceID {
services, err := p.ListServicesByNamespaceID(aws.String(nsID))
services, err := p.ListServicesByNamespaceID(ctx, aws.String(nsID))
if err != nil {
return err
}
@ -260,7 +278,7 @@ func (p *AWSSDProvider) submitCreates(namespaces []*sd.NamespaceSummary, changes
srv := services[srvName]
if srv == nil {
// when service is missing create a new one
srv, err = p.CreateService(&nsID, &srvName, ch)
srv, err = p.CreateService(ctx, &nsID, &srvName, ch)
if err != nil {
return err
}
@ -268,13 +286,13 @@ func (p *AWSSDProvider) submitCreates(namespaces []*sd.NamespaceSummary, changes
services[*srv.Name] = srv
} else if ch.RecordTTL.IsConfigured() && *srv.DnsConfig.DnsRecords[0].TTL != int64(ch.RecordTTL) {
// update service when TTL differ
err = p.UpdateService(srv, ch)
err = p.UpdateService(ctx, srv, ch)
if err != nil {
return err
}
}
err = p.RegisterInstance(srv, ch)
err = p.RegisterInstance(ctx, srv, ch)
if err != nil {
return err
}
@ -284,11 +302,11 @@ func (p *AWSSDProvider) submitCreates(namespaces []*sd.NamespaceSummary, changes
return nil
}
func (p *AWSSDProvider) submitDeletes(namespaces []*sd.NamespaceSummary, changes []*endpoint.Endpoint) error {
func (p *AWSSDProvider) submitDeletes(ctx context.Context, namespaces []*sdtypes.NamespaceSummary, changes []*endpoint.Endpoint) error {
changesByNamespaceID := p.changesByNamespaceID(namespaces, changes)
for nsID, changeList := range changesByNamespaceID {
services, err := p.ListServicesByNamespaceID(aws.String(nsID))
services, err := p.ListServicesByNamespaceID(ctx, aws.String(nsID))
if err != nil {
return err
}
@ -302,7 +320,7 @@ func (p *AWSSDProvider) submitDeletes(namespaces []*sd.NamespaceSummary, changes
return fmt.Errorf("service \"%s\" is missing when trying to delete \"%v\"", srvName, hostname)
}
err := p.DeregisterInstance(srv, ch)
err := p.DeregisterInstance(ctx, srv, ch)
if err != nil {
return err
}
@ -313,53 +331,51 @@ func (p *AWSSDProvider) submitDeletes(namespaces []*sd.NamespaceSummary, changes
}
// ListNamespaces returns all namespaces matching defined namespace filter
func (p *AWSSDProvider) ListNamespaces() ([]*sd.NamespaceSummary, error) {
namespaces := make([]*sd.NamespaceSummary, 0)
func (p *AWSSDProvider) ListNamespaces(ctx context.Context) ([]*sdtypes.NamespaceSummary, error) {
namespaces := make([]*sdtypes.NamespaceSummary, 0)
f := func(resp *sd.ListNamespacesOutput, lastPage bool) bool {
for _, ns := range resp.Namespaces {
if !p.namespaceFilter.Match(aws.StringValue(ns.Name)) {
continue
}
namespaces = append(namespaces, ns)
paginator := sd.NewListNamespacesPaginator(p.client, &sd.ListNamespacesInput{
Filters: []sdtypes.NamespaceFilter{p.namespaceTypeFilter},
})
for paginator.HasMorePages() {
resp, err := paginator.NextPage(ctx)
if err != nil {
return nil, err
}
return true
}
err := p.client.ListNamespacesPages(&sd.ListNamespacesInput{
Filters: []*sd.NamespaceFilter{p.namespaceTypeFilter},
}, f)
if err != nil {
return nil, err
for _, ns := range resp.Namespaces {
if !p.namespaceFilter.Match(*ns.Name) {
continue
}
namespaces = append(namespaces, &ns)
}
}
return namespaces, nil
}
// ListServicesByNamespaceID returns list of services in given namespace. Returns map[srv_name]*sd.Service
func (p *AWSSDProvider) ListServicesByNamespaceID(namespaceID *string) (map[string]*sd.Service, error) {
services := make([]*sd.ServiceSummary, 0)
// ListServicesByNamespaceID returns list of services in given namespace.
func (p *AWSSDProvider) ListServicesByNamespaceID(ctx context.Context, namespaceID *string) (map[string]*sdtypes.Service, error) {
services := make([]sdtypes.ServiceSummary, 0)
f := func(resp *sd.ListServicesOutput, lastPage bool) bool {
services = append(services, resp.Services...)
return true
}
err := p.client.ListServicesPages(&sd.ListServicesInput{
Filters: []*sd.ServiceFilter{{
Name: aws.String(sd.ServiceFilterNameNamespaceId),
Values: []*string{namespaceID},
paginator := sd.NewListServicesPaginator(p.client, &sd.ListServicesInput{
Filters: []sdtypes.ServiceFilter{{
Name: sdtypes.ServiceFilterNameNamespaceId,
Values: []string{*namespaceID},
}},
MaxResults: aws.Int64(100),
}, f)
if err != nil {
return nil, err
MaxResults: aws.Int32(100),
})
for paginator.HasMorePages() {
resp, err := paginator.NextPage(ctx)
if err != nil {
return nil, err
}
services = append(services, resp.Services...)
}
servicesMap := make(map[string]*sd.Service)
servicesMap := make(map[string]*sdtypes.Service)
for _, serviceSummary := range services {
service := &sd.Service{
service := &sdtypes.Service{
Arn: serviceSummary.Arn,
CreateDate: serviceSummary.CreateDate,
Description: serviceSummary.Description,
@ -373,13 +389,13 @@ func (p *AWSSDProvider) ListServicesByNamespaceID(namespaceID *string) (map[stri
Type: serviceSummary.Type,
}
servicesMap[aws.StringValue(service.Name)] = service
servicesMap[*service.Name] = service
}
return servicesMap, nil
}
// CreateService creates a new service in AWS API. Returns the created service.
func (p *AWSSDProvider) CreateService(namespaceID *string, srvName *string, ep *endpoint.Endpoint) (*sd.Service, error) {
func (p *AWSSDProvider) CreateService(ctx context.Context, namespaceID *string, srvName *string, ep *endpoint.Endpoint) (*sdtypes.Service, error) {
log.Infof("Creating a new service \"%s\" in \"%s\" namespace", *srvName, *namespaceID)
srvType := p.serviceTypeFromEndpoint(ep)
@ -391,17 +407,18 @@ func (p *AWSSDProvider) CreateService(namespaceID *string, srvName *string, ep *
}
if !p.dryRun {
out, err := p.client.CreateService(&sd.CreateServiceInput{
out, err := p.client.CreateService(ctx, &sd.CreateServiceInput{
Name: srvName,
Description: aws.String(ep.Labels[endpoint.AWSSDDescriptionLabel]),
DnsConfig: &sd.DnsConfig{
RoutingPolicy: aws.String(routingPolicy),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(srvType),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: routingPolicy,
DnsRecords: []sdtypes.DnsRecord{{
Type: srvType,
TTL: aws.Int64(ttl),
}},
},
NamespaceId: namespaceID,
Tags: p.tags,
})
if err != nil {
return nil, err
@ -411,11 +428,11 @@ func (p *AWSSDProvider) CreateService(namespaceID *string, srvName *string, ep *
}
// return mock service summary in case of dry run
return &sd.Service{Id: aws.String("dry-run-service"), Name: aws.String("dry-run-service")}, nil
return &sdtypes.Service{Id: aws.String("dry-run-service"), Name: aws.String("dry-run-service")}, nil
}
// UpdateService updates the specified service with information from provided endpoint.
func (p *AWSSDProvider) UpdateService(service *sd.Service, ep *endpoint.Endpoint) error {
func (p *AWSSDProvider) UpdateService(ctx context.Context, service *sdtypes.Service, ep *endpoint.Endpoint) error {
log.Infof("Updating service \"%s\"", *service.Name)
srvType := p.serviceTypeFromEndpoint(ep)
@ -426,13 +443,13 @@ func (p *AWSSDProvider) UpdateService(service *sd.Service, ep *endpoint.Endpoint
}
if !p.dryRun {
_, err := p.client.UpdateService(&sd.UpdateServiceInput{
_, err := p.client.UpdateService(ctx, &sd.UpdateServiceInput{
Id: service.Id,
Service: &sd.ServiceChange{
Service: &sdtypes.ServiceChange{
Description: aws.String(ep.Labels[endpoint.AWSSDDescriptionLabel]),
DnsConfig: &sd.DnsConfigChange{
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(srvType),
DnsConfig: &sdtypes.DnsConfigChange{
DnsRecords: []sdtypes.DnsRecord{{
Type: srvType,
TTL: aws.Int64(ttl),
}},
},
@ -447,7 +464,7 @@ func (p *AWSSDProvider) UpdateService(service *sd.Service, ep *endpoint.Endpoint
}
// DeleteService deletes empty Service from AWS API if its owner id match
func (p *AWSSDProvider) DeleteService(service *sd.Service) error {
func (p *AWSSDProvider) DeleteService(ctx context.Context, service *sdtypes.Service) error {
log.Debugf("Check if service \"%s\" owner id match and it can be deleted", *service.Name)
if !p.dryRun && p.cleanEmptyService {
// convert ownerID string to service description format
@ -455,39 +472,42 @@ func (p *AWSSDProvider) DeleteService(service *sd.Service) error {
label[endpoint.OwnerLabelKey] = p.ownerID
label[endpoint.AWSSDDescriptionLabel] = label.SerializePlain(false)
if strings.HasPrefix(aws.StringValue(service.Description), label[endpoint.AWSSDDescriptionLabel]) {
if strings.HasPrefix(*service.Description, label[endpoint.AWSSDDescriptionLabel]) {
log.Infof("Deleting service \"%s\"", *service.Name)
_, err := p.client.DeleteService(&sd.DeleteServiceInput{
_, err := p.client.DeleteService(ctx, &sd.DeleteServiceInput{
Id: aws.String(*service.Id),
})
return err
}
log.Debugf("Skipping service removal %s because owner id does not match, found: \"%s\", required: \"%s\"", aws.StringValue(service.Name), aws.StringValue(service.Description), label[endpoint.AWSSDDescriptionLabel])
log.Debugf("Skipping service removal %s because owner id does not match, found: \"%s\", required: \"%s\"", *service.Name, *service.Description, label[endpoint.AWSSDDescriptionLabel])
}
return nil
}
// RegisterInstance creates a new instance in given service.
func (p *AWSSDProvider) RegisterInstance(service *sd.Service, ep *endpoint.Endpoint) error {
func (p *AWSSDProvider) RegisterInstance(ctx context.Context, service *sdtypes.Service, ep *endpoint.Endpoint) error {
for _, target := range ep.Targets {
log.Infof("Registering a new instance \"%s\" for service \"%s\" (%s)", target, *service.Name, *service.Id)
attr := make(map[string]*string)
attr := make(map[string]string)
if ep.RecordType == endpoint.RecordTypeCNAME {
switch ep.RecordType {
case endpoint.RecordTypeCNAME:
if p.isAWSLoadBalancer(target) {
attr[sdInstanceAttrAlias] = aws.String(target)
attr[sdInstanceAttrAlias] = target
} else {
attr[sdInstanceAttrCname] = aws.String(target)
attr[sdInstanceAttrCname] = target
}
} else if ep.RecordType == endpoint.RecordTypeA {
attr[sdInstanceAttrIPV4] = aws.String(target)
} else {
case endpoint.RecordTypeA:
attr[sdInstanceAttrIPV4] = target
case endpoint.RecordTypeAAAA:
attr[sdInstanceAttrIPV6] = target
default:
return fmt.Errorf("invalid endpoint type (%v)", ep)
}
if !p.dryRun {
_, err := p.client.RegisterInstance(&sd.RegisterInstanceInput{
_, err := p.client.RegisterInstance(ctx, &sd.RegisterInstanceInput{
ServiceId: service.Id,
Attributes: attr,
InstanceId: aws.String(p.targetToInstanceID(target)),
@ -502,12 +522,12 @@ func (p *AWSSDProvider) RegisterInstance(service *sd.Service, ep *endpoint.Endpo
}
// DeregisterInstance removes an instance from given service.
func (p *AWSSDProvider) DeregisterInstance(service *sd.Service, ep *endpoint.Endpoint) error {
func (p *AWSSDProvider) DeregisterInstance(ctx context.Context, service *sdtypes.Service, ep *endpoint.Endpoint) error {
for _, target := range ep.Targets {
log.Infof("De-registering an instance \"%s\" for service \"%s\" (%s)", target, *service.Name, *service.Id)
if !p.dryRun {
_, err := p.client.DeregisterInstance(&sd.DeregisterInstanceInput{
_, err := p.client.DeregisterInstance(ctx, &sd.DeregisterInstanceInput{
InstanceId: aws.String(p.targetToInstanceID(target)),
ServiceId: service.Id,
})
@ -531,43 +551,7 @@ func (p *AWSSDProvider) targetToInstanceID(target string) string {
return strings.ToLower(target)
}
// nolint: deadcode
// used from unit test
func namespaceToNamespaceSummary(namespace *sd.Namespace) *sd.NamespaceSummary {
if namespace == nil {
return nil
}
return &sd.NamespaceSummary{
Id: namespace.Id,
Type: namespace.Type,
Name: namespace.Name,
Arn: namespace.Arn,
}
}
// nolint: deadcode
// used from unit test
func serviceToServiceSummary(service *sd.Service) *sd.ServiceSummary {
if service == nil {
return nil
}
return &sd.ServiceSummary{
Arn: service.Arn,
CreateDate: service.CreateDate,
Description: service.Description,
DnsConfig: service.DnsConfig,
HealthCheckConfig: service.HealthCheckConfig,
HealthCheckCustomConfig: service.HealthCheckCustomConfig,
Id: service.Id,
InstanceCount: service.InstanceCount,
Name: service.Name,
Type: service.Type,
}
}
func (p *AWSSDProvider) changesByNamespaceID(namespaces []*sd.NamespaceSummary, changes []*endpoint.Endpoint) map[string][]*endpoint.Endpoint {
func (p *AWSSDProvider) changesByNamespaceID(namespaces []*sdtypes.NamespaceSummary, changes []*endpoint.Endpoint) map[string][]*endpoint.Endpoint {
changesByNsID := make(map[string][]*endpoint.Endpoint)
for _, ns := range namespaces {
@ -600,8 +584,8 @@ func (p *AWSSDProvider) changesByNamespaceID(namespaces []*sd.NamespaceSummary,
}
// returns list of all namespaces matching given hostname
func matchingNamespaces(hostname string, namespaces []*sd.NamespaceSummary) []*sd.NamespaceSummary {
matchingNamespaces := make([]*sd.NamespaceSummary, 0)
func matchingNamespaces(hostname string, namespaces []*sdtypes.NamespaceSummary) []*sdtypes.NamespaceSummary {
matchingNamespaces := make([]*sdtypes.NamespaceSummary, 0)
for _, ns := range namespaces {
if *ns.Name == hostname {
@ -621,26 +605,30 @@ func (p *AWSSDProvider) parseHostname(hostname string) (namespace string, servic
}
// determine service routing policy based on endpoint type
func (p *AWSSDProvider) routingPolicyFromEndpoint(ep *endpoint.Endpoint) string {
if ep.RecordType == endpoint.RecordTypeA {
return sd.RoutingPolicyMultivalue
func (p *AWSSDProvider) routingPolicyFromEndpoint(ep *endpoint.Endpoint) sdtypes.RoutingPolicy {
if ep.RecordType == endpoint.RecordTypeA || ep.RecordType == endpoint.RecordTypeAAAA {
return sdtypes.RoutingPolicyMultivalue
}
return sd.RoutingPolicyWeighted
return sdtypes.RoutingPolicyWeighted
}
// determine service type (A, CNAME) from given endpoint
func (p *AWSSDProvider) serviceTypeFromEndpoint(ep *endpoint.Endpoint) string {
if ep.RecordType == endpoint.RecordTypeCNAME {
// determine service type (A, AAAA, CNAME) from given endpoint
func (p *AWSSDProvider) serviceTypeFromEndpoint(ep *endpoint.Endpoint) sdtypes.RecordType {
switch ep.RecordType {
case endpoint.RecordTypeCNAME:
// FIXME service type is derived from the first target only. Theoretically this may be problem.
// But I don't see a scenario where one endpoint contains targets of different types.
if p.isAWSLoadBalancer(ep.Targets[0]) {
// ALIAS target uses DNS record type of A
return sd.RecordTypeA
// ALIAS target uses DNS record of type A
return sdtypes.RecordTypeA
}
return sd.RecordTypeCname
return sdtypes.RecordTypeCname
case endpoint.RecordTypeAAAA:
return sdtypes.RecordTypeAaaa
default:
return sdtypes.RecordTypeA
}
return sd.RecordTypeA
}
// determine if a given hostname belongs to an AWS load balancer

View File

@ -25,9 +25,9 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
sd "github.com/aws/aws-sdk-go/service/servicediscovery"
"github.com/aws/aws-sdk-go-v2/aws"
sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery"
sdtypes "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -45,17 +45,17 @@ var (
type AWSSDClientStub struct {
// map[namespace_id]namespace
namespaces map[string]*sd.Namespace
namespaces map[string]*sdtypes.Namespace
// map[namespace_id] => map[service_id]instance
services map[string]map[string]*sd.Service
services map[string]map[string]*sdtypes.Service
// map[service_id] => map[inst_id]instance
instances map[string]map[string]*sd.Instance
instances map[string]map[string]*sdtypes.Instance
}
func (s *AWSSDClientStub) CreateService(input *sd.CreateServiceInput) (*sd.CreateServiceOutput, error) {
srv := &sd.Service{
func (s *AWSSDClientStub) CreateService(ctx context.Context, input *sd.CreateServiceInput, optFns ...func(*sd.Options)) (*sd.CreateServiceOutput, error) {
srv := &sdtypes.Service{
Id: aws.String(strconv.Itoa(rand.Intn(10000))),
DnsConfig: input.DnsConfig,
Name: input.Name,
@ -66,7 +66,7 @@ func (s *AWSSDClientStub) CreateService(input *sd.CreateServiceInput) (*sd.Creat
nsServices, ok := s.services[*input.NamespaceId]
if !ok {
nsServices = make(map[string]*sd.Service)
nsServices = make(map[string]*sdtypes.Service)
s.services[*input.NamespaceId] = nsServices
}
nsServices[*srv.Id] = srv
@ -76,14 +76,14 @@ func (s *AWSSDClientStub) CreateService(input *sd.CreateServiceInput) (*sd.Creat
}, nil
}
func (s *AWSSDClientStub) DeregisterInstance(input *sd.DeregisterInstanceInput) (*sd.DeregisterInstanceOutput, error) {
func (s *AWSSDClientStub) DeregisterInstance(ctx context.Context, input *sd.DeregisterInstanceInput, optFns ...func(options *sd.Options)) (*sd.DeregisterInstanceOutput, error) {
serviceInstances := s.instances[*input.ServiceId]
delete(serviceInstances, *input.InstanceId)
return &sd.DeregisterInstanceOutput{}, nil
}
func (s *AWSSDClientStub) GetService(input *sd.GetServiceInput) (*sd.GetServiceOutput, error) {
func (s *AWSSDClientStub) GetService(ctx context.Context, input *sd.GetServiceInput, optFns ...func(options *sd.Options)) (*sd.GetServiceOutput, error) {
for _, entry := range s.services {
srv, ok := entry[*input.Id]
if ok {
@ -96,18 +96,18 @@ func (s *AWSSDClientStub) GetService(input *sd.GetServiceInput) (*sd.GetServiceO
return nil, errors.New("service not found")
}
func (s *AWSSDClientStub) DiscoverInstancesWithContext(ctx context.Context, input *sd.DiscoverInstancesInput, opts ...request.Option) (*sd.DiscoverInstancesOutput, error) {
instances := make([]*sd.HttpInstanceSummary, 0)
func (s *AWSSDClientStub) DiscoverInstances(ctx context.Context, input *sd.DiscoverInstancesInput, opts ...func(options *sd.Options)) (*sd.DiscoverInstancesOutput, error) {
instances := make([]sdtypes.HttpInstanceSummary, 0)
var foundNs bool
for _, ns := range s.namespaces {
if aws.StringValue(ns.Name) == aws.StringValue(input.NamespaceName) {
if *ns.Name == *input.NamespaceName {
foundNs = true
for _, srv := range s.services[*ns.Id] {
if aws.StringValue(srv.Name) == aws.StringValue(input.ServiceName) {
if *srv.Name == *input.ServiceName {
for _, inst := range s.instances[*srv.Id] {
instances = append(instances, instanceToHTTPInstanceSummary(inst))
instances = append(instances, *instanceToHTTPInstanceSummary(inst))
}
}
}
@ -123,57 +123,50 @@ func (s *AWSSDClientStub) DiscoverInstancesWithContext(ctx context.Context, inpu
}, nil
}
func (s *AWSSDClientStub) ListNamespacesPages(input *sd.ListNamespacesInput, fn func(*sd.ListNamespacesOutput, bool) bool) error {
namespaces := make([]*sd.NamespaceSummary, 0)
filter := input.Filters[0]
func (s *AWSSDClientStub) ListNamespaces(ctx context.Context, input *sd.ListNamespacesInput, optFns ...func(options *sd.Options)) (*sd.ListNamespacesOutput, error) {
namespaces := make([]sdtypes.NamespaceSummary, 0)
for _, ns := range s.namespaces {
if filter != nil && *filter.Name == sd.NamespaceFilterNameType {
if *ns.Type != *filter.Values[0] {
if len(input.Filters) > 0 && input.Filters[0].Name == sdtypes.NamespaceFilterNameType {
if ns.Type != sdtypes.NamespaceType(input.Filters[0].Values[0]) {
// skip namespaces not matching filter
continue
}
}
namespaces = append(namespaces, namespaceToNamespaceSummary(ns))
namespaces = append(namespaces, *namespaceToNamespaceSummary(ns))
}
fn(&sd.ListNamespacesOutput{
return &sd.ListNamespacesOutput{
Namespaces: namespaces,
}, true)
return nil
}, nil
}
func (s *AWSSDClientStub) ListServicesPages(input *sd.ListServicesInput, fn func(*sd.ListServicesOutput, bool) bool) error {
services := make([]*sd.ServiceSummary, 0)
func (s *AWSSDClientStub) ListServices(ctx context.Context, input *sd.ListServicesInput, optFns ...func(options *sd.Options)) (*sd.ListServicesOutput, error) {
services := make([]sdtypes.ServiceSummary, 0)
// get namespace filter
filter := input.Filters[0]
if filter == nil || *filter.Name != sd.ServiceFilterNameNamespaceId {
return errors.New("missing namespace filter")
if len(input.Filters) == 0 || input.Filters[0].Name != sdtypes.ServiceFilterNameNamespaceId {
return nil, errors.New("missing namespace filter")
}
nsID := filter.Values[0]
nsID := input.Filters[0].Values[0]
for _, srv := range s.services[*nsID] {
services = append(services, serviceToServiceSummary(srv))
for _, srv := range s.services[nsID] {
services = append(services, *serviceToServiceSummary(srv))
}
fn(&sd.ListServicesOutput{
return &sd.ListServicesOutput{
Services: services,
}, true)
return nil
}, nil
}
func (s *AWSSDClientStub) RegisterInstance(input *sd.RegisterInstanceInput) (*sd.RegisterInstanceOutput, error) {
func (s *AWSSDClientStub) RegisterInstance(ctx context.Context, input *sd.RegisterInstanceInput, optFns ...func(options *sd.Options)) (*sd.RegisterInstanceOutput, error) {
srvInstances, ok := s.instances[*input.ServiceId]
if !ok {
srvInstances = make(map[string]*sd.Instance)
srvInstances = make(map[string]*sdtypes.Instance)
s.instances[*input.ServiceId] = srvInstances
}
srvInstances[*input.InstanceId] = &sd.Instance{
srvInstances[*input.InstanceId] = &sdtypes.Instance{
Id: input.InstanceId,
Attributes: input.Attributes,
CreatorRequestId: input.CreatorRequestId,
@ -182,8 +175,8 @@ func (s *AWSSDClientStub) RegisterInstance(input *sd.RegisterInstanceInput) (*sd
return &sd.RegisterInstanceOutput{}, nil
}
func (s *AWSSDClientStub) UpdateService(input *sd.UpdateServiceInput) (*sd.UpdateServiceOutput, error) {
out, err := s.GetService(&sd.GetServiceInput{Id: input.Id})
func (s *AWSSDClientStub) UpdateService(ctx context.Context, input *sd.UpdateServiceInput, optFns ...func(options *sd.Options)) (*sd.UpdateServiceOutput, error) {
out, err := s.GetService(ctx, &sd.GetServiceInput{Id: input.Id})
if err != nil {
return nil, err
}
@ -197,8 +190,8 @@ func (s *AWSSDClientStub) UpdateService(input *sd.UpdateServiceInput) (*sd.Updat
return &sd.UpdateServiceOutput{}, nil
}
func (s *AWSSDClientStub) DeleteService(input *sd.DeleteServiceInput) (*sd.DeleteServiceOutput, error) {
out, err := s.GetService(&sd.GetServiceInput{Id: input.Id})
func (s *AWSSDClientStub) DeleteService(ctx context.Context, input *sd.DeleteServiceInput, optFns ...func(options *sd.Options)) (*sd.DeleteServiceOutput, error) {
out, err := s.GetService(ctx, &sd.GetServiceInput{Id: input.Id})
if err != nil {
return nil, err
}
@ -221,39 +214,69 @@ func newTestAWSSDProvider(api AWSSDClient, domainFilter endpoint.DomainFilter, n
}
}
// nolint: deadcode
// used for unit test
func instanceToHTTPInstanceSummary(instance *sd.Instance) *sd.HttpInstanceSummary {
func instanceToHTTPInstanceSummary(instance *sdtypes.Instance) *sdtypes.HttpInstanceSummary {
if instance == nil {
return nil
}
return &sd.HttpInstanceSummary{
return &sdtypes.HttpInstanceSummary{
InstanceId: instance.Id,
Attributes: instance.Attributes,
}
}
func namespaceToNamespaceSummary(namespace *sdtypes.Namespace) *sdtypes.NamespaceSummary {
if namespace == nil {
return nil
}
return &sdtypes.NamespaceSummary{
Id: namespace.Id,
Type: namespace.Type,
Name: namespace.Name,
Arn: namespace.Arn,
}
}
func serviceToServiceSummary(service *sdtypes.Service) *sdtypes.ServiceSummary {
if service == nil {
return nil
}
return &sdtypes.ServiceSummary{
Arn: service.Arn,
CreateDate: service.CreateDate,
Description: service.Description,
DnsConfig: service.DnsConfig,
HealthCheckConfig: service.HealthCheckConfig,
HealthCheckCustomConfig: service.HealthCheckCustomConfig,
Id: service.Id,
InstanceCount: service.InstanceCount,
Name: service.Name,
Type: service.Type,
}
}
func TestAWSSDProvider_Records(t *testing.T) {
namespaces := map[string]*sd.Namespace{
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sd.Service{
services := map[string]map[string]*sdtypes.Service{
"private": {
"a-srv": {
Id: aws.String("a-srv"),
Name: aws.String("service1"),
NamespaceId: aws.String("private"),
Description: aws.String("owner-id"),
DnsConfig: &sd.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeA),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(100),
}},
},
@ -261,12 +284,12 @@ func TestAWSSDProvider_Records(t *testing.T) {
"alias-srv": {
Id: aws.String("alias-srv"),
Name: aws.String("service2"),
NamespaceId: aws.String("private"),
Description: aws.String("owner-id"),
DnsConfig: &sd.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeA),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(100),
}},
},
@ -274,47 +297,68 @@ func TestAWSSDProvider_Records(t *testing.T) {
"cname-srv": {
Id: aws.String("cname-srv"),
Name: aws.String("service3"),
NamespaceId: aws.String("private"),
Description: aws.String("owner-id"),
DnsConfig: &sd.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeCname),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeCname,
TTL: aws.Int64(80),
}},
},
},
"aaaa-srv": {
Id: aws.String("aaaa-srv"),
Name: aws.String("service4"),
Description: aws.String("owner-id"),
DnsConfig: &sdtypes.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeAaaa,
TTL: aws.Int64(100),
}},
},
},
},
}
instances := map[string]map[string]*sd.Instance{
instances := map[string]map[string]*sdtypes.Instance{
"a-srv": {
"1.2.3.4": {
Id: aws.String("1.2.3.4"),
Attributes: map[string]*string{
sdInstanceAttrIPV4: aws.String("1.2.3.4"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.4",
},
},
"1.2.3.5": {
Id: aws.String("1.2.3.5"),
Attributes: map[string]*string{
sdInstanceAttrIPV4: aws.String("1.2.3.5"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.5",
},
},
},
"alias-srv": {
"load-balancer.us-east-1.elb.amazonaws.com": {
Id: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
Attributes: map[string]*string{
sdInstanceAttrAlias: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
Attributes: map[string]string{
sdInstanceAttrAlias: "load-balancer.us-east-1.elb.amazonaws.com",
},
},
},
"cname-srv": {
"cname.target.com": {
Id: aws.String("cname.target.com"),
Attributes: map[string]*string{
sdInstanceAttrCname: aws.String("cname.target.com"),
Attributes: map[string]string{
sdInstanceAttrCname: "cname.target.com",
},
},
},
"aaaa-srv": {
"0000:0000:0000:0000:abcd:abcd:abcd:abcd": {
Id: aws.String("0000:0000:0000:0000:abcd:abcd:abcd:abcd"),
Attributes: map[string]string{
sdInstanceAttrIPV6: "0000:0000:0000:0000:abcd:abcd:abcd:abcd",
},
},
},
@ -324,6 +368,7 @@ func TestAWSSDProvider_Records(t *testing.T) {
{DNSName: "service1.private.com", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"}, RecordType: endpoint.RecordTypeA, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
{DNSName: "service2.private.com", Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
{DNSName: "service3.private.com", Targets: endpoint.Targets{"cname.target.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 80, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
{DNSName: "service4.private.com", Targets: endpoint.Targets{"0000:0000:0000:0000:abcd:abcd:abcd:abcd"}, RecordType: endpoint.RecordTypeAAAA, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
}
api := &AWSSDClientStub{
@ -340,18 +385,18 @@ func TestAWSSDProvider_Records(t *testing.T) {
}
func TestAWSSDProvider_ApplyChanges(t *testing.T) {
namespaces := map[string]*sd.Namespace{
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: make(map[string]map[string]*sd.Service),
instances: make(map[string]map[string]*sd.Instance),
services: make(map[string]map[string]*sdtypes.Service),
instances: make(map[string]map[string]*sdtypes.Instance),
}
expectedEndpoints := []*endpoint.Endpoint{
@ -371,7 +416,7 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) {
// make sure services were created
assert.Len(t, api.services["private"], 3)
existingServices, _ := provider.ListServicesByNamespaceID(namespaces["private"].Id)
existingServices, _ := provider.ListServicesByNamespaceID(context.Background(), namespaces["private"].Id)
assert.NotNil(t, existingServices["service1"])
assert.NotNil(t, existingServices["service2"])
assert.NotNil(t, existingServices["service3"])
@ -392,16 +437,16 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) {
}
func TestAWSSDProvider_ListNamespaces(t *testing.T) {
namespaces := map[string]*sd.Namespace{
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
"public": {
Id: aws.String("public"),
Name: aws.String("public.com"),
Type: aws.String(sd.NamespaceTypeDnsPublic),
Type: sdtypes.NamespaceTypeDnsPublic,
},
}
@ -413,20 +458,20 @@ func TestAWSSDProvider_ListNamespaces(t *testing.T) {
msg string
domainFilter endpoint.DomainFilter
namespaceTypeFilter string
expectedNamespaces []*sd.NamespaceSummary
expectedNamespaces []*sdtypes.NamespaceSummary
}{
{"public filter", endpoint.NewDomainFilter([]string{}), "public", []*sd.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
{"private filter", endpoint.NewDomainFilter([]string{}), "private", []*sd.NamespaceSummary{namespaceToNamespaceSummary(namespaces["private"])}},
{"domain filter", endpoint.NewDomainFilter([]string{"public.com"}), "", []*sd.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
{"non-existing domain", endpoint.NewDomainFilter([]string{"xxx.com"}), "", []*sd.NamespaceSummary{}},
{"public filter", endpoint.NewDomainFilter([]string{}), "public", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
{"private filter", endpoint.NewDomainFilter([]string{}), "private", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["private"])}},
{"domain filter", endpoint.NewDomainFilter([]string{"public.com"}), "", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
{"non-existing domain", endpoint.NewDomainFilter([]string{"xxx.com"}), "", []*sdtypes.NamespaceSummary{}},
} {
provider := newTestAWSSDProvider(api, tc.domainFilter, tc.namespaceTypeFilter, "")
result, err := provider.ListNamespaces()
result, err := provider.ListNamespaces(context.Background())
require.NoError(t, err)
expectedMap := make(map[string]*sd.NamespaceSummary)
resultMap := make(map[string]*sd.NamespaceSummary)
expectedMap := make(map[string]*sdtypes.NamespaceSummary)
resultMap := make(map[string]*sdtypes.NamespaceSummary)
for _, ns := range tc.expectedNamespaces {
expectedMap[*ns.Id] = ns
}
@ -441,20 +486,20 @@ func TestAWSSDProvider_ListNamespaces(t *testing.T) {
}
func TestAWSSDProvider_ListServicesByNamespace(t *testing.T) {
namespaces := map[string]*sd.Namespace{
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
"public": {
Id: aws.String("public"),
Name: aws.String("public.com"),
Type: aws.String(sd.NamespaceTypeDnsPublic),
Type: sdtypes.NamespaceTypeDnsPublic,
},
}
services := map[string]map[string]*sd.Service{
services := map[string]map[string]*sdtypes.Service{
"private": {
"srv1": {
Id: aws.String("srv1"),
@ -482,48 +527,74 @@ func TestAWSSDProvider_ListServicesByNamespace(t *testing.T) {
}
for _, tc := range []struct {
expectedServices map[string]*sd.Service
expectedServices map[string]*sdtypes.Service
}{
{map[string]*sd.Service{"service1": services["private"]["srv1"], "service2": services["private"]["srv2"]}},
{map[string]*sdtypes.Service{"service1": services["private"]["srv1"], "service2": services["private"]["srv2"]}},
} {
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
result, err := provider.ListServicesByNamespaceID(namespaces["private"].Id)
result, err := provider.ListServicesByNamespaceID(context.Background(), namespaces["private"].Id)
require.NoError(t, err)
assert.Equal(t, tc.expectedServices, result)
}
}
func TestAWSSDProvider_CreateService(t *testing.T) {
namespaces := map[string]*sd.Namespace{
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: make(map[string]map[string]*sd.Service),
services: make(map[string]map[string]*sdtypes.Service),
}
expectedServices := make(map[string]*sd.Service)
expectedServices := make(map[string]*sdtypes.Service)
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
// A type
provider.CreateService(aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{
provider.CreateService(context.Background(), aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{
Labels: map[string]string{
endpoint.AWSSDDescriptionLabel: "A-srv",
},
RecordType: endpoint.RecordTypeA,
RecordTTL: 60,
Targets: endpoint.Targets{"1.2.3.4"},
})
expectedServices["A-srv"] = &sd.Service{
Name: aws.String("A-srv"),
DnsConfig: &sd.DnsConfig{
RoutingPolicy: aws.String(sd.RoutingPolicyMultivalue),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeA),
expectedServices["A-srv"] = &sdtypes.Service{
Name: aws.String("A-srv"),
Description: aws.String("A-srv"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyMultivalue,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(60),
}},
},
NamespaceId: aws.String("private"),
}
// AAAA type
provider.CreateService(context.Background(), aws.String("private"), aws.String("AAAA-srv"), &endpoint.Endpoint{
Labels: map[string]string{
endpoint.AWSSDDescriptionLabel: "AAAA-srv",
},
RecordType: endpoint.RecordTypeAAAA,
RecordTTL: 60,
Targets: endpoint.Targets{"::1234:5678:"},
})
expectedServices["AAAA-srv"] = &sdtypes.Service{
Name: aws.String("AAAA-srv"),
Description: aws.String("AAAA-srv"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyMultivalue,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeAaaa,
TTL: aws.Int64(60),
}},
},
@ -531,17 +602,21 @@ func TestAWSSDProvider_CreateService(t *testing.T) {
}
// CNAME type
provider.CreateService(aws.String("private"), aws.String("CNAME-srv"), &endpoint.Endpoint{
provider.CreateService(context.Background(), aws.String("private"), aws.String("CNAME-srv"), &endpoint.Endpoint{
Labels: map[string]string{
endpoint.AWSSDDescriptionLabel: "CNAME-srv",
},
RecordType: endpoint.RecordTypeCNAME,
RecordTTL: 80,
Targets: endpoint.Targets{"cname.target.com"},
})
expectedServices["CNAME-srv"] = &sd.Service{
Name: aws.String("CNAME-srv"),
DnsConfig: &sd.DnsConfig{
RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeCname),
expectedServices["CNAME-srv"] = &sdtypes.Service{
Name: aws.String("CNAME-srv"),
Description: aws.String("CNAME-srv"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeCname,
TTL: aws.Int64(80),
}},
},
@ -549,17 +624,21 @@ func TestAWSSDProvider_CreateService(t *testing.T) {
}
// ALIAS type
provider.CreateService(aws.String("private"), aws.String("ALIAS-srv"), &endpoint.Endpoint{
provider.CreateService(context.Background(), aws.String("private"), aws.String("ALIAS-srv"), &endpoint.Endpoint{
Labels: map[string]string{
endpoint.AWSSDDescriptionLabel: "ALIAS-srv",
},
RecordType: endpoint.RecordTypeCNAME,
RecordTTL: 100,
Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"},
})
expectedServices["ALIAS-srv"] = &sd.Service{
Name: aws.String("ALIAS-srv"),
DnsConfig: &sd.DnsConfig{
RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeA),
expectedServices["ALIAS-srv"] = &sdtypes.Service{
Name: aws.String("ALIAS-srv"),
Description: aws.String("ALIAS-srv"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(100),
}},
},
@ -569,7 +648,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) {
validateAWSSDServicesMapsEqual(t, expectedServices, api.services["private"])
}
func validateAWSSDServicesMapsEqual(t *testing.T, expected map[string]*sd.Service, services map[string]*sd.Service) {
func validateAWSSDServicesMapsEqual(t *testing.T, expected map[string]*sdtypes.Service, services map[string]*sdtypes.Service) {
require.Len(t, services, len(expected))
for _, srv := range services {
@ -577,31 +656,31 @@ func validateAWSSDServicesMapsEqual(t *testing.T, expected map[string]*sd.Servic
}
}
func validateAWSSDServicesEqual(t *testing.T, expected *sd.Service, srv *sd.Service) {
assert.Equal(t, aws.StringValue(expected.Description), aws.StringValue(srv.Description))
assert.Equal(t, aws.StringValue(expected.Name), aws.StringValue(srv.Name))
func validateAWSSDServicesEqual(t *testing.T, expected *sdtypes.Service, srv *sdtypes.Service) {
assert.Equal(t, *expected.Description, *srv.Description)
assert.Equal(t, *expected.Name, *srv.Name)
assert.True(t, reflect.DeepEqual(*expected.DnsConfig, *srv.DnsConfig))
}
func TestAWSSDProvider_UpdateService(t *testing.T) {
namespaces := map[string]*sd.Namespace{
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sd.Service{
services := map[string]map[string]*sdtypes.Service{
"private": {
"srv1": {
Id: aws.String("srv1"),
Name: aws.String("service1"),
DnsConfig: &sd.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: aws.String(sd.RoutingPolicyMultivalue),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeA),
Id: aws.String("srv1"),
Name: aws.String("service1"),
NamespaceId: aws.String("private"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyMultivalue,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(60),
}},
},
@ -617,7 +696,7 @@ func TestAWSSDProvider_UpdateService(t *testing.T) {
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
// update service with different TTL
provider.UpdateService(services["private"]["srv1"], &endpoint.Endpoint{
provider.UpdateService(context.Background(), services["private"]["srv1"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeA,
RecordTTL: 100,
})
@ -626,15 +705,15 @@ func TestAWSSDProvider_UpdateService(t *testing.T) {
}
func TestAWSSDProvider_DeleteService(t *testing.T) {
namespaces := map[string]*sd.Namespace{
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sd.Service{
services := map[string]map[string]*sdtypes.Service{
"private": {
"srv1": {
Id: aws.String("srv1"),
@ -665,16 +744,16 @@ func TestAWSSDProvider_DeleteService(t *testing.T) {
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "owner-id")
// delete first service
err := provider.DeleteService(services["private"]["srv1"])
err := provider.DeleteService(context.Background(), services["private"]["srv1"])
assert.NoError(t, err)
assert.Len(t, api.services["private"], 2)
// delete third service
err1 := provider.DeleteService(services["private"]["srv3"])
err1 := provider.DeleteService(context.Background(), services["private"]["srv3"])
assert.NoError(t, err1)
assert.Len(t, api.services["private"], 1)
expectedServices := map[string]*sd.Service{
expectedServices := map[string]*sdtypes.Service{
"srv2": {
Id: aws.String("srv2"),
Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"),
@ -687,130 +766,157 @@ func TestAWSSDProvider_DeleteService(t *testing.T) {
}
func TestAWSSDProvider_RegisterInstance(t *testing.T) {
namespaces := map[string]*sd.Namespace{
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sd.Service{
services := map[string]map[string]*sdtypes.Service{
"private": {
"a-srv": {
Id: aws.String("a-srv"),
Name: aws.String("service1"),
DnsConfig: &sd.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeA),
Id: aws.String("a-srv"),
Name: aws.String("service1"),
NamespaceId: aws.String("private"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(60),
}},
},
},
"cname-srv": {
Id: aws.String("cname-srv"),
Name: aws.String("service2"),
DnsConfig: &sd.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeCname),
Id: aws.String("cname-srv"),
Name: aws.String("service2"),
NamespaceId: aws.String("private"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeCname,
TTL: aws.Int64(60),
}},
},
},
"alias-srv": {
Id: aws.String("alias-srv"),
Name: aws.String("service3"),
DnsConfig: &sd.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
DnsRecords: []*sd.DnsRecord{{
Type: aws.String(sd.RecordTypeA),
Id: aws.String("alias-srv"),
Name: aws.String("service3"),
NamespaceId: aws.String("private"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(60),
}},
},
},
"aaaa-srv": {
Id: aws.String("aaaa-srv"),
Name: aws.String("service4"),
Description: aws.String("owner-id"),
DnsConfig: &sdtypes.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeAaaa,
TTL: aws.Int64(100),
}},
},
},
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: services,
instances: make(map[string]map[string]*sd.Instance),
instances: make(map[string]map[string]*sdtypes.Instance),
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
expectedInstances := make(map[string]*sd.Instance)
expectedInstances := make(map[string]*sdtypes.Instance)
// IP-based instance
provider.RegisterInstance(services["private"]["a-srv"], &endpoint.Endpoint{
// IPv4-based instance
provider.RegisterInstance(context.Background(), services["private"]["a-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeA,
DNSName: "service1.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"},
})
expectedInstances["1.2.3.4"] = &sd.Instance{
expectedInstances["1.2.3.4"] = &sdtypes.Instance{
Id: aws.String("1.2.3.4"),
Attributes: map[string]*string{
sdInstanceAttrIPV4: aws.String("1.2.3.4"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.4",
},
}
expectedInstances["1.2.3.5"] = &sd.Instance{
expectedInstances["1.2.3.5"] = &sdtypes.Instance{
Id: aws.String("1.2.3.5"),
Attributes: map[string]*string{
sdInstanceAttrIPV4: aws.String("1.2.3.5"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.5",
},
}
// AWS ELB instance (ALIAS)
provider.RegisterInstance(services["private"]["alias-srv"], &endpoint.Endpoint{
provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeCNAME,
DNSName: "service1.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com", "load-balancer.us-west-2.elb.amazonaws.com"},
})
expectedInstances["load-balancer.us-east-1.elb.amazonaws.com"] = &sd.Instance{
expectedInstances["load-balancer.us-east-1.elb.amazonaws.com"] = &sdtypes.Instance{
Id: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
Attributes: map[string]*string{
sdInstanceAttrAlias: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
Attributes: map[string]string{
sdInstanceAttrAlias: "load-balancer.us-east-1.elb.amazonaws.com",
},
}
expectedInstances["load-balancer.us-west-2.elb.amazonaws.com"] = &sd.Instance{
expectedInstances["load-balancer.us-west-2.elb.amazonaws.com"] = &sdtypes.Instance{
Id: aws.String("load-balancer.us-west-2.elb.amazonaws.com"),
Attributes: map[string]*string{
sdInstanceAttrAlias: aws.String("load-balancer.us-west-2.elb.amazonaws.com"),
Attributes: map[string]string{
sdInstanceAttrAlias: "load-balancer.us-west-2.elb.amazonaws.com",
},
}
// AWS NLB instance (ALIAS)
provider.RegisterInstance(services["private"]["alias-srv"], &endpoint.Endpoint{
provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeCNAME,
DNSName: "service1.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"load-balancer.elb.us-west-2.amazonaws.com"},
})
expectedInstances["load-balancer.elb.us-west-2.amazonaws.com"] = &sd.Instance{
expectedInstances["load-balancer.elb.us-west-2.amazonaws.com"] = &sdtypes.Instance{
Id: aws.String("load-balancer.elb.us-west-2.amazonaws.com"),
Attributes: map[string]*string{
sdInstanceAttrAlias: aws.String("load-balancer.elb.us-west-2.amazonaws.com"),
Attributes: map[string]string{
sdInstanceAttrAlias: "load-balancer.elb.us-west-2.amazonaws.com",
},
}
// CNAME instance
provider.RegisterInstance(services["private"]["cname-srv"], &endpoint.Endpoint{
provider.RegisterInstance(context.Background(), services["private"]["cname-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeCNAME,
DNSName: "service2.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"cname.target.com"},
})
expectedInstances["cname.target.com"] = &sd.Instance{
expectedInstances["cname.target.com"] = &sdtypes.Instance{
Id: aws.String("cname.target.com"),
Attributes: map[string]*string{
sdInstanceAttrCname: aws.String("cname.target.com"),
Attributes: map[string]string{
sdInstanceAttrCname: "cname.target.com",
},
}
// IPv6-based instance
provider.RegisterInstance(context.Background(), services["private"]["aaaa-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeAAAA,
DNSName: "service4.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"0000:0000:0000:0000:abcd:abcd:abcd:abcd"},
})
expectedInstances["0000:0000:0000:0000:abcd:abcd:abcd:abcd"] = &sdtypes.Instance{
Id: aws.String("0000:0000:0000:0000:abcd:abcd:abcd:abcd"),
Attributes: map[string]string{
sdInstanceAttrIPV6: "0000:0000:0000:0000:abcd:abcd:abcd:abcd",
},
}
@ -825,15 +931,15 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) {
}
func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
namespaces := map[string]*sd.Namespace{
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sd.Service{
services := map[string]map[string]*sdtypes.Service{
"private": {
"srv1": {
Id: aws.String("srv1"),
@ -842,12 +948,12 @@ func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
},
}
instances := map[string]map[string]*sd.Instance{
instances := map[string]map[string]*sdtypes.Instance{
"srv1": {
"1.2.3.4": {
Id: aws.String("1.2.3.4"),
Attributes: map[string]*string{
sdInstanceAttrIPV4: aws.String("1.2.3.4"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.4",
},
},
},
@ -861,7 +967,43 @@ func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
provider.DeregisterInstance(services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))
provider.DeregisterInstance(context.Background(), services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))
assert.Len(t, instances["srv1"], 0)
}
func TestAWSSDProvider_awsTags(t *testing.T) {
tests := []struct {
Expectation []sdtypes.Tag
Input map[string]string
}{
{
Expectation: []sdtypes.Tag{
{
Key: aws.String("key1"),
Value: aws.String("value1"),
},
{
Key: aws.String("key2"),
Value: aws.String("value2"),
},
},
Input: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
{
Expectation: []sdtypes.Tag{},
Input: map[string]string{},
},
{
Expectation: []sdtypes.Tag{},
Input: nil,
},
}
for _, test := range tests {
require.ElementsMatch(t, test.Expectation, awsTags(test.Input))
}
}

View File

@ -21,6 +21,7 @@ import (
"context"
"fmt"
"strings"
"time"
log "github.com/sirupsen/logrus"
@ -60,13 +61,14 @@ type AzureProvider struct {
userAssignedIdentityClientID string
activeDirectoryAuthorityHost string
zonesClient ZonesClient
zonesCache *zonesCache[dns.Zone]
recordSetsClient RecordSetsClient
}
// NewAzureProvider creates a new Azure provider.
//
// Returns the provider or an error if a provider could not be created.
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, dryRun bool) (*AzureProvider, error) {
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, dryRun bool) (*AzureProvider, error) {
cfg, err := getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
@ -93,6 +95,7 @@ func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zon
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
activeDirectoryAuthorityHost: cfg.ActiveDirectoryAuthorityHost,
zonesClient: zonesClient,
zonesCache: &zonesCache[dns.Zone]{duration: zonesCacheDuration},
recordSetsClient: recordSetsClient,
}, nil
}
@ -167,6 +170,10 @@ func (p *AzureProvider) ApplyChanges(ctx context.Context, changes *plan.Changes)
func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) {
log.Debugf("Retrieving Azure DNS zones for resource group: %s.", p.resourceGroup)
if !p.zonesCache.Expired() {
log.Debugf("Using cached Azure DNS zones for resource group: %s zone count: %d.", p.resourceGroup, len(p.zonesCache.Get()))
return p.zonesCache.Get(), nil
}
var zones []dns.Zone
pager := p.zonesClient.NewListByResourceGroupPager(p.resourceGroup, &dns.ZonesClientListByResourceGroupOptions{Top: nil})
for pager.More() {
@ -183,7 +190,8 @@ func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) {
}
}
}
log.Debugf("Found %d Azure DNS zone(s).", len(zones))
log.Debugf("Found %d Azure DNS zone(s). Updating zones cache", len(zones))
p.zonesCache.Reset(zones)
return zones, nil
}

View File

@ -21,6 +21,7 @@ import (
"context"
"fmt"
"strings"
"time"
azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
@ -55,13 +56,14 @@ type AzurePrivateDNSProvider struct {
userAssignedIdentityClientID string
activeDirectoryAuthorityHost string
zonesClient PrivateZonesClient
zonesCache *zonesCache[privatedns.PrivateZone]
recordSetsClient PrivateRecordSetsClient
}
// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
//
// Returns the provider or an error if a provider could not be created.
func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, dryRun bool) (*AzurePrivateDNSProvider, error) {
func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, dryRun bool) (*AzurePrivateDNSProvider, error) {
cfg, err := getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
@ -88,6 +90,7 @@ func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainF
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
activeDirectoryAuthorityHost: cfg.ActiveDirectoryAuthorityHost,
zonesClient: zonesClient,
zonesCache: &zonesCache[privatedns.PrivateZone]{duration: zonesCacheDuration},
recordSetsClient: recordSetsClient,
}, nil
}
@ -177,7 +180,10 @@ func (p *AzurePrivateDNSProvider) ApplyChanges(ctx context.Context, changes *pla
func (p *AzurePrivateDNSProvider) zones(ctx context.Context) ([]privatedns.PrivateZone, error) {
log.Debugf("Retrieving Azure Private DNS zones for Resource Group '%s'", p.resourceGroup)
if !p.zonesCache.Expired() {
log.Debugf("Using cached Azure Private DNS zones for resource group: %s zone count: %d.", p.resourceGroup, len(p.zonesCache.Get()))
return p.zonesCache.Get(), nil
}
var zones []privatedns.PrivateZone
pager := p.zonesClient.NewListByResourceGroupPager(p.resourceGroup, &privatedns.PrivateZonesClientListByResourceGroupOptions{Top: nil})
@ -198,7 +204,8 @@ func (p *AzurePrivateDNSProvider) zones(ctx context.Context) ([]privatedns.Priva
}
}
log.Debugf("Found %d Azure Private DNS zone(s).", len(zones))
log.Debugf("Found %d Azure Private DNS zone(s). Updating zones cache", len(zones))
p.zonesCache.Reset(zones)
return zones, nil
}

View File

@ -238,6 +238,7 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilt
dryRun: dryRun,
resourceGroup: resourceGroup,
zonesClient: privateZonesClient,
zonesCache: &zonesCache[privatedns.PrivateZone]{duration: 0},
recordSetsClient: privateRecordsClient,
}
}

View File

@ -238,6 +238,7 @@ func newAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoin
userAssignedIdentityClientID: userAssignedIdentityClientID,
activeDirectoryAuthorityHost: activeDirectoryAuthorityHost,
zonesClient: zonesClient,
zonesCache: &zonesCache[dns.Zone]{duration: 0},
recordSetsClient: recordsClient,
}
}

50
provider/azure/cache.go Normal file
View File

@ -0,0 +1,50 @@
/*
Copyright 2024 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 (
"time"
)
// zonesCache is a cache for Azure zones(private or public)
type zonesCache[T any] struct {
age time.Time
duration time.Duration
zones []T
}
// Reset method to reset the zones and update the age. This will be used to update the cache
// after making a new API call to get the zones.
func (z *zonesCache[T]) Reset(zones []T) {
if z.duration > time.Duration(0) {
z.age = time.Now()
z.zones = zones
}
}
// Get method to retrieve the cached zones. If cache is not expired, this will be used
// instead of making a new API call to get the zones.
func (z *zonesCache[T]) Get() []T {
return z.zones
}
// Expired method to check if the cache has expired based on duration or if zones are empty.
// If cache is expired, a new API call will be made to get the zones. If zones are empty, a new
// API call will be made to get the zones. This case comes in at the time of initialization.
func (z *zonesCache[T]) Expired() bool {
return len(z.zones) < 1 || time.Since(z.age) > z.duration
}

View File

@ -0,0 +1,78 @@
/*
Copyright 2024 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 (
"testing"
"time"
dns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
"github.com/stretchr/testify/assert"
)
func TestzonesCache(t *testing.T) {
now := time.Now()
zoneName := "example.com"
var testCases = map[string]struct {
z *zonesCache[dns.Zone]
expired bool
}{
"inactive-zone-cache": {
&zonesCache[dns.Zone]{
duration: 0 * time.Second,
},
true,
},
"empty-active-zone-cache": {
&zonesCache[dns.Zone]{
duration: 30 * time.Second,
},
true,
},
"expired-zone-cache": {
&zonesCache[dns.Zone]{
age: now.Add(-300 * time.Second),
duration: 30 * time.Second,
},
true,
},
"active-zone-cache": {
&zonesCache[dns.Zone]{
zones: []dns.Zone{{
Name: &zoneName,
}},
duration: 30 * time.Second,
age: now,
},
false,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
assert.Equal(t, testCase.expired, testCase.z.Expired())
var resetZoneLength = 1
if testCase.z.duration == 0 {
resetZoneLength = 0
}
testCase.z.Reset([]dns.Zone{{
Name: &zoneName,
}})
assert.Len(t, testCase.z.Get(), resetZoneLength)
})
}
}

View File

@ -148,8 +148,8 @@ type RecordParamsTypes interface {
cloudflare.UpdateDNSRecordParams | cloudflare.CreateDNSRecordParams
}
// getUpdateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
func getUpdateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams {
// updateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams {
return cloudflare.UpdateDNSRecordParams{
Name: cfc.ResourceRecord.Name,
TTL: cfc.ResourceRecord.TTL,
@ -368,7 +368,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord)
continue
}
recordParam := getUpdateDNSRecordParam(*change)
recordParam := updateDNSRecordParam(*change)
regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(*change)
recordParam.ID = recordID
err := p.Client.UpdateDNSRecord(ctx, resourceContainer, recordParam)

View File

@ -54,7 +54,6 @@ type mockCloudFlareClient struct {
var ExampleDomain = []cloudflare.DNSRecord{
{
ID: "1234567890",
ZoneID: "001",
Name: "foobar.bar.com",
Type: endpoint.RecordTypeA,
TTL: 120,
@ -63,7 +62,6 @@ var ExampleDomain = []cloudflare.DNSRecord{
},
{
ID: "2345678901",
ZoneID: "001",
Name: "foobar.bar.com",
Type: endpoint.RecordTypeA,
TTL: 120,
@ -72,7 +70,6 @@ var ExampleDomain = []cloudflare.DNSRecord{
},
{
ID: "1231231233",
ZoneID: "002",
Name: "bar.foo.com",
Type: endpoint.RecordTypeA,
TTL: 1,
@ -340,7 +337,6 @@ func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endp
}
err = provider.ApplyChanges(context.Background(), changes)
if err != nil {
t.Fatalf("cannot apply changes, %s", err)
}
@ -1179,7 +1175,6 @@ func TestProviderPropertiesIdempotency(t *testing.T) {
"001": {
{
ID: "1234567890",
ZoneID: "001",
Name: "foobar.bar.com",
Type: endpoint.RecordTypeA,
TTL: 120,
@ -1283,7 +1278,6 @@ func TestCloudflareComplexUpdate(t *testing.T) {
planned := plan.Calculate()
err = provider.ApplyChanges(context.Background(), planned.Changes)
if err != nil {
t.Errorf("should not fail, %s", err)
}
@ -1335,7 +1329,6 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
"001": {
{
ID: "1234567890",
ZoneID: "001",
Name: "foobar.bar.com",
Type: endpoint.RecordTypeA,
TTL: 1,

View File

@ -20,6 +20,7 @@ import (
"context"
"fmt"
"os"
"strconv"
"strings"
"github.com/digitalocean/godo"
@ -158,13 +159,15 @@ func (p *DigitalOceanProvider) Records(ctx context.Context) ([]*endpoint.Endpoin
endpoints := []*endpoint.Endpoint{}
for _, zone := range zones {
records, err := p.fetchRecords(ctx, zone.Name)
if err != nil {
return nil, err
}
for _, r := range records {
if provider.SupportedRecordType(r.Type) {
if p.SupportedRecordType(r.Type) {
name := r.Name + "." + zone.Name
data := r.Data
// root name is identified by @ and should be
// translated to zone name for the endpoint entry.
@ -172,7 +175,11 @@ func (p *DigitalOceanProvider) Records(ctx context.Context) ([]*endpoint.Endpoin
name = zone.Name
}
ep := endpoint.NewEndpointWithTTL(name, r.Type, endpoint.TTL(r.TTL), r.Data)
if r.Type == endpoint.RecordTypeMX {
data = fmt.Sprintf("%d %s", r.Priority, r.Data)
}
ep := endpoint.NewEndpointWithTTL(name, r.Type, endpoint.TTL(r.TTL), data)
endpoints = append(endpoints, ep)
}
@ -283,16 +290,33 @@ func makeDomainEditRequest(domain, name, recordType, data string, ttl int) *godo
// For some reason the DO API requires the '.' at the end of "data" in case of CNAME request.
// Example: {"type":"CNAME","name":"hello","data":"www.example.com."}
if recordType == endpoint.RecordTypeCNAME && !strings.HasSuffix(data, ".") {
if (recordType == endpoint.RecordTypeCNAME || recordType == endpoint.RecordTypeMX) && !strings.HasSuffix(data, ".") {
data += "."
}
return &godo.DomainRecordEditRequest{
request := &godo.DomainRecordEditRequest{
Name: adjustedName,
Type: recordType,
Data: data,
TTL: ttl,
}
if recordType == endpoint.RecordTypeMX {
priority, domain, err := parseMxTarget(data)
if err == nil {
request.Priority = int(priority)
request.Data = provider.EnsureTrailingDot(domain)
} else {
log.WithFields(log.Fields{
"domain": domain,
"dnsName": name,
"recordType": recordType,
"data": data,
}).Warn("Unable to parse MX target")
}
}
return request
}
// submitChanges applies an instance of `digitalOceanChanges` to the DigitalOcean API.
@ -303,13 +327,19 @@ func (p *DigitalOceanProvider) submitChanges(ctx context.Context, changes *digit
}
for _, c := range changes.Creates {
log.WithFields(log.Fields{
logFields := log.Fields{
"domain": c.Domain,
"dnsName": c.Options.Name,
"recordType": c.Options.Type,
"data": c.Options.Data,
"ttl": c.Options.TTL,
}).Debug("Creating domain record")
}
if c.Options.Type == endpoint.RecordTypeMX {
logFields["priority"] = c.Options.Priority
}
log.WithFields(logFields).Debug("Creating domain record")
if p.DryRun {
continue
@ -322,13 +352,17 @@ func (p *DigitalOceanProvider) submitChanges(ctx context.Context, changes *digit
}
for _, u := range changes.Updates {
log.WithFields(log.Fields{
logFields := log.Fields{
"domain": u.Domain,
"dnsName": u.Options.Name,
"recordType": u.Options.Type,
"data": u.Options.Data,
"ttl": u.Options.TTL,
}).Debug("Updating domain record")
}
if u.Options.Type == endpoint.RecordTypeMX {
logFields["priority"] = u.Options.Priority
}
log.WithFields(logFields).Debug("Updating domain record")
if p.DryRun {
continue
@ -589,6 +623,16 @@ func processDeleteActions(
return nil
}
// SupportedRecordType returns true if the record type is supported by the provider
func (p *DigitalOceanProvider) SupportedRecordType(recordType string) bool {
switch recordType {
case "MX":
return true
default:
return provider.SupportedRecordType(recordType)
}
}
// ApplyChanges applies the given set of generic changes to the provider.
func (p *DigitalOceanProvider) ApplyChanges(ctx context.Context, planChanges *plan.Changes) error {
// TODO: This should only retrieve zones affected by the given `planChanges`.
@ -617,3 +661,18 @@ func (p *DigitalOceanProvider) ApplyChanges(ctx context.Context, planChanges *pl
return p.submitChanges(ctx, &changes)
}
func parseMxTarget(mxTarget string) (priority int64, exchange string, err error) {
targetParts := strings.SplitN(mxTarget, " ", 2)
if len(targetParts) != 2 {
return priority, exchange, fmt.Errorf("mx target needs to be of form '10 example.com'")
}
priorityRaw, exchange := targetParts[0], targetParts[1]
priority, err = strconv.ParseInt(priorityRaw, 10, 32)
if err != nil {
return priority, exchange, fmt.Errorf("invalid priority specified")
}
return priority, exchange, nil
}

View File

@ -100,6 +100,9 @@ func (m *mockDigitalOceanClient) Records(ctx context.Context, domain string, opt
{ID: 1, Name: "foo.ext-dns-test", Type: "CNAME"},
{ID: 2, Name: "bar.ext-dns-test", Type: "CNAME"},
{ID: 3, Name: "@", Type: endpoint.RecordTypeCNAME},
{ID: 4, Name: "@", Type: endpoint.RecordTypeMX, Priority: 10, Data: "mx1.foo.com."},
{ID: 5, Name: "@", Type: endpoint.RecordTypeMX, Priority: 10, Data: "mx2.foo.com."},
{ID: 6, Name: "@", Type: endpoint.RecordTypeTXT, Data: "SOME-TXT-TEXT"},
}, &godo.Response{
Links: &godo.Links{
Pages: &godo.Pages{
@ -344,6 +347,28 @@ func TestDigitalOceanMakeDomainEditRequest(t *testing.T) {
Data: "bar.example.com.",
TTL: customTTL,
}, r4)
// Ensure that MX records have `.` appended.
r5 := makeDomainEditRequest("example.com", "foo.example.com", endpoint.RecordTypeMX,
"10 mx.example.com", digitalOceanRecordTTL)
assert.Equal(t, &godo.DomainRecordEditRequest{
Type: endpoint.RecordTypeMX,
Name: "foo",
Data: "mx.example.com.",
Priority: 10,
TTL: digitalOceanRecordTTL,
}, r5)
// Ensure that MX records do not have an extra `.` appended if they already have a `.`
r6 := makeDomainEditRequest("example.com", "foo.example.com", endpoint.RecordTypeMX,
"10 mx.example.com.", digitalOceanRecordTTL)
assert.Equal(t, &godo.DomainRecordEditRequest{
Type: endpoint.RecordTypeMX,
Name: "foo",
Data: "mx.example.com.",
Priority: 10,
TTL: digitalOceanRecordTTL,
}, r6)
}
func TestDigitalOceanApplyChanges(t *testing.T) {
@ -375,6 +400,8 @@ func TestDigitalOceanProcessCreateActions(t *testing.T) {
"example.com": {
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "foo.example.com"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeMX, "10 mx.example.com"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "SOME-TXT-TEXT"),
},
}
@ -382,7 +409,7 @@ func TestDigitalOceanProcessCreateActions(t *testing.T) {
err := processCreateActions(recordsByDomain, createsByDomain, &changes)
require.NoError(t, err)
assert.Equal(t, 2, len(changes.Creates))
assert.Equal(t, 4, len(changes.Creates))
assert.Equal(t, 0, len(changes.Updates))
assert.Equal(t, 0, len(changes.Deletes))
@ -405,6 +432,25 @@ func TestDigitalOceanProcessCreateActions(t *testing.T) {
TTL: digitalOceanRecordTTL,
},
},
{
Domain: "example.com",
Options: &godo.DomainRecordEditRequest{
Name: "@",
Type: endpoint.RecordTypeMX,
Priority: 10,
Data: "mx.example.com.",
TTL: digitalOceanRecordTTL,
},
},
{
Domain: "example.com",
Options: &godo.DomainRecordEditRequest{
Name: "@",
Type: endpoint.RecordTypeTXT,
Data: "SOME-TXT-TEXT",
TTL: digitalOceanRecordTTL,
},
},
}
if !elementsMatch(t, expectedCreates, changes.Creates) {
@ -436,6 +482,29 @@ func TestDigitalOceanProcessUpdateActions(t *testing.T) {
Data: "foo.example.com.",
TTL: digitalOceanRecordTTL,
},
{
ID: 4,
Name: "@",
Type: endpoint.RecordTypeMX,
Data: "mx1.example.com.",
Priority: 10,
TTL: digitalOceanRecordTTL,
},
{
ID: 5,
Name: "@",
Type: endpoint.RecordTypeMX,
Data: "mx2.example.com.",
Priority: 10,
TTL: digitalOceanRecordTTL,
},
{
ID: 6,
Name: "@",
Type: endpoint.RecordTypeTXT,
Data: "SOME_TXTX_TEXT",
TTL: digitalOceanRecordTTL,
},
},
}
@ -443,6 +512,8 @@ func TestDigitalOceanProcessUpdateActions(t *testing.T) {
"example.com": {
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "10.11.12.13"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "bar.example.com"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeMX, "10 mx3.example.com"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "ANOTHER-TXT"),
},
}
@ -450,9 +521,9 @@ func TestDigitalOceanProcessUpdateActions(t *testing.T) {
err := processUpdateActions(recordsByDomain, updatesByDomain, &changes)
require.NoError(t, err)
assert.Equal(t, 2, len(changes.Creates))
assert.Equal(t, 4, len(changes.Creates))
assert.Equal(t, 0, len(changes.Updates))
assert.Equal(t, 3, len(changes.Deletes))
assert.Equal(t, 6, len(changes.Deletes))
expectedCreates := []*digitalOceanChangeCreate{
{
@ -473,6 +544,25 @@ func TestDigitalOceanProcessUpdateActions(t *testing.T) {
TTL: digitalOceanRecordTTL,
},
},
{
Domain: "example.com",
Options: &godo.DomainRecordEditRequest{
Name: "@",
Type: endpoint.RecordTypeMX,
Data: "mx3.example.com.",
Priority: 10,
TTL: digitalOceanRecordTTL,
},
},
{
Domain: "example.com",
Options: &godo.DomainRecordEditRequest{
Name: "@",
Type: endpoint.RecordTypeTXT,
Data: "ANOTHER-TXT",
TTL: digitalOceanRecordTTL,
},
},
}
if !elementsMatch(t, expectedCreates, changes.Creates) {
@ -492,6 +582,18 @@ func TestDigitalOceanProcessUpdateActions(t *testing.T) {
Domain: "example.com",
RecordID: 3,
},
{
Domain: "example.com",
RecordID: 4,
},
{
Domain: "example.com",
RecordID: 5,
},
{
Domain: "example.com",
RecordID: 6,
},
}
if !elementsMatch(t, expectedDeletes, changes.Deletes) {
@ -597,6 +699,26 @@ func TestDigitalOceanGetMatchingDomainRecords(t *testing.T) {
Type: endpoint.RecordTypeA,
Data: "9.10.11.12",
},
{
ID: 5,
Name: "@",
Type: endpoint.RecordTypeMX,
Priority: 10,
Data: "mx1.foo.com.",
},
{
ID: 6,
Name: "@",
Type: endpoint.RecordTypeMX,
Priority: 10,
Data: "mx2.foo.com.",
},
{
ID: 7,
Name: "@",
Type: endpoint.RecordTypeTXT,
Data: "MYTXT",
},
}
ep1 := endpoint.NewEndpoint("foo.com", endpoint.RecordTypeCNAME)
@ -627,6 +749,17 @@ func TestDigitalOceanGetMatchingDomainRecords(t *testing.T) {
r2 := getMatchingDomainRecords(records, "example.com", ep4)
assert.Equal(t, 1, len(r2))
assert.Equal(t, "9.10.11.12", r2[0].Data)
ep5 := endpoint.NewEndpoint("example.com", endpoint.RecordTypeMX)
r3 := getMatchingDomainRecords(records, "example.com", ep5)
assert.Equal(t, 2, len(r3))
assert.Equal(t, "mx1.foo.com.", r3[0].Data)
assert.Equal(t, "mx2.foo.com.", r3[1].Data)
ep6 := endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT)
r4 := getMatchingDomainRecords(records, "example.com", ep6)
assert.Equal(t, 1, len(r4))
assert.Equal(t, "MYTXT", r4[0].Data)
}
func validateDigitalOceanZones(t *testing.T, zones []godo.Domain, expected []godo.Domain) {
@ -663,7 +796,7 @@ func TestDigitalOceanAllRecords(t *testing.T) {
if err != nil {
t.Errorf("should not fail, %s", err)
}
require.Equal(t, 5, len(records))
require.Equal(t, 7, len(records))
provider.Client = &mockDigitalOceanRecordsFail{}
_, err = provider.Records(ctx)
@ -678,11 +811,15 @@ func TestDigitalOceanMergeRecordsByNameType(t *testing.T) {
endpoint.NewEndpoint("bar.example.com", "A", "1.2.3.4"),
endpoint.NewEndpoint("foo.example.com", "A", "5.6.7.8"),
endpoint.NewEndpoint("foo.example.com", "CNAME", "somewhere.out.there.com"),
endpoint.NewEndpoint("bar.example.com", "MX", "10 bar.mx1.com"),
endpoint.NewEndpoint("bar.example.com", "MX", "10 bar.mx2.com"),
endpoint.NewEndpoint("foo.example.com", "TXT", "txtone"),
endpoint.NewEndpoint("foo.example.com", "TXT", "txttwo"),
}
merged := mergeEndpointsByNameType(xs)
assert.Equal(t, 3, len(merged))
assert.Equal(t, 5, len(merged))
sort.SliceStable(merged, func(i, j int) bool {
if merged[i].DNSName != merged[j].DNSName {
return merged[i].DNSName < merged[j].DNSName
@ -693,14 +830,22 @@ func TestDigitalOceanMergeRecordsByNameType(t *testing.T) {
assert.Equal(t, "A", merged[0].RecordType)
assert.Equal(t, 1, len(merged[0].Targets))
assert.Equal(t, "1.2.3.4", merged[0].Targets[0])
assert.Equal(t, "foo.example.com", merged[1].DNSName)
assert.Equal(t, "A", merged[1].RecordType)
assert.Equal(t, "MX", merged[1].RecordType)
assert.Equal(t, 2, len(merged[1].Targets))
assert.ElementsMatch(t, []string{"1.2.3.4", "5.6.7.8"}, merged[1].Targets)
assert.ElementsMatch(t, []string{"10 bar.mx1.com", "10 bar.mx2.com"}, merged[1].Targets)
assert.Equal(t, "foo.example.com", merged[2].DNSName)
assert.Equal(t, "CNAME", merged[2].RecordType)
assert.Equal(t, 1, len(merged[2].Targets))
assert.Equal(t, "somewhere.out.there.com", merged[2].Targets[0])
assert.Equal(t, "A", merged[2].RecordType)
assert.Equal(t, 2, len(merged[2].Targets))
assert.ElementsMatch(t, []string{"1.2.3.4", "5.6.7.8"}, merged[2].Targets)
assert.Equal(t, "foo.example.com", merged[3].DNSName)
assert.Equal(t, "CNAME", merged[3].RecordType)
assert.Equal(t, 1, len(merged[3].Targets))
assert.Equal(t, "somewhere.out.there.com", merged[3].Targets[0])
assert.Equal(t, "foo.example.com", merged[4].DNSName)
assert.Equal(t, "TXT", merged[4].RecordType)
assert.Equal(t, 2, len(merged[4].Targets))
assert.ElementsMatch(t, []string{"txtone", "txttwo"}, merged[4].Targets)
}

View File

@ -194,7 +194,7 @@ func (p *GoogleProvider) Zones(ctx context.Context) (map[string]*dns.ManagedZone
log.Debugf("Matching zones against domain filters: %v", p.domainFilter)
if err := p.managedZonesClient.List(p.project).Pages(ctx, f); err != nil {
return nil, err
return nil, provider.NewSoftError(fmt.Errorf("failed to list zones: %w", err))
}
if len(zones) == 0 {
@ -228,7 +228,7 @@ func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.End
for _, z := range zones {
if err := p.resourceRecordSetsClient.List(p.project, z.Name).Pages(ctx, f); err != nil {
return nil, err
return nil, provider.NewSoftError(fmt.Errorf("failed to list records in zone %s: %w", z.Name, err))
}
}
@ -302,7 +302,7 @@ func (p *GoogleProvider) submitChange(ctx context.Context, change *dns.Change) e
}
if _, err := p.changesClient.Create(p.project, zone, c).Do(); err != nil {
return err
return provider.NewSoftError(fmt.Errorf("failed to create changes: %w", err))
}
time.Sleep(p.batchChangeInterval)

View File

@ -59,7 +59,8 @@ func (m *mockManagedZonesCreateCall) Do(opts ...googleapi.CallOption) (*dns.Mana
}
type mockManagedZonesListCall struct {
project string
project string
zonesListSoftErr error
}
func (m *mockManagedZonesListCall) Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error {
@ -71,22 +72,29 @@ func (m *mockManagedZonesListCall) Pages(ctx context.Context, f func(*dns.Manage
}
}
if m.zonesListSoftErr != nil {
return m.zonesListSoftErr
}
return f(&dns.ManagedZonesListResponse{ManagedZones: zones})
}
type mockManagedZonesClient struct{}
type mockManagedZonesClient struct {
zonesErr error
}
func (m *mockManagedZonesClient) Create(project string, managedZone *dns.ManagedZone) managedZonesCreateCallInterface {
return &mockManagedZonesCreateCall{project: project, managedZone: managedZone}
}
func (m *mockManagedZonesClient) List(project string) managedZonesListCallInterface {
return &mockManagedZonesListCall{project: project}
return &mockManagedZonesListCall{project: project, zonesListSoftErr: m.zonesErr}
}
type mockResourceRecordSetsListCall struct {
project string
managedZone string
project string
managedZone string
recordsListSoftErr error
}
func (m *mockResourceRecordSetsListCall) Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error {
@ -102,13 +110,19 @@ func (m *mockResourceRecordSetsListCall) Pages(ctx context.Context, f func(*dns.
resp = append(resp, v)
}
if m.recordsListSoftErr != nil {
return m.recordsListSoftErr
}
return f(&dns.ResourceRecordSetsListResponse{Rrsets: resp})
}
type mockResourceRecordSetsClient struct{}
type mockResourceRecordSetsClient struct {
recordsErr error
}
func (m *mockResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface {
return &mockResourceRecordSetsListCall{project: project, managedZone: managedZone}
return &mockResourceRecordSetsListCall{project: project, managedZone: managedZone, recordsListSoftErr: m.recordsErr}
}
type mockChangesCreateCall struct {
@ -242,25 +256,12 @@ func TestGoogleZonesVisibilityFilterPrivatePeering(t *testing.T) {
zones, err := provider.Zones(context.Background())
require.NoError(t, err)
validateZones(t, zones, map[string]*dns.ManagedZone{
"svc-local": {Name: "svc-local", DnsName: "svc.local.", Id: 1005, Visibility: "private"},
})
}
func TestGoogleZones(t *testing.T) {
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
zones, err := provider.Zones(context.Background())
require.NoError(t, err)
validateZones(t, zones, map[string]*dns.ManagedZone{
"zone-1-ext-dns-test-2-gcp-zalan-do": {Name: "zone-1-ext-dns-test-2-gcp-zalan-do", DnsName: "zone-1.ext-dns-test-2.gcp.zalan.do."},
"zone-2-ext-dns-test-2-gcp-zalan-do": {Name: "zone-2-ext-dns-test-2-gcp-zalan-do", DnsName: "zone-2.ext-dns-test-2.gcp.zalan.do."},
"zone-3-ext-dns-test-2-gcp-zalan-do": {Name: "zone-3-ext-dns-test-2-gcp-zalan-do", DnsName: "zone-3.ext-dns-test-2.gcp.zalan.do."},
})
}
func TestGoogleRecords(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, endpoint.TTL(1), "1.2.3.4"),
@ -268,7 +269,7 @@ func TestGoogleRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(3), "foo.elb.amazonaws.com"),
}
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, originalEndpoints)
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, originalEndpoints, nil, nil)
records, err := provider.Records(context.Background())
require.NoError(t, err)
@ -299,6 +300,8 @@ func TestGoogleRecordsFilter(t *testing.T) {
provider.NewZoneIDFilter([]string{""}),
false,
originalEndpoints,
nil,
nil,
)
// these records should be filtered out since they don't match a hosted zone or domain filter.
@ -343,6 +346,8 @@ func TestGoogleApplyChanges(t *testing.T) {
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "qux.elb.amazonaws.com"),
},
nil,
nil,
)
createRecords := []*endpoint.Endpoint{
@ -407,7 +412,7 @@ func TestGoogleApplyChangesDryRun(t *testing.T) {
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, googleRecordTTL, "qux.elb.amazonaws.com"),
}
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), true, originalEndpoints)
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), true, originalEndpoints, nil, nil)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
@ -449,12 +454,12 @@ func TestGoogleApplyChangesDryRun(t *testing.T) {
}
func TestGoogleApplyChangesEmpty(t *testing.T) {
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}, nil, nil)
assert.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{}))
}
func TestNewFilteredRecords(t *testing.T) {
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
provider := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}, nil, nil)
records := provider.newFilteredRecords([]*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 1, "8.8.4.4"),
@ -603,6 +608,26 @@ func TestGoogleBatchChangeSetExceedingNameChange(t *testing.T) {
require.Equal(t, 0, len(batchCs))
}
func TestSoftErrListZonesConflict(t *testing.T) {
p := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{}), false, []*endpoint.Endpoint{}, provider.NewSoftError(fmt.Errorf("failed to list zones")), nil)
zones, err := p.Zones(context.Background())
require.Error(t, err)
require.ErrorIs(t, err, provider.SoftError)
require.Empty(t, zones)
}
func TestSoftErrListRecordsConflict(t *testing.T) {
p := newGoogleProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), provider.NewZoneIDFilter([]string{}), false, []*endpoint.Endpoint{}, nil, provider.NewSoftError(fmt.Errorf("failed to list records in zone")))
records, err := p.Records(context.Background())
require.Error(t, err)
require.ErrorIs(t, err, provider.SoftError)
require.Empty(t, records)
}
func sortChangesByName(cs *dns.Change) {
sort.SliceStable(cs.Additions, func(i, j int) bool {
return cs.Additions[i].Name < cs.Additions[j].Name
@ -647,7 +672,7 @@ func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected
assert.Equal(t, expected.Type, record.Type)
}
func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider {
func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, dryRun bool, _ []*endpoint.Endpoint) *GoogleProvider {
provider := &GoogleProvider{
project: "zalando-external-dns-test",
dryRun: false,
@ -694,7 +719,6 @@ func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilt
Visibility: "private",
})
createZone(t, provider, &dns.ManagedZone{
Name: "svc-local",
DnsName: "svc.local.",
@ -703,27 +727,31 @@ func newGoogleProviderZoneOverlap(t *testing.T, domainFilter endpoint.DomainFilt
})
createZone(t, provider, &dns.ManagedZone{
Name: "svc-local-peer",
DnsName: "svc.local.",
Id: 10006,
Visibility: "private",
Name: "svc-local-peer",
DnsName: "svc.local.",
Id: 10006,
Visibility: "private",
PeeringConfig: &dns.ManagedZonePeeringConfig{TargetNetwork: nil},
})
provider.dryRun = dryRun
return provider
}
func newGoogleProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider {
func newGoogleProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint, zonesErr, recordsErr error) *GoogleProvider {
provider := &GoogleProvider{
project: "zalando-external-dns-test",
dryRun: false,
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
resourceRecordSetsClient: &mockResourceRecordSetsClient{},
managedZonesClient: &mockManagedZonesClient{},
changesClient: &mockChangesClient{},
project: "zalando-external-dns-test",
dryRun: false,
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
resourceRecordSetsClient: &mockResourceRecordSetsClient{
recordsErr: recordsErr,
},
managedZonesClient: &mockManagedZonesClient{
zonesErr: zonesErr,
},
changesClient: &mockChangesClient{},
}
createZone(t, provider, &dns.ManagedZone{
@ -754,10 +782,10 @@ func newGoogleProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDF
return provider
}
func createZone(t *testing.T, provider *GoogleProvider, zone *dns.ManagedZone) {
func createZone(t *testing.T, p *GoogleProvider, zone *dns.ManagedZone) {
zone.Description = "Testing zone for kubernetes.io/external-dns"
if _, err := provider.managedZonesClient.Create("zalando-external-dns-test", zone).Do(); err != nil {
if _, err := p.managedZonesClient.Create("zalando-external-dns-test", zone).Do(); err != nil {
if err, ok := err.(*googleapi.Error); !ok || err.Code != http.StatusConflict {
require.NoError(t, err)
}
@ -770,8 +798,7 @@ func setupGoogleRecords(t *testing.T, provider *GoogleProvider, endpoints []*end
clearGoogleRecords(t, provider, "zone-3-ext-dns-test-2-gcp-zalan-do")
ctx := context.Background()
records, err := provider.Records(ctx)
require.NoError(t, err)
records, _ := provider.Records(ctx)
validateEndpoints(t, records, []*endpoint.Endpoint{})
@ -779,15 +806,15 @@ func setupGoogleRecords(t *testing.T, provider *GoogleProvider, endpoints []*end
Create: endpoints,
}))
records, err = provider.Records(ctx)
require.NoError(t, err)
records, _ = provider.Records(ctx)
validateEndpoints(t, records, endpoints)
}
func clearGoogleRecords(t *testing.T, provider *GoogleProvider, zone string) {
recordSets := []*dns.ResourceRecordSet{}
require.NoError(t, provider.resourceRecordSetsClient.List(provider.project, zone).Pages(context.Background(), func(resp *dns.ResourceRecordSetsListResponse) error {
provider.resourceRecordSetsClient.List(provider.project, zone).Pages(context.Background(), func(resp *dns.ResourceRecordSetsListResponse) error {
for _, r := range resp.Rrsets {
switch r.Type {
case endpoint.RecordTypeA, endpoint.RecordTypeCNAME:
@ -795,7 +822,7 @@ func clearGoogleRecords(t *testing.T, provider *GoogleProvider, zone string) {
}
}
return nil
}))
})
if len(recordSets) != 0 {
_, err := provider.changesClient.Create(provider.project, zone, &dns.Change{

View File

@ -22,6 +22,7 @@ import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"math"
"net"
"net/http"
@ -43,9 +44,7 @@ type pdnsChangeType string
const (
apiBase = "/api/v1"
// Unless we use something like pdnsproxy (discontinued upstream), this value will _always_ be localhost
defaultServerID = "localhost"
defaultTTL = 300
defaultTTL = 300
// PdnsDelete and PdnsReplace are effectively an enum for "pgo.RrSet.changetype"
// TODO: Can we somehow get this from the pgo swagger client library itself?
@ -66,6 +65,7 @@ type PDNSConfig struct {
DomainFilter endpoint.DomainFilter
DryRun bool
Server string
ServerID string
APIKey string
TLSConfig TLSConfig
}
@ -137,6 +137,7 @@ type PDNSAPIProvider interface {
// PDNSAPIClient : Struct that encapsulates all the PowerDNS specific implementation details
type PDNSAPIClient struct {
dryRun bool
serverID string
authCtx context.Context
client *pgo.APIClient
domainFilter endpoint.DomainFilter
@ -146,7 +147,7 @@ type PDNSAPIClient struct {
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones
func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err error) {
for i := 0; i < retryLimit; i++ {
zones, resp, err = c.client.ZonesApi.ListZones(c.authCtx, defaultServerID)
zones, resp, err = c.client.ZonesApi.ListZones(c.authCtx, c.serverID)
if err != nil {
log.Debugf("Unable to fetch zones %v", err)
log.Debugf("Retrying ListZones() ... %d", i)
@ -156,8 +157,7 @@ func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err
return zones, resp, err
}
log.Errorf("Unable to fetch zones. %v", err)
return zones, resp, err
return zones, resp, provider.NewSoftError(fmt.Errorf("unable to list zones: %v", err))
}
// PartitionZones : Method returns a slice of zones that adhere to the domain filter and a slice of ones that does not adhere to the filter
@ -180,7 +180,7 @@ func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zo
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones-zone_id
func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Response, err error) {
for i := 0; i < retryLimit; i++ {
zone, resp, err = c.client.ZonesApi.ListZone(c.authCtx, defaultServerID, zoneID)
zone, resp, err = c.client.ZonesApi.ListZone(c.authCtx, c.serverID, zoneID)
if err != nil {
log.Debugf("Unable to fetch zone %v", err)
log.Debugf("Retrying ListZone() ... %d", i)
@ -190,15 +190,14 @@ func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Respo
return zone, resp, err
}
log.Errorf("Unable to list zone. %v", err)
return zone, resp, err
return zone, resp, provider.NewSoftError(fmt.Errorf("unable to list zone: %v", err))
}
// PatchZone : Method used to update the contents of a particular zone from PowerDNS
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#patch--servers-server_id-zones-zone_id
func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *http.Response, err error) {
for i := 0; i < retryLimit; i++ {
resp, err = c.client.ZonesApi.PatchZone(c.authCtx, defaultServerID, zoneID, zoneStruct)
resp, err = c.client.ZonesApi.PatchZone(c.authCtx, c.serverID, zoneID, zoneStruct)
if err != nil {
log.Debugf("Unable to patch zone %v", err)
log.Debugf("Retrying PatchZone() ... %d", i)
@ -208,8 +207,7 @@ func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *htt
return resp, err
}
log.Errorf("Unable to patch zone. %v", err)
return resp, err
return resp, provider.NewSoftError(fmt.Errorf("unable to patch zone: %v", err))
}
// PDNSProvider is an implementation of the Provider interface for PowerDNS
@ -245,6 +243,7 @@ func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, err
provider := &PDNSProvider{
client: &PDNSAPIClient{
dryRun: config.DryRun,
serverID: config.ServerID,
authCtx: context.WithValue(ctx, pgo.ContextAPIKey, pgo.APIKey{Key: config.APIKey}),
client: pgo.NewAPIClient(pdnsClientConfig),
domainFilter: config.DomainFilter,
@ -314,7 +313,7 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
records := []pgo.Record{}
RecordType_ := ep.RecordType
for _, t := range ep.Targets {
if ep.RecordType == "CNAME" || ep.RecordType == "ALIAS" {
if ep.RecordType == "CNAME" || ep.RecordType == "ALIAS" || ep.RecordType == "MX" || ep.RecordType == "SRV" {
t = provider.EnsureTrailingDot(t)
}
records = append(records, pgo.Record{Content: t})
@ -335,7 +334,7 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
// DELETEs explicitly forbid a TTL, therefore only PATCHes need the TTL
if changetype == PdnsReplace {
if int64(ep.RecordTTL) > int64(math.MaxInt32) {
return nil, errors.New("value of record TTL overflows, limited to int32")
return nil, provider.NewSoftError(fmt.Errorf("value of record TTL overflows, limited to int32"))
}
if ep.RecordTTL == 0 {
// No TTL was specified for the record, we use the default
@ -418,8 +417,7 @@ func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpo
for _, zone := range filteredZones {
z, _, err := p.client.ListZone(zone.Id)
if err != nil {
log.Warnf("Unable to fetch Records")
return nil, err
return nil, provider.NewSoftError(fmt.Errorf("unable to fetch records: %v", err))
}
for _, rr := range z.Rrsets {

View File

@ -18,7 +18,7 @@ package pdns
import (
"context"
"errors"
"fmt"
"net/http"
"regexp"
"strings"
@ -29,6 +29,7 @@ import (
"github.com/stretchr/testify/suite"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/provider"
)
// FIXME: What do we do about labels?
@ -117,6 +118,27 @@ var (
},
}
// RRSet with MX record
RRSetMXRecord = pgo.RrSet{
Name: "example.com.",
Type_: "MX",
Ttl: 300,
Records: []pgo.Record{
{Content: "10 mailhost1.example.com", Disabled: false, SetPtr: false},
{Content: "10 mailhost2.example.com", Disabled: false, SetPtr: false},
},
}
// RRSet with SRV record
RRSetSRVRecord = pgo.RrSet{
Name: "_service._tls.example.com.",
Type_: "SRV",
Ttl: 300,
Records: []pgo.Record{
{Content: "100 1 443 service.example.com", Disabled: false, SetPtr: false},
},
}
endpointsDisabledRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
}
@ -144,6 +166,8 @@ var (
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"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "10 mailhost1.example.com", "10 mailhost2.example.com"),
endpoint.NewEndpointWithTTL("_service._tls.example.com", endpoint.RecordTypeSRV, endpoint.TTL(300), "100 1 443 service.example.com"),
}
endpointsMultipleZones = []*endpoint.Endpoint{
@ -233,7 +257,7 @@ var (
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{RRSetCNAMERecord, RRSetTXTRecord, RRSetMultipleRecords, RRSetALIASRecord},
Rrsets: []pgo.RrSet{RRSetCNAMERecord, RRSetTXTRecord, RRSetMultipleRecords, RRSetALIASRecord, RRSetMXRecord, RRSetSRVRecord},
}
ZoneEmptyToSimplePatch = pgo.Zone{
@ -691,7 +715,7 @@ type PDNSAPIClientStubPatchZoneFailure struct {
// Just overwrite the PatchZone method to introduce a failure
func (c *PDNSAPIClientStubPatchZoneFailure) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
return nil, errors.New("Generic PDNS Error")
return nil, provider.NewSoftError(fmt.Errorf("Generic PDNS Error"))
}
/******************************************************************************/
@ -703,7 +727,7 @@ type PDNSAPIClientStubListZoneFailure struct {
// Just overwrite the ListZone method to introduce a failure
func (c *PDNSAPIClientStubListZoneFailure) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
return pgo.Zone{}, nil, errors.New("Generic PDNS Error")
return pgo.Zone{}, nil, provider.NewSoftError(fmt.Errorf("Generic PDNS Error"))
}
/******************************************************************************/
@ -715,7 +739,7 @@ type PDNSAPIClientStubListZonesFailure struct {
// Just overwrite the ListZones method to introduce a failure
func (c *PDNSAPIClientStubListZonesFailure) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{}, nil, errors.New("Generic PDNS Error")
return []pgo.Zone{}, nil, provider.NewSoftError(fmt.Errorf("Generic PDNS Error"))
}
/******************************************************************************/
@ -879,12 +903,14 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSRecords() {
}
_, err = p.Records(ctx)
assert.NotNil(suite.T(), err)
assert.ErrorIs(suite.T(), err, provider.SoftError)
p = &PDNSProvider{
client: &PDNSAPIClientStubListZonesFailure{},
}
_, err = p.Records(ctx)
assert.NotNil(suite.T(), err)
assert.ErrorIs(suite.T(), err, provider.SoftError)
}
func (suite *NewPDNSProviderTestSuite) TestPDNSConvertEndpointsToZones() {
@ -944,6 +970,20 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSConvertEndpointsToZones() {
}
}
// Check endpoints of type MX and SRV always have their values end with a trailing dot.
zlist, err = p.ConvertEndpointsToZones(endpointsMixedRecords, PdnsReplace)
assert.Nil(suite.T(), err)
for _, z := range zlist {
for _, rs := range z.Rrsets {
if rs.Type_ == "MX" || rs.Type_ == "SRV" {
for _, r := range rs.Records {
assert.Equal(suite.T(), uint8(0x2e), r.Content[len(r.Content)-1])
}
}
}
}
// Check endpoints of type CNAME are converted to ALIAS on the domain apex
zlist, err = p.ConvertEndpointsToZones(endpointsApexRecords, PdnsReplace)
assert.Nil(suite.T(), err)
@ -1024,6 +1064,7 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSmutateRecords() {
// Check inserting endpoints from a single zone
err = p.mutateRecords(endpointsSimpleRecord, pdnsChangeType("REPLACE"))
assert.NotNil(suite.T(), err)
assert.ErrorIs(suite.T(), err, provider.SoftError)
}
func (suite *NewPDNSProviderTestSuite) TestPDNSClientPartitionZones() {

View File

@ -1,551 +0,0 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rdns
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"math/rand"
"os"
"regexp"
"strings"
"time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
clientv3 "go.etcd.io/etcd/client/v3"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
const (
etcdTimeout = 5 * time.Second
rdnsMaxHosts = 10
rdnsOriginalLabel = "originalText"
rdnsPrefix = "/rdnsv3"
rdnsTimeout = 5 * time.Second
)
func init() {
rand.New(rand.NewSource(time.Now().UnixNano()))
}
// RDNSClient is an interface to work with Rancher DNS(RDNS) records in etcdv3 backend.
type RDNSClient interface {
Get(key string) ([]RDNSRecord, error)
List(rootDomain string) ([]RDNSRecord, error)
Set(value RDNSRecord) error
Delete(key string) error
}
// RDNSConfig contains configuration to create a new Rancher DNS(RDNS) provider.
type RDNSConfig struct {
DryRun bool
DomainFilter endpoint.DomainFilter
RootDomain string
}
// RDNSProvider is an implementation of Provider for Rancher DNS(RDNS).
type RDNSProvider struct {
provider.BaseProvider
client RDNSClient
dryRun bool
domainFilter endpoint.DomainFilter
rootDomain string
}
// RDNSRecord represents Rancher DNS(RDNS) etcdv3 record.
type RDNSRecord struct {
AggregationHosts []string `json:"aggregation_hosts,omitempty"`
Host string `json:"host,omitempty"`
Text string `json:"text,omitempty"`
TTL uint32 `json:"ttl,omitempty"`
Key string `json:"-"`
}
// RDNSRecordType represents Rancher DNS(RDNS) etcdv3 record type.
type RDNSRecordType struct {
Type string `json:"type,omitempty"`
Domain string `json:"domain,omitempty"`
}
type etcdv3Client struct {
client *clientv3.Client
ctx context.Context
}
var _ RDNSClient = etcdv3Client{}
// NewRDNSProvider initializes a new Rancher DNS(RDNS) based Provider.
func NewRDNSProvider(config RDNSConfig) (*RDNSProvider, error) {
client, err := newEtcdv3Client()
if err != nil {
return nil, err
}
domain := os.Getenv("RDNS_ROOT_DOMAIN")
if domain == "" {
return nil, errors.New("needed root domain environment")
}
return &RDNSProvider{
client: client,
dryRun: config.DryRun,
domainFilter: config.DomainFilter,
rootDomain: domain,
}, nil
}
// Records returns all DNS records found in Rancher DNS(RDNS) etcdv3 backend. Depending on the record fields
// it may be mapped to one or two records of type A, TXT, A+TXT.
func (p RDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
var result []*endpoint.Endpoint
rs, err := p.client.List(p.rootDomain)
if err != nil {
return nil, err
}
for _, r := range rs {
domains := strings.Split(strings.TrimPrefix(r.Key, rdnsPrefix+"/"), "/")
keyToDNSNameSplits(domains)
dnsName := strings.Join(domains, ".")
if !p.domainFilter.Match(dnsName) {
continue
}
// only return rdnsMaxHosts at most
if len(r.AggregationHosts) > 0 {
if len(r.AggregationHosts) > rdnsMaxHosts {
r.AggregationHosts = r.AggregationHosts[:rdnsMaxHosts]
}
ep := endpoint.NewEndpointWithTTL(
dnsName,
endpoint.RecordTypeA,
endpoint.TTL(r.TTL),
r.AggregationHosts...,
)
ep.Labels[rdnsOriginalLabel] = r.Text
result = append(result, ep)
}
if r.Text != "" {
ep := endpoint.NewEndpoint(
dnsName,
endpoint.RecordTypeTXT,
r.Text,
)
result = append(result, ep)
}
}
return result, nil
}
// ApplyChanges stores changes back to etcdv3 converting them to Rancher DNS(RDNS) format and aggregating A and TXT records.
func (p RDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
grouped := map[string][]*endpoint.Endpoint{}
for _, ep := range changes.Create {
grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
}
for _, ep := range changes.UpdateNew {
if ep.RecordType == endpoint.RecordTypeA {
// append useless domain records to the changes.Delete
if err := p.filterAndRemoveUseless(ep, changes); err != nil {
return err
}
}
grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
}
for dnsName, group := range grouped {
if !p.domainFilter.Match(dnsName) {
log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", dnsName)
continue
}
var rs []RDNSRecord
for _, ep := range group {
if ep.RecordType == endpoint.RecordTypeTXT {
continue
}
for _, target := range ep.Targets {
rs = append(rs, RDNSRecord{
Host: target,
Text: ep.Labels[rdnsOriginalLabel],
Key: keyFor(ep.DNSName) + "/" + formatKey(target),
TTL: uint32(ep.RecordTTL),
})
}
}
// Add the TXT attribute to the existing A record
for _, ep := range group {
if ep.RecordType != endpoint.RecordTypeTXT {
continue
}
for i, r := range rs {
if strings.Contains(r.Key, keyFor(ep.DNSName)) {
r.Text = ep.Targets[0]
rs[i] = r
}
}
}
for _, r := range rs {
log.Infof("Add/set key %s to Host=%s, Text=%s, TTL=%d", r.Key, r.Host, r.Text, r.TTL)
if !p.dryRun {
err := p.client.Set(r)
if err != nil {
return err
}
}
}
}
for _, ep := range changes.Delete {
key := keyFor(ep.DNSName)
log.Infof("Delete key %s", key)
if !p.dryRun {
err := p.client.Delete(key)
if err != nil {
return err
}
}
}
return nil
}
// filterAndRemoveUseless filter and remove useless records.
func (p *RDNSProvider) filterAndRemoveUseless(ep *endpoint.Endpoint, changes *plan.Changes) error {
rs, err := p.client.Get(keyFor(ep.DNSName))
if err != nil {
return err
}
for _, r := range rs {
exist := false
for _, target := range ep.Targets {
if strings.Contains(r.Key, formatKey(target)) {
exist = true
continue
}
}
if !exist {
ds := strings.Split(strings.TrimPrefix(r.Key, rdnsPrefix+"/"), "/")
keyToDNSNameSplits(ds)
changes.Delete = append(changes.Delete, &endpoint.Endpoint{
DNSName: strings.Join(ds, "."),
})
}
}
return nil
}
// newEtcdv3Client is an etcdv3 client constructor.
func newEtcdv3Client() (RDNSClient, error) {
cfg := &clientv3.Config{}
endpoints := os.Getenv("ETCD_URLS")
ca := os.Getenv("ETCD_CA_FILE")
cert := os.Getenv("ETCD_CERT_FILE")
key := os.Getenv("ETCD_KEY_FILE")
name := os.Getenv("ETCD_TLS_SERVER_NAME")
insecure := os.Getenv("ETCD_TLS_INSECURE")
if endpoints == "" {
endpoints = "http://localhost:2379"
}
urls := strings.Split(endpoints, ",")
scheme := strings.ToLower(urls[0])[0:strings.Index(strings.ToLower(urls[0]), "://")]
switch scheme {
case "http":
cfg.Endpoints = urls
case "https":
var certificates []tls.Certificate
insecure = strings.ToLower(insecure)
isInsecure := insecure == "true" || insecure == "yes" || insecure == "1"
if ca != "" && key == "" || cert == "" && key != "" {
return nil, errors.New("either both cert and key or none must be provided")
}
if cert != "" {
cert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("could not load TLS cert: %w", err)
}
certificates = append(certificates, cert)
}
config := &tls.Config{
Certificates: certificates,
InsecureSkipVerify: isInsecure,
ServerName: name,
}
if ca != "" {
roots := x509.NewCertPool()
pem, err := os.ReadFile(ca)
if err != nil {
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: %w", err)
}
config.RootCAs = roots
}
cfg.Endpoints = urls
cfg.TLS = config
default:
return nil, errors.New("etcdv3 URLs must start with either http:// or https://")
}
c, err := clientv3.New(*cfg)
if err != nil {
return nil, err
}
return etcdv3Client{c, context.Background()}, nil
}
// Get return A records stored in etcdv3 stored anywhere under the given key (recursively).
func (c etcdv3Client) Get(key string) ([]RDNSRecord, error) {
ctx, cancel := context.WithTimeout(c.ctx, rdnsTimeout)
defer cancel()
result, err := c.client.Get(ctx, key, clientv3.WithPrefix())
if err != nil {
return nil, err
}
rs := make([]RDNSRecord, 0)
for _, v := range result.Kvs {
r := new(RDNSRecord)
if err := json.Unmarshal(v.Value, r); err != nil {
return nil, fmt.Errorf("%s: %w", v.Key, err)
}
r.Key = string(v.Key)
rs = append(rs, *r)
}
return rs, nil
}
// List return all records stored in etcdv3 stored anywhere under the given rootDomain (recursively).
func (c etcdv3Client) List(rootDomain string) ([]RDNSRecord, error) {
ctx, cancel := context.WithTimeout(c.ctx, rdnsTimeout)
defer cancel()
path := keyFor(rootDomain)
result, err := c.client.Get(ctx, path, clientv3.WithPrefix())
if err != nil {
return nil, err
}
return c.aggregationRecords(result)
}
// Set persists records data into etcdv3.
func (c etcdv3Client) Set(r RDNSRecord) error {
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
defer cancel()
v, err := json.Marshal(&r)
if err != nil {
return err
}
if r.Text == "" && r.Host == "" {
return nil
}
_, err = c.client.Put(ctx, r.Key, string(v))
if err != nil {
return err
}
return nil
}
// Delete deletes record from etcdv3.
func (c etcdv3Client) Delete(key string) error {
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
defer cancel()
_, err := c.client.Delete(ctx, key, clientv3.WithPrefix())
return err
}
// aggregationRecords will aggregation multi A records under the given path.
// e.g. A: 1_1_1_1.xxx.lb.rancher.cloud & 2_2_2_2.sample.lb.rancher.cloud => sample.lb.rancher.cloud {"aggregation_hosts": ["1.1.1.1", "2.2.2.2"]}
// e.g. TXT: sample.lb.rancher.cloud => sample.lb.rancher.cloud => {"text": "xxx"}
func (c etcdv3Client) aggregationRecords(result *clientv3.GetResponse) ([]RDNSRecord, error) {
var rs []RDNSRecord
bx := make(map[RDNSRecordType]RDNSRecord)
for _, n := range result.Kvs {
r := new(RDNSRecord)
if err := json.Unmarshal(n.Value, r); err != nil {
return nil, fmt.Errorf("%s: %w", n.Key, err)
}
r.Key = string(n.Key)
if r.Host == "" && r.Text == "" {
continue
}
if r.Host != "" {
c := RDNSRecord{
AggregationHosts: r.AggregationHosts,
Host: r.Host,
Text: r.Text,
TTL: r.TTL,
Key: r.Key,
}
n, isContinue := appendRecords(c, endpoint.RecordTypeA, bx, rs)
if isContinue {
continue
}
rs = n
}
if r.Text != "" && r.Host == "" {
c := RDNSRecord{
AggregationHosts: []string{},
Host: r.Host,
Text: r.Text,
TTL: r.TTL,
Key: r.Key,
}
n, isContinue := appendRecords(c, endpoint.RecordTypeTXT, bx, rs)
if isContinue {
continue
}
rs = n
}
}
return rs, nil
}
// appendRecords append record to an array
func appendRecords(r RDNSRecord, dnsType string, bx map[RDNSRecordType]RDNSRecord, rs []RDNSRecord) ([]RDNSRecord, bool) {
dnsName := keyToParentDNSName(r.Key)
bt := RDNSRecordType{Domain: dnsName, Type: dnsType}
if v, ok := bx[bt]; ok {
// skip the TXT records if already added to record list.
// append A record if dnsName already added to record list but not found the value.
// the same record might be found in multiple etcdv3 nodes.
if bt.Type == endpoint.RecordTypeA {
exist := false
for _, h := range v.AggregationHosts {
if h == r.Host {
exist = true
break
}
}
if !exist {
for i, t := range rs {
if !strings.HasPrefix(r.Key, t.Key) {
continue
}
t.Host = ""
t.AggregationHosts = append(t.AggregationHosts, r.Host)
bx[bt] = t
rs[i] = t
}
}
}
return rs, true
}
if bt.Type == endpoint.RecordTypeA {
r.AggregationHosts = append(r.AggregationHosts, r.Host)
}
r.Key = rdnsPrefix + dnsNameToKey(dnsName)
r.Host = ""
bx[bt] = r
rs = append(rs, r)
return rs, false
}
// keyFor used to get a path as etcdv3 preferred.
// e.g. sample.lb.rancher.cloud => /rdnsv3/cloud/rancher/lb/sample
func keyFor(fqdn string) string {
return rdnsPrefix + dnsNameToKey(fqdn)
}
// keyToParentDNSName used to get dnsName.
// e.g. /rdnsv3/cloud/rancher/lb/sample/xxx => xxx.sample.lb.rancher.cloud
// e.g. /rdnsv3/cloud/rancher/lb/sample/xxx/1_1_1_1 => xxx.sample.lb.rancher.cloud
func keyToParentDNSName(key string) string {
ds := strings.Split(strings.TrimPrefix(key, rdnsPrefix+"/"), "/")
keyToDNSNameSplits(ds)
dns := strings.Join(ds, ".")
prefix := strings.Split(dns, ".")[0]
p := `^\d{1,3}_\d{1,3}_\d{1,3}_\d{1,3}$`
m, _ := regexp.MatchString(p, prefix)
if prefix != "" && strings.Contains(prefix, "_") && m {
// 1_1_1_1.xxx.sample.lb.rancher.cloud => xxx.sample.lb.rancher.cloud
return strings.Join(strings.Split(dns, ".")[1:], ".")
}
return dns
}
// dnsNameToKey used to convert domain to a path as etcdv3 preferred.
// e.g. sample.lb.rancher.cloud => /cloud/rancher/lb/sample
func dnsNameToKey(domain string) string {
ss := strings.Split(domain, ".")
last := len(ss) - 1
for i := 0; i < len(ss)/2; i++ {
ss[i], ss[last-i] = ss[last-i], ss[i]
}
return "/" + strings.Join(ss, "/")
}
// keyToDNSNameSplits used to reverse etcdv3 path to domain splits.
// e.g. /cloud/rancher/lb/sample => [sample lb rancher cloud]
func keyToDNSNameSplits(ss []string) {
for i := 0; i < len(ss)/2; i++ {
j := len(ss) - i - 1
ss[i], ss[j] = ss[j], ss[i]
}
}
// formatKey used to format a key as etcdv3 preferred
// e.g. 1.1.1.1 => 1_1_1_1
// e.g. sample.lb.rancher.cloud => sample_lb_rancher_cloud
func formatKey(key string) string {
return strings.Replace(key, ".", "_", -1)
}

View File

@ -1,355 +0,0 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rdns
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"go.etcd.io/etcd/api/v3/mvccpb"
clientv3 "go.etcd.io/etcd/client/v3"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
)
type fakeEtcdv3Client struct {
rs map[string]RDNSRecord
}
func (c fakeEtcdv3Client) Get(key string) ([]RDNSRecord, error) {
rs := make([]RDNSRecord, 0)
for k, v := range c.rs {
if strings.Contains(k, key) {
rs = append(rs, v)
}
}
return rs, nil
}
func (c fakeEtcdv3Client) List(rootDomain string) ([]RDNSRecord, error) {
var result []RDNSRecord
for key, value := range c.rs {
rootPath := rdnsPrefix + dnsNameToKey(rootDomain)
if strings.HasPrefix(key, rootPath) {
value.Key = key
result = append(result, value)
}
}
r := &clientv3.GetResponse{}
for _, v := range result {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
k := &mvccpb.KeyValue{
Key: []byte(v.Key),
Value: b,
}
r.Kvs = append(r.Kvs, k)
}
return c.aggregationRecords(r)
}
func (c fakeEtcdv3Client) Set(r RDNSRecord) error {
c.rs[r.Key] = r
return nil
}
func (c fakeEtcdv3Client) Delete(key string) error {
ks := make([]string, 0)
for k := range c.rs {
if strings.Contains(k, key) {
ks = append(ks, k)
}
}
for _, v := range ks {
delete(c.rs, v)
}
return nil
}
func TestARecordTranslation(t *testing.T) {
expectedTarget1 := "1.2.3.4"
expectedTarget2 := "2.3.4.5"
expectedTargets := []string{expectedTarget1, expectedTarget2}
expectedDNSName := "p1xaf1.lb.rancher.cloud"
expectedRecordType := endpoint.RecordTypeA
client := fakeEtcdv3Client{
map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1/1_2_3_4": {Host: expectedTarget1},
"/rdnsv3/cloud/rancher/lb/p1xaf1/2_3_4_5": {Host: expectedTarget2},
},
}
provider := RDNSProvider{
client: client,
rootDomain: "lb.rancher.cloud",
}
endpoints, err := provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
if len(endpoints) != 1 {
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
}
ep := endpoints[0]
if ep.DNSName != expectedDNSName {
t.Errorf("got unexpected DNS name: %s != %s", ep.DNSName, expectedDNSName)
}
assert.Contains(t, expectedTargets, ep.Targets[0])
assert.Contains(t, expectedTargets, ep.Targets[1])
if ep.RecordType != expectedRecordType {
t.Errorf("got unexpected DNS record type: %s != %s", ep.RecordType, expectedRecordType)
}
}
func TestTXTRecordTranslation(t *testing.T) {
expectedTarget := "string"
expectedDNSName := "p1xaf1.lb.rancher.cloud"
expectedRecordType := endpoint.RecordTypeTXT
client := fakeEtcdv3Client{
map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1": {Text: expectedTarget},
},
}
provider := RDNSProvider{
client: client,
rootDomain: "lb.rancher.cloud",
}
endpoints, err := provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
if len(endpoints) != 1 {
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
}
if endpoints[0].DNSName != expectedDNSName {
t.Errorf("got unexpected DNS name: %s != %s", endpoints[0].DNSName, expectedDNSName)
}
if endpoints[0].Targets[0] != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Targets[0], expectedTarget)
}
if endpoints[0].RecordType != expectedRecordType {
t.Errorf("got unexpected DNS record type: %s != %s", endpoints[0].RecordType, expectedRecordType)
}
}
func TestAWithTXTRecordTranslation(t *testing.T) {
expectedTargets := map[string]string{
endpoint.RecordTypeA: "1.2.3.4",
endpoint.RecordTypeTXT: "string",
}
expectedDNSName := "p1xaf1.lb.rancher.cloud"
client := fakeEtcdv3Client{
map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1": {Host: "1.2.3.4", Text: "string"},
},
}
provider := RDNSProvider{
client: client,
rootDomain: "lb.rancher.cloud",
}
endpoints, err := provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
if len(endpoints) != len(expectedTargets) {
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
}
for _, ep := range endpoints {
expectedTarget := expectedTargets[ep.RecordType]
if expectedTarget == "" {
t.Errorf("got unexpected DNS record type: %s", ep.RecordType)
continue
}
delete(expectedTargets, ep.RecordType)
if ep.DNSName != expectedDNSName {
t.Errorf("got unexpected DNS name: %s != %s", ep.DNSName, expectedDNSName)
}
if ep.Targets[0] != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", ep.Targets[0], expectedTarget)
}
}
}
func TestRDNSApplyChanges(t *testing.T) {
client := fakeEtcdv3Client{
map[string]RDNSRecord{},
}
provider := RDNSProvider{
client: client,
rootDomain: "lb.rancher.cloud",
}
changes1 := &plan.Changes{
Create: []*endpoint.Endpoint{
endpoint.NewEndpoint("p1xaf1.lb.rancher.cloud", endpoint.RecordTypeA, "5.5.5.5", "6.6.6.6"),
endpoint.NewEndpoint("p1xaf1.lb.rancher.cloud", endpoint.RecordTypeTXT, "string1"),
},
}
if err := provider.ApplyChanges(context.Background(), changes1); err != nil {
t.Error(err)
}
expectedRecords1 := map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1/5_5_5_5": {Host: "5.5.5.5", Text: "string1"},
"/rdnsv3/cloud/rancher/lb/p1xaf1/6_6_6_6": {Host: "6.6.6.6", Text: "string1"},
}
client.validateRecords(client.rs, expectedRecords1, t)
changes2 := &plan.Changes{
Create: []*endpoint.Endpoint{
endpoint.NewEndpoint("abx1v1.lb.rancher.cloud", endpoint.RecordTypeA, "7.7.7.7"),
},
UpdateNew: []*endpoint.Endpoint{
endpoint.NewEndpoint("p1xaf1.lb.rancher.cloud", endpoint.RecordTypeA, "8.8.8.8", "9.9.9.9"),
},
}
records, _ := provider.Records(context.Background())
for _, ep := range records {
if ep.DNSName == "p1xaf1.lb.rancher.cloud" {
changes2.UpdateOld = append(changes2.UpdateOld, ep)
}
}
if err := provider.ApplyChanges(context.Background(), changes2); err != nil {
t.Error(err)
}
expectedRecords2 := map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1/8_8_8_8": {Host: "8.8.8.8"},
"/rdnsv3/cloud/rancher/lb/p1xaf1/9_9_9_9": {Host: "9.9.9.9"},
"/rdnsv3/cloud/rancher/lb/abx1v1/7_7_7_7": {Host: "7.7.7.7"},
}
client.validateRecords(client.rs, expectedRecords2, t)
changes3 := &plan.Changes{
Delete: []*endpoint.Endpoint{
endpoint.NewEndpoint("p1xaf1.lb.rancher.cloud", endpoint.RecordTypeA, "8.8.8.8", "9.9.9.9"),
},
}
if err := provider.ApplyChanges(context.Background(), changes3); err != nil {
t.Error(err)
}
expectedRecords3 := map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/abx1v1/7_7_7_7": {Host: "7.7.7.7"},
}
client.validateRecords(client.rs, expectedRecords3, t)
}
func (c fakeEtcdv3Client) aggregationRecords(result *clientv3.GetResponse) ([]RDNSRecord, error) {
var rs []RDNSRecord
bx := make(map[RDNSRecordType]RDNSRecord)
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())
}
r.Key = string(n.Key)
if r.Host == "" && r.Text == "" {
continue
}
if r.Host != "" {
c := RDNSRecord{
AggregationHosts: r.AggregationHosts,
Host: r.Host,
Text: r.Text,
TTL: r.TTL,
Key: r.Key,
}
n, isContinue := appendRecords(c, endpoint.RecordTypeA, bx, rs)
if isContinue {
continue
}
rs = n
}
if r.Text != "" && r.Host == "" {
c := RDNSRecord{
AggregationHosts: []string{},
Host: r.Host,
Text: r.Text,
TTL: r.TTL,
Key: r.Key,
}
n, isContinue := appendRecords(c, endpoint.RecordTypeTXT, bx, rs)
if isContinue {
continue
}
rs = n
}
}
return rs, nil
}
func (c fakeEtcdv3Client) validateRecords(rs, expectedRs map[string]RDNSRecord, t *testing.T) {
if len(rs) != len(expectedRs) {
t.Errorf("wrong number of records: %d != %d", len(rs), len(expectedRs))
}
for key, value := range rs {
if _, ok := expectedRs[key]; !ok {
t.Errorf("unexpected record %s", key)
continue
}
expected := expectedRs[key]
delete(expectedRs, key)
if value.Host != expected.Host {
t.Errorf("wrong host for record %s: %s != %s", key, value.Host, expected.Host)
}
if value.Text != expected.Text {
t.Errorf("wrong text for record %s: %s != %s", key, value.Text, expected.Text)
}
}
}

View File

@ -17,8 +17,7 @@ limitations under the License.
package provider
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/route53"
route53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
)
const (
@ -52,7 +51,7 @@ func (f ZoneTypeFilter) Match(rawZoneType interface{}) bool {
case zoneTypePrivate:
return zoneType == zoneTypePrivate
}
case *route53.HostedZone:
case route53types.HostedZone:
// If the zone has no config we assume it's a public zone since the config's field
// `PrivateZone` is false by default in go.
if zoneType.Config == nil {
@ -61,9 +60,9 @@ func (f ZoneTypeFilter) Match(rawZoneType interface{}) bool {
switch f.zoneType {
case zoneTypePublic:
return !aws.BoolValue(zoneType.Config.PrivateZone)
return !zoneType.Config.PrivateZone
case zoneTypePrivate:
return aws.BoolValue(zoneType.Config.PrivateZone)
return zoneType.Config.PrivateZone
}
}

View File

@ -19,8 +19,7 @@ package provider
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/route53"
route53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
"github.com/stretchr/testify/assert"
)
@ -28,8 +27,8 @@ import (
func TestZoneTypeFilterMatch(t *testing.T) {
publicZoneStr := "public"
privateZoneStr := "private"
publicZoneAWS := &route53.HostedZone{Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}}
privateZoneAWS := &route53.HostedZone{Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}}
publicZoneAWS := route53types.HostedZone{Config: &route53types.HostedZoneConfig{PrivateZone: false}}
privateZoneAWS := route53types.HostedZone{Config: &route53types.HostedZoneConfig{PrivateZone: true}}
for _, tc := range []struct {
zoneTypeFilter string
@ -37,10 +36,10 @@ func TestZoneTypeFilterMatch(t *testing.T) {
zones []interface{}
}{
{
"", true, []interface{}{publicZoneStr, privateZoneStr, &route53.HostedZone{}},
"", true, []interface{}{publicZoneStr, privateZoneStr, route53types.HostedZone{}},
},
{
"public", true, []interface{}{publicZoneStr, publicZoneAWS, &route53.HostedZone{}},
"public", true, []interface{}{publicZoneStr, publicZoneAWS, route53types.HostedZone{}},
},
{
"public", false, []interface{}{privateZoneStr, privateZoneAWS},
@ -49,15 +48,17 @@ func TestZoneTypeFilterMatch(t *testing.T) {
"private", true, []interface{}{privateZoneStr, privateZoneAWS},
},
{
"private", false, []interface{}{publicZoneStr, publicZoneAWS, &route53.HostedZone{}},
"private", false, []interface{}{publicZoneStr, publicZoneAWS, route53types.HostedZone{}},
},
{
"unknown", false, []interface{}{publicZoneStr},
},
} {
zoneTypeFilter := NewZoneTypeFilter(tc.zoneTypeFilter)
for _, zone := range tc.zones {
assert.Equal(t, tc.matches, zoneTypeFilter.Match(zone))
}
t.Run(tc.zoneTypeFilter, func(t *testing.T) {
zoneTypeFilter := NewZoneTypeFilter(tc.zoneTypeFilter)
for _, zone := range tc.zones {
assert.Equal(t, tc.matches, zoneTypeFilter.Match(zone))
}
})
}
}

View File

@ -23,9 +23,10 @@ import (
"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"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/sets"
@ -34,11 +35,11 @@ import (
"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.
// DynamoDBAPI is the subset of the AWS DynamoDB 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)
DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error)
Scan(context.Context, *dynamodb.ScanInput, ...func(*dynamodb.Options)) (*dynamodb.ScanOutput, error)
BatchExecuteStatement(context.Context, *dynamodb.BatchExecuteStatementInput, ...func(*dynamodb.Options)) (*dynamodb.BatchExecuteStatementOutput, error)
}
// DynamoDBRegistry implements registry interface with ownership implemented via an AWS DynamoDB table.
@ -225,7 +226,7 @@ func (im *DynamoDBRegistry) ApplyChanges(ctx context.Context, changes *plan.Chan
Delete: endpoint.FilterEndpointsByOwnerID(im.ownerID, changes.Delete),
}
statements := make([]*dynamodb.BatchStatementRequest, 0, len(filteredChanges.Create)+len(filteredChanges.UpdateNew))
statements := make([]dynamodbtypes.BatchStatementRequest, 0, len(filteredChanges.Create)+len(filteredChanges.UpdateNew))
for _, r := range filteredChanges.Create {
if r.Labels == nil {
r.Labels = make(map[string]string)
@ -286,12 +287,15 @@ func (im *DynamoDBRegistry) ApplyChanges(ctx context.Context, changes *plan.Chan
}
}
err := im.executeStatements(ctx, statements, func(request *dynamodb.BatchStatementRequest, response *dynamodb.BatchStatementResponse) error {
err := im.executeStatements(ctx, statements, func(request dynamodbtypes.BatchStatementRequest, response dynamodbtypes.BatchStatementResponse) error {
var context string
if strings.HasPrefix(*request.Statement, "INSERT") {
if aws.StringValue(response.Error.Code) == "DuplicateItem" {
if response.Error.Code == dynamodbtypes.BatchStatementErrorCodeEnumDuplicateItem {
// We lost a race with a different owner or another owner has an orphaned ownership record.
key := fromDynamoKey(request.Parameters[0])
key, err := fromDynamoKey(request.Parameters[0])
if err != nil {
return err
}
for i, endpoint := range filteredChanges.Create {
if endpoint.Key() == key {
log.Infof("Skipping endpoint %v because owner does not match", endpoint)
@ -303,11 +307,19 @@ func (im *DynamoDBRegistry) ApplyChanges(ctx context.Context, changes *plan.Chan
}
}
}
context = fmt.Sprintf("inserting dynamodb record %q", aws.StringValue(request.Parameters[0].S))
var record string
if err := attributevalue.Unmarshal(request.Parameters[0], &record); err != nil {
return fmt.Errorf("inserting dynamodb record: %w", err)
}
context = fmt.Sprintf("inserting dynamodb record %q", record)
} else {
context = fmt.Sprintf("updating dynamodb record %q", aws.StringValue(request.Parameters[1].S))
var record string
if err := attributevalue.Unmarshal(request.Parameters[1], &record); err != nil {
return fmt.Errorf("inserting dynamodb record: %w", err)
}
context = fmt.Sprintf("updating dynamodb record %q", record)
}
return fmt.Errorf("%s: %s: %s", context, aws.StringValue(response.Error.Code), aws.StringValue(response.Error.Message))
return fmt.Errorf("%s: %s: %s", context, response.Error.Code, *response.Error.Message)
})
if err != nil {
im.recordsCache = nil
@ -326,7 +338,7 @@ func (im *DynamoDBRegistry) ApplyChanges(ctx context.Context, changes *plan.Chan
return err
}
statements = make([]*dynamodb.BatchStatementRequest, 0, len(filteredChanges.Delete)+len(im.orphanedLabels))
statements = make([]dynamodbtypes.BatchStatementRequest, 0, len(filteredChanges.Delete)+len(im.orphanedLabels))
for _, r := range filteredChanges.Delete {
statements = im.appendDelete(statements, r.Key())
}
@ -335,9 +347,13 @@ func (im *DynamoDBRegistry) ApplyChanges(ctx context.Context, changes *plan.Chan
delete(im.labels, r)
}
im.orphanedLabels = nil
return im.executeStatements(ctx, statements, func(request *dynamodb.BatchStatementRequest, response *dynamodb.BatchStatementResponse) error {
return im.executeStatements(ctx, statements, func(request dynamodbtypes.BatchStatementRequest, response dynamodbtypes.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))
record, err := fromDynamoKey(request.Parameters[0])
if err != nil {
return fmt.Errorf("deleting dynamodb record: %w", err)
}
return fmt.Errorf("deleting dynamodb record %q: %s: %s", record, response.Error.Code, *response.Error.Message)
})
}
@ -347,7 +363,7 @@ func (im *DynamoDBRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*
}
func (im *DynamoDBRegistry) readLabels(ctx context.Context) error {
table, err := im.dynamodbAPI.DescribeTableWithContext(ctx, &dynamodb.DescribeTableInput{
table, err := im.dynamodbAPI.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(im.table),
})
if err != nil {
@ -356,8 +372,8 @@ func (im *DynamoDBRegistry) readLabels(ctx context.Context) error {
foundKey := false
for _, def := range table.Table.AttributeDefinitions {
if aws.StringValue(def.AttributeName) == "k" {
if aws.StringValue(def.AttributeType) != "S" {
if *def.AttributeName == "k" {
if def.AttributeType != dynamodbtypes.ScalarAttributeTypeS {
return fmt.Errorf("table %q attribute \"k\" must have type \"S\"", im.table)
}
foundKey = true
@ -367,7 +383,7 @@ func (im *DynamoDBRegistry) readLabels(ctx context.Context) error {
return fmt.Errorf("table %q must have attribute \"k\" of type \"S\"", im.table)
}
if aws.StringValue(table.Table.KeySchema[0].AttributeName) != "k" {
if *table.Table.KeySchema[0].AttributeName != "k" {
return fmt.Errorf("table %q must have hash key \"k\"", im.table)
}
if len(table.Table.KeySchema) > 1 {
@ -375,76 +391,92 @@ func (im *DynamoDBRegistry) readLabels(ctx context.Context) error {
}
labels := map[endpoint.EndpointKey]endpoint.Labels{}
err = im.dynamodbAPI.ScanPagesWithContext(ctx, &dynamodb.ScanInput{
scanPaginator := dynamodb.NewScanPaginator(im.dynamodbAPI, &dynamodb.ScanInput{
TableName: aws.String(im.table),
FilterExpression: aws.String("o = :ownerval"),
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":ownerval": {S: aws.String(im.ownerID)},
ExpressionAttributeValues: map[string]dynamodbtypes.AttributeValue{
":ownerval": &dynamodbtypes.AttributeValueMemberS{Value: 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)
for scanPaginator.HasMorePages() {
output, err := scanPaginator.NextPage(ctx)
if err != nil {
return fmt.Errorf("scanning table %q: %w", im.table, err)
}
for _, item := range output.Items {
k, err := fromDynamoKey(item["k"])
if err != nil {
return fmt.Errorf("querying dynamodb for key: %w", err)
}
l, err := fromDynamoLabels(item["l"], im.ownerID)
if err != nil {
return fmt.Errorf("querying dynamodb for labels: %w", err)
}
labels[k] = l
}
}
im.labels = labels
return nil
}
func fromDynamoKey(key *dynamodb.AttributeValue) endpoint.EndpointKey {
split := strings.SplitN(aws.StringValue(key.S), "#", 3)
func fromDynamoKey(key dynamodbtypes.AttributeValue) (endpoint.EndpointKey, error) {
var ep string
if err := attributevalue.Unmarshal(key, &ep); err != nil {
return endpoint.EndpointKey{}, fmt.Errorf("unmarshalling endpoint key: %w", err)
}
split := strings.SplitN(ep, "#", 3)
return endpoint.EndpointKey{
DNSName: split[0],
RecordType: split[1],
SetIdentifier: split[2],
}, nil
}
func toDynamoKey(key endpoint.EndpointKey) dynamodbtypes.AttributeValue {
return &dynamodbtypes.AttributeValueMemberS{
Value: fmt.Sprintf("%s#%s#%s", key.DNSName, key.RecordType, key.SetIdentifier),
}
}
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 {
func fromDynamoLabels(label dynamodbtypes.AttributeValue, owner string) (endpoint.Labels, error) {
labels := endpoint.NewLabels()
for k, v := range label.M {
labels[k] = aws.StringValue(v.S)
if err := attributevalue.Unmarshal(label, &labels); err != nil {
return endpoint.Labels{}, fmt.Errorf("unmarshalling labels: %w", err)
}
labels[endpoint.OwnerLabelKey] = owner
return labels
return labels, nil
}
func toDynamoLabels(labels endpoint.Labels) *dynamodb.AttributeValue {
labelMap := make(map[string]*dynamodb.AttributeValue, len(labels))
func toDynamoLabels(labels endpoint.Labels) dynamodbtypes.AttributeValue {
labelMap := make(map[string]dynamodbtypes.AttributeValue, len(labels))
for k, v := range labels {
if k == endpoint.OwnerLabelKey {
continue
}
labelMap[k] = &dynamodb.AttributeValue{S: aws.String(v)}
labelMap[k] = &dynamodbtypes.AttributeValueMemberS{Value: v}
}
return &dynamodb.AttributeValue{M: labelMap}
return &dynamodbtypes.AttributeValueMemberM{Value: labelMap}
}
func (im *DynamoDBRegistry) appendInsert(statements []*dynamodb.BatchStatementRequest, key endpoint.EndpointKey, new endpoint.Labels) []*dynamodb.BatchStatementRequest {
return append(statements, &dynamodb.BatchStatementRequest{
Statement: aws.String(fmt.Sprintf("INSERT INTO %q VALUE {'k':?, 'o':?, 'l':?}", im.table)),
Parameters: []*dynamodb.AttributeValue{
func (im *DynamoDBRegistry) appendInsert(statements []dynamodbtypes.BatchStatementRequest, key endpoint.EndpointKey, new endpoint.Labels) []dynamodbtypes.BatchStatementRequest {
return append(statements, dynamodbtypes.BatchStatementRequest{
Statement: aws.String(fmt.Sprintf("INSERT INTO %q VALUE {'k':?, 'o':?, 'l':?}", im.table)),
ConsistentRead: aws.Bool(true),
Parameters: []dynamodbtypes.AttributeValue{
toDynamoKey(key),
{S: aws.String(im.ownerID)},
&dynamodbtypes.AttributeValueMemberS{
Value: im.ownerID,
},
toDynamoLabels(new),
},
ConsistentRead: aws.Bool(true),
})
}
func (im *DynamoDBRegistry) appendUpdate(statements []*dynamodb.BatchStatementRequest, key endpoint.EndpointKey, old endpoint.Labels, new endpoint.Labels) []*dynamodb.BatchStatementRequest {
func (im *DynamoDBRegistry) appendUpdate(statements []dynamodbtypes.BatchStatementRequest, key endpoint.EndpointKey, old endpoint.Labels, new endpoint.Labels) []dynamodbtypes.BatchStatementRequest {
if len(old) == len(new) {
equal := true
for k, v := range old {
@ -458,28 +490,28 @@ func (im *DynamoDBRegistry) appendUpdate(statements []*dynamodb.BatchStatementRe
}
}
return append(statements, &dynamodb.BatchStatementRequest{
return append(statements, dynamodbtypes.BatchStatementRequest{
Statement: aws.String(fmt.Sprintf("UPDATE %q SET \"l\"=? WHERE \"k\"=?", im.table)),
Parameters: []*dynamodb.AttributeValue{
Parameters: []dynamodbtypes.AttributeValue{
toDynamoLabels(new),
toDynamoKey(key),
},
})
}
func (im *DynamoDBRegistry) appendDelete(statements []*dynamodb.BatchStatementRequest, key endpoint.EndpointKey) []*dynamodb.BatchStatementRequest {
return append(statements, &dynamodb.BatchStatementRequest{
func (im *DynamoDBRegistry) appendDelete(statements []dynamodbtypes.BatchStatementRequest, key endpoint.EndpointKey) []dynamodbtypes.BatchStatementRequest {
return append(statements, dynamodbtypes.BatchStatementRequest{
Statement: aws.String(fmt.Sprintf("DELETE FROM %q WHERE \"k\"=? AND \"o\"=?", im.table)),
Parameters: []*dynamodb.AttributeValue{
Parameters: []dynamodbtypes.AttributeValue{
toDynamoKey(key),
{S: aws.String(im.ownerID)},
&dynamodbtypes.AttributeValueMemberS{Value: im.ownerID},
},
})
}
func (im *DynamoDBRegistry) executeStatements(ctx context.Context, statements []*dynamodb.BatchStatementRequest, handleErr func(request *dynamodb.BatchStatementRequest, response *dynamodb.BatchStatementResponse) error) error {
func (im *DynamoDBRegistry) executeStatements(ctx context.Context, statements []dynamodbtypes.BatchStatementRequest, handleErr func(request dynamodbtypes.BatchStatementRequest, response dynamodbtypes.BatchStatementResponse) error) error {
for len(statements) > 0 {
var chunk []*dynamodb.BatchStatementRequest
var chunk []dynamodbtypes.BatchStatementRequest
if len(statements) > int(dynamodbMaxBatchSize) {
chunk = statements[:dynamodbMaxBatchSize]
statements = statements[dynamodbMaxBatchSize:]
@ -488,7 +520,7 @@ func (im *DynamoDBRegistry) executeStatements(ctx context.Context, statements []
statements = nil
}
output, err := im.dynamodbAPI.BatchExecuteStatementWithContext(ctx, &dynamodb.BatchExecuteStatementInput{
output, err := im.dynamodbAPI.BatchExecuteStatement(ctx, &dynamodb.BatchExecuteStatementInput{
Statements: chunk,
})
if err != nil {
@ -501,9 +533,13 @@ func (im *DynamoDBRegistry) executeStatements(ctx context.Context, statements []
op, _, _ := strings.Cut(*request.Statement, " ")
var key string
if op == "UPDATE" {
key = *request.Parameters[1].S
if err := attributevalue.Unmarshal(request.Parameters[1], &key); err != nil {
return err
}
} else {
key = *request.Parameters[0].S
if err := attributevalue.Unmarshal(request.Parameters[0], &key); err != nil {
return err
}
}
log.Infof("%s dynamodb record %q", op, key)
} else {

View File

@ -22,9 +22,10 @@ import (
"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/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/sets"
@ -69,40 +70,40 @@ func TestDynamoDBRegistryNew(t *testing.T) {
func TestDynamoDBRegistryRecordsBadTable(t *testing.T) {
for _, tc := range []struct {
name string
setup func(desc *dynamodb.TableDescription)
setup func(desc *dynamodbtypes.TableDescription)
expected string
}{
{
name: "missing attribute k",
setup: func(desc *dynamodb.TableDescription) {
setup: func(desc *dynamodbtypes.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")
setup: func(desc *dynamodbtypes.TableDescription) {
desc.AttributeDefinitions[0].AttributeType = "SS"
},
expected: "table \"test-table\" attribute \"k\" must have type \"S\"",
},
{
name: "wrong key",
setup: func(desc *dynamodb.TableDescription) {
setup: func(desc *dynamodbtypes.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{
setup: func(desc *dynamodbtypes.TableDescription) {
desc.AttributeDefinitions = append(desc.AttributeDefinitions, dynamodbtypes.AttributeDefinition{
AttributeName: aws.String("o"),
AttributeType: aws.String("S"),
AttributeType: dynamodbtypes.ScalarAttributeTypeS,
})
desc.KeySchema = append(desc.KeySchema, &dynamodb.KeySchemaElement{
desc.KeySchema = append(desc.KeySchema, dynamodbtypes.KeySchemaElement{
AttributeName: aws.String("o"),
KeyType: aws.String("RANGE"),
KeyType: dynamodbtypes.KeyTypeRange,
})
},
expected: "table \"test-table\" must not have a range key",
@ -559,8 +560,8 @@ func TestDynamoDBRegistryApplyChanges(t *testing.T) {
},
},
stubConfig: DynamoDBStubConfig{
ExpectInsertError: map[string]string{
"new.test-zone.example.org#CNAME#set-new": "DuplicateItem",
ExpectInsertError: map[string]dynamodbtypes.BatchStatementErrorCodeEnum{
"new.test-zone.example.org#CNAME#set-new": dynamodbtypes.BatchStatementErrorCodeEnumDuplicateItem,
},
ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
},
@ -620,7 +621,7 @@ func TestDynamoDBRegistryApplyChanges(t *testing.T) {
},
},
stubConfig: DynamoDBStubConfig{
ExpectInsertError: map[string]string{
ExpectInsertError: map[string]dynamodbtypes.BatchStatementErrorCodeEnum{
"new.test-zone.example.org#CNAME#set-new": "TestingError",
},
},
@ -928,7 +929,7 @@ func TestDynamoDBRegistryApplyChanges(t *testing.T) {
},
},
stubConfig: DynamoDBStubConfig{
ExpectUpdateError: map[string]string{
ExpectUpdateError: map[string]dynamodbtypes.BatchStatementErrorCodeEnum{
"bar.test-zone.example.org#CNAME#": "TestingError",
},
},
@ -1073,15 +1074,15 @@ func TestDynamoDBRegistryApplyChanges(t *testing.T) {
type DynamoDBStub struct {
t *testing.T
stubConfig *DynamoDBStubConfig
tableDescription dynamodb.TableDescription
tableDescription dynamodbtypes.TableDescription
changesApplied bool
}
type DynamoDBStubConfig struct {
ExpectInsert map[string]map[string]string
ExpectInsertError map[string]string
ExpectInsertError map[string]dynamodbtypes.BatchStatementErrorCodeEnum
ExpectUpdate map[string]map[string]string
ExpectUpdateError map[string]string
ExpectUpdateError map[string]dynamodbtypes.BatchStatementErrorCodeEnum
ExpectDelete sets.Set[string]
}
@ -1100,17 +1101,17 @@ func newDynamoDBAPIStub(t *testing.T, stubConfig *DynamoDBStubConfig) (*DynamoDB
stub := &DynamoDBStub{
t: t,
stubConfig: stubConfig,
tableDescription: dynamodb.TableDescription{
AttributeDefinitions: []*dynamodb.AttributeDefinition{
tableDescription: dynamodbtypes.TableDescription{
AttributeDefinitions: []dynamodbtypes.AttributeDefinition{
{
AttributeName: aws.String("k"),
AttributeType: aws.String("S"),
AttributeType: dynamodbtypes.ScalarAttributeTypeS,
},
},
KeySchema: []*dynamodb.KeySchemaElement{
KeySchema: []dynamodbtypes.KeySchemaElement{
{
AttributeName: aws.String("k"),
KeyType: aws.String("HASH"),
KeyType: dynamodbtypes.KeyTypeHash,
},
},
},
@ -1131,7 +1132,7 @@ func newDynamoDBAPIStub(t *testing.T, stubConfig *DynamoDBStubConfig) (*DynamoDB
}
}
func (r *DynamoDBStub) DescribeTableWithContext(ctx aws.Context, input *dynamodb.DescribeTableInput, opts ...request.Option) (*dynamodb.DescribeTableOutput, error) {
func (r *DynamoDBStub) DescribeTable(ctx context.Context, input *dynamodb.DescribeTableInput, opts ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) {
assert.NotNil(r.t, ctx)
assert.Equal(r.t, "test-table", *input.TableName, "table name")
return &dynamodb.DescribeTableOutput{
@ -1139,75 +1140,80 @@ func (r *DynamoDBStub) DescribeTableWithContext(ctx aws.Context, input *dynamodb
}, nil
}
func (r *DynamoDBStub) ScanPagesWithContext(ctx aws.Context, input *dynamodb.ScanInput, fn func(*dynamodb.ScanOutput, bool) bool, opts ...request.Option) error {
func (r *DynamoDBStub) Scan(ctx context.Context, input *dynamodb.ScanInput, opts ...func(*dynamodb.Options)) (*dynamodb.ScanOutput, 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)
var owner string
assert.Nil(r.t, attributevalue.Unmarshal(input.ExpressionAttributeValues[":ownerval"], &owner))
assert.Equal(r.t, "test-owner", owner)
assert.Equal(r.t, "k,l", *input.ProjectionExpression)
assert.True(r.t, *input.ConsistentRead)
fn(&dynamodb.ScanOutput{
Items: []map[string]*dynamodb.AttributeValue{
return &dynamodb.ScanOutput{
Items: []map[string]dynamodbtypes.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": &dynamodbtypes.AttributeValueMemberS{Value: "bar.test-zone.example.org#CNAME#"},
"l": &dynamodbtypes.AttributeValueMemberM{Value: map[string]dynamodbtypes.AttributeValue{
endpoint.ResourceLabelKey: &dynamodbtypes.AttributeValueMemberS{Value: "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": &dynamodbtypes.AttributeValueMemberS{Value: "baz.test-zone.example.org#A#set-1"},
"l": &dynamodbtypes.AttributeValueMemberM{Value: map[string]dynamodbtypes.AttributeValue{
endpoint.ResourceLabelKey: &dynamodbtypes.AttributeValueMemberS{Value: "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": &dynamodbtypes.AttributeValueMemberS{Value: "baz.test-zone.example.org#A#set-2"},
"l": &dynamodbtypes.AttributeValueMemberM{Value: map[string]dynamodbtypes.AttributeValue{
endpoint.ResourceLabelKey: &dynamodbtypes.AttributeValueMemberS{Value: "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")},
"k": &dynamodbtypes.AttributeValueMemberS{Value: "quux.test-zone.example.org#A#set-2"},
"l": &dynamodbtypes.AttributeValueMemberM{Value: map[string]dynamodbtypes.AttributeValue{
endpoint.ResourceLabelKey: &dynamodbtypes.AttributeValueMemberS{Value: "ingress/default/quux-ingress"},
}},
},
},
}, true)
return nil
}, nil
}
func (r *DynamoDBStub) BatchExecuteStatementWithContext(context aws.Context, input *dynamodb.BatchExecuteStatementInput, option ...request.Option) (*dynamodb.BatchExecuteStatementOutput, error) {
func (r *DynamoDBStub) BatchExecuteStatement(context context.Context, input *dynamodb.BatchExecuteStatementInput, option ...func(*dynamodb.Options)) (*dynamodb.BatchExecuteStatementOutput, error) {
assert.NotNil(r.t, context)
hasDelete := strings.HasPrefix(strings.ToLower(aws.StringValue(input.Statements[0].Statement)), "delete")
hasDelete := strings.HasPrefix(strings.ToLower(*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))
responses := make([]dynamodbtypes.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) {
assert.Equal(r.t, hasDelete, strings.HasPrefix(strings.ToLower(*statement.Statement), "delete"))
switch *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)
var key string
assert.Nil(r.t, attributevalue.Unmarshal(statement.Parameters[0], &key))
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))
var testOwner string
assert.Nil(r.t, attributevalue.Unmarshal(statement.Parameters[1], &testOwner))
assert.Equal(r.t, "test-owner", testOwner)
responses = append(responses, &dynamodb.BatchStatementResponse{})
responses = append(responses, dynamodbtypes.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)
var key string
assert.Nil(r.t, attributevalue.Unmarshal(statement.Parameters[0], &key))
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),
responses = append(responses, dynamodbtypes.BatchStatementResponse{
Error: &dynamodbtypes.BatchStatementError{
Code: code,
Message: aws.String("testing error"),
},
})
@ -1218,10 +1224,15 @@ func (r *DynamoDBStub) BatchExecuteStatementWithContext(context aws.Context, inp
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))
var testOwner string
assert.Nil(r.t, attributevalue.Unmarshal(statement.Parameters[1], &testOwner))
assert.Equal(r.t, "test-owner", testOwner)
for label, attribute := range statement.Parameters[2].M {
value := aws.StringValue(attribute.S)
var labels map[string]string
err := attributevalue.Unmarshal(statement.Parameters[2], &labels)
assert.Nil(r.t, err)
for label, value := range labels {
expectedValue, found := expectedLabels[label]
assert.True(r.t, found, "insert for key %q has unexpected label %q", key, label)
delete(expectedLabels, label)
@ -1232,17 +1243,18 @@ func (r *DynamoDBStub) BatchExecuteStatementWithContext(context aws.Context, inp
r.t.Errorf("insert for key %q did not get expected label %q", key, label)
}
responses = append(responses, &dynamodb.BatchStatementResponse{})
responses = append(responses, dynamodbtypes.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)
var key string
assert.Nil(r.t, attributevalue.Unmarshal(statement.Parameters[1], &key))
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),
responses = append(responses, dynamodbtypes.BatchStatementResponse{
Error: &dynamodbtypes.BatchStatementError{
Code: code,
Message: aws.String("testing error"),
},
})
@ -1253,8 +1265,10 @@ func (r *DynamoDBStub) BatchExecuteStatementWithContext(context aws.Context, inp
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)
var labels map[string]string
assert.Nil(r.t, attributevalue.Unmarshal(statement.Parameters[0], &labels))
for label, value := range labels {
expectedValue, found := expectedLabels[label]
assert.True(r.t, found, "update for key %q has unexpected label %q", key, label)
delete(expectedLabels, label)
@ -1265,10 +1279,10 @@ func (r *DynamoDBStub) BatchExecuteStatementWithContext(context aws.Context, inp
r.t.Errorf("update for key %q did not get expected label %q", key, label)
}
responses = append(responses, &dynamodb.BatchStatementResponse{})
responses = append(responses, dynamodbtypes.BatchStatementResponse{})
default:
r.t.Errorf("unexpected statement: %s", aws.StringValue(statement.Statement))
r.t.Errorf("unexpected statement: %s", *statement.Statement)
}
}

View File

@ -102,6 +102,11 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
continue
}
if node.Spec.Unschedulable {
log.Debugf("Skipping node %s because it is unschedulable", node.Name)
continue
}
log.Debugf("creating endpoint for node %s", node.Name)
ttl := getTTLFromAnnotations(node.Annotations, fmt.Sprintf("node/%s", node.Name))

View File

@ -101,6 +101,7 @@ func testNodeSourceEndpoints(t *testing.T) {
nodeAddresses []v1.NodeAddress
labels map[string]string
annotations map[string]string
unschedulable bool // default to false
expected []*endpoint.Endpoint
expectError bool
}{
@ -321,6 +322,13 @@ func testNodeSourceEndpoints(t *testing.T) {
{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(10)},
},
},
{
title: "unschedulable node return nothing",
nodeName: "node1",
nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
unschedulable: true,
expected: []*endpoint.Endpoint{},
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
@ -342,6 +350,9 @@ func testNodeSourceEndpoints(t *testing.T) {
Labels: tc.labels,
Annotations: tc.annotations,
},
Spec: v1.NodeSpec{
Unschedulable: tc.unschedulable,
},
Status: v1.NodeStatus{
Addresses: tc.nodeAddresses,
},