Merge branch 'master' of github.com:7onn/external-dns into 7onn/cloudflare-tags

This commit is contained in:
Tom M G 2025-07-21 10:58:20 +02:00
commit b9f9cde0ed
No known key found for this signature in database
GPG Key ID: 90674BFE48C97FB0
144 changed files with 5109 additions and 1530 deletions

View File

@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@v4.2.2
# https://github.com/renovatebot/github-action
- name: self-hosted renovate
uses: renovatebot/github-action@v43.0.1
uses: renovatebot/github-action@v43.0.3
with:
# https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -26,6 +26,7 @@ linters:
- sloglint # Ensure consistent code style when using log/slog
- asciicheck # Checks that all code identifiers does not have non-ASCII symbols in the name
- nilnil # Checks that there is no simultaneous return of nil error and an nil value. ref: https://golangci-lint.run/usage/linters/#nilnil
- nonamedreturns # Checks that functions with named return values do not return named values. https://golangci-lint.run/usage/linters/#nonamedreturns
- cyclop # Checks function and package cyclomatic complexity. https://golangci-lint.run/usage/linters/#cyclop
# tests
@ -40,7 +41,7 @@ linters:
- name: confusing-naming
disabled: true
cyclop: # Lower cyclomatic complexity threshold after the max complexity is lowered
max-complexity: 51
max-complexity: 44
testifylint:
# Enable all checkers (https://github.com/Antonboom/testifylint#checkers).
# Default: false

View File

@ -81,7 +81,11 @@ No new provider will be added to ExternalDNS _in-tree_.
ExternalDNS has introduced a webhook system, which can be used to add a new provider.
See PR #3063 for all the discussions about it.
Known providers using webhooks:
Some known providers using webhooks are the ones in the table below.
**NOTE**: The maintainers of ExternalDNS have not reviewed those providers, use them at your own risk and following the license
and usage recommendations provided by the respective projects. The maintainers of ExternalDNS take no responsibility for any issue or damage
from the usage of any externally developed webhook.
| Provider | Repo |
| --------------------- | -------------------------------------------------------------------- |

4
apis/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- apis

View File

@ -18,12 +18,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [UNRELEASED]
## [v1.18.0] - 2025-07-14
### Changed
- Update RBAC for `Service` source to support `EndpointSlices`. ([#5493](https://github.com/kubernetes-sigs/external-dns/pull/5493)) _@vflaux_
- Update _ExternalDNS_ OCI image version to [v0.18.0](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.18.0). ([#5633](https://github.com/kubernetes-sigs/external-dns/pull/5633)) _@elafarge_
### Fixed
- Fixed the lack of schema support for `create-only` dns policy in helm values ([#5627](https://github.com/kubernetes-sigs/external-dns/pull/5627)) _@coltonhughes_
- Fixed the type of `.extraContainers` from `object` to `list` (array). ([#5564](https://github.com/kubernetes-sigs/external-dns/pull/5564)) _@svengreb_
## [v1.17.0] - 2025-06-04
@ -275,6 +279,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.18.0]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.18.0
[v1.17.0]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.17.0
[v1.16.1]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.16.1
[v1.16.0]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.16.0

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.17.0
appVersion: 0.17.0
version: 1.18.0
appVersion: 0.18.0
keywords:
- kubernetes
- k8s

View File

@ -1,6 +1,6 @@
# external-dns
![Version: 1.17.0](https://img.shields.io/badge/Version-1.17.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.17.0](https://img.shields.io/badge/AppVersion-0.17.0-informational?style=flat-square)
![Version: 1.18.0](https://img.shields.io/badge/Version-1.18.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.18.0](https://img.shields.io/badge/AppVersion-0.18.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.17.0
helm upgrade --install external-dns external-dns/external-dns --version 1.18.0
```
## Providers
@ -109,6 +109,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. |
| gatewayNamespace | string | `nil` | _Gateway API_ gateway namespace to watch. |
| 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. |
@ -127,7 +128,7 @@ If `namespaced` is set to `true`, please ensure that `sources` my only contains
| podAnnotations | object | `{}` | Annotations to add to the `Pod`. |
| podLabels | object | `{}` | Labels to add to the `Pod`. |
| podSecurityContext | object | See _values.yaml_ | [Pod security context](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#podsecuritycontext-v1-core), this supports full customisation. |
| policy | string | `"upsert-only"` | How DNS records are synchronized between sources and providers; available values are `sync` & `upsert-only`. |
| policy | string | `"upsert-only"` | How DNS records are synchronized between sources and providers; available values are `create-only`, `sync`, & `upsert-only`. |
| priorityClassName | string | `nil` | Priority class name for the `Pod`. |
| provider.name | string | `"aws"` | _ExternalDNS_ provider name; for the available providers and how to configure them see [README](https://github.com/kubernetes-sigs/external-dns/blob/master/charts/external-dns/README.md#providers). |
| provider.webhook.args | list | `[]` | Extra arguments to provide for the `webhook` container. |

View File

@ -103,3 +103,12 @@ labelSelector:
matchLabels:
{{ include "external-dns.selectorLabels" . | nindent 4 }}
{{- end }}
{{/*
Check if any Gateway API sources are enabled
*/}}
{{- define "external-dns.hasGatewaySources" -}}
{{- if or (has "gateway-httproute" .Values.sources) (has "gateway-grpcroute" .Values.sources) (has "gateway-tlsroute" .Values.sources) (has "gateway-tcproute" .Values.sources) (has "gateway-udproute" .Values.sources) -}}
true
{{- end -}}
{{- end }}

View File

@ -58,14 +58,18 @@ rules:
resources: ["dnsendpoints/status"]
verbs: ["*"]
{{- end }}
{{- if or (has "gateway-httproute" .Values.sources) (has "gateway-grpcroute" .Values.sources) (has "gateway-tlsroute" .Values.sources) (has "gateway-tcproute" .Values.sources) (has "gateway-udproute" .Values.sources) }}
{{- if include "external-dns.hasGatewaySources" . }}
{{- if or (not .Values.namespaced) (and .Values.namespaced (not .Values.gatewayNamespace)) }}
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
{{- end }}
{{- if not .Values.namespaced }}
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get","watch","list"]
{{- end }}
{{- end }}
{{- if has "gateway-httproute" .Values.sources }}
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["httproutes"]
@ -127,4 +131,31 @@ rules:
{{- with .Values.rbac.additionalPermissions }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- if and .Values.rbac.create .Values.namespaced (include "external-dns.hasGatewaySources" .) }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ template "external-dns.fullname" . }}-namespaces
labels:
{{- include "external-dns.labels" . | nindent 4 }}
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get","watch","list"]
{{- if .Values.gatewayNamespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ template "external-dns.fullname" . }}-gateway
namespace: {{ .Values.gatewayNamespace }}
labels:
{{- include "external-dns.labels" . | nindent 4 }}
rules:
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
{{- end }}
{{- end }}
{{- end }}

View File

@ -13,4 +13,39 @@ subjects:
- kind: ServiceAccount
name: {{ template "external-dns.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- if and .Values.rbac.create .Values.namespaced (include "external-dns.hasGatewaySources" .) }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ template "external-dns.fullname" . }}-namespaces
labels:
{{- include "external-dns.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ template "external-dns.fullname" . }}-namespaces
subjects:
- kind: ServiceAccount
name: {{ template "external-dns.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- if .Values.gatewayNamespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ template "external-dns.fullname" . }}-gateway
namespace: {{ .Values.gatewayNamespace }}
labels:
{{- include "external-dns.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ template "external-dns.fullname" . }}-gateway
subjects:
- kind: ServiceAccount
name: {{ template "external-dns.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -111,6 +111,9 @@ spec:
{{- if .Values.namespaced }}
- --namespace={{ .Release.Namespace }}
{{- end }}
{{- if .Values.gatewayNamespace }}
- --gateway-namespace={{ .Values.gatewayNamespace }}
{{- end }}
{{- range .Values.domainFilters }}
- --domain-filter={{ . }}
{{- end }}

View File

@ -156,3 +156,238 @@ tests:
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["udproutes"]
verbs: ["get","watch","list"]
- it: should create Role instead of ClusterRole when namespaced is true
set:
namespaced: true
sources:
- service
asserts:
- isKind:
of: Role
template: clusterrole.yaml
- equal:
path: metadata.name
value: rbac-external-dns
template: clusterrole.yaml
- it: should create RoleBinding instead of ClusterRoleBinding when namespaced is true
set:
namespaced: true
sources:
- service
asserts:
- isKind:
of: RoleBinding
template: clusterrolebinding.yaml
- equal:
path: metadata.name
value: rbac-external-dns-viewer
template: clusterrolebinding.yaml
- it: should create all required resources when namespaced=true and gatewayNamespace is specified
set:
namespaced: true
gatewayNamespace: gateway-ns
sources:
- gateway-httproute
asserts:
# Should have: main Role + ClusterRole for namespaces + Gateway Role
- hasDocuments:
count: 3
template: clusterrole.yaml
- hasDocuments:
count: 3
template: clusterrolebinding.yaml
# Main role should exist and contain route permissions but NOT gateway permissions
- isKind:
of: Role
documentSelector:
path: metadata.name
value: rbac-external-dns
template: clusterrole.yaml
- contains:
path: rules
content:
apiGroups: ["gateway.networking.k8s.io"]
resources: ["httproutes"]
verbs: ["get","watch","list"]
documentSelector:
path: metadata.name
value: rbac-external-dns
template: clusterrole.yaml
- notContains:
path: rules
content:
apiGroups: ["gateway.networking.k8s.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
documentSelector:
path: metadata.name
value: rbac-external-dns
template: clusterrole.yaml
# ClusterRole for namespaces should exist
- isKind:
of: ClusterRole
documentSelector:
path: metadata.name
value: rbac-external-dns-namespaces
template: clusterrole.yaml
# Gateway role should exist and have gateway permissions only
- isKind:
of: Role
documentSelector:
path: metadata.name
value: rbac-external-dns-gateway
template: clusterrole.yaml
- equal:
path: metadata.namespace
value: gateway-ns
documentSelector:
path: metadata.name
value: rbac-external-dns-gateway
template: clusterrole.yaml
- contains:
path: rules
content:
apiGroups: ["gateway.networking.k8s.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
documentSelector:
path: metadata.name
value: rbac-external-dns-gateway
template: clusterrole.yaml
- it: should create main Role with gateway permissions and ClusterRole for namespaces when namespaced=true and gatewayNamespace is not set
set:
namespaced: true
sources:
- gateway-httproute
asserts:
# Should have: main Role + ClusterRole for namespaces
- hasDocuments:
count: 2
template: clusterrole.yaml
- hasDocuments:
count: 2
template: clusterrolebinding.yaml
# Main Role should exist and contain gateway permissions
- isKind:
of: Role
documentSelector:
path: metadata.name
value: rbac-external-dns
template: clusterrole.yaml
- contains:
path: rules
content:
apiGroups: ["gateway.networking.k8s.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
documentSelector:
path: metadata.name
value: rbac-external-dns
template: clusterrole.yaml
- contains:
path: rules
content:
apiGroups: ["gateway.networking.k8s.io"]
resources: ["httproutes"]
verbs: ["get","watch","list"]
documentSelector:
path: metadata.name
value: rbac-external-dns
template: clusterrole.yaml
# ClusterRole for namespaces should exist
- isKind:
of: ClusterRole
documentSelector:
path: metadata.name
value: rbac-external-dns-namespaces
template: clusterrole.yaml
- contains:
path: rules
content:
apiGroups: [""]
resources: ["namespaces"]
verbs: ["get","watch","list"]
documentSelector:
path: metadata.name
value: rbac-external-dns-namespaces
template: clusterrole.yaml
- it: should create ClusterRole with all permissions when namespaced=false and gateway sources exist
set:
namespaced: false
sources:
- gateway-httproute
asserts:
- hasDocuments:
count: 1
template: clusterrole.yaml
- hasDocuments:
count: 1
template: clusterrolebinding.yaml
- isKind:
of: ClusterRole
template: clusterrole.yaml
- contains:
path: rules
content:
apiGroups: ["gateway.networking.k8s.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
template: clusterrole.yaml
- contains:
path: rules
content:
apiGroups: [""]
resources: ["namespaces"]
verbs: ["get","watch","list"]
template: clusterrole.yaml
- it: should create only ClusterRole when namespaced=false and no gateway sources
set:
namespaced: false
sources:
- service
- ingress
asserts:
- hasDocuments:
count: 1
template: clusterrole.yaml
- hasDocuments:
count: 1
template: clusterrolebinding.yaml
- isKind:
of: ClusterRole
template: clusterrole.yaml
- notContains:
path: rules
content:
apiGroups: ["gateway.networking.k8s.io"]
template: clusterrole.yaml
- it: should create only Role when namespaced=true and no gateway sources
set:
namespaced: true
sources:
- service
- ingress
asserts:
- hasDocuments:
count: 1
template: clusterrole.yaml
- hasDocuments:
count: 1
template: clusterrolebinding.yaml
- isKind:
of: Role
template: clusterrole.yaml
- isKind:
of: RoleBinding
template: clusterrolebinding.yaml

View File

@ -270,12 +270,13 @@
}
},
"policy": {
"description": "How DNS records are synchronized between sources and providers; available values are `sync` \u0026 `upsert-only`.",
"description": "How DNS records are synchronized between sources and providers; available values are `create-only`, `sync`, \u0026 `upsert-only`.",
"default": "upsert-only",
"type": [
"string"
],
"enum": [
"create-only",
"sync",
"upsert-only"
]

View File

@ -205,13 +205,16 @@ triggerLoopOnEvent: false
# -- if `true`, _ExternalDNS_ will run in a namespaced scope (`Role`` and `Rolebinding`` will be namespaced too).
namespaced: false
# -- _Gateway API_ gateway namespace to watch.
gatewayNamespace: # @schema type:[string, null]; default: null
# -- _Kubernetes_ resources to monitor for DNS entries.
sources:
- service
- ingress
# -- How DNS records are synchronized between sources and providers; available values are `sync` & `upsert-only`.
policy: upsert-only # @schema enum:[sync, upsert-only]; type:string; default: "upsert-only"
# -- How DNS records are synchronized between sources and providers; available values are `create-only`, `sync`, & `upsert-only`.
policy: upsert-only # @schema enum:[create-only, sync, upsert-only]; type:string; default: "upsert-only"
# -- Specify the registry for storing ownership and labels.
# Valid values are `txt`, `aws-sd`, `dynamodb` & `noop`.

4
controller/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- controller

View File

@ -37,7 +37,6 @@ import (
var (
registryErrorsTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "registry",
Name: "errors_total",
Help: "Number of Registry errors.",
@ -45,7 +44,6 @@ var (
)
sourceErrorsTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "source",
Name: "errors_total",
Help: "Number of Source errors.",
@ -53,7 +51,6 @@ var (
)
sourceEndpointsTotal = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "source",
Name: "endpoints_total",
Help: "Number of Endpoints in all sources",
@ -61,7 +58,6 @@ var (
)
registryEndpointsTotal = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "registry",
Name: "endpoints_total",
Help: "Number of Endpoints in the registry",
@ -69,7 +65,6 @@ var (
)
lastSyncTimestamp = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "controller",
Name: "last_sync_timestamp_seconds",
Help: "Timestamp of last successful sync with the DNS provider",
@ -77,7 +72,6 @@ var (
)
lastReconcileTimestamp = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "controller",
Name: "last_reconcile_timestamp_seconds",
Help: "Timestamp of last attempted sync with the DNS provider",
@ -85,7 +79,6 @@ var (
)
controllerNoChangesTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "controller",
Name: "no_op_runs_total",
Help: "Number of reconcile loops ending up with no changes on the DNS provider side.",
@ -108,7 +101,6 @@ var (
registryRecords = metrics.NewGaugedVectorOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "registry",
Name: "records",
Help: "Number of registry records partitioned by label name (vector).",
@ -118,7 +110,6 @@ var (
sourceRecords = metrics.NewGaugedVectorOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "source",
Name: "records",
Help: "Number of source records partitioned by label name (vector).",
@ -128,7 +119,6 @@ var (
verifiedRecords = metrics.NewGaugedVectorOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "controller",
Name: "verified_records",
Help: "Number of DNS records that exists both in source and registry (vector).",
@ -138,7 +128,6 @@ var (
consecutiveSoftErrors = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "controller",
Name: "consecutive_soft_errors",
Help: "Number of consecutive soft errors in reconciliation loop.",

View File

@ -33,6 +33,8 @@ import (
log "github.com/sirupsen/logrus"
"k8s.io/klog/v2"
"sigs.k8s.io/external-dns/source/wrappers"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
"sigs.k8s.io/external-dns/pkg/apis/externaldns/validation"
@ -424,11 +426,11 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e
return nil, err
}
// Combine multiple sources into a single, deduplicated source.
combinedSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets, sourceCfg.ForceDefaultTargets))
combinedSource := wrappers.NewDedupSource(wrappers.NewMultiSource(sources, sourceCfg.DefaultTargets, sourceCfg.ForceDefaultTargets))
// Filter targets
targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets)
combinedSource = source.NewNAT64Source(combinedSource, cfg.NAT64Networks)
combinedSource = source.NewTargetFilterSource(combinedSource, targetFilter)
combinedSource = wrappers.NewNAT64Source(combinedSource, cfg.NAT64Networks)
combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter)
return combinedSource, nil
}

View File

@ -66,6 +66,73 @@ Multiple hostnames can be specified through a comma-separated list, e.g.
For `Pods`, uses the `Pod`'s `Status.PodIP`, unless they are `hostNetwork: true` in which case the NodeExternalIP is used for IPv4 and NodeInternalIP for IPv6.
Notes:
- This annotation `overrides` any automatically derived hostnames (e.g., from Ingress.spec.rules[].host).
- Hostnames must match the domain filter set in ExternalDNS (e.g., --domain-filter=example.com).
- This is an alpha annotation — subject to change; newer versions may support alternatives or deprecate it.
- This annotation is helpful for:
- Services or other resources without native hostname fields.
- Explicit overrides or multi-host situations.
- Avoiding reliance on auto-detection or heuristics.
### Use Cases for `external-dns.alpha.kubernetes.io/hostname` annotation
#### Explicit Hostname Mapping for Services
You have a Service (e.g. of type LoadBalancer or ClusterIP) and want to expose it under a custom DNS name:
```yml
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
external-dns.alpha.kubernetes.io/hostname: app.example.com
spec:
type: LoadBalancer
...
```
> ExternalDNS will create a A or CNAME record for app.example.com pointing to the external IP or hostname of the service.
#### Multi-Hostname Records
You can assign multiple hostnames by separating them with commas:
```yml
annotations:
external-dns.alpha.kubernetes.io/hostname: api.example.com,api.internal.example.com
```
> ExternalDNS will create two DNS records for the same service.
#### Static DNS Assignment Without Ingress Rules
When using Ingress, you usually declare hostnames in the spec.rules[].host. But with this annotation, you can manage DNS independently:
```yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
external-dns.alpha.kubernetes.io/hostname: www.example.com
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80
```
> Useful when DNS management is decoupled from routing logic.
## external-dns.alpha.kubernetes.io/ingress-hostname-source
Specifies where to get the domain for an `Ingress` resource.

View File

@ -70,11 +70,11 @@ import (
)
func TestMe(t *testing.T) {
hook := testutils.LogsUnderTestWithLogLeve(log.WarnLevel, t)
hook := testutils.LogsUnderTestWithLogLevel(log.WarnLevel, t)
... function under tests ...
testutils.TestHelperLogContains("example warning message", hook, t)
// provide negative assertion
testuitls.TestHelperLogNotContains("this message should not be shown", hook, t)
testutils.TestHelperLogNotContains("this message should not be shown", hook, t)
}
```

View File

@ -50,7 +50,7 @@
| `--service-type-filter=SERVICE-TYPE-FILTER` | The service types to filter by. Specify multiple times for multiple filters to be applied. (optional, default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName) |
| `--source=source` | The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy) |
| `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) |
| `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group |
| `--[no-]traefik-enable-legacy` | Enable legacy listeners on Resources under the traefik.containo.us API Group |
| `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group |
| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, webhook) |
| `--provider-cache-time=0s` | The time to cache the DNS provider record list requests. |

View File

@ -20,6 +20,7 @@ curl https://localhost:7979/metrics
| Name | Metric Type | Subsystem | Help |
|:---------------------------------|:------------|:------------|:------------------------------------------------------|
| build_info | Gauge | | A metric with a constant '1' value labeled with 'version' and 'revision' of external_dns and the 'go_version', 'os' and the 'arch' used the build. |
| consecutive_soft_errors | Gauge | controller | Number of consecutive soft errors in reconciliation loop. |
| last_reconcile_timestamp_seconds | Gauge | controller | Timestamp of last attempted sync with the DNS provider |
| last_sync_timestamp_seconds | Gauge | controller | Timestamp of last successful sync with the DNS provider |

View File

@ -5,26 +5,26 @@ A source in ExternalDNS defines where DNS records are discovered from within you
ExternalDNS watches the specified sources for hostname information and uses it to create, update, or delete DNS records accordingly. Multiple sources can be configured simultaneously to support diverse environments.
| Source | Resources | annotation-filter | label-filter |
| --------------------------------------- | ----------------------------------------------------------------------------- | ----------------- | ------------ |
| ambassador-host | Host.getambassador.io | Yes | Yes |
|-----------------------------------------|-------------------------------------------------------------------------------|:-----------------:|:------------:|
| ambassador-host | Host.getambassador.io | Yes | Yes |
| connector | | | |
| contour-httpproxy | HttpProxy.projectcontour.io | Yes | |
| contour-httpproxy | HttpProxy.projectcontour.io | Yes | |
| cloudfoundry | | | |
| [crd](crd.md) | DNSEndpoint.externaldns.k8s.io | Yes | Yes |
| [f5-virtualserver](f5-virtualserver.md) | VirtualServer.cis.f5.com | Yes | |
| [gateway-grpcroute](gateway.md) | GRPCRoute.gateway.networking.k8s.io | Yes | Yes |
| [gateway-httproute](gateway.md) | HTTPRoute.gateway.networking.k8s.io | Yes | Yes |
| [gateway-tcproute](gateway.md) | TCPRoute.gateway.networking.k8s.io | Yes | Yes |
| [gateway-tlsroute](gateway.md) | TLSRoute.gateway.networking.k8s.io | Yes | Yes |
| [gateway-udproute](gateway.md) | UDPRoute.gateway.networking.k8s.io | Yes | Yes |
| [crd](crd.md) | DNSEndpoint.externaldns.k8s.io | Yes | Yes |
| [f5-virtualserver](f5-virtualserver.md) | VirtualServer.cis.f5.com | Yes | |
| [gateway-grpcroute](gateway.md) | GRPCRoute.gateway.networking.k8s.io | Yes | Yes |
| [gateway-httproute](gateway.md) | HTTPRoute.gateway.networking.k8s.io | Yes | Yes |
| [gateway-tcproute](gateway.md) | TCPRoute.gateway.networking.k8s.io | Yes | Yes |
| [gateway-tlsroute](gateway.md) | TLSRoute.gateway.networking.k8s.io | Yes | Yes |
| [gateway-udproute](gateway.md) | UDPRoute.gateway.networking.k8s.io | Yes | Yes |
| [gloo-proxy](gloo-proxy.md) | Proxy.gloo.solo.io | | |
| [ingress](ingress.md) | Ingress.networking.k8s.io | Yes | Yes |
| [istio-gateway](istio.md) | Gateway.networking.istio.io | Yes | |
| [istio-virtualservice](istio.md) | VirtualService.networking.istio.io | Yes | |
| [kong-tcpingress](kong.md) | TCPIngress.configuration.konghq.com | Yes | |
| [node](nodes.md) | Node | Yes | Yes |
| [openshift-route](openshift.md) | Route.route.openshift.io | Yes | Yes |
| [pod](pod.md) | Pod | | |
| [service](service.md) | Service | Yes | Yes |
| skipper-routegroup | RouteGroup.zalando.org | Yes | |
| [traefik-proxy](traefik-proxy.md) | IngressRoute.traefik.io IngressRouteTCP.traefik.io IngressRouteUDP.traefik.io | Yes | |
| [ingress](ingress.md) | Ingress.networking.k8s.io | Yes | Yes |
| [istio-gateway](istio.md) | Gateway.networking.istio.io | Yes | |
| [istio-virtualservice](istio.md) | VirtualService.networking.istio.io | Yes | |
| [kong-tcpingress](kong.md) | TCPIngress.configuration.konghq.com | Yes | |
| [node](nodes.md) | Node | Yes | Yes |
| [openshift-route](openshift.md) | Route.route.openshift.io | Yes | Yes |
| [pod](pod.md) | Pod | Yes | Yes |
| [service](service.md) | Service | Yes | Yes |
| skipper-routegroup | RouteGroup.zalando.org | Yes | |
| [traefik-proxy](traefik-proxy.md) | IngressRoute.traefik.io IngressRouteTCP.traefik.io IngressRouteUDP.traefik.io | Yes | |

View File

@ -5,9 +5,13 @@ It is meant to supplement the other provider-specific setup tutorials.
**Note:** Using the Istio Gateway source requires Istio >=1.0.0.
* Manifest (for clusters without RBAC enabled)
* Manifest (for clusters with RBAC enabled)
* Update existing ExternalDNS Deployment
**Note:** Currently supported versions are `1.25` and `1.26` with `v1beta1` stored version.
- [Support status of Istio releases](https://istio.io/latest/docs/releases/supported-releases/)
- Manifest (for clusters without RBAC enabled)
- Manifest (for clusters with RBAC enabled)
- Update existing ExternalDNS Deployment
## Manifest (for clusters without RBAC enabled)
@ -119,9 +123,9 @@ spec:
## Update existing ExternalDNS Deployment
* For clusters with running `external-dns`, you can just update the deployment.
* With access to the `kube-system` namespace, update the existing `external-dns` deployment.
* Add a parameter to the arguments of the container to create dns entries with `--source=istio-gateway`.
- For clusters with running `external-dns`, you can just update the deployment.
- With access to the `kube-system` namespace, update the existing `external-dns` deployment.
- Add a parameter to the arguments of the container to create dns entries with `--source=istio-gateway`.
Execute the following command or update the argument.
@ -148,13 +152,13 @@ The following are relevant snippets from that tutorial.
With automatic sidecar injection:
```bash
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.25/samples/httpbin/httpbin.yaml
```
Otherwise:
```bash
kubectl apply -f <(istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml)
kubectl apply -f <(istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.25/samples/httpbin/httpbin.yaml)
```
### Using a Gateway as a source
@ -320,13 +324,13 @@ EOF
## Debug ExternalDNS
* Look for the deployment pod to see the status
- Look for the deployment pod to see the status
```console$ kubectl get pods | grep external-dns
external-dns-6b84999479-4knv9 1/1 Running 0 3h29m
```
* Watch for the logs as follows
- Watch for the logs as follows
```console
kubectl logs -f external-dns-6b84999479-4knv9
@ -336,7 +340,7 @@ At this point, you can `create` or `update` any `Istio Gateway` object with `hos
> **ATTENTION**: Make sure to specify those whose account is related to the DNS record.
* Successful executions will print the following
- Successful executions will print the following
```console
time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.com A"
@ -345,7 +349,7 @@ time="2020-01-17T06:08:08Z" level=info msg="2 record(s) in zone example.com. wer
time="2020-01-17T06:09:08Z" level=info msg="All records are already up to date, there are no changes for the matching hosted zones"
```
* If there's any problem around `clusterrole`, you would see the errors showing wrong permissions:
- If there's any problem around `clusterrole`, you would see the errors showing wrong permissions:
```console
source \"gateways\" in API group \"networking.istio.io\" at the cluster scope"

View File

@ -82,15 +82,11 @@ kubectl delete -f externaldns.yaml
| Flag | Description |
|--------------------------|----------------------------------------------------------|
| --traefik-disable-legacy | Disable listeners on Resources under traefik.containo.us |
| --traefik-enable-legacy | Enable listeners on Resources under traefik.containo.us |
| --traefik-disable-new | Disable listeners on Resources under traefik.io |
### Disabling Resource Listeners
### Resource Listeners
Traefik has deprecated the legacy API group, `traefik.containo.us`, in favor of `traefik.io`. By default the `traefik-proxy` source will listen for resources under both API groups; however, this may cause timeouts with the following message
Traefik has deprecated the legacy API group, _traefik.containo.us_, in favor of _traefik.io_. By default the `traefik-proxy` source listen for resources under traefik.io API groups.
```sh
FATA[0060] failed to sync traefik.io/v1alpha1, Resource=ingressroutes: context deadline exceeded
```
In this case you can disable one or the other API groups with `--traefik-disable-new` or `--traefik-disable-legacy`
If needed, you can enable legacy listener with `--traefik-enable-legacy` and also disable new listener with `--traefik-disable-new`.

View File

@ -473,6 +473,8 @@ env:
Finally, install the ExternalDNS chart with Helm using the configuration specified in your values.yaml file:
```shell
helm repo add --force-update external-dns https://kubernetes-sigs.github.io/external-dns/
helm upgrade --install external-dns external-dns/external-dns --values values.yaml
```
@ -894,6 +896,11 @@ For any given DNS name, only **one** of the following routing policies can be us
- `external-dns.alpha.kubernetes.io/aws-geolocation-continent-code`
- `external-dns.alpha.kubernetes.io/aws-geolocation-country-code`
- `external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code`
- Geoproximity routing:
- `external-dns.alpha.kubernetes.io/aws-geoproximity-region`
- `external-dns.alpha.kubernetes.io/aws-geoproximity-local-zone-group`
- `external-dns.alpha.kubernetes.io/aws-geoproximity-coordinates`
- `external-dns.alpha.kubernetes.io/aws-geoproximity-bias`
- Multi-value answer:`external-dns.alpha.kubernetes.io/aws-multi-value-answer`
### Associating DNS records with healthchecks

View File

@ -169,7 +169,7 @@ Very important here, is to set the `hostPort`(only works if the PodSecurityPolic
Now we need to define a headless service to use to expose the Kafka pods. There are generally two approaches to use expose the nodeport of a Headless service:
1. Add `--fqdn-template={{name}}.example.org`
1. Add `--fqdn-template={{ .Name }}.example.org`
2. Use a full annotation
If you go with #1, you just need to define the headless service, here is an example of the case #2:
@ -190,22 +190,24 @@ spec:
component: kafka
```
This will create 3 dns records:
This will create 4 dns records:
```sh
kafka-0.example.org
kafka-1.example.org
kafka-2.example.org
kafka-0.example.org IP-0
kafka-1.example.org IP-1
kafka-2.example.org IP-2
example.org IP-0,IP-1,IP-2
```
If you set `--fqdn-template={{name}}.example.org` you can omit the annotation.
Generally it is a better approach to use `--fqdn-template={{name}}.example.org`, because then
you would get the service name inside the generated A records:
> !Notice rood domain with records `example.org`
If you set `--fqdn-template={{ .Name }}.example.org` you can omit the annotation.
```sh
kafka-0.ksvc.example.org
kafka-1.ksvc.example.org
kafka-2.ksvc.example.org
kafka-0.ksvc.example.org IP-0
kafka-1.ksvc.example.org IP-1
kafka-2.ksvc.example.org IP-2
ksvc.example.org IP-0,IP-1,IP-2
```
#### Using pods' HostIPs as targets

View File

@ -68,7 +68,7 @@ func EncryptText(text string, aesKey []byte, nonceEncoded []byte) (string, error
// DecryptText decrypt gziped data using a supplied AES encryption key ang ungzip it
// in case of decryption failed, will return original input and decryption error
func DecryptText(text string, aesKey []byte) (decryptResult string, encryptNonce string, err error) {
func DecryptText(text string, aesKey []byte) (string, string, error) {
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", "", err
@ -100,7 +100,7 @@ func DecryptText(text string, aesKey []byte) (decryptResult string, encryptNonce
}
// decompressData gzip compressed data
func decompressData(data []byte) (resData []byte, err error) {
func decompressData(data []byte) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, err
@ -115,7 +115,7 @@ func decompressData(data []byte) (resData []byte, err error) {
}
// compressData by gzip, for minify data stored in registry
func compressData(data []byte) (compressedData []byte, err error) {
func compressData(data []byte) ([]byte, error) {
var b bytes.Buffer
gz, err := gzip.NewWriterLevel(&b, gzip.BestCompression)
if err != nil {

View File

@ -87,6 +87,6 @@ func TestGenerateNonceError(t *testing.T) {
type faultyReader struct{}
func (f *faultyReader) Read(p []byte) (n int, err error) {
func (f *faultyReader) Read(p []byte) (int, error) {
return 0, io.ErrUnexpectedEOF
}

View File

@ -949,3 +949,9 @@ func TestDomainFilterNormalizeDomain(t *testing.T) {
assert.Equal(t, r.expect, gotName)
}
}
func TestMatchTargetFilterReturnsProperEmptyVal(t *testing.T) {
var emptyFilters []string
assert.True(t, matchFilter(emptyFilters, "sometarget.com", true))
assert.False(t, matchFilter(emptyFilters, "sometarget.com", false))
}

View File

@ -19,6 +19,7 @@ package endpoint
import (
"fmt"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
@ -353,7 +354,21 @@ func (e *Endpoint) String() string {
return fmt.Sprintf("%s %d IN %s %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.SetIdentifier, e.Targets, e.ProviderSpecific)
}
// Apply filter to slice of endpoints and return new filtered slice that includes
// UniqueOrderedTargets removes duplicate targets from the Endpoint and sorts them in lexicographical order.
func (e *Endpoint) UniqueOrderedTargets() {
result := make([]string, 0, len(e.Targets))
existing := make(map[string]bool)
for _, target := range e.Targets {
if _, ok := existing[target]; !ok {
result = append(result, target)
existing[target] = true
}
}
slices.Sort(result)
e.Targets = result
}
// FilterEndpointsByOwnerID Apply filter to slice of endpoints and return new filtered slice that includes
// only endpoints that match.
func FilterEndpointsByOwnerID(ownerID string, eps []*Endpoint) []*Endpoint {
filtered := []*Endpoint{}

View File

@ -925,3 +925,46 @@ func TestCheckEndpoint(t *testing.T) {
})
}
}
func TestEndpoint_UniqueOrderedTargets(t *testing.T) {
tests := []struct {
name string
targets []string
expected Targets
want bool
}{
{
name: "no duplicates",
targets: []string{"b.example.com", "a.example.com"},
expected: Targets{"a.example.com", "b.example.com"},
},
{
name: "with duplicates",
targets: []string{"a.example.com", "b.example.com", "a.example.com"},
expected: Targets{"a.example.com", "b.example.com"},
},
{
name: "already sorted",
targets: []string{"a.example.com", "b.example.com"},
expected: Targets{"a.example.com", "b.example.com"},
},
{
name: "all duplicates",
targets: []string{"a.example.com", "a.example.com", "a.example.com"},
expected: Targets{"a.example.com"},
},
{
name: "empty",
targets: []string{},
expected: Targets{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ep := &Endpoint{Targets: tt.targets}
ep.UniqueOrderedTargets()
assert.Equal(t, tt.expected, ep.Targets)
})
}
}

View File

@ -26,12 +26,13 @@ import (
// TargetFilterInterface defines the interface to select matching targets for a specific provider or runtime
type TargetFilterInterface interface {
Match(target string) bool
IsEnabled() bool
}
// TargetNetFilter holds a lists of valid target names
type TargetNetFilter struct {
// FilterNets define what targets to match
FilterNets []*net.IPNet
// filterNets define what targets to match
filterNets []*net.IPNet
// excludeNets define what targets not to match
excludeNets []*net.IPNet
}
@ -42,11 +43,9 @@ func prepareTargetFilters(filters []string) []*net.IPNet {
for _, filter := range filters {
filter = strings.TrimSpace(filter)
_, filterNet, err := net.ParseCIDR(filter)
if err != nil {
log.Errorf("Invalid target net filter: %s", filter)
continue
}
@ -57,12 +56,17 @@ func prepareTargetFilters(filters []string) []*net.IPNet {
// NewTargetNetFilterWithExclusions returns a new TargetNetFilter, given a list of matches and exclusions
func NewTargetNetFilterWithExclusions(targetFilterNets []string, excludeNets []string) TargetNetFilter {
return TargetNetFilter{FilterNets: prepareTargetFilters(targetFilterNets), excludeNets: prepareTargetFilters(excludeNets)}
return TargetNetFilter{filterNets: prepareTargetFilters(targetFilterNets), excludeNets: prepareTargetFilters(excludeNets)}
}
// Match checks whether a target can be found in the TargetNetFilter.
func (tf TargetNetFilter) Match(target string) bool {
return matchTargetNetFilter(tf.FilterNets, target, true) && !matchTargetNetFilter(tf.excludeNets, target, false)
return matchTargetNetFilter(tf.filterNets, target, true) && !matchTargetNetFilter(tf.excludeNets, target, false)
}
// IsEnabled returns true if any filters or exclusions are set.
func (tf TargetNetFilter) IsEnabled() bool {
return len(tf.filterNets) > 0 || len(tf.excludeNets) > 0
}
// matchTargetNetFilter determines if any `filters` match `target`.
@ -73,9 +77,9 @@ func matchTargetNetFilter(filters []*net.IPNet, target string, emptyval bool) bo
return emptyval
}
for _, filter := range filters {
ip := net.ParseIP(target)
ip := net.ParseIP(target)
for _, filter := range filters {
if filter.Contains(ip) {
return true
}

View File

@ -66,6 +66,18 @@ var targetFilterTests = []targetFilterTest{
[]string{"10.1.2.3"},
false,
},
{
[]string{},
[]string{"10.0.0.0/8"},
[]string{"49.13.41.161"},
true,
},
{
[]string{},
[]string{"10.0.0.0/8"},
[]string{"10.0.1.101"},
false,
},
}
func TestTargetFilterWithExclusions(t *testing.T) {
@ -89,8 +101,21 @@ func TestTargetFilterMatchWithEmptyFilter(t *testing.T) {
}
}
func TestMatchTargetFilterReturnsProperEmptyVal(t *testing.T) {
emptyFilters := []string{}
assert.True(t, matchFilter(emptyFilters, "sometarget.com", true))
assert.False(t, matchFilter(emptyFilters, "sometarget.com", false))
func TestTargetNetFilter_IsEnabled(t *testing.T) {
tests := []struct {
name string
filterNets []string
excludeNets []string
want bool
}{
{"both empty", []string{}, []string{}, false},
{"filterNets non-empty", []string{"10.0.0.0/8"}, []string{}, true},
{"excludeNets non-empty", []string{}, []string{"10.0.0.0/8"}, true},
{"both non-empty", []string{"10.0.0.0/8"}, []string{"192.168.0.0/16"}, true},
}
for _, tt := range tests {
tf := NewTargetNetFilterWithExclusions(tt.filterNets, tt.excludeNets)
assert.Equal(t, tt.want, tf.IsEnabled())
}
}

88
go.mod
View File

@ -4,31 +4,32 @@ go 1.24.2
require (
cloud.google.com/go/compute/metadata v0.7.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
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.3.0
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.0
github.com/Yamashou/gqlgenc v0.32.1
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.1
github.com/Yamashou/gqlgenc v0.33.0
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.107
github.com/aws/aws-sdk-go-v2 v1.36.5
github.com/aws/aws-sdk-go-v2/config v1.29.17
github.com/aws/aws-sdk-go-v2/credentials v1.17.70
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4
github.com/aws/aws-sdk-go-v2/service/route53 v1.52.2
github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0
github.com/aws/aws-sdk-go-v2 v1.36.6
github.com/aws/aws-sdk-go-v2/config v1.29.18
github.com/aws/aws-sdk-go-v2/credentials v1.17.71
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.6
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.1
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1
github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.8
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1
github.com/bodgit/tsig v1.2.2
github.com/cenkalti/backoff/v5 v5.0.2
github.com/civo/civogo v0.6.1
github.com/cloudflare/cloudflare-go v0.115.0
github.com/cloudflare/cloudflare-go/v4 v4.6.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.155.0
github.com/digitalocean/godo v1.159.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
@ -37,38 +38,38 @@ require (
github.com/goccy/go-yaml v1.18.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/linki/instrumented_http v0.3.0
github.com/linode/linodego v1.52.1
github.com/linode/linodego v1.53.0
github.com/maxatome/go-testdeep v1.14.0
github.com/miekg/dns v1.1.66
github.com/miekg/dns v1.1.67
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.94.0
github.com/oracle/oci-go-sdk/v65 v65.95.2
github.com/ovh/go-ovh v1.9.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pluralsh/gqlclient v1.12.2
github.com/projectcontour/contour v1.32.0
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_model v0.6.2
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33
github.com/prometheus/common v0.65.0
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
github.com/transip/gotransip/v6 v6.26.0
go.etcd.io/etcd/api/v3 v3.6.1
go.etcd.io/etcd/client/v3 v3.6.1
go.etcd.io/etcd/api/v3 v3.6.2
go.etcd.io/etcd/client/v3 v3.6.2
go.uber.org/ratelimit v0.3.1
golang.org/x/net v0.41.0
golang.org/x/net v0.42.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.15.0
golang.org/x/text v0.26.0
golang.org/x/sync v0.16.0
golang.org/x/text v0.27.0
golang.org/x/time v0.12.0
google.golang.org/api v0.239.0
google.golang.org/api v0.242.0
gopkg.in/ns1/ns1-go.v2 v2.14.4
istio.io/api v1.26.2
istio.io/client-go v1.26.2
k8s.io/api v0.33.2
k8s.io/apimachinery v0.33.2
k8s.io/client-go v0.33.2
k8s.io/api v0.33.3
k8s.io/apimachinery v0.33.3
k8s.io/client-go v0.33.3
k8s.io/klog/v2 v2.130.1
sigs.k8s.io/controller-runtime v0.21.0
sigs.k8s.io/gateway-api v1.3.0
@ -78,22 +79,22 @@ require (
cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect
github.com/99designs/gqlgen v0.17.71 // indirect
github.com/99designs/gqlgen v0.17.73 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@ -153,7 +154,6 @@ require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/schollz/progressbar/v3 v3.8.6 // indirect
@ -162,11 +162,15 @@ require (
github.com/sosodev/duration v1.3.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/vektah/gqlparser/v2 v2.5.25 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.26 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.1 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
@ -175,11 +179,11 @@ require (
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect

182
go.sum
View File

@ -12,12 +12,12 @@ code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTg
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.71 h1:6JdwweHlSMWGY+6VWY5ey0tO+sF8LckbUV0NmdOQi04=
github.com/99designs/gqlgen v0.17.71/go.mod h1:3yz6ekwCAjC90zaFvPoy+mEjaKiyYJjhtCnwn1seoxE=
github.com/99designs/gqlgen v0.17.73 h1:A3Ki+rHWqKbAOlg5fxiZBnz6OjW3nwupDHEG15gEsrg=
github.com/99designs/gqlgen v0.17.73/go.mod h1:2RyGWjy2k7W9jxrs8MOQthXGkD3L3oGr0jXW3Pu8lGg=
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.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
@ -48,8 +48,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83
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.20.0 h1:WqsoU+5aA9kDypiBzWbLSkESQUA3NDLNvkjTFzipX3I=
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.0/go.mod h1:/lGdCgv0e1qrS4ithe2qTU6q23IT8kqZhMlFBQmuNi0=
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.1 h1:O4a7qJCbH2bQPzsk7NNIm9/2orkYEH7g4Uerdp0gzps=
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.1/go.mod h1:/lGdCgv0e1qrS4ithe2qTU6q23IT8kqZhMlFBQmuNi0=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
@ -76,8 +76,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.32.1 h1:EHs9//xQxXlyltkSFXM+fhO2rTXcWNw6FPKRJ6t+iQQ=
github.com/Yamashou/gqlgenc v0.32.1/go.mod h1:o5SxKt9d3+oUZ2i0V3CW8lHFyunfLR+KcKHubS4zf5E=
github.com/Yamashou/gqlgenc v0.33.0 h1:0fxTnNE8/JVmFpfo7reA5pEgOcr7VjNc+/nEpVhNjfc=
github.com/Yamashou/gqlgenc v0.33.0/go.mod h1:MZGXx/nALyxcehcFeLGmYiNsJ+hQTOGJzNYCGNX4rL0=
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=
@ -114,42 +114,42 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
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-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3 h1:xQYRnbQ+ypDMCLiFlLw5cF7Xd6K+oaL7jco2zwIMqTs=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3/go.mod h1:X7RC8FFkx0bjNJRBddd3xdoDaDmNLSxICFdIdJ7asqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
github.com/aws/aws-sdk-go-v2 v1.36.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQGdU=
github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
github.com/aws/aws-sdk-go-v2/config v1.29.18 h1:x4T1GRPnqKV8HMJOMtNktbpQMl3bIsfx8KbqmveUO2I=
github.com/aws/aws-sdk-go-v2/config v1.29.18/go.mod h1:bvz8oXugIsH8K7HLhBv06vDqnFv3NsGDt2Znpk7zmOU=
github.com/aws/aws-sdk-go-v2/credentials v1.17.71 h1:r2w4mQWnrTMJjOyIsZtGp3R3XGY3nqHn8C26C2lQWgA=
github.com/aws/aws-sdk-go-v2/credentials v1.17.71/go.mod h1:E7VF3acIup4GB5ckzbKFrCK0vTvEQxOxgdq4U3vcMCY=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.6 h1:gBfrCR6IwAhmx+oCf9i9FJo1+Cxx5f0In+PaYQbkqbU=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.6/go.mod h1:zAO6MqUum/2yfE/Ig1LPPtzCBudQtrGBaz1gcNzgAoY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 h1:D9ixiWSG4lyUBL2DDNK924Px9V/NBVpML90MHqyTADY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33/go.mod h1:caS/m4DI+cij2paz3rtProRBI4s/+TCiWoaWZuQ9010=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4 h1:Rv6o9v2AfdEIKoAa7pQpJ5ch9ji2HevFUvGY6ufawlI=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 h1:QHaS/SHXfyNycuu4GiWb+AfW5T3bput6X5E3Ai/Q31M=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.1 h1:UoEWyfuQ/yNOuDENk5nn+AgNCH2Y5yzQEv6YbTyhIV8=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.1/go.mod h1:K1I47BjiTRX00pBxfJLYK80QFRcf6blev2wbjgC5Cyc=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.1 h1:WD2RDt93+IgNvlxEKkx/b3BQrpw5G/YpDHvGXweO5wE=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.1/go.mod h1:8ZWruWnVWtJwjSHEtMWFcI1W6L6PD6i+uKCJ9EiJBbE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
github.com/aws/aws-sdk-go-v2/service/route53 v1.52.2 h1:dXHWVVPx2W2fq2PTugj8QXpJ0YTRAGx0KLPKhMBmcsY=
github.com/aws/aws-sdk-go-v2/service/route53 v1.52.2/go.mod h1:wi1naoiPnCQG3cyjsivwPON1ZmQt/EJGxFqXzubBTAw=
github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7 h1:1eaP4/444jrv04HhJdwTHtgnyxWgxwdLjSYBGq+oMB4=
github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7/go.mod h1:czoZQabc2chvmV/ak4oGSNR9CbcUw2bef3tatmwtoIA=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.18 h1:QnGWwpTiazs1Y74RwA8VUfAtKuJQbnQ98DBFnSywj0s=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.18/go.mod h1:gWOI6Vb0Bbmsi0Ejvtt3RkwKpdoa/SOYTVUlzqYPRLc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 h1:vvbXsA2TVO80/KT7ZqCbx934dt6PY+vQ8hZpUZ/cpYg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18/go.mod h1:m2JJHledjBGNMsLOF1g9gbAxprzq3KjC8e4lxtn+eWg=
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 h1:R3nSX1hguRy6MnknHiepSvqnnL8ansFwK2hidPesAYU=
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1/go.mod h1:fmSiB4OAghn85lQgk7XN9l9bpFg5Bm1v3HuaXKytPEw=
github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.8 h1:PPQUm3zG6XzctspDTWC6vO3DvP/RZ+04RB11r98yb6E=
github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.8/go.mod h1:C1n2zhotURaNj/BNgdPdhXh/i6V53rI3RmVEaNDakSM=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 h1:rGtWqkQbPk7Bkwuv3NzpE/scwwL9sC1Ul3tn9x83DUI=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6/go.mod h1:u4ku9OLv4TO4bCPdxf4fA1upaMaJmP9ZijGk3AAOC6Q=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 h1:OV/pxyXh+eMA0TExHEC4jyWdumLxNbzz1P0zJoezkJc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4/go.mod h1:8Mm5VGYwtm+r305FfPSuc+aFkrypeylGYhFim6XEPoc=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 h1:aUrLQwJfZtwv3/ZNG2xRtEen+NqI3iesuacjP51Mv1s=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1/go.mod h1:3wFBZKoWnX3r+Sm7in79i54fBmNfwhdNdQuscCw7QIk=
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
@ -186,6 +186,8 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
github.com/cloudflare/cloudflare-go/v4 v4.6.0 h1:ZaWwXjHFR5NoY8UEf4QFY0g3KTi72kqqEXpajV610/o=
github.com/cloudflare/cloudflare-go/v4 v4.6.0/go.mod h1:XcYpLe7Mf6FN87kXzEWVnJ6z+vskW/k6eUqgqfhFE9k=
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=
@ -250,8 +252,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
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.155.0 h1:+Y09Nz1TTXFSq5fdgSpqvCKfEpN35FU9WIOMuEuCwgg=
github.com/digitalocean/godo v1.155.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM=
github.com/digitalocean/godo v1.159.0 h1:GQLfVueriDHYpwLzDcbydHs6nBvQBO8/r8r9imPC434=
github.com/digitalocean/godo v1.159.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM=
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=
@ -674,10 +676,8 @@ github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
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.52.1 h1:HJ1cz1n9n3chRP9UrtqmP91+xTi0Q5l+H/4z4tpkwgQ=
github.com/linode/linodego v1.52.1/go.mod h1:zEN2sX+cSdp67EuRY1HJiyuLujoa7HqvVwNEcJv3iXw=
github.com/linode/linodego v1.53.0 h1:UWr7bUUVMtcfsuapC+6blm6+jJLPd7Tf9MZUpdOERnI=
github.com/linode/linodego v1.53.0/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA=
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=
@ -723,8 +723,8 @@ github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.6/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
@ -819,8 +819,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.94.0 h1:6Vbv7oCb8plv7wNnx0cI+6kBQ7RUpZAvj3tQaHDXULo=
github.com/oracle/oci-go-sdk/v65 v65.94.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
github.com/oracle/oci-go-sdk/v65 v65.95.2 h1:0HJ0AgpLydp/DtvYrF2d4str2BjXOVAeNbuW7E07g94=
github.com/oracle/oci-go-sdk/v65 v65.95.2/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso=
@ -882,8 +882,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@ -907,8 +907,8 @@ github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3/go.mod h1:rtQlpHw+eR6UrqaS3kX1VYeaCxzCVdimDS7g5Ln4pPc=
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@ -916,8 +916,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.33 h1:KhF0WejiUTDbL5X55nXowP7zNopwpowa6qaMAWyIE+0=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33/go.mod h1:792k1RTU+5JeMXm35/e2Wgp71qPH/DmDoZrRc+EFZDk=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 h1:48+VFHsyVcAHIN2v1Ao9v1/RkjJS5AwctFucBrfYNIA=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34/go.mod h1:zFWiHphneiey3s8HOtAEnGrRlWivNaxW5T6d5Xfco7g=
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=
@ -990,7 +990,17 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
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.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550=
@ -1012,8 +1022,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.25 h1:FmWtFEa+invTIzWlWK6Vk7BVEZU/97QBzeI8Z1JjGt8=
github.com/vektah/gqlparser/v2 v2.5.25/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/vektah/gqlparser/v2 v2.5.26 h1:REqqFkO8+SOEgZHR/eHScjjVjGS8Nk3RMO/juiTobN4=
github.com/vektah/gqlparser/v2 v2.5.26/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
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=
@ -1040,12 +1050,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.6.1 h1:yJ9WlDih9HT457QPuHt/TH/XtsdN2tubyxyQHSHPsEo=
go.etcd.io/etcd/api/v3 v3.6.1/go.mod h1:lnfuqoGsXMlZdTJlact3IB56o3bWp1DIlXPIGKRArto=
go.etcd.io/etcd/client/pkg/v3 v3.6.1 h1:CxDVv8ggphmamrXM4Of8aCC8QHzDM4tGcVr9p2BSoGk=
go.etcd.io/etcd/client/pkg/v3 v3.6.1/go.mod h1:aTkCp+6ixcVTZmrJGa7/Mc5nMNs59PEgBbq+HCmWyMc=
go.etcd.io/etcd/client/v3 v3.6.1 h1:KelkcizJGsskUXlsxjVrSmINvMMga0VWwFF0tSPGEP0=
go.etcd.io/etcd/client/v3 v3.6.1/go.mod h1:fCbPUdjWNLfx1A6ATo9syUmFVxqHH9bCnPLBZmnLmMY=
go.etcd.io/etcd/api/v3 v3.6.2 h1:25aCkIMjUmiiOtnBIp6PhNj4KdcURuBak0hU2P1fgRc=
go.etcd.io/etcd/api/v3 v3.6.2/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
go.etcd.io/etcd/client/pkg/v3 v3.6.2 h1:zw+HRghi/G8fKpgKdOcEKpnBTE4OO39T6MegA0RopVU=
go.etcd.io/etcd/client/pkg/v3 v3.6.2/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
go.etcd.io/etcd/client/v3 v3.6.2 h1:RgmcLJxkpHqpFvgKNwAQHX3K+wsSARMXKgjmUSpoSKQ=
go.etcd.io/etcd/client/v3 v3.6.2/go.mod h1:PL7e5QMKzjybn0FosgiWvCUDzvdChpo5UgGR4Sk4Gzc=
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=
@ -1115,8 +1125,8 @@ golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
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=
@ -1184,8 +1194,8 @@ golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY
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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
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=
@ -1203,8 +1213,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180425194835-bb9c189858d9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1261,8 +1271,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.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
@ -1270,8 +1280,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.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
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=
@ -1282,8 +1292,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
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=
@ -1326,8 +1336,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.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
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 +1350,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.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
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=
@ -1449,16 +1459,16 @@ istio.io/client-go v1.26.2/go.mod h1:eAImguSJPdaDiSSS2CEsywNHE8WWfqd3WfS18Rj8ynI
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.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8=
k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE=
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.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
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=
@ -1467,8 +1477,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.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA=
k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
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=

4
internal/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- internal

View File

@ -37,7 +37,7 @@ func TestComputeMetrics(t *testing.T) {
t.Errorf("Expected not empty metrics registry, got %d", len(reg.Metrics))
}
assert.Len(t, reg.Metrics, 19)
assert.Len(t, reg.Metrics, 20)
}
func TestGenerateMarkdownTableRenderer(t *testing.T) {

4
kustomize/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- kustomize

4
pkg/apis/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- apis

View File

@ -209,7 +209,7 @@ type Config struct {
WebhookProviderReadTimeout time.Duration
WebhookProviderWriteTimeout time.Duration
WebhookServer bool
TraefikDisableLegacy bool
TraefikEnableLegacy bool
TraefikDisableNew bool
NAT64Networks []string
ExcludeUnschedulable bool
@ -359,7 +359,7 @@ var defaultConfig = &Config{
TLSCA: "",
TLSClientCert: "",
TLSClientCertKey: "",
TraefikDisableLegacy: false,
TraefikEnableLegacy: false,
TraefikDisableNew: false,
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
@ -486,7 +486,7 @@ func App(cfg *Config) *kingpin.Application {
app.Flag("service-type-filter", "The service types to filter by. Specify multiple times for multiple filters to be applied. (optional, default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").Default(defaultConfig.ServiceTypeFilter...).StringsVar(&cfg.ServiceTypeFilter)
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver", "f5-transportserver", "traefik-proxy")
app.Flag("target-net-filter", "Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.TargetNetFilter)
app.Flag("traefik-disable-legacy", "Disable listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableLegacy)).BoolVar(&cfg.TraefikDisableLegacy)
app.Flag("traefik-enable-legacy", "Enable legacy listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikEnableLegacy)).BoolVar(&cfg.TraefikEnableLegacy)
app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew)
// Flags related to providers

97
pkg/http/http.go Normal file
View File

@ -0,0 +1,97 @@
/*
Copyright 2025 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.
*/
// ref: https://github.com/linki/instrumented_http/blob/master/client.go
package http
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
)
var (
requestDuration = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "request_duration_seconds",
Help: "The HTTP request latencies in seconds.",
Subsystem: "http",
ConstLabels: prometheus.Labels{"handler": "instrumented_http"},
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
},
[]string{"scheme", "host", "path", "method", "status"},
)
)
func init() {
prometheus.MustRegister(requestDuration)
}
type CustomRoundTripper struct {
next http.RoundTripper
}
// CancelRequest is a no-op to satisfy interfaces that require it.
// https://github.com/kubernetes/client-go/blob/34f52c14eaed7e50c845cc14e85e1c4c91e77470/transport/transport.go#L346
func (r *CustomRoundTripper) CancelRequest(_ *http.Request) {
}
func (r *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
resp, err := r.next.RoundTrip(req)
status := ""
if resp != nil {
status = fmt.Sprintf("%d", resp.StatusCode)
}
labels := prometheus.Labels{
"scheme": req.URL.Scheme,
"host": req.URL.Host,
"path": pathProcessor(req.URL.Path),
"method": req.Method,
"status": status,
}
requestDuration.With(labels).Observe(time.Since(start).Seconds())
return resp, err
}
func NewInstrumentedClient(next *http.Client) *http.Client {
if next == nil {
next = http.DefaultClient
}
next.Transport = NewInstrumentedTransport(next.Transport)
return next
}
func NewInstrumentedTransport(next http.RoundTripper) http.RoundTripper {
if next == nil {
next = http.DefaultTransport
}
return &CustomRoundTripper{next: next}
}
func pathProcessor(path string) string {
parts := strings.Split(path, "/")
return parts[len(parts)-1]
}

81
pkg/http/http_test.go Normal file
View File

@ -0,0 +1,81 @@
/*
Copyright 2025 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 http
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/require"
)
type dummyTransport struct{}
func (d *dummyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("dummy error")
}
func TestNewInstrumentedTransport(t *testing.T) {
dt := &dummyTransport{}
rt := NewInstrumentedTransport(dt)
crt, ok := rt.(*CustomRoundTripper)
require.True(t, ok)
require.Equal(t, dt, crt.next)
// Should default to http.DefaultTransport if nil
rt2 := NewInstrumentedTransport(nil)
crt2, ok := rt2.(*CustomRoundTripper)
require.True(t, ok)
require.Equal(t, http.DefaultTransport, crt2.next)
}
func TestNewInstrumentedClient(t *testing.T) {
client := &http.Client{Transport: &dummyTransport{}}
result := NewInstrumentedClient(client)
require.Equal(t, client, result)
_, ok := result.Transport.(*CustomRoundTripper)
require.True(t, ok)
// Should default to http.DefaultClient if nil
result2 := NewInstrumentedClient(nil)
require.Equal(t, http.DefaultClient, result2)
_, ok = result2.Transport.(*CustomRoundTripper)
require.True(t, ok)
}
func TestPathProcessor(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"/foo/bar", "bar"},
{"/foo/", ""},
{"/", ""},
{"", ""},
{"/foo/bar/baz", "baz"},
{"foo/bar", "bar"},
{"foo", "foo"},
{"foo/", ""},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
require.Equal(t, tt.expected, pathProcessor(tt.input))
})
}
}

4
pkg/metrics/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- metrics

View File

@ -17,25 +17,44 @@ limitations under the License.
package metrics
import (
"runtime"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/version"
log "github.com/sirupsen/logrus"
cfg "sigs.k8s.io/external-dns/pkg/apis/externaldns"
)
const (
Namespace = "external_dns"
)
var (
RegisterMetric = NewMetricsRegister()
)
func init() {
RegisterMetric.MustRegister(NewGaugeFuncMetric(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "build_info",
Help: fmt.Sprintf(
"A metric with a constant '1' value labeled with 'version' and 'revision' of %s and the 'go_version', 'os' and the 'arch' used the build.",
Namespace,
),
ConstLabels: prometheus.Labels{
"version": cfg.Version,
"revision": version.GetRevision(),
"go_version": version.GoVersion,
"os": version.GoOS,
"arch": version.GoArch,
},
}))
}
func NewMetricsRegister() *MetricRegistry {
reg := prometheus.WrapRegistererWith(
prometheus.Labels{
"version": cfg.Version,
"arch": runtime.GOARCH,
"go_version": runtime.Version(),
},
prometheus.Labels{},
prometheus.DefaultRegisterer)
return &MetricRegistry{
Registerer: reg,
@ -54,7 +73,7 @@ func NewMetricsRegister() *MetricRegistry {
// }
func (m *MetricRegistry) MustRegister(cs IMetric) {
switch v := cs.(type) {
case CounterMetric, GaugeMetric, CounterVecMetric, GaugeVecMetric:
case CounterMetric, GaugeMetric, CounterVecMetric, GaugeVecMetric, GaugeFuncMetric:
if _, exists := m.mName[cs.Get().FQDN]; exists {
return
} else {
@ -70,6 +89,8 @@ func (m *MetricRegistry) MustRegister(cs IMetric) {
m.Registerer.MustRegister(metric.Gauge)
case CounterVecMetric:
m.Registerer.MustRegister(metric.CounterVec)
case GaugeFuncMetric:
m.Registerer.MustRegister(metric.GaugeFunc)
}
log.Debugf("Register metric: %s", cs.Get().FQDN)
default:

View File

@ -88,6 +88,7 @@ func (g GaugeVecMetric) SetWithLabels(value float64, lvs ...string) {
}
func NewGaugeWithOpts(opts prometheus.GaugeOpts) GaugeMetric {
opts.Namespace = Namespace
return GaugeMetric{
Metric: Metric{
Type: "gauge",
@ -104,6 +105,7 @@ func NewGaugeWithOpts(opts prometheus.GaugeOpts) GaugeMetric {
// NewGaugedVectorOpts creates a new GaugeVec based on the provided GaugeOpts and
// partitioned by the given label names.
func NewGaugedVectorOpts(opts prometheus.GaugeOpts, labelNames []string) GaugeVecMetric {
opts.Namespace = Namespace
return GaugeVecMetric{
Metric: Metric{
Type: "gauge",
@ -118,6 +120,7 @@ func NewGaugedVectorOpts(opts prometheus.GaugeOpts, labelNames []string) GaugeVe
}
func NewCounterWithOpts(opts prometheus.CounterOpts) CounterMetric {
opts.Namespace = Namespace
return CounterMetric{
Metric: Metric{
Type: "counter",
@ -132,6 +135,7 @@ func NewCounterWithOpts(opts prometheus.CounterOpts) CounterMetric {
}
func NewCounterVecWithOpts(opts prometheus.CounterOpts, labelNames []string) CounterVecMetric {
opts.Namespace = Namespace
return CounterVecMetric{
Metric: Metric{
Type: "counter",
@ -144,3 +148,31 @@ func NewCounterVecWithOpts(opts prometheus.CounterOpts, labelNames []string) Cou
CounterVec: prometheus.NewCounterVec(opts, labelNames),
}
}
type GaugeFuncMetric struct {
Metric
GaugeFunc prometheus.GaugeFunc
}
func (g GaugeFuncMetric) Get() *Metric {
return &g.Metric
}
func NewGaugeFuncMetric(opts prometheus.GaugeOpts) GaugeFuncMetric {
return GaugeFuncMetric{
Metric: Metric{
Type: "gauge",
Name: opts.Name,
FQDN: func() string {
if opts.Subsystem != "" {
return fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name)
}
return opts.Name
}(),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
},
GaugeFunc: prometheus.NewGaugeFunc(opts, func() float64 { return 1 }),
}
}

View File

@ -17,18 +17,17 @@ limitations under the License.
package metrics
import (
"reflect"
"testing"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
)
func TestNewGaugeWithOpts(t *testing.T) {
opts := prometheus.GaugeOpts{
Name: "test_gauge",
Namespace: "test_namespace",
Subsystem: "test_subsystem",
Help: "This is a test gauge",
}
@ -37,7 +36,7 @@ func TestNewGaugeWithOpts(t *testing.T) {
assert.Equal(t, "gauge", gaugeMetric.Type)
assert.Equal(t, "test_gauge", gaugeMetric.Name)
assert.Equal(t, "test_namespace", gaugeMetric.Namespace)
assert.Equal(t, Namespace, gaugeMetric.Namespace)
assert.Equal(t, "test_subsystem", gaugeMetric.Subsystem)
assert.Equal(t, "This is a test gauge", gaugeMetric.Help)
assert.Equal(t, "test_subsystem_test_gauge", gaugeMetric.FQDN)
@ -47,7 +46,6 @@ func TestNewGaugeWithOpts(t *testing.T) {
func TestNewCounterWithOpts(t *testing.T) {
opts := prometheus.CounterOpts{
Name: "test_counter",
Namespace: "test_namespace",
Subsystem: "test_subsystem",
Help: "This is a test counter",
}
@ -56,7 +54,7 @@ func TestNewCounterWithOpts(t *testing.T) {
assert.Equal(t, "counter", counterMetric.Type)
assert.Equal(t, "test_counter", counterMetric.Name)
assert.Equal(t, "test_namespace", counterMetric.Namespace)
assert.Equal(t, Namespace, counterMetric.Namespace)
assert.Equal(t, "test_subsystem", counterMetric.Subsystem)
assert.Equal(t, "This is a test counter", counterMetric.Help)
assert.Equal(t, "test_subsystem_test_counter", counterMetric.FQDN)
@ -77,7 +75,7 @@ func TestNewCounterVecWithOpts(t *testing.T) {
assert.Equal(t, "counter", counterVecMetric.Type)
assert.Equal(t, "test_counter_vec", counterVecMetric.Name)
assert.Equal(t, "test_namespace", counterVecMetric.Namespace)
assert.Equal(t, Namespace, counterVecMetric.Namespace)
assert.Equal(t, "test_subsystem", counterVecMetric.Subsystem)
assert.Equal(t, "This is a test counter vector", counterVecMetric.Help)
assert.Equal(t, "test_subsystem_test_counter_vec", counterVecMetric.FQDN)
@ -113,3 +111,20 @@ func TestGaugeV_SetWithLabels(t *testing.T) {
assert.Len(t, m.Label, 2)
}
func TestNewBuildInfoCollector(t *testing.T) {
metric := NewGaugeFuncMetric(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "build_info",
ConstLabels: prometheus.Labels{
"version": "0.0.1",
"goversion": "1.24",
"arch": "arm64",
},
})
desc := metric.GaugeFunc.Desc()
assert.Equal(t, "external_dns_build_info", reflect.ValueOf(desc).Elem().FieldByName("fqName").String())
assert.Contains(t, desc.String(), "version=\"0.0.1\"")
}

4
pkg/rfc2317/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- rfc2317

View File

@ -98,7 +98,7 @@ func CidrToInAddr(cidr string) (string, error) {
// reverseaddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP
// address addr suitable for rDNS (PTR) record lookup or an error if it fails
// to parse the IP address.
func reverseaddr(addr string) (arpa string, err error) {
func reverseaddr(addr string) (string, error) {
ip := net.ParseIP(addr)
if ip == nil {
return "", &net.DNSError{Err: "unrecognized address", Name: addr}

4
pkg/tlsutils/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- tls

4
plan/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- plan

View File

@ -18,6 +18,7 @@ package plan
import (
"fmt"
"slices"
"strings"
"github.com/google/go-cmp/cmp"
@ -339,10 +340,16 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Ma
return filtered
}
var idnaProfile = idna.New(
idna.MapForLookup(),
idna.Transitional(true),
idna.StrictDomainName(false),
)
// normalizeDNSName converts a DNS name to a canonical form, so that we can use string equality
// it: removes space, get ASCII version of dnsName complient with Section 5 of RFC 5891, ensures there is a trailing dot
func normalizeDNSName(dnsName string) string {
s, err := idna.Lookup.ToASCII(strings.TrimSpace(dnsName))
s, err := idnaProfile.ToASCII(strings.TrimSpace(dnsName))
if err != nil {
log.Warnf(`Got error while parsing DNSName %s: %v`, dnsName, err)
}
@ -353,15 +360,8 @@ func normalizeDNSName(dnsName string) string {
}
func IsManagedRecord(record string, managedRecords, excludeRecords []string) bool {
for _, r := range excludeRecords {
if record == r {
return false
}
if slices.Contains(excludeRecords, record) {
return false
}
for _, r := range managedRecords {
if record == r {
return true
}
}
return false
return slices.Contains(managedRecords, record)
}

View File

@ -1028,7 +1028,7 @@ func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) {
}
}
func TestNormalizeDNSName(t *testing.T) {
func TestNormalizeDNSName(tt *testing.T) {
records := []struct {
dnsName string
expect string
@ -1061,6 +1061,18 @@ func TestNormalizeDNSName(t *testing.T) {
"foo.com.",
"foo.com.",
},
{
"_foo.com.",
"_foo.com.",
},
{
"\u005Ffoo.com.",
"_foo.com.",
},
{
".foo.com.",
".foo.com.",
},
{
"foo123.COM",
"foo123.com.",
@ -1097,10 +1109,20 @@ func TestNormalizeDNSName(t *testing.T) {
"xn--nordic--w1a.kitty😸.com.",
"xn--nordic--w1a.xn--kitty-pd34d.com.",
},
{
"*.example.com.",
"*.example.com.",
},
{
"*.example.com",
"*.example.com.",
},
}
for _, r := range records {
gotName := normalizeDNSName(r.dnsName)
assert.Equal(t, r.expect, gotName)
tt.Run(r.dnsName, func(t *testing.T) {
gotName := normalizeDNSName(r.dnsName)
assert.Equal(t, r.expect, gotName)
})
}
}

View File

@ -40,7 +40,7 @@ const (
maxInt = int(maxUint >> 1)
)
// edgeDNSClient is a proxy interface of the Akamai edgegrid configdns-v2 package that can be stubbed for testing.
// AkamaiDNSService is a proxy interface of the Akamai edgegrid configdns-v2 package that can be stubbed for testing.
type AkamaiDNSService interface {
ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error)
GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error)
@ -208,7 +208,8 @@ func (p AkamaiProvider) fetchZones() (akamaiZones, error) {
}
// Records returns the list of records in a given zone.
func (p AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) {
func (p AkamaiProvider) Records(context.Context) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
zones, err := p.fetchZones() // returns a filtered set of zones
if err != nil {
log.Warnf("Failed to identify target zones! Error: %s", err.Error())

View File

@ -29,7 +29,7 @@ import (
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
"github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz"
"github.com/denverdino/aliyungo/metadata"
yaml "github.com/goccy/go-yaml"
"github.com/goccy/go-yaml"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
@ -49,22 +49,22 @@ const (
// AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing.
// See https://help.aliyun.com/document_detail/29739.html for descriptions of all of its methods.
type AlibabaCloudDNSAPI interface {
AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error)
DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error)
UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error)
DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error)
DescribeDomains(request *alidns.DescribeDomainsRequest) (response *alidns.DescribeDomainsResponse, err error)
AddDomainRecord(request *alidns.AddDomainRecordRequest) (*alidns.AddDomainRecordResponse, error)
DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (*alidns.DeleteDomainRecordResponse, error)
UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (*alidns.UpdateDomainRecordResponse, error)
DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (*alidns.DescribeDomainRecordsResponse, error)
DescribeDomains(request *alidns.DescribeDomainsRequest) (*alidns.DescribeDomainsResponse, error)
}
// AlibabaCloudPrivateZoneAPI is a minimal implementation of Private Zone API that we actually use, used primarily for unit testing.
// See https://help.aliyun.com/document_detail/66234.html for descriptions of all of its methods.
type AlibabaCloudPrivateZoneAPI interface {
AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error)
DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (response *pvtz.DeleteZoneRecordResponse, err error)
UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (response *pvtz.UpdateZoneRecordResponse, err error)
DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (response *pvtz.DescribeZoneRecordsResponse, err error)
DescribeZones(request *pvtz.DescribeZonesRequest) (response *pvtz.DescribeZonesResponse, err error)
DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (response *pvtz.DescribeZoneInfoResponse, err error)
AddZoneRecord(request *pvtz.AddZoneRecordRequest) (*pvtz.AddZoneRecordResponse, error)
DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (*pvtz.DeleteZoneRecordResponse, error)
UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (*pvtz.UpdateZoneRecordResponse, error)
DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (*pvtz.DescribeZoneRecordsResponse, error)
DescribeZones(request *pvtz.DescribeZonesRequest) (*pvtz.DescribeZonesResponse, error)
DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (*pvtz.DescribeZoneInfoResponse, error)
}
// AlibabaCloudProvider implements the DNS provider for Alibaba Cloud.
@ -284,19 +284,18 @@ func (p *AlibabaCloudProvider) refreshStsToken(sleepTime time.Duration) {
// Records gets the current records.
//
// Returns the current records or an error if the operation failed.
func (p *AlibabaCloudProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
func (p *AlibabaCloudProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
if p.privateZone {
endpoints, err = p.privateZoneRecords()
return p.privateZoneRecords()
} else {
endpoints, err = p.recordsForDNS()
return p.recordsForDNS()
}
return endpoints, err
}
// ApplyChanges applies the given changes.
//
// Returns nil if the operation was successful or an error if the operation failed.
func (p *AlibabaCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
func (p *AlibabaCloudProvider) ApplyChanges(_ context.Context, changes *plan.Changes) error {
if changes == nil || len(changes.Create)+len(changes.Delete)+len(changes.UpdateNew) == 0 {
// No op
return nil
@ -318,11 +317,12 @@ func (p *AlibabaCloudProvider) getDNSName(rr, domain string) string {
// recordsForDNS gets the current records.
//
// Returns the current records or an error if the operation failed.
func (p *AlibabaCloudProvider) recordsForDNS() (endpoints []*endpoint.Endpoint, _ error) {
func (p *AlibabaCloudProvider) recordsForDNS() ([]*endpoint.Endpoint, error) {
records, err := p.records()
if err != nil {
return nil, err
}
endpoints := make([]*endpoint.Endpoint, 0, len(records))
for _, recordList := range p.groupRecords(records) {
name := p.getDNSName(recordList[0].RR, recordList[0].DomainName)
recordType := recordList[0].Type
@ -360,8 +360,8 @@ func (p *AlibabaCloudProvider) getRecordKeyByEndpoint(endpoint *endpoint.Endpoin
return endpoint.RecordType + ":" + endpoint.DNSName
}
func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) (endpointMap map[string][]alidns.Record) {
endpointMap = make(map[string][]alidns.Record)
func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) map[string][]alidns.Record {
endpointMap := make(map[string][]alidns.Record)
for _, record := range records {
key := p.getRecordKey(record)
@ -675,7 +675,7 @@ func (p *AlibabaCloudProvider) updateRecords(recordMap map[string][]alidns.Recor
return nil
}
func (p *AlibabaCloudProvider) splitDNSName(dnsName string, hostedZoneDomains []string) (rr string, domain string) {
func (p *AlibabaCloudProvider) splitDNSName(dnsName string, hostedZoneDomains []string) (string, string) {
name := strings.TrimSuffix(dnsName, ".")
// sort zones by dot count; make sure subdomains sort earlier
@ -683,6 +683,8 @@ func (p *AlibabaCloudProvider) splitDNSName(dnsName string, hostedZoneDomains []
return strings.Count(hostedZoneDomains[i], ".") > strings.Count(hostedZoneDomains[j], ".")
})
var rr, domain string
for _, filter := range hostedZoneDomains {
if strings.HasSuffix(name, "."+filter) {
rr = name[0 : len(name)-len(filter)-1]
@ -819,8 +821,8 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone
return result, nil
}
func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) (endpointMap map[string][]pvtz.Record) {
endpointMap = make(map[string][]pvtz.Record)
func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) map[string][]pvtz.Record {
endpointMap := make(map[string][]pvtz.Record)
for _, record := range zone.records {
key := record.Type + ":" + record.Rr
@ -834,12 +836,14 @@ func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone)
// recordsForPrivateZone gets the current records.
//
// Returns the current records or an error if the operation failed.
func (p *AlibabaCloudProvider) privateZoneRecords() (endpoints []*endpoint.Endpoint, _ error) {
func (p *AlibabaCloudProvider) privateZoneRecords() ([]*endpoint.Endpoint, error) {
zones, err := p.getPrivateZones()
if err != nil {
return nil, err
}
endpoints := make([]*endpoint.Endpoint, 0)
for _, zone := range zones {
recordMap := p.groupPrivateZoneRecords(zone)
for _, recordList := range recordMap {
@ -908,7 +912,7 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibaba
func (p *AlibabaCloudProvider) createPrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error {
for _, endpoint := range endpoints {
for _, target := range endpoint.Targets {
p.createPrivateZoneRecord(zones, endpoint, target)
_ = p.createPrivateZoneRecord(zones, endpoint, target)
}
}
return nil

View File

@ -55,7 +55,7 @@ func NewMockAlibabaCloudDNSAPI() *MockAlibabaCloudDNSAPI {
return &api
}
func (m *MockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error) {
func (m *MockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecordRequest) (*alidns.AddDomainRecordResponse, error) {
ttl, _ := request.TTL.GetValue()
m.records = append(m.records, alidns.Record{
RecordId: "3",
@ -65,11 +65,10 @@ func (m *MockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecord
RR: request.RR,
Value: request.Value,
})
response = alidns.CreateAddDomainRecordResponse()
return response, nil
return alidns.CreateAddDomainRecordResponse(), nil
}
func (m *MockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error) {
func (m *MockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (*alidns.DeleteDomainRecordResponse, error) {
var result []alidns.Record
for _, record := range m.records {
if record.RecordId != request.RecordId {
@ -77,24 +76,24 @@ func (m *MockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomain
}
}
m.records = result
response = alidns.CreateDeleteDomainRecordResponse()
response := alidns.CreateDeleteDomainRecordResponse()
response.RecordId = request.RecordId
return response, nil
}
func (m *MockAlibabaCloudDNSAPI) UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error) {
func (m *MockAlibabaCloudDNSAPI) UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (*alidns.UpdateDomainRecordResponse, error) {
ttl, _ := request.TTL.GetValue64()
for i := range m.records {
if m.records[i].RecordId == request.RecordId {
m.records[i].TTL = ttl
}
}
response = alidns.CreateUpdateDomainRecordResponse()
response := alidns.CreateUpdateDomainRecordResponse()
response.RecordId = request.RecordId
return response, nil
}
func (m *MockAlibabaCloudDNSAPI) DescribeDomains(request *alidns.DescribeDomainsRequest) (response *alidns.DescribeDomainsResponse, err error) {
func (m *MockAlibabaCloudDNSAPI) DescribeDomains(request *alidns.DescribeDomainsRequest) (*alidns.DescribeDomainsResponse, error) {
var result alidns.DomainsInDescribeDomains
for _, record := range m.records {
domain := alidns.Domain{}
@ -103,19 +102,19 @@ func (m *MockAlibabaCloudDNSAPI) DescribeDomains(request *alidns.DescribeDomains
DomainName: domain.DomainName,
})
}
response = alidns.CreateDescribeDomainsResponse()
response := alidns.CreateDescribeDomainsResponse()
response.Domains = result
return response, nil
}
func (m *MockAlibabaCloudDNSAPI) DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error) {
func (m *MockAlibabaCloudDNSAPI) DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (*alidns.DescribeDomainRecordsResponse, error) {
var result []alidns.Record
for _, record := range m.records {
if record.DomainName == request.DomainName {
result = append(result, record)
}
}
response = alidns.CreateDescribeDomainRecordsResponse()
response := alidns.CreateDescribeDomainRecordsResponse()
response.DomainRecords.Record = result
return response, nil
}
@ -158,7 +157,7 @@ func NewMockAlibabaCloudPrivateZoneAPI() *MockAlibabaCloudPrivateZoneAPI {
return &api
}
func (m *MockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error) {
func (m *MockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneRecordRequest) (*pvtz.AddZoneRecordResponse, error) {
ttl, _ := request.Ttl.GetValue()
m.records = append(m.records, pvtz.Record{
RecordId: 3,
@ -167,11 +166,10 @@ func (m *MockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneReco
Rr: request.Rr,
Value: request.Value,
})
response = pvtz.CreateAddZoneRecordResponse()
return response, nil
return pvtz.CreateAddZoneRecordResponse(), nil
}
func (m *MockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (response *pvtz.DeleteZoneRecordResponse, err error) {
func (m *MockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (*pvtz.DeleteZoneRecordResponse, error) {
recordID, _ := request.RecordId.GetValue64()
var result []pvtz.Record
@ -181,11 +179,10 @@ func (m *MockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZo
}
}
m.records = result
response = pvtz.CreateDeleteZoneRecordResponse()
return response, nil
return pvtz.CreateDeleteZoneRecordResponse(), nil
}
func (m *MockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (response *pvtz.UpdateZoneRecordResponse, err error) {
func (m *MockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (*pvtz.UpdateZoneRecordResponse, error) {
recordID, _ := request.RecordId.GetValue64()
ttl, _ := request.Ttl.GetValue()
for i := range m.records {
@ -193,24 +190,23 @@ func (m *MockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZo
m.records[i].Ttl = ttl
}
}
response = pvtz.CreateUpdateZoneRecordResponse()
return response, nil
return pvtz.CreateUpdateZoneRecordResponse(), nil
}
func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (response *pvtz.DescribeZoneRecordsResponse, err error) {
response = pvtz.CreateDescribeZoneRecordsResponse()
func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (*pvtz.DescribeZoneRecordsResponse, error) {
response := pvtz.CreateDescribeZoneRecordsResponse()
response.Records.Record = append(response.Records.Record, m.records...)
return response, nil
}
func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZones(request *pvtz.DescribeZonesRequest) (response *pvtz.DescribeZonesResponse, err error) {
response = pvtz.CreateDescribeZonesResponse()
func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZones(_ *pvtz.DescribeZonesRequest) (*pvtz.DescribeZonesResponse, error) {
response := pvtz.CreateDescribeZonesResponse()
response.Zones.Zone = append(response.Zones.Zone, m.zone)
return response, nil
}
func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (response *pvtz.DescribeZoneInfoResponse, err error) {
response = pvtz.CreateDescribeZoneInfoResponse()
func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(_ *pvtz.DescribeZoneInfoRequest) (*pvtz.DescribeZoneInfoResponse, error) {
response := pvtz.CreateDescribeZoneInfoResponse()
response.ZoneId = m.zone.ZoneId
response.ZoneName = m.zone.ZoneName
response.BindVpcs = pvtz.BindVpcsInDescribeZoneInfo{Vpc: make([]pvtz.VpcInDescribeZoneInfo, len(m.zone.Vpcs.Vpc))}
@ -336,7 +332,7 @@ func TestAlibabaCloudProvider_ApplyChanges_HaveNoDefinedZoneDomain(t *testing.T)
changes := plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "www.example.com", //no found this zone by API: DescribeDomains
DNSName: "www.example.com", // no found this zone by API: DescribeDomains
RecordType: "A",
RecordTTL: 300,
Targets: endpoint.NewTargets("9.9.9.9"),

View File

@ -53,19 +53,27 @@ const (
// providerSpecificEvaluateTargetHealth specifies whether an AWS ALIAS record
// has the EvaluateTargetHealth field set to true. Present iff the endpoint
// has a `providerSpecificAlias` value of `true`.
providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health"
providerSpecificWeight = "aws/weight"
providerSpecificRegion = "aws/region"
providerSpecificFailover = "aws/failover"
providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code"
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
providerSpecificHealthCheckID = "aws/health-check-id"
sameZoneAlias = "same-zone"
providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health"
providerSpecificWeight = "aws/weight"
providerSpecificRegion = "aws/region"
providerSpecificFailover = "aws/failover"
providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code"
providerSpecificGeolocationCountryCode = "aws/geolocation-country-code"
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
providerSpecificGeoProximityLocationAWSRegion = "aws/geoproximity-region"
providerSpecificGeoProximityLocationBias = "aws/geoproximity-bias"
providerSpecificGeoProximityLocationCoordinates = "aws/geoproximity-coordinates"
providerSpecificGeoProximityLocationLocalZoneGroup = "aws/geoproximity-local-zone-group"
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
providerSpecificHealthCheckID = "aws/health-check-id"
sameZoneAlias = "same-zone"
// Currently supported up to 10 health checks or hosted zones.
// https://docs.aws.amazon.com/Route53/latest/APIReference/API_ListTagsForResources.html#API_ListTagsForResources_RequestSyntax
batchSize = 10
batchSize = 10
minLatitude = -90.0
maxLatitude = 90.0
minLongitude = -180.0
maxLongitude = 180.0
)
// see elb: https://docs.aws.amazon.com/general/latest/gr/elb.html
@ -78,6 +86,7 @@ var canonicalHostedZones = map[string]string{
"ca-central-1.elb.amazonaws.com": "ZQSVJUPU6J1EY",
"ca-west-1.elb.amazonaws.com": "Z06473681N0SF6OS049SD",
"ap-east-1.elb.amazonaws.com": "Z3DQVH9N71FHZ0",
"ap-east-2.elb.amazonaws.com": "Z02789141MW7T1WBU19PO",
"ap-south-1.elb.amazonaws.com": "ZP97RAFLXTNZK",
"ap-south-2.elb.amazonaws.com": "Z0173938T07WNTVAEPZN",
"ap-northeast-2.elb.amazonaws.com": "ZWKZPGTI48KDX",
@ -115,6 +124,7 @@ var canonicalHostedZones = map[string]string{
"elb.ca-central-1.amazonaws.com": "Z2EPGBW3API2WT",
"elb.ca-west-1.amazonaws.com": "Z02754302KBB00W2LKWZ9",
"elb.ap-east-1.amazonaws.com": "Z12Y7K3UBGUAD1",
"elb.ap-east-2.amazonaws.com": "Z09176273OC2HWIAUNYW",
"elb.ap-south-1.amazonaws.com": "ZVDDRBQ08TROA",
"elb.ap-south-2.amazonaws.com": "Z0711778386UTO08407HT",
"elb.ap-northeast-3.amazonaws.com": "Z1GWIQ4HH19I5X",
@ -153,6 +163,7 @@ var canonicalHostedZones = map[string]string{
"us-east-2.vpce.amazonaws.com": "ZC8PG0KIFKBRI",
"af-south-1.vpce.amazonaws.com": "Z09302161J80N9A7UTP7U",
"ap-east-1.vpce.amazonaws.com": "Z2LIHJ7PKBEMWN",
"ap-east-2.vpce.amazonaws.com": "Z09379811HWP0POAUWVN3",
"ap-northeast-1.vpce.amazonaws.com": "Z2E726K9Y6RL4W",
"ap-northeast-2.vpce.amazonaws.com": "Z27UANNT0PRK1T",
"ap-northeast-3.vpce.amazonaws.com": "Z376B5OMM2JZL2",
@ -186,6 +197,7 @@ var canonicalHostedZones = map[string]string{
"execute-api.us-west-2.amazonaws.com": "Z2OJLYMUO9EFXC",
"execute-api.af-south-1.amazonaws.com": "Z2DHW2332DAMTN",
"execute-api.ap-east-1.amazonaws.com": "Z3FD1VL90ND7K5",
"execute-api.ap-east-2.amazonaws.com": "Z02909591O7FG9Q56HWB1",
"execute-api.ap-south-1.amazonaws.com": "Z3VO1THU9YC4UR",
"execute-api.ap-northeast-2.amazonaws.com": "Z20JF4UZKIW1U8",
"execute-api.ap-southeast-1.amazonaws.com": "ZL327KTPIQFUL",
@ -231,6 +243,12 @@ type profiledZone struct {
zone *route53types.HostedZone
}
type geoProximity struct {
location *route53types.GeoProximityLocation
endpoint *endpoint.Endpoint
isSet bool
}
func (cs Route53Changes) Route53Changes() []route53types.Change {
var ret []route53types.Change
for _, c := range cs {
@ -454,10 +472,10 @@ func containsOctalSequence(domain string) bool {
}
// Records returns the list of records in a given hosted zone.
func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
func (p *AWSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
zones, err := p.zones(ctx)
if err != nil {
return nil, provider.NewSoftErrorf("records retrieval failed: %w", err)
return nil, provider.NewSoftErrorf("records retrieval failed: %v", err)
}
return p.records(ctx, zones)
@ -542,6 +560,8 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZon
ep.WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, *r.GeoLocation.SubdivisionCode)
}
}
case r.GeoProximityLocation != nil:
handleGeoProximityLocationRecord(&r, ep)
default:
// one of the above needs to be set, otherwise SetIdentifier doesn't make sense
}
@ -560,6 +580,25 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZon
return endpoints, nil
}
func handleGeoProximityLocationRecord(r *route53types.ResourceRecordSet, ep *endpoint.Endpoint) {
if region := aws.ToString(r.GeoProximityLocation.AWSRegion); region != "" {
ep.WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, region)
}
if bias := r.GeoProximityLocation.Bias; bias != nil {
ep.WithProviderSpecific(providerSpecificGeoProximityLocationBias, fmt.Sprintf("%d", aws.ToInt32(bias)))
}
if coords := r.GeoProximityLocation.Coordinates; coords != nil {
coordinates := fmt.Sprintf("%s,%s", aws.ToString(coords.Latitude), aws.ToString(coords.Longitude))
ep.WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, coordinates)
}
if localZoneGroup := aws.ToString(r.GeoProximityLocation.LocalZoneGroup); localZoneGroup != "" {
ep.WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, localZoneGroup)
}
}
// Identify if old and new endpoints require DELETE/CREATE instead of UPDATE.
func (p *AWSProvider) requiresDeleteCreate(old *endpoint.Endpoint, newE *endpoint.Endpoint) bool {
// a change of a record type
@ -602,6 +641,10 @@ func (p *AWSProvider) createUpdateChanges(newEndpoints, oldEndpoints []*endpoint
var updates []*endpoint.Endpoint
for i, newE := range newEndpoints {
if i >= len(oldEndpoints) || oldEndpoints[i] == nil {
log.Debugf("skip %s as endpoint not found in current endpoints", newE.DNSName)
continue
}
oldE := oldEndpoints[i]
if p.requiresDeleteCreate(oldE, newE) {
deletes = append(deletes, oldE)
@ -832,12 +875,32 @@ func (p *AWSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoi
} else {
ep.DeleteProviderSpecificProperty(providerSpecificEvaluateTargetHealth)
}
adjustGeoProximityLocationEndpoint(ep)
}
endpoints = append(endpoints, aliasCnameAaaaEndpoints...)
return endpoints, nil
}
// if the endpoint is using geoproximity, set the bias to 0 if not set
// this is needed to avoid unnecessary Upserts if the desired endpoint doesn't specify a bias
func adjustGeoProximityLocationEndpoint(ep *endpoint.Endpoint) {
if ep.SetIdentifier == "" {
return
}
_, ok1 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion)
_, ok2 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup)
_, ok3 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates)
if ok1 || ok2 || ok3 {
// check if ep has bias property and if not, set it to 0
if _, ok := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationBias); !ok {
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationBias, "0")
}
}
}
// newChange returns a route53 Change
// 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
@ -926,6 +989,8 @@ func (p *AWSProvider) newChange(action route53types.ChangeAction, ep *endpoint.E
if useGeolocation {
change.ResourceRecordSet.GeoLocation = geolocation
}
withChangeForGeoProximityEndpoint(change, ep)
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificHealthCheckID); ok {
@ -939,12 +1004,107 @@ func (p *AWSProvider) newChange(action route53types.ChangeAction, ep *endpoint.E
return change
}
func newGeoProximity(ep *endpoint.Endpoint) *geoProximity {
return &geoProximity{
location: &route53types.GeoProximityLocation{},
endpoint: ep,
isSet: false,
}
}
func (gp *geoProximity) withAWSRegion() *geoProximity {
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion); ok {
gp.location.AWSRegion = aws.String(prop)
gp.isSet = true
}
return gp
}
// add a method to set the local zone group for the geoproximity location
func (gp *geoProximity) withLocalZoneGroup() *geoProximity {
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup); ok {
gp.location.LocalZoneGroup = aws.String(prop)
gp.isSet = true
}
return gp
}
// add a method to set the bias for the geoproximity location
func (gp *geoProximity) withBias() *geoProximity {
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationBias); ok {
bias, err := strconv.ParseInt(prop, 10, 32)
if err != nil {
log.Warnf("Failed parsing value of %s: %s: %v; using bias of 0", providerSpecificGeoProximityLocationBias, prop, err)
bias = 0
}
gp.location.Bias = aws.Int32(int32(bias))
gp.isSet = true
}
return gp
}
// validateCoordinates checks if the given latitude and longitude are valid.
func validateCoordinates(lat, long string) error {
latitude, err := strconv.ParseFloat(lat, 64)
if err != nil || latitude < minLatitude || latitude > maxLatitude {
return fmt.Errorf("invalid latitude: must be a number between %f and %f", minLatitude, maxLatitude)
}
longitude, err := strconv.ParseFloat(long, 64)
if err != nil || longitude < minLongitude || longitude > maxLongitude {
return fmt.Errorf("invalid longitude: must be a number between %f and %f", minLongitude, maxLongitude)
}
return nil
}
func (gp *geoProximity) withCoordinates() *geoProximity {
if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates); ok {
coordinates := strings.Split(prop, ",")
if len(coordinates) == 2 {
latitude := coordinates[0]
longitude := coordinates[1]
if err := validateCoordinates(latitude, longitude); err != nil {
log.Warnf("Invalid coordinates %s for name=%s setIdentifier=%s; %v", prop, gp.endpoint.DNSName, gp.endpoint.SetIdentifier, err)
} else {
gp.location.Coordinates = &route53types.Coordinates{
Latitude: aws.String(latitude),
Longitude: aws.String(longitude),
}
gp.isSet = true
}
} else {
log.Warnf("Invalid coordinates format for %s: %s; expected format 'latitude,longitude'", providerSpecificGeoProximityLocationCoordinates, prop)
}
}
return gp
}
func (gp *geoProximity) build() *route53types.GeoProximityLocation {
if gp.isSet {
return gp.location
}
return nil
}
func withChangeForGeoProximityEndpoint(change *Route53Change, ep *endpoint.Endpoint) {
geoProx := newGeoProximity(ep).
withAWSRegion().
withCoordinates().
withLocalZoneGroup().
withBias()
change.ResourceRecordSet.GeoProximityLocation = geoProx.build()
}
// searches for `changes` that are contained in `queue` and returns the `changes` separated by whether they were found in the queue (`foundChanges`) or not (`notFoundChanges`)
func findChangesInQueue(changes Route53Changes, queue Route53Changes) (foundChanges, notFoundChanges Route53Changes) {
func findChangesInQueue(changes Route53Changes, queue Route53Changes) (Route53Changes, Route53Changes) {
if queue == nil {
return Route53Changes{}, changes
}
var foundChanges, notFoundChanges Route53Changes
for _, c := range changes {
found := false
for _, qc := range queue {
@ -959,7 +1119,7 @@ func findChangesInQueue(changes Route53Changes, queue Route53Changes) (foundChan
}
}
return
return foundChanges, notFoundChanges
}
// group the given changes by name and ownership relation to ensure these are always submitted in the same transaction to Route53;

View File

@ -583,6 +583,42 @@ func TestAWSRecords(t *testing.T) {
SubdivisionCode: aws.String("NY"),
},
},
{
Name: aws.String("geoproximitylocation-region.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeA,
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
SetIdentifier: aws.String("test-set-1"),
GeoProximityLocation: &route53types.GeoProximityLocation{
AWSRegion: aws.String("us-west-2"),
Bias: aws.Int32(10),
},
},
{
Name: aws.String("geoproximitylocation-localzone.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeA,
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
SetIdentifier: aws.String("test-set-1"),
GeoProximityLocation: &route53types.GeoProximityLocation{
LocalZoneGroup: aws.String("usw2-pdx1-az1"),
Bias: aws.Int32(10),
},
},
{
Name: aws.String("geoproximitylocation-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeA,
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
SetIdentifier: aws.String("test-set-1"),
GeoProximityLocation: &route53types.GeoProximityLocation{
Coordinates: &route53types.Coordinates{
Latitude: aws.String("90"),
Longitude: aws.String("90"),
},
Bias: aws.Int32(0),
},
},
{
Name: aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeCname,
@ -636,6 +672,9 @@ func TestAWSRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"),
endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"),
endpoint.NewEndpointWithTTL("geoproximitylocation-region.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"),
endpoint.NewEndpointWithTTL("geoproximitylocation-localzone.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-pdx1-az1").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"),
endpoint.NewEndpointWithTTL("geoproximitylocation-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, "90,90").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "0"),
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(defaultTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id").WithProviderSpecific(providerSpecificAlias, "false"),
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"),
endpoint.NewEndpointWithTTL("mail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, endpoint.TTL(defaultTTL), "10 mailhost1.example.com", "20 mailhost2.example.com"),
@ -670,6 +709,7 @@ func TestAWSAdjustEndpoints(t *testing.T) {
endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"),
endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health
endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
endpoint.NewEndpoint("a-test-geoproximity-no-bias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2"),
}
records, err := provider.AdjustEndpoints(records)
@ -687,6 +727,7 @@ func TestAWSAdjustEndpoints(t *testing.T) {
endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health
endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
endpoint.NewEndpoint("a-test-geoproximity-no-bias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "0"),
})
}
@ -845,6 +886,27 @@ func TestAWSApplyChanges(t *testing.T) {
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1111")}, {Value: aws.String("2606:4700:4700::1001")}},
},
{
Name: aws.String("delete-test-geoproximity.zone-2.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeA,
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
SetIdentifier: aws.String("geoproximity-delete"),
GeoProximityLocation: &route53types.GeoProximityLocation{
AWSRegion: aws.String("us-west-2"),
Bias: aws.Int32(10),
},
},
{
Name: aws.String("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeA,
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
SetIdentifier: aws.String("geoproximity-update"),
GeoProximityLocation: &route53types.GeoProximityLocation{
LocalZoneGroup: aws.String("usw2-lax1-az2"),
},
},
{
Name: aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeA,
@ -915,6 +977,13 @@ func TestAWSApplyChanges(t *testing.T) {
endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpoint("create-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"),
endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-geoproximity-region.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").
WithSetIdentifier("geoproximity-region").
WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").
WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"),
endpoint.NewEndpoint("create-test-geoproximity-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").
WithSetIdentifier("geoproximity-coordinates").
WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, "60,60"),
}
currentRecords := []*endpoint.Endpoint{
@ -930,6 +999,9 @@ func TestAWSApplyChanges(t *testing.T) {
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "bar.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpoint("update-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"),
endpoint.NewEndpoint("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").
WithSetIdentifier("geoproximity-update").
WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-lax1-az2"),
endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("weighted-to-simple").WithProviderSpecific(providerSpecificWeight, "10"),
endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificWeight, "10"),
@ -951,6 +1023,9 @@ func TestAWSApplyChanges(t *testing.T) {
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "baz.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
endpoint.NewEndpoint("update-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1001", "2606:4700:4700::1111"),
endpoint.NewEndpoint("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").
WithSetIdentifier("geoproximity-update").
WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-phx2-az1"),
endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("simple-to-weighted").WithProviderSpecific(providerSpecificWeight, "10"),
endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificRegion, "us-east-1"),
@ -969,6 +1044,7 @@ func TestAWSApplyChanges(t *testing.T) {
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "qux.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
endpoint.NewEndpoint("delete-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"),
endpoint.NewEndpoint("delete-test-geoproximity.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("geoproximity-delete").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"),
endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mailhost1.foo.elb.amazonaws.com"),
}
@ -1118,6 +1194,40 @@ func TestAWSApplyChanges(t *testing.T) {
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}},
},
{
Name: aws.String("create-test-geoproximity-region.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeA,
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}},
SetIdentifier: aws.String("geoproximity-region"),
GeoProximityLocation: &route53types.GeoProximityLocation{
AWSRegion: aws.String("us-west-2"),
Bias: aws.Int32(10),
},
},
{
Name: aws.String("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeA,
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}},
SetIdentifier: aws.String("geoproximity-update"),
GeoProximityLocation: &route53types.GeoProximityLocation{
LocalZoneGroup: aws.String("usw2-phx2-az1"),
},
},
{
Name: aws.String("create-test-geoproximity-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do."),
Type: route53types.RRTypeA,
TTL: aws.Int64(defaultTTL),
ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}},
SetIdentifier: aws.String("geoproximity-coordinates"),
GeoProximityLocation: &route53types.GeoProximityLocation{
Coordinates: &route53types.Coordinates{
Latitude: aws.String("60"),
Longitude: aws.String("60"),
},
},
},
})
validateRecords(t, listAWSRecords(t, provider.clients[defaultAWSProfile], "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []route53types.ResourceRecordSet{
{
@ -1902,7 +2012,7 @@ func validateEndpoints(t *testing.T, provider *AWSProvider, endpoints []*endpoin
normalized, err := provider.AdjustEndpoints(endpoints)
assert.NoError(t, err)
assert.True(t, testutils.SameEndpoints(normalized, expected), "actual and normalized endpoints don't match. %+v:%+v", endpoints, normalized)
assert.True(t, testutils.SameEndpoints(normalized, expected), "normalized and expected endpoints don't match. %+v:%+v", normalized, expected)
}
func validateAWSZones(t *testing.T, zones map[string]*route53types.HostedZone, expected map[string]*route53types.HostedZone) {
@ -2370,3 +2480,373 @@ func TestConvertOctalToAscii(t *testing.T) {
})
}
}
func TestGeoProximityWithAWSRegion(t *testing.T) {
tests := []struct {
name string
region string
hasRegion bool
expectedSet bool
expectedRegion string
}{
{
name: "valid AWS region",
region: "us-west-2",
hasRegion: true,
expectedSet: true,
expectedRegion: "us-west-2",
},
{
name: "another valid AWS region",
region: "eu-central-1",
hasRegion: true,
expectedSet: true,
expectedRegion: "eu-central-1",
},
{
name: "empty region string",
region: "",
hasRegion: true,
expectedSet: true,
expectedRegion: "",
},
{
name: "no region property set",
region: "",
hasRegion: false,
expectedSet: false,
expectedRegion: "",
},
{
name: "region with special characters",
region: "us-gov-west-1",
hasRegion: true,
expectedSet: true,
expectedRegion: "us-gov-west-1",
},
{
name: "region with numbers",
region: "ap-southeast-3",
hasRegion: true,
expectedSet: true,
expectedRegion: "ap-southeast-3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ep := &endpoint.Endpoint{
DNSName: "test.example.com",
SetIdentifier: "test-set",
}
if tt.hasRegion {
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion, tt.region)
}
gp := newGeoProximity(ep)
result := gp.withAWSRegion()
assert.Equal(t, tt.expectedSet, result.isSet)
if tt.expectedSet {
assert.NotNil(t, result.location.AWSRegion)
assert.Equal(t, tt.expectedRegion, *result.location.AWSRegion)
} else {
assert.Nil(t, result.location.AWSRegion)
}
// Verify the method returns the same instance for chaining
assert.Equal(t, gp, result)
})
}
}
func TestGeoProximityWithLocalZoneGroup(t *testing.T) {
tests := []struct {
name string
localZoneGroup string
hasLocalZoneGroup bool
expectedSet bool
expectedLocalZoneGroup string
}{
{
name: "valid local zone group",
localZoneGroup: "usw2-lax1-az1",
hasLocalZoneGroup: true,
expectedSet: true,
expectedLocalZoneGroup: "usw2-lax1-az1",
},
{
name: "empty local zone group",
localZoneGroup: "",
hasLocalZoneGroup: true,
expectedSet: true,
expectedLocalZoneGroup: "",
},
{
name: "no local zone group property",
localZoneGroup: "",
hasLocalZoneGroup: false,
expectedSet: false,
expectedLocalZoneGroup: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ep := &endpoint.Endpoint{
DNSName: "test.example.com",
SetIdentifier: "test-set",
}
if tt.hasLocalZoneGroup {
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup, tt.localZoneGroup)
}
gp := newGeoProximity(ep)
result := gp.withLocalZoneGroup()
assert.Equal(t, tt.expectedSet, result.isSet)
if tt.expectedSet {
assert.NotNil(t, result.location.LocalZoneGroup)
assert.Equal(t, tt.expectedLocalZoneGroup, *result.location.LocalZoneGroup)
} else {
assert.Nil(t, result.location.LocalZoneGroup)
}
// Verify method returns same instance for chaining
assert.Equal(t, gp, result)
})
}
}
func TestGeoProximityWithCoordinates(t *testing.T) {
tests := []struct {
name string
coordinates string
expectedSet bool
expectedLat string
expectedLong string
shouldHaveCoords bool
}{
{
name: "valid coordinates",
coordinates: "45.0,90.0",
expectedSet: true,
expectedLat: "45.0",
expectedLong: "90.0",
shouldHaveCoords: true,
},
{
name: "edge case min coordinates",
coordinates: "-90.0,-180.0",
expectedSet: true,
expectedLat: "-90.0",
expectedLong: "-180.0",
shouldHaveCoords: true,
},
{
name: "edge case max coordinates",
coordinates: "90.0,180.0",
expectedSet: true,
expectedLat: "90.0",
expectedLong: "180.0",
shouldHaveCoords: true,
},
{
name: "invalid latitude too high",
coordinates: "91.0,90.0",
expectedSet: false,
shouldHaveCoords: false,
},
{
name: "invalid longitude too low",
coordinates: "45.0,-181.0",
expectedSet: false,
shouldHaveCoords: false,
},
{
name: "invalid format - single value",
coordinates: "45.0",
expectedSet: false,
shouldHaveCoords: false,
},
{
name: "invalid format - three values",
coordinates: "45.0,90.0,10.0",
expectedSet: false,
shouldHaveCoords: false,
},
{
name: "invalid format - non-numeric",
coordinates: "abc,def",
expectedSet: false,
shouldHaveCoords: false,
},
{
name: "no coordinates property",
coordinates: "",
expectedSet: false,
shouldHaveCoords: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ep := &endpoint.Endpoint{}
if tt.coordinates != "" {
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates, tt.coordinates)
}
gp := newGeoProximity(ep)
result := gp.withCoordinates()
assert.Equal(t, tt.expectedSet, result.isSet)
if tt.shouldHaveCoords {
assert.NotNil(t, result.location.Coordinates)
assert.Equal(t, tt.expectedLat, *result.location.Coordinates.Latitude)
assert.Equal(t, tt.expectedLong, *result.location.Coordinates.Longitude)
} else {
assert.Nil(t, result.location.Coordinates)
}
})
}
}
func TestGeoProximityWithBias(t *testing.T) {
tests := []struct {
name string
bias string
hasBias bool
expectedSet bool
expectedBias int32
}{
{
name: "valid positive bias",
bias: "10",
hasBias: true,
expectedSet: true,
expectedBias: 10,
},
{
name: "valid negative bias",
bias: "-5",
hasBias: true,
expectedSet: true,
expectedBias: -5,
},
{
name: "zero bias",
bias: "0",
hasBias: true,
expectedSet: true,
expectedBias: 0,
},
{
name: "large positive bias",
bias: "99",
hasBias: true,
expectedSet: true,
expectedBias: 99,
},
{
name: "large negative bias",
bias: "-99",
hasBias: true,
expectedSet: true,
expectedBias: -99,
},
{
name: "invalid bias - non-numeric",
bias: "abc",
hasBias: true,
expectedSet: true,
expectedBias: 0, // defaults to 0 on error
},
{
name: "invalid bias - float",
bias: "10.5",
hasBias: true,
expectedSet: true,
expectedBias: 0, // defaults to 0 on error
},
{
name: "empty bias string",
bias: "",
hasBias: true,
expectedSet: true,
expectedBias: 0, // defaults to 0 on error
},
{
name: "no bias property",
bias: "",
hasBias: false,
expectedSet: false,
expectedBias: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ep := &endpoint.Endpoint{
DNSName: "test.example.com",
SetIdentifier: "test-set",
}
if tt.hasBias {
ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationBias, tt.bias)
}
gp := newGeoProximity(ep)
result := gp.withBias()
assert.Equal(t, tt.expectedSet, result.isSet)
if tt.expectedSet {
assert.NotNil(t, result.location.Bias)
assert.Equal(t, tt.expectedBias, *result.location.Bias)
} else {
assert.Nil(t, result.location.Bias)
}
// Verify method returns same instance for chaining
assert.Equal(t, gp, result)
})
}
}
func TestAWSProvider_createUpdateChanges_NewMoreThanOld(t *testing.T) {
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"foo.bar."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), true, false, nil)
oldEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("record1.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "1.1.1.1"),
nil,
}
newEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("record1.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "1.1.1.1"),
endpoint.NewEndpointWithTTL("record2.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "2.2.2.2"),
endpoint.NewEndpointWithTTL("record3.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "3.3.3.3"),
}
changes := provider.createUpdateChanges(newEndpoints, oldEndpoints)
// record2 should be created, record1 should be upserted
var creates, upserts, deletes int
for _, c := range changes {
switch c.Action {
case route53types.ChangeActionCreate:
creates++
case route53types.ChangeActionUpsert:
upserts++
case route53types.ChangeActionDelete:
deletes++
}
}
require.Equal(t, 0, creates, "should create the extra new endpoint")
require.Equal(t, 1, upserts, "should upsert the matching endpoint")
require.Equal(t, 0, deletes, "should not delete anything")
}

View File

@ -20,16 +20,16 @@ import (
"context"
"fmt"
"net/http"
"strings"
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"
extdnshttp "sigs.k8s.io/external-dns/pkg/http"
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
)
@ -84,12 +84,7 @@ func newV2Config(awsConfig AWSSessionConfig) (awsv2.Config, 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.WithHTTPClient(extdnshttp.NewInstrumentedClient(&http.Client{})),
config.WithSharedConfigProfile(awsConfig.Profile),
}

View File

@ -130,12 +130,14 @@ func awsTags(tags map[string]string) []sdtypes.Tag {
}
// Records returns list of all endpoints.
func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
func (p *AWSSDProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
namespaces, err := p.ListNamespaces(ctx)
if err != nil {
return nil, err
}
endpoints := make([]*endpoint.Endpoint, 0)
for _, ns := range namespaces {
services, err := p.ListServicesByNamespaceID(ctx, ns.Id)
if err != nil {
@ -244,12 +246,14 @@ func (p *AWSSDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes)
return nil
}
func (p *AWSSDProvider) updatesToCreates(changes *plan.Changes) (creates []*endpoint.Endpoint, deletes []*endpoint.Endpoint) {
func (p *AWSSDProvider) updatesToCreates(changes *plan.Changes) ([]*endpoint.Endpoint, []*endpoint.Endpoint) {
updateNewMap := map[string]*endpoint.Endpoint{}
for _, e := range changes.UpdateNew {
updateNewMap[e.DNSName] = e
}
var creates, deletes []*endpoint.Endpoint
for _, old := range changes.UpdateOld {
current := updateNewMap[old.DNSName]
@ -618,12 +622,10 @@ func matchingNamespaces(hostname string, namespaces []*sdtypes.NamespaceSummary)
return matchingNamespaces
}
// parse hostname to namespace (domain) and service
func (p *AWSSDProvider) parseHostname(hostname string) (namespace string, service string) {
// parseHostname parse hostname to namespace (domain) and service
func (p *AWSSDProvider) parseHostname(hostname string) (string, string) {
parts := strings.Split(hostname, ".")
service = parts[0]
namespace = strings.Join(parts[1:], ".")
return
return strings.Join(parts[1:], "."), parts[0]
}
// determine service routing policy based on endpoint type

View File

@ -106,12 +106,14 @@ func NewAzureProvider(configFile string, domainFilter *endpoint.DomainFilter, zo
// Records gets the current records.
//
// Returns the current records or an error if the operation failed.
func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
func (p *AzureProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
zones, err := p.zones(ctx)
if err != nil {
return nil, err
}
endpoints := make([]*endpoint.Endpoint, 0)
for _, zone := range zones {
pager := p.recordSetsClient.NewListAllByDNSZonePager(p.resourceGroup, *zone.Name, &dns.RecordSetsClientListAllByDNSZoneOptions{Top: nil})
for pager.More() {

View File

@ -101,7 +101,7 @@ func NewAzurePrivateDNSProvider(configFile string, domainFilter *endpoint.Domain
// Records gets the current records.
//
// Returns the current records or an error if the operation failed.
func (p *AzurePrivateDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
func (p *AzurePrivateDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
zones, err := p.zones(ctx)
if err != nil {
return nil, err
@ -109,12 +109,13 @@ func (p *AzurePrivateDNSProvider) Records(ctx context.Context) (endpoints []*end
log.Debugf("Retrieving Azure Private DNS Records for resource group '%s'", p.resourceGroup)
endpoints := make([]*endpoint.Endpoint, 0)
for _, zone := range zones {
pager := p.recordSetsClient.NewListPager(p.resourceGroup, *zone.Name, &privatedns.RecordSetsClientListOptions{Top: nil})
for pager.More() {
nextResult, err := pager.NextPage(ctx)
if err != nil {
return nil, provider.NewSoftError(fmt.Errorf("failed to fetch dns records: %w", err))
return nil, provider.NewSoftErrorf("failed to fetch dns records: %v", err)
}
for _, recordSet := range nextResult.Value {

View File

@ -31,7 +31,6 @@ import (
var (
cachedRecordsCallsTotal = metrics.NewCounterVecWithOpts(
prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "provider",
Name: "cache_records_calls",
Help: "Number of calls to the provider cache Records list.",
@ -42,7 +41,6 @@ var (
)
cachedApplyChangesCallsTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "provider",
Name: "cache_apply_changes_calls",
Help: "Number of calls to the provider cache ApplyChanges.",

View File

@ -1169,7 +1169,7 @@ func TestCivoChangesEmpty(t *testing.T) {
// This function is an adapted copy of the testify package's ElementsMatch function with the
// call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to
// other structs. It also ignores ordering when comparing unlike cmp.Equal.
func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) {
func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) bool {
if listA == nil && listB == nil {
return true
} else if listA == nil {

View File

@ -29,6 +29,9 @@ import (
"strings"
"github.com/cloudflare/cloudflare-go"
cloudflarev4 "github.com/cloudflare/cloudflare-go/v4"
"github.com/cloudflare/cloudflare-go/v4/addressing"
"github.com/cloudflare/cloudflare-go/v4/option"
log "github.com/sirupsen/logrus"
"golang.org/x/net/publicsuffix"
@ -109,17 +112,18 @@ type cloudFlareDNS interface {
CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error)
DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error
UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error
ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error)
CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error
UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error
DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error
ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse]
CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error
UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error
DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error
CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error)
DeleteCustomHostname(ctx context.Context, zoneID string, customHostnameID string) error
CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error)
}
type zoneService struct {
service *cloudflare.API
service *cloudflare.API
serviceV4 *cloudflarev4.Client
}
func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
@ -257,7 +261,7 @@ type CloudFlareProvider struct {
type cloudFlareChange struct {
Action changeAction
ResourceRecord cloudflare.DNSRecord
RegionalHostname cloudflare.RegionalHostname
RegionalHostname regionalHostname
CustomHostnames map[string]cloudflare.CustomHostname
CustomHostnamesPrev []string
}
@ -328,8 +332,9 @@ func NewCloudFlareProvider(
) (*CloudFlareProvider, error) {
// initialize via chosen auth method and returns new API object
var (
config *cloudflare.API
err error
config *cloudflare.API
configV4 *cloudflarev4.Client
err error
)
if os.Getenv("CF_API_TOKEN") != "" {
token := os.Getenv("CF_API_TOKEN")
@ -341,8 +346,15 @@ func NewCloudFlareProvider(
token = strings.TrimSpace(string(tokenBytes))
}
config, err = cloudflare.NewWithAPIToken(token)
configV4 = cloudflarev4.NewClient(
option.WithAPIToken(token),
)
} else {
config, err = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL"))
configV4 = cloudflarev4.NewClient(
option.WithAPIKey(os.Getenv("CF_API_KEY")),
option.WithAPIEmail(os.Getenv("CF_API_EMAIL")),
)
}
if err != nil {
return nil, fmt.Errorf("failed to initialize cloudflare provider: %w", err)
@ -353,7 +365,7 @@ func NewCloudFlareProvider(
}
return &CloudFlareProvider{
Client: zoneService{config},
Client: zoneService{config, configV4},
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
proxiedByDefault: proxiedByDefault,
@ -689,12 +701,12 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
return fmt.Errorf("failed to build desired regional hostnames: %w", err)
}
if len(desiredRegionalHostnames) > 0 {
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, resourceContainer)
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, zoneID)
if err != nil {
return fmt.Errorf("could not fetch regional hostnames from zone, %w", err)
}
regionalHostnamesChanges := regionalHostnamesChanges(desiredRegionalHostnames, regionalHostnames)
if !p.submitRegionalHostnameChanges(ctx, regionalHostnamesChanges, resourceContainer) {
if !p.submitRegionalHostnameChanges(ctx, zoneID, regionalHostnamesChanges) {
failedChange = true
}
}

View File

@ -22,7 +22,9 @@ import (
"maps"
"slices"
cloudflare "github.com/cloudflare/cloudflare-go"
cloudflarev4 "github.com/cloudflare/cloudflare-go/v4"
"github.com/cloudflare/cloudflare-go/v4/addressing"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
@ -40,54 +42,75 @@ var recordTypeRegionalHostnameSupported = map[string]bool{
"CNAME": true,
}
// RegionalHostnamesMap is a map of regional hostnames keyed by hostname.
type RegionalHostnamesMap map[string]cloudflare.RegionalHostname
type regionalHostname struct {
hostname string
regionKey string
}
// regionalHostnamesMap is a map of regional hostnames keyed by hostname.
type regionalHostnamesMap map[string]regionalHostname
type regionalHostnameChange struct {
action changeAction
cloudflare.RegionalHostname
regionalHostname
}
func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) {
return z.service.ListDataLocalizationRegionalHostnames(ctx, rc, rp)
func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] {
return z.serviceV4.Addressing.RegionalHostnames.ListAutoPaging(ctx, params)
}
func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error {
_, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp)
func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error {
_, err := z.serviceV4.Addressing.RegionalHostnames.New(ctx, params)
return err
}
func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error {
_, err := z.service.UpdateDataLocalizationRegionalHostname(ctx, rc, rp)
func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error {
_, err := z.serviceV4.Addressing.RegionalHostnames.Edit(ctx, hostname, params)
return err
}
func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error {
return z.service.DeleteDataLocalizationRegionalHostname(ctx, rc, hostname)
func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error {
_, err := z.serviceV4.Addressing.RegionalHostnames.Delete(ctx, hostname, params)
return err
}
// listDataLocalizationRegionalHostnamesParams is a function that returns the appropriate RegionalHostname List Param based on the zoneID
func listDataLocalizationRegionalHostnamesParams(zoneID string) addressing.RegionalHostnameListParams {
return addressing.RegionalHostnameListParams{
ZoneID: cloudflarev4.F(zoneID),
}
}
// createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
func createDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.CreateDataLocalizationRegionalHostnameParams {
return cloudflare.CreateDataLocalizationRegionalHostnameParams{
Hostname: rhc.Hostname,
RegionKey: rhc.RegionKey,
func createDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameNewParams {
return addressing.RegionalHostnameNewParams{
ZoneID: cloudflarev4.F(zoneID),
Hostname: cloudflarev4.F(rhc.hostname),
RegionKey: cloudflarev4.F(rhc.regionKey),
}
}
// updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
func updateDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams {
return cloudflare.UpdateDataLocalizationRegionalHostnameParams{
Hostname: rhc.Hostname,
RegionKey: rhc.RegionKey,
func updateDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameEditParams {
return addressing.RegionalHostnameEditParams{
ZoneID: cloudflarev4.F(zoneID),
RegionKey: cloudflarev4.F(rhc.regionKey),
}
}
// deleteDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
func deleteDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameDeleteParams {
return addressing.RegionalHostnameDeleteParams{
ZoneID: cloudflarev4.F(zoneID),
}
}
// submitRegionalHostnameChanges applies a set of regional hostname changes, returns false if at least one fails
func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, rhChanges []regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool {
func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, zoneID string, rhChanges []regionalHostnameChange) bool {
failedChange := false
for _, rhChange := range rhChanges {
if !p.submitRegionalHostnameChange(ctx, rhChange, resourceContainer) {
if !p.submitRegionalHostnameChange(ctx, zoneID, rhChange) {
failedChange = true
}
}
@ -96,12 +119,12 @@ func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context,
}
// submitRegionalHostnameChange applies a single regional hostname change, returns false if it fails
func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, rhChange regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool {
func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, zoneID string, rhChange regionalHostnameChange) bool {
changeLog := log.WithFields(log.Fields{
"hostname": rhChange.Hostname,
"region_key": rhChange.RegionKey,
"action": rhChange.action,
"zone": resourceContainer.Identifier,
"hostname": rhChange.hostname,
"region_key": rhChange.regionKey,
"action": rhChange.action.String(),
"zone": zoneID,
})
if p.DryRun {
changeLog.Debug("Dry run: skipping regional hostname change", rhChange.action)
@ -110,21 +133,22 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r
switch rhChange.action {
case cloudFlareCreate:
changeLog.Debug("Creating regional hostname")
regionalHostnameParam := createDataLocalizationRegionalHostnameParams(rhChange)
if err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil {
params := createDataLocalizationRegionalHostnameParams(zoneID, rhChange)
if err := p.Client.CreateDataLocalizationRegionalHostname(ctx, params); err != nil {
changeLog.Errorf("failed to create regional hostname: %v", err)
return false
}
case cloudFlareUpdate:
changeLog.Debug("Updating regional hostname")
regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(rhChange)
if err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil {
params := updateDataLocalizationRegionalHostnameParams(zoneID, rhChange)
if err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, rhChange.hostname, params); err != nil {
changeLog.Errorf("failed to update regional hostname: %v", err)
return false
}
case cloudFlareDelete:
changeLog.Debug("Deleting regional hostname")
if err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, rhChange.Hostname); err != nil {
params := deleteDataLocalizationRegionalHostnameParams(zoneID, rhChange)
if err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, rhChange.hostname, params); err != nil {
changeLog.Errorf("failed to delete regional hostname: %v", err)
return false
}
@ -132,34 +156,41 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r
return true
}
func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, resourceContainer *cloudflare.ResourceContainer) (RegionalHostnamesMap, error) {
rhs, err := p.Client.ListDataLocalizationRegionalHostnames(ctx, resourceContainer, cloudflare.ListDataLocalizationRegionalHostnamesParams{})
if err != nil {
return nil, convertCloudflareError(err)
// listDataLocalisationRegionalHostnames fetches the current regional hostnames for the given zone ID.
//
// It returns a map of hostnames to regional hostnames, or an error if the request fails.
func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, zoneID string) (regionalHostnamesMap, error) {
params := listDataLocalizationRegionalHostnamesParams(zoneID)
iter := p.Client.ListDataLocalizationRegionalHostnames(ctx, params)
rhsMap := make(regionalHostnamesMap)
for rh := range autoPagerIterator(iter) {
rhsMap[rh.Hostname] = regionalHostname{
hostname: rh.Hostname,
regionKey: rh.RegionKey,
}
}
rhsMap := make(RegionalHostnamesMap)
for _, r := range rhs {
rhsMap[r.Hostname] = r
if iter.Err() != nil {
return nil, convertCloudflareError(iter.Err())
}
return rhsMap, nil
}
// regionalHostname returns a RegionalHostname for the given endpoint.
// regionalHostname returns a regionalHostname for the given endpoint.
//
// If the regional services feature is not enabled or the record type does not support regional hostnames,
// it returns an empty RegionalHostname.
// it returns an empty regionalHostname.
// If the endpoint has a specific region key set, it uses that; otherwise, it defaults to the region key configured in the provider.
func (p *CloudFlareProvider) regionalHostname(ep *endpoint.Endpoint) cloudflare.RegionalHostname {
func (p *CloudFlareProvider) regionalHostname(ep *endpoint.Endpoint) regionalHostname {
if !p.RegionalServicesConfig.Enabled || !recordTypeRegionalHostnameSupported[ep.RecordType] {
return cloudflare.RegionalHostname{}
return regionalHostname{}
}
regionKey := p.RegionalServicesConfig.RegionKey
if epRegionKey, exists := ep.GetProviderSpecificProperty(annotations.CloudflareRegionKey); exists {
regionKey = epRegionKey
}
return cloudflare.RegionalHostname{
Hostname: ep.DNSName,
RegionKey: regionKey,
return regionalHostname{
hostname: ep.DNSName,
regionKey: regionKey,
}
}
@ -185,14 +216,14 @@ func (p *CloudFlareProvider) addEnpointsProviderSpecificRegionKeyProperty(ctx co
return nil
}
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, cloudflare.ZoneIdentifier(zoneID))
regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, zoneID)
if err != nil {
return err
}
for _, ep := range supportedEndpoints {
if rh, found := regionalHostnames[ep.DNSName]; found {
ep.SetProviderSpecificProperty(annotations.CloudflareRegionKey, rh.RegionKey)
ep.SetProviderSpecificProperty(annotations.CloudflareRegionKey, rh.regionKey)
}
}
return nil
@ -203,67 +234,67 @@ func (p *CloudFlareProvider) addEnpointsProviderSpecificRegionKeyProperty(ctx co
// If there is a delete and a create or update action for the same hostname,
// The create or update takes precedence.
// Returns an error for conflicting region keys.
func desiredRegionalHostnames(changes []*cloudFlareChange) ([]cloudflare.RegionalHostname, error) {
rhs := make(map[string]cloudflare.RegionalHostname)
func desiredRegionalHostnames(changes []*cloudFlareChange) ([]regionalHostname, error) {
rhs := make(map[string]regionalHostname)
for _, change := range changes {
if change.RegionalHostname.Hostname == "" {
if change.RegionalHostname.hostname == "" {
continue
}
rh, found := rhs[change.RegionalHostname.Hostname]
rh, found := rhs[change.RegionalHostname.hostname]
if !found {
if change.Action == cloudFlareDelete {
rhs[change.RegionalHostname.Hostname] = cloudflare.RegionalHostname{
Hostname: change.RegionalHostname.Hostname,
RegionKey: "", // Indicate that this regional hostname should not exists
rhs[change.RegionalHostname.hostname] = regionalHostname{
hostname: change.RegionalHostname.hostname,
regionKey: "", // Indicate that this regional hostname should not exists
}
continue
}
rhs[change.RegionalHostname.Hostname] = change.RegionalHostname
rhs[change.RegionalHostname.hostname] = change.RegionalHostname
continue
}
if change.Action == cloudFlareDelete {
// A previous regional hostname exists so we can skip this delete action
continue
}
if rh.RegionKey == "" {
if rh.regionKey == "" {
// If the existing regional hostname has no region key, we can overwrite it
rhs[change.RegionalHostname.Hostname] = change.RegionalHostname
rhs[change.RegionalHostname.hostname] = change.RegionalHostname
continue
}
if rh.RegionKey != change.RegionalHostname.RegionKey {
return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.Hostname, rh.RegionKey, change.RegionalHostname.RegionKey)
if rh.regionKey != change.RegionalHostname.regionKey {
return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.hostname, rh.regionKey, change.RegionalHostname.regionKey)
}
}
return slices.Collect(maps.Values(rhs)), nil
}
// regionalHostnamesChanges build a list of changes needed to synchronize the current regional hostnames state with the desired state.
func regionalHostnamesChanges(desired []cloudflare.RegionalHostname, regionalHostnames RegionalHostnamesMap) []regionalHostnameChange {
func regionalHostnamesChanges(desired []regionalHostname, regionalHostnames regionalHostnamesMap) []regionalHostnameChange {
changes := make([]regionalHostnameChange, 0)
for _, rh := range desired {
current, found := regionalHostnames[rh.Hostname]
if rh.RegionKey == "" {
current, found := regionalHostnames[rh.hostname]
if rh.regionKey == "" {
// If the region key is empty, we don't want a regional hostname
if !found {
continue
}
changes = append(changes, regionalHostnameChange{
action: cloudFlareDelete,
RegionalHostname: rh,
regionalHostname: rh,
})
continue
}
if !found {
changes = append(changes, regionalHostnameChange{
action: cloudFlareCreate,
RegionalHostname: rh,
regionalHostname: rh,
})
continue
}
if rh.RegionKey != current.RegionKey {
if rh.regionKey != current.regionKey {
changes = append(changes, regionalHostnameChange{
action: cloudFlareUpdate,
RegionalHostname: rh,
regionalHostname: rh,
})
}
}

View File

@ -24,6 +24,7 @@ import (
"testing"
cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/cloudflare-go/v4/addressing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@ -33,57 +34,67 @@ import (
"sigs.k8s.io/external-dns/plan"
)
func (m *mockCloudFlareClient) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) {
if strings.Contains(rc.Identifier, "rherror") {
return nil, fmt.Errorf("failed to list regional hostnames")
func (m *mockCloudFlareClient) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] {
zoneID := params.ZoneID.Value
if strings.Contains(zoneID, "rherror") {
return &mockAutoPager[addressing.RegionalHostnameListResponse]{err: fmt.Errorf("failed to list regional hostnames")}
}
results := make([]addressing.RegionalHostnameListResponse, 0, len(m.regionalHostnames[zoneID]))
for _, rh := range m.regionalHostnames[zoneID] {
results = append(results, addressing.RegionalHostnameListResponse{
Hostname: rh.hostname,
RegionKey: rh.regionKey,
})
}
return &mockAutoPager[addressing.RegionalHostnameListResponse]{
items: results,
}
return m.regionalHostnames[rc.Identifier], nil
}
func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error {
if strings.Contains(rp.Hostname, "rherror") {
func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error {
if strings.Contains(params.Hostname.Value, "rherror") {
return fmt.Errorf("failed to create regional hostname")
}
m.Actions = append(m.Actions, MockAction{
Name: "CreateDataLocalizationRegionalHostname",
ZoneId: rc.Identifier,
ZoneId: params.ZoneID.Value,
RecordId: "",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: rp.Hostname,
RegionKey: rp.RegionKey,
RegionalHostname: regionalHostname{
hostname: params.Hostname.Value,
regionKey: params.RegionKey.Value,
},
})
return nil
}
func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error {
if strings.Contains(rp.Hostname, "rherror") {
func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error {
if strings.Contains(hostname, "rherror") {
return fmt.Errorf("failed to update regional hostname")
}
m.Actions = append(m.Actions, MockAction{
Name: "UpdateDataLocalizationRegionalHostname",
ZoneId: rc.Identifier,
ZoneId: params.ZoneID.Value,
RecordId: "",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: rp.Hostname,
RegionKey: rp.RegionKey,
RegionalHostname: regionalHostname{
hostname: hostname,
regionKey: params.RegionKey.Value,
},
})
return nil
}
func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error {
func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error {
if strings.Contains(hostname, "rherror") {
return fmt.Errorf("failed to delete regional hostname")
}
m.Actions = append(m.Actions, MockAction{
Name: "DeleteDataLocalizationRegionalHostname",
ZoneId: rc.Identifier,
ZoneId: params.ZoneID.Value,
RecordId: "",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: hostname,
RegionalHostname: regionalHostname{
hostname: hostname,
},
})
return nil
@ -93,14 +104,14 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
tests := []struct {
name string
records map[string]cloudflare.DNSRecord
regionalHostnames []cloudflare.RegionalHostname
regionalHostnames []regionalHostname
endpoints []*endpoint.Endpoint
want []MockAction
}{
{
name: "create",
records: map[string]cloudflare.DNSRecord{},
regionalHostnames: []cloudflare.RegionalHostname{},
regionalHostnames: []regionalHostname{},
endpoints: []*endpoint.Endpoint{
{
RecordType: "A",
@ -131,9 +142,9 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
{
Name: "CreateDataLocalizationRegionalHostname",
ZoneId: "001",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "create.bar.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "create.bar.com",
regionKey: "eu",
},
},
},
@ -150,10 +161,10 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
Proxied: proxyDisabled,
},
},
regionalHostnames: []cloudflare.RegionalHostname{
regionalHostnames: []regionalHostname{
{
Hostname: "update.bar.com",
RegionKey: "us",
hostname: "update.bar.com",
regionKey: "us",
},
},
endpoints: []*endpoint.Endpoint{
@ -186,9 +197,9 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
{
Name: "UpdateDataLocalizationRegionalHostname",
ZoneId: "001",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "update.bar.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "update.bar.com",
regionKey: "eu",
},
},
},
@ -205,10 +216,10 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
Proxied: proxyDisabled,
},
},
regionalHostnames: []cloudflare.RegionalHostname{
regionalHostnames: []regionalHostname{
{
Hostname: "delete.bar.com",
RegionKey: "us",
hostname: "delete.bar.com",
regionKey: "us",
},
},
endpoints: []*endpoint.Endpoint{},
@ -222,8 +233,8 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
{
Name: "DeleteDataLocalizationRegionalHostname",
ZoneId: "001",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "delete.bar.com",
RegionalHostname: regionalHostname{
hostname: "delete.bar.com",
},
},
},
@ -240,10 +251,10 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
Proxied: proxyDisabled,
},
},
regionalHostnames: []cloudflare.RegionalHostname{
regionalHostnames: []regionalHostname{
{
Hostname: "nochange.bar.com",
RegionKey: "eu",
hostname: "nochange.bar.com",
regionKey: "eu",
},
},
endpoints: []*endpoint.Endpoint{
@ -273,7 +284,7 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
Records: map[string]map[string]cloudflare.DNSRecord{
"001": tt.records,
},
regionalHostnames: map[string][]cloudflare.RegionalHostname{
regionalHostnames: map[string][]regionalHostname{
"001": tt.regionalHostnames,
},
},
@ -323,9 +334,9 @@ func TestCloudflareRegionalHostnameDefaults(t *testing.T) {
{
Name: "CreateDataLocalizationRegionalHostname",
ZoneId: "001",
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "bar.com",
RegionKey: "us",
RegionalHostname: regionalHostname{
hostname: "bar.com",
regionKey: "us",
},
},
},
@ -341,7 +352,7 @@ func Test_regionalHostname(t *testing.T) {
tests := []struct {
name string
args args
want cloudflare.RegionalHostname
want regionalHostname
}{
{
name: "no region key",
@ -355,9 +366,9 @@ func Test_regionalHostname(t *testing.T) {
RegionKey: "",
},
},
want: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "",
want: regionalHostname{
hostname: "example.com",
regionKey: "",
},
},
{
@ -372,9 +383,9 @@ func Test_regionalHostname(t *testing.T) {
RegionKey: "us",
},
},
want: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "us",
want: regionalHostname{
hostname: "example.com",
regionKey: "us",
},
},
{
@ -395,9 +406,9 @@ func Test_regionalHostname(t *testing.T) {
RegionKey: "us",
},
},
want: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
want: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
{
@ -418,9 +429,9 @@ func Test_regionalHostname(t *testing.T) {
RegionKey: "us",
},
},
want: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "",
want: regionalHostname{
hostname: "example.com",
regionKey: "",
},
},
{
@ -441,7 +452,7 @@ func Test_regionalHostname(t *testing.T) {
RegionKey: "us",
},
},
want: cloudflare.RegionalHostname{},
want: regionalHostname{},
},
{
name: "disabled",
@ -460,9 +471,9 @@ func Test_regionalHostname(t *testing.T) {
Enabled: false,
},
},
want: cloudflare.RegionalHostname{
Hostname: "",
RegionKey: "",
want: regionalHostname{
hostname: "",
regionKey: "",
},
},
}
@ -479,7 +490,7 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
tests := []struct {
name string
changes []*cloudFlareChange
want []cloudflare.RegionalHostname
want []regionalHostname
wantErr bool
}{
{
@ -501,23 +512,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
changes: []*cloudFlareChange{
{
Action: cloudFlareCreate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
{
Action: cloudFlareUpdate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
},
want: []cloudflare.RegionalHostname{
want: []regionalHostname{
{
Hostname: "example.com",
RegionKey: "eu",
hostname: "example.com",
regionKey: "eu",
},
},
wantErr: false,
@ -527,16 +538,16 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
changes: []*cloudFlareChange{
{
Action: cloudFlareCreate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
{
Action: cloudFlareUpdate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "us", // Different region key
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "us", // Different region key
},
},
},
@ -548,38 +559,38 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
changes: []*cloudFlareChange{
{
Action: cloudFlareCreate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example1.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example1.com",
regionKey: "eu",
},
},
{
Action: cloudFlareUpdate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example2.com",
RegionKey: "us",
RegionalHostname: regionalHostname{
hostname: "example2.com",
regionKey: "us",
},
},
{
Action: cloudFlareDelete,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example3.com",
RegionKey: "us",
RegionalHostname: regionalHostname{
hostname: "example3.com",
regionKey: "us",
},
},
},
want: []cloudflare.RegionalHostname{
want: []regionalHostname{
{
Hostname: "example1.com",
RegionKey: "eu",
hostname: "example1.com",
regionKey: "eu",
},
{
Hostname: "example2.com",
RegionKey: "us",
hostname: "example2.com",
regionKey: "us",
},
{
Hostname: "example3.com",
RegionKey: "",
hostname: "example3.com",
regionKey: "",
},
},
wantErr: false,
@ -589,16 +600,16 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
changes: []*cloudFlareChange{
{
Action: cloudFlareCreate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "", // Empty region key
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "", // Empty region key
},
},
},
want: []cloudflare.RegionalHostname{
want: []regionalHostname{
{
Hostname: "example.com",
RegionKey: "",
hostname: "example.com",
regionKey: "",
},
},
wantErr: false,
@ -608,23 +619,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
changes: []*cloudFlareChange{
{
Action: cloudFlareCreate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "", // Empty region key
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "", // Empty region key
},
},
{
Action: cloudFlareUpdate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
},
want: []cloudflare.RegionalHostname{
want: []regionalHostname{
{
Hostname: "example.com",
RegionKey: "eu",
hostname: "example.com",
regionKey: "eu",
},
},
wantErr: false,
@ -634,23 +645,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
changes: []*cloudFlareChange{
{
Action: cloudFlareCreate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
{
Action: cloudFlareUpdate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu", // Empty region key
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu", // Empty region key
},
},
},
want: []cloudflare.RegionalHostname{
want: []regionalHostname{
{
Hostname: "example.com",
RegionKey: "eu",
hostname: "example.com",
regionKey: "eu",
},
},
wantErr: false,
@ -660,23 +671,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
changes: []*cloudFlareChange{
{
Action: cloudFlareDelete,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
{
Action: cloudFlareCreate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
},
want: []cloudflare.RegionalHostname{
want: []regionalHostname{
{
Hostname: "example.com",
RegionKey: "eu",
hostname: "example.com",
regionKey: "eu",
},
},
wantErr: false,
@ -686,23 +697,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
changes: []*cloudFlareChange{
{
Action: cloudFlareCreate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
{
Action: cloudFlareDelete,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "eu",
RegionalHostname: regionalHostname{
hostname: "example.com",
regionKey: "eu",
},
},
},
want: []cloudflare.RegionalHostname{
want: []regionalHostname{
{
Hostname: "example.com",
RegionKey: "eu",
hostname: "example.com",
regionKey: "eu",
},
},
wantErr: false,
@ -718,10 +729,10 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
assert.NoError(t, err)
}
sort.Slice(got, func(i, j int) bool {
return got[i].Hostname < got[j].Hostname
return got[i].hostname < got[j].hostname
})
sort.Slice(tt.want, func(i, j int) bool {
return tt.want[i].Hostname < tt.want[j].Hostname
return tt.want[i].hostname < tt.want[j].hostname
})
assert.Equal(t, tt.want, got)
})
@ -731,74 +742,74 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) {
func Test_dataLocalizationRegionalHostnamesChanges(t *testing.T) {
tests := []struct {
name string
desired []cloudflare.RegionalHostname
regionalHostnames RegionalHostnamesMap
desired []regionalHostname
regionalHostnames regionalHostnamesMap
want []regionalHostnameChange
}{
{
name: "empty desired and current lists",
desired: []cloudflare.RegionalHostname{},
regionalHostnames: RegionalHostnamesMap{},
desired: []regionalHostname{},
regionalHostnames: regionalHostnamesMap{},
want: []regionalHostnameChange{},
},
{
name: "multiple changes",
desired: []cloudflare.RegionalHostname{
desired: []regionalHostname{
{
Hostname: "create.example.com",
RegionKey: "eu",
hostname: "create.example.com",
regionKey: "eu",
},
{
Hostname: "update.example.com",
RegionKey: "eu",
hostname: "update.example.com",
regionKey: "eu",
},
{
Hostname: "delete.example.com",
RegionKey: "",
hostname: "delete.example.com",
regionKey: "",
},
{
Hostname: "nochange.example.com",
RegionKey: "us",
hostname: "nochange.example.com",
regionKey: "us",
},
{
Hostname: "absent.example.com",
RegionKey: "",
hostname: "absent.example.com",
regionKey: "",
},
},
regionalHostnames: RegionalHostnamesMap{
"update.example.com": cloudflare.RegionalHostname{
Hostname: "update.example.com",
RegionKey: "us",
regionalHostnames: regionalHostnamesMap{
"update.example.com": regionalHostname{
hostname: "update.example.com",
regionKey: "us",
},
"delete.example.com": cloudflare.RegionalHostname{
Hostname: "delete.example.com",
RegionKey: "ap",
"delete.example.com": regionalHostname{
hostname: "delete.example.com",
regionKey: "ap",
},
"nochange.example.com": cloudflare.RegionalHostname{
Hostname: "nochange.example.com",
RegionKey: "us",
"nochange.example.com": regionalHostname{
hostname: "nochange.example.com",
regionKey: "us",
},
},
want: []regionalHostnameChange{
{
action: cloudFlareCreate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "create.example.com",
RegionKey: "eu",
regionalHostname: regionalHostname{
hostname: "create.example.com",
regionKey: "eu",
},
},
{
action: cloudFlareUpdate,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "update.example.com",
RegionKey: "eu",
regionalHostname: regionalHostname{
hostname: "update.example.com",
regionKey: "eu",
},
},
{
action: cloudFlareDelete,
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "delete.example.com",
RegionKey: "",
regionalHostname: regionalHostname{
hostname: "delete.example.com",
regionKey: "",
},
},
},
@ -834,7 +845,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
t.Parallel()
type fields struct {
Records map[string]cloudflare.DNSRecord
RegionalHostnames []cloudflare.RegionalHostname
RegionalHostnames []regionalHostname
RegionKey string
}
type args struct {
@ -869,7 +880,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
name: "create fails",
fields: fields{
Records: map[string]cloudflare.DNSRecord{},
RegionalHostnames: []cloudflare.RegionalHostname{},
RegionalHostnames: []regionalHostname{},
RegionKey: "us",
},
args: args{
@ -899,8 +910,8 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
Content: "127.0.0.1",
},
},
RegionalHostnames: []cloudflare.RegionalHostname{
{Hostname: "rherror.bar.com", RegionKey: "us"},
RegionalHostnames: []regionalHostname{
{hostname: "rherror.bar.com", regionKey: "us"},
},
RegionKey: "us",
},
@ -941,8 +952,8 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
Content: "127.0.0.1",
},
},
RegionalHostnames: []cloudflare.RegionalHostname{
{Hostname: "rherror.bar.com", RegionKey: "us"},
RegionalHostnames: []regionalHostname{
{hostname: "rherror.bar.com", regionKey: "us"},
},
RegionKey: "us",
},
@ -964,7 +975,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
name: "conflicting regional keys",
fields: fields{
Records: map[string]cloudflare.DNSRecord{},
RegionalHostnames: []cloudflare.RegionalHostname{},
RegionalHostnames: []regionalHostname{},
RegionKey: "us",
},
args: args{
@ -1008,7 +1019,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
Records: map[string]map[string]cloudflare.DNSRecord{
"001": records,
},
regionalHostnames: map[string][]cloudflare.RegionalHostname{
regionalHostnames: map[string][]regionalHostname{
"001": tt.fields.RegionalHostnames,
},
},
@ -1034,7 +1045,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
t.Parallel()
type fields struct {
Records map[string]cloudflare.DNSRecord
RegionalHostnames []cloudflare.RegionalHostname
RegionalHostnames []regionalHostname
RegionKey string
}
type args struct {
@ -1050,7 +1061,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
name: "create dry run",
fields: fields{
Records: map[string]cloudflare.DNSRecord{},
RegionalHostnames: []cloudflare.RegionalHostname{},
RegionalHostnames: []regionalHostname{},
RegionKey: "us",
},
args: args{
@ -1080,8 +1091,8 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
Content: "127.0.0.1",
},
},
RegionalHostnames: []cloudflare.RegionalHostname{
{Hostname: "foo.bar.com", RegionKey: "us"},
RegionalHostnames: []regionalHostname{
{hostname: "foo.bar.com", regionKey: "us"},
},
RegionKey: "us",
},
@ -1122,8 +1133,8 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
Content: "127.0.0.1",
},
},
RegionalHostnames: []cloudflare.RegionalHostname{
{Hostname: "foo.bar.com", RegionKey: "us"},
RegionalHostnames: []regionalHostname{
{hostname: "foo.bar.com", regionKey: "us"},
},
RegionKey: "us",
},
@ -1157,7 +1168,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
Records: map[string]map[string]cloudflare.DNSRecord{
"001": records,
},
regionalHostnames: map[string][]cloudflare.RegionalHostname{
regionalHostnames: map[string][]regionalHostname{
"001": tt.fields.RegionalHostnames,
},
},

View File

@ -49,7 +49,7 @@ type MockAction struct {
ZoneId string
RecordId string
RecordData cloudflare.DNSRecord
RegionalHostname cloudflare.RegionalHostname
RegionalHostname regionalHostname
}
type mockCloudFlareClient struct {
@ -61,7 +61,7 @@ type mockCloudFlareClient struct {
listZonesContextError error
dnsRecordsError error
customHostnames map[string][]cloudflare.CustomHostname
regionalHostnames map[string][]cloudflare.RegionalHostname
regionalHostnames map[string][]regionalHostname
}
var ExampleDomain = []cloudflare.DNSRecord{
@ -103,7 +103,7 @@ func NewMockCloudFlareClient() *mockCloudFlareClient {
"002": {},
},
customHostnames: map[string][]cloudflare.CustomHostname{},
regionalHostnames: map[string][]cloudflare.RegionalHostname{},
regionalHostnames: map[string][]regionalHostname{},
}
}
@ -1796,8 +1796,8 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
}
change, _ := p.newCloudFlareChange(cloudFlareCreate, ep, ep.Targets[0], nil)
if change.RegionalHostname.RegionKey != "us" {
t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.RegionKey)
if change.RegionalHostname.regionKey != "us" {
t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.regionKey)
}
var freeValidCommentBuilder strings.Builder
@ -2017,8 +2017,8 @@ func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) {
ID: "1234567890",
Content: "my-tunnel-guid-here.cfargotunnel.com",
},
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "my-domain-here.app",
RegionalHostname: regionalHostname{
hostname: "my-domain-here.app",
},
},
{
@ -2029,9 +2029,9 @@ func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) {
ID: "9876543210",
Content: "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/external-dns/my-domain-here-app",
},
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "my-domain-here.app",
RegionKey: "",
RegionalHostname: regionalHostname{
hostname: "my-domain-here.app",
regionKey: "",
},
},
}
@ -2080,8 +2080,8 @@ func TestCloudFlareProvider_submitChangesApex(t *testing.T) {
ID: "1234567890",
Content: "my-tunnel-guid-here.cfargotunnel.com",
},
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "@", // APEX record
RegionalHostname: regionalHostname{
hostname: "@", // APEX record
},
},
{
@ -2092,9 +2092,9 @@ func TestCloudFlareProvider_submitChangesApex(t *testing.T) {
ID: "9876543210",
Content: "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/external-dns/my-domain-here-app",
},
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "@", // APEX record
RegionKey: "",
RegionalHostname: regionalHostname{
hostname: "@", // APEX record
regionKey: "",
},
},
}

View File

@ -0,0 +1,34 @@
/*
Copyright 2025 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 cloudflare
type autoPager[T any] interface {
Next() bool
Current() T
Err() error
}
// autoPagerIterator returns an iterator over an autoPager.
func autoPagerIterator[T any](iter autoPager[T]) func(yield func(T) bool) {
return func(yield func(T) bool) {
for iter.Next() {
if !yield(iter.Current()) {
return
}
}
}
}

View File

@ -0,0 +1,94 @@
/*
Copyright 2025 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 cloudflare
import (
"errors"
"slices"
"testing"
"github.com/stretchr/testify/assert"
)
type mockAutoPager[T any] struct {
items []T
index int
err error
errIndex int
}
func (m *mockAutoPager[T]) Next() bool {
m.index++
return !m.hasError() && m.hasNext()
}
func (m *mockAutoPager[T]) Current() T {
if m.hasNext() && !m.hasError() {
return m.items[m.index-1]
}
var zero T
return zero
}
func (m *mockAutoPager[T]) Err() error {
return m.err
}
func (m *mockAutoPager[T]) hasError() bool {
return m.err != nil && m.errIndex <= m.index
}
func (m *mockAutoPager[T]) hasNext() bool {
return m.index > 0 && m.index <= len(m.items)
}
func TestAutoPagerIterator(t *testing.T) {
t.Run("iterate empty", func(t *testing.T) {
pager := &mockAutoPager[string]{}
iterator := autoPagerIterator(pager)
collected := slices.Collect(iterator)
assert.Empty(t, collected)
})
t.Run("iterate all items", func(t *testing.T) {
pager := &mockAutoPager[int]{items: []int{1, 2, 3, 4, 5}}
iterator := autoPagerIterator(pager)
collected := slices.Collect(iterator)
assert.Equal(t, []int{1, 2, 3, 4, 5}, collected)
})
t.Run("iterate with early termination", func(t *testing.T) {
pager := &mockAutoPager[int]{items: []int{1, 2, 3, 4, 5}}
iterator := autoPagerIterator(pager)
var collected []int
for item := range iterator {
collected = append(collected, item)
if item == 3 {
break
}
}
assert.Equal(t, []int{1, 2, 3}, collected)
})
t.Run("iterate with error at index", func(t *testing.T) {
expectedErr := errors.New("pager error")
pager := &mockAutoPager[int]{items: []int{1, 2, 3, 4, 5}, err: expectedErr, errIndex: 3}
iterator := autoPagerIterator(pager)
collected := slices.Collect(iterator)
assert.Equal(t, []int{1, 2}, collected)
})
}

View File

@ -194,7 +194,7 @@ func isEmpty(xs interface{}) bool {
// This function is an adapted copy of the testify package's ElementsMatch function with the
// call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to
// other structs. It also ignores ordering when comparing unlike cmp.Equal.
func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) {
func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) bool {
if listA == nil && listB == nil {
return true
} else if listA == nil {

View File

@ -130,7 +130,7 @@ func NewDnsimpleProvider(domainFilter *endpoint.DomainFilter, zoneIDFilter provi
}
// GetAccountID returns the account ID given DNSimple credentials.
func (p *dnsimpleProvider) GetAccountID(ctx context.Context) (accountID string, err error) {
func (p *dnsimpleProvider) GetAccountID(ctx context.Context) (string, error) {
// get DNSimple client accountID
whoamiResponse, err := p.identity.Whoami(ctx)
if err != nil {
@ -191,11 +191,12 @@ func (p *dnsimpleProvider) Zones(ctx context.Context) (map[string]dnsimple.Zone,
}
// Records returns a list of endpoints in a given zone
func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
func (p *dnsimpleProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
zones, err := p.Zones(ctx)
if err != nil {
return nil, err
}
endpoints := make([]*endpoint.Endpoint, 0)
for _, zone := range zones {
page := 1
listOptions := &dnsimple.ZoneRecordListOptions{}
@ -318,7 +319,7 @@ func (p *dnsimpleProvider) submitChanges(ctx context.Context, changes []*dnsimpl
}
// GetRecordID returns the record ID for a given record name and zone.
func (p *dnsimpleProvider) GetRecordID(ctx context.Context, zone string, recordName string) (recordID int64, err error) {
func (p *dnsimpleProvider) GetRecordID(ctx context.Context, zone string, recordName string) (int64, error) {
page := 1
listOptions := &dnsimple.ZoneRecordListOptions{Name: &recordName}
for {

View File

@ -292,9 +292,8 @@ func (f *zoneFilter) Zones(zones map[string]string) map[string]string {
// EndpointZoneID determines zoneID for endpoint from map[zoneID]zoneName by taking longest suffix zoneName match in endpoint DNSName
// returns empty string if no matches are found
func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) (zoneID string, name string) {
var matchZoneID string
var matchZoneName string
func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) (string, string) {
var matchZoneID, matchZoneName, name string
for zoneID, zoneName := range zones {
if strings.HasSuffix(endpoint.DNSName, "."+zoneName) && len(zoneName) > len(matchZoneName) {
matchZoneName = zoneName

View File

@ -19,14 +19,14 @@ import (
)
type DomainClientAdapter interface {
ListDomains() (domains []domain.ListResponse, err error)
ListDomains() ([]domain.ListResponse, error)
}
type domainClient struct {
Client *domain.Domain
}
func (p *domainClient) ListDomains() (domains []domain.ListResponse, err error) {
func (p *domainClient) ListDomains() ([]domain.ListResponse, error) {
return p.Client.ListDomains()
}
@ -54,9 +54,9 @@ type standardError struct {
type LiveDNSClientAdapter interface {
GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error)
CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error)
CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error)
DeleteDomainRecord(fqdn, name, recordtype string) (err error)
UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error)
UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error)
}
type LiveDNSClient struct {
@ -67,11 +67,11 @@ func NewLiveDNSClient(client *livedns.LiveDNS) LiveDNSClientAdapter {
return &LiveDNSClient{client}
}
func (p *LiveDNSClient) GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error) {
func (p *LiveDNSClient) GetDomainRecords(fqdn string) ([]livedns.DomainRecord, error) {
return p.Client.GetDomainRecords(fqdn)
}
func (p *LiveDNSClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) {
func (p *LiveDNSClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) {
res, err := p.Client.CreateDomainRecord(fqdn, name, recordtype, ttl, values)
if err != nil {
return standardResponse{}, err
@ -93,11 +93,11 @@ func (p *LiveDNSClient) CreateDomainRecord(fqdn, name, recordtype string, ttl in
}, err
}
func (p *LiveDNSClient) DeleteDomainRecord(fqdn, name, recordtype string) (err error) {
func (p *LiveDNSClient) DeleteDomainRecord(fqdn, name, recordtype string) error {
return p.Client.DeleteDomainRecord(fqdn, name, recordtype)
}
func (p *LiveDNSClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) {
func (p *LiveDNSClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) {
res, err := p.Client.UpdateDomainRecordByNameAndType(fqdn, name, recordtype, ttl, values)
if err != nil {
return standardResponse{}, err

View File

@ -83,12 +83,12 @@ func NewGandiProvider(ctx context.Context, domainFilter *endpoint.DomainFilter,
return gandiProvider, nil
}
func (p *GandiProvider) Zones() (zones []string, err error) {
func (p *GandiProvider) Zones() ([]string, error) {
availableDomains, err := p.DomainClient.ListDomains()
if err != nil {
return nil, err
}
zones = []string{}
zones := []string{}
for _, domain := range availableDomains {
if !p.domainFilter.Match(domain.FQDN) {
log.Debugf("Excluding domain %s by domain-filter", domain.FQDN)
@ -156,7 +156,7 @@ func (p *GandiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes)
return p.submitChanges(ctx, combinedChanges)
}
func (p *GandiProvider) submitChanges(ctx context.Context, changes []*GandiChanges) error {
func (p *GandiProvider) submitChanges(_ context.Context, changes []*GandiChanges) error {
if len(changes) == 0 {
log.Infof("All records are already up to date")
return nil

View File

@ -49,7 +49,7 @@ const (
// Mock all methods
func (m *mockGandiClient) GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error) {
func (m *mockGandiClient) GetDomainRecords(fqdn string) ([]livedns.DomainRecord, error) {
m.Actions = append(m.Actions, MockAction{
Name: "GetDomainRecords",
FQDN: fqdn,
@ -62,7 +62,7 @@ func (m *mockGandiClient) GetDomainRecords(fqdn string) (records []livedns.Domai
return m.RecordsToReturn, nil
}
func (m *mockGandiClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) {
func (m *mockGandiClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) {
m.Actions = append(m.Actions, MockAction{
Name: "CreateDomainRecord",
FQDN: fqdn,
@ -81,7 +81,7 @@ func (m *mockGandiClient) CreateDomainRecord(fqdn, name, recordtype string, ttl
return standardResponse{}, nil
}
func (m *mockGandiClient) DeleteDomainRecord(fqdn, name, recordtype string) (err error) {
func (m *mockGandiClient) DeleteDomainRecord(fqdn, name, recordtype string) error {
m.Actions = append(m.Actions, MockAction{
Name: "DeleteDomainRecord",
FQDN: fqdn,
@ -98,7 +98,7 @@ func (m *mockGandiClient) DeleteDomainRecord(fqdn, name, recordtype string) (err
return nil
}
func (m *mockGandiClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) {
func (m *mockGandiClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) {
m.Actions = append(m.Actions, MockAction{
Name: "UpdateDomainRecordByNameAndType",
FQDN: fqdn,
@ -117,7 +117,7 @@ func (m *mockGandiClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype
return standardResponse{}, nil
}
func (m *mockGandiClient) ListDomains() (domains []domain.ListResponse, err error) {
func (m *mockGandiClient) ListDomains() ([]domain.ListResponse, error) {
m.Actions = append(m.Actions, MockAction{
Name: "ListDomains",
})

View File

@ -121,7 +121,9 @@ func (z gdZoneIDName) add(zoneID string, zoneRecord *gdRecords) {
z[zoneID] = zoneRecord
}
func (z gdZoneIDName) findZoneRecord(hostname string) (suitableZoneID string, suitableZoneRecord *gdRecords) {
func (z gdZoneIDName) findZoneRecord(hostname string) (string, *gdRecords) {
var suitableZoneID string
var suitableZoneRecord *gdRecords
for zoneID, zoneRecord := range z {
if hostname == zoneRecord.zone || strings.HasSuffix(hostname, "."+zoneRecord.zone) {
if suitableZoneRecord == nil || len(zoneRecord.zone) > len(suitableZoneRecord.zone) {
@ -131,11 +133,11 @@ func (z gdZoneIDName) findZoneRecord(hostname string) (suitableZoneID string, su
}
}
return
return suitableZoneID, suitableZoneRecord
}
// NewGoDaddyProvider initializes a new GoDaddy DNS based Provider.
func NewGoDaddyProvider(ctx context.Context, domainFilter *endpoint.DomainFilter, ttl int64, apiKey, apiSecret string, useOTE, dryRun bool) (*GDProvider, error) {
func NewGoDaddyProvider(_ context.Context, domainFilter *endpoint.DomainFilter, ttl int64, apiKey, apiSecret string, useOTE, dryRun bool) (*GDProvider, error) {
client, err := NewClient(useOTE, apiKey, apiSecret)
if err != nil {
return nil, err
@ -218,7 +220,7 @@ func (p *GDProvider) zonesRecords(ctx context.Context, all bool) ([]string, []gd
return zones, allRecords, nil
}
func (p *GDProvider) records(ctx *context.Context, zone string, all bool) (*gdRecords, error) {
func (p *GDProvider) records(_ *context.Context, zone string, all bool) (*gdRecords, error) {
var recordsIds []gdRecordField
log.Debugf("GoDaddy: Getting records for %s", zone)

View File

@ -20,17 +20,17 @@ import (
"context"
"fmt"
"sort"
"strings"
"time"
"cloud.google.com/go/compute/metadata"
"github.com/linki/instrumented_http"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2/google"
dns "google.golang.org/api/dns/v1"
googleapi "google.golang.org/api/googleapi"
"google.golang.org/api/option"
extdnshttp "sigs.k8s.io/external-dns/pkg/http"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
@ -131,12 +131,7 @@ func NewGoogleProvider(ctx context.Context, project string, domainFilter *endpoi
return nil, err
}
gcloud = instrumented_http.NewClient(gcloud, &instrumented_http.Callbacks{
PathProcessor: func(path string) string {
parts := strings.Split(path, "/")
return parts[len(parts)-1]
},
})
gcloud = extdnshttp.NewInstrumentedClient(gcloud)
dnsClient, err := dns.NewService(ctx, option.WithHTTPClient(gcloud))
if err != nil {
@ -207,12 +202,14 @@ func (p *GoogleProvider) Zones(ctx context.Context) (map[string]*dns.ManagedZone
}
// Records returns the list of records in all relevant zones.
func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
func (p *GoogleProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
zones, err := p.Zones(ctx)
if err != nil {
return nil, err
}
endpoints := make([]*endpoint.Endpoint, 0)
f := func(resp *dns.ResourceRecordSetsListResponse) error {
for _, r := range resp.Rrsets {
if !p.SupportedRecordType(r.Type) {
@ -226,7 +223,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, provider.NewSoftError(fmt.Errorf("failed to list records in zone %s: %w", z.Name, err))
return nil, provider.NewSoftErrorf("failed to list records in zone %s: %v", z.Name, err)
}
}

View File

@ -230,7 +230,7 @@ func (f *filter) Zones(zones map[string]string) map[string]string {
// EndpointZoneID determines zoneID for endpoint from map[zoneID]zoneName by taking longest suffix zoneName match in endpoint DNSName
// returns empty string if no match found
func (f *filter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) (zoneID string) {
func (f *filter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) string {
var matchZoneID, matchZoneName string
for zoneID, zoneName := range zones {
if strings.HasSuffix(endpoint.DNSName, zoneName) && len(zoneName) > len(matchZoneName) {

View File

@ -70,7 +70,7 @@ func buildZoneResponseItems(scope dns.ListZonesScopeEnum, privateZones, globalZo
}
}
func (c *mockOCIDNSClient) ListZones(_ context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) {
func (c *mockOCIDNSClient) ListZones(_ context.Context, request dns.ListZonesRequest) (dns.ListZonesResponse, error) {
if request.Page == nil || *request.Page == "0" {
return dns.ListZonesResponse{
Items: buildZoneResponseItems(request.Scope, []dns.ZoneSummary{testPrivateZoneSummaryBaz}, []dns.ZoneSummary{testGlobalZoneSummaryFoo}),
@ -82,9 +82,11 @@ func (c *mockOCIDNSClient) ListZones(_ context.Context, request dns.ListZonesReq
}, nil
}
func (c *mockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) {
func (c *mockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (dns.GetZoneRecordsResponse, error) {
var response dns.GetZoneRecordsResponse
var err error
if request.ZoneNameOrId == nil {
return
return response, err
}
switch *request.ZoneNameOrId {
@ -120,12 +122,11 @@ func (c *mockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZo
}}
}
}
return
return response, err
}
func (c *mockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) {
return // Provider does not use the response so nothing to do here.
func (c *mockOCIDNSClient) PatchZoneRecords(_ context.Context, request dns.PatchZoneRecordsRequest) (dns.PatchZoneRecordsResponse, error) {
return dns.PatchZoneRecordsResponse{}, nil
}
// newOCIProvider creates an OCI provider with API calls mocked out.
@ -549,7 +550,7 @@ func newMutableMockOCIDNSClient(zones []dns.ZoneSummary, recordsByZone map[strin
return c
}
func (c *mutableMockOCIDNSClient) ListZones(ctx context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) {
func (c *mutableMockOCIDNSClient) ListZones(_ context.Context, _ dns.ListZonesRequest) (dns.ListZonesResponse, error) {
var zones []dns.ZoneSummary
for _, v := range c.zones {
zones = append(zones, v)
@ -557,16 +558,15 @@ func (c *mutableMockOCIDNSClient) ListZones(ctx context.Context, request dns.Lis
return dns.ListZonesResponse{Items: zones}, nil
}
func (c *mutableMockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) {
func (c *mutableMockOCIDNSClient) GetZoneRecords(_ context.Context, request dns.GetZoneRecordsRequest) (dns.GetZoneRecordsResponse, error) {
var response dns.GetZoneRecordsResponse
if request.ZoneNameOrId == nil {
err = errors.New("no name or id")
return
return response, errors.New("no name or id")
}
records, ok := c.records[*request.ZoneNameOrId]
if !ok {
err = errors.New("zone not found")
return
return response, errors.New("zone not found")
}
var items []dns.Record
@ -575,7 +575,7 @@ func (c *mutableMockOCIDNSClient) GetZoneRecords(ctx context.Context, request dn
}
response.Items = items
return
return response, nil
}
func ociRecordKey(rType, domain string, ip string) string {
@ -592,16 +592,15 @@ func sortEndpointTargets(endpoints []*endpoint.Endpoint) {
}
}
func (c *mutableMockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) {
func (c *mutableMockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (dns.PatchZoneRecordsResponse, error) {
var response dns.PatchZoneRecordsResponse
if request.ZoneNameOrId == nil {
err = errors.New("no name or id")
return
return response, errors.New("no name or id")
}
records, ok := c.records[*request.ZoneNameOrId]
if !ok {
err = errors.New("zone not found")
return
return response, errors.New("zone not found")
}
// Ensure that ADD operations occur after REMOVE.
@ -622,11 +621,10 @@ func (c *mutableMockOCIDNSClient) PatchZoneRecords(ctx context.Context, request
case dns.RecordOperationOperationRemove:
delete(records, k)
default:
err = fmt.Errorf("unsupported operation %q", op.Operation)
return
return response, fmt.Errorf("unsupported operation %q", op.Operation)
}
}
return
return response, nil
}
// TestMutableMockOCIDNSClient exists because one must always test one's tests

View File

@ -242,7 +242,7 @@ func (p *OVHProvider) handleSingleZoneUpdate(ctx context.Context, zoneName strin
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) (err error) {
func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zones, records := p.lastRunZones, p.lastRunRecords
defer func() {
p.lastRunRecords = []ovhRecord{}

View File

@ -114,15 +114,14 @@ func (tlsConfig *TLSConfig) setHTTPClient(pdnsClientConfig *pgo.Configuration) e
}
// Function for debug printing
func stringifyHTTPResponseBody(r *http.Response) (body string) {
func stringifyHTTPResponseBody(r *http.Response) string {
if r == nil {
return ""
}
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
body = buf.String()
return body
_, _ = buf.ReadFrom(r.Body)
return buf.String()
}
// PDNSAPIProvider : Interface used and extended by the PDNSAPIClient struct as
@ -145,7 +144,10 @@ type PDNSAPIClient struct {
// ListZones : Method returns all enabled zones from PowerDNS
// 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) {
func (c *PDNSAPIClient) ListZones() ([]pgo.Zone, *http.Response, error) {
var zones []pgo.Zone
var resp *http.Response
var err error
for i := 0; i < retryLimit; i++ {
zones, resp, err = c.client.ZonesApi.ListZones(c.authCtx, c.serverID)
if err != nil {
@ -157,11 +159,14 @@ func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err
return zones, resp, err
}
return zones, resp, provider.NewSoftError(fmt.Errorf("unable to list zones: %w", err))
return zones, resp, provider.NewSoftErrorf("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
func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zone, residualZones []pgo.Zone) {
func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone) {
var filteredZones []pgo.Zone
var residualZones []pgo.Zone
if c.domainFilter.IsConfigured() {
for _, zone := range zones {
if c.domainFilter.Match(zone.Name) {
@ -178,9 +183,9 @@ func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zo
// ListZone : Method returns the details of a specific zone from PowerDNS
// 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) {
func (c *PDNSAPIClient) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
for i := 0; i < retryLimit; i++ {
zone, resp, err = c.client.ZonesApi.ListZone(c.authCtx, c.serverID, 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,12 +195,14 @@ func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Respo
return zone, resp, err
}
return zone, resp, provider.NewSoftError(fmt.Errorf("unable to list zone: %w", err))
return pgo.Zone{}, nil, provider.NewSoftErrorf("unable to list zone")
}
// 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) {
func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i < retryLimit; i++ {
resp, err = c.client.ZonesApi.PatchZone(c.authCtx, c.serverID, zoneID, zoneStruct)
if err != nil {
@ -207,7 +214,7 @@ func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *htt
return resp, err
}
return resp, provider.NewSoftError(fmt.Errorf("unable to patch zone: %w", err))
return resp, provider.NewSoftErrorf("unable to patch zone: %v", err)
}
// PDNSProvider is an implementation of the Provider interface for PowerDNS
@ -252,9 +259,9 @@ func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, err
return provider, nil
}
func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) {
endpoints = []*endpoint.Endpoint{}
targets := []string{}
func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) ([]*endpoint.Endpoint, error) {
endpoints := make([]*endpoint.Endpoint, 0)
targets := make([]string, 0)
rrType_ := rr.Type_
for _, record := range rr.Records {
@ -271,8 +278,8 @@ func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpo
}
// ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs
func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) {
zonelist = []pgo.Zone{}
func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) ([]pgo.Zone, error) {
var zoneList = make([]pgo.Zone, 0)
endpoints := make([]*endpoint.Endpoint, len(eps))
copy(endpoints, eps)
@ -354,7 +361,7 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
}
}
if len(zone.Rrsets) > 0 {
zonelist = append(zonelist, zone)
zoneList = append(zoneList, zone)
}
}
@ -379,9 +386,9 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
log.Warnf("No matching zones were found for the following endpoints: %+v", endpoints)
}
log.Debugf("Zone List generated from Endpoints: %+v", zonelist)
log.Debugf("Zone List generated from Endpoints: %+v", zoneList)
return zonelist, nil
return zoneList, nil
}
// mutateRecords takes a list of endpoints and creates, replaces or deletes them based on the changetype
@ -407,17 +414,19 @@ func (p *PDNSProvider) mutateRecords(endpoints []*endpoint.Endpoint, changetype
}
// Records returns all DNS records controlled by the configured PDNS server (for all zones)
func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
func (p *PDNSProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error) {
zones, _, err := p.client.ListZones()
if err != nil {
return nil, err
}
filteredZones, _ := p.client.PartitionZones(zones)
var endpoints []*endpoint.Endpoint
for _, zone := range filteredZones {
z, _, err := p.client.ListZone(zone.Id)
if err != nil {
return nil, provider.NewSoftError(fmt.Errorf("unable to fetch records: %w", err))
return nil, provider.NewSoftErrorf("unable to fetch records: %v", err)
}
for _, rr := range z.Rrsets {

View File

@ -28,10 +28,11 @@ import (
"net/url"
"strings"
"github.com/linki/instrumented_http"
log "github.com/sirupsen/logrus"
"golang.org/x/net/html"
extdnshttp "sigs.k8s.io/external-dns/pkg/http"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/provider"
)
@ -71,7 +72,8 @@ func newPiholeClient(cfg PiholeConfig) (piholeAPI, error) {
},
},
}
cl := instrumented_http.NewClient(httpClient, &instrumented_http.Callbacks{})
cl := extdnshttp.NewInstrumentedClient(httpClient)
p := &piholeClient{
cfg: cfg,

View File

@ -30,9 +30,10 @@ import (
"strconv"
"strings"
"github.com/linki/instrumented_http"
log "github.com/sirupsen/logrus"
extdnshttp "sigs.k8s.io/external-dns/pkg/http"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/provider"
)
@ -65,7 +66,7 @@ func newPiholeClientV6(cfg PiholeConfig) (piholeAPI, error) {
},
}
cl := instrumented_http.NewClient(httpClient, &instrumented_http.Callbacks{})
cl := extdnshttp.NewInstrumentedClient(httpClient)
p := &piholeClientV6{
cfg: cfg,
@ -143,12 +144,13 @@ func isValidIPv6(ip string) bool {
}
func (p *piholeClientV6) listRecords(ctx context.Context, rtype string) ([]*endpoint.Endpoint, error) {
out := make([]*endpoint.Endpoint, 0)
results, err := p.getConfigValue(ctx, rtype)
if err != nil {
return nil, err
}
endpoints := make(map[string]*endpoint.Endpoint)
for _, rec := range results {
recs := strings.FieldsFunc(rec, func(r rune) bool {
return r == ' ' || r == ','
@ -163,17 +165,17 @@ func (p *piholeClientV6) listRecords(ctx context.Context, rtype string) ([]*endp
DNSName, Target = recs[1], recs[0]
switch rtype {
case endpoint.RecordTypeA:
//PiHole return A and AAAA records. Filter to only keep the A records
// PiHole return A and AAAA records. Filter to only keep the A records
if !isValidIPv4(Target) {
continue
}
case endpoint.RecordTypeAAAA:
//PiHole return A and AAAA records. Filter to only keep the AAAA records
// PiHole return A and AAAA records. Filter to only keep the AAAA records
if !isValidIPv6(Target) {
continue
}
case endpoint.RecordTypeCNAME:
//PiHole return only CNAME records.
// PiHole return only CNAME records.
// CNAME format is DNSName,target, ttl?
DNSName, Target = recs[0], recs[1]
if len(recs) == 3 { // TTL is present
@ -186,7 +188,18 @@ func (p *piholeClientV6) listRecords(ctx context.Context, rtype string) ([]*endp
}
}
out = append(out, endpoint.NewEndpointWithTTL(DNSName, rtype, Ttl, Target))
ep := endpoint.NewEndpointWithTTL(DNSName, rtype, Ttl, Target)
if oldEp, ok := endpoints[DNSName]; ok {
ep.Targets = append(oldEp.Targets, Target)
}
endpoints[DNSName] = ep
}
out := make([]*endpoint.Endpoint, 0, len(endpoints))
for _, ep := range endpoints {
out = append(out, ep)
}
return out, nil
}
@ -272,37 +285,44 @@ func (p *piholeClientV6) apply(ctx context.Context, action string, ep *endpoint.
return nil
}
if p.cfg.DryRun {
log.Infof("DRY RUN: %s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, ep.Targets[0])
return nil
}
log.Infof("%s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, ep.Targets[0])
// Get the current record
if strings.Contains(ep.DNSName, "*") {
return provider.NewSoftError(errors.New("UNSUPPORTED: Pihole DNS names cannot return wildcard"))
}
switch ep.RecordType {
case endpoint.RecordTypeA, endpoint.RecordTypeAAAA:
apiUrl = p.generateApiUrl(apiUrl, fmt.Sprintf("%s %s", ep.Targets, ep.DNSName))
case endpoint.RecordTypeCNAME:
if ep.RecordTTL.IsConfigured() {
apiUrl = p.generateApiUrl(apiUrl, fmt.Sprintf("%s,%s,%d", ep.DNSName, ep.Targets, ep.RecordTTL))
} else {
apiUrl = p.generateApiUrl(apiUrl, fmt.Sprintf("%s,%s", ep.DNSName, ep.Targets))
if ep.RecordType == endpoint.RecordTypeCNAME && len(ep.Targets) > 1 {
return provider.NewSoftError(errors.New("UNSUPPORTED: Pihole CNAME records cannot have multiple targets"))
}
for _, target := range ep.Targets {
if p.cfg.DryRun {
log.Infof("DRY RUN: %s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, target)
continue
}
}
req, err := http.NewRequestWithContext(ctx, action, apiUrl, nil)
if err != nil {
return err
}
log.Infof("%s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, target)
_, err = p.do(req)
if err != nil {
return err
targetApiUrl := apiUrl
switch ep.RecordType {
case endpoint.RecordTypeA, endpoint.RecordTypeAAAA:
targetApiUrl = p.generateApiUrl(targetApiUrl, fmt.Sprintf("%s %s", target, ep.DNSName))
case endpoint.RecordTypeCNAME:
if ep.RecordTTL.IsConfigured() {
targetApiUrl = p.generateApiUrl(targetApiUrl, fmt.Sprintf("%s,%s,%d", ep.DNSName, target, ep.RecordTTL))
} else {
targetApiUrl = p.generateApiUrl(targetApiUrl, fmt.Sprintf("%s,%s", ep.DNSName, target))
}
}
req, err := http.NewRequestWithContext(ctx, action, targetApiUrl, nil)
if err != nil {
return err
}
_, err = p.do(req)
if err != nil {
return err
}
}
return nil
@ -400,6 +420,14 @@ func (p *piholeClientV6) do(req *http.Request) ([]byte, error) {
if err := json.Unmarshal(jRes, &apiError); err != nil {
return nil, fmt.Errorf("failed to unmarshal error response: %w", err)
}
// Ignore if the entry already exists when adding a record
if strings.Contains(apiError.Error.Message, "Item already present") {
return jRes, nil
}
// Ignore if the entry does not exist when deleting a record
if res.StatusCode == http.StatusNotFound && req.Method == http.MethodDelete {
return jRes, nil
}
if log.IsLevelEnabled(log.DebugLevel) {
log.Debugf("Error on request %s", req.URL)
if req.Body != nil {

View File

@ -23,10 +23,10 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"sigs.k8s.io/external-dns/endpoint"
)
@ -192,10 +192,14 @@ func TestListRecordsV6(t *testing.T) {
"192.168.178.33 service1.example.com",
"192.168.178.34 service2.example.com",
"192.168.178.34 service3.example.com",
"192.168.178.35 service8.example.com",
"192.168.178.36 service8.example.com",
"fc00::1:192:168:1:1 service4.example.com",
"fc00::1:192:168:1:2 service5.example.com",
"fc00::1:192:168:1:3 service6.example.com",
"::ffff:192.168.20.3 service7.example.com",
"fc00::1:192:168:1:4 service9.example.com",
"fc00::1:192:168:1:5 service9.example.com",
"192.168.20.3 service7.example.com"
]
}
@ -237,37 +241,70 @@ func TestListRecordsV6(t *testing.T) {
}
// Ensure A records were parsed correctly
expected := [][]string{
{"service1.example.com", "192.168.178.33"},
{"service2.example.com", "192.168.178.34"},
{"service3.example.com", "192.168.178.34"},
{"service7.example.com", "192.168.20.3"},
expected := []*endpoint.Endpoint{
{
DNSName: "service1.example.com",
Targets: []string{"192.168.178.33"},
},
{
DNSName: "service2.example.com",
Targets: []string{"192.168.178.34"},
},
{
DNSName: "service3.example.com",
Targets: []string{"192.168.178.34"},
},
{
DNSName: "service7.example.com",
Targets: []string{"192.168.20.3"},
},
{
DNSName: "service8.example.com",
Targets: []string{"192.168.178.35", "192.168.178.36"},
},
}
// Test retrieve A records unfiltered
arecs, err := cl.listRecords(context.Background(), endpoint.RecordTypeA)
if err != nil {
t.Fatal(err)
}
if len(arecs) != len(expected) {
t.Fatalf("Expected %d A records returned, got: %d", len(expected), len(arecs))
}
for idx, rec := range arecs {
if rec.DNSName != expected[idx][0] {
t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
}
if rec.Targets[0] != expected[idx][1] {
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
expectedMap := make(map[string]*endpoint.Endpoint)
for _, ep := range expected {
expectedMap[ep.DNSName] = ep
}
for _, rec := range arecs {
if ep, ok := expectedMap[rec.DNSName]; ok {
if cmp.Diff(ep.Targets, rec.Targets) != "" {
t.Errorf("Got invalid targets for %s: %v, expected: %v", rec.DNSName, rec.Targets, ep.Targets)
}
}
}
// Ensure AAAA records were parsed correctly
expected = [][]string{
{"service4.example.com", "fc00::1:192:168:1:1"},
{"service5.example.com", "fc00::1:192:168:1:2"},
{"service6.example.com", "fc00::1:192:168:1:3"},
{"service7.example.com", "::ffff:192.168.20.3"},
expected = []*endpoint.Endpoint{
{
DNSName: "service4.example.com",
Targets: []string{"fc00::1:192:168:1:1"},
},
{
DNSName: "service5.example.com",
Targets: []string{"fc00::1:192:168:1:2"},
},
{
DNSName: "service6.example.com",
Targets: []string{"fc00::1:192:168:1:3"},
},
{
DNSName: "service7.example.com",
Targets: []string{"::ffff:192.168.20.3"},
},
{
DNSName: "service9.example.com",
Targets: []string{"fc00::1:192:168:1:4", "fc00::1:192:168:1:5"},
},
}
// Test retrieve AAAA records unfiltered
arecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeAAAA)
if err != nil {
@ -278,20 +315,34 @@ func TestListRecordsV6(t *testing.T) {
t.Fatalf("Expected %d AAAA records returned, got: %d", len(expected), len(arecs))
}
for idx, rec := range arecs {
if rec.DNSName != expected[idx][0] {
t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
}
if rec.Targets[0] != expected[idx][1] {
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
expectedMap = make(map[string]*endpoint.Endpoint)
for _, ep := range expected {
expectedMap[ep.DNSName] = ep
}
for _, rec := range arecs {
if ep, ok := expectedMap[rec.DNSName]; ok {
if cmp.Diff(ep.Targets, rec.Targets) != "" {
t.Errorf("Got invalid targets for %s: %v, expected: %v", rec.DNSName, rec.Targets, ep.Targets)
}
}
}
// Ensure CNAME records were parsed correctly
expected = [][]string{
{"source1.example.com", "target1.domain.com", "1000"},
{"source2.example.com", "target2.domain.com", "50"},
{"source3.example.com", "target3.domain.com"},
expected = []*endpoint.Endpoint{
{
DNSName: "source1.example.com",
Targets: []string{"target1.domain.com"},
RecordTTL: 1000,
},
{
DNSName: "source2.example.com",
Targets: []string{"target2.domain.com"},
RecordTTL: 50,
},
{
DNSName: "source3.example.com",
Targets: []string{"target3.domain.com"},
},
}
// Test retrieve CNAME records unfiltered
@ -303,17 +354,14 @@ func TestListRecordsV6(t *testing.T) {
t.Fatalf("Expected %d CAME records returned, got: %d", len(expected), len(cnamerecs))
}
for idx, rec := range cnamerecs {
if rec.DNSName != expected[idx][0] {
t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
}
if rec.Targets[0] != expected[idx][1] {
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
}
if len(expected[idx]) == 3 {
expectedTTL, _ := strconv.ParseInt(expected[idx][2], 10, 64)
if int64(rec.RecordTTL) != expectedTTL {
t.Error("Got invalid TTL:", rec.RecordTTL, "expected:", expected[idx][2])
expectedMap = make(map[string]*endpoint.Endpoint)
for _, ep := range expected {
expectedMap[ep.DNSName] = ep
}
for _, rec := range arecs {
if ep, ok := expectedMap[rec.DNSName]; ok {
if cmp.Diff(ep.Targets, rec.Targets) != "" {
t.Errorf("Got invalid targets for %s: %v, expected: %v", rec.DNSName, rec.Targets, ep.Targets)
}
}
}
@ -432,8 +480,34 @@ func TestErrorsV6(t *testing.T) {
if len(resp) != 2 {
t.Fatal("Expected one records returned, got:", len(resp))
}
if resp[1].RecordTTL != 0 {
t.Fatal("Expected no TTL returned, got:", resp[0].RecordTTL)
expected := []*endpoint.Endpoint{
{
DNSName: "source1.example.com",
Targets: []string{"target1.domain.com"},
RecordTTL: 100,
},
{
DNSName: "source2.example.com",
Targets: []string{"target2.domain.com"},
},
}
expectedMap := make(map[string]*endpoint.Endpoint)
for _, ep := range expected {
expectedMap[ep.DNSName] = ep
}
for _, rec := range resp {
if ep, ok := expectedMap[rec.DNSName]; ok {
if cmp.Diff(ep.Targets, rec.Targets) != "" {
t.Errorf("Got invalid targets for %s: %v, expected: %v", rec.DNSName, rec.Targets, ep.Targets)
}
if ep.RecordTTL != rec.RecordTTL {
t.Errorf("Got invalid TTL for %s: %d, expected: %d", rec.DNSName, rec.RecordTTL, ep.RecordTTL)
}
} else {
t.Errorf("Unexpected record found: %s", rec.DNSName)
}
}
}
@ -717,6 +791,10 @@ func TestCreateRecordV6(t *testing.T) {
if r.Method == http.MethodPut && (r.URL.Path == "/api/config/dns/hosts/192.168.1.1 test.example.com" ||
r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:1 test.example.com" ||
r.URL.Path == "/api/config/dns/cnameRecords/source1.example.com,target1.domain.com" ||
r.URL.Path == "/api/config/dns/hosts/192.168.1.2 test.example.com" ||
r.URL.Path == "/api/config/dns/hosts/192.168.1.3 test.example.com" ||
r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:2 test.example.com" ||
r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:3 test.example.com" ||
r.URL.Path == "/api/config/dns/cnameRecords/source2.example.com,target2.domain.com,500") {
// Return A records
@ -748,6 +826,16 @@ func TestCreateRecordV6(t *testing.T) {
t.Fatal(err)
}
// Test create multiple A records
ep = &endpoint.Endpoint{
DNSName: "test.example.com",
Targets: []string{"192.168.1.2", "192.168.1.3"},
RecordType: endpoint.RecordTypeA,
}
if err := cl.createRecord(context.Background(), ep); err != nil {
t.Fatal(err)
}
// Test create AAAA record
ep = &endpoint.Endpoint{
DNSName: "test.example.com",
@ -758,6 +846,16 @@ func TestCreateRecordV6(t *testing.T) {
t.Fatal(err)
}
// Test create multiple AAAA records
ep = &endpoint.Endpoint{
DNSName: "test.example.com",
Targets: []string{"fc00::1:192:168:1:2", "fc00::1:192:168:1:3"},
RecordType: endpoint.RecordTypeAAAA,
}
if err := cl.createRecord(context.Background(), ep); err != nil {
t.Fatal(err)
}
// Test create CNAME record
ep = &endpoint.Endpoint{
DNSName: "source1.example.com",
@ -779,6 +877,16 @@ func TestCreateRecordV6(t *testing.T) {
t.Fatal(err)
}
// Test create CNAME record with multiple targets and ensure it fails
ep = &endpoint.Endpoint{
DNSName: "source3.example.com",
Targets: []string{"target3.domain.com", "target4.domain.com"},
RecordType: endpoint.RecordTypeCNAME,
}
if err := cl.createRecord(context.Background(), ep); err == nil {
t.Fatal(err)
}
// Test create a wildcard record and ensure it fails
ep = &endpoint.Endpoint{
DNSName: "*.example.com",

View File

@ -19,6 +19,9 @@ package pihole
import (
"context"
"errors"
"slices"
"github.com/google/go-cmp/cmp"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
@ -32,7 +35,8 @@ var ErrNoPiholeServer = errors.New("no pihole server found in the environment or
// PiholeProvider is an implementation of Provider for Pi-hole Local DNS.
type PiholeProvider struct {
provider.BaseProvider
api piholeAPI
api piholeAPI
apiVersion string
}
// PiholeConfig is used for configuring a PiholeProvider.
@ -70,7 +74,7 @@ func NewPiholeProvider(cfg PiholeConfig) (*PiholeProvider, error) {
if err != nil {
return nil, err
}
return &PiholeProvider{api: api}, nil
return &PiholeProvider{api: api, apiVersion: cfg.APIVersion}, nil
}
// Records implements Provider, populating a slice of endpoints from
@ -105,6 +109,19 @@ func (p *PiholeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
updateNew := make(map[piholeEntryKey]*endpoint.Endpoint)
for _, ep := range changes.UpdateNew {
key := piholeEntryKey{ep.DNSName, ep.RecordType}
// If the API version is 6, we need to handle multiple targets for the same DNS name.
if p.apiVersion == "6" {
if existing, ok := updateNew[key]; ok {
existing.Targets = append(existing.Targets, ep.Targets...)
// Deduplicate targets
slices.Sort(existing.Targets)
existing.Targets = slices.Compact(existing.Targets)
ep = existing
}
}
updateNew[key] = ep
}
@ -112,14 +129,23 @@ func (p *PiholeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
// Check if this existing entry has an exact match for an updated entry and skip it if so.
key := piholeEntryKey{ep.DNSName, ep.RecordType}
if newRecord := updateNew[key]; newRecord != nil {
// PiHole only has a single target; no need to compare other fields.
if newRecord.Targets[0] == ep.Targets[0] {
delete(updateNew, key)
continue
// If the API version is 6, we need to handle multiple targets for the same DNS name.
if p.apiVersion == "6" {
if cmp.Diff(ep.Targets, newRecord.Targets) == "" {
delete(updateNew, key)
continue
}
} else {
// For API version <= 5, we only check the first target.
if newRecord.Targets[0] == ep.Targets[0] {
delete(updateNew, key)
continue
}
}
if err := p.api.deleteRecord(ctx, ep); err != nil {
return err
}
}
if err := p.api.deleteRecord(ctx, ep); err != nil {
return err
}
}

View File

@ -22,6 +22,7 @@ import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
)
@ -60,7 +61,7 @@ func (t *testPiholeClientV6) createRecord(_ context.Context, ep *endpoint.Endpoi
func (t *testPiholeClientV6) deleteRecord(_ context.Context, ep *endpoint.Endpoint) error {
newEPs := make([]*endpoint.Endpoint, 0)
for _, existing := range t.endpoints {
if existing.DNSName != ep.DNSName && existing.Targets[0] != ep.Targets[0] {
if existing.DNSName != ep.DNSName || cmp.Diff(existing.Targets, ep.Targets) != "" || existing.RecordType != ep.RecordType {
newEPs = append(newEPs, existing)
}
}
@ -82,7 +83,8 @@ func (r *requestTrackerV6) clear() {
func TestErrorHandling(t *testing.T) {
requests := requestTrackerV6{}
p := &PiholeProvider{
api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests},
api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests},
apiVersion: "6",
}
p.api.(*testPiholeClientV6).trigger = "AERROR"
@ -121,7 +123,8 @@ func TestNewPiholeProviderV6(t *testing.T) {
func TestProviderV6(t *testing.T) {
requests := requestTrackerV6{}
p := &PiholeProvider{
api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests},
api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests},
apiVersion: "6",
}
records, err := p.Records(context.Background())
@ -342,6 +345,11 @@ func TestProviderV6(t *testing.T) {
Targets: []string{"10.0.0.1"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "test2.example.com",
Targets: []string{"10.0.0.2"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "test1.example.com",
Targets: []string{"fc00::1:192:168:1:1"},
@ -383,7 +391,7 @@ func TestProviderV6(t *testing.T) {
expectedCreateA := endpoint.Endpoint{
DNSName: "test2.example.com",
Targets: []string{"10.0.0.1"},
Targets: []string{"10.0.0.1", "10.0.0.2"},
RecordType: endpoint.RecordTypeA,
}
expectedDeleteA := endpoint.Endpoint{

View File

@ -66,17 +66,17 @@ func NewPluralProvider(cluster, provider string) (*PluralProvider, error) {
}, nil
}
func (p *PluralProvider) Records(_ context.Context) (endpoints []*endpoint.Endpoint, err error) {
func (p *PluralProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error) {
records, err := p.Client.DnsRecords()
if err != nil {
return
return nil, err
}
endpoints = make([]*endpoint.Endpoint, len(records))
endpoints := make([]*endpoint.Endpoint, len(records))
for i, record := range records {
endpoints[i] = endpoint.NewEndpoint(record.Name, record.Type, record.Records...)
}
return
return endpoints, nil
}
func (p *PluralProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) {

View File

@ -169,13 +169,13 @@ func NewRfc2136Provider(hosts []string, port int, zoneNames []string, insecure b
}
// KeyData will return TKEY name and TSIG handle to use for followon actions with a secure connection
func (r *rfc2136Provider) KeyData(nameserver string) (keyName string, handle *gss.Client, err error) {
handle, err = gss.NewClient(new(dns.Client))
func (r *rfc2136Provider) KeyData(nameserver string) (string, *gss.Client, error) {
handle, err := gss.NewClient(new(dns.Client))
if err != nil {
return keyName, handle, err
return "", handle, err
}
keyName, _, err = handle.NegotiateContextWithCredentials(nameserver, r.krb5Realm, r.krb5Username, r.krb5Password)
keyName, _, err := handle.NegotiateContextWithCredentials(nameserver, r.krb5Realm, r.krb5Username, r.krb5Password)
if err != nil {
return keyName, handle, err
}
@ -247,7 +247,7 @@ OuterLoop:
return eps, nil
}
func (r *rfc2136Provider) IncomeTransfer(m *dns.Msg, nameserver string) (env chan *dns.Envelope, err error) {
func (r *rfc2136Provider) IncomeTransfer(m *dns.Msg, nameserver string) (chan *dns.Envelope, error) {
t := new(dns.Transfer)
if !r.insecure && !r.gssTsig {
t.TsigSecret = map[string]string{r.tsigKeyName: r.tsigSecret}
@ -408,9 +408,11 @@ func (r *rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Change
zone := findMsgZone(ep, r.zoneNames)
m[zone].SetUpdate(zone)
r.UpdateRecord(m[zone], changes.UpdateOld[i], ep)
// calculate corresponding index in the unsplitted UpdateOld for current endpoint ep in chunk
j := (c * r.batchChangeSize) + i
r.UpdateRecord(m[zone], changes.UpdateOld[j], ep)
if r.createPTR && (ep.RecordType == "A" || ep.RecordType == "AAAA") {
r.RemoveReverseRecord(changes.UpdateOld[i].Targets[0], ep.DNSName)
r.RemoveReverseRecord(changes.UpdateOld[j].Targets[0], ep.DNSName)
r.AddReverseRecord(ep.Targets[0], ep.DNSName)
}
}

View File

@ -149,7 +149,7 @@ func (r *rfc2136Stub) setOutput(output []string) error {
return nil
}
func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error) {
func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (chan *dns.Envelope, error) {
outChan := make(chan *dns.Envelope)
go func() {
for _, e := range r.output {
@ -256,6 +256,17 @@ func createRfc2136StubProviderWithStrategy(stub *rfc2136Stub, strategy string) (
return NewRfc2136Provider([]string{"rfc2136-host1", "rfc2136-host2", "rfc2136-host3"}, 0, nil, false, "key", "secret", "hmac-sha512", true, &endpoint.DomainFilter{}, false, 300*time.Second, false, false, "", "", "", 50, tlsConfig, strategy, stub)
}
func createRfc2136StubProviderWithBatchChangeSize(stub *rfc2136Stub, batchChangeSize int) (provider.Provider, error) {
tlsConfig := TLSConfig{
UseTLS: false,
SkipTLSVerify: false,
CAFilePath: "",
ClientCertFilePath: "",
ClientCertKeyFilePath: "",
}
return NewRfc2136Provider([]string{""}, 0, nil, false, "key", "secret", "hmac-sha512", true, &endpoint.DomainFilter{}, false, 300*time.Second, false, false, "", "", "", batchChangeSize, tlsConfig, "", stub)
}
func extractUpdateSectionFromMessage(msg fmt.Stringer) []string {
const searchPattern = "UPDATE SECTION:"
updateSectionOffset := strings.Index(msg.String(), searchPattern)
@ -959,3 +970,44 @@ func TestRandomLoadBalancing(t *testing.T) {
assert.Greater(t, len(nameserverCounts), 1, "Expected multiple nameservers to be used in random strategy")
}
// TestRfc2136ApplyChangesWithMultipleChunks tests Updates with multiple chunks
func TestRfc2136ApplyChangesWithMultipleChunks(t *testing.T) {
stub := newStub()
provider, err := createRfc2136StubProviderWithBatchChangeSize(stub, 2)
assert.NoError(t, err)
var oldRecords []*endpoint.Endpoint
var newRecords []*endpoint.Endpoint
for i := 1; i <= 4; i++ {
oldRecords = append(oldRecords, &endpoint.Endpoint{
DNSName: fmt.Sprintf("%s%d%s", "v", i, ".foo.com"),
RecordType: "A",
Targets: []string{fmt.Sprintf("10.0.0.%d", i)},
RecordTTL: endpoint.TTL(400),
})
newRecords = append(newRecords, &endpoint.Endpoint{
DNSName: fmt.Sprintf("%s%d%s", "v", i, ".foo.com"),
RecordType: "A",
Targets: []string{fmt.Sprintf("10.0.1.%d", i)},
RecordTTL: endpoint.TTL(400),
})
}
p := &plan.Changes{
UpdateOld: oldRecords,
UpdateNew: newRecords,
}
err = provider.ApplyChanges(context.Background(), p)
assert.NoError(t, err)
assert.Len(t, stub.updateMsgs, 4)
assert.Contains(t, stub.updateMsgs[0].String(), "\nv1.foo.com.\t0\tNONE\tA\t10.0.0.1\nv1.foo.com.\t400\tIN\tA\t10.0.1.1\n")
assert.Contains(t, stub.updateMsgs[0].String(), "\nv2.foo.com.\t0\tNONE\tA\t10.0.0.2\nv2.foo.com.\t400\tIN\tA\t10.0.1.2\n")
assert.Contains(t, stub.updateMsgs[2].String(), "\nv3.foo.com.\t0\tNONE\tA\t10.0.0.3\nv3.foo.com.\t400\tIN\tA\t10.0.1.3\n")
assert.Contains(t, stub.updateMsgs[2].String(), "\nv4.foo.com.\t0\tNONE\tA\t10.0.0.4\nv4.foo.com.\t400\tIN\tA\t10.0.1.4\n")
}

View File

@ -43,7 +43,6 @@ const (
var (
recordsErrorsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider",
Name: "records_errors_total",
Help: "Errors with Records method",
@ -51,7 +50,6 @@ var (
)
recordsRequestsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider",
Name: "records_requests_total",
Help: "Requests with Records method",
@ -59,7 +57,6 @@ var (
)
applyChangesErrorsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider",
Name: "applychanges_errors_total",
Help: "Errors with ApplyChanges method",
@ -67,7 +64,6 @@ var (
)
applyChangesRequestsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider",
Name: "applychanges_requests_total",
Help: "Requests with ApplyChanges method",
@ -75,7 +71,6 @@ var (
)
adjustEndpointsErrorsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider",
Name: "adjustendpoints_errors_total",
Help: "Errors with AdjustEndpoints method",
@ -83,7 +78,6 @@ var (
)
adjustEndpointsRequestsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider",
Name: "adjustendpoints_requests_total",
Help: "Requests with AdjustEndpoints method",

View File

@ -41,21 +41,23 @@ func (z ZoneIDName) Add(zoneID, zoneName string) {
// SRV records as per RFC 2782, or TXT record for services) that are not
// IDNA-aware and cannot represent non-ASCII labels. Skipping these labels
// ensures compatibility with such use cases.
func (z ZoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) {
func (z ZoneIDName) FindZone(hostname string) (string, string) {
var name string
domain_labels := strings.Split(hostname, ".")
for i, label := range domain_labels {
domainLabels := strings.Split(hostname, ".")
for i, label := range domainLabels {
if strings.Contains(label, "_") {
continue
}
convertedLabel, err := idna.Lookup.ToUnicode(label)
if err != nil {
log.Warnf("Failed to convert label '%s' of hostname '%s' to its Unicode form: %v", label, hostname, err)
log.Warnf("Failed to convert label %q of hostname %q to its Unicode form: %v", label, hostname, err)
convertedLabel = label
}
domain_labels[i] = convertedLabel
domainLabels[i] = convertedLabel
}
name = strings.Join(domain_labels, ".")
name = strings.Join(domainLabels, ".")
var suitableZoneID, suitableZoneName string
for zoneID, zoneName := range z {
if name == zoneName || strings.HasSuffix(name, "."+zoneName) {
@ -65,5 +67,5 @@ func (z ZoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName
}
}
}
return
return suitableZoneID, suitableZoneName
}

View File

@ -80,5 +80,5 @@ func TestZoneIDName(t *testing.T) {
hook := testutils.LogsUnderTestWithLogLevel(log.WarnLevel, t)
_, _ = z.FindZone("???")
testutils.TestHelperLogContains("Failed to convert label '???' of hostname '???' to its Unicode form: idna: disallowed rune U+003F", hook, t)
testutils.TestHelperLogContains("Failed to convert label \"???\" of hostname \"???\" to its Unicode form: idna: disallowed rune U+003F", hook, t)
}

4
registry/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- registry

View File

@ -338,7 +338,7 @@ func newaffixNameMapper(prefix, suffix, wildcardReplacement string) affixNameMap
// extractRecordTypeDefaultPosition extracts record type from the default position
// when not using '%{record_type}' in the prefix/suffix
func extractRecordTypeDefaultPosition(name string) (baseName, recordType string) {
func extractRecordTypeDefaultPosition(name string) (string, string) {
nameS := strings.Split(name, "-")
for _, t := range getSupportedTypes() {
if nameS[0] == strings.ToLower(t) {
@ -350,7 +350,7 @@ func extractRecordTypeDefaultPosition(name string) (baseName, recordType string)
// dropAffixExtractType strips TXT record to find an endpoint name it manages
// it also returns the record type
func (pr affixNameMapper) dropAffixExtractType(name string) (baseName, recordType string) {
func (pr affixNameMapper) dropAffixExtractType(name string) (string, string) {
prefix := pr.prefix
suffix := pr.suffix
@ -397,7 +397,7 @@ func (pr affixNameMapper) isSuffix() bool {
return len(pr.prefix) == 0 && len(pr.suffix) > 0
}
func (pr affixNameMapper) toEndpointName(txtDNSName string) (endpointName string, recordType string) {
func (pr affixNameMapper) toEndpointName(txtDNSName string) (string, string) {
lowerDNSName := strings.ToLower(txtDNSName)
// drop prefix

View File

@ -18,7 +18,6 @@ package registry
import (
"context"
"fmt"
"reflect"
"strings"
"testing"
@ -1511,7 +1510,6 @@ func TestNewTXTScheme(t *testing.T) {
assert.Nil(t, ctx.Value(provider.RecordsContextKey))
}
err := r.ApplyChanges(ctx, changes)
fmt.Println(err)
require.NoError(t, err)
}

4
scripts/OWNERS Normal file
View File

@ -0,0 +1,4 @@
# See the OWNERS docs at https://go.k8s.io/owners
labels:
- scripts

View File

@ -196,7 +196,7 @@ func (sc *ambassadorHostSource) endpointsFromHost(host *ambassador.Host, targets
if host.Spec != nil {
hostname := host.Spec.Hostname
if hostname != "" {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
}
}
@ -228,7 +228,7 @@ func (sc *ambassadorHostSource) targetsFromAmbassadorLoadBalancer(ctx context.Co
//
// Returns namespace, name, error.
func parseAmbLoadBalancerService(service string) (namespace, name string, err error) {
func parseAmbLoadBalancerService(service string) (string, string, error) {
// Start by assuming that we have namespace/name.
parts := strings.Split(service, "/")
@ -294,13 +294,7 @@ func newUnstructuredConverter() (*unstructuredConverter, error) {
// Filter a list of Ambassador Host Resources to only return the ones that
// contain the required External-DNS annotation filter
func (sc *ambassadorHostSource) filterByAnnotations(ambassadorHosts []*ambassador.Host) ([]*ambassador.Host, error) {
// External-DNS Annotation Filter
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(sc.annotationFilter)
if err != nil {
return nil, err
}

View File

@ -18,6 +18,10 @@ import (
)
const (
// AnnotationKeyPrefix is set on all annotations consumed by external-dns (outside of user templates)
// to provide easy filtering.
AnnotationKeyPrefix = "external-dns.alpha.kubernetes.io/"
// CloudflareProxiedKey The annotation used for determining if traffic will go through Cloudflare
CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname"
@ -25,31 +29,31 @@ const (
CloudflareRecordCommentKey = "external-dns.alpha.kubernetes.io/cloudflare-record-comment"
CloudflareRecordTagsKey = "external-dns.alpha.kubernetes.io/cloudflare-record-tags"
AWSPrefix = "external-dns.alpha.kubernetes.io/aws-"
SCWPrefix = "external-dns.alpha.kubernetes.io/scw-"
WebhookPrefix = "external-dns.alpha.kubernetes.io/webhook-"
CloudflarePrefix = "external-dns.alpha.kubernetes.io/cloudflare-"
AWSPrefix = AnnotationKeyPrefix + "aws-"
SCWPrefix = AnnotationKeyPrefix + "scw-"
WebhookPrefix = AnnotationKeyPrefix + "webhook-"
CloudflarePrefix = AnnotationKeyPrefix + "cloudflare-"
TtlKey = "external-dns.alpha.kubernetes.io/ttl"
TtlKey = AnnotationKeyPrefix + "ttl"
ttlMinimum = 1
ttlMaximum = math.MaxInt32
SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier"
AliasKey = "external-dns.alpha.kubernetes.io/alias"
TargetKey = "external-dns.alpha.kubernetes.io/target"
SetIdentifierKey = AnnotationKeyPrefix + "set-identifier"
AliasKey = AnnotationKeyPrefix + "alias"
TargetKey = AnnotationKeyPrefix + "target"
// The annotation used for figuring out which controller is responsible
ControllerKey = "external-dns.alpha.kubernetes.io/controller"
ControllerKey = AnnotationKeyPrefix + "controller"
// The annotation used for defining the desired hostname
HostnameKey = "external-dns.alpha.kubernetes.io/hostname"
HostnameKey = AnnotationKeyPrefix + "hostname"
// The annotation used for specifying whether the public or private interface address is used
AccessKey = "external-dns.alpha.kubernetes.io/access"
AccessKey = AnnotationKeyPrefix + "access"
// The annotation used for specifying the type of endpoints to use for headless services
EndpointsTypeKey = "external-dns.alpha.kubernetes.io/endpoints-type"
EndpointsTypeKey = AnnotationKeyPrefix + "endpoints-type"
// The annotation used to determine the source of hostnames for ingresses. This is an optional field - all
// available hostname sources are used if not specified.
IngressHostnameSourceKey = "external-dns.alpha.kubernetes.io/ingress-hostname-source"
IngressHostnameSourceKey = AnnotationKeyPrefix + "ingress-hostname-source"
// The value of the controller annotation so that we feel responsible
ControllerValue = "dns-controller"
// The annotation used for defining the desired hostname
InternalHostnameKey = "external-dns.alpha.kubernetes.io/internal-hostname"
InternalHostnameKey = AnnotationKeyPrefix + "internal-hostname"
)

View File

@ -47,6 +47,12 @@ func TestParseAnnotationFilter(t *testing.T) {
expectedSelector: labels.Set{}.AsSelector(),
expectError: false,
},
{
name: "wrong annotation filter",
annotationFilter: "=test",
expectedSelector: nil,
expectError: true,
},
}
for _, tt := range tests {

View File

@ -25,7 +25,6 @@ import (
projectcontour "github.com/projectcontour/contour/apis/projectcontour/v1"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/dynamic"
@ -205,18 +204,14 @@ func (sc *httpProxySource) endpointsFromTemplate(httpProxy *projectcontour.HTTPP
var endpoints []*endpoint.Endpoint
for _, hostname := range hostnames {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
}
return endpoints, nil
}
// filterByAnnotations filters a list of configs by a given annotation selector.
func (sc *httpProxySource) filterByAnnotations(httpProxies []*projectcontour.HTTPProxy) ([]*projectcontour.HTTPProxy, error) {
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(sc.annotationFilter)
if err != nil {
return nil, err
}
@ -263,7 +258,7 @@ func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTP
if virtualHost := httpProxy.Spec.VirtualHost; virtualHost != nil {
if fqdn := virtualHost.Fqdn; fqdn != "" {
endpoints = append(endpoints, endpointsForHostname(fqdn, targets, ttl, providerSpecific, setIdentifier, resource)...)
endpoints = append(endpoints, EndpointsForHostname(fqdn, targets, ttl, providerSpecific, setIdentifier, resource)...)
}
}
@ -271,7 +266,7 @@ func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTP
if !sc.ignoreHostnameAnnotation {
hostnameList := annotations.HostnamesFromAnnotations(httpProxy.Annotations)
for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
}
}

Some files were not shown because too many files have changed in this diff Show More