mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
Merge branch 'master' of github.com:7onn/external-dns into 7onn/cloudflare-tags
This commit is contained in:
commit
b9f9cde0ed
2
.github/workflows/dependency-update.yaml
vendored
2
.github/workflows/dependency-update.yaml
vendored
@ -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 }}
|
||||
|
@ -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
|
||||
|
@ -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
4
apis/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- apis
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
# external-dns
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
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. |
|
||||
|
@ -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 }}
|
||||
|
@ -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 }}
|
||||
|
@ -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 }}
|
||||
|
@ -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 }}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
|
@ -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
4
controller/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- controller
|
@ -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.",
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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. |
|
||||
|
@ -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 |
|
||||
|
@ -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 | |
|
||||
|
@ -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"
|
||||
|
@ -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`.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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{}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
88
go.mod
@ -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
182
go.sum
@ -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
4
internal/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- internal
|
@ -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
4
kustomize/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- kustomize
|
4
pkg/apis/OWNERS
Normal file
4
pkg/apis/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- apis
|
@ -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
97
pkg/http/http.go
Normal 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
81
pkg/http/http_test.go
Normal 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
4
pkg/metrics/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- metrics
|
@ -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:
|
||||
|
@ -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 }),
|
||||
}
|
||||
}
|
||||
|
@ -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
4
pkg/rfc2317/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- rfc2317
|
@ -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
4
pkg/tlsutils/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- tls
|
4
plan/OWNERS
Normal file
4
plan/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- plan
|
22
plan/plan.go
22
plan/plan.go
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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"),
|
||||
|
@ -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;
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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.",
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
@ -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: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
34
provider/cloudflare/pagination.go
Normal file
34
provider/cloudflare/pagination.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
94
provider/cloudflare/pagination_test.go
Normal file
94
provider/cloudflare/pagination_test.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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{}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
4
registry/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- registry
|
@ -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
|
||||
|
@ -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
4
scripts/OWNERS
Normal file
@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
labels:
|
||||
- scripts
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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 {
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user