diff --git a/README.md b/README.md index ac5c8959cd..c91fc76ec1 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz - [Akamai Edge DNS](https://learn.akamai.com/en-us/products/cloud_security/edge_dns.html) - [GoDaddy](https://www.godaddy.com) - [Gandi](https://www.gandi.net) -- [IBM Cloud DNS](https://www.ibm.com/cloud/dns) - [Plural](https://www.plural.sh/) - [Pi-hole](https://pi-hole.net/) - [Alibaba Cloud DNS](https://www.alibabacloud.com/help/en/dns) @@ -110,11 +109,12 @@ from the usage of any externally developed webhook. | Netic | https://github.com/neticdk/external-dns-tidydns-webhook | | OpenStack Designate | https://github.com/inovex/external-dns-designate-webhook | | OpenWRT | https://github.com/renanqts/external-dns-openwrt-webhook | -| RouterOS | https://github.com/benfiola/external-dns-routeros-provider | | SAKURA Cloud | https://github.com/sacloud/external-dns-sacloud-webhook | +| Simply | https://github.com/uozalp/external-dns-simply-webhook | | STACKIT | https://github.com/stackitcloud/external-dns-stackit-webhook | | Unbound | https://github.com/guillomep/external-dns-unbound-webhook | | Unifi | https://github.com/kashalls/external-dns-unifi-webhook | +| UniFi | https://github.com/lexfrei/external-dns-unifios-webhook | | Volcengine Cloud | https://github.com/volcengine/external-dns-volcengine-webhook | | Vultr | https://github.com/vultr/external-dns-vultr-webhook | | Yandex Cloud | https://github.com/ismailbaskin/external-dns-yandex-webhook/ | diff --git a/charts/external-dns/CHANGELOG.md b/charts/external-dns/CHANGELOG.md index 309452b022..5766bab30c 100644 --- a/charts/external-dns/CHANGELOG.md +++ b/charts/external-dns/CHANGELOG.md @@ -22,6 +22,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add option to set `annotationPrefix` ([#5889](https://github.com/kubernetes-sigs/external-dns/pull/5889)) _@lexfrei_ +### Changed + +- Grant `networking.k8s.io/ingresses` and `gateway.solo.io/gateways` permissions when using `gloo-proxy` source. ([#5909](https://github.com/kubernetes-sigs/external-dns/pull/5909)) _@cucxabong_ + +### Fixed + +- Fixed the missing schema for `.provider.webhook.serviceMonitor` configs ([#5932](https://github.com/kubernetes-sigs/external-dns/pull/5932)) _@chrisbsmith_ + ## [v1.19.0] - 2025-09-08 ### Added diff --git a/charts/external-dns/templates/clusterrole.yaml b/charts/external-dns/templates/clusterrole.yaml index 52c525ead2..b3ef006ce2 100644 --- a/charts/external-dns/templates/clusterrole.yaml +++ b/charts/external-dns/templates/clusterrole.yaml @@ -26,7 +26,7 @@ rules: resources: ["endpointslices"] verbs: ["get","watch","list"] {{- end }} -{{- if or (has "ingress" .Values.sources) (has "istio-gateway" .Values.sources) (has "istio-virtualservice" .Values.sources) (has "contour-httpproxy" .Values.sources) (has "openshift-route" .Values.sources) (has "skipper-routegroup" .Values.sources) }} +{{- if or (has "ingress" .Values.sources) (has "istio-gateway" .Values.sources) (has "istio-virtualservice" .Values.sources) (has "contour-httpproxy" .Values.sources) (has "openshift-route" .Values.sources) (has "skipper-routegroup" .Values.sources) (has "gloo-proxy" .Values.sources) }} - apiGroups: ["extensions","networking.k8s.io"] resources: ["ingresses"] verbs: ["get","watch","list"] @@ -99,7 +99,7 @@ rules: {{- end }} {{- if has "gloo-proxy" .Values.sources }} - apiGroups: ["gloo.solo.io","gateway.solo.io"] - resources: ["proxies","virtualservices"] + resources: ["proxies","virtualservices","gateways"] verbs: ["get","watch","list"] {{- end }} {{- if has "kong-tcpingress" .Values.sources }} diff --git a/charts/external-dns/tests/json-schema_test.yaml b/charts/external-dns/tests/json-schema_test.yaml index 85e29e5404..3538cb7504 100644 --- a/charts/external-dns/tests/json-schema_test.yaml +++ b/charts/external-dns/tests/json-schema_test.yaml @@ -62,3 +62,12 @@ tests: readinessProbe: null asserts: - notFailedTemplate: {} + + - it: should not fail when provider webhook serviceMonitor interval is not null + set: + provider: + webhook: + serviceMonitor: + interval: 30s + asserts: + - notFailedTemplate: {} diff --git a/charts/external-dns/tests/rbac_test.yaml b/charts/external-dns/tests/rbac_test.yaml index 4658d7ee2d..ab4a1576eb 100644 --- a/charts/external-dns/tests/rbac_test.yaml +++ b/charts/external-dns/tests/rbac_test.yaml @@ -520,3 +520,27 @@ tests: resources: ["virtualservices"] verbs: ["get","watch","list"] template: clusterrole.yaml + - it: should create default RBAC rules for 'GlooEdge' when 'gloo-proxy' is set + set: + sources: + - gloo-proxy + asserts: + - template: clusterrole.yaml + equal: + path: rules + value: + - apiGroups: [""] + resources: ["nodes"] + verbs: ["list","watch"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get","watch","list"] + - apiGroups: [""] + resources: ["services"] + verbs: ["get","watch","list"] + - apiGroups: ["extensions","networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get","watch","list"] + - apiGroups: ["gloo.solo.io","gateway.solo.io"] + resources: ["proxies","virtualservices","gateways"] + verbs: ["get","watch","list"] diff --git a/charts/external-dns/values.schema.json b/charts/external-dns/values.schema.json index ca698c0db0..b4c5e3a42f 100644 --- a/charts/external-dns/values.schema.json +++ b/charts/external-dns/values.schema.json @@ -502,10 +502,16 @@ "type": "object", "properties": { "bearerTokenFile": { - "type": "null" + "type": [ + "string", + "null" + ] }, "interval": { - "type": "null" + "type": [ + "string", + "null" + ] }, "metricRelabelings": { "type": "array" @@ -514,10 +520,16 @@ "type": "array" }, "scheme": { - "type": "null" + "type": [ + "string", + "null" + ] }, "scrapeTimeout": { - "type": "null" + "type": [ + "string", + "null" + ] }, "tlsConfig": { "type": "object" diff --git a/charts/external-dns/values.yaml b/charts/external-dns/values.yaml index 5c5f9fe364..46fbe8f7c7 100644 --- a/charts/external-dns/values.yaml +++ b/charts/external-dns/values.yaml @@ -296,11 +296,11 @@ provider: # @schema type: [object, string] # -- Optional [Service Monitor](https://prometheus-operator.dev/docs/operator/design/#servicemonitor) configuration for the `webhook` container. # @default -- See _values.yaml_ serviceMonitor: - interval: - scheme: + interval: # @schema type:[string, null]; default: null + scheme: # @schema type:[string, null]; default: null tlsConfig: {} - bearerTokenFile: - scrapeTimeout: + bearerTokenFile: # @schema type:[string, null]; default: null + scrapeTimeout: # @schema type:[string, null]; default: null metricRelabelings: [] relabelings: [] diff --git a/controller/execute.go b/controller/execute.go index ea32ee27df..b1978520ea 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -249,7 +249,7 @@ func buildProvider( case "dnsimple": p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun) case "coredns", "skydns": - p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun) + p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.TXTOwnerID, cfg.CoreDNSStrictlyOwned, cfg.DryRun) case "exoscale": p, err = exoscale.NewExoscaleProvider( cfg.ExoscaleAPIEnvironment, diff --git a/docs/20190708-external-dns-incubator.md b/docs/20190708-external-dns-incubator.md index 2a49c40f63..979b214e4f 100644 --- a/docs/20190708-external-dns-incubator.md +++ b/docs/20190708-external-dns-incubator.md @@ -18,7 +18,7 @@ [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) is a project that synchronizes Kubernetes' Services, Ingresses and other Kubernetes resources to DNS backends for several DNS providers. -The projects was started as a Kubernetes Incubator project in February 2017 and being the Kubernetes incubation initiative officially over, the maintainers want to propose the project to be moved to the kubernetes GitHub organization or to kubernetes-sigs, under the sponsorship of sig-network. +The project was started as a Kubernetes Incubator project in February 2017 and being the Kubernetes incubation initiative officially over, the maintainers want to propose the project to be moved to the kubernetes GitHub organization or to kubernetes-sigs, under the sponsorship of sig-network. ## Motivation @@ -62,7 +62,7 @@ Given that the kubernetes-sigs organization will eventually be shut down, the po - Move the project elsewhere -We believe that those alternatives would result in a worse outcome for the community compared to moving the project to the any of the other official Kubernetes organizations. +We believe that those alternatives would result in a worse outcome for the community compared to moving the project to any of the other official Kubernetes organizations. In fact, shutting down ExternalDNS can cause: - The community to rebuild the same solution as already happened multiple times before the project was launched. Currently ExternalDNS is easy to be found, referenced in many articles/tutorials and for that reason not exposed to that risk. @@ -89,7 +89,7 @@ We have evidence that many companies are using ExternalDNS in production, but it The project was quoted by a number of tutorials on the web, including the [official tutorials from AWS](https://aws.amazon.com/blogs/opensource/unified-service-discovery-ecs-kubernetes/). -ExternalDNS can't be consider to be "done": while the core functionality has been implemented, there is lack of integration testing and structural changes that are needed. +ExternalDNS can't be considered to be "done": while the core functionality has been implemented, there is lack of integration testing and structural changes that are needed. Those are identified in the project roadmap, which is roughly made of the following items: @@ -132,7 +132,7 @@ The release pipeline for the project is currently fully owned by Zalando. It run The docker registry service is provided as best effort with no sort of SLA and the maintainers team openly suggests the users to build and maintain their own docker image based on the provided Dockerfiles. -Providing a vanity URL for the docker images was consider a non goal till now, but the community seems to be wanting official images from a GCR domain, similarly to what is available for other parts of official Kubernetes projects. +Providing a vanity URL for the docker images was considered a non goal till now, but the community seems to be wanting official images from a GCR domain, similarly to what is available for other parts of official Kubernetes projects. ExternalDNS does not follow a specific release cycle. Releases are made often when there are major contributions (i.e. new providers) or important bug fixes. That said, the default branch is considered stable and can be used as well to build images. diff --git a/docs/advanced/configuration-precedence.md b/docs/advanced/configuration-precedence.md index 008aa255b7..2165fb0c4f 100644 --- a/docs/advanced/configuration-precedence.md +++ b/docs/advanced/configuration-precedence.md @@ -37,7 +37,7 @@ flowchart TD 3. **Environment Variables** - May override defaults, and in some cases may take precedence over CLI flags and annotations. - - Behavior depends on how the variable is mapped in the code. Where or not it replicates CLI flag or provider specific. Example: `kubectl` or `cloudflare`. + - Behavior depends on how the variable is mapped in the code. Whether or not it replicates CLI flag or provider specific. Example: `kubectl` or `cloudflare`. 4. **Defaults** - If none of the above specify a value, ExternalDNS falls back to its defaults. diff --git a/docs/advanced/fqdn-templating.md b/docs/advanced/fqdn-templating.md index a885f87c6c..23b2f4afc4 100644 --- a/docs/advanced/fqdn-templating.md +++ b/docs/advanced/fqdn-templating.md @@ -241,7 +241,7 @@ This is helpful in scenarios such as: - You must still ensure the resulting FQDN is valid and unique. - Since Go templates can be error-prone, test your template with simple examples before deploying. Mismatched field names or nil values (e.g., missing labels) will result in errors or skipped entries. -## FaQ +## FAQ ### Can I specify multiple global FQDN templates? diff --git a/docs/annotations/annotations.md b/docs/annotations/annotations.md index 9784a3ab6b..96ad1e71a4 100644 --- a/docs/annotations/annotations.md +++ b/docs/annotations/annotations.md @@ -151,14 +151,108 @@ If the annotation is not present, use the domains from both the spec and annotat ## external-dns.alpha.kubernetes.io/ingress -This annotation allows ExternalDNS to work with Istio Gateways that don't have a public IP. +This annotation allows ExternalDNS to work with Istio & GlooEdge Gateways that don't have a public IP. -It can be used to address a specific architectural pattern, when a Kubernetes Ingress directs all public traffic to the Istio Gateway: +It can be used to address a specific architectural pattern, when a Kubernetes Ingress directs all public traffic to an Istio or GlooEdge Gateway: - **The Challenge**: By default, ExternalDNS sources the public IP address for a DNS record from a Service of type LoadBalancer. -However, in some service mesh setups, the Istio Gateway's Service is of type ClusterIP, with all public traffic routed to it via a separate Kubernetes Ingress object. This setup leaves the Gateway without a public IP that ExternalDNS can discover. +However, in some setups, the Gateway's Service is of type ClusterIP, with all public traffic routed to it via a separate Kubernetes Ingress object. This setup leaves the Gateway without a public IP that ExternalDNS can discover. -- **The Solution**: The annotation on the Istio Gateway tells ExternalDNS to ignore the Gateway's Service IP. Instead, it directs ExternalDNS to a specified Ingress resource to find the target LoadBalancer IP address. +- **The Solution**: The annotation on the Istio/GlooEdge Gateway tells ExternalDNS to ignore the Gateway's Service IP. Instead, it directs ExternalDNS to a specified Ingress resource to find the target LoadBalancer IP address. + +### Use Cases for `external-dns.alpha.kubernetes.io/ingress` annotation + +#### Getting target from Ingress backed Gloo Gateway + +```yml +apiVersion: gateway.solo.io/v1 +kind: Gateway +metadata: + annotations: + external-dns.alpha.kubernetes.io/ingress: gateway-proxy + labels: + app: gloo + name: gateway-proxy + namespace: gloo-system +spec: + bindAddress: '::' + bindPort: 8080 + options: {} + proxyNames: + - gateway-proxy + ssl: false + useProxyProto: false +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gateway-proxy + namespace: gloo-system +spec: + ingressClassName: alb + rules: + - host: cool-service.example.com + http: + paths: + - backend: + service: + name: gateway-proxy + port: + name: http + path: / + pathType: Prefix +status: + loadBalancer: + ingress: + - hostname: k8s-alb-c4aa37c880-740590208.us-east-1.elb.amazonaws.com +--- +# This object is generated by GlooEdge Control Plane from Gateway and VirtualService. +# We have no direct control on this resource +apiVersion: gloo.solo.io/v1 +kind: Proxy +metadata: + labels: + created_by: gloo-gateway + name: gateway-proxy + namespace: gloo-system +spec: + listeners: + - bindAddress: '::' + bindPort: 8080 + httpListener: + virtualHosts: + - domains: + - cool-service.example.com + metadataStatic: + sources: + - observedGeneration: "6652" + resourceKind: '*v1.VirtualService' + resourceRef: + name: cool-service + namespace: gloo-system + name: cool-service + routes: + - matchers: + - prefix: / + metadataStatic: + sources: + - observedGeneration: "6652" + resourceKind: '*v1.VirtualService' + resourceRef: + name: cool-service + namespace: gloo-system + upgrades: + - websocket: {} + metadataStatic: + sources: + - observedGeneration: "6111" + resourceKind: '*v1.Gateway' + resourceRef: + name: gateway-proxy + namespace: gloo-system + name: listener-::-8080 + useProxyProto: false +``` ## external-dns.alpha.kubernetes.io/internal-hostname diff --git a/docs/contributing/design.md b/docs/contributing/design.md index 55614c5550..79eb1aef93 100644 --- a/docs/contributing/design.md +++ b/docs/contributing/design.md @@ -19,7 +19,7 @@ You can pick which `Source` and `Provider` to use at runtime via the `--source` ## Adding a DNS Provider -A typical way to start on, e.g. a CoreDNS provider, would be to add a `coredns.go` to the providers package and implement the interface methods. Then you would have to register your provider under a name in `main.go`, e.g. `coredns`, and would be able to trigger it's functions via setting `--provider=coredns`. +A typical way to start on, e.g. a CoreDNS provider, would be to add a `coredns.go` to the providers package and implement the interface methods. Then you would have to register your provider under a name in `main.go`, e.g. `coredns`, and would be able to trigger its functions via setting `--provider=coredns`. Note, how your provider doesn't need to know anything about where the DNS records come from, nor does it have to figure out the difference between the current and the desired state, it merely executes the actions calculated by the plan. diff --git a/docs/contributing/dev-guide.md b/docs/contributing/dev-guide.md index 2572c02210..7102e33918 100644 --- a/docs/contributing/dev-guide.md +++ b/docs/contributing/dev-guide.md @@ -18,7 +18,7 @@ Building and/or testing `external-dns` requires additional tooling. ## First Steps -***Configure Development Environment** +***Configure Development Environment*** You must have a working [Go environment](https://go.dev/doc/install), compile the build, and set up testing. @@ -78,6 +78,11 @@ func TestMe(t *testing.T) { } ``` +## Complete test on local env + +It's possible to run ExternalDNS locally. CoreDNS can be used for easier testing. +See the [related tutorials](../tutorials/coredns-etc.md) for full instructions. + ### Continuous Integration When submitting a pull request, you'll notice that we run several automated processes on your proposed change. Some of these processes are tests to ensure your contribution aligns with our standards. While we strive for accuracy, some users may find these tests confusing. @@ -154,7 +159,7 @@ When building local images with ko you can't specify the registry used to create ❯❯ export KO_DOCKER_REPO=ko.local ❯❯ export VERSION=v1 ❯❯ docker context use rancher-desktop ## (optional) this command is only required when using rancher-desktop -❯❯ ls -al /var/run/docker.sock ## (optional) validate tha docker runtime is configured correctly and symlink exist +❯❯ ls -al /var/run/docker.sock ## (optional) validate that docker runtime is configured correctly and symlink exists ❯❯ ko build --tags ${VERSION} ❯❯ docker images @@ -275,7 +280,7 @@ This helm chart comes with a JSON schema generated from values with [helm schema ❯❯ scripts/helm-tools.sh --docs ``` -6. Run helm unittets. +6. Run helm unittests. ```sh ❯❯ make helm-test diff --git a/docs/faq.md b/docs/faq.md index b5693b9de8..c16c339d77 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -168,7 +168,7 @@ Under certain circumstances you want to force ExternalDNS to create CNAME record Why should I want to force ExternalDNS to create CNAME records for ELB/ALB? Some motivations of users were: > "Our hosted zones records are synchronized with our enterprise DNS. The record type ALIAS is an AWS proprietary record type and AWS allows you to set a DNS record directly on AWS resources. -> Since this is not a DNS RfC standard and therefore can not be transferred and created in our enterprise DNS. So we need to force CNAME creation instead." +> Since this is not a DNS RFC standard and therefore can not be transferred and created in our enterprise DNS. So we need to force CNAME creation instead." or @@ -251,14 +251,14 @@ If you need to search for multiple ingress classes, you can specify the flag mul `--ingress-class=internal --ingress-class=external`. The `--ingress-class` flag will check both the `spec.ingressClassName` field and the deprecated `kubernetes.io/ingress.class` annotation. -The `spec.ingressClassName` tasks precedence over the annotation if both are supplied. +The `spec.ingressClassName` takes precedence over the annotation if both are supplied. **Backward compatibility** The previous `--annotation-filter` flag can still be used to restrict which objects ExternalDNS considers; for example, `--annotation-filter=kubernetes.io/ingress.class in (public,dmz)`. However, beware when using annotation filters with multiple sources, e.g. `--source=service --source=ingress`, since `--annotation-filter` will filter every given source object. -If you need to use annotation filters against a specific source you have to run a separated external dns service containing only the wanted `--source` and `--annotation-filter`. +If you need to use annotation filters against a specific source you have to run a separated external dns service containing only the wanted `--source` and `--annotation-filter`. Note: the `--ingress-class` flag cannot be used at the same time as the `--annotation-filter=kubernetes.io/ingress.class in (...)` flag; if you do this an error will be raised. diff --git a/docs/flags.md b/docs/flags.md index 43d57ac3c2..402940609b 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -97,6 +97,7 @@ | `--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) | | `--cloudflare-record-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (default: '') | | `--coredns-prefix="/skydns/"` | When using the CoreDNS provider, specify the prefix name | +| `--[no-]coredns-strictly-owned` | When using the CoreDNS provider, store and filter strictly by txt-owner-id using an extra field inside of the etcd service (default: false) | | `--akamai-serviceconsumerdomain=""` | When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified) | | `--akamai-client-token=""` | When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified) | | `--akamai-client-secret=""` | When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified) | @@ -175,6 +176,7 @@ | `--[no-]once` | When enabled, exits the synchronization loop after the first iteration (default: disabled) | | `--[no-]dry-run` | When enabled, prints DNS record changes rather than actually performing them (default: disabled) | | `--[no-]events` | When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled) | +| `--min-ttl=0s` | Configure global TTL for records in duration format. This value is used when the TTL for a source is not set or set to 0. (optional; examples: 1m12s, 72s, 72) | | `--log-format=text` | The format in which log messages are printed (default: text, options: text, json) | | `--metrics-address=":7979"` | Specify where to serve the metrics and health check endpoint (default: :7979) | | `--log-level=info` | Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal) | diff --git a/docs/registry/txt.md b/docs/registry/txt.md index 8626d97378..9681cb6c39 100644 --- a/docs/registry/txt.md +++ b/docs/registry/txt.md @@ -208,6 +208,8 @@ Caching is enabled by specifying a cache duration with the `--txt-cache-interval ## OwnerID migration +> Automating DNS migrations with third-party tools can be risky. DNS is often business-critical, and without deep understanding of the environment, 3rd party automation tools can do more harm than good. + The owner ID of the TXT records managed by external-dns instance can be updated. When `--migrate-from-txt-owner` is set, it will enable the migration checks @@ -293,3 +295,33 @@ spec: If you didn't set the owner ID, the value set by external-dns is `default`. You can set the `--migrate-from-txt-owner` flag to `default` to migrate the associated records. + +### OwnerID migration: multi-cluster considerations + +> Warning: The `--migrate-from-txt-owner` flag combined with `policy=sync` can be unsafe in shared hosted zones when multiple clusters previously used the same TXT owner value (for example `default`). + +In a shared hosted zone, if one cluster runs ExternalDNS with `policy=sync` and `--migrate-from-txt-owner=default`, it may attempt to delete DNS records that belong to other clusters which still use `owner=default`. +To avoid this, do not share the same TXT owner value across clusters in any zone where `policy=sync` or migration flags will be used. + +#### Per-cluster owner IDs + +For multi-cluster setups sharing a hosted zone: + +- Assign a **unique** `--txt-owner-id` to each cluster (for example `cluster1`, `cluster2`) and document this convention clearly in your platform configuration. +- Avoid using a common owner such as `default` across clusters in a shared zone if any cluster will run with `policy=sync` or use `--migrate-from-txt-owner`. + +#### Example migration sequence for shared zones + +When migrating from a shared owner (such as `default`) in a shared hosted zone: + +1. While still using `policy=upsert-only` (or equivalent), roll out cluster-specific `--txt-owner-id` values and ensure *new* records are created with the cluster’s own owner ID. +2. Avoid `--migrate-from-txt-owner=` unless you can guarantee that only a single cluster has records with `` in that hosted zone, or perform the migration in an isolated zone where only that cluster writes records. + +### When to avoid owner migration + +The following pattern is **not recommended** and may cause record deletion for other clusters: + +- Multiple clusters share a Route53 hosted zone and all existing records use `owner=default`. +- Only one cluster is upgraded to use `policy=sync`, `--txt-owner-id=`, and `--migrate-from-txt-owner=default`, while other clusters still use `owner=default`. + +In this situation, the upgraded cluster can treat other clusters’ records as orphans and schedule them for deletion during synchronization. Prefer per-cluster zones, manual TXT record adjustment, or fully coordinated migration of all clusters if the migration flag must be used. diff --git a/docs/sources/gloo-proxy.md b/docs/sources/gloo-proxy.md index 03d58b1245..bafcf626fe 100644 --- a/docs/sources/gloo-proxy.md +++ b/docs/sources/gloo-proxy.md @@ -104,3 +104,52 @@ spec: - --registry=txt - --txt-owner-id=my-identifier ``` + +## Gateway Annotation + +To support setups where an Ingress resource is used to provision an external LB you can add the following annotation to your Gateway + +**Note:** The Ingress namespace can be omitted if its in the same namespace as the gateway + +```bash +$ cat < E(services from Cluster A) + store--> F(services from Cluster B) + store--> G(services from someone else) + end + subgraph Cluster A + A(external-dns with stictly-owned) + end + A --> E + subgraph Cluster B + B(external-dns with stictly-owned) + end + B --> F + store --> CoreDNS +``` + +This features works directly without any change to CoreDNS. CoreDNS will ignore this field inside the etcd record. + +### Other entries inside etcd + +Service entries in etcd without an `ownedby` field will be filtered out by the provider if `strictly-owned` is activated. +Warning: If you activate `strictly-owned` afterwards, these entries will be ignored as the `ownedby` field is empty. + +### Ways to migrate to a multi cluster setup + +Ways: + +1. Add the correct owner to all services inside etcd by adding the field `ownedby` to the JSON. +2. Remove all services and allow them to be required again after restarting the provider. (Possible downtime.) + ## Specific service annotation options ### Groups diff --git a/docs/tutorials/crd.md b/docs/tutorials/crd.md index 81834a8b93..53c8fa5795 100644 --- a/docs/tutorials/crd.md +++ b/docs/tutorials/crd.md @@ -69,3 +69,45 @@ spec: - **With `--force-default-targets` (Legacy Behavior):** A CNAME record for `smoke-nt.example.com` will be created pointing to `1.2.3.4`. `--force-default-targets` allows migration path to clean CRD resources. + +### DNSEndpoint with an SRV record + +Here's an example of a `DNSEndpoint` with an SRV record: + +```yaml +--- +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: test-srv + namespace: default +spec: + endpoints: + - dnsName: _sip._udp.test.example.com + recordTTL: 180 + recordType: SRV + targets: + - 1 50 5060 sip1-n1.test.example.com + - 1 50 5060 sip1-n2.test.example.com +``` + +### DNSEndpoint with an NAPTR record + +Here's an example of a `DNSEndpoint` with an NAPTR record: + +```yaml +--- +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: test-naptr + namespace: default +spec: + endpoints: + - dnsName: test.example.com + recordTTL: 180 + recordType: NAPTR + targets: + - 50 50 "S" "SIPS+D2T" "" _sips._tcp.test.example.com. + - 100 50 "S" "SIP+D2U" "" _sip._udp.test.example.com. +``` diff --git a/e2e/deployment.yaml b/e2e/deployment.yaml new file mode 100644 index 0000000000..0885e8a937 --- /dev/null +++ b/e2e/deployment.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: demo-app + name: demo-app +spec: + replicas: 1 + selector: + matchLabels: + app: demo-app + template: + metadata: + labels: + app: demo-app + spec: + containers: + - image: traefik/whoami:latest # minimal demo app + name: demo-app diff --git a/e2e/provider/coredns.yaml b/e2e/provider/coredns.yaml new file mode 100644 index 0000000000..14d02737fb --- /dev/null +++ b/e2e/provider/coredns.yaml @@ -0,0 +1,98 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: coredns + namespace: default +data: + Corefile: | + external.dns:5353 { + errors + log + etcd { + stubzones + path /skydns + endpoint http://etcd-0.etcd:2379 + } + cache 30 + forward . /etc/resolv.conf + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coredns + namespace: default + labels: + app: coredns +spec: + replicas: 1 + selector: + matchLabels: + app: coredns + template: + metadata: + labels: + app: coredns + spec: + hostNetwork: true + dnsPolicy: Default + containers: + - name: coredns + image: coredns/coredns:1.13.1 + args: [ "-conf", "/etc/coredns/Corefile" ] + volumeMounts: + - name: config-volume + mountPath: /etc/coredns + ports: + - containerPort: 5353 + name: dns + protocol: UDP + - containerPort: 5353 + name: dns-tcp + protocol: TCP + livenessProbe: + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 60 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 5 + readinessProbe: + httpGet: + path: /ready + port: 8181 + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 5 + volumes: + - name: config-volume + configMap: + name: coredns + items: + - key: Corefile + path: Corefile +--- +apiVersion: v1 +kind: Service +metadata: + name: coredns + namespace: default + labels: + app: coredns +spec: + selector: + app: coredns + ports: + - name: dns + port: 5353 + targetPort: 5353 + protocol: UDP + - name: dns-tcp + port: 5353 + targetPort: 5353 + protocol: TCP diff --git a/e2e/provider/etcd.yaml b/e2e/provider/etcd.yaml new file mode 100644 index 0000000000..6a58c2e3a9 --- /dev/null +++ b/e2e/provider/etcd.yaml @@ -0,0 +1,121 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: etcd + namespace: default +spec: + type: ClusterIP + clusterIP: None + selector: + app: etcd + publishNotReadyAddresses: true + ports: + - name: etcd-client + port: 2379 + - name: etcd-server + port: 2380 + - name: etcd-metrics + port: 8080 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + namespace: default + name: etcd +spec: + serviceName: etcd + replicas: 1 + podManagementPolicy: Parallel + updateStrategy: + type: RollingUpdate + selector: + matchLabels: + app: etcd + template: + metadata: + labels: + app: etcd + annotations: + serviceName: etcd + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app + operator: In + values: + - etcd + topologyKey: "kubernetes.io/hostname" + containers: + - name: etcd + image: quay.io/coreos/etcd:v3.6.0 + imagePullPolicy: IfNotPresent + ports: + - name: etcd-client + containerPort: 2379 + - name: etcd-server + containerPort: 2380 + - name: etcd-metrics + containerPort: 8080 + readinessProbe: + httpGet: + path: /readyz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 30 + livenessProbe: + httpGet: + path: /livez + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + env: + - name: K8S_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: HOSTNAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: SERVICE_NAME + valueFrom: + fieldRef: + fieldPath: metadata.annotations['serviceName'] + - name: ETCDCTL_ENDPOINTS + value: $(HOSTNAME).$(SERVICE_NAME):2379 + - name: URI_SCHEME + value: "http" + command: + - /usr/local/bin/etcd + args: + - --name=$(HOSTNAME) + - --data-dir=/data + - --wal-dir=/data/wal + - --listen-peer-urls=$(URI_SCHEME)://0.0.0.0:2380 + - --listen-client-urls=$(URI_SCHEME)://0.0.0.0:2379 + - --advertise-client-urls=$(URI_SCHEME)://$(HOSTNAME).$(SERVICE_NAME):2379 + - --initial-cluster-state=new + - --initial-cluster-token=etcd-$(K8S_NAMESPACE) + - --initial-cluster=etcd-0=$(URI_SCHEME)://etcd-0.$(SERVICE_NAME):2380 + - --initial-advertise-peer-urls=$(URI_SCHEME)://$(HOSTNAME).$(SERVICE_NAME):2380 + - --listen-metrics-urls=http://0.0.0.0:8080 + volumeMounts: + - name: etcd-data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: etcd-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi diff --git a/e2e/service.yaml b/e2e/service.yaml new file mode 100644 index 0000000000..9484d69b99 --- /dev/null +++ b/e2e/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: demo-app + name: demo-app + annotations: + external-dns.alpha.kubernetes.io/hostname: externaldns-e2e.external.dns +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 8080 + selector: + app: demo-app + clusterIP: None diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index b1a0543607..68c8f630b1 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -260,8 +260,9 @@ func NewEndpointWithTTL(dnsName, recordType string, ttl TTL, targets ...string) for idx, target := range targets { // Only trim trailing dots for domain name record types, not for TXT or NAPTR records // TXT records can contain arbitrary text including multiple dots + // SRV can contain dots in their target part (RFC2782) switch recordType { - case RecordTypeTXT, RecordTypeNAPTR: + case RecordTypeTXT, RecordTypeNAPTR, RecordTypeSRV: cleanTargets[idx] = target default: cleanTargets[idx] = strings.TrimSuffix(target, ".") @@ -484,11 +485,15 @@ func (t Targets) ValidateMXRecord() bool { func (t Targets) ValidateSRVRecord() bool { for _, target := range t { - // SRV records must have a priority, weight, and port value, e.g. "10 5 5060 example.com" - // as per https://www.rfc-editor.org/rfc/rfc2782.txt + // SRV records must have a priority, weight, a port value and a target e.g. "10 5 5060 example.com." + // as per https://www.rfc-editor.org/rfc/rfc2782.txt the target host has to end with a dot. targetParts := strings.Fields(strings.TrimSpace(target)) if len(targetParts) != 4 { - log.Debugf("Invalid SRV record target: %s. SRV records must have a priority, weight, and port value, e.g. '10 5 5060 example.com'", target) + log.Debugf("Invalid SRV record target: %s. SRV records must have a priority, weight, a port value and a target host, e.g. '10 5 5060 example.com.'", target) + return false + } + if !strings.HasSuffix(targetParts[3], ".") { + log.Debugf("Invalid SRV record target: %s. Target host does not end with a dot.'", target) return false } diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go index ef5fbc7821..381828c3b4 100644 --- a/endpoint/endpoint_test.go +++ b/endpoint/endpoint_test.go @@ -583,7 +583,7 @@ func TestIsOwnedBy(t *testing.T) { Labels: tt.fields.Labels, } if got := e.IsOwnedBy(tt.args.ownerID); got != tt.want { - t.Errorf("Endpoint.IsOwnedBy() = %v, want %v", got, tt.want) + t.Errorf("Endpoint.isOwnedBy() = %v, want %v", got, tt.want) } }) } @@ -787,7 +787,7 @@ func TestPDNScheckEndpoint(t *testing.T) { endpoint: Endpoint{ DNSName: "_service._tls.example.com", RecordType: RecordTypeSRV, - Targets: Targets{"10 20 5060 service.example.com"}, + Targets: Targets{"10 20 5060 service.example.com."}, }, expected: true, }, @@ -805,7 +805,16 @@ func TestPDNScheckEndpoint(t *testing.T) { endpoint: Endpoint{ DNSName: "_service._tls.example.com", RecordType: RecordTypeSRV, - Targets: Targets{"10 20 abc service.example.com"}, + Targets: Targets{"10 20 abc service.example.com."}, + }, + expected: false, + }, + { + description: "Invalid SRV record with missing dot for target host", + endpoint: Endpoint{ + DNSName: "_service._tls.example.com", + RecordType: RecordTypeSRV, + Targets: Targets{"10 20 5060 service.example.com"}, }, expected: false, }, @@ -895,7 +904,7 @@ func TestCheckEndpoint(t *testing.T) { endpoint: Endpoint{ DNSName: "_service._tcp.example.com", RecordType: RecordTypeSRV, - Targets: Targets{"10 5 5060 example.com"}, + Targets: Targets{"10 5 5060 example.com."}, }, expected: true, }, @@ -904,7 +913,7 @@ func TestCheckEndpoint(t *testing.T) { endpoint: Endpoint{ DNSName: "_service._tcp.example.com", RecordType: RecordTypeSRV, - Targets: Targets{"10 5 example.com"}, + Targets: Targets{"10 5 example.com."}, }, expected: false, }, diff --git a/go.mod b/go.mod index 4dec93d155..a4ee9b2416 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module sigs.k8s.io/external-dns -go 1.25 +go 1.25.5 require ( cloud.google.com/go/compute/metadata v0.9.0 @@ -13,15 +13,15 @@ require ( 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.39.6 - github.com/aws/aws-sdk-go-v2/config v1.31.20 - github.com/aws/aws-sdk-go-v2/credentials v1.18.24 - github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.23 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.52.6 - github.com/aws/aws-sdk-go-v2/service/route53 v1.59.5 - github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.16 - github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 - github.com/aws/smithy-go v1.23.2 + github.com/aws/aws-sdk-go-v2 v1.41.0 + github.com/aws/aws-sdk-go-v2/config v1.32.5 + github.com/aws/aws-sdk-go-v2/credentials v1.19.5 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.29 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.53.5 + github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 + github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.21 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 + github.com/aws/smithy-go v1.24.0 github.com/bodgit/tsig v1.2.2 github.com/cenkalti/backoff/v5 v5.0.3 github.com/civo/civogo v0.6.5 @@ -30,53 +30,53 @@ require ( 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.168.0 + github.com/digitalocean/godo v1.170.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 github.com/go-gandi/go-gandi v0.7.0 github.com/go-logr/logr v1.4.3 - github.com/goccy/go-yaml v1.18.0 + github.com/goccy/go-yaml v1.19.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/linode/linodego v1.61.0 + github.com/linode/linodego v1.62.0 github.com/maxatome/go-testdeep v1.14.0 - github.com/miekg/dns v1.1.68 + github.com/miekg/dns v1.1.69 github.com/neticdk/tidydns-go v1.2.1 github.com/openshift/api v0.0.0-20251015095338-264e80a2b6e7 github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 - github.com/oracle/oci-go-sdk/v65 v65.104.1 + github.com/oracle/oci-go-sdk/v65 v65.105.1 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.33.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.67.2 + github.com/prometheus/common v0.67.4 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/transip/gotransip/v6 v6.26.1 go.etcd.io/etcd/api/v3 v3.6.6 go.etcd.io/etcd/client/v3 v3.6.6 go.uber.org/ratelimit v0.3.1 - golang.org/x/net v0.47.0 - golang.org/x/oauth2 v0.33.0 - golang.org/x/sync v0.18.0 - golang.org/x/text v0.31.0 + golang.org/x/net v0.48.0 + golang.org/x/oauth2 v0.34.0 + golang.org/x/sync v0.19.0 + golang.org/x/text v0.32.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.256.0 - gopkg.in/ns1/ns1-go.v2 v2.15.1 - istio.io/api v1.28.0 - istio.io/client-go v1.28.0 - k8s.io/api v0.34.2 - k8s.io/apimachinery v0.34.2 - k8s.io/client-go v0.34.2 + google.golang.org/api v0.257.0 + gopkg.in/ns1/ns1-go.v2 v2.16.0 + istio.io/api v1.28.1 + istio.io/client-go v1.28.1 + k8s.io/api v0.34.3 + k8s.io/apimachinery v0.34.3 + k8s.io/client-go v0.34.3 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d sigs.k8s.io/controller-runtime v0.22.4 - sigs.k8s.io/gateway-api v1.4.0 + sigs.k8s.io/gateway-api v1.4.1 ) require ( @@ -89,16 +89,17 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -114,7 +115,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.2 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-resty/resty/v2 v2.16.5 // indirect + github.com/go-resty/resty/v2 v2.17.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gofrs/flock v0.10.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect @@ -177,24 +178,24 @@ require ( 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.6 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/tools v0.38.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect - google.golang.org/grpc v1.76.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/tools v0.39.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index d48199b02f..eb4964b169 100644 --- a/go.sum +++ b/go.sum @@ -116,44 +116,46 @@ 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.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= -github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= -github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc= -github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0= -github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg= -github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.23 h1:lbCh6aGAGHC/tZn30uaB5C1Txr5nRMr86ObRrDRZTYU= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.23/go.mod h1:JX1mhxc+O8hXWVVoA+gh9Y2iDLEY3AQQ2/Ix6dQKnQQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8= +github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.29 h1:dQFhl5Bnl/SK1EVpgElK5dckAE+lMHXnl5WCeRvNEG0= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.29/go.mod h1:BtBP1TCx5BTCh1uTVXpo3b/odnRECBpZdL5oHQarJJs= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.52.6 h1:jlPkBSbMSpqVk47u9kqblihtXlmzYv3ZFXtuNKUNwDc= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.52.6/go.mod h1:6eUUnWOJ8sucL5Uk8rPkFo8FYioM0CTNGHga8hwzXVc= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.4 h1:/uHlzAMroQ8CDKyCxC0sTgZKQNZUoG9USaWQ8PT3fG4= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.4/go.mod h1:nZ9KOFbkwpJtaM4VaBI+Jh6b3QrAyRX/k2hcNogeUZc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.13 h1:FScsqdRyKFkw3u2ysLeWC0dbaz9I+g0xJ1JlQpH6bPo= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.13/go.mod h1:wkhwIaGltEuG4SRwNzPiJmf/tDp+yL5ym55Lt4bheno= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= -github.com/aws/aws-sdk-go-v2/service/route53 v1.59.5 h1:4Uy8lhrh4E9jS/MtmzjuEuvX7zOZTbNuPe+zkvtvRRU= -github.com/aws/aws-sdk-go-v2/service/route53 v1.59.5/go.mod h1:TUbfYOisWZWyT2qjmlMh93ERw1Ry8G4q/yT2Q8TsDag= -github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.16 h1:lzAqM9zMFwAy3ghxjeJfROdwnzO/KCPY8RYEAYpGbCM= -github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.16/go.mod h1:lYyuDbeQ6vtjRP4gb9h2MReluEb0US5u+07X84akGKg= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= -github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= -github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.53.5 h1:mSBrQCXMjEvLHsYyJVbN8QQlcITXwHEuu+8mX9e2bSo= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.53.5/go.mod h1:eEuD0vTf9mIzsSjGBFWIaNQwtH5/mzViJOVQfnMY5DE= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.9 h1:mB79k/ZTxQL4oDPxLAf2rhcUEvXlHkj3loGA2O9xREk= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.9/go.mod h1:wXQmLDkBNh60jxAaRldON9poacv+GiSIBw/kRuT/mtE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.16 h1:8g4OLy3zfNzLV20wXmZgx+QumI9WhWHnd4GCdvETxs4= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.16/go.mod h1:5a78jwLMs7BaesU0UIhLfVy2ZmOEgOy6ewYQXKTD37Q= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 h1:80pDB3Tpmb2RCSZORrK9/3iQxsd+w6vSzVqpT1FGiwE= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0/go.mod h1:6EZUGGNLPLh5Unt30uEoA+KQcByERfXIkax9qrc80nA= +github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.21 h1:/YhTlE24/FbF2gmPITNfSx1X2UzTHTiDcv8DR5vxLdY= +github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.21/go.mod h1:6rO2Gn8dZ3wsaQUwKDNqU8nkL69VKkHnVduy+wc/11k= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -253,8 +255,8 @@ github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace/go.mod h1:TK05 github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/digitalocean/godo v1.168.0 h1:mlORtUcPD91LQeJoznrH3XvfvgK3t8Wvrpph9giUT/Q= -github.com/digitalocean/godo v1.168.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= +github.com/digitalocean/godo v1.170.0 h1:T/hAGb6qK//y+XJ1K/BcsRzGBlI9iEdiFoGFlZ1DVhQ= +github.com/digitalocean/godo v1.170.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= 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= @@ -404,8 +406,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= -github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+gN0= +github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -423,8 +425,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= +github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg= @@ -682,8 +684,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/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM= -github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI= +github.com/linode/linodego v1.62.0 h1:eCo1sepsIPGkI66Cz9IaCylWxKDD2aSc5UYq20iBMfw= +github.com/linode/linodego v1.62.0/go.mod h1:FoIEsuZMRlXiUt6RnuGcPTek5iAO3VfE6bjMpGlcQ2U= 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= @@ -729,8 +731,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.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= -github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= +github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= 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= @@ -828,8 +830,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.104.1 h1:kGwTmvlg/dpsqk84+On93xw+ztOcS/jPU2MP/WesAvM= -github.com/oracle/oci-go-sdk/v65 v65.104.1/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY= +github.com/oracle/oci-go-sdk/v65 v65.105.1 h1:Ib0x36ViiDzWPLH/TERZ6ifLZUrjVX8vahaD4tiIvds= +github.com/oracle/oci-go-sdk/v65 v65.105.1/go.mod h1:8ZzvzuEG/cFLFZhxg/Mg1w19KqyXBKO3c17QIc5PkGs= 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= @@ -891,8 +893,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.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= 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= @@ -966,8 +968,8 @@ github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKv github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -1075,20 +1077,20 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -1138,8 +1140,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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= @@ -1163,8 +1165,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1201,14 +1203,14 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1218,8 +1220,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= @@ -1271,13 +1273,13 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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= @@ -1285,8 +1287,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -1327,8 +1329,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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= @@ -1343,8 +1345,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.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= -google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= +google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= +google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= 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= @@ -1360,10 +1362,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 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-20250811230008-5f3141c8851a h1:DMCgtIAIQGZqJXMVzJF4MV8BlWoJh2ZuFiRdAleyr58= -google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a/go.mod h1:y2yVLIE/CSMCPXaHnSKXxu1spLPnglFLegmgdY23uuE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 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= @@ -1377,8 +1379,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1416,8 +1418,8 @@ gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/ns1/ns1-go.v2 v2.15.1 h1:8rri2TzAPYcVbBGXn48+dz1Xg30PzHfZ4k8A9JOS0Z0= -gopkg.in/ns1/ns1-go.v2 v2.15.1/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= +gopkg.in/ns1/ns1-go.v2 v2.16.0 h1:mUczKFnrCystSV7yIODzVSbENoud3T7DwstmyVZfqg4= +gopkg.in/ns1/ns1-go.v2 v2.16.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -1445,15 +1447,15 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -istio.io/api v1.28.0 h1:0fYY9G03CAdFwE/fCkpr0v7kKsy+Hz9OCCjLNBNLbnU= -istio.io/api v1.28.0/go.mod h1:BD3qv/ekm16kvSgvSpuiDawgKhEwG97wx849CednJSg= -istio.io/client-go v1.28.0 h1:EqP19aYNvH42VQAmS/mHXZ51PU3nlrnF6MeeGldJSas= -istio.io/client-go v1.28.0/go.mod h1:mcFWH+wv9ltQqoDYyfLeVFyRZuD7n1Fj7TD5RGohqSU= +istio.io/api v1.28.1 h1:A1DzBrL6PUmDDT8uvs43wxmlg6FvdKNiZvZC1PdA50M= +istio.io/api v1.28.1/go.mod h1:BD3qv/ekm16kvSgvSpuiDawgKhEwG97wx849CednJSg= +istio.io/client-go v1.28.1 h1:oB5bD3r64rEcrXuqYMNjaON2Shz15tn8mNOGv53wrN4= +istio.io/client-go v1.28.1/go.mod h1:mcFWH+wv9ltQqoDYyfLeVFyRZuD7n1Fj7TD5RGohqSU= 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.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= 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= @@ -1462,8 +1464,8 @@ k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5U 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.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= 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= @@ -1472,8 +1474,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.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= @@ -1511,8 +1513,8 @@ sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gE sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/controller-tools v0.3.1-0.20200517180335-820a4a27ea84/go.mod h1:enhtKGfxZD1GFEoMgP8Fdbu+uKQ/cq1/WGJhdVChfvI= -sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ= -sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk= +sigs.k8s.io/gateway-api v1.4.1 h1:NPxFutNkKNa8UfLd2CMlEuhIPMQgDQ6DXNKG9sHbJU8= +sigs.k8s.io/gateway-api v1.4.1/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 327f74cacf..b6284094ca 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -119,6 +119,7 @@ type Config struct { CloudflareRegionalServices bool CloudflareRegionKey string CoreDNSPrefix string + CoreDNSStrictlyOwned bool AkamaiServiceConsumerDomain string AkamaiClientToken string AkamaiClientSecret string @@ -270,6 +271,7 @@ var defaultConfig = &Config{ Compatibility: "", ConnectorSourceServer: "localhost:8080", CoreDNSPrefix: "/skydns/", + CoreDNSStrictlyOwned: false, CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", CRDSourceKind: "DNSEndpoint", DefaultTargets: []string{}, @@ -312,6 +314,7 @@ var defaultConfig = &Config{ ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, MetricsAddress: ":7979", MinEventSyncInterval: 5 * time.Second, + MinTTL: 0, Namespace: "", NAT64Networks: []string{}, NS1Endpoint: "", @@ -705,6 +708,7 @@ func bindFlags(b FlagBinder, cfg *Config) { b.StringVar("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')", "", &cfg.CloudflareDNSRecordsComment) b.StringVar("coredns-prefix", "When using the CoreDNS provider, specify the prefix name", defaultConfig.CoreDNSPrefix, &cfg.CoreDNSPrefix) + b.BoolVar("coredns-strictly-owned", "When using the CoreDNS provider, store and filter strictly by txt-owner-id using an extra field inside of the etcd service (default: false)", defaultConfig.CoreDNSStrictlyOwned, &cfg.CoreDNSStrictlyOwned) b.StringVar("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiServiceConsumerDomain, &cfg.AkamaiServiceConsumerDomain) b.StringVar("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiClientToken, &cfg.AkamaiClientToken) b.StringVar("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiClientSecret, &cfg.AkamaiClientSecret) @@ -804,6 +808,7 @@ func bindFlags(b FlagBinder, cfg *Config) { b.BoolVar("once", "When enabled, exits the synchronization loop after the first iteration (default: disabled)", defaultConfig.Once, &cfg.Once) b.BoolVar("dry-run", "When enabled, prints DNS record changes rather than actually performing them (default: disabled)", defaultConfig.DryRun, &cfg.DryRun) b.BoolVar("events", "When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled)", defaultConfig.UpdateEvents, &cfg.UpdateEvents) + b.DurationVar("min-ttl", "Configure global TTL for records in duration format. This value is used when the TTL for a source is not set or set to 0. (optional; examples: 1m12s, 72s, 72)", defaultConfig.MinTTL, &cfg.MinTTL) // Miscellaneous flags b.EnumVar("log-format", "The format in which log messages are printed (default: text, options: text, json)", defaultConfig.LogFormat, &cfg.LogFormat, "text", "json") diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 610babbfbd..ed89984ae5 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -225,6 +225,7 @@ var ( TXTCacheInterval: 12 * time.Hour, Interval: 10 * time.Minute, MinEventSyncInterval: 50 * time.Second, + MinTTL: 40 * time.Second, Once: true, DryRun: true, UpdateEvents: true, @@ -372,6 +373,7 @@ func TestParseFlags(t *testing.T) { "--dynamodb-table=custom-table", "--interval=10m", "--min-event-sync-interval=50s", + "--min-ttl=40s", "--once", "--dry-run", "--events", @@ -495,6 +497,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_TXT_NEW_FORMAT_ONLY": "1", "EXTERNAL_DNS_INTERVAL": "10m", "EXTERNAL_DNS_MIN_EVENT_SYNC_INTERVAL": "50s", + "EXTERNAL_DNS_MIN_TTL": "40s", "EXTERNAL_DNS_ONCE": "1", "EXTERNAL_DNS_DRY_RUN": "1", "EXTERNAL_DNS_EVENTS": "1", @@ -746,7 +749,7 @@ func TestParseFlagsCliFlagSeparatedValue(t *testing.T) { assert.ElementsMatch(t, []string{"service"}, cfg.Sources) } -// Env vars are accpeted by Kingpin backend but ignored by Cobra +// Env vars are accepted by Kingpin backend but ignored by Cobra func TestEnvVarsIgnoredByCobraBackend(t *testing.T) { t.Setenv("EXTERNAL_DNS_CLI", "cobra") t.Setenv("EXTERNAL_DNS_NAMESPACE", "ns-from-env") diff --git a/plan/conflict.go b/plan/conflict.go index bf153dcfc6..a1e34ad34b 100644 --- a/plan/conflict.go +++ b/plan/conflict.go @@ -66,7 +66,7 @@ func (s PerResource) ResolveUpdate(current *endpoint.Endpoint, candidates []*end } // ResolveRecordTypes attempts to detect and resolve record type conflicts in desired -// endpoints for a domain. For eample if the there is more than 1 candidate and at lease one +// endpoints for a domain. For example if there is more than 1 candidate and at least one // of them is a CNAME. Per [RFC 1034 3.6.2] domains that contain a CNAME can not contain any // other record types. The default policy will prefer A and AAAA record types when a conflict is // detected (consistent with [endpoint.Targets.Less]). diff --git a/plan/plan.go b/plan/plan.go index 325a3d80fa..90cfb742e6 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -183,75 +183,41 @@ func (p *Plan) Calculate() *Plan { t.addCandidate(desired) } + changes := p.calculateChanges(t) + + plan := &Plan{ + Current: p.Current, + Desired: p.Desired, + Changes: changes, + // The default for ExternalDNS is to always only consider A/AAAA and CNAMEs. + // Everything else is an add on or something to be considered. + ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + } + + return plan +} + +func (p *Plan) calculateChanges(t planTable) *Changes { changes := &Changes{} for key, row := range t.rows { + switch { // dns name not taken - if len(row.current) == 0 { + case len(row.current) == 0: recordsByType := t.resolver.ResolveRecordTypes(key, row) for _, records := range recordsByType { if len(records.candidates) > 0 { changes.Create = append(changes.Create, t.resolver.ResolveCreate(records.candidates)) } } - } // dns name released or possibly owned by a different external dns - if len(row.current) > 0 && len(row.candidates) == 0 { + case len(row.candidates) == 0: changes.Delete = append(changes.Delete, row.current...) - } // dns name is taken - if len(row.current) > 0 && len(row.candidates) > 0 { - creates := []*endpoint.Endpoint{} - - // apply changes for each record type - recordsByType := t.resolver.ResolveRecordTypes(key, row) - for _, records := range recordsByType { - // record type not desired - if records.current != nil && len(records.candidates) == 0 { - changes.Delete = append(changes.Delete, records.current) - } - - // new record type desired - if records.current == nil && len(records.candidates) > 0 { - update := t.resolver.ResolveCreate(records.candidates) - // creates are evaluated after all domain records have been processed to - // validate that this external dns has ownership claim on the domain before - // adding the records to planned changes. - creates = append(creates, update) - } - - // update existing record - if records.current != nil && len(records.candidates) > 0 { - update := t.resolver.ResolveUpdate(records.current, records.candidates) - - if shouldUpdateTTL(update, records.current) || targetChanged(update, records.current) || p.shouldUpdateProviderSpecific(update, records.current) || - p.isOldOwnerIdSetAndDifferent(records.current) { - inheritOwner(records.current, update) - changes.UpdateNew = append(changes.UpdateNew, update) - changes.UpdateOld = append(changes.UpdateOld, records.current) - } - } - } - - if len(creates) > 0 { - // only add creates if the external dns has ownership claim on the domain - ownersMatch := true - for _, current := range row.current { - if p.OwnerID != "" && !current.IsOwnedBy(p.OwnerID) { - ownersMatch = false - } - } - - if ownersMatch { - changes.Create = append(changes.Create, creates...) - } else if log.GetLevel() == log.DebugLevel { - for _, current := range row.current { - log.Debugf(`Skipping endpoint %v because owner id does not match for one or more items to create, found: "%s", required: "%s"`, current, current.Labels[endpoint.OwnerLabelKey], p.OwnerID) - } - } - } + case len(row.candidates) > 0: + p.appendTakenDNSNameChanges(t, changes, key, row) } } @@ -267,16 +233,75 @@ func (p *Plan) Calculate() *Plan { changes.UpdateNew = endpoint.FilterEndpointsByOwnerID(p.OwnerID, changes.UpdateNew) } - plan := &Plan{ - Current: p.Current, - Desired: p.Desired, - Changes: changes, - // The default for ExternalDNS is to always only consider A/AAAA and CNAMEs. - // Everything else is an add on or something to be considered. - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + return changes +} + +func (p *Plan) appendTakenDNSNameChanges(t planTable, changes *Changes, key planKey, row *planTableRow) { + // apply changes for each record type + rowChanges := p.calculatePlanTableRowChanges(t, key, row) + changes.Delete = append(changes.Delete, rowChanges.Delete...) + changes.UpdateNew = append(changes.UpdateNew, rowChanges.UpdateNew...) + changes.UpdateOld = append(changes.UpdateOld, rowChanges.UpdateOld...) + if len(rowChanges.Create) == 0 { + return } - return plan + // only add creates if the external dns has ownership claim on the domain + ownersMatch := true + if p.OwnerID != "" { + for _, current := range row.current { + if !current.IsOwnedBy(p.OwnerID) { + ownersMatch = false + break + } + } + } + + if ownersMatch { + changes.Create = append(changes.Create, rowChanges.Create...) + } else if log.GetLevel() == log.DebugLevel { + for _, current := range row.current { + log.Debugf(`Skipping endpoint %v because owner id does not match for one or more items to create, found: "%s", required: "%s"`, current, current.Labels[endpoint.OwnerLabelKey], p.OwnerID) + } + } +} + +func (p *Plan) calculatePlanTableRowChanges(t planTable, key planKey, row *planTableRow) *Changes { + changes := &Changes{} + + recordsByType := t.resolver.ResolveRecordTypes(key, row) + for _, records := range recordsByType { + switch { + // record type not desired + case records.current != nil && len(records.candidates) == 0: + changes.Delete = append(changes.Delete, records.current) + + // new record type desired + case records.current == nil && len(records.candidates) > 0: + update := t.resolver.ResolveCreate(records.candidates) + // creates are evaluated after all domain records have been processed to + // validate that this external dns has ownership claim on the domain before + // adding the records to planned changes. + changes.Create = append(changes.Create, update) + + // update existing record + case records.current != nil && len(records.candidates) > 0: + p.appendEndpointUpdates(t, changes, records.current, records.candidates) + } + } + + return changes +} + +func (p *Plan) appendEndpointUpdates(t planTable, changes *Changes, current *endpoint.Endpoint, candidates []*endpoint.Endpoint) { + update := t.resolver.ResolveUpdate(current, candidates) + + if shouldUpdateTTL(update, current) || targetChanged(update, current) || + p.shouldUpdateProviderSpecific(update, current) || p.isOldOwnerIdSetAndDifferent(current) { + inheritOwner(current, update) + changes.UpdateNew = append(changes.UpdateNew, update) + changes.UpdateOld = append(changes.UpdateOld, current) + } } func (p *Plan) isOldOwnerIdSetAndDifferent(current *endpoint.Endpoint) bool { diff --git a/plan/plan_test.go b/plan/plan_test.go index acce96b27d..68c7788204 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -711,7 +711,7 @@ func (suite *PlanTestSuite) TestCurrentWithConflictingDesired() { } // TestNoCurrentWithConflictingDesired simulates where the desired records result in conflicting records types. -// This could be the result of multiple sources generating conflicting records types. In this case there the +// This could be the result of multiple sources generating conflicting records types. In this case, the // conflict resolver should prefer the A and AAAA record and drop the other candidate record types. func (suite *PlanTestSuite) TestNoCurrentWithConflictingDesired() { current := []*endpoint.Endpoint{} diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 3d0c821ba3..fae8e5ad39 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -901,6 +901,9 @@ func (p *CloudFlareProvider) getDNSRecordsMap(ctx context.Context, zoneID string // for faster getRecordID lookup recordsMap := make(DNSRecordsMap) params := dns.RecordListParams{ZoneID: cloudflare.F(zoneID)} + if p.DNSRecordsConfig.PerPage > 0 { + params.PerPage = cloudflare.F(float64(p.DNSRecordsConfig.PerPage)) + } iter := p.Client.ListDNSRecords(ctx, params) for record := range autoPagerIterator(iter) { recordsMap[newDNSRecordIndex(record)] = record diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go index d11f8a5bce..cae7a54c4f 100644 --- a/provider/cloudflare/cloudflare_test.go +++ b/provider/cloudflare/cloudflare_test.go @@ -59,14 +59,15 @@ type MockAction struct { } type mockCloudFlareClient struct { - Zones map[string]string - Records map[string]map[string]dns.RecordResponse - Actions []MockAction - listZonesError error // For v4 ListZones - getZoneError error // For v4 GetZone - dnsRecordsError error - customHostnames map[string][]cloudflarev0.CustomHostname - regionalHostnames map[string][]regionalHostname + Zones map[string]string + Records map[string]map[string]dns.RecordResponse + Actions []MockAction + listZonesError error // For v4 ListZones + getZoneError error // For v4 GetZone + dnsRecordsError error + customHostnames map[string][]cloudflarev0.CustomHostname + regionalHostnames map[string][]regionalHostname + dnsRecordsListParams dns.RecordListParams } var ExampleDomain = []dns.RecordResponse{ @@ -270,6 +271,7 @@ func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, params dns.R } func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, params dns.RecordListParams) autoPager[dns.RecordResponse] { + m.dnsRecordsListParams = params if m.dnsRecordsError != nil { return &mockAutoPager[dns.RecordResponse]{err: m.dnsRecordsError} } @@ -1061,6 +1063,35 @@ func TestCloudflareRecords(t *testing.T) { } } +func TestGetDNSRecordsMapWithPerPage(t *testing.T) { + client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{ + "001": ExampleDomain, + }) + + ctx := t.Context() + + t.Run("PerPage set to positive value", func(t *testing.T) { + provider := &CloudFlareProvider{ + Client: client, + DNSRecordsConfig: DNSRecordsConfig{PerPage: 100}, + } + _, err := provider.getDNSRecordsMap(ctx, "001") + assert.NoError(t, err) + assert.True(t, client.dnsRecordsListParams.PerPage.Present) + assert.InEpsilon(t, float64(100), client.dnsRecordsListParams.PerPage.Value, 0.0001) + }) + + t.Run("PerPage not set", func(t *testing.T) { + provider := &CloudFlareProvider{ + Client: client, + DNSRecordsConfig: DNSRecordsConfig{}, + } + _, err := provider.getDNSRecordsMap(ctx, "001") + assert.NoError(t, err) + assert.False(t, client.dnsRecordsListParams.PerPage.Present) + }) +} + func TestCloudflareProvider(t *testing.T) { var err error diff --git a/provider/coredns/coredns.go b/provider/coredns/coredns.go index 6a93e35d62..861c01ba97 100644 --- a/provider/coredns/coredns.go +++ b/provider/coredns/coredns.go @@ -28,6 +28,7 @@ import ( "time" log "github.com/sirupsen/logrus" + "go.etcd.io/etcd/api/v3/mvccpb" etcdcv3 "go.etcd.io/etcd/client/v3" "sigs.k8s.io/external-dns/pkg/tlsutils" @@ -84,10 +85,15 @@ type Service struct { // Etcd key where we found this service and ignored from json un-/marshaling Key string `json:"-"` + + // OwnedBy is used to prevent service to be added by different external-dns (only used by external-dns) + OwnedBy string `json:"ownedby,omitempty"` } type etcdClient struct { - client *etcdcv3.Client + client *etcdcv3.Client + ownerID string + strictlyOwned bool } var _ coreDNSClient = etcdClient{} @@ -106,11 +112,21 @@ func (c etcdClient) GetServices(ctx context.Context, prefix string) ([]*Service, var svcs []*Service bx := make(map[Service]bool) for _, n := range r.Kvs { - svc := new(Service) - if err := json.Unmarshal(n.Value, svc); err != nil { - return nil, fmt.Errorf("%s: %w", n.Key, err) + svc, err := c.unmarshalService(n) + if err != nil { + return nil, err + } + if c.strictlyOwned && svc.OwnedBy != c.ownerID { + continue + } + b := Service{ + Host: svc.Host, + Port: svc.Port, + Priority: svc.Priority, + Weight: svc.Weight, + Text: svc.Text, + Key: string(n.Key), } - b := Service{Host: svc.Host, Port: svc.Port, Priority: svc.Priority, Weight: svc.Weight, Text: svc.Text, Key: string(n.Key)} if _, ok := bx[b]; ok { // skip the service if already added to service list. // the same service might be found in multiple etcd nodes. @@ -132,6 +148,25 @@ func (c etcdClient) SaveService(ctx context.Context, service *Service) error { ctx, cancel := context.WithTimeout(ctx, etcdTimeout) defer cancel() + // check only for empty OwnedBy + if c.strictlyOwned && service.OwnedBy != c.ownerID { + r, err := c.client.Get(ctx, service.Key) + if err != nil { + return fmt.Errorf("etcd get %q: %w", service.Key, err) + } + // Key missing -> treat as owned (safe to create) + if r != nil && len(r.Kvs) != 0 { + svc, err := c.unmarshalService(r.Kvs[0]) + if err != nil { + return fmt.Errorf("failed to unmarshal value for key %q: %w", service.Key, err) + } + if svc.OwnedBy != c.ownerID { + return fmt.Errorf("key %q is not owned by this provider", service.Key) + } + } + service.OwnedBy = c.ownerID + } + value, err := json.Marshal(&service) if err != nil { return err @@ -148,8 +183,38 @@ func (c etcdClient) DeleteService(ctx context.Context, key string) error { ctx, cancel := context.WithTimeout(ctx, etcdTimeout) defer cancel() - _, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix()) - return err + if c.strictlyOwned { + rs, err := c.client.Get(ctx, key, etcdcv3.WithPrefix()) + if err != nil { + return err + } + for _, r := range rs.Kvs { + svc, err := c.unmarshalService(r) + if err != nil { + return err + } + if svc.OwnedBy != c.ownerID { + continue + } + + _, err = c.client.Delete(ctx, string(r.Key)) + if err != nil { + return err + } + } + return err + } else { + _, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix()) + return err + } +} + +func (c etcdClient) unmarshalService(n *mvccpb.KeyValue) (*Service, error) { + svc := new(Service) + if err := json.Unmarshal(n.Value, svc); err != nil { + return nil, fmt.Errorf("failed to unmarshal %q: %w", n.Key, err) + } + return svc, nil } // builds etcd client config depending on connection scheme and TLS parameters @@ -183,7 +248,7 @@ func getETCDConfig() (*etcdcv3.Config, error) { } // the newETCDClient is an etcd client constructor -func newETCDClient() (coreDNSClient, error) { +func newETCDClient(ownerID string, strictlyOwned bool) (coreDNSClient, error) { cfg, err := getETCDConfig() if err != nil { return nil, err @@ -192,12 +257,12 @@ func newETCDClient() (coreDNSClient, error) { if err != nil { return nil, err } - return etcdClient{c}, nil + return etcdClient{c, ownerID, strictlyOwned}, nil } // NewCoreDNSProvider is a CoreDNS provider constructor -func NewCoreDNSProvider(domainFilter *endpoint.DomainFilter, prefix string, dryRun bool) (provider.Provider, error) { - client, err := newETCDClient() +func NewCoreDNSProvider(domainFilter *endpoint.DomainFilter, prefix, ownerID string, strictlyOwned, dryRun bool) (provider.Provider, error) { + client, err := newETCDClient(ownerID, strictlyOwned) if err != nil { return nil, err } diff --git a/provider/coredns/coredns_test.go b/provider/coredns/coredns_test.go index e92b5f368a..5b8c930e1e 100644 --- a/provider/coredns/coredns_test.go +++ b/provider/coredns/coredns_test.go @@ -75,14 +75,24 @@ func (m *MockEtcdKV) Put(ctx context.Context, key, input string, _ ...etcdcv3.Op return args.Get(0).(*etcdcv3.PutResponse), args.Error(1) } -func (m *MockEtcdKV) Get(ctx context.Context, key string, _ ...etcdcv3.OpOption) (*etcdcv3.GetResponse, error) { - args := m.Called(ctx, key) - return args.Get(0).(*etcdcv3.GetResponse), args.Error(1) +func (m *MockEtcdKV) Get(ctx context.Context, key string, opts ...etcdcv3.OpOption) (*etcdcv3.GetResponse, error) { + if len(opts) == 0 { + args := m.Called(ctx, key) + return args.Get(0).(*etcdcv3.GetResponse), args.Error(1) + } else { + args := m.Called(ctx, key, opts[0]) + return args.Get(0).(*etcdcv3.GetResponse), args.Error(1) + } } func (m *MockEtcdKV) Delete(ctx context.Context, key string, opts ...etcdcv3.OpOption) (*etcdcv3.DeleteResponse, error) { - args := m.Called(ctx, key, opts[0]) - return args.Get(0).(*etcdcv3.DeleteResponse), args.Error(1) + if len(opts) == 0 { + args := m.Called(ctx, key) + return args.Get(0).(*etcdcv3.DeleteResponse), args.Error(1) + } else { + args := m.Called(ctx, key, opts[0]) + return args.Get(0).(*etcdcv3.DeleteResponse), args.Error(1) + } } func TestETCDConfig(t *testing.T) { @@ -494,14 +504,15 @@ func TestGetServices_Success(t *testing.T) { value, err := json.Marshal(svc) require.NoError(t, err) mockKV := new(MockEtcdKV) - mockKV.On("Get", mock.Anything, "/prefix").Return(&etcdcv3.GetResponse{ - Kvs: []*mvccpb.KeyValue{ - { - Key: []byte("/prefix/1"), - Value: value, + mockKV.On("Get", mock.Anything, "/prefix", mock.AnythingOfType("clientv3.OpOption")). + Return(&etcdcv3.GetResponse{ + Kvs: []*mvccpb.KeyValue{ + { + Key: []byte("/prefix/1"), + Value: value, + }, }, - }, - }, nil) + }, nil) c := etcdClient{ client: &etcdcv3.Client{ @@ -527,18 +538,19 @@ func TestGetServices_Duplicate(t *testing.T) { value, err := json.Marshal(svc) require.NoError(t, err) - mockKV.On("Get", mock.Anything, "/prefix").Return(&etcdcv3.GetResponse{ - Kvs: []*mvccpb.KeyValue{ - { - Key: []byte("/prefix/1"), - Value: value, - }, - { - Key: []byte("/prefix/1"), - Value: value, + mockKV.On("Get", mock.Anything, "/prefix", mock.AnythingOfType("clientv3.OpOption")). + Return(&etcdcv3.GetResponse{ + Kvs: []*mvccpb.KeyValue{ + { + Key: []byte("/prefix/1"), + Value: value, + }, + { + Key: []byte("/prefix/1"), + Value: value, + }, }, - }, - }, nil) + }, nil) result, err := c.GetServices(context.Background(), "/prefix") assert.NoError(t, err) @@ -560,18 +572,19 @@ func TestGetServices_Multiple(t *testing.T) { value2, err := json.Marshal(svc2) require.NoError(t, err) - mockKV.On("Get", mock.Anything, "/prefix").Return(&etcdcv3.GetResponse{ - Kvs: []*mvccpb.KeyValue{ - { - Key: []byte("/prefix/1"), - Value: value, - }, - { - Key: []byte("/prefix/2"), - Value: value2, + mockKV.On("Get", mock.Anything, "/prefix", mock.AnythingOfType("clientv3.OpOption")). + Return(&etcdcv3.GetResponse{ + Kvs: []*mvccpb.KeyValue{ + { + Key: []byte("/prefix/1"), + Value: value, + }, + { + Key: []byte("/prefix/2"), + Value: value2, + }, }, - }, - }, nil) + }, nil) result, err := c.GetServices(context.Background(), "/prefix") assert.NoError(t, err) @@ -579,26 +592,114 @@ func TestGetServices_Multiple(t *testing.T) { assert.Equal(t, priority, result[1].Priority) } -func TestGetServices_UnmarshalError(t *testing.T) { +func TestGetServices_FilterOutOtherServicesOwnerIDSetButNothingChanged(t *testing.T) { mockKV := new(MockEtcdKV) c := etcdClient{ client: &etcdcv3.Client{ KV: mockKV, }, + ownerID: "owner", + strictlyOwned: false, } - mockKV.On("Get", mock.Anything, "/prefix").Return(&etcdcv3.GetResponse{ - Kvs: []*mvccpb.KeyValue{ - { - Key: []byte("/prefix/1"), - Value: []byte("invalid-json"), + svc := Service{Host: "example.com", Port: 80, Priority: 1, Weight: 10, Text: "hello", OwnedBy: "owner"} + value, err := json.Marshal(svc) + require.NoError(t, err) + svc2 := Service{Host: "example.com", Port: 80, Priority: 0, Weight: 10, Text: "hello", OwnedBy: ""} + value2, err := json.Marshal(svc2) + require.NoError(t, err) + svc3 := Service{Host: "example.com", Port: 80, Priority: 0, Weight: 10, Text: "hello", OwnedBy: "managed-by-someone-else"} + value3, err := json.Marshal(svc3) + require.NoError(t, err) + + mockKV.On("Get", mock.Anything, "/prefix", mock.AnythingOfType("clientv3.OpOption")). + Return(&etcdcv3.GetResponse{ + Kvs: []*mvccpb.KeyValue{ + { + Key: []byte("/prefix/1"), + Value: value, + }, + { + Key: []byte("/prefix/2"), + Value: value2, + }, + { + Key: []byte("/prefix/3"), + Value: value3, + }, }, - { - Key: []byte("/prefix/1"), - Value: []byte("invalid-json"), + }, nil) + + result, err := c.GetServices(context.Background(), "/prefix") + assert.NoError(t, err) + assert.Len(t, result, 3) +} + +func TestGetServices_FilterOutOtherServicesWithStrictlyOwned(t *testing.T) { + mockKV := new(MockEtcdKV) + c := etcdClient{ + client: &etcdcv3.Client{ + KV: mockKV, + }, + ownerID: "owned-by", + strictlyOwned: true, + } + + svc := Service{Host: "example.com", Port: 80, Priority: 1, Weight: 10, Text: "hello", OwnedBy: "owned-by"} + value, err := json.Marshal(svc) + require.NoError(t, err) + svc2 := Service{Host: "example.com", Port: 80, Priority: 0, Weight: 10, Text: "hello", OwnedBy: ""} + value2, err := json.Marshal(svc2) + require.NoError(t, err) + svc3 := Service{Host: "example.com", Port: 80, Priority: 0, Weight: 10, Text: "hello", OwnedBy: "owned-by-someone-else"} + value3, err := json.Marshal(svc3) + require.NoError(t, err) + + mockKV.On("Get", mock.Anything, "/prefix", mock.AnythingOfType("clientv3.OpOption")). + Return(&etcdcv3.GetResponse{ + Kvs: []*mvccpb.KeyValue{ + { + Key: []byte("/prefix/1"), + Value: value, + }, + { + Key: []byte("/prefix/2"), + Value: value2, + }, + { + Key: []byte("/prefix/3"), + Value: value3, + }, }, + }, nil) + + result, err := c.GetServices(context.Background(), "/prefix") + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "owned-by", result[0].OwnedBy) +} + +func TestGetServices_UnmarshalError(t *testing.T) { + mockKV := new(MockEtcdKV) + c := etcdClient{ + client: &etcdcv3.Client{ + KV: mockKV, }, - }, nil) + } + + mockKV.On("Get", mock.Anything, "/prefix", mock.AnythingOfType("clientv3.OpOption")). + Return(&etcdcv3.GetResponse{ + Kvs: []*mvccpb.KeyValue{ + { + Key: []byte("/prefix/1"), + Value: []byte("invalid-json"), + }, + { + Key: []byte("/prefix/1"), + Value: []byte("invalid-json"), + }, + }, + }, nil) _, err := c.GetServices(context.Background(), "/prefix") assert.Error(t, err) @@ -613,7 +714,8 @@ func TestGetServices_GetError(t *testing.T) { }, } - mockKV.On("Get", mock.Anything, "/prefix").Return(&etcdcv3.GetResponse{}, errors.New("etcd failure")) + mockKV.On("Get", mock.Anything, "/prefix", mock.AnythingOfType("clientv3.OpOption")). + Return(&etcdcv3.GetResponse{}, errors.New("etcd failure")) _, err := c.GetServices(context.Background(), "/prefix") assert.Error(t, err) @@ -664,12 +766,151 @@ func TestDeleteService(t *testing.T) { } } +func TestDeleteServiceWithStrictlyOwned(t *testing.T) { + tests := []struct { + name string + ownerID string + key string + existingServices []Service + deletedKeys []string + }{ + { + name: "successful deletion with owned by (same) with strictly owned", + key: "/skydns/local/test", + ownerID: "owned-by", + existingServices: []Service{{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/skydns/local/test", + OwnedBy: "owned-by", + }}, + deletedKeys: []string{"/skydns/local/test"}, + }, + { + name: "prevent deletion with owned by (no one) with strictly owned", + key: "/skydns/local/test", + ownerID: "owned-by", + existingServices: []Service{{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/skydns/local/test", + }}, + deletedKeys: []string{}, + }, + { + name: "prevent deletion with owned by (other) with strictly owned", + key: "/skydns/local/test", + ownerID: "owned-by", + existingServices: []Service{{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/skydns/local/test", + OwnedBy: "owned-by-other", + }}, + deletedKeys: []string{}, + }, + { + name: "successful partial deletion with owned by (same) with strictly owned", + key: "/skydns/local/test", + ownerID: "owned-by", + existingServices: []Service{ + { + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/skydns/local/test/1", + OwnedBy: "owned-by", + }, + { + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/skydns/local/test/2", + }, + { + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/skydns/local/test/3", + OwnedBy: "owned-by-other", + }, + { + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/skydns/local/test/4", + OwnedBy: "owned-by", + }, + }, + deletedKeys: []string{"/skydns/local/test/1", "/skydns/local/test/4"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockKV := new(MockEtcdKV) + for _, key := range tt.deletedKeys { + mockKV.On("Delete", mock.Anything, key). + Return(&etcdcv3.DeleteResponse{}, nil) + } + kvs := []*mvccpb.KeyValue{} + for _, service := range tt.existingServices { + actualValue, err := json.Marshal(&service) + require.NoError(t, err) + kvs = append(kvs, &mvccpb.KeyValue{ + Key: []byte(service.Key), + Value: actualValue, + }) + } + + mockKV.On("Get", mock.Anything, tt.key, mock.AnythingOfType("clientv3.OpOption")).Return(&etcdcv3.GetResponse{ + Kvs: kvs, + }, nil) + + c := etcdClient{ + client: &etcdcv3.Client{ + KV: mockKV, + }, + ownerID: tt.ownerID, + strictlyOwned: true, + } + + err := c.DeleteService(context.Background(), tt.key) + + require.NoError(t, err) + mockKV.AssertExpectations(t) + }) + } +} + func TestSaveService(t *testing.T) { type testCase struct { - name string - service *Service - mockPutErr error - wantErr bool + name string + ownerID string + strictlyOwned bool + service *Service + expectedService *Service + exists bool + ignoreGetCall bool + mockPutErr error + wantErr bool } tests := []testCase{ { @@ -682,6 +923,181 @@ func TestSaveService(t *testing.T) { Text: "hello", Key: "/prefix/1", }, + expectedService: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + }, + }, + { + name: "success with 'owned-by' without strictly owned", + ownerID: "owned-by", + exists: true, + service: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + }, + expectedService: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + }, + }, + { + name: "success with 'owned-by' (creation) without strictly owned", + ownerID: "owned-by", + exists: false, + service: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + }, + expectedService: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + }, + }, + { + name: "success with 'owned-by' (update) without strictly owned (owner not changed)", + ownerID: "owned-by", + exists: true, + service: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + OwnedBy: "owned-by", + }, + expectedService: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + OwnedBy: "owned-by", + }, + }, + { + name: "success with different 'owned-by' without strictly owned", + ownerID: "owned-by", + exists: true, + service: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + OwnedBy: "other-owned-by", + }, + expectedService: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + OwnedBy: "other-owned-by", + }, + }, + { + name: "failed with 'owned-by' is empty with strictly owned", + ownerID: "owned-by", + strictlyOwned: true, + exists: true, + service: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + }, + wantErr: true, + }, + { + name: "success with 'owned-by' (creation) with strictly owned", + ownerID: "owned-by", + strictlyOwned: true, + exists: false, + service: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + }, + expectedService: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + OwnedBy: "owned-by", + }, + }, + { + name: "success with 'owned-by' (update) with strictly owned (owner not changed)", + ownerID: "owned-by", + strictlyOwned: true, + exists: true, + ignoreGetCall: true, + service: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + OwnedBy: "owned-by", + }, + expectedService: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + OwnedBy: "owned-by", + }, + }, + { + name: "failed with different 'owned-by' with strictly owned", + ownerID: "owned-by", + strictlyOwned: true, + exists: true, + service: &Service{ + Host: "example.com", + Port: 80, + Priority: 1, + Weight: 10, + Text: "hello", + Key: "/prefix/1", + OwnedBy: "other-owned-by", + }, + wantErr: true, }, { name: "etcd put error", @@ -689,6 +1105,10 @@ func TestSaveService(t *testing.T) { Host: "example.com", Key: "/prefix/2", }, + expectedService: &Service{ + Host: "example.com", + Key: "/prefix/2", + }, mockPutErr: errors.New("etcd failure"), wantErr: true, }, @@ -697,15 +1117,37 @@ func TestSaveService(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockKV := new(MockEtcdKV) - value, err := json.Marshal(&tt.service) + value, err := json.Marshal(&tt.expectedService) require.NoError(t, err) - mockKV.On("Put", mock.Anything, tt.service.Key, string(value)). - Return(&etcdcv3.PutResponse{}, tt.mockPutErr) + if tt.expectedService != nil { + mockKV.On("Put", mock.Anything, tt.service.Key, string(value)). + Return(&etcdcv3.PutResponse{}, tt.mockPutErr) + } + actualValue, err := json.Marshal(&tt.service) + require.NoError(t, err) + if tt.strictlyOwned && !tt.ignoreGetCall { + if tt.exists { + mockKV.On("Get", mock.Anything, tt.service.Key).Return(&etcdcv3.GetResponse{ + Kvs: []*mvccpb.KeyValue{ + { + Key: []byte(tt.service.Key), + Value: actualValue, + }, + }, + }, nil) + } else { + mockKV.On("Get", mock.Anything, tt.service.Key).Return(&etcdcv3.GetResponse{ + Kvs: []*mvccpb.KeyValue{}, + }, nil) + } + } c := etcdClient{ client: &etcdcv3.Client{ KV: mockKV, }, + ownerID: tt.ownerID, + strictlyOwned: tt.strictlyOwned, } err = c.SaveService(context.Background(), tt.service) @@ -746,7 +1188,7 @@ func TestNewCoreDNSProvider(t *testing.T) { t.Run(tt.name, func(t *testing.T) { testutils.TestHelperEnvSetter(t, tt.envs) - provider, err := NewCoreDNSProvider(&endpoint.DomainFilter{}, "/prefix/", false) + provider, err := NewCoreDNSProvider(&endpoint.DomainFilter{}, "/prefix/", "", false, false) if tt.wantErr { require.Error(t, err) assert.EqualError(t, err, tt.errMsg) diff --git a/provider/pdns/pdns.go b/provider/pdns/pdns.go index 748e220ed4..998199db47 100644 --- a/provider/pdns/pdns.go +++ b/provider/pdns/pdns.go @@ -26,6 +26,7 @@ import ( "math" "net" "net/http" + "slices" "sort" "strings" "time" @@ -60,6 +61,15 @@ const ( retryAfterTime = 250 * time.Millisecond ) +// record types which require to have trailing dot +var trailingTypes = []string{ + endpoint.RecordTypeCNAME, + endpoint.RecordTypeMX, + endpoint.RecordTypeSRV, + endpoint.RecordTypeNS, + "ALIAS", +} + // PDNSConfig is comprised of the fields necessary to create a new PDNSProvider type PDNSConfig struct { DomainFilter *endpoint.DomainFilter @@ -271,7 +281,7 @@ func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) ([]*endpoint.Endpoi } } if rr.Type_ == "ALIAS" { - rrType_ = "CNAME" + rrType_ = endpoint.RecordTypeCNAME } endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rrType_, endpoint.TTL(rr.Ttl), targets...)) return endpoints, nil @@ -320,13 +330,13 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet records := []pgo.Record{} RecordType_ := ep.RecordType for _, t := range ep.Targets { - if ep.RecordType == "CNAME" || ep.RecordType == "ALIAS" || ep.RecordType == "MX" || ep.RecordType == "SRV" { + if slices.Contains(trailingTypes, ep.RecordType) { t = provider.EnsureTrailingDot(t) } records = append(records, pgo.Record{Content: t}) } - if dnsname == zone.Name && ep.RecordType == "CNAME" { + if dnsname == zone.Name && ep.RecordType == endpoint.RecordTypeCNAME { log.Debugf("Converting APEX record %s from CNAME to ALIAS", dnsname) RecordType_ = "ALIAS" } diff --git a/provider/pdns/pdns_test.go b/provider/pdns/pdns_test.go index cac49221ef..4f7d282ef4 100644 --- a/provider/pdns/pdns_test.go +++ b/provider/pdns/pdns_test.go @@ -37,7 +37,7 @@ var ( // Simple RRSets that contain 1 A record and 1 TXT record RRSetSimpleARecord = pgo.RrSet{ Name: "example.com.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Records: []pgo.Record{ {Content: "8.8.8.8", Disabled: false, SetPtr: false}, @@ -45,7 +45,7 @@ var ( } RRSetSimpleTXTRecord = pgo.RrSet{ Name: "example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Records: []pgo.Record{ {Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"", Disabled: false, SetPtr: false}, @@ -53,7 +53,7 @@ var ( } RRSetLongARecord = pgo.RrSet{ Name: "a.very.long.domainname.example.com.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Records: []pgo.Record{ {Content: "8.8.8.8", Disabled: false, SetPtr: false}, @@ -61,7 +61,7 @@ var ( } RRSetLongTXTRecord = pgo.RrSet{ Name: "a.very.long.domainname.example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Records: []pgo.Record{ {Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"", Disabled: false, SetPtr: false}, @@ -70,7 +70,7 @@ var ( // RRSet with one record disabled RRSetDisabledRecord = pgo.RrSet{ Name: "example.com.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Records: []pgo.Record{ {Content: "8.8.8.8", Disabled: false, SetPtr: false}, @@ -80,7 +80,7 @@ var ( RRSetCNAMERecord = pgo.RrSet{ Name: "cname.example.com.", - Type_: "CNAME", + Type_: endpoint.RecordTypeCNAME, Ttl: 300, Records: []pgo.Record{ {Content: "example.com.", Disabled: false, SetPtr: false}, @@ -98,7 +98,7 @@ var ( RRSetTXTRecord = pgo.RrSet{ Name: "example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Records: []pgo.Record{ {Content: "'would smell as sweet'", Disabled: false, SetPtr: false}, @@ -108,7 +108,7 @@ var ( // Multiple PDNS records in an RRSet of a single type RRSetMultipleRecords = pgo.RrSet{ Name: "example.com.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Records: []pgo.Record{ {Content: "8.8.8.8", Disabled: false, SetPtr: false}, @@ -120,7 +120,7 @@ var ( // RRSet with MX record RRSetMXRecord = pgo.RrSet{ Name: "example.com.", - Type_: "MX", + Type_: endpoint.RecordTypeMX, Ttl: 300, Records: []pgo.Record{ {Content: "10 mailhost1.example.com", Disabled: false, SetPtr: false}, @@ -131,13 +131,24 @@ var ( // RRSet with SRV record RRSetSRVRecord = pgo.RrSet{ Name: "_service._tls.example.com.", - Type_: "SRV", + Type_: endpoint.RecordTypeSRV, Ttl: 300, Records: []pgo.Record{ {Content: "100 1 443 service.example.com", Disabled: false, SetPtr: false}, }, } + // RRSet with NS record + RRSetNSRecord = pgo.RrSet{ + Name: "sub.example.com.", + Type_: endpoint.RecordTypeNS, + Ttl: 300, + Records: []pgo.Record{ + {Content: "ns1.example.com", Disabled: false, SetPtr: false}, + {Content: "ns2.example.com", Disabled: false, SetPtr: false}, + }, + } + endpointsDisabledRecord = []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"), } @@ -185,6 +196,7 @@ var ( endpoint.NewEndpointWithTTL("alias.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.by.any.other.name.com"), endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "10 mailhost1.example.com", "10 mailhost2.example.com"), endpoint.NewEndpointWithTTL("_service._tls.example.com", endpoint.RecordTypeSRV, endpoint.TTL(300), "100 1 443 service.example.com"), + endpoint.NewEndpointWithTTL("sub.example.com", endpoint.RecordTypeNS, endpoint.TTL(300), "ns1.example.com", "ns2.example.com"), } endpointsMultipleZones = []*endpoint.Endpoint{ @@ -274,7 +286,7 @@ var ( Type_: "Zone", Url: "/api/v1/servers/localhost/zones/example.com.", Kind: "Native", - Rrsets: []pgo.RrSet{RRSetCNAMERecord, RRSetTXTRecord, RRSetMultipleRecords, RRSetALIASRecord, RRSetMXRecord, RRSetSRVRecord}, + Rrsets: []pgo.RrSet{RRSetCNAMERecord, RRSetTXTRecord, RRSetMultipleRecords, RRSetALIASRecord, RRSetMXRecord, RRSetSRVRecord, RRSetNSRecord}, } ZoneEmptyToSimplePatch = pgo.Zone{ @@ -286,7 +298,7 @@ var ( Rrsets: []pgo.RrSet{ { Name: "example.com.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -300,7 +312,7 @@ var ( }, { Name: "example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -324,7 +336,7 @@ var ( Rrsets: []pgo.RrSet{ { Name: "a.very.long.domainname.example.com.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -338,7 +350,7 @@ var ( }, { Name: "a.very.long.domainname.example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -352,7 +364,7 @@ var ( }, { Name: "example.com.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -366,7 +378,7 @@ var ( }, { Name: "example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -390,7 +402,7 @@ var ( Rrsets: []pgo.RrSet{ { Name: "a.very.long.domainname.example.com.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -404,7 +416,7 @@ var ( }, { Name: "a.very.long.domainname.example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -428,7 +440,7 @@ var ( Rrsets: []pgo.RrSet{ { Name: "mock.test.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -442,7 +454,7 @@ var ( }, { Name: "mock.test.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -466,7 +478,7 @@ var ( Rrsets: []pgo.RrSet{ { Name: "abcd.mock.test.", - Type_: "A", + Type_: endpoint.RecordTypeA, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -480,7 +492,7 @@ var ( }, { Name: "abcd.mock.test.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -504,7 +516,7 @@ var ( Rrsets: []pgo.RrSet{ { Name: "example.com.", - Type_: "A", + Type_: endpoint.RecordTypeA, Changetype: "DELETE", Records: []pgo.Record{ { @@ -517,7 +529,7 @@ var ( }, { Name: "example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Changetype: "DELETE", Records: []pgo.Record{ { @@ -540,7 +552,7 @@ var ( Rrsets: []pgo.RrSet{ { Name: "cname.example.com.", - Type_: "CNAME", + Type_: endpoint.RecordTypeCNAME, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -554,7 +566,7 @@ var ( }, { Name: "cname.example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -582,7 +594,7 @@ var ( }, { Name: "example.com.", - Type_: "TXT", + Type_: endpoint.RecordTypeTXT, Ttl: 300, Changetype: "REPLACE", Records: []pgo.Record{ @@ -955,27 +967,21 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSConvertEndpointsToZones() { suite.NoError(err) suite.Equal([]pgo.Zone{ZoneEmptyToLongPatch}, zlist) - // Check endpoints of type CNAME always have their target records end with a dot. + // Check endpoints of type CNAME, ALIAS, MX, SRV, and NS always have their values end with a trailing dot. zlist, err = p.ConvertEndpointsToZones(endpointsMixedRecords, PdnsReplace) suite.NoError(err) - for _, z := range zlist { - for _, rs := range z.Rrsets { - if rs.Type_ == "CNAME" { - for _, r := range rs.Records { - suite.Equal(uint8(0x2e), r.Content[len(r.Content)-1]) - } - } - } + trailingTypes := map[string]bool{ + endpoint.RecordTypeCNAME: true, + "ALIAS": true, + endpoint.RecordTypeMX: true, + endpoint.RecordTypeSRV: true, + endpoint.RecordTypeNS: true, } - // Check endpoints of type MX and SRV always have their values end with a trailing dot. - zlist, err = p.ConvertEndpointsToZones(endpointsMixedRecords, PdnsReplace) - suite.NoError(err) - for _, z := range zlist { for _, rs := range z.Rrsets { - if rs.Type_ == "MX" || rs.Type_ == "SRV" { + if trailingTypes[rs.Type_] { for _, r := range rs.Records { suite.Equal(uint8(0x2e), r.Content[len(r.Content)-1]) } diff --git a/provider/rfc2136/rfc2136_test.go b/provider/rfc2136/rfc2136_test.go index 6adebe979a..8c4c6938d1 100644 --- a/provider/rfc2136/rfc2136_test.go +++ b/provider/rfc2136/rfc2136_test.go @@ -547,7 +547,7 @@ func TestRfc2136GetRecords(t *testing.T) { } // Make sure the test version of SendMessage raises an error -// if a zone update ever contains records outside of it's zone +// if a zone update ever contains records outside of its zone // as the TestRfc2136ApplyChanges tests all assume this func TestRfc2136SendMessage(t *testing.T) { stub := newStub() diff --git a/registry/txt.go b/registry/txt.go index 7802367ce2..417434c851 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -158,7 +158,7 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st } func getSupportedTypes() []string { - return []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS, endpoint.RecordTypeMX} + return []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS, endpoint.RecordTypeMX, endpoint.RecordTypeSRV, endpoint.RecordTypeNAPTR} } func (im *TXTRegistry) GetDomainFilter() endpoint.DomainFilterInterface { diff --git a/registry/txt_test.go b/registry/txt_test.go index b7bb36732b..6a09cc4a21 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -122,6 +122,15 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) { newEndpointWithOwner("txt.aaaa-dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""), newEndpointWithOwner("txt.mx-mail.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newMultiTargetEndpointWithOwner( + "_sip._udp.sip1.test-zone.example.org", + []string{"1 50 5060 sip1-n1.test-zone.example.org", "1 50 5060 sip1-n2.test-zone.example.org"}, + endpoint.RecordTypeSRV, + "", + ), + newEndpointWithOwner("txt._sip._udp.sip1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("sip1.test-zone.example.org", `10 "U" "SIP+DTU" "" _sip._udp.sip1.test-zone.example.org.`, endpoint.RecordTypeNAPTR, ""), + newEndpointWithOwner("txt.sip1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ @@ -226,6 +235,25 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) { endpoint.OwnerLabelKey: "owner", }, }, + { + DNSName: "_sip._udp.sip1.test-zone.example.org", + Targets: endpoint.Targets{ + "1 50 5060 sip1-n1.test-zone.example.org", + "1 50 5060 sip1-n2.test-zone.example.org", + }, + RecordType: endpoint.RecordTypeSRV, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "sip1.test-zone.example.org", + Targets: endpoint.Targets{`10 "U" "SIP+DTU" "" _sip._udp.sip1.test-zone.example.org.`}, + RecordType: endpoint.RecordTypeNAPTR, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, } r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, "") @@ -265,6 +293,15 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) { newEndpointWithOwner("aaaa-dualstack-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""), newEndpointWithOwner("mx-mail-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newMultiTargetEndpointWithOwner( + "_sip._udp.sip1.test-zone.example.org", + []string{"1 50 5060 sip1-n1.test-zone.example.org", "1 50 5060 sip1-n2.test-zone.example.org"}, + endpoint.RecordTypeSRV, + "", + ), + newEndpointWithOwner("_sip-txt._udp.sip1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("sip1.test-zone.example.org", `10 "U" "SIP+DTU" "" _sip._udp.sip1.test-zone.example.org.`, endpoint.RecordTypeNAPTR, ""), + newEndpointWithOwner("sip1-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ @@ -361,6 +398,25 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) { endpoint.OwnerLabelKey: "owner", }, }, + { + DNSName: "_sip._udp.sip1.test-zone.example.org", + Targets: endpoint.Targets{ + "1 50 5060 sip1-n1.test-zone.example.org", + "1 50 5060 sip1-n2.test-zone.example.org", + }, + RecordType: endpoint.RecordTypeSRV, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "sip1.test-zone.example.org", + Targets: endpoint.Targets{`10 "U" "SIP+DTU" "" _sip._udp.sip1.test-zone.example.org.`}, + RecordType: endpoint.RecordTypeNAPTR, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, } r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") @@ -398,6 +454,15 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { newEndpointWithOwner("aaaa-dualstack.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner-2\"", endpoint.RecordTypeTXT, ""), newEndpointWithOwner("mail.test-zone.example.org", "10 onemail.example.com", endpoint.RecordTypeMX, ""), newEndpointWithOwner("mx-mail.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newMultiTargetEndpointWithOwner( + "_sip._udp.sip1.test-zone.example.org", + []string{"1 50 5060 sip1-n1.test-zone.example.org", "1 50 5060 sip1-n2.test-zone.example.org"}, + endpoint.RecordTypeSRV, + "", + ), + newEndpointWithOwner("_sip._udp.sip1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), + newEndpointWithOwner("sip1.test-zone.example.org", `10 "U" "SIP+DTU" "" _sip._udp.sip1.test-zone.example.org.`, endpoint.RecordTypeNAPTR, ""), + newEndpointWithOwner("sip1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, }) expectedRecords := []*endpoint.Endpoint{ @@ -488,6 +553,25 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { endpoint.OwnerLabelKey: "owner", }, }, + { + DNSName: "_sip._udp.sip1.test-zone.example.org", + Targets: endpoint.Targets{ + "1 50 5060 sip1-n1.test-zone.example.org", + "1 50 5060 sip1-n2.test-zone.example.org", + }, + RecordType: endpoint.RecordTypeSRV, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, + { + DNSName: "sip1.test-zone.example.org", + Targets: endpoint.Targets{`10 "U" "SIP+DTU" "" _sip._udp.sip1.test-zone.example.org.`}, + RecordType: endpoint.RecordTypeNAPTR, + Labels: map[string]string{ + endpoint.OwnerLabelKey: "owner", + }, + }, } r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, "") diff --git a/registry/txt_utils_test.go b/registry/txt_utils_test.go index e37c5495be..a81b619f2d 100644 --- a/registry/txt_utils_test.go +++ b/registry/txt_utils_test.go @@ -24,10 +24,23 @@ func newEndpointWithOwner(dnsName, target, recordType, ownerID string) *endpoint return newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID, nil) } +func newMultiTargetEndpointWithOwner(dnsName string, targets endpoint.Targets, recordType, ownerID string) *endpoint.Endpoint { + return newMultiTargetEndpointWithOwnerAndLabels(dnsName, targets, recordType, ownerID, nil) +} + func newEndpointWithOwnerAndOwnedRecord(dnsName, target, recordType, ownerID, ownedRecord string) *endpoint.Endpoint { return newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID, endpoint.Labels{endpoint.OwnedRecordLabelKey: ownedRecord}) } +func newMultiTargetEndpointWithOwnerAndLabels(dnsName string, targets endpoint.Targets, recordType, ownerID string, labels endpoint.Labels) *endpoint.Endpoint { + e := endpoint.NewEndpoint(dnsName, recordType, targets...) + e.Labels[endpoint.OwnerLabelKey] = ownerID + for k, v := range labels { + e.Labels[k] = v + } + return e +} + func newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID string, labels endpoint.Labels) *endpoint.Endpoint { e := endpoint.NewEndpoint(dnsName, recordType, target) e.Labels[endpoint.OwnerLabelKey] = ownerID diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh new file mode 100755 index 0000000000..2e5ba49025 --- /dev/null +++ b/scripts/e2e-test.sh @@ -0,0 +1,226 @@ +#!/bin/bash + +set -e + +KO_VERSION="0.18.0" +KIND_VERSION="0.30.0" +ALPINE_VERSION="3.22" + +echo "Starting end-to-end tests for external-dns with local provider..." + +# Install kind +echo "Installing kind..." +curl -Lo ./kind https://kind.sigs.k8s.io/dl/v${KIND_VERSION}/kind-linux-amd64 +chmod +x ./kind +sudo mv ./kind /usr/local/bin/kind + +# Create kind cluster +echo "Creating kind cluster..." +kind create cluster + +# Install kubectl +echo "Installing kubectl..." +curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +chmod +x kubectl +sudo mv kubectl /usr/local/bin/kubectl + +# Install ko +echo "Installing ko..." +curl -sSfL "https://github.com/ko-build/ko/releases/download/v${KO_VERSION}/ko_${KO_VERSION}_linux_x86_64.tar.gz" > ko.tar.gz +tar xzf ko.tar.gz ko +chmod +x ./ko +sudo mv ko /usr/local/bin/ko + +# Build external-dns +echo "Building external-dns..." +# Use ko with --local to save the image to Docker daemon +EXTERNAL_DNS_IMAGE_FULL=$(KO_DOCKER_REPO=ko.local VERSION=$(git describe --tags --always --dirty) \ + ko build --tags "$(git describe --tags --always --dirty)" --bare --sbom none \ + --platform=linux/amd64 --local .) +echo "Built image: $EXTERNAL_DNS_IMAGE_FULL" + +# Extract image name and tag (strip the @sha256 digest for kind load and kustomize) +EXTERNAL_DNS_IMAGE="${EXTERNAL_DNS_IMAGE_FULL%%@*}" +echo "Using image reference: $EXTERNAL_DNS_IMAGE" + +# apply etcd deployment as provider +echo "Applying etcd" +kubectl apply -f e2e/provider/etcd.yaml + +# Build a DNS testing image with dig +echo "Building DNS test image with dig..." +docker build -t dns-test:v1 -f - . < "$TEMP_KUSTOMIZE_DIR/deployment-args-patch.yaml" +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + template: + spec: + hostNetwork: true + containers: + - name: external-dns + args: + - --source=service + - --provider=coredns + - --txt-owner-id=external.dns + - --policy=sync + - --log-level=debug + env: + - name: ETCD_URLS + value: http://etcd-0.etcd:2379 +EOF + +# Update kustomization.yaml to include the patch +cat < "$TEMP_KUSTOMIZE_DIR/kustomization.yaml" +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +images: + - name: registry.k8s.io/external-dns/external-dns + newName: ${EXTERNAL_DNS_IMAGE%%:*} + newTag: ${EXTERNAL_DNS_IMAGE##*:} + +resources: + - ./external-dns-deployment.yaml + - ./external-dns-serviceaccount.yaml + - ./external-dns-clusterrole.yaml + - ./external-dns-clusterrolebinding.yaml + +patchesStrategicMerge: + - ./deployment-args-patch.yaml +EOF + +# Apply the kustomization +kubectl kustomize "$TEMP_KUSTOMIZE_DIR" | kubectl apply -f - + +# add a wait for the deployment to be available +kubectl wait --for=condition=available --timeout=60s deployment/external-dns || true + +kubectl describe pods -l app=external-dns +kubectl describe deployment external-dns +kubectl logs -l app=external-dns + +# Cleanup temporary directory +rm -rf "$TEMP_KUSTOMIZE_DIR" + +# Apply kubernetes yaml with service +echo "Applying Kubernetes service..." +kubectl apply -f e2e + +# Wait for convergence +echo "Waiting for convergence (90 seconds)..." +sleep 90 # normal loop is 60 seconds, this is enough and should not cause flakes + +# Check that the records are present +echo "Checking services again..." +kubectl get svc -owide +kubectl logs -l app=external-dns + +# Check that the DNS records are present using our DNS server +echo "Testing DNS server functionality..." + +# Get the node IP where the pod is running (since we're using hostNetwork) +NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}') +echo "Node IP: $NODE_IP" + +# Test our DNS server with dig +echo "Testing DNS server with dig..." + +# Create DNS test job that uses dig to query our DNS server +cat </dev/null || true + fi + if [ ! -z "$LOCAL_PROVIDER_PID" ]; then + kill $LOCAL_PROVIDER_PID 2>/dev/null || true + fi + kind delete cluster 2>/dev/null || true +} + +# Set trap to cleanup on script exit +trap cleanup EXIT diff --git a/source/ambassador_host.go b/source/ambassador_host.go index bb072371d9..545e17e3b0 100644 --- a/source/ambassador_host.go +++ b/source/ambassador_host.go @@ -148,7 +148,7 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp for _, host := range ambassadorHosts { fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name) - // look for the "exernal-dns.ambassador-service" annotation. If it is not there then just ignore this `Host` + // look for the "external-dns.ambassador-service" annotation. If it is not there then just ignore this `Host` service, found := host.Annotations[ambHostAnnotation] if !found { log.Debugf("Host %s ignored: no annotation %q found", fullname, ambHostAnnotation) diff --git a/source/gateway_httproute_test.go b/source/gateway_httproute_test.go index 031ae93cf7..d82c51e64e 100644 --- a/source/gateway_httproute_test.go +++ b/source/gateway_httproute_test.go @@ -1329,7 +1329,7 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) { gateways: []*v1beta1.Gateway{ { ObjectMeta: metav1.ObjectMeta{ - Name: "overriden-gateway", + Name: "overridden-gateway", Namespace: "gateway-namespace", Annotations: map[string]string{ annotations.TargetKey: "4.3.2.1", @@ -1350,12 +1350,12 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) { Hostnames: hostnames("test.example.internal"), CommonRouteSpec: v1.CommonRouteSpec{ ParentRefs: []v1.ParentReference{ - gwParentRef("gateway-namespace", "overriden-gateway"), + gwParentRef("gateway-namespace", "overridden-gateway"), }, }, }, Status: httpRouteStatus( // The route is attached to both gateways. - gwParentRef("gateway-namespace", "overriden-gateway"), + gwParentRef("gateway-namespace", "overridden-gateway"), ), }}, endpoints: []*endpoint.Endpoint{ @@ -1371,7 +1371,7 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) { gateways: []*v1beta1.Gateway{ { ObjectMeta: metav1.ObjectMeta{ - Name: "overriden-gateway", + Name: "overridden-gateway", Namespace: "gateway-namespace", Annotations: map[string]string{ annotations.TargetKey: "4.3.2.1", @@ -1402,13 +1402,13 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) { Hostnames: hostnames("test.example.internal"), CommonRouteSpec: v1.CommonRouteSpec{ ParentRefs: []v1.ParentReference{ - gwParentRef("gateway-namespace", "overriden-gateway"), + gwParentRef("gateway-namespace", "overridden-gateway"), gwParentRef("gateway-namespace", "test"), }, }, }, Status: httpRouteStatus( - gwParentRef("gateway-namespace", "overriden-gateway"), + gwParentRef("gateway-namespace", "overridden-gateway"), gwParentRef("gateway-namespace", "test"), ), }}, diff --git a/source/gloo_proxy.go b/source/gloo_proxy.go index cfeefa2ed0..a443c4eb25 100644 --- a/source/gloo_proxy.go +++ b/source/gloo_proxy.go @@ -25,12 +25,20 @@ import ( log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/kubernetes" + kubeinformers "k8s.io/client-go/informers" + coreinformers "k8s.io/client-go/informers/core/v1" + netinformers "k8s.io/client-go/informers/networking/v1" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/informers" ) var ( @@ -44,6 +52,11 @@ var ( Version: "v1", Resource: "virtualservices", } + gatewayGVR = schema.GroupVersionResource{ + Group: "gateway.solo.io", + Version: "v1", + Resource: "gateways", + } ) // Basic redefinition of "Proxy" CRD : https://github.com/solo-io/gloo/blob/v1.4.6/projects/gloo/pkg/api/v1/proxy.pb.go @@ -58,7 +71,22 @@ type proxySpec struct { } type proxySpecListener struct { - HTTPListener proxySpecHTTPListener `json:"httpListener,omitempty"` + HTTPListener proxySpecHTTPListener `json:"httpListener,omitempty"` + MetadataStatic proxyMetadataStatic `json:"metadataStatic,omitempty"` +} + +type proxyMetadataStatic struct { + Source []proxyMetadataStaticSource `json:"sources,omitempty"` +} + +type proxyMetadataStaticSource struct { + ResourceKind string `json:"resourceKind,omitempty"` + ResourceRef proxyMetadataStaticSourceResourceRef `json:"resourceRef,omitempty"` +} + +type proxyMetadataStaticSourceResourceRef struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` } type proxySpecHTTPListener struct { @@ -96,17 +124,49 @@ type proxyVirtualHostMetadataSourceResourceRef struct { } type glooSource struct { - dynamicKubeClient dynamic.Interface - kubeClient kubernetes.Interface - glooNamespaces []string + serviceInformer coreinformers.ServiceInformer + ingressInformer netinformers.IngressInformer + proxyInformer kubeinformers.GenericInformer + virtualServiceInformer kubeinformers.GenericInformer + gatewayInformer kubeinformers.GenericInformer + glooNamespaces []string } // NewGlooSource creates a new glooSource with the given config -func NewGlooSource(dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, +func NewGlooSource(ctx context.Context, dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, glooNamespaces []string) (Source, error) { + informerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, 0) + serviceInformer := informerFactory.Core().V1().Services() + ingressInformer := informerFactory.Networking().V1().Ingresses() + + _, _ = serviceInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + _, _ = ingressInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + + dynamicInformerFactory := dynamicinformer.NewDynamicSharedInformerFactory(dynamicKubeClient, 0) + + proxyInformer := dynamicInformerFactory.ForResource(proxyGVR) + virtualServiceInformer := dynamicInformerFactory.ForResource(virtualServiceGVR) + gatewayInformer := dynamicInformerFactory.ForResource(gatewayGVR) + + _, _ = proxyInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + _, _ = virtualServiceInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + _, _ = gatewayInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + + informerFactory.Start(ctx.Done()) + dynamicInformerFactory.Start(ctx.Done()) + if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + return nil, err + } + if err := informers.WaitForDynamicCacheSync(ctx, dynamicInformerFactory); err != nil { + return nil, err + } + return &glooSource{ - dynamicKubeClient, - kubeClient, + serviceInformer, + ingressInformer, + proxyInformer, + virtualServiceInformer, + gatewayInformer, glooNamespaces, }, nil } @@ -119,32 +179,45 @@ func (gs *glooSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro endpoints := []*endpoint.Endpoint{} for _, ns := range gs.glooNamespaces { - proxies, err := gs.dynamicKubeClient.Resource(proxyGVR).Namespace(ns).List(ctx, metav1.ListOptions{}) + proxyObjects, err := gs.proxyInformer.Lister().ByNamespace(ns).List(labels.Everything()) if err != nil { return nil, err } - for _, obj := range proxies.Items { - proxy := proxy{} - jsonString, err := obj.MarshalJSON() - if err != nil { + + for _, obj := range proxyObjects { + unstructuredObj, ok := obj.(*unstructured.Unstructured) + if !ok { return nil, err } - err = json.Unmarshal(jsonString, &proxy) + + jsonData, err := json.Marshal(unstructuredObj.Object) if err != nil { return nil, err } + + var proxy proxy + if err = json.Unmarshal(jsonData, &proxy); err != nil { + return nil, err + } log.Debugf("Gloo: Find %s proxy", proxy.Metadata.Name) proxyTargets := annotations.TargetsFromTargetAnnotation(proxy.Metadata.Annotations) if len(proxyTargets) == 0 { - proxyTargets, err = gs.proxyTargets(ctx, proxy.Metadata.Name, ns) + proxyTargets, err = gs.targetsFromGatewayIngress(&proxy) + if err != nil { + return nil, err + } + } + + if len(proxyTargets) == 0 { + proxyTargets, err = gs.proxyTargets(proxy.Metadata.Name, ns) if err != nil { return nil, err } } log.Debugf("Gloo[%s]: Find %d target(s) (%+v)", proxy.Metadata.Name, len(proxyTargets), proxyTargets) - proxyEndpoints, err := gs.generateEndpointsFromProxy(ctx, &proxy, proxyTargets) + proxyEndpoints, err := gs.generateEndpointsFromProxy(&proxy, proxyTargets) if err != nil { return nil, err } @@ -155,14 +228,14 @@ func (gs *glooSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro return endpoints, nil } -func (gs *glooSource) generateEndpointsFromProxy(ctx context.Context, proxy *proxy, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { +func (gs *glooSource) generateEndpointsFromProxy(proxy *proxy, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { endpoints := []*endpoint.Endpoint{} resource := fmt.Sprintf("proxy/%s/%s", proxy.Metadata.Namespace, proxy.Metadata.Name) for _, listener := range proxy.Spec.Listeners { for _, virtualHost := range listener.HTTPListener.VirtualHosts { - ants, err := gs.annotationsFromProxySource(ctx, virtualHost) + ants, err := gs.annotationsFromProxySource(virtualHost) if err != nil { return nil, err } @@ -176,37 +249,53 @@ func (gs *glooSource) generateEndpointsFromProxy(ctx context.Context, proxy *pro return endpoints, nil } -func (gs *glooSource) annotationsFromProxySource(ctx context.Context, virtualHost proxyVirtualHost) (map[string]string, error) { +func (gs *glooSource) annotationsFromProxySource(virtualHost proxyVirtualHost) (map[string]string, error) { ants := map[string]string{} for _, src := range virtualHost.Metadata.Source { - kind := sourceKind(src.Kind) - if kind != nil { - source, err := gs.dynamicKubeClient.Resource(*kind).Namespace(src.Namespace).Get(ctx, src.Name, metav1.GetOptions{}) - if err != nil { - return nil, err - } - for key, value := range source.GetAnnotations() { - ants[key] = value - } + if src.Kind != "*v1.VirtualService" { + log.Debugf("Unsupported listener source. Expecting '*v1.VirtualService', got (%s)", src.Kind) + continue + } + + virtualServiceObj, err := gs.virtualServiceInformer.Lister().ByNamespace(src.Namespace).Get(src.Name) + if err != nil { + return nil, err + } + unstructuredVirtualService, ok := virtualServiceObj.(*unstructured.Unstructured) + if !ok { + log.Error("unexpected object: it is not *unstructured.Unstructured") + continue + } + + for key, value := range unstructuredVirtualService.GetAnnotations() { + ants[key] = value } } + for _, src := range virtualHost.MetadataStatic.Source { - kind := sourceKind(src.ResourceKind) - if kind != nil { - source, err := gs.dynamicKubeClient.Resource(*kind).Namespace(src.ResourceRef.Namespace).Get(ctx, src.ResourceRef.Name, metav1.GetOptions{}) - if err != nil { - return nil, err - } - for key, value := range source.GetAnnotations() { - ants[key] = value - } + if src.ResourceKind != "*v1.VirtualService" { + log.Debugf("Unsupported listener source. Expecting '*v1.VirtualService', got (%s)", src.ResourceKind) + continue + } + virtualServiceObj, err := gs.virtualServiceInformer.Lister().ByNamespace(src.ResourceRef.Namespace).Get(src.ResourceRef.Name) + if err != nil { + return nil, err + } + unstructuredVirtualService, ok := virtualServiceObj.(*unstructured.Unstructured) + if !ok { + log.Error("unexpected object: it is not *unstructured.Unstructured") + continue + } + + for key, value := range unstructuredVirtualService.GetAnnotations() { + ants[key] = value } } return ants, nil } -func (gs *glooSource) proxyTargets(ctx context.Context, name string, namespace string) (endpoint.Targets, error) { - svc, err := gs.kubeClient.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{}) +func (gs *glooSource) proxyTargets(name string, namespace string) (endpoint.Targets, error) { + svc, err := gs.serviceInformer.Lister().Services(namespace).Get(name) if err != nil { return nil, err } @@ -228,9 +317,48 @@ func (gs *glooSource) proxyTargets(ctx context.Context, name string, namespace s return targets, nil } -func sourceKind(kind string) *schema.GroupVersionResource { - if kind == "*v1.VirtualService" { - return &virtualServiceGVR +func (gs *glooSource) targetsFromGatewayIngress(proxy *proxy) (endpoint.Targets, error) { + targets := make(endpoint.Targets, 0) + + for _, listener := range proxy.Spec.Listeners { + for _, source := range listener.MetadataStatic.Source { + if source.ResourceKind != "*v1.Gateway" { + log.Debugf("Unsupported listener source. Expecting '*v1.Gateway', got (%s)", source.ResourceKind) + continue + } + gatewayObj, err := gs.gatewayInformer.Lister().ByNamespace(source.ResourceRef.Namespace).Get(source.ResourceRef.Name) + if err != nil { + return nil, err + } + unstructuredGateway, ok := gatewayObj.(*unstructured.Unstructured) + if !ok { + log.Error("unexpected object: it is not *unstructured.Unstructured") + continue + } + + if ingressStr, ok := unstructuredGateway.GetAnnotations()[annotations.Ingress]; ok && ingressStr != "" { + namespace, name, err := ParseIngress(ingressStr) + if err != nil { + return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", unstructuredGateway.GetNamespace(), unstructuredGateway.GetName(), err) + } + if namespace == "" { + namespace = unstructuredGateway.GetNamespace() + } + + ingress, err := gs.ingressInformer.Lister().Ingresses(namespace).Get(name) + if err != nil { + return nil, err + } + + for _, lb := range ingress.Status.LoadBalancer.Ingress { + if lb.IP != "" { + targets = append(targets, lb.IP) + } else if lb.Hostname != "" { + targets = append(targets, lb.Hostname) + } + } + } + } } - return nil + return targets, nil } diff --git a/source/gloo_proxy_test.go b/source/gloo_proxy_test.go index 03eb12b364..b79e73c094 100644 --- a/source/gloo_proxy_test.go +++ b/source/gloo_proxy_test.go @@ -19,10 +19,12 @@ package source import ( "context" "encoding/json" + "fmt" "testing" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -212,7 +214,7 @@ var externalProxySource = metav1.PartialObjectMetadata{ } // Proxy with metadata static test -var proxyMetadataStatic = proxy{ +var proxyWithMetadataStatic = proxy{ TypeMeta: metav1.TypeMeta{ APIVersion: proxyGVR.GroupVersion().String(), Kind: "Proxy", @@ -261,10 +263,10 @@ var proxyMetadataStatic = proxy{ }, } -var proxyMetadataStaticSvc = corev1.Service{ +var proxyWithMetadataStaticSvc = corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Name: proxyMetadataStatic.Metadata.Name, - Namespace: proxyMetadataStatic.Metadata.Namespace, + Name: proxyWithMetadataStatic.Metadata.Name, + Namespace: proxyWithMetadataStatic.Metadata.Namespace, }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, @@ -286,14 +288,14 @@ var proxyMetadataStaticSvc = corev1.Service{ }, } -var proxyMetadataStaticSource = metav1.PartialObjectMetadata{ +var proxyWithMetadataStaticSource = metav1.PartialObjectMetadata{ TypeMeta: metav1.TypeMeta{ APIVersion: virtualServiceGVR.GroupVersion().String(), Kind: "VirtualService", }, ObjectMeta: metav1.ObjectMeta{ - Name: proxyMetadataStatic.Spec.Listeners[0].HTTPListener.VirtualHosts[1].MetadataStatic.Source[0].ResourceRef.Name, - Namespace: proxyMetadataStatic.Spec.Listeners[0].HTTPListener.VirtualHosts[1].MetadataStatic.Source[0].ResourceRef.Namespace, + Name: proxyWithMetadataStatic.Spec.Listeners[0].HTTPListener.VirtualHosts[1].MetadataStatic.Source[0].ResourceRef.Name, + Namespace: proxyWithMetadataStatic.Spec.Listeners[0].HTTPListener.VirtualHosts[1].MetadataStatic.Source[0].ResourceRef.Namespace, Annotations: map[string]string{ "external-dns.alpha.kubernetes.io/ttl": "420", "external-dns.alpha.kubernetes.io/aws-geolocation-country-code": "ES", @@ -392,21 +394,98 @@ var targetAnnotatedProxySource = metav1.PartialObjectMetadata{ }, } +// Proxy backed by Ingress +var gatewayIngressAnnotatedProxy = proxy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: proxyGVR.GroupVersion().String(), + Kind: "Proxy", + }, + Metadata: metav1.ObjectMeta{ + Name: "gateway-ingress-annotated", + Namespace: defaultGlooNamespace, + }, + Spec: proxySpec{ + Listeners: []proxySpecListener{ + { + HTTPListener: proxySpecHTTPListener{ + VirtualHosts: []proxyVirtualHost{ + { + Domains: []string{"k.test"}, + MetadataStatic: proxyVirtualHostMetadataStatic{ + Source: []proxyVirtualHostMetadataStaticSource{ + { + ResourceKind: "*v1.Unknown", + ResourceRef: proxyVirtualHostMetadataSourceResourceRef{ + Name: "my-unknown-svc", + Namespace: "unknown", + }, + }, + }, + }, + }, + }, + }, + MetadataStatic: proxyMetadataStatic{ + Source: []proxyMetadataStaticSource{ + { + ResourceKind: "*v1.Gateway", + ResourceRef: proxyMetadataStaticSourceResourceRef{ + Name: "gateway-ingress-annotated", + Namespace: defaultGlooNamespace, + }, + }, + }, + }, + }, + }, + }, +} + +var gatewayIngressAnnotatedProxyGateway = metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayGVR.GroupVersion().String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: gatewayIngressAnnotatedProxy.Spec.Listeners[0].MetadataStatic.Source[0].ResourceRef.Name, + Namespace: gatewayIngressAnnotatedProxy.Spec.Listeners[0].MetadataStatic.Source[0].ResourceRef.Namespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/ingress": fmt.Sprintf("%s/%s", gatewayIngressAnnotatedProxy.Spec.Listeners[0].MetadataStatic.Source[0].ResourceRef.Namespace, gatewayIngressAnnotatedProxy.Spec.Listeners[0].MetadataStatic.Source[0].ResourceRef.Name), + }, + }, +} + +var gatewayIngressAnnotatedProxyIngress = networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: gatewayIngressAnnotatedProxy.Spec.Listeners[0].MetadataStatic.Source[0].ResourceRef.Name, + Namespace: gatewayIngressAnnotatedProxy.Spec.Listeners[0].MetadataStatic.Source[0].ResourceRef.Namespace, + }, + Status: networkingv1.IngressStatus{ + LoadBalancer: networkingv1.IngressLoadBalancerStatus{ + Ingress: []networkingv1.IngressLoadBalancerIngress{ + { + Hostname: "example.com", + }, + }, + }, + }, +} + func TestGlooSource(t *testing.T) { t.Parallel() fakeKubernetesClient := fakeKube.NewSimpleClientset() fakeDynamicClient := fakeDynamic.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), map[schema.GroupVersionResource]string{ - proxyGVR: "ProxyList", + proxyGVR: "ProxyList", + virtualServiceGVR: "VirtualServiceList", + gatewayGVR: "GatewayList", }) - source, err := NewGlooSource(fakeDynamicClient, fakeKubernetesClient, []string{defaultGlooNamespace}) - assert.NoError(t, err) - assert.NotNil(t, source) - internalProxyUnstructured := unstructured.Unstructured{} externalProxyUnstructured := unstructured.Unstructured{} + gatewayIngressAnnotatedProxyUnstructured := unstructured.Unstructured{} + gatewayIngressAnnotatedProxyGatewayUnstructured := unstructured.Unstructured{} proxyMetadataStaticUnstructured := unstructured.Unstructured{} targetAnnotatedProxyUnstructured := unstructured.Unstructured{} @@ -421,7 +500,13 @@ func TestGlooSource(t *testing.T) { externalProxyAsJSON, err := json.Marshal(externalProxy) assert.NoError(t, err) - proxyMetadataStaticAsJSON, err := json.Marshal(proxyMetadataStatic) + gatewayIngressAnnotatedProxyAsJSON, err := json.Marshal(gatewayIngressAnnotatedProxy) + assert.NoError(t, err) + + gatewayIngressAnnotatedProxyGatewayAsJSON, err := json.Marshal(gatewayIngressAnnotatedProxyGateway) + assert.NoError(t, err) + + proxyMetadataStaticAsJSON, err := json.Marshal(proxyWithMetadataStatic) assert.NoError(t, err) targetAnnotatedProxyAsJSON, err := json.Marshal(targetAnnotatedProxy) @@ -433,7 +518,7 @@ func TestGlooSource(t *testing.T) { externalProxySvcAsJSON, err := json.Marshal(externalProxySource) assert.NoError(t, err) - proxyMetadataStaticSvcAsJSON, err := json.Marshal(proxyMetadataStaticSource) + proxyMetadataStaticSvcAsJSON, err := json.Marshal(proxyWithMetadataStaticSource) assert.NoError(t, err) targetAnnotatedProxySvcAsJSON, err := json.Marshal(targetAnnotatedProxySource) @@ -441,6 +526,8 @@ func TestGlooSource(t *testing.T) { assert.NoError(t, internalProxyUnstructured.UnmarshalJSON(internalProxyAsJSON)) assert.NoError(t, externalProxyUnstructured.UnmarshalJSON(externalProxyAsJSON)) + assert.NoError(t, gatewayIngressAnnotatedProxyUnstructured.UnmarshalJSON(gatewayIngressAnnotatedProxyAsJSON)) + assert.NoError(t, gatewayIngressAnnotatedProxyGatewayUnstructured.UnmarshalJSON(gatewayIngressAnnotatedProxyGatewayAsJSON)) assert.NoError(t, proxyMetadataStaticUnstructured.UnmarshalJSON(proxyMetadataStaticAsJSON)) assert.NoError(t, targetAnnotatedProxyUnstructured.UnmarshalJSON(targetAnnotatedProxyAsJSON)) @@ -449,6 +536,18 @@ func TestGlooSource(t *testing.T) { assert.NoError(t, proxyMetadataStaticSourceUnstructured.UnmarshalJSON(proxyMetadataStaticSvcAsJSON)) assert.NoError(t, targetAnnotatedProxySourceUnstructured.UnmarshalJSON(targetAnnotatedProxySvcAsJSON)) + _, err = fakeKubernetesClient.CoreV1().Services(internalProxySvc.GetNamespace()).Create(context.Background(), &internalProxySvc, metav1.CreateOptions{}) + assert.NoError(t, err) + _, err = fakeKubernetesClient.CoreV1().Services(externalProxySvc.GetNamespace()).Create(context.Background(), &externalProxySvc, metav1.CreateOptions{}) + assert.NoError(t, err) + _, err = fakeKubernetesClient.CoreV1().Services(proxyWithMetadataStaticSvc.GetNamespace()).Create(context.Background(), &proxyWithMetadataStaticSvc, metav1.CreateOptions{}) + assert.NoError(t, err) + _, err = fakeKubernetesClient.CoreV1().Services(targetAnnotatedProxySvc.GetNamespace()).Create(context.Background(), &targetAnnotatedProxySvc, metav1.CreateOptions{}) + assert.NoError(t, err) + + _, err = fakeKubernetesClient.NetworkingV1().Ingresses(gatewayIngressAnnotatedProxyIngress.GetNamespace()).Create(context.Background(), &gatewayIngressAnnotatedProxyIngress, metav1.CreateOptions{}) + assert.NoError(t, err) + // Create proxy resources _, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &internalProxyUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) @@ -458,30 +557,31 @@ func TestGlooSource(t *testing.T) { assert.NoError(t, err) _, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &targetAnnotatedProxyUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) + _, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &gatewayIngressAnnotatedProxyUnstructured, metav1.CreateOptions{}) + assert.NoError(t, err) // Create proxy source _, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(internalProxySource.Namespace).Create(context.Background(), &internalProxySourceUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) _, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(externalProxySource.Namespace).Create(context.Background(), &externalProxySourceUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) - _, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(proxyMetadataStaticSource.Namespace).Create(context.Background(), &proxyMetadataStaticSourceUnstructured, metav1.CreateOptions{}) + _, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(proxyWithMetadataStaticSource.Namespace).Create(context.Background(), &proxyMetadataStaticSourceUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) _, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(targetAnnotatedProxySource.Namespace).Create(context.Background(), &targetAnnotatedProxySourceUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) - // Create proxy service resources - _, err = fakeKubernetesClient.CoreV1().Services(internalProxySvc.GetNamespace()).Create(context.Background(), &internalProxySvc, metav1.CreateOptions{}) + // Create gateway resource + _, err = fakeDynamicClient.Resource(gatewayGVR).Namespace(gatewayIngressAnnotatedProxyGateway.Namespace).Create(context.Background(), &gatewayIngressAnnotatedProxyGatewayUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) - _, err = fakeKubernetesClient.CoreV1().Services(externalProxySvc.GetNamespace()).Create(context.Background(), &externalProxySvc, metav1.CreateOptions{}) - assert.NoError(t, err) - _, err = fakeKubernetesClient.CoreV1().Services(proxyMetadataStaticSvc.GetNamespace()).Create(context.Background(), &proxyMetadataStaticSvc, metav1.CreateOptions{}) - assert.NoError(t, err) - _, err = fakeKubernetesClient.CoreV1().Services(targetAnnotatedProxySvc.GetNamespace()).Create(context.Background(), &targetAnnotatedProxySvc, metav1.CreateOptions{}) + + source, err := NewGlooSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, []string{defaultGlooNamespace}) assert.NoError(t, err) + assert.NotNil(t, source) endpoints, err := source.Endpoints(context.Background()) assert.NoError(t, err) - assert.Len(t, endpoints, 10) + assert.Len(t, endpoints, 11) + assert.ElementsMatch(t, endpoints, []*endpoint.Endpoint{ { DNSName: "a.test", @@ -537,7 +637,7 @@ func TestGlooSource(t *testing.T) { }, { DNSName: "f.test", - Targets: []string{proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP}, + Targets: []string{proxyWithMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyWithMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyWithMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP}, RecordType: endpoint.RecordTypeA, RecordTTL: 0, Labels: endpoint.Labels{}, @@ -545,7 +645,7 @@ func TestGlooSource(t *testing.T) { }, { DNSName: "g.test", - Targets: []string{proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP}, + Targets: []string{proxyWithMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyWithMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyWithMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP}, RecordType: endpoint.RecordTypeA, RecordTTL: 0, Labels: endpoint.Labels{}, @@ -553,7 +653,7 @@ func TestGlooSource(t *testing.T) { }, { DNSName: "h.test", - Targets: []string{proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP}, + Targets: []string{proxyWithMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyWithMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyWithMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP}, RecordType: endpoint.RecordTypeA, SetIdentifier: "identifier", RecordTTL: 420, @@ -586,5 +686,12 @@ func TestGlooSource(t *testing.T) { }, }, }, + { + DNSName: "k.test", + Targets: []string{gatewayIngressAnnotatedProxyIngress.Status.LoadBalancer.Ingress[0].Hostname}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{}}, }) } diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go index ce12f1cf76..c17598cc0f 100644 --- a/source/istio_gateway_test.go +++ b/source/istio_gateway_test.go @@ -425,7 +425,7 @@ func testEndpointsFromGatewayConfig(t *testing.T) { }, }, { - title: "one gateway, ingress in seperate namespace", + title: "one gateway, ingress in separate namespace", ingresses: []fakeIngress{ { hostnames: []string{"lb.com"}, diff --git a/source/service.go b/source/service.go index afc7289e70..000fd628a1 100644 --- a/source/service.go +++ b/source/service.go @@ -38,6 +38,7 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/source/informers" "sigs.k8s.io/external-dns/source/annotations" @@ -81,6 +82,7 @@ type serviceSource struct { nodeInformer coreinformers.NodeInformer serviceTypeFilter *serviceTypes exposeInternalIPv6 bool + excludeUnschedulable bool // process Services with legacy annotations compatibility string @@ -97,7 +99,7 @@ func NewServiceSource( ignoreHostnameAnnotation bool, labelSelector labels.Selector, resolveLoadBalancerHostname, - listenEndpointEvents, exposeInternalIPv6 bool, + listenEndpointEvents, exposeInternalIPv6, excludeUnschedulable bool, ) (Source, error) { tmpl, err := fqdn.ParseTemplate(fqdnTemplate) if err != nil { @@ -223,6 +225,7 @@ func NewServiceSource( resolveLoadBalancerHostname: resolveLoadBalancerHostname, listenEndpointEvents: listenEndpointEvents, exposeInternalIPv6: exposeInternalIPv6, + excludeUnschedulable: excludeUnschedulable, }, nil } @@ -811,6 +814,10 @@ func (sc *serviceSource) extractNodePortTargets(svc *v1.Service) (endpoint.Targe } for _, node := range nodes { + if node.Spec.Unschedulable && sc.excludeUnschedulable { + log.Debugf("Skipping node %s - unschedulable", node.Name) + continue + } for _, address := range node.Status.Addresses { switch address.Type { case v1.NodeExternalIP: @@ -855,7 +862,7 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, hostname stri // see https://en.wikipedia.org/wiki/SRV_record // build a target with a priority of 0, weight of 50, and pointing the given port on the given host - target := fmt.Sprintf("0 50 %d %s", port.NodePort, hostname) + target := fmt.Sprintf("0 50 %d %s", port.NodePort, provider.EnsureTrailingDot(hostname)) // take the service name from the K8s Service object // it is safe to use since it is DNS compatible diff --git a/source/service_fqdn_test.go b/source/service_fqdn_test.go index 3132be4d27..eb900f7144 100644 --- a/source/service_fqdn_test.go +++ b/source/service_fqdn_test.go @@ -412,7 +412,7 @@ func TestServiceSourceFqdnTemplatingExamples(t *testing.T) { }, }, }, - fqdnTemplate: `{{ $name := .Name }}{{ range .Spec.Ports -}}{{ $name }}{{ if eq .Name "http2" }}.http2{{ else if eq .Name "debug" }}.debug{{ end }}.example.tld{{printf "," }}{{ end }}`, + fqdnTemplate: `{{ $name := .Name }}{{ range .Spec.Ports -}}{{ $name }}{{ if eq .Name "http2" }}.http2{{ else if eq .Name "debug" }}.debug{{ end }}.example.tld.{{printf "," }}{{ end }}`, expected: []*endpoint.Endpoint{ // TODO: This test shows that there is a bug that needs to be fixed in the external-dns logic. {DNSName: "", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.51.100.22", "192.51.100.5"}}, @@ -736,15 +736,15 @@ func TestServiceSourceFqdnTemplatingExamples(t *testing.T) { expected: []*endpoint.Endpoint{ // TODO: This test shows that there is a bug that needs to be fixed in the external-dns logic. Not a critical issue, as will be filtered out by the source. {DNSName: "", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.96.41.132", "203.0.113.10"}}, - {DNSName: "_service-one._tcp", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30080 ", "0 50 30082 "}}, // TODO: wrong SRV target format - {DNSName: "_service-one._tcp.debug.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30080 debug.host.tld", "0 50 30082 debug.host.tld"}}, - {DNSName: "_service-one._tcp.http.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30080 http.host.tld", "0 50 30082 http.host.tld"}}, - {DNSName: "_service-three._tcp", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 25565 "}}, // TODO: wrong SRV target format - {DNSName: "_service-three._tcp.debug.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 25565 debug.host.tld"}}, - {DNSName: "_service-three._tcp.minecraft.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 25565 minecraft.host.tld"}}, - {DNSName: "_service-three._udp", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30083 "}}, // TODO: wrong SRV target format - {DNSName: "_service-three._udp.debug.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30083 debug.host.tld"}}, - {DNSName: "_service-three._udp.minecraft.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30083 minecraft.host.tld"}}, + {DNSName: "_service-one._tcp", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30080 .", "0 50 30082 ."}}, // TODO: wrong SRV target format + {DNSName: "_service-one._tcp.debug.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30080 debug.host.tld.", "0 50 30082 debug.host.tld."}}, + {DNSName: "_service-one._tcp.http.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30080 http.host.tld.", "0 50 30082 http.host.tld."}}, + {DNSName: "_service-three._tcp", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 25565 ."}}, // TODO: wrong SRV target format + {DNSName: "_service-three._tcp.debug.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 25565 debug.host.tld."}}, + {DNSName: "_service-three._tcp.minecraft.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 25565 minecraft.host.tld."}}, + {DNSName: "_service-three._udp", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30083 ."}}, // TODO: wrong SRV target format + {DNSName: "_service-three._udp.debug.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30083 debug.host.tld."}}, + {DNSName: "_service-three._udp.minecraft.host.tld", RecordType: endpoint.RecordTypeSRV, Targets: endpoint.Targets{"0 50 30083 minecraft.host.tld."}}, {DNSName: "debug.host.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"203.0.113.10"}}, {DNSName: "http.host.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"203.0.113.10"}}, {DNSName: "minecraft.host.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"203.0.113.10"}}, @@ -809,6 +809,7 @@ func TestServiceSourceFqdnTemplatingExamples(t *testing.T) { false, false, true, + true, ) require.NoError(t, err) diff --git a/source/service_test.go b/source/service_test.go index 6d9e3a8d8f..bec4309bc2 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -98,6 +98,7 @@ func (suite *ServiceSuite) SetupTest() { false, false, false, + false, ) suite.NoError(err, "should initialize service source") } @@ -181,6 +182,7 @@ func testServiceSourceNewServiceSource(t *testing.T) { false, false, false, + false, ) if ti.expectError { @@ -1165,6 +1167,7 @@ func testServiceSourceEndpoints(t *testing.T) { tc.resolveLoadBalancerHostname, false, false, + false, ) require.NoError(t, err) @@ -1381,6 +1384,7 @@ func testMultipleServicesEndpoints(t *testing.T) { false, false, false, + false, ) require.NoError(t, err) @@ -1686,6 +1690,7 @@ func TestClusterIpServices(t *testing.T) { false, false, false, + false, ) require.NoError(t, err) @@ -1718,6 +1723,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { fqdnTemplate string ignoreHostnameAnnotation bool exposeInternalIPv6 bool + ignoreUnscheduledNodes bool labels map[string]string annotations map[string]string lbs []string @@ -1741,7 +1747,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { annotations.HostnameKey: "foo.example.org.", }, expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, @@ -1816,7 +1822,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, fqdnTemplate: "{{.Name}}.bar.example.com", expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, @@ -1856,7 +1862,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { annotations.HostnameKey: "foo.example.org.", }, expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, }, @@ -1892,7 +1898,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { annotations.HostnameKey: "foo.example.org.", }, expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, @@ -1938,7 +1944,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { annotations.HostnameKey: "foo.example.org.", }, expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, @@ -1987,7 +1993,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { annotations.HostnameKey: "foo.example.org.", }, expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA}, }, nodes: []*v1.Node{{ @@ -2031,7 +2037,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { annotations.HostnameKey: "foo.example.org.", }, expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA}, }, nodes: []*v1.Node{{ @@ -2087,7 +2093,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { annotations.AccessKey: "private", }, expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, }, @@ -2127,7 +2133,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { annotations.AccessKey: "public", }, expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, @@ -2170,7 +2176,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, exposeInternalIPv6: true, expected: []*endpoint.Endpoint{ - {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2", "2001:DB8::3", "2001:DB8::4"}, RecordType: endpoint.RecordTypeAAAA}, }, @@ -2383,6 +2389,55 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }}, }, + { + title: "NodePort services ignore unschedulable node", + ignoreUnscheduledNodes: true, + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + labels: map[string]string{}, + annotations: map[string]string{ + annotations.HostnameKey: "foo.example.org.", + annotations.AccessKey: "public", + }, + expected: []*endpoint.Endpoint{ + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org."}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, + }, + nodes: []*v1.Node{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Spec: v1.NodeSpec{ + Unschedulable: true, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, + {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, + }, + }, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + Spec: v1.NodeSpec{ + Unschedulable: false, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, + {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::3"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::4"}, + }, + }, + }}, + }, } { t.Run(tc.title, func(t *testing.T) { @@ -2463,6 +2518,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { false, false, tc.exposeInternalIPv6, + tc.ignoreUnscheduledNodes, ) require.NoError(t, err) @@ -3371,6 +3427,7 @@ func TestHeadlessServices(t *testing.T) { false, false, tc.exposeInternalIPv6, + false, ) require.NoError(t, err) @@ -3507,6 +3564,7 @@ func TestMultipleServicesPointingToSameLoadBalancer(t *testing.T) { false, false, false, + true, ) require.NoError(t, err) assert.NotNil(t, src) @@ -3873,6 +3931,7 @@ func TestMultipleHeadlessServicesPointingToPodsOnTheSameNode(t *testing.T) { false, false, false, + true, ) require.NoError(t, err) assert.NotNil(t, src) @@ -4331,6 +4390,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { false, false, false, + true, ) require.NoError(t, err) @@ -4541,6 +4601,7 @@ func TestExternalServices(t *testing.T) { false, false, false, + true, ) require.NoError(t, err) @@ -4603,6 +4664,7 @@ func BenchmarkServiceEndpoints(b *testing.B) { false, false, false, + true, ) require.NoError(b, err) @@ -4702,6 +4764,7 @@ func TestNewServiceSourceInformersEnabled(t *testing.T) { false, false, false, + false, ) require.NoError(t, err) svcSrc, ok := svc.(*serviceSource) @@ -4733,6 +4796,7 @@ func TestNewServiceSourceWithServiceTypeFilters_Unsupported(t *testing.T) { false, false, false, + false, ) require.Errorf(t, err, "unsupported service type filter: \"UnknownType\". Supported types are: [\"ClusterIP\" \"NodePort\" \"LoadBalancer\" \"ExternalName\"]") require.Nil(t, svc, "ServiceSource should be nil when an unsupported service type is provided") @@ -4912,6 +4976,7 @@ func TestEndpointSlicesIndexer(t *testing.T) { false, false, false, + true, ) require.NoError(t, err) ss, ok := src.(*serviceSource) @@ -4999,6 +5064,7 @@ func TestPodTransformerInServiceSource(t *testing.T) { false, false, false, + false, ) require.NoError(t, err) ss, ok := src.(*serviceSource) diff --git a/source/skipper_routegroup.go b/source/skipper_routegroup.go index e1a7ee7f4d..83676b31d0 100644 --- a/source/skipper_routegroup.go +++ b/source/skipper_routegroup.go @@ -325,7 +325,7 @@ func (sc *routeGroupSource) endpointsFromTemplate(rg *routeGroup) ([]*endpoint.E return endpoints, nil } -// annotation logic ported from source/ingress.go without Spec.TLS part, because it'S not supported in RouteGroup +// annotation logic ported from source/ingress.go without Spec.TLS part, because it's not supported in RouteGroup func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.Endpoint { endpoints := []*endpoint.Endpoint{} diff --git a/source/store.go b/source/store.go index 103ec94a3d..81f3b135be 100644 --- a/source/store.go +++ b/source/store.go @@ -429,7 +429,12 @@ func buildServiceSource(ctx context.Context, p ClientGenerator, cfg *Config) (So if err != nil { return nil, err } - return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, cfg.ExposeInternalIPv6) + return NewServiceSource(ctx, client, cfg.Namespace, + cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, + cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, + cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, + cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, + cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable) } // buildIngressSource creates an Ingress source for exposing Kubernetes ingresses as DNS records. @@ -522,7 +527,7 @@ func buildGlooProxySource(ctx context.Context, p ClientGenerator, cfg *Config) ( if err != nil { return nil, err } - return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespaces) + return NewGlooSource(ctx, dynamicClient, kubernetesClient, cfg.GlooNamespaces) } func buildTraefikProxySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { diff --git a/source/wrappers/dedupsource.go b/source/wrappers/dedupsource.go index 2d9fc6c367..1fde740988 100644 --- a/source/wrappers/dedupsource.go +++ b/source/wrappers/dedupsource.go @@ -40,7 +40,7 @@ func NewDedupSource(source source.Source) source.Source { func (ms *dedupSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { log.Debug("dedupSource: collecting endpoints and removing duplicates") result := make([]*endpoint.Endpoint, 0) - collected := map[string]bool{} + collected := make(map[string]struct{}) endpoints, err := ms.source.Endpoints(ctx) if err != nil { @@ -63,7 +63,7 @@ func (ms *dedupSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, err continue } - collected[identifier] = true + collected[identifier] = struct{}{} result = append(result, ep) }