mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
Merge branch 'master' of https://github.com/kubernetes-sigs/external-dns into fix/issue-4914
This commit is contained in:
commit
e67d7bfb80
8
.github/ISSUE_TEMPLATE/---bug-report.md
vendored
8
.github/ISSUE_TEMPLATE/---bug-report.md
vendored
@ -7,8 +7,10 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!
|
||||
<!--
|
||||
Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!
|
||||
|
||||
Make sure to validate the behavior against latest release https://github.com/kubernetes-sigs/external-dns/releases as we don't support past versions.
|
||||
-->
|
||||
|
||||
**What happened**:
|
||||
@ -17,6 +19,10 @@ assignees: ''
|
||||
|
||||
**How to reproduce it (as minimally and precisely as possible)**:
|
||||
|
||||
<!--
|
||||
Please provide as much detail as possible, including Kubernetes manifests with spec.status, ExternalDNS arguments, and logs. A bug that cannot be reproduced won't be fixed.
|
||||
-->
|
||||
|
||||
**Anything else we need to know?**:
|
||||
|
||||
**Environment**:
|
||||
|
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.5
|
||||
with:
|
||||
# https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
2
.github/workflows/lint.yaml
vendored
2
.github/workflows/lint.yaml
vendored
@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
# Run Spectral
|
||||
- name: Lint OpenAPI spec
|
||||
uses: stoplightio/spectral-action@577bade2d6e0eeb50528c94182a5588bf961ae8f # v0.8.12
|
||||
uses: stoplightio/spectral-action@6416fd018ae38e60136775066eb3e98172143141 # v0.8.13
|
||||
with:
|
||||
file_glob: 'api/*.yaml'
|
||||
|
||||
|
@ -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
|
||||
|
@ -66,6 +66,7 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz
|
||||
- [Plural](https://www.plural.sh/)
|
||||
- [Pi-hole](https://pi-hole.net/)
|
||||
- [Alibaba Cloud DNS](https://www.alibabacloud.com/help/en/dns)
|
||||
- [Myra Security DNS](https://www.myrasecurity.com/en/saasp/application-security/secure-dns/)
|
||||
|
||||
ExternalDNS is, by default, aware of the records it is managing, therefore it can safely manage non-empty hosted zones.
|
||||
We strongly encourage you to set `--txt-owner-id` to a unique value that doesn't change for the lifetime of your cluster.
|
||||
@ -81,7 +82,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 |
|
||||
| --------------------- | -------------------------------------------------------------------- |
|
||||
@ -100,6 +105,7 @@ Known providers using webhooks:
|
||||
| IONOS | https://github.com/ionos-cloud/external-dns-ionos-webhook |
|
||||
| Infoblox | https://github.com/AbsaOSS/external-dns-infoblox-webhook |
|
||||
| Mikrotik | https://github.com/mirceanton/external-dns-provider-mikrotik |
|
||||
| Myra Security | https://github.com/Myra-Security-GmbH/external-dns-myrasec-webhook |
|
||||
| Netcup | https://github.com/mrueg/external-dns-netcup-webhook |
|
||||
| Netic | https://github.com/neticdk/external-dns-tidydns-webhook |
|
||||
| OpenStack Designate | https://github.com/inovex/external-dns-designate-webhook |
|
||||
@ -199,6 +205,7 @@ The following tutorials are provided:
|
||||
- [IONOS Cloud](docs/tutorials/ionoscloud.md)
|
||||
- [Istio Gateway Source](docs/sources/istio.md)
|
||||
- [Linode](docs/tutorials/linode.md)
|
||||
- [Myra Security](docs/tutorials/myra.md)
|
||||
- [NS1](docs/tutorials/ns1.md)
|
||||
- [NS Record Creation with CRD Source](docs/sources/ns-record.md)
|
||||
- [MX Record Creation with CRD Source](docs/sources/mx-record.md)
|
||||
|
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"
|
||||
@ -423,11 +425,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. |
|
||||
@ -66,7 +66,7 @@
|
||||
| `--google-zone-visibility=` | When using the Google provider, filter for zones with this visibility (optional, options: public, private) |
|
||||
| `--alibaba-cloud-config-file="/etc/kubernetes/alibaba-cloud.json"` | When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud) |
|
||||
| `--alibaba-cloud-zone-type=` | When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private) |
|
||||
| `--aws-zone-type=` | When using the AWS provider, filter for zones of this type (optional, options: public, private) |
|
||||
| `--aws-zone-type=` | When using the AWS provider, filter for zones of this type (optional, default: any, options: public, private) |
|
||||
| `--aws-zone-tags=` | When using the AWS provider, filter for zones with these tags |
|
||||
| `--aws-profile=` | When using the AWS provider, name of the profile to use |
|
||||
| `--aws-assume-role=""` | When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional) |
|
||||
|
@ -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 |
|
||||
|
@ -20,6 +20,7 @@ Provider supported configurations
|
||||
| Google GCP | n/a | yes | 300 |
|
||||
| InMemory | n/a | n/a | n/a |
|
||||
| Linode | n/a | n/a | n/a |
|
||||
| Myra Security | n/a | yes | 300 |
|
||||
| NS1 | n/a | yes | 10 |
|
||||
| OCI | yes | yes | 300 |
|
||||
| OVH | n/a | yes | 0 |
|
||||
|
@ -118,19 +118,19 @@ Note that the key used for encryption should be a secure key and properly manage
|
||||
Python
|
||||
|
||||
```python
|
||||
python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())'
|
||||
python -c 'import os,base64; print(base64.standard_b64encode(os.urandom(32)).decode())'
|
||||
```
|
||||
|
||||
Bash
|
||||
|
||||
```shell
|
||||
dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d -- '\n' | tr -- '+/' '-_'; echo
|
||||
dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64; echo
|
||||
```
|
||||
|
||||
OpenSSL
|
||||
|
||||
```shell
|
||||
openssl rand -base64 32 | tr -- '+/' '-_'
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
PowerShell
|
||||
@ -138,7 +138,7 @@ PowerShell
|
||||
```powershell
|
||||
# Add System.Web assembly to session, just in case
|
||||
Add-Type -AssemblyName System.Web
|
||||
[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Web.Security.Membership]::GeneratePassword(32,4))).Replace("+","-").Replace("/","_")
|
||||
[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Web.Security.Membership]::GeneratePassword(32,4)))
|
||||
```
|
||||
|
||||
Terraform
|
||||
@ -146,7 +146,6 @@ Terraform
|
||||
```hcl
|
||||
resource "random_password" "txt_key" {
|
||||
length = 32
|
||||
override_special = "-_"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -5,7 +5,7 @@ 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 |
|
||||
| connector | | | |
|
||||
| contour-httpproxy | HttpProxy.projectcontour.io | Yes | |
|
||||
@ -24,7 +24,7 @@ ExternalDNS watches the specified sources for hostname information and uses it t
|
||||
| [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 | | |
|
||||
| [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
|
||||
|
@ -4,6 +4,37 @@ This tutorial describes how to setup ExternalDNS for usage within a Kubernetes c
|
||||
|
||||
Make sure to use **>=0.4.2** version of ExternalDNS for this tutorial.
|
||||
|
||||
## CloudFlare SDK Migration Status
|
||||
|
||||
ExternalDNS is currently migrating from the legacy CloudFlare Go SDK v0 to the modern v4 SDK to improve performance, reliability, and access to newer CloudFlare features. The migration status is:
|
||||
|
||||
**✅ Fully migrated to v4 SDK:**
|
||||
|
||||
- Zone management (listing, filtering, pagination)
|
||||
- Zone details retrieval (`GetZone`)
|
||||
- Zone ID lookup by name (`ZoneIDByName`)
|
||||
- Zone plan detection (fully v4 implementation)
|
||||
- Regional services (data localization)
|
||||
|
||||
**🔄 Still using legacy v0 SDK:**
|
||||
|
||||
- DNS record management (create, update, delete records)
|
||||
- Custom hostnames
|
||||
- Proxied records
|
||||
|
||||
This mixed approach ensures continued functionality while gradually modernizing the codebase. Users should not experience any breaking changes during this transition.
|
||||
|
||||
### SDK Dependencies
|
||||
|
||||
ExternalDNS currently uses:
|
||||
|
||||
- **cloudflare-go v0.115.0+**: Legacy SDK for DNS records, custom hostnames, and proxied record features
|
||||
- **cloudflare-go/v4 v4.6.0+**: Modern SDK for all zone management and regional services operations
|
||||
|
||||
Zone management has been fully migrated to the v4 SDK, providing improved performance and reliability.
|
||||
|
||||
Both SDKs are automatically managed as Go module dependencies and require no special configuration from users.
|
||||
|
||||
## Creating a Cloudflare DNS zone
|
||||
|
||||
We highly recommend to read this tutorial if you haven't used Cloudflare before:
|
||||
@ -353,7 +384,7 @@ The custom hostname DNS must resolve to the Cloudflare DNS record (`external-dns
|
||||
|
||||
Requires [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) product and "SSL and Certificates" API permission.
|
||||
|
||||
Due to a limitation within the cloudflare-go v0 API, the custom hostname page size is fixed at 50.
|
||||
**Note:** Due to using the legacy cloudflare-go v0 API for custom hostname management, the custom hostname page size is fixed at 50. This limitation will be addressed in a future migration to the v4 SDK.
|
||||
|
||||
## Using CRD source to manage DNS records in Cloudflare
|
||||
|
||||
|
@ -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
|
||||
|
215
docs/tutorials/myra.md
Normal file
215
docs/tutorials/myra.md
Normal file
@ -0,0 +1,215 @@
|
||||
# Myra ExternalDNS Webhook
|
||||
|
||||
This guide provides quick instructions for setting up and testing the [Myra ExternalDNS Webhook](https://github.com/Myra-Security-GmbH/external-dns-myrasec-webhook) in a Kubernetes environment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes cluster (v1.19+)
|
||||
- `kubectl` configured to access your cluster
|
||||
- Docker for building the container image
|
||||
- MyraSec API credentials (API key and secret)
|
||||
- Domain registered with MyraSec
|
||||
|
||||
## Quick Installation
|
||||
|
||||
### 1. Build and Push the Docker Image
|
||||
|
||||
```bash
|
||||
# From the project root
|
||||
docker build -t myra-webhook:latest .
|
||||
|
||||
# Tag the image for your container registry
|
||||
docker tag myra-webhook:latest YOUR_REGISTRY/myra-webhook:latest
|
||||
|
||||
# Push to your container registry
|
||||
docker push YOUR_REGISTRY/myra-webhook:latest
|
||||
```
|
||||
|
||||
> **Important**: The image must be pushed to a container registry accessible by your Kubernetes cluster. Update the image reference in the deployment YAML file to match your registry path.
|
||||
|
||||
### 2. Configure API Credentials
|
||||
|
||||
Create a secret with your MyraSec API credentials:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic myra-webhook-secrets \
|
||||
--from-literal=myrasec-api-key=YOUR_API_KEY \
|
||||
--from-literal=myrasec-api-secret=YOUR_API_SECRET \
|
||||
--from-literal=domain-filter=YOUR_DOMAIN.com
|
||||
```
|
||||
|
||||
Alternatively, apply the provided secret template after editing:
|
||||
|
||||
```bash
|
||||
# Edit the secret file first
|
||||
vi deploy/myra-webhook-secrets.yaml
|
||||
|
||||
# Then apply
|
||||
kubectl apply -f deploy/myra-webhook-secrets.yaml
|
||||
```
|
||||
|
||||
### 3. Deploy the Webhook and ExternalDNS
|
||||
|
||||
```bash
|
||||
# Apply the combined deployment
|
||||
kubectl apply -f deploy/combined-deployment.yaml
|
||||
```
|
||||
|
||||
This deploys:
|
||||
|
||||
- ConfigMap with webhook configuration
|
||||
- ServiceAccount, ClusterRole, and ClusterRoleBinding for RBAC
|
||||
- Deployment with two containers:
|
||||
- myra-webhook: The webhook provider implementation
|
||||
- external-dns: The ExternalDNS controller using the webhook provider
|
||||
|
||||
### 4. Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check if pods are running
|
||||
kubectl get pods -l app=myra-externaldns
|
||||
|
||||
# Check logs for the webhook container
|
||||
kubectl logs -l app=myra-externaldns -c myra-webhook
|
||||
|
||||
# Check logs for the external-dns container
|
||||
kubectl logs -l app=myra-externaldns -c external-dns
|
||||
```
|
||||
|
||||
## Manual Testing with NGINX Demo
|
||||
|
||||
### 1. Deploy the NGINX Demo Application
|
||||
|
||||
```bash
|
||||
# Edit the domain in the nginx-demo.yaml file to match your domain
|
||||
vi deploy/nginx-demo.yaml
|
||||
|
||||
# Most important part is to set the correct domain in the external-dns.alpha.kubernetes.io/hostname annotation
|
||||
# Example:
|
||||
# annotations:
|
||||
# external-dns.alpha.kubernetes.io/enabled: "true"
|
||||
# external-dns.alpha.kubernetes.io/hostname: "nginx-demo.dummydomainforkubes.de"
|
||||
# external-dns.alpha.kubernetes.io/target: "9.2.3.4"
|
||||
|
||||
# Apply the demo resources
|
||||
kubectl apply -f deploy/nginx-demo.yaml
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
||||
- NGINX Deployment
|
||||
- Service for the deployment
|
||||
- Ingress resource with ExternalDNS annotations
|
||||
|
||||
### 2. Verify DNS Record Creation
|
||||
|
||||
After deploying the demo application, ExternalDNS should automatically create DNS records in MyraSec:
|
||||
|
||||
```bash
|
||||
# Check external-dns logs to see record creation
|
||||
kubectl logs -l app=myra-externaldns -c external-dns | grep "nginx-demo"
|
||||
|
||||
# Verify the webhook logs
|
||||
kubectl logs -l app=myra-externaldns -c myra-webhook | grep "Created DNS record"
|
||||
```
|
||||
|
||||
You can also verify through the MyraSec dashboard that the records were created.
|
||||
|
||||
### 3. Testing Record Deletion
|
||||
|
||||
To test record deletion:
|
||||
|
||||
```bash
|
||||
# Delete the nginx-demo resources or remove annotation from ingress
|
||||
kubectl delete -f deploy/nginx-demo.yaml
|
||||
|
||||
# Delete the ingress resource or remove annotation from ingress
|
||||
# If resource is still active, external dns might still see the record and manage it
|
||||
kubectl delete ingress nginx-demo -n default
|
||||
|
||||
# Check external-dns logs to see record deletion
|
||||
kubectl logs -l app=myra-externaldns -c external-dns | grep "nginx-demo" | grep "delete"
|
||||
|
||||
# Verify the webhook logs
|
||||
kubectl logs -l app=myra-externaldns -c myra-webhook | grep "Deleted DNS record"
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The webhook can be configured through the ConfigMap:
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `dry-run` | Run in dry-run mode without making actual changes | `"false"` |
|
||||
| `environment` | Environment name (affects private IP handling) | `"prod"` |
|
||||
| `log-level` | Logging level (debug, info, warn, error) | `"debug"` |
|
||||
| `ttl` | Default TTL for DNS records | `"300"` |
|
||||
| `webhook-listen-address` | Address and port for the webhook server | `":8080"` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Webhook not receiving requests**
|
||||
- Ensure the `webhook-provider-url` in the external-dns args is correct
|
||||
- Check network connectivity between containers
|
||||
|
||||
2. **DNS records not being created**
|
||||
- Verify MyraSec API credentials are correct
|
||||
- Check if the domain filter is properly configured
|
||||
- Look for error messages in the webhook and external-dns logs
|
||||
|
||||
3. **Permissions issues**
|
||||
- Ensure the ServiceAccount has the correct RBAC permissions
|
||||
|
||||
### Getting Help
|
||||
|
||||
For more detailed logs:
|
||||
|
||||
```bash
|
||||
# Set log level to debug in the ConfigMap
|
||||
kubectl edit configmap myra-externaldns-config
|
||||
# Change log-level to "debug"
|
||||
|
||||
# Restart the pods
|
||||
kubectl rollout restart deployment myra-externaldns
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
The webhook supports different environment configurations through the `environment` setting in the ConfigMap:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: myra-externaldns-config
|
||||
data:
|
||||
environment: "prod" # Can be "prod", "staging", "dev", etc.
|
||||
```
|
||||
|
||||
The environment setting affects how the webhook handles certain operations:
|
||||
|
||||
| Environment | Behavior |
|
||||
|-------------|----------|
|
||||
| `prod`, `production`, `staging` | Strict mode: Skips private IP records, enforces stricter validation |
|
||||
| `dev`, `development`, `test`, etc. | Development mode: Allows private IP records, more permissive validation |
|
||||
|
||||
To modify the environment:
|
||||
|
||||
```bash
|
||||
# Edit the ConfigMap directly
|
||||
kubectl edit configmap myra-externaldns-config
|
||||
|
||||
# Or apply an updated YAML file
|
||||
kubectl apply -f updated-config.yaml
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
For production deployments, consider:
|
||||
|
||||
1. Using a proper image registry instead of `latest` tag
|
||||
2. Setting resource limits appropriate for your environment
|
||||
3. Configuring horizontal pod autoscaling
|
||||
4. Using Helm for deployment management
|
@ -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
|
||||
}
|
||||
|
@ -25,7 +25,8 @@ import (
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"sigs.k8s.io/external-dns/internal/idna"
|
||||
)
|
||||
|
||||
type MatchAllDomainFilters []DomainFilterInterface
|
||||
@ -247,9 +248,9 @@ func (df *DomainFilter) MatchParent(domain string) bool {
|
||||
}
|
||||
|
||||
// normalizeDomain converts a domain to a canonical form, so that we can filter on it
|
||||
// it: trim "." suffix, get Unicode version of domain complient with Section 5 of RFC 5891
|
||||
// it: trim "." suffix, get Unicode version of domain compliant with Section 5 of RFC 5891
|
||||
func normalizeDomain(domain string) string {
|
||||
s, err := idna.Lookup.ToUnicode(strings.TrimSuffix(domain, "."))
|
||||
s, err := idna.Profile.ToUnicode(strings.TrimSuffix(domain, "."))
|
||||
if err != nil {
|
||||
log.Warnf(`Got error while parsing domain %s: %v`, domain, err)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
100
go.mod
100
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/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/civo/civogo v0.6.2
|
||||
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.160.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,63 +38,63 @@ 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.96.0
|
||||
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.4
|
||||
go.etcd.io/etcd/client/v3 v3.6.4
|
||||
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.243.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
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.16.2 // indirect
|
||||
cloud.google.com/go/auth v0.16.3 // 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
|
||||
@ -120,7 +121,7 @@ require (
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // 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.4 // 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,13 +179,13 @@ 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
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // 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-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
|
210
go.sum
210
go.sum
@ -2,8 +2,8 @@ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxo
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
|
||||
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
|
||||
cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
|
||||
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
@ -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=
|
||||
@ -172,20 +172,22 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
|
||||
github.com/civo/civogo v0.6.1 h1:PFOh7rBU0vmj7LTDIv3z7l9uXG4SZyyzScCl3wyTFSc=
|
||||
github.com/civo/civogo v0.6.1/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
|
||||
github.com/civo/civogo v0.6.2 h1:tQegf+coNxIKhLjOo5bwAV04CPSk6ealSod55XHb7cw=
|
||||
github.com/civo/civogo v0.6.2/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cloudflare-go v0.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.160.0 h1:3Wa6mOzv1m5DZQDANAk8u6v4DIUm5x2i4tZ7ke28lhs=
|
||||
github.com/digitalocean/godo v1.160.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=
|
||||
@ -501,8 +503,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
|
||||
@ -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.96.0 h1:ew0WavsB6N/I6etYCC160cD5qDXbek/1xZgujqTzork=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.96.0/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.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
|
||||
go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
|
||||
go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
|
||||
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
|
||||
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.243.0 h1:sw+ESIJ4BVnlJcWu9S+p2Z6Qq1PjG77T8IJ1xtp4jZQ=
|
||||
google.golang.org/api v0.243.0/go.mod h1:GE4QtYfaybx1KmeHMdBnNnyLzBZCVihGBXAmJu/uUr8=
|
||||
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=
|
||||
@ -1355,12 +1365,12 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@ -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) {
|
||||
|
29
internal/idna/idna.go
Normal file
29
internal/idna/idna.go
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 idna
|
||||
|
||||
import (
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
var (
|
||||
Profile = idna.New(
|
||||
idna.MapForLookup(),
|
||||
idna.Transitional(true),
|
||||
idna.StrictDomainName(false),
|
||||
)
|
||||
)
|
59
internal/idna/idna_test.go
Normal file
59
internal/idna/idna_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
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 idna
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProfileWithDefault(t *testing.T) {
|
||||
tets := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "*.GÖPHER.com",
|
||||
expected: "*.göpher.com",
|
||||
},
|
||||
{
|
||||
input: "*._abrakadabra.com",
|
||||
expected: "*._abrakadabra.com",
|
||||
},
|
||||
{
|
||||
input: "_abrakadabra.com",
|
||||
expected: "_abrakadabra.com",
|
||||
},
|
||||
{
|
||||
input: "*.foo.kube.example.com",
|
||||
expected: "*.foo.kube.example.com",
|
||||
},
|
||||
{
|
||||
input: "xn--bcher-kva.example.com",
|
||||
expected: "bücher.example.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tets {
|
||||
t.Run(strings.ToLower(tt.input), func(t *testing.T) {
|
||||
result, err := Profile.ToUnicode(tt.input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
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
|
@ -115,7 +115,6 @@ type Config struct {
|
||||
CloudflareCustomHostnamesCertificateAuthority string
|
||||
CloudflareRegionalServices bool
|
||||
CloudflareRegionKey string
|
||||
CloudflareRecordComment string
|
||||
CoreDNSPrefix string
|
||||
AkamaiServiceConsumerDomain string
|
||||
AkamaiClientToken string
|
||||
@ -209,7 +208,7 @@ type Config struct {
|
||||
WebhookProviderReadTimeout time.Duration
|
||||
WebhookProviderWriteTimeout time.Duration
|
||||
WebhookServer bool
|
||||
TraefikDisableLegacy bool
|
||||
TraefikEnableLegacy bool
|
||||
TraefikDisableNew bool
|
||||
NAT64Networks []string
|
||||
ExcludeUnschedulable bool
|
||||
@ -359,7 +358,7 @@ var defaultConfig = &Config{
|
||||
TLSCA: "",
|
||||
TLSClientCert: "",
|
||||
TLSClientCertKey: "",
|
||||
TraefikDisableLegacy: false,
|
||||
TraefikEnableLegacy: false,
|
||||
TraefikDisableNew: false,
|
||||
TransIPAccountName: "",
|
||||
TransIPPrivateKeyFile: "",
|
||||
@ -486,7 +485,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
|
||||
@ -505,7 +504,7 @@ func App(cfg *Config) *kingpin.Application {
|
||||
app.Flag("google-zone-visibility", "When using the Google provider, filter for zones with this visibility (optional, options: public, private)").Default(defaultConfig.GoogleZoneVisibility).EnumVar(&cfg.GoogleZoneVisibility, "", "public", "private")
|
||||
app.Flag("alibaba-cloud-config-file", "When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud)").Default(defaultConfig.AlibabaCloudConfigFile).StringVar(&cfg.AlibabaCloudConfigFile)
|
||||
app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private")
|
||||
app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
|
||||
app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, default: any, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
|
||||
app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter)
|
||||
app.Flag("aws-profile", "When using the AWS provider, name of the profile to use").Default("").StringsVar(&cfg.AWSProfiles)
|
||||
app.Flag("aws-assume-role", "When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole)
|
||||
@ -535,7 +534,7 @@ func App(cfg *Config) *kingpin.Application {
|
||||
app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage)
|
||||
app.Flag("cloudflare-regional-services", "When using the Cloudflare provider, specify if Regional Services feature will be used (default: disabled)").Default(strconv.FormatBool(defaultConfig.CloudflareRegionalServices)).BoolVar(&cfg.CloudflareRegionalServices)
|
||||
app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the default region for Regional Services. Any value other than an empty string will enable the Regional Services feature (optional)").StringVar(&cfg.CloudflareRegionKey)
|
||||
app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment)
|
||||
app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareDNSRecordsComment)
|
||||
|
||||
app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)
|
||||
app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain)
|
||||
|
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
|
16
plan/plan.go
16
plan/plan.go
@ -18,13 +18,14 @@ package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/internal/idna"
|
||||
)
|
||||
|
||||
// PropertyComparator is used in Plan for comparing the previous and current custom annotations.
|
||||
@ -342,7 +343,7 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Ma
|
||||
// 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 := idna.Profile.ToASCII(strings.TrimSpace(dnsName))
|
||||
if err != nil {
|
||||
log.Warnf(`Got error while parsing DNSName %s: %v`, dnsName, err)
|
||||
}
|
||||
@ -353,15 +354,8 @@ func normalizeDNSName(dnsName string) string {
|
||||
}
|
||||
|
||||
func IsManagedRecord(record string, managedRecords, excludeRecords []string) bool {
|
||||
for _, r := range excludeRecords {
|
||||
if record == r {
|
||||
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 {
|
||||
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))}
|
||||
|
@ -60,12 +60,20 @@ const (
|
||||
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
|
||||
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)
|
||||
@ -691,7 +734,11 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
|
||||
log.Infof("Desired change: %s %s %s", c.Action, *c.ResourceRecordSet.Name, c.ResourceRecordSet.Type)
|
||||
}
|
||||
|
||||
if !p.dryRun {
|
||||
if p.dryRun {
|
||||
log.Debug("Dry run mode, skipping change submission")
|
||||
continue
|
||||
}
|
||||
|
||||
params := &route53.ChangeResourceRecordSetsInput{
|
||||
HostedZoneId: aws.String(z),
|
||||
ChangeBatch: &route53types.ChangeBatch{
|
||||
@ -742,7 +789,6 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
|
||||
time.Sleep(p.batchChangeInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if failedUpdate {
|
||||
failedZones = append(failedZones, z)
|
||||
@ -832,12 +878,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 +992,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 +1007,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 +1122,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),
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ type AWSSDProvider struct {
|
||||
// only consider namespaces ending in this suffix
|
||||
namespaceFilter *endpoint.DomainFilter
|
||||
// filter namespace by type (private or public)
|
||||
namespaceTypeFilter sdtypes.NamespaceFilter
|
||||
namespaceTypeFilter []sdtypes.NamespaceFilter
|
||||
// enables service without instances cleanup
|
||||
cleanEmptyService bool
|
||||
// filter services for removal
|
||||
@ -102,21 +102,28 @@ func NewAWSSDProvider(domainFilter *endpoint.DomainFilter, namespaceType string,
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// newSdNamespaceFilter initialized AWS SD Namespace Filter based on given string config
|
||||
func newSdNamespaceFilter(namespaceTypeConfig string) sdtypes.NamespaceFilter {
|
||||
// newSdNamespaceFilter returns NamespaceFilter based on the given namespace type configuration.
|
||||
// If the config is "public", it filters for public namespaces; if "private", for private namespaces.
|
||||
// For any other value (including empty), it returns filters for both public and private namespaces.
|
||||
// ref: https://docs.aws.amazon.com/cloud-map/latest/api/API_ListNamespaces.html
|
||||
func newSdNamespaceFilter(namespaceTypeConfig string) []sdtypes.NamespaceFilter {
|
||||
switch namespaceTypeConfig {
|
||||
case sdNamespaceTypePublic:
|
||||
return sdtypes.NamespaceFilter{
|
||||
return []sdtypes.NamespaceFilter{
|
||||
{
|
||||
Name: sdtypes.NamespaceFilterNameType,
|
||||
Values: []string{string(sdtypes.NamespaceTypeDnsPublic)},
|
||||
},
|
||||
}
|
||||
case sdNamespaceTypePrivate:
|
||||
return sdtypes.NamespaceFilter{
|
||||
return []sdtypes.NamespaceFilter{
|
||||
{
|
||||
Name: sdtypes.NamespaceFilterNameType,
|
||||
Values: []string{string(sdtypes.NamespaceTypeDnsPrivate)},
|
||||
},
|
||||
}
|
||||
default:
|
||||
return sdtypes.NamespaceFilter{}
|
||||
return []sdtypes.NamespaceFilter{}
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,12 +137,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 +253,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]
|
||||
|
||||
@ -350,7 +361,7 @@ func (p *AWSSDProvider) ListNamespaces(ctx context.Context) ([]*sdtypes.Namespac
|
||||
namespaces := make([]*sdtypes.NamespaceSummary, 0)
|
||||
|
||||
paginator := sd.NewListNamespacesPaginator(p.client, &sd.ListNamespacesInput{
|
||||
Filters: []sdtypes.NamespaceFilter{p.namespaceTypeFilter},
|
||||
Filters: p.namespaceTypeFilter,
|
||||
})
|
||||
for paginator.HasMorePages() {
|
||||
resp, err := paginator.NextPage(ctx)
|
||||
@ -618,12 +629,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
|
||||
|
@ -254,14 +254,14 @@ func TestAWSSDProvider_ApplyChanges_Update(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// apply creates
|
||||
provider.ApplyChanges(ctx, &plan.Changes{
|
||||
_ = provider.ApplyChanges(ctx, &plan.Changes{
|
||||
Create: oldEndpoints,
|
||||
})
|
||||
|
||||
ctx = context.Background()
|
||||
|
||||
// apply update
|
||||
provider.ApplyChanges(ctx, &plan.Changes{
|
||||
_ = provider.ApplyChanges(ctx, &plan.Changes{
|
||||
UpdateOld: oldEndpoints,
|
||||
UpdateNew: newEndpoints,
|
||||
})
|
||||
@ -306,6 +306,7 @@ func TestAWSSDProvider_ListNamespaces(t *testing.T) {
|
||||
}{
|
||||
{"public filter", endpoint.NewDomainFilter([]string{}), "public", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
|
||||
{"private filter", endpoint.NewDomainFilter([]string{}), "private", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["private"])}},
|
||||
{"optional filter", endpoint.NewDomainFilter([]string{}), "", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"]), namespaceToNamespaceSummary(namespaces["private"])}},
|
||||
{"domain filter", endpoint.NewDomainFilter([]string{"public.com"}), "", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
|
||||
{"non-existing domain", endpoint.NewDomainFilter([]string{"xxx.com"}), "", []*sdtypes.NamespaceSummary{}},
|
||||
} {
|
||||
@ -913,7 +914,7 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) {
|
||||
}
|
||||
|
||||
// AWS NLB instance (ALIAS)
|
||||
provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{
|
||||
_ = provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
DNSName: "service1.private.com.",
|
||||
RecordTTL: 300,
|
||||
@ -927,7 +928,7 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) {
|
||||
}
|
||||
|
||||
// CNAME instance
|
||||
provider.RegisterInstance(context.Background(), services["private"]["cname-srv"], &endpoint.Endpoint{
|
||||
_ = provider.RegisterInstance(context.Background(), services["private"]["cname-srv"], &endpoint.Endpoint{
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
DNSName: "service2.private.com.",
|
||||
RecordTTL: 300,
|
||||
@ -1001,7 +1002,7 @@ func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
|
||||
|
||||
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
|
||||
|
||||
provider.DeregisterInstance(context.Background(), services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))
|
||||
_ = provider.DeregisterInstance(context.Background(), services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))
|
||||
|
||||
assert.Empty(t, instances["srv1"])
|
||||
}
|
||||
|
@ -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,10 @@ 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"
|
||||
"github.com/cloudflare/cloudflare-go/v4/zones"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
||||
@ -103,16 +107,16 @@ var recordTypeCustomHostnameSupported = map[string]bool{
|
||||
// cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly.
|
||||
type cloudFlareDNS interface {
|
||||
ZoneIDByName(zoneName string) (string, error)
|
||||
ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error)
|
||||
ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error)
|
||||
ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone]
|
||||
GetZone(ctx context.Context, zoneID string) (*zones.Zone, error)
|
||||
ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error)
|
||||
CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.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)
|
||||
@ -120,10 +124,27 @@ type cloudFlareDNS interface {
|
||||
|
||||
type zoneService struct {
|
||||
service *cloudflare.API
|
||||
serviceV4 *cloudflarev4.Client
|
||||
}
|
||||
|
||||
func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
|
||||
return z.service.ZoneIDByName(zoneName)
|
||||
// Use v4 API to find zone by name
|
||||
params := zones.ZoneListParams{
|
||||
Name: cloudflarev4.F(zoneName),
|
||||
}
|
||||
|
||||
iter := z.serviceV4.Zones.ListAutoPaging(context.Background(), params)
|
||||
for zone := range autoPagerIterator(iter) {
|
||||
if zone.Name == zoneName {
|
||||
return zone.ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := iter.Err(); err != nil {
|
||||
return "", fmt.Errorf("failed to list zones from CloudFlare API: %w", err)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("zone %q not found in CloudFlare account - verify the zone exists and API credentials have access to it", zoneName)
|
||||
}
|
||||
|
||||
func (z zoneService) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) {
|
||||
@ -143,12 +164,12 @@ func (z zoneService) DeleteDNSRecord(ctx context.Context, rc *cloudflare.Resourc
|
||||
return z.service.DeleteDNSRecord(ctx, rc, recordID)
|
||||
}
|
||||
|
||||
func (z zoneService) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
|
||||
return z.service.ListZonesContext(ctx, opts...)
|
||||
func (z zoneService) ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone] {
|
||||
return z.serviceV4.Zones.ListAutoPaging(ctx, params)
|
||||
}
|
||||
|
||||
func (z zoneService) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) {
|
||||
return z.service.ZoneDetails(ctx, zoneID)
|
||||
func (z zoneService) GetZone(ctx context.Context, zoneID string) (*zones.Zone, error) {
|
||||
return z.serviceV4.Zones.Get(ctx, zones.ZoneGetParams{ZoneID: cloudflarev4.F(zoneID)})
|
||||
}
|
||||
|
||||
func (z zoneService) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) {
|
||||
@ -163,21 +184,31 @@ func (z zoneService) CreateCustomHostname(ctx context.Context, zoneID string, ch
|
||||
return z.service.CreateCustomHostname(ctx, zoneID, ch)
|
||||
}
|
||||
|
||||
// listZonesV4Params returns the appropriate Zone List Params for v4 API
|
||||
func listZonesV4Params() zones.ZoneListParams {
|
||||
return zones.ZoneListParams{}
|
||||
}
|
||||
|
||||
type DNSRecordsConfig struct {
|
||||
PerPage int
|
||||
Comment string
|
||||
}
|
||||
|
||||
func (c *DNSRecordsConfig) trimAndValidateComment(dnsName, comment string, paidZone func(string) bool) string {
|
||||
if len(comment) > freeZoneMaxCommentLength {
|
||||
if !paidZone(dnsName) {
|
||||
log.Warnf("DNS record comment is invalid. Trimming comment of %s. To avoid endless syncs, please set it to less than %d chars.", dnsName, freeZoneMaxCommentLength)
|
||||
return comment[:freeZoneMaxCommentLength]
|
||||
} else if len(comment) > paidZoneMaxCommentLength {
|
||||
log.Warnf("DNS record comment is invalid. Trimming comment of %s. To avoid endless syncs, please set it to less than %d chars.", dnsName, paidZoneMaxCommentLength)
|
||||
return comment[:paidZoneMaxCommentLength]
|
||||
if len(comment) <= freeZoneMaxCommentLength {
|
||||
return comment
|
||||
}
|
||||
|
||||
maxLength := freeZoneMaxCommentLength
|
||||
if paidZone(dnsName) {
|
||||
maxLength = paidZoneMaxCommentLength
|
||||
}
|
||||
|
||||
if len(comment) > maxLength {
|
||||
log.Warnf("DNS record comment is invalid. Trimming comment of %s. To avoid endless syncs, please set it to less than %d chars.", dnsName, maxLength)
|
||||
return comment[:maxLength]
|
||||
}
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
@ -193,13 +224,13 @@ func (p *CloudFlareProvider) ZoneHasPaidPlan(hostname string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
zoneDetails, err := p.Client.ZoneDetails(context.Background(), zoneID)
|
||||
zoneDetails, err := p.Client.GetZone(context.Background(), zoneID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get zone %s details %v", zone, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return zoneDetails.Plan.IsSubscribed
|
||||
return zoneDetails.Plan.IsSubscribed //nolint:staticcheck // SA1019: Plan.IsSubscribed is deprecated but no replacement available yet
|
||||
}
|
||||
|
||||
// CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
|
||||
@ -220,7 +251,7 @@ type CloudFlareProvider struct {
|
||||
type cloudFlareChange struct {
|
||||
Action changeAction
|
||||
ResourceRecord cloudflare.DNSRecord
|
||||
RegionalHostname cloudflare.RegionalHostname
|
||||
RegionalHostname regionalHostname
|
||||
CustomHostnames map[string]cloudflare.CustomHostname
|
||||
CustomHostnamesPrev []string
|
||||
}
|
||||
@ -239,6 +270,7 @@ func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams
|
||||
Type: cfc.ResourceRecord.Type,
|
||||
Content: cfc.ResourceRecord.Content,
|
||||
Priority: cfc.ResourceRecord.Priority,
|
||||
Comment: cloudflare.StringPtr(cfc.ResourceRecord.Comment),
|
||||
}
|
||||
|
||||
return params
|
||||
@ -253,6 +285,7 @@ func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordPar
|
||||
Type: cfc.ResourceRecord.Type,
|
||||
Content: cfc.ResourceRecord.Content,
|
||||
Priority: cfc.ResourceRecord.Priority,
|
||||
Comment: cfc.ResourceRecord.Comment,
|
||||
}
|
||||
|
||||
return params
|
||||
@ -288,6 +321,7 @@ func NewCloudFlareProvider(
|
||||
// initialize via chosen auth method and returns new API object
|
||||
var (
|
||||
config *cloudflare.API
|
||||
configV4 *cloudflarev4.Client
|
||||
err error
|
||||
)
|
||||
if os.Getenv("CF_API_TOKEN") != "" {
|
||||
@ -300,8 +334,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)
|
||||
@ -312,7 +353,7 @@ func NewCloudFlareProvider(
|
||||
}
|
||||
|
||||
return &CloudFlareProvider{
|
||||
Client: zoneService{config},
|
||||
Client: zoneService{config, configV4},
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
proxiedByDefault: proxiedByDefault,
|
||||
@ -324,8 +365,8 @@ func NewCloudFlareProvider(
|
||||
}
|
||||
|
||||
// Zones returns the list of hosted zones.
|
||||
func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, error) {
|
||||
var result []cloudflare.Zone
|
||||
func (p *CloudFlareProvider) Zones(ctx context.Context) ([]zones.Zone, error) {
|
||||
var result []zones.Zone
|
||||
|
||||
// if there is a zoneIDfilter configured
|
||||
// && if the filter isn't just a blank string (used in tests)
|
||||
@ -333,34 +374,38 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
|
||||
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
|
||||
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
|
||||
log.Debugf("looking up zone %q", zoneID)
|
||||
detailResponse, err := p.Client.ZoneDetails(ctx, zoneID)
|
||||
detailResponse, err := p.Client.GetZone(ctx, zoneID)
|
||||
if err != nil {
|
||||
log.Errorf("zone %q lookup failed, %v", zoneID, err)
|
||||
return result, err
|
||||
return result, convertCloudflareError(err)
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
"zoneName": detailResponse.Name,
|
||||
"zoneID": detailResponse.ID,
|
||||
}).Debugln("adding zone for consideration")
|
||||
result = append(result, detailResponse)
|
||||
result = append(result, *detailResponse)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
log.Debugln("no zoneIDFilter configured, looking at all zones")
|
||||
|
||||
zonesResponse, err := p.Client.ListZonesContext(ctx)
|
||||
if err != nil {
|
||||
return nil, convertCloudflareError(err)
|
||||
}
|
||||
|
||||
for _, zone := range zonesResponse.Result {
|
||||
params := listZonesV4Params()
|
||||
iter := p.Client.ListZones(ctx, params)
|
||||
for zone := range autoPagerIterator(iter) {
|
||||
if !p.domainFilter.Match(zone.Name) {
|
||||
log.Debugf("zone %q not in domain filter", zone.Name)
|
||||
continue
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
"zoneName": zone.Name,
|
||||
"zoneID": zone.ID,
|
||||
}).Debugln("adding zone for consideration")
|
||||
result = append(result, zone)
|
||||
}
|
||||
if iter.Err() != nil {
|
||||
return nil, convertCloudflareError(iter.Err())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@ -579,7 +624,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
|
||||
"record": change.ResourceRecord.Name,
|
||||
"type": change.ResourceRecord.Type,
|
||||
"ttl": change.ResourceRecord.TTL,
|
||||
"action": change.Action,
|
||||
"action": change.Action.String(),
|
||||
"zone": zoneID,
|
||||
}
|
||||
|
||||
@ -646,12 +691,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
|
||||
}
|
||||
}
|
||||
@ -703,7 +748,7 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]
|
||||
}
|
||||
|
||||
// changesByZone separates a multi-zone change into a single change per zone.
|
||||
func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
|
||||
func (p *CloudFlareProvider) changesByZone(zones []zones.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
|
||||
changes := make(map[string][]*cloudFlareChange)
|
||||
zoneNameIDMapper := provider.ZoneIDName{}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
@ -27,9 +27,12 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
"github.com/cloudflare/cloudflare-go/v4/zones"
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/internal/testutils"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
@ -48,19 +51,18 @@ type MockAction struct {
|
||||
ZoneId string
|
||||
RecordId string
|
||||
RecordData cloudflare.DNSRecord
|
||||
RegionalHostname cloudflare.RegionalHostname
|
||||
RegionalHostname regionalHostname
|
||||
}
|
||||
|
||||
type mockCloudFlareClient struct {
|
||||
Zones map[string]string
|
||||
Records map[string]map[string]cloudflare.DNSRecord
|
||||
Actions []MockAction
|
||||
listZonesError error
|
||||
zoneDetailsError error
|
||||
listZonesContextError error
|
||||
listZonesError error // For v4 ListZones
|
||||
getZoneError error // For v4 GetZone
|
||||
dnsRecordsError error
|
||||
customHostnames map[string][]cloudflare.CustomHostname
|
||||
regionalHostnames map[string][]cloudflare.RegionalHostname
|
||||
regionalHostnames map[string][]regionalHostname
|
||||
}
|
||||
|
||||
var ExampleDomain = []cloudflare.DNSRecord{
|
||||
@ -102,7 +104,7 @@ func NewMockCloudFlareClient() *mockCloudFlareClient {
|
||||
"002": {},
|
||||
},
|
||||
customHostnames: map[string][]cloudflare.CustomHostname{},
|
||||
regionalHostnames: map[string][]cloudflare.RegionalHostname{},
|
||||
regionalHostnames: map[string][]regionalHostname{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,54 +337,60 @@ func (m *mockCloudFlareClient) DeleteCustomHostname(ctx context.Context, zoneID
|
||||
}
|
||||
|
||||
func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) {
|
||||
// Simulate iterator error (line 144)
|
||||
if m.listZonesError != nil {
|
||||
return "", fmt.Errorf("failed to list zones from CloudFlare API: %w", m.listZonesError)
|
||||
}
|
||||
|
||||
for id, name := range m.Zones {
|
||||
if name == zoneName {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("Unknown zone: " + zoneName)
|
||||
// Use the improved error message (line 147)
|
||||
return "", fmt.Errorf("zone %q not found in CloudFlare account - verify the zone exists and API credentials have access to it", zoneName)
|
||||
}
|
||||
|
||||
func (m *mockCloudFlareClient) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
|
||||
if m.listZonesContextError != nil {
|
||||
return cloudflare.ZonesResponse{}, m.listZonesContextError
|
||||
// V4 Zone methods
|
||||
func (m *mockCloudFlareClient) ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone] {
|
||||
if m.listZonesError != nil {
|
||||
return &mockAutoPager[zones.Zone]{
|
||||
err: m.listZonesError,
|
||||
}
|
||||
}
|
||||
|
||||
result := []cloudflare.Zone{}
|
||||
var results []zones.Zone
|
||||
|
||||
for zoneId, zoneName := range m.Zones {
|
||||
result = append(result, cloudflare.Zone{
|
||||
ID: zoneId,
|
||||
for id, zoneName := range m.Zones {
|
||||
results = append(results, zones.Zone{
|
||||
ID: id,
|
||||
Name: zoneName,
|
||||
Plan: zones.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")}, //nolint:SA1019 // Plan.IsSubscribed is deprecated but no replacement available yet
|
||||
})
|
||||
}
|
||||
|
||||
return cloudflare.ZonesResponse{
|
||||
Result: result,
|
||||
ResultInfo: cloudflare.ResultInfo{
|
||||
Page: 1,
|
||||
TotalPages: 1,
|
||||
},
|
||||
}, nil
|
||||
return &mockAutoPager[zones.Zone]{
|
||||
items: results,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) {
|
||||
if m.zoneDetailsError != nil {
|
||||
return cloudflare.Zone{}, m.zoneDetailsError
|
||||
func (m *mockCloudFlareClient) GetZone(ctx context.Context, zoneID string) (*zones.Zone, error) {
|
||||
if m.getZoneError != nil {
|
||||
return nil, m.getZoneError
|
||||
}
|
||||
|
||||
for id, zoneName := range m.Zones {
|
||||
if zoneID == id {
|
||||
return cloudflare.Zone{
|
||||
return &zones.Zone{
|
||||
ID: zoneID,
|
||||
Name: zoneName,
|
||||
Plan: cloudflare.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")},
|
||||
Plan: zones.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")}, //nolint:SA1019 // Plan.IsSubscribed is deprecated but no replacement available yet
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
|
||||
return nil, errors.New("Unknown zoneID: " + zoneID)
|
||||
}
|
||||
|
||||
func getCustomHostnameIdxByID(chs []cloudflare.CustomHostname, customHostnameID string) int {
|
||||
@ -841,7 +849,7 @@ func TestCloudflareZones(t *testing.T) {
|
||||
func TestCloudflareZonesFailed(t *testing.T) {
|
||||
|
||||
client := NewMockCloudFlareClient()
|
||||
client.zoneDetailsError = errors.New("zone lookup failed")
|
||||
client.getZoneError = errors.New("zone lookup failed")
|
||||
|
||||
provider := &CloudFlareProvider{
|
||||
Client: client,
|
||||
@ -877,7 +885,7 @@ func TestCloudFlareZonesWithIDFilter(t *testing.T) {
|
||||
func TestCloudflareListZonesRateLimited(t *testing.T) {
|
||||
// Create a mock client that returns a rate limit error
|
||||
client := NewMockCloudFlareClient()
|
||||
client.listZonesContextError = &cloudflare.Error{
|
||||
client.listZonesError = &cloudflare.Error{
|
||||
StatusCode: 429,
|
||||
ErrorCodes: []int{10000},
|
||||
Type: cloudflare.ErrorTypeRateLimit,
|
||||
@ -896,7 +904,7 @@ func TestCloudflareListZonesRateLimited(t *testing.T) {
|
||||
func TestCloudflareListZonesRateLimitedStringError(t *testing.T) {
|
||||
// Create a mock client that returns a rate limit error
|
||||
client := NewMockCloudFlareClient()
|
||||
client.listZonesContextError = errors.New("exceeded available rate limit retries")
|
||||
client.listZonesError = errors.New("exceeded available rate limit retries")
|
||||
p := &CloudFlareProvider{Client: client}
|
||||
|
||||
// Call the Zones function
|
||||
@ -909,7 +917,7 @@ func TestCloudflareListZonesRateLimitedStringError(t *testing.T) {
|
||||
func TestCloudflareListZoneInternalErrors(t *testing.T) {
|
||||
// Create a mock client that returns a internal server error
|
||||
client := NewMockCloudFlareClient()
|
||||
client.listZonesContextError = &cloudflare.Error{
|
||||
client.listZonesError = &cloudflare.Error{
|
||||
StatusCode: 500,
|
||||
ErrorCodes: []int{20000},
|
||||
Type: cloudflare.ErrorTypeService,
|
||||
@ -949,7 +957,7 @@ func TestCloudflareRecords(t *testing.T) {
|
||||
t.Errorf("expected to fail")
|
||||
}
|
||||
client.dnsRecordsError = nil
|
||||
client.listZonesContextError = &cloudflare.Error{
|
||||
client.listZonesError = &cloudflare.Error{
|
||||
StatusCode: 429,
|
||||
ErrorCodes: []int{10000},
|
||||
Type: cloudflare.ErrorTypeRateLimit,
|
||||
@ -960,7 +968,7 @@ func TestCloudflareRecords(t *testing.T) {
|
||||
t.Error("expected a rate limit error")
|
||||
}
|
||||
|
||||
client.listZonesContextError = &cloudflare.Error{
|
||||
client.listZonesError = &cloudflare.Error{
|
||||
StatusCode: 500,
|
||||
ErrorCodes: []int{10000},
|
||||
Type: cloudflare.ErrorTypeService,
|
||||
@ -971,7 +979,7 @@ func TestCloudflareRecords(t *testing.T) {
|
||||
t.Error("expected a internal server error")
|
||||
}
|
||||
|
||||
client.listZonesContextError = errors.New("failed to list zones")
|
||||
client.listZonesError = errors.New("failed to list zones")
|
||||
_, err = p.Records(ctx)
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail")
|
||||
@ -1795,8 +1803,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
|
||||
@ -1949,8 +1957,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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -1961,9 +1969,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: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -2012,8 +2020,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
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -2024,9 +2032,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: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -2584,7 +2592,7 @@ func TestZoneHasPaidPlan(t *testing.T) {
|
||||
assert.True(t, cfprovider.ZoneHasPaidPlan("subdomain.bar.com"))
|
||||
assert.False(t, cfprovider.ZoneHasPaidPlan("invaliddomain"))
|
||||
|
||||
client.zoneDetailsError = errors.New("zone lookup failed")
|
||||
client.getZoneError = errors.New("zone lookup failed")
|
||||
cfproviderWithZoneError := &CloudFlareProvider{
|
||||
Client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||
@ -2592,6 +2600,7 @@ func TestZoneHasPaidPlan(t *testing.T) {
|
||||
}
|
||||
assert.False(t, cfproviderWithZoneError.ZoneHasPaidPlan("subdomain.foo.com"))
|
||||
}
|
||||
|
||||
func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) {
|
||||
hook := testutils.LogsUnderTestWithLogLevel(log.ErrorLevel, t)
|
||||
|
||||
@ -2760,3 +2769,488 @@ func TestCloudFlareProvider_SupportedAdditionalRecordTypes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudflareZoneChanges(t *testing.T) {
|
||||
client := NewMockCloudFlareClient()
|
||||
cfProvider := &CloudFlareProvider{
|
||||
Client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||
}
|
||||
|
||||
// Test zone listing and filtering
|
||||
zones, err := cfProvider.Zones(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, zones, 2)
|
||||
|
||||
// Verify zone names
|
||||
zoneNames := make([]string, len(zones))
|
||||
for i, zone := range zones {
|
||||
zoneNames[i] = zone.Name
|
||||
}
|
||||
assert.Contains(t, zoneNames, "foo.com")
|
||||
assert.Contains(t, zoneNames, "bar.com")
|
||||
|
||||
// Test zone filtering with specific zone ID
|
||||
providerWithZoneFilter := &CloudFlareProvider{
|
||||
Client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||
zoneIDFilter: provider.NewZoneIDFilter([]string{"001"}),
|
||||
}
|
||||
|
||||
filteredZones, err := providerWithZoneFilter.Zones(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, filteredZones, 1)
|
||||
assert.Equal(t, "bar.com", filteredZones[0].Name) // zone 001 is bar.com
|
||||
assert.Equal(t, "001", filteredZones[0].ID)
|
||||
|
||||
// Test zone changes grouping
|
||||
changes := []*cloudFlareChange{
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
ResourceRecord: cloudflare.DNSRecord{Name: "test1.foo.com", Type: "A", Content: "1.2.3.4"},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
ResourceRecord: cloudflare.DNSRecord{Name: "test2.foo.com", Type: "A", Content: "1.2.3.5"},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
ResourceRecord: cloudflare.DNSRecord{Name: "test1.bar.com", Type: "A", Content: "1.2.3.6"},
|
||||
},
|
||||
}
|
||||
|
||||
changesByZone := cfProvider.changesByZone(zones, changes)
|
||||
assert.Len(t, changesByZone, 2)
|
||||
assert.Len(t, changesByZone["001"], 1) // bar.com zone (test1.bar.com)
|
||||
assert.Len(t, changesByZone["002"], 2) // foo.com zone (test1.foo.com, test2.foo.com)
|
||||
|
||||
// Test paid plan detection
|
||||
assert.False(t, cfProvider.ZoneHasPaidPlan("subdomain.foo.com")) // free plan
|
||||
assert.True(t, cfProvider.ZoneHasPaidPlan("subdomain.bar.com")) // paid plan
|
||||
}
|
||||
|
||||
func TestCloudflareZoneErrors(t *testing.T) {
|
||||
client := NewMockCloudFlareClient()
|
||||
|
||||
// Test list zones error
|
||||
client.listZonesError = errors.New("failed to list zones")
|
||||
cfProvider := &CloudFlareProvider{
|
||||
Client: client,
|
||||
}
|
||||
|
||||
zones, err := cfProvider.Zones(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to list zones")
|
||||
assert.Nil(t, zones)
|
||||
|
||||
// Test get zone error
|
||||
client.listZonesError = nil
|
||||
client.getZoneError = errors.New("failed to get zone")
|
||||
|
||||
// This should still work for listing but fail when getting individual zones
|
||||
zones, err = cfProvider.Zones(context.Background())
|
||||
assert.NoError(t, err) // List works, individual gets may fail internally
|
||||
assert.NotNil(t, zones)
|
||||
}
|
||||
|
||||
func TestCloudflareZoneFiltering(t *testing.T) {
|
||||
client := NewMockCloudFlareClient()
|
||||
|
||||
// Test with domain filter only
|
||||
cfProvider := &CloudFlareProvider{
|
||||
Client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}),
|
||||
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||
}
|
||||
|
||||
zones, err := cfProvider.Zones(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, zones, 1)
|
||||
assert.Equal(t, "foo.com", zones[0].Name)
|
||||
|
||||
// Test with zone ID filter
|
||||
providerWithIDFilter := &CloudFlareProvider{
|
||||
Client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{}),
|
||||
zoneIDFilter: provider.NewZoneIDFilter([]string{"002"}),
|
||||
}
|
||||
|
||||
filteredZones, err := providerWithIDFilter.Zones(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, filteredZones, 1)
|
||||
assert.Equal(t, "foo.com", filteredZones[0].Name) // zone 002 is foo.com
|
||||
assert.Equal(t, "002", filteredZones[0].ID)
|
||||
}
|
||||
|
||||
func TestCloudflareZonePlanDetection(t *testing.T) {
|
||||
client := NewMockCloudFlareClient()
|
||||
cfProvider := &CloudFlareProvider{
|
||||
Client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||
}
|
||||
|
||||
// Test free plan detection (foo.com)
|
||||
assert.False(t, cfProvider.ZoneHasPaidPlan("foo.com"))
|
||||
assert.False(t, cfProvider.ZoneHasPaidPlan("subdomain.foo.com"))
|
||||
assert.False(t, cfProvider.ZoneHasPaidPlan("deep.subdomain.foo.com"))
|
||||
|
||||
// Test paid plan detection (bar.com)
|
||||
assert.True(t, cfProvider.ZoneHasPaidPlan("bar.com"))
|
||||
assert.True(t, cfProvider.ZoneHasPaidPlan("subdomain.bar.com"))
|
||||
assert.True(t, cfProvider.ZoneHasPaidPlan("deep.subdomain.bar.com"))
|
||||
|
||||
// Test invalid domain
|
||||
assert.False(t, cfProvider.ZoneHasPaidPlan("invalid.domain.com"))
|
||||
|
||||
// Test with zone error
|
||||
client.getZoneError = errors.New("zone lookup failed")
|
||||
providerWithError := &CloudFlareProvider{
|
||||
Client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||
}
|
||||
assert.False(t, providerWithError.ZoneHasPaidPlan("subdomain.foo.com"))
|
||||
}
|
||||
|
||||
func TestCloudflareChangesByZone(t *testing.T) {
|
||||
client := NewMockCloudFlareClient()
|
||||
cfProvider := &CloudFlareProvider{
|
||||
Client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
|
||||
zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
|
||||
}
|
||||
|
||||
zones, err := cfProvider.Zones(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, zones, 2)
|
||||
|
||||
// Test empty changes
|
||||
emptyChanges := []*cloudFlareChange{}
|
||||
changesByZone := cfProvider.changesByZone(zones, emptyChanges)
|
||||
assert.Len(t, changesByZone, 2) // Should return map with zones but empty slices
|
||||
assert.Empty(t, changesByZone["001"]) // bar.com zone should have no changes
|
||||
assert.Empty(t, changesByZone["002"]) // foo.com zone should have no changes
|
||||
|
||||
// Test changes for different zones
|
||||
changes := []*cloudFlareChange{
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
ResourceRecord: cloudflare.DNSRecord{Name: "api.foo.com", Type: "A", Content: "1.2.3.4"},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareUpdate,
|
||||
ResourceRecord: cloudflare.DNSRecord{Name: "www.foo.com", Type: "CNAME", Content: "foo.com"},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareCreate,
|
||||
ResourceRecord: cloudflare.DNSRecord{Name: "mail.bar.com", Type: "MX", Content: "10 mail.bar.com"},
|
||||
},
|
||||
{
|
||||
Action: cloudFlareDelete,
|
||||
ResourceRecord: cloudflare.DNSRecord{Name: "old.bar.com", Type: "A", Content: "5.6.7.8"},
|
||||
},
|
||||
}
|
||||
|
||||
changesByZone = cfProvider.changesByZone(zones, changes)
|
||||
assert.Len(t, changesByZone, 2)
|
||||
|
||||
// Verify bar.com zone changes (zone 001)
|
||||
barChanges := changesByZone["001"]
|
||||
assert.Len(t, barChanges, 2)
|
||||
assert.Equal(t, "mail.bar.com", barChanges[0].ResourceRecord.Name)
|
||||
assert.Equal(t, "old.bar.com", barChanges[1].ResourceRecord.Name)
|
||||
|
||||
// Verify foo.com zone changes (zone 002)
|
||||
fooChanges := changesByZone["002"]
|
||||
assert.Len(t, fooChanges, 2)
|
||||
assert.Equal(t, "api.foo.com", fooChanges[0].ResourceRecord.Name)
|
||||
assert.Equal(t, "www.foo.com", fooChanges[1].ResourceRecord.Name)
|
||||
}
|
||||
|
||||
func TestConvertCloudflareError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputError error
|
||||
expectSoftError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Rate limit error via Error type",
|
||||
inputError: &cloudflare.Error{StatusCode: 429, Type: cloudflare.ErrorTypeRateLimit},
|
||||
expectSoftError: true,
|
||||
description: "CloudFlare API rate limit error should be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Rate limit error via ClientRateLimited",
|
||||
inputError: &cloudflare.Error{StatusCode: 429, ErrorCodes: []int{10000}, Type: cloudflare.ErrorTypeRateLimit}, // Complete rate limit error
|
||||
expectSoftError: true,
|
||||
description: "CloudFlare client rate limited error should be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Server error 500",
|
||||
inputError: &cloudflare.Error{StatusCode: 500},
|
||||
expectSoftError: true,
|
||||
description: "Server error (500+) should be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Server error 502",
|
||||
inputError: &cloudflare.Error{StatusCode: 502},
|
||||
expectSoftError: true,
|
||||
description: "Server error (502) should be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Server error 503",
|
||||
inputError: &cloudflare.Error{StatusCode: 503},
|
||||
expectSoftError: true,
|
||||
description: "Server error (503) should be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Rate limit string error",
|
||||
inputError: errors.New("exceeded available rate limit retries"),
|
||||
expectSoftError: true,
|
||||
description: "String error containing rate limit message should be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Rate limit string error mixed case",
|
||||
inputError: errors.New("request failed: exceeded available rate limit retries for this operation"),
|
||||
expectSoftError: true,
|
||||
description: "String error containing rate limit message should be converted to soft error regardless of context",
|
||||
},
|
||||
{
|
||||
name: "Client error 400",
|
||||
inputError: &cloudflare.Error{StatusCode: 400},
|
||||
expectSoftError: false,
|
||||
description: "Client error (400) should not be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Client error 401",
|
||||
inputError: &cloudflare.Error{StatusCode: 401},
|
||||
expectSoftError: false,
|
||||
description: "Client error (401) should not be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Client error 404",
|
||||
inputError: &cloudflare.Error{StatusCode: 404},
|
||||
expectSoftError: false,
|
||||
description: "Client error (404) should not be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Generic error",
|
||||
inputError: errors.New("some generic error"),
|
||||
expectSoftError: false,
|
||||
description: "Generic error should not be converted to soft error",
|
||||
},
|
||||
{
|
||||
name: "Network error",
|
||||
inputError: errors.New("connection refused"),
|
||||
expectSoftError: false,
|
||||
description: "Network error should not be converted to soft error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := convertCloudflareError(tt.inputError)
|
||||
|
||||
if tt.expectSoftError {
|
||||
assert.ErrorIs(t, result, provider.SoftError,
|
||||
"Expected soft error for %s: %s", tt.name, tt.description)
|
||||
|
||||
// Verify the original error message is preserved in the soft error
|
||||
assert.Contains(t, result.Error(), tt.inputError.Error(),
|
||||
"Original error message should be preserved")
|
||||
} else {
|
||||
assert.NotErrorIs(t, result, provider.SoftError,
|
||||
"Expected non-soft error for %s: %s", tt.name, tt.description)
|
||||
assert.Equal(t, tt.inputError, result,
|
||||
"Non-soft errors should be returned unchanged")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCloudflareErrorInContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMock func(*mockCloudFlareClient)
|
||||
function func(*CloudFlareProvider) error
|
||||
expectSoftError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Zones with GetZone rate limit error",
|
||||
setupMock: func(client *mockCloudFlareClient) {
|
||||
client.Zones = map[string]string{"zone1": "example.com"}
|
||||
client.getZoneError = &cloudflare.Error{StatusCode: 429, Type: cloudflare.ErrorTypeRateLimit}
|
||||
},
|
||||
function: func(p *CloudFlareProvider) error {
|
||||
p.zoneIDFilter.ZoneIDs = []string{"zone1"}
|
||||
_, err := p.Zones(context.Background())
|
||||
return err
|
||||
},
|
||||
expectSoftError: true,
|
||||
description: "Zones function should convert GetZone rate limit errors to soft errors",
|
||||
},
|
||||
{
|
||||
name: "Zones with GetZone server error",
|
||||
setupMock: func(client *mockCloudFlareClient) {
|
||||
client.Zones = map[string]string{"zone1": "example.com"}
|
||||
client.getZoneError = &cloudflare.Error{StatusCode: 500}
|
||||
},
|
||||
function: func(p *CloudFlareProvider) error {
|
||||
p.zoneIDFilter.ZoneIDs = []string{"zone1"}
|
||||
_, err := p.Zones(context.Background())
|
||||
return err
|
||||
},
|
||||
expectSoftError: true,
|
||||
description: "Zones function should convert GetZone server errors to soft errors",
|
||||
},
|
||||
{
|
||||
name: "Zones with GetZone client error",
|
||||
setupMock: func(client *mockCloudFlareClient) {
|
||||
client.Zones = map[string]string{"zone1": "example.com"}
|
||||
client.getZoneError = &cloudflare.Error{StatusCode: 404}
|
||||
},
|
||||
function: func(p *CloudFlareProvider) error {
|
||||
p.zoneIDFilter.ZoneIDs = []string{"zone1"}
|
||||
_, err := p.Zones(context.Background())
|
||||
return err
|
||||
},
|
||||
expectSoftError: false,
|
||||
description: "Zones function should not convert GetZone client errors to soft errors",
|
||||
},
|
||||
{
|
||||
name: "Zones with ListZones rate limit error",
|
||||
setupMock: func(client *mockCloudFlareClient) {
|
||||
client.listZonesError = errors.New("exceeded available rate limit retries")
|
||||
},
|
||||
function: func(p *CloudFlareProvider) error {
|
||||
_, err := p.Zones(context.Background())
|
||||
return err
|
||||
},
|
||||
expectSoftError: true,
|
||||
description: "Zones function should convert ListZones rate limit string errors to soft errors",
|
||||
},
|
||||
{
|
||||
name: "Zones with ListZones server error",
|
||||
setupMock: func(client *mockCloudFlareClient) {
|
||||
client.listZonesError = &cloudflare.Error{StatusCode: 503}
|
||||
},
|
||||
function: func(p *CloudFlareProvider) error {
|
||||
_, err := p.Zones(context.Background())
|
||||
return err
|
||||
},
|
||||
expectSoftError: true,
|
||||
description: "Zones function should convert ListZones server errors to soft errors",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := NewMockCloudFlareClient()
|
||||
tt.setupMock(client)
|
||||
|
||||
p := &CloudFlareProvider{
|
||||
Client: client,
|
||||
zoneIDFilter: provider.ZoneIDFilter{},
|
||||
}
|
||||
|
||||
err := tt.function(p)
|
||||
assert.Error(t, err, "Expected an error from %s", tt.name)
|
||||
|
||||
if tt.expectSoftError {
|
||||
assert.ErrorIs(t, err, provider.SoftError,
|
||||
"Expected soft error for %s: %s", tt.name, tt.description)
|
||||
} else {
|
||||
assert.NotErrorIs(t, err, provider.SoftError,
|
||||
"Expected non-soft error for %s: %s", tt.name, tt.description)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudFlareZonesDomainFilter(t *testing.T) {
|
||||
// Set required environment variables for CloudFlare provider
|
||||
t.Setenv("CF_API_TOKEN", "test-token")
|
||||
|
||||
client := NewMockCloudFlareClient()
|
||||
|
||||
// Create a domain filter that only matches "bar.com"
|
||||
// This should filter out "foo.com" and trigger the debug log
|
||||
domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
|
||||
|
||||
p, err := NewCloudFlareProvider(
|
||||
domainFilter,
|
||||
provider.NewZoneIDFilter([]string{""}), // empty zone ID filter so it uses ListZones path
|
||||
false, // proxied
|
||||
false, // dry run
|
||||
RegionalServicesConfig{},
|
||||
CustomHostnamesConfig{},
|
||||
DNSRecordsConfig{PerPage: 50},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Replace the real client with our mock
|
||||
p.Client = client
|
||||
|
||||
// Capture debug logs to verify the filter log message
|
||||
oldLevel := log.GetLevel()
|
||||
log.SetLevel(log.DebugLevel)
|
||||
defer log.SetLevel(oldLevel)
|
||||
|
||||
// Use a custom formatter to capture log output
|
||||
var logOutput strings.Builder
|
||||
log.SetOutput(&logOutput)
|
||||
defer log.SetOutput(os.Stderr)
|
||||
|
||||
// Call Zones() which should trigger the domain filter logic
|
||||
zones, err := p.Zones(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should only return the "bar.com" zone since "foo.com" is filtered out
|
||||
assert.Len(t, zones, 1)
|
||||
assert.Equal(t, "bar.com", zones[0].Name)
|
||||
assert.Equal(t, "001", zones[0].ID)
|
||||
|
||||
// Verify that the debug log was written for the filtered zone
|
||||
logString := logOutput.String()
|
||||
assert.Contains(t, logString, `zone \"foo.com\" not in domain filter`)
|
||||
assert.Contains(t, logString, "no zoneIDFilter configured, looking at all zones")
|
||||
}
|
||||
|
||||
func TestZoneIDByNameIteratorError(t *testing.T) {
|
||||
client := NewMockCloudFlareClient()
|
||||
|
||||
// Set up an error that will be returned by the ListZones iterator (line 144)
|
||||
client.listZonesError = fmt.Errorf("CloudFlare API connection timeout")
|
||||
|
||||
// Call ZoneIDByName which should hit line 144 (iterator error handling)
|
||||
zoneID, err := client.ZoneIDByName("example.com")
|
||||
|
||||
// Should return empty zone ID and the wrapped iterator error
|
||||
assert.Empty(t, zoneID)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to list zones from CloudFlare API")
|
||||
assert.Contains(t, err.Error(), "CloudFlare API connection timeout")
|
||||
}
|
||||
|
||||
func TestZoneIDByNameZoneNotFound(t *testing.T) {
|
||||
client := NewMockCloudFlareClient()
|
||||
|
||||
// Set up mock to return different zones but not the one we're looking for
|
||||
client.Zones = map[string]string{
|
||||
"zone456": "different.com",
|
||||
"zone789": "another.com",
|
||||
}
|
||||
|
||||
// Call ZoneIDByName for a zone that doesn't exist, should hit line 147 (zone not found)
|
||||
zoneID, err := client.ZoneIDByName("nonexistent.com")
|
||||
|
||||
// Should return empty zone ID and the improved error message
|
||||
assert.Empty(t, zoneID)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), `zone "nonexistent.com" not found in CloudFlare account`)
|
||||
assert.Contains(t, err.Error(), "verify the zone exists and API credentials have access to it")
|
||||
}
|
||||
|
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 == ','
|
||||
@ -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,30 +285,36 @@ 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"))
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, action, apiUrl, nil)
|
||||
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
|
||||
}
|
||||
|
||||
log.Infof("%s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, target)
|
||||
|
||||
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
|
||||
}
|
||||
@ -304,6 +323,7 @@ func (p *piholeClientV6) apply(ctx context.Context, action string, ep *endpoint.
|
||||
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])
|
||||
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)
|
||||
}
|
||||
if rec.Targets[0] != expected[idx][1] {
|
||||
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
|
||||
}
|
||||
}
|
||||
|
||||
// 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])
|
||||
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)
|
||||
}
|
||||
if rec.Targets[0] != expected[idx][1] {
|
||||
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
|
||||
}
|
||||
}
|
||||
|
||||
// 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])
|
||||
expectedMap = make(map[string]*endpoint.Endpoint)
|
||||
for _, ep := range expected {
|
||||
expectedMap[ep.DNSName] = ep
|
||||
}
|
||||
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])
|
||||
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"
|
||||
@ -33,6 +36,7 @@ var ErrNoPiholeServer = errors.New("no pihole server found in the environment or
|
||||
type PiholeProvider struct {
|
||||
provider.BaseProvider
|
||||
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,16 +129,25 @@ 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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pure creates before applying new updated state.
|
||||
for _, ep := range changes.Create {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
@ -83,6 +84,7 @@ func TestErrorHandling(t *testing.T) {
|
||||
requests := requestTrackerV6{}
|
||||
p := &PiholeProvider{
|
||||
api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests},
|
||||
apiVersion: "6",
|
||||
}
|
||||
|
||||
p.api.(*testPiholeClientV6).trigger = "AERROR"
|
||||
@ -122,6 +124,7 @@ func TestProviderV6(t *testing.T) {
|
||||
requests := requestTrackerV6{}
|
||||
p := &PiholeProvider{
|
||||
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",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user