Repository: kubernetes-sigs/external-dns

Issue:
ChangeResourceRecordSetsRecordTypes in IAM Permissions with ABAC example is missing TXT type
The example for IAM Permissions with ABAC here: https://github.com/kubernetes-sigs/external-dns/blob/07ae419795280b827d0d5c2fc19e6d5b21b5b124/docs/tutorials/aws.md?plain=1#L63 is missing TXT type, which leads to failure to create TXT records and errors such as (sanitized): 

```
level=error msg="Failure in zone management.stage.inbake.com. when submitting change batch: operation error Route 53: ChangeResourceRecordSets, https response error StatusCode: 403, RequestID: a4d8-4055-9449-4421a5c67f48, api error AccessDenied: User: arn:aws:sts::123456789012:assumed-role/external-dns-role/external-d-49df7ce5-60ec-4942-944d is not authorized to perform: route53:ChangeResourceRecordSets on resource: arn:aws:route53:::hostedzone/XXXXYYYYZZZ because no identity-based policy allows the route53:ChangeResourceRecordSets action" profile=default zoneID=/hostedzone/XXXXYYYYZZZ zoneName=mydomain.com.
```


Hints from issue comments:
@TobyTheHutt: I'll take a shot at this.

Solution patch:
diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md
index d9195b39..4ff5e91a 100644
--- a/.github/ISSUE_TEMPLATE/---bug-report.md
+++ b/.github/ISSUE_TEMPLATE/---bug-report.md
@@ -7,8 +7,10 @@ assignees: ''
 
 ---
 
-<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!
+<!--
+Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!
 
+Make sure to validate the behavior against latest release https://github.com/kubernetes-sigs/external-dns/releases as we don't support past versions.
 -->
 
 **What happened**:
@@ -17,6 +19,10 @@ assignees: ''
 
 **How to reproduce it (as minimally and precisely as possible)**:
 
+<!--
+Please provide as much detail as possible, including Kubernetes manifests with spec.status, ExternalDNS arguments, and logs. A bug that cannot be reproduced won't be fixed.
+-->
+
 **Anything else we need to know?**:
 
 **Environment**:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9edbb7cc..9d8e391a 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -25,16 +25,16 @@ jobs:
     steps:
 
     - name: Check out code into the Go module directory
-      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 
     - name: Set up Go 1.x
-      uses: actions/setup-go@v5
+      uses: actions/setup-go@v6
       with:
         go-version-file: go.mod
         check-latest: true
       id: go
 
-    - name: Install CI
+    - name: Install Dependencies
       run: |
         go get -v -t -d ./...
 
@@ -45,6 +45,9 @@ jobs:
       if: github.actor == 'nektos/act' && matrix.os == 'ubuntu-latest'
 
     - name: Test
+      env:
+        GOMAXPROCS: 1
+        GOMEMLIMIT: 2048MiB
       run: make test
 
     - name: Send coverage
diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml
index 9feb4f8e..3fd71d44 100644
--- a/.github/workflows/codeql-analysis.yaml
+++ b/.github/workflows/codeql-analysis.yaml
@@ -26,9 +26,9 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
     - name: Install go version
-      uses: actions/setup-go@v5
+      uses: actions/setup-go@v6
       with:
         go-version-file: go.mod
 
diff --git a/.github/workflows/dependency-update.yaml b/.github/workflows/dependency-update.yaml
index 6ab818a8..224835f7 100644
--- a/.github/workflows/dependency-update.yaml
+++ b/.github/workflows/dependency-update.yaml
@@ -14,10 +14,10 @@ jobs:
     if: github.repository == 'kubernetes-sigs/external-dns'
     steps:
       - name: checkout
-        uses: actions/checkout@v4.2.2
+        uses: actions/checkout@v5.0.0
       # https://github.com/renovatebot/github-action
       - name: self-hosted renovate
-        uses: renovatebot/github-action@v43.0.3
+        uses: renovatebot/github-action@v43.0.10
         with:
           # https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication
           token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 1f46ff48..2deff321 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -18,11 +18,11 @@ jobs:
     name: Release Docs
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+      - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
         with:
           python-version: "3.12"
           cache: "pip"
diff --git a/.github/workflows/gh-workflow-approve.yaml b/.github/workflows/gh-workflow-approve.yaml
index c63152d1..571a17d8 100644
--- a/.github/workflows/gh-workflow-approve.yaml
+++ b/.github/workflows/gh-workflow-approve.yaml
@@ -17,7 +17,7 @@ jobs:
       actions: write
     steps:
       - name: Update PR
-        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
         continue-on-error: true
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/json-yaml-validate.yml b/.github/workflows/json-yaml-validate.yml
index 6b7550f4..e4c53b9e 100644
--- a/.github/workflows/json-yaml-validate.yml
+++ b/.github/workflows/json-yaml-validate.yml
@@ -14,7 +14,7 @@ jobs:
   json-yaml-validate:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 
       - name: json-yaml-validate
         uses: GrantBirki/json-yaml-validate@v3.3.1
diff --git a/.github/workflows/lint-test-chart.yaml b/.github/workflows/lint-test-chart.yaml
index e257c754..95a34c29 100644
--- a/.github/workflows/lint-test-chart.yaml
+++ b/.github/workflows/lint-test-chart.yaml
@@ -25,12 +25,12 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
         with:
           fetch-depth: 0
 
       - name: Install Helm
-        uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0
+        uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
         with:
           version: latest
 
@@ -178,7 +178,7 @@ jobs:
         run: ah lint --kind helm --path ./charts/external-dns || exit 1
 
       - name: Install Python
-        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
         with:
           token: ${{ github.token }}
           python-version: "3.x"
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index c04b5e89..d0648799 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -18,7 +18,7 @@ jobs:
     steps:
 
     - name: Check out code into the Go module directory
-      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 
     - name: Lint markdown
       uses: nosborn/github-action-markdown-cli@v3.5.0
@@ -27,7 +27,7 @@ jobs:
         config_file: ".markdownlint.json"
 
     - name: Set up Go
-      uses: actions/setup-go@v5
+      uses: actions/setup-go@v6
       with:
         go-version-file: go.mod
 
@@ -48,15 +48,16 @@ jobs:
       with:
         verify: true
         args: --timeout=30m
-        version: v2.1
+        version: v2.2
 
-      # Run Spectral
+      # https://github.com/daveshanley/vacuum
     - name: Lint OpenAPI spec
-      uses: stoplightio/spectral-action@577bade2d6e0eeb50528c94182a5588bf961ae8f # v0.8.12
-      with:
-        file_glob: 'api/*.yaml'
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # Increases rate limit from 60 to 5000 requests
+      run: |
+        go tool vacuum lint -d --fail-severity warn --show-rules api/*.yaml
 
-    - uses: actions/setup-python@v5
+    - uses: actions/setup-python@v6
     # https://github.com/pre-commit/action
     - name: Verify with pre-commit
       uses: pre-commit/action@v3.0.1
diff --git a/.github/workflows/release-chart.yaml b/.github/workflows/release-chart.yaml
index 0527d574..247e8166 100644
--- a/.github/workflows/release-chart.yaml
+++ b/.github/workflows/release-chart.yaml
@@ -25,7 +25,7 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
         with:
           fetch-depth: 0
 
@@ -145,7 +145,7 @@ jobs:
           git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
 
       - name: Install Helm
-        uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0
+        uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
         with:
           version: latest
 
diff --git a/.github/workflows/staging-image-tester.yaml b/.github/workflows/staging-image-tester.yaml
index a6950195..205bbea6 100644
--- a/.github/workflows/staging-image-tester.yaml
+++ b/.github/workflows/staging-image-tester.yaml
@@ -21,10 +21,10 @@ jobs:
     steps:
 
     - name: Check out code into the Go module directory
-      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 
     - name: Set up Go 1.x
-      uses: actions/setup-go@v5
+      uses: actions/setup-go@v6
       with:
         go-version-file: go.mod
       id: go
diff --git a/Makefile b/Makefile
index cd81126a..ee8605fe 100644
--- a/Makefile
+++ b/Makefile
@@ -48,16 +48,17 @@ go-lint: golangci-lint-install
 licensecheck:
 	@echo ">> checking license header"
 	@licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \
-               awk 'NR<=5' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \
-       done); \
-       if [ -n "$${licRes}" ]; then \
-               echo "license header checking failed:"; echo "$${licRes}"; \
-               exit 1; \
-       fi
-
-#? oas-lint: Requires to install spectral. See github.com/stoplightio/spectral
+            awk 'NR<=5' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \
+        done); \
+        if [ -n "$${licRes}" ]; then \
+            echo "license header checking failed:"; echo "$${licRes}"; \
+            exit 1; \
+        fi
+
+#? oas-lint: Execute OpenAPI Specification (OAS) linting https://quobix.com/vacuum/
+.PHONY: go-lint
 oas-lint:
-	spectral lint api/*.yaml
+	go tool vacuum lint -d --fail-severity warn api/*.yaml
 
 #? lint: Run all the linters
 .PHONY: lint
diff --git a/README.md b/README.md
index 45ad8e06..4997156d 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz
 - [Plural](https://www.plural.sh/)
 - [Pi-hole](https://pi-hole.net/)
 - [Alibaba Cloud DNS](https://www.alibabacloud.com/help/en/dns)
+- [Myra Security DNS](https://www.myrasecurity.com/en/saasp/application-security/secure-dns/)
 
 ExternalDNS is, by default, aware of the records it is managing, therefore it can safely manage non-empty hosted zones.
 We strongly encourage you to set `--txt-owner-id` to a unique value that doesn't change for the lifetime of your cluster.
@@ -104,11 +105,13 @@ from the usage of any externally developed webhook.
 | IONOS                 | https://github.com/ionos-cloud/external-dns-ionos-webhook            |
 | Infoblox              | https://github.com/AbsaOSS/external-dns-infoblox-webhook             |
 | Mikrotik              | https://github.com/mirceanton/external-dns-provider-mikrotik         |
+| Myra Security         | https://github.com/Myra-Security-GmbH/external-dns-myrasec-webhook   |
 | Netcup                | https://github.com/mrueg/external-dns-netcup-webhook                 |
 | Netic                 | https://github.com/neticdk/external-dns-tidydns-webhook              |
 | OpenStack Designate   | https://github.com/inovex/external-dns-designate-webhook             |
 | 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              |
 | 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               |
@@ -160,13 +163,19 @@ The following table clarifies the current status of the providers according to t
 
 ## Kubernetes version compatibility
 
-A [breaking change](https://github.com/kubernetes-sigs/external-dns/pull/2281) was added in external-dns v0.10.0.
+Breaking changes were introduced in external-dns in the following versions:
 
-| ExternalDNS                    |      <= 0.9.x      |     >= 0.10.0      |
-| ------------------------------ | :----------------: | :----------------: |
-| Kubernetes <= 1.18             | :white_check_mark: |        :x:         |
-| Kubernetes >= 1.19 and <= 1.21 | :white_check_mark: | :white_check_mark: |
-| Kubernetes >= 1.22             |        :x:         | :white_check_mark: |
+- [`v0.10.0`](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.10.0): use of `networking.k8s.io/ingresses` instead of `extensions/ingresses` (see [#2281](https://github.com/kubernetes-sigs/external-dns/pull/2281))
+- [`v0.18.0`](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.18.0): use of `discovery.k8s.io/endpointslices` instead of `endpoints` (see [#5493](https://github.com/kubernetes-sigs/external-dns/pull/5493))
+- [`v0.19.0`](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.19.0): expose external ipv6 by default (see [#5575](https://github.com/kubernetes-sigs/external-dns/pull/5575) and disable legacy listeners on traefik.containo.us API Group (see [#5565](https://github.com/kubernetes-sigs/external-dns/pull/5565))
+
+| ExternalDNS                  |      ≤ 0.9.x       | ≥ 0.10.x and ≤ 0.17.x |      ≥ 0.18.x      |
+| ---------------------------- | :----------------: | :-------------------: | :----------------: |
+| Kubernetes ≤ 1.18            | :white_check_mark: |          :x:          |        :x:         |
+| Kubernetes 1.19 and 1.20     | :white_check_mark: |  :white_check_mark:   |        :x:         |
+| Kubernetes 1.21              | :white_check_mark: |  :white_check_mark:   | :white_check_mark: |
+| Kubernetes ≥ 1.22 and ≤ 1.32 |        :x:         |  :white_check_mark:   | :white_check_mark: |
+| Kubernetes ≥ 1.33            |        :x:         |          :x:          | :white_check_mark: |
 
 ## Running ExternalDNS
 
@@ -203,6 +212,7 @@ The following tutorials are provided:
 - [IONOS Cloud](docs/tutorials/ionoscloud.md)
 - [Istio Gateway Source](docs/sources/istio.md)
 - [Linode](docs/tutorials/linode.md)
+- [Myra Security](docs/tutorials/myra.md)
 - [NS1](docs/tutorials/ns1.md)
 - [NS Record Creation with CRD Source](docs/sources/ns-record.md)
 - [MX Record Creation with CRD Source](docs/sources/mx-record.md)
diff --git a/charts/external-dns/CHANGELOG.md b/charts/external-dns/CHANGELOG.md
index b0e88965..b05b7752 100644
--- a/charts/external-dns/CHANGELOG.md
+++ b/charts/external-dns/CHANGELOG.md
@@ -18,6 +18,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [UNRELEASED]
 
+### Added
+
+- Add option to configure `annotationFilter` via dedicated chart value. ([#5737](https://github.com/kubernetes-sigs/external-dns/pull/5737)) _@dshatokhin_
+
+### Changed
+
+- Grant `discovery.k8s.io/endpointslices` permission only when using `service` source. ([#5746](https://github.com/kubernetes-sigs/external-dns/pull/5746)) _@vflaux_
+
 ## [v1.18.0] - 2025-07-14
 
 ### Changed
diff --git a/charts/external-dns/README.md b/charts/external-dns/README.md
index 33286d91..bae1d5f2 100644
--- a/charts/external-dns/README.md
+++ b/charts/external-dns/README.md
@@ -94,6 +94,7 @@ If `namespaced` is set to `true`, please ensure that `sources` my only contains
 | Key | Type | Default | Description |
 |-----|------|---------|-------------|
 | affinity | object | `{}` | Affinity settings for `Pod` [scheduling](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/). If an explicit label selector is not provided for pod affinity or pod anti-affinity one will be created from the pod selector labels. |
+| annotationFilter | string | `nil` | Filter resources queried for endpoints by annotation selector. |
 | automountServiceAccountToken | bool | `true` | Set this to `false` to [opt out of API credential automounting](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting) for the `Pod`. |
 | commonLabels | object | `{}` | Labels to add to all chart resources. |
 | deploymentAnnotations | object | `{}` | Annotations to add to the `Deployment`. |
@@ -117,7 +118,7 @@ If `namespaced` is set to `true`, please ensure that `sources` my only contains
 | imagePullSecrets | list | `[]` | Image pull secrets. |
 | initContainers | list | `[]` | [Init containers](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) to add to the `Pod` definition. |
 | interval | string | `"1m"` | Interval for DNS updates. |
-| labelFilter | string | `nil` | Filter resources queried for endpoints by label selector |
+| labelFilter | string | `nil` | Filter resources queried for endpoints by label selector. |
 | livenessProbe | object | See _values.yaml_ | [Liveness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) configuration for the `external-dns` container. |
 | logFormat | string | `"text"` | Log format. |
 | logLevel | string | `"info"` | Log level. |
diff --git a/charts/external-dns/templates/clusterrole.yaml b/charts/external-dns/templates/clusterrole.yaml
index f418416e..52c525ea 100644
--- a/charts/external-dns/templates/clusterrole.yaml
+++ b/charts/external-dns/templates/clusterrole.yaml
@@ -20,6 +20,8 @@ rules:
   - apiGroups: [""]
     resources: ["services"]
     verbs: ["get","watch","list"]
+{{- end }}
+{{- if has "service" .Values.sources }}
   - apiGroups: ["discovery.k8s.io"]
     resources: ["endpointslices"]
     verbs: ["get","watch","list"]
diff --git a/charts/external-dns/templates/deployment.yaml b/charts/external-dns/templates/deployment.yaml
index 7db11837..70f231fb 100644
--- a/charts/external-dns/templates/deployment.yaml
+++ b/charts/external-dns/templates/deployment.yaml
@@ -123,6 +123,9 @@ spec:
             {{- if .Values.labelFilter }}
             - --label-filter={{ .Values.labelFilter }}
             {{- end }}
+            {{- if .Values.annotationFilter }}
+            - --annotation-filter={{ .Values.annotationFilter }}
+            {{- end }}
             {{- range .Values.managedRecordTypes }}
             - --managed-record-types={{ . }}
             {{- end }}
diff --git a/charts/external-dns/values.schema.json b/charts/external-dns/values.schema.json
index 75e7fdd4..22bfef2a 100644
--- a/charts/external-dns/values.schema.json
+++ b/charts/external-dns/values.schema.json
@@ -6,6 +6,13 @@
       "description": "Affinity settings for `Pod` [scheduling](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/). If an explicit label selector is not provided for pod affinity or pod anti-affinity one will be created from the pod selector labels.",
       "type": "object"
     },
+    "annotationFilter": {
+      "description": "Filter resources queried for endpoints by annotation selector.",
+      "type": [
+        "string",
+        "null"
+      ]
+    },
     "automountServiceAccountToken": {
       "description": "Set this to `false` to [opt out of API credential automounting](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting) for the `Pod`.",
       "type": "boolean"
@@ -23,9 +30,8 @@
       "type": "object",
       "properties": {
         "type": {
-          "type": [
-            "string"
-          ],
+          "default": "Recreate",
+          "type": "string",
           "enum": [
             "Recreate",
             "RollingUpdate"
@@ -98,6 +104,13 @@
         "null"
       ]
     },
+    "gatewayNamespace": {
+      "description": "_Gateway API_ gateway namespace to watch.",
+      "type": [
+        "string",
+        "null"
+      ]
+    },
     "global": {
       "type": "object",
       "properties": {
@@ -151,7 +164,7 @@
       "type": "string"
     },
     "labelFilter": {
-      "description": "Filter resources queried for endpoints by label selector",
+      "description": "Filter resources queried for endpoints by label selector.",
       "type": [
         "string",
         "null"
@@ -192,9 +205,7 @@
     "logFormat": {
       "description": "Log format.",
       "default": "text",
-      "type": [
-        "string"
-      ],
+      "type": "string",
       "enum": [
         "text",
         "json"
@@ -203,9 +214,7 @@
     "logLevel": {
       "description": "Log level.",
       "default": "info",
-      "type": [
-        "string"
-      ],
+      "type": "string",
       "enum": [
         "panic",
         "debug",
@@ -272,9 +281,7 @@
     "policy": {
       "description": "How DNS records are synchronized between sources and providers; available values are `create-only`, `sync`, \u0026 `upsert-only`.",
       "default": "upsert-only",
-      "type": [
-        "string"
-      ],
+      "type": "string",
       "enum": [
         "create-only",
         "sync",
@@ -367,6 +374,7 @@
                       ]
                     },
                     "port": {
+                      "default": "string",
                       "type": [
                         "integer",
                         "string"
@@ -420,6 +428,7 @@
                       ]
                     },
                     "port": {
+                      "default": "string",
                       "type": [
                         "integer",
                         "string"
diff --git a/charts/external-dns/values.yaml b/charts/external-dns/values.yaml
index dc5ec8d1..9b9292c5 100644
--- a/charts/external-dns/values.yaml
+++ b/charts/external-dns/values.yaml
@@ -12,7 +12,7 @@ image:  # @schema additionalProperties: false
   # -- Image tag for the `external-dns` container, this will default to `.Chart.AppVersion` if not set.
   tag:  # @schema type:[string, null]
   # -- Image pull policy for the `external-dns` container.
-  pullPolicy: IfNotPresent  # @schema enum:[IfNotPresent, Always];
+  pullPolicy: IfNotPresent  # @schema enum:[IfNotPresent, Always]
 
 # -- Image pull secrets.
 imagePullSecrets: []  # @schema item: object
@@ -44,11 +44,11 @@ service:
   # -- Service HTTP port.
   port: 7979  # @schema minimum:0; default:7979
   # -- Service IP families (e.g. IPv4 and/or IPv6).
-  ipFamilies: []  # @schema type: [array, null]; item: string; itemEnum: ["IPv4", "IPv6"]; minItems:0; maxItems:2; uniqueItems: true;
+  ipFamilies: []  # @schema type: [array, null]; item: string; itemEnum: ["IPv4", "IPv6"]; minItems:0; maxItems:2; uniqueItems: true
   #  - IPv4
   #  - IPv6
   # -- Service IP family policy.
-  ipFamilyPolicy:  # @schema type: [string, null];  enum:[SingleStack, PreferDualStack, RequireDualStack, null];
+  ipFamilyPolicy:  # @schema type: [string, null];  enum:[SingleStack, PreferDualStack, RequireDualStack, null]
 
 rbac:  # @schema additionalProperties: true
   # -- If `true`, create a `ClusterRole` & `ClusterRoleBinding` with access to the Kubernetes API.
@@ -234,13 +234,16 @@ domainFilters: []
 # -- Intentionally exclude domains from being managed.
 excludeDomains: []
 
-# -- Filter resources queried for endpoints by label selector
+# -- Filter resources queried for endpoints by label selector.
 labelFilter:  # @schema type: [string,null]; default: null
 
+# -- Filter resources queried for endpoints by annotation selector.
+annotationFilter:  # @schema type: [string,null]; default: null
+
 # -- Record types to manage (default: A, AAAA, CNAME)
-managedRecordTypes: []  # @schema type: [array, null]; item: string; uniqueItems: true;
+managedRecordTypes: []  # @schema type: [array, null]; item: string; uniqueItems: true
 
-provider:  # @schema type: [object, string];
+provider:  # @schema type: [object, string]
   # -- _ExternalDNS_ provider name; for the available providers and how to configure them see [README](https://github.com/kubernetes-sigs/external-dns/blob/master/charts/external-dns/README.md#providers).
   name: aws
   webhook:
@@ -300,7 +303,7 @@ provider:  # @schema type: [object, string];
 
 # -- Extra arguments to provide to _ExternalDNS_.
 # An array or map can be used, with maps allowing for value overrides; maps also support slice values to use the same arg multiple times.
-extraArgs: {}  # @schema type: [array, null, object]; item: string; uniqueItems: true;
+extraArgs: {}  # @schema type: [array, null, object]; item: string; uniqueItems: true
 
 secretConfiguration:
   # -- If `true`, create a `Secret` to store sensitive provider configuration (**DEPRECATED**).
diff --git a/controller/controller.go b/controller/controller.go
index 220da28c..bdbddfb6 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -27,6 +27,7 @@ import (
 	log "github.com/sirupsen/logrus"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/pkg/events"
 	"sigs.k8s.io/external-dns/pkg/metrics"
 	"sigs.k8s.io/external-dns/plan"
 	"sigs.k8s.io/external-dns/provider"
@@ -173,7 +174,8 @@ type Controller struct {
 	// The runAtMutex is for atomic updating of nextRunAt and lastRunAt
 	runAtMutex sync.Mutex
 	// The lastRunAt used for throttling and batching reconciliation
-	lastRunAt time.Time
+	lastRunAt    time.Time
+	EventEmitter events.EventEmitter
 	// MangedRecordTypes are DNS record types that will be considered for management.
 	ManagedRecordTypes []string
 	// ExcludeRecordTypes are DNS record types that will be excluded from management.
@@ -244,6 +246,8 @@ func (c *Controller) RunOnce(ctx context.Context) error {
 			registryErrorsTotal.Counter.Inc()
 			deprecatedRegistryErrors.Counter.Inc()
 			return err
+		} else {
+			emitChangeEvent(c.EventEmitter, *plan.Changes, events.RecordReady)
 		}
 	} else {
 		controllerNoChangesTotal.Counter.Inc()
diff --git a/controller/events.go b/controller/events.go
new file mode 100644
index 00000000..79b43c76
--- /dev/null
+++ b/controller/events.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+	"sigs.k8s.io/external-dns/pkg/events"
+	"sigs.k8s.io/external-dns/plan"
+)
+
+// This function emits events for each change in the provided plan.Changes object using the given EventEmitter.
+// It handles create, update, and delete changes, assigning appropriate actions and reasons to each event.
+// If the emitter is nil, it does nothing.
+func emitChangeEvent(e events.EventEmitter, ch plan.Changes, reason events.Reason) {
+	if e == nil {
+		return
+	}
+	for _, change := range ch.Create {
+		e.Add(events.NewEvent(change.RefObject(), change.Describe(), events.ActionCreate, reason))
+	}
+	for _, change := range ch.UpdateNew {
+		e.Add(events.NewEvent(change.RefObject(), change.Describe(), events.ActionUpdate, reason))
+	}
+	for _, change := range ch.Delete {
+		e.Add(events.NewEvent(change.RefObject(), change.Describe(), events.ActionDelete, events.RecordDeleted))
+	}
+}
diff --git a/controller/execute.go b/controller/execute.go
index a69a1a7a..47940c87 100644
--- a/controller/execute.go
+++ b/controller/execute.go
@@ -33,11 +33,10 @@ import (
 	log "github.com/sirupsen/logrus"
 	"k8s.io/klog/v2"
 
-	"sigs.k8s.io/external-dns/source/wrappers"
-
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
 	"sigs.k8s.io/external-dns/pkg/apis/externaldns/validation"
+	"sigs.k8s.io/external-dns/pkg/events"
 	"sigs.k8s.io/external-dns/pkg/metrics"
 	"sigs.k8s.io/external-dns/plan"
 	"sigs.k8s.io/external-dns/provider"
@@ -70,6 +69,7 @@ import (
 	webhookapi "sigs.k8s.io/external-dns/provider/webhook/api"
 	"sigs.k8s.io/external-dns/registry"
 	"sigs.k8s.io/external-dns/source"
+	"sigs.k8s.io/external-dns/source/wrappers"
 )
 
 func Execute() {
@@ -119,7 +119,7 @@ func Execute() {
 		os.Exit(0)
 	}
 
-	ctrl, err := buildController(cfg, endpointsSource, prvdr, domainFilter)
+	ctrl, err := buildController(ctx, cfg, endpointsSource, prvdr, domainFilter)
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -344,7 +344,13 @@ func buildProvider(
 	return p, err
 }
 
-func buildController(cfg *externaldns.Config, src source.Source, p provider.Provider, filter *endpoint.DomainFilter) (*Controller, error) {
+func buildController(
+	ctx context.Context,
+	cfg *externaldns.Config,
+	src source.Source,
+	p provider.Provider,
+	filter *endpoint.DomainFilter,
+) (*Controller, error) {
 	policy, ok := plan.Policies[cfg.Policy]
 	if !ok {
 		return nil, fmt.Errorf("unknown policy: %s", cfg.Policy)
@@ -353,6 +359,20 @@ func buildController(cfg *externaldns.Config, src source.Source, p provider.Prov
 	if err != nil {
 		return nil, err
 	}
+	eventsCfg := events.NewConfig(
+		events.WithKubeConfig(cfg.KubeConfig, cfg.APIServerURL, cfg.RequestTimeout),
+		events.WithEmitEvents(cfg.EmitEvents),
+		events.WithDryRun(cfg.DryRun))
+	var eventEmitter events.EventEmitter
+	if eventsCfg.IsEnabled() {
+		eventCtrl, err := events.NewEventController(eventsCfg)
+		if err != nil {
+			log.Fatal(err)
+		}
+		eventCtrl.Run(ctx)
+		eventEmitter = eventCtrl
+	}
+
 	return &Controller{
 		Source:               src,
 		Registry:             reg,
@@ -362,6 +382,7 @@ func buildController(cfg *externaldns.Config, src source.Source, p provider.Prov
 		ManagedRecordTypes:   cfg.ManagedDNSRecordTypes,
 		ExcludeRecordTypes:   cfg.ExcludeDNSRecordTypes,
 		MinEventSyncInterval: cfg.MinEventSyncInterval,
+		EventEmitter:         eventEmitter,
 	}, nil
 }
 
@@ -426,10 +447,15 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e
 	}
 	// Combine multiple sources into a single, deduplicated source.
 	combinedSource := wrappers.NewDedupSource(wrappers.NewMultiSource(sources, sourceCfg.DefaultTargets, sourceCfg.ForceDefaultTargets))
+	cfg.AddSourceWrapper("dedup")
+	combinedSource = wrappers.NewNAT64Source(combinedSource, cfg.NAT64Networks)
+	cfg.AddSourceWrapper("nat64")
 	// Filter targets
 	targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets)
-	combinedSource = wrappers.NewNAT64Source(combinedSource, cfg.NAT64Networks)
-	combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter)
+	if targetFilter.IsEnabled() {
+		combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter)
+		cfg.AddSourceWrapper("target-filter")
+	}
 	return combinedSource, nil
 }
 
diff --git a/docs/advanced/events.md b/docs/advanced/events.md
new file mode 100644
index 00000000..1d08a8bc
--- /dev/null
+++ b/docs/advanced/events.md
@@ -0,0 +1,140 @@
+# Kubernetes Events in External-DNS
+
+External-DNS manages DNS records dynamically based on Kubernetes resources like Services and Ingresses.
+Emitting Kubernetes Events provides a lightweight observable way for users and systems to understand what External-DNS is doing, especially in production environments where DNS correctness is essential.
+
+> Note; events is currently alpha feature. Functionality is limited and subject to change
+
+## ✨ Why Events Matter
+
+Kubernetes Events enable External-DNS to provide real-time feedback to users and controllers, complementing logs with a simpler way to track DNS changes. This enhances debugging, monitoring, and automation around DNS operations.
+
+### Use Cases of Emitting Events
+
+| Use Case                                          | Description                                                                                                  |
+|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
+| **DNS Record Visibility**                         | Events show what DNS records were created, updated, or deleted (e.g., `Created A record "api.example.com"`). |
+| **Developer Feedback**                            | Users deploying Ingresses or Services can see if External-DNS processed their resource.                      |
+| **Surface Errors, Debugging and Troubleshooting** | Easily identify resource misannotations, sync failures, or IAM permission issues.                            |
+| **Error Reporting**                               | Emit warning events when record sync fails due to provider issues, duplicate records, or misconfigurations.  |
+| **Integration with Alerting/Automation/Auditing** | This enables automated remediation or notifications when DNS sync fails or changes unexpectedly.             |
+| **Observability**                                 | Trace why a DNS record wasn’t created.                                                                       |
+| **Alerting/automation**                           | Trigger actions based on failed events.                                                                      |
+| **Operator and Developer feedback**               | It removes the black-box feeling of "I deployed an Ingress, but why doesn’t the DNS work?"                   |
+
+## Consuming Events
+
+You can observe External-DNS events using:
+
+```sh
+kubectl describe service <name>
+kubectl get events --field-selector involvedObject.kind=Service
+kubectl get events --field-selector type=Normal|Warning
+kubectl get events --field-selector reason=RecordReady|RecordDeleted|RecordError
+```
+
+Or integrate with tools like:
+
+- Prometheus (via event exporters)
+- Loki/Fluentd for log/event aggregation
+- Argo CD / Flux for GitOps monitoring
+
+### Practices for Understanding Events
+
+- **Action field**: Events include a short label describing the `Action`, such as `Created`, `Updated`, `Deleted`, or `FailedSync`
+- **Reason field**: Events include a short label `Reason` is why the action was taken, such as `RecordReady`, `RecordDeleted`, or `RecordError`.
+- **Type field**:
+  - `Normal` means the operation succeeded (e.g., a DNS record was created).
+  - `Warning`  indicates a problem (e.g., DNS sync failed due to configuration or provider issues).
+- **Linked** resource: Events are attached to the relevant Kubernetes resource (like an `Ingress` or `Service`), so you can view them with tools like `kubectl describe`.
+- **Event noise**: If you see repeated identical events, it may indicate a misconfiguration or an issue worth investigating.
+
+### Sequence Overview: External-DNS Endpoint Reconciliation and Event Emission
+
+The following sequence diagram illustrates the core workflow of how External-DNS processes endpoints, applies DNS changes, and emits Kubernetes events:
+
+1. **Endpoint Collection**
+   The `Source` component generates `Endpoint` objects, each linked to a `ReferenceObject` (such as a Service or Ingress).
+
+2. **Plan Construction**
+   A `Plan` aggregates multiple `Endpoints` and prepares a list of desired DNS changes.
+
+3. **Change Application**
+   The `Plan` sends the changes to a DNS `Provider`, which attempts to apply them. Each `Endpoint` is labeled with the result: `Success`, `Failed`, or `Skip`.
+
+4. **Event Emission**
+   Based on the result, an `Event` is created for each `Endpoint`, referencing the original `ReferenceObject`. These events are then emitted via the `EventEmitter`.
+
+This sequence ensures DNS records are managed declaratively and provides real-time visibility into the system’s behavior through Kubernetes Events.
+
+```mermaid
+sequenceDiagram
+  participant Source
+  participant ReferenceObject
+  participant Endpoint
+  participant Plan
+  participant Event
+  participant Provider
+  participant EventEmitter
+
+
+  loop Process each Endpoint
+    Source->>Endpoint: Add Endpoint with Reference
+  end
+
+  Endpoint-->>ReferenceObject: Contains
+  Plan-->>Endpoint: Contains multiple
+
+  loop Apply Changes
+    Plan->>Provider: Apply Endpoint Changes
+    Provider-->>Endpoint: Label with Skip/Success/Failed
+    Provider-->>Plan: Return Result
+  end
+
+
+  loop Process each Event
+    Provider->>Plan: Label with Skip/Success/Failed
+    Plan-->>Event: Construct Event
+    Event-->>ReferenceObject: Contains
+    Event->>EventEmitter: Emit
+  end
+```
+
+### Caveats
+
+- Events are ephemeral (default retention is ~1 hour).
+- They are best-effort and not guaranteed to be delivered or stored long-term.
+- Not a substitute for logging or metrics, but complementary.
+
+## Supported Sources
+
+Events are emitted for all sources that External-DNS supports. The following table lists the sources and whether they currently emit events.
+If a source does not emit events, it may in the future.
+
+| Source                 | Supported |
+|:-----------------------|:---------:|
+| `ambassador-host`      |           |
+| `cloudfoundry`         |           |
+| `connector`            |           |
+| `contour-httpproxy`    |           |
+| `crd`                  |           |
+| `empty`                |           |
+| `f5-transportserver`   |           |
+| `f5-virtualserver`     |           |
+| `fake`                 |     ✅    |
+| `gateway-grpcroute`    |           |
+| `gateway-httproute`    |           |
+| `gateway-tcproute`     |           |
+| `gateway-tlsroute`     |           |
+| `gateway-udproute`     |           |
+| `gloo-proxy`           |           |
+| `ingress`              |           |
+| `istio-gateway`        |           |
+| `istio-virtualservice` |           |
+| `kong-tcpingress`      |           |
+| `node`                 |           |
+| `openshift-route`      |           |
+| `pod`                  |           |
+| `service`              |           |
+| `skipper-routegroup`   |           |
+| `traefik-proxy`        |           |
diff --git a/docs/annotations/annotations.md b/docs/annotations/annotations.md
index bc3cffc1..76af0a9c 100644
--- a/docs/annotations/annotations.md
+++ b/docs/annotations/annotations.md
@@ -4,25 +4,25 @@ ExternalDNS sources support a number of annotations on the Kubernetes resources
 
 The following table documents which sources support which annotations:
 
-| Source       | controller | hostname | internal-hostname | target  | ttl     | (provider-specific) |
-|--------------|------------|----------|-------------------|---------|---------|---------------------|
-| Ambassador   |            |          |                   | Yes     | Yes     | Yes                 |
-| Connector    |            |          |                   |         |         |                     |
-| Contour      | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |
-| CloudFoundry |            |          |                   |         |         |                     |
-| CRD          |            |          |                   |         |         |                     |
-| F5           |            |          |                   | Yes     | Yes     |                     |
-| Gateway      | Yes        | Yes[^1]  |                   | Yes[^4] | Yes     | Yes                 |
-| Gloo         |            |          |                   | Yes     | Yes[^5] | Yes[^5]             |
-| Ingress      | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |
-| Istio        | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |
-| Kong         |            | Yes[^1]  |                   | Yes     | Yes     | Yes                 |
-| Node         | Yes        |          |                   | Yes     | Yes     |                     |
-| OpenShift    | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |
-| Pod          |            | Yes      | Yes               | Yes     |         |                     |
-| Service      | Yes        | Yes[^1]  | Yes[^1][^2]       | Yes[^3] | Yes     | Yes                 |
-| Skipper      | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |
-| Traefik      |            | Yes[^1]  |                   | Yes     | Yes     | Yes                 |
+| Source       | controller | hostname | internal-hostname | target  | ttl     | (provider-specific) | ingress |
+|--------------|------------|----------|-------------------|---------|---------|---------------------|:-------:|
+| Ambassador   |            |          |                   | Yes     | Yes     | Yes                 |   No    |
+| Connector    |            |          |                   |         |         |                     |   No    |
+| Contour      | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |   No    |
+| CloudFoundry |            |          |                   |         |         |                     |   No    |
+| CRD          |            |          |                   |         |         |                     |   No    |
+| F5           |            |          |                   | Yes     | Yes     |                     |   No    |
+| Gateway      | Yes        | Yes[^1]  |                   | Yes[^4] | Yes     | Yes                 |   No    |
+| Gloo         |            |          |                   | Yes     | Yes[^5] | Yes[^5]             |   No    |
+| Ingress      | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |   No    |
+| Istio        | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |   Yes   |
+| Kong         |            | Yes[^1]  |                   | Yes     | Yes     | Yes                 |   No    |
+| Node         | Yes        |          |                   | Yes     | Yes     |                     |   No    |
+| OpenShift    | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |   No    |
+| Pod          |            | Yes      | Yes               | Yes     |         |                     |   No    |
+| Service      | Yes        | Yes[^1]  | Yes[^1][^2]       | Yes[^3] | Yes     | Yes                 |   No    |
+| Skipper      | Yes        | Yes[^1]  |                   | Yes     | Yes     | Yes                 |   No    |
+| Traefik      |            | Yes[^1]  |                   | Yes     | Yes     | Yes                 |   No    |
 
 [^1]: Unless the `--ignore-hostname-annotation` flag is specified.
 [^2]: Only behaves differently than `hostname` for `Service`s of type `ClusterIP` or `LoadBalancer`.
@@ -143,6 +143,17 @@ If the value is `annotation-only`, use only the domains from the `Ingress` annot
 
 If the annotation is not present, use the domains from both the spec and annotations.
 
+## external-dns.alpha.kubernetes.io/ingress
+
+This annotation allows ExternalDNS to work with Istio 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:
+
+- **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.
+
+- **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.
+
 ## external-dns.alpha.kubernetes.io/internal-hostname
 
 Specifies the domain for the resource's DNS records that are for use from internal networks.
diff --git a/docs/contributing/source-wrappers.md b/docs/contributing/source-wrappers.md
new file mode 100644
index 00000000..dd9583ea
--- /dev/null
+++ b/docs/contributing/source-wrappers.md
@@ -0,0 +1,149 @@
+# 🧩 Source Wrappers/Middleware
+
+## Overview
+
+In ExternalDNS, a **Source** is a component responsible for discovering DNS records from Kubernetes resources (e.g., `Ingress`, `Service`, `Gateway`, etc.).
+
+**Source Wrappers** are middleware-like components that sit between the source and the plan generation. They extend or modify the behavior of the original sources by transforming, filtering, or enriching the DNS records before they're processed by the planner and provider.
+
+---
+
+## Why Wrappers?
+
+Wrappers solve these key challenges:
+
+- ✂️ **Filtering**: Remove unwanted targets or records from sources based on labels, annotations, targets and etc.
+- 🔗 **Aggregation**: Combine Endpoints from multiple underlying sources. For example, from both Kubernetes Services and Ingresses.
+- 🧹 **Deduplication**: Prevent duplicate DNS records across sources.
+- 🌐 **Target transformation**: Rewrite targets for IPv6 networks or alter endpoint attributes like FQDNS or targets.
+- 🧪 **Testing and simulation**: Use the `FakeSource` or wrappers for dry-runs or simulations.
+- 🔁 **Composability**: Chain multiple behaviors without modifying core sources.
+- 🔐 **Access Control**: Limits endpoint exposure based on policies or user access.
+- 📊 **Observability**: Adds logging, debugging, or metrics around source behavior.
+
+---
+
+## Built In Wrappers
+
+|       Wrapper        | Purpose                                 | Use Case                              |
+|:--------------------:|:----------------------------------------|:--------------------------------------|
+|    `MultiSource`     | Combine multiple sources.               | Aggregate `Ingress`, `Service`, etc.  |
+|    `DedupSource`     | Remove duplicate DNS records.           | Avoid duplicate records from sources. |
+| `TargetFilterSource` | Include/exclude targets based on CIDRs. | Exclude internal IPs.                 |
+|    `NAT64Source`     | Add NAT64-prefixed AAAA records.        | Support IPv6 with NAT64.              |
+
+### Use Cases
+
+### 1.1 `TargetFilterSource`
+
+Filters targets (e.g. IPs or hostnames) based on inclusion or exclusion rules.
+
+📌 **Use case**: Only publish public IPs, exclude test environments.
+
+```yaml
+--target-net-filter=192.168.0.0/16
+--exclude-target-nets=10.0.0.0/8
+```
+
+### 2.1 `NAT64Source`
+
+Converts IPv4 targets to IPv6 using NAT64 prefixes.
+
+📌 **Use case**: Publish AAAA records for IPv6-only clients in NAT64 environments.
+
+```yaml
+--nat64-prefix=64:ff9b::/96
+```
+
+---
+
+## How Wrappers Work
+
+Wrappers wrap a `Source` and implement the same `Source` interface (e.g., `Endpoints(ctx)`).
+
+They typically follow this pattern:
+
+```go
+package wrappers
+
+type myWrapper struct {
+	next source.Source
+}
+
+func (m *myWrapper) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
+	eps, err := m.next.Endpoints(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	// Modify, filter, or enrich endpoints as needed
+	return eps, nil
+}
+
+// AddEventHandler must be implemented to satisfy the source.Source interface.
+func (m *myWrapper) AddEventHandler(ctx context.Context, handler func()) {
+	log.Debugf("myWrapper: adding event handler")
+	m.next.AddEventHandler(ctx, handler)
+}
+```
+
+This allows wrappers to be stacked or composed together.
+
+---
+
+### Composition of Wrappers
+
+Wrappers are often composed like this:
+
+```go
+source := NewMultiSource(actualSources, defaultTargets)
+source = NewDedupSource(source)
+source = NewNAT64Source(source, cfg.NAT64Networks)
+source = NewTargetFilterSource(source, targetFilter)
+```
+
+Each wrapper processes the output of the previous one.
+
+---
+
+## High Level Design
+
+- Source: Implements the base logic for extracting DNS endpoints (e.g. IngressSource, ServiceSource, etc.)
+- Wrappers: Decorate the source (e.g. DedupSource, TargetFilterSource) to enhance or filter endpoint data
+- Plan: Compares the endpoints from Source with DNS state from Provider and produces create/update/delete changes
+- Provider: Applies changes to actual DNS services (e.g. Route53, Cloudflare, Azure DNS)
+
+```mermaid
+sequenceDiagram
+    participant ExternalDNS
+    participant Source
+    participant Wrapper
+    participant DedupWrapper as DedupSource
+    participant Provider
+    participant Plan
+
+    ExternalDNS->>Source: Initialize source (e.g. Ingress, Service)
+    Source-->>ExternalDNS: Implements Source interface
+
+    ExternalDNS->>Wrapper: Wrap with decorators (e.g. dedup, filters)
+    Wrapper->>DedupWrapper: Compose with DedupSource
+    DedupWrapper-->>Wrapper: Return enriched Source
+
+    Wrapper-->>ExternalDNS: Return final wrapped Source
+
+    ExternalDNS->>Plan: Generate plan from Source
+    Plan->>Wrapper: Call Endpoints(ctx)
+    Wrapper->>DedupWrapper: Call Endpoints(ctx)
+    DedupWrapper->>Source: Call Endpoints(ctx)
+    Source-->>DedupWrapper: Return []*Endpoint
+    DedupWrapper-->>Wrapper: Return de-duplicated []*Endpoint
+    Wrapper-->>Plan: Return transformed []*Endpoint
+
+    ExternalDNS->>Provider: ApplyChanges(plan)
+    Provider-->>ExternalDNS: Sync DNS records
+```
+
+## Learn More
+
+- [Source Interface](https://github.com/kubernetes-sigs/external-dns/blob/master/source/source.go)
+- [Wrappers Source Code](https://github.com/kubernetes-sigs/external-dns/tree/master/source/wrappers)
diff --git a/docs/faq.md b/docs/faq.md
index 5b5a1fc9..090938ca 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -193,7 +193,7 @@ $ docker run \
   -e EXTERNAL_DNS_SOURCE=$'service\ningress' \
   -e EXTERNAL_DNS_PROVIDER=google \
   -e EXTERNAL_DNS_DOMAIN_FILTER=$'foo.com\nbar.com' \
-  registry.k8s.io/external-dns/external-dns:v0.18.0
+  registry.k8s.io/external-dns/external-dns:v0.19.0
 time="2017-08-08T14:10:26Z" level=info msg="config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ...
 ```
 
diff --git a/docs/flags.md b/docs/flags.md
index 68f6307c..ba1e39c1 100644
--- a/docs/flags.md
+++ b/docs/flags.md
@@ -52,6 +52,7 @@
 | `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) |
 | `--[no-]traefik-enable-legacy` | Enable legacy listeners on Resources under the traefik.containo.us API Group |
 | `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group |
+| `--events-emit=EVENTS-EMIT` | Events that should be emitted. Specify multiple times for multiple events support (optional, default: none, expected: RecordReady, RecordDeleted, RecordError) |
 | `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, webhook) |
 | `--provider-cache-time=0s` | The time to cache the DNS provider record list requests. |
 | `--domain-filter=` | Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional) |
@@ -66,7 +67,7 @@
 | `--google-zone-visibility=` | When using the Google provider, filter for zones with this visibility (optional, options: public, private) |
 | `--alibaba-cloud-config-file="/etc/kubernetes/alibaba-cloud.json"` | When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud) |
 | `--alibaba-cloud-zone-type=` | When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private) |
-| `--aws-zone-type=` | When using the AWS provider, filter for zones of this type (optional, options: public, private) |
+| `--aws-zone-type=` | When using the AWS provider, filter for zones of this type (optional, default: any, options: public, private) |
 | `--aws-zone-tags=` | When using the AWS provider, filter for zones with these tags |
 | `--aws-profile=` | When using the AWS provider, name of the profile to use |
 | `--aws-assume-role=""` | When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional) |
diff --git a/docs/monitoring/metrics.md b/docs/monitoring/metrics.md
index 3da6d52a..2d317ea6 100644
--- a/docs/monitoring/metrics.md
+++ b/docs/monitoring/metrics.md
@@ -26,6 +26,7 @@ curl https://localhost:7979/metrics
 | last_sync_timestamp_seconds | Gauge | controller | Timestamp of last successful sync with the DNS provider |
 | no_op_runs_total | Counter | controller | Number of reconcile loops ending up with no changes on the DNS provider side. |
 | verified_records | Gauge | controller | Number of DNS records that exists both in source and registry (vector). |
+| request_duration_seconds | Summaryvec | http | The HTTP request latencies in seconds. |
 | cache_apply_changes_calls | Counter | provider | Number of calls to the provider cache ApplyChanges. |
 | cache_records_calls | Counter | provider | Number of calls to the provider cache Records list. |
 | endpoints_total | Gauge | registry | Number of Endpoints in the registry |
@@ -76,7 +77,6 @@ curl https://localhost:7979/metrics
 | go_memstats_sys_bytes |
 | go_sched_gomaxprocs_threads |
 | go_threads |
-| http_request_duration_seconds |
 | process_cpu_seconds_total |
 | process_max_fds |
 | process_network_receive_bytes_total |
diff --git a/docs/providers.md b/docs/providers.md
index 22862293..0aafa2fd 100644
--- a/docs/providers.md
+++ b/docs/providers.md
@@ -20,6 +20,7 @@ Provider supported configurations
 | Google GCP    | n/a        | yes     | 300                   |
 | InMemory      | n/a        | n/a     | n/a                   |
 | Linode        | n/a        | n/a     | n/a                   |
+| Myra Security | n/a        | yes     | 300                   |
 | NS1           | n/a        | yes     | 10                    |
 | OCI           | yes        | yes     | 300                   |
 | OVH           | n/a        | yes     | 0                     |
diff --git a/docs/registry/dynamodb.md b/docs/registry/dynamodb.md
index 15aa97cd..a7a30d79 100644
--- a/docs/registry/dynamodb.md
+++ b/docs/registry/dynamodb.md
@@ -81,7 +81,7 @@ spec:
     spec:
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.18.0
+          image: registry.k8s.io/external-dns/external-dns:v0.19.0
           args:
             - --source=service
             - --source=ingress
diff --git a/docs/registry/txt.md b/docs/registry/txt.md
index e3ae66d6..d538eb28 100644
--- a/docs/registry/txt.md
+++ b/docs/registry/txt.md
@@ -118,19 +118,19 @@ Note that the key used for encryption should be a secure key and properly manage
 Python
 
 ```python
-python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())'
+python -c 'import os,base64; print(base64.standard_b64encode(os.urandom(32)).decode())'
 ```
 
 Bash
 
 ```shell
-dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d -- '\n' | tr -- '+/' '-_'; echo
+dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64; echo
 ```
 
 OpenSSL
 
 ```shell
-openssl rand -base64 32 | tr -- '+/' '-_'
+openssl rand -base64 32
 ```
 
 PowerShell
@@ -138,7 +138,7 @@ PowerShell
 ```powershell
 # Add System.Web assembly to session, just in case
 Add-Type -AssemblyName System.Web
-[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Web.Security.Membership]::GeneratePassword(32,4))).Replace("+","-").Replace("/","_")
+[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Web.Security.Membership]::GeneratePassword(32,4)))
 ```
 
 Terraform
@@ -146,7 +146,6 @@ Terraform
 ```hcl
 resource "random_password" "txt_key" {
   length           = 32
-  override_special = "-_"
 }
 ```
 
diff --git a/docs/release.md b/docs/release.md
index b65d956f..58b05b2e 100644
--- a/docs/release.md
+++ b/docs/release.md
@@ -14,7 +14,7 @@ A new staging image is released weekly and can be found at [gcr.io/k8s-staging-e
 Example command to fetch `10` most recent staging images:
 
 ```sh
-export EXT_DNS_VERSION="v0.18.0"
+export EXT_DNS_VERSION="v0.19.0"
 curl -sLk https://gcr.io/v2/k8s-staging-external-dns/external-dns/tags/list | jq | grep "$EXT_DNS_VERSION" | tail -n 10
 ```
 
@@ -41,9 +41,9 @@ You must be an official maintainer of the project to be able to do a release.
 - Run `scripts/releaser.sh` to create a new GitHub release. Alternatively you can create a release in the GitHub UI making sure to click on the autogenerate release node feature.
 - The step above will trigger the Kubernetes based CI/CD system [Prow](https://prow.k8s.io/?repo=kubernetes-sigs%2Fexternal-dns). Verify that a new image was built and uploaded to `gcr.io/k8s-staging-external-dns/external-dns`.
 - Create a PR in the [k8s.io repo](https://github.com/kubernetes/k8s.io) by taking the current staging image using the sha256 digest. They can be obtained with `scripts/get-sha256.sh`. Once the PR is merged, the image will be live with the corresponding tag specified in the PR.
-  - See https://github.com/kubernetes/k8s.io/pull/540 for reference
+  - See https://github.com/kubernetes/k8s.io/pull/8466 for reference
 - Verify that the image is pullable with the given tag
-  - `docker run registry.k8s.io/external-dns/external-dns:v0.16.0 --version`
+  - `docker run registry.k8s.io/external-dns/external-dns:v0.x.0 --version`
 - Branch out from the default branch and run `scripts/version-updater.sh` to update the image tag used in the kustomization.yaml and in documentation.
 - Create the PR with this version change.
 - Create an issue to release the corresponding Helm chart via the chart release process (below) assigned to a chart maintainer
diff --git a/docs/sources/gateway-api.md b/docs/sources/gateway-api.md
index bcd127c6..1333ffa0 100644
--- a/docs/sources/gateway-api.md
+++ b/docs/sources/gateway-api.md
@@ -88,7 +88,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         # Add desired Gateway API Route sources.
         - --source=gateway-httproute
diff --git a/docs/sources/gloo-proxy.md b/docs/sources/gloo-proxy.md
index 00446b13..fe60b654 100644
--- a/docs/sources/gloo-proxy.md
+++ b/docs/sources/gloo-proxy.md
@@ -24,7 +24,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=gloo-proxy
         - --gloo-namespace=custom-gloo-system # gloo system namespace. Specify multiple times for multiple namespaces. Omit to use the default (gloo-system)
@@ -96,7 +96,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=gloo-proxy
         - --gloo-namespace=custom-gloo-system # gloo system namespace. Specify multiple times for multiple namespaces. Omit to use the default (gloo-system)
diff --git a/docs/sources/istio.md b/docs/sources/istio.md
index e2c046e3..2b1863d0 100644
--- a/docs/sources/istio.md
+++ b/docs/sources/istio.md
@@ -33,7 +33,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
@@ -107,7 +107,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
diff --git a/docs/sources/kong.md b/docs/sources/kong.md
index 652273d6..961717f8 100644
--- a/docs/sources/kong.md
+++ b/docs/sources/kong.md
@@ -24,7 +24,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=kong-tcpingress
         - --provider=aws
@@ -92,7 +92,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=kong-tcpingress
         - --provider=aws
diff --git a/docs/sources/nodes.md b/docs/sources/nodes.md
index 7f90abc3..7753723b 100644
--- a/docs/sources/nodes.md
+++ b/docs/sources/nodes.md
@@ -23,7 +23,7 @@ spec:
   serviceAccountName: external-dns
   containers:
   - name: external-dns
-    image: registry.k8s.io/external-dns/external-dns:v0.18.0 # update this to the desired external-dns version
+    image: registry.k8s.io/external-dns/external-dns:v0.19.0 # update this to the desired external-dns version
     args:
     - --source=node # will use nodes as source
     - --provider=aws
@@ -59,7 +59,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=node # will use nodes as source
         - --provider=aws
@@ -133,7 +133,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=node # will use nodes as source
         - --provider=aws
diff --git a/docs/sources/openshift.md b/docs/sources/openshift.md
index 7696ace5..d1b122d0 100644
--- a/docs/sources/openshift.md
+++ b/docs/sources/openshift.md
@@ -72,7 +72,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=openshift-route
         - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
@@ -143,7 +143,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=openshift-route
         - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
diff --git a/docs/tutorials/akamai-edgedns.md b/docs/tutorials/akamai-edgedns.md
index 54f72ef5..9d27bad1 100644
--- a/docs/tutorials/akamai-edgedns.md
+++ b/docs/tutorials/akamai-edgedns.md
@@ -104,7 +104,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service  # or ingress or both
         - --provider=akamai
@@ -193,7 +193,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service  # or ingress or both
         - --provider=akamai
diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md
index c8f8b01c..5c071057 100644
--- a/docs/tutorials/alibabacloud.md
+++ b/docs/tutorials/alibabacloud.md
@@ -113,7 +113,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
@@ -190,7 +190,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
diff --git a/docs/tutorials/aws-public-private-route53.md b/docs/tutorials/aws-public-private-route53.md
index 1cff4f47..ec9a064c 100644
--- a/docs/tutorials/aws-public-private-route53.md
+++ b/docs/tutorials/aws-public-private-route53.md
@@ -242,7 +242,7 @@ spec:
         - --txt-owner-id=external-dns
         - --ingress-class=external-ingress
         - --aws-zone-type=public
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         name: external-dns-public
 ```
 
@@ -280,7 +280,7 @@ spec:
         - --txt-owner-id=dev.k8s.nexus
         - --ingress-class=internal-ingress
         - --aws-zone-type=private
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         name: external-dns-private
 ```
 
diff --git a/docs/tutorials/aws-sd.md b/docs/tutorials/aws-sd.md
index 6e0919b7..f68a9675 100644
--- a/docs/tutorials/aws-sd.md
+++ b/docs/tutorials/aws-sd.md
@@ -181,7 +181,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         env:
           - name: AWS_REGION
             value: us-east-1 # put your CloudMap NameSpace region
@@ -251,7 +251,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         env:
           - name: AWS_REGION
             value: us-east-1 # put your CloudMap NameSpace region
diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md
index 6585aa9e..b1479ade 100644
--- a/docs/tutorials/aws.md
+++ b/docs/tutorials/aws.md
@@ -9,9 +9,6 @@ Record Sets and Hosted Zones. You'll want to create this Policy in IAM first. In
 our example, we'll call the policy `AllowExternalDNSUpdates` (but you can call
 it whatever you prefer).
 
-If you prefer, you may fine-tune the policy to permit updates only to explicit
-Hosted Zone IDs.
-
 ```json
 {
   "Version": "2012-10-17",
@@ -19,7 +16,9 @@ Hosted Zone IDs.
     {
       "Effect": "Allow",
       "Action": [
-        "route53:ChangeResourceRecordSets"
+        "route53:ChangeResourceRecordSets",
+        "route53:ListResourceRecordSets",
+        "route53:ListTagsForResources"
       ],
       "Resource": [
         "arn:aws:route53:::hostedzone/*"
@@ -28,9 +27,7 @@ Hosted Zone IDs.
     {
       "Effect": "Allow",
       "Action": [
-        "route53:ListHostedZones",
-        "route53:ListResourceRecordSets",
-        "route53:ListTagsForResources"
+        "route53:ListHostedZones"
       ],
       "Resource": [
         "*"
@@ -51,7 +48,9 @@ You can use Attribute-based access control(ABAC) for advanced deployments.
     {
       "Effect": "Allow",
       "Action": [
-        "route53:ChangeResourceRecordSets"
+        "route53:ChangeResourceRecordSets",
+        "route53:ListResourceRecordSets",
+        "route53:ListTagsForResources"
       ],
       "Resource": [
         "arn:aws:route53:::hostedzone/*"
@@ -67,9 +66,7 @@ You can use Attribute-based access control(ABAC) for advanced deployments.
     {
       "Effect": "Allow",
       "Action": [
-        "route53:ListHostedZones",
-        "route53:ListResourceRecordSets",
-        "route53:ListTagsForResources"
+        "route53:ListHostedZones"
       ],
       "Resource": [
         "*"
@@ -79,6 +76,11 @@ You can use Attribute-based access control(ABAC) for advanced deployments.
 }
 ```
 
+### Further improvements
+
+Both policies can be further enhanced by tightening them down following the [principle of least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege).
+Explicitly providing a list of selected zones instead of `*` you can scope the deployment down allowing changes only to zones from the list hence reducing the blast radius and improving auditability.
+
 Additional resources:
 
 - AWS IAM actions [documentation](https://www.awsiamactions.io/?o=route53%3A)
@@ -502,7 +504,7 @@ spec:
     spec:
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.18.0
+          image: registry.k8s.io/external-dns/external-dns:v0.19.0
           args:
             - --source=service
             - --source=ingress
@@ -1053,7 +1055,7 @@ A simple way to implement randomised startup is with an init container:
     spec:
       initContainers:
       - name: init-jitter
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         command:
         - /bin/sh
         - -c
diff --git a/docs/tutorials/azure-private-dns.md b/docs/tutorials/azure-private-dns.md
index 77123334..949320a1 100644
--- a/docs/tutorials/azure-private-dns.md
+++ b/docs/tutorials/azure-private-dns.md
@@ -144,7 +144,7 @@ spec:
     spec:
       containers:
       - name: externaldns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
@@ -220,7 +220,7 @@ spec:
       serviceAccountName: externaldns
       containers:
       - name: externaldns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
@@ -296,7 +296,7 @@ spec:
       serviceAccountName: externaldns
       containers:
       - name: externaldns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md
index 7e031d60..6d7a88e7 100644
--- a/docs/tutorials/azure.md
+++ b/docs/tutorials/azure.md
@@ -58,8 +58,11 @@ The following fields are used:
 * `aadClientSecret` is associated with the Service Principal. This is only used with Service Principal method documented in the next section.
 * `useManagedIdentityExtension` - this is set to `true` if you use either AKS Kubelet Identity or AAD Pod Identities methods documented in the next section.
 * `userAssignedIdentityID` - this contains the client id from the Managed identity when using the AAD Pod Identities method documented in the next setion.
-* `activeDirectoryAuthorityHost` - this contains the uri to overwrite the default provided AAD Endpoint. This is useful for providing additional support where the endpoint is not available in the default cloud config from the [azure-sdk-for-go](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud#pkg-variables).
+* `activeDirectoryAuthorityHost` - this contains the URI to override the default Azure Active Directory authority endpoint.
+  This is useful for Azure Stack Cloud deployments or custom environments.
 * `useWorkloadIdentityExtension` - this is set to `true` if you use Workload Identity method documented in the next section.
+* `ResourceManagerAudience` - this specifies the audience for the Azure Resource Manager service when using Azure Stack Cloud. This is required for Azure Stack Cloud deployments to authenticate with the correct Resource Manager endpoint.
+* `ResourceManagerEndpoint` - this specifies the endpoint URL for the Azure Resource Manager service when using Azure Stack Cloud. This is required for Azure Stack Cloud deployments to point to the correct Resource Manager instance.
 
 The Azure DNS provider expects, by default, that the configuration file is at `/etc/kubernetes/azure.json`.  This can be overridden with the `--azure-config-file` option when starting ExternalDNS.
 
@@ -534,7 +537,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
@@ -606,7 +609,7 @@ spec:
       serviceAccountName: external-dns
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.18.0
+          image: registry.k8s.io/external-dns/external-dns:v0.19.0
           args:
             - --source=service
             - --source=ingress
@@ -682,7 +685,7 @@ spec:
       serviceAccountName: external-dns
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.18.0
+          image: registry.k8s.io/external-dns/external-dns:v0.19.0
           args:
             - --source=service
             - --source=ingress
diff --git a/docs/tutorials/civo.md b/docs/tutorials/civo.md
index 8c7e3a11..b73c2c26 100644
--- a/docs/tutorials/civo.md
+++ b/docs/tutorials/civo.md
@@ -40,7 +40,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -107,7 +107,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md
index 1147e0e2..d5b5764f 100644
--- a/docs/tutorials/cloudflare.md
+++ b/docs/tutorials/cloudflare.md
@@ -4,6 +4,37 @@ This tutorial describes how to setup ExternalDNS for usage within a Kubernetes c
 
 Make sure to use **>=0.4.2** version of ExternalDNS for this tutorial.
 
+## CloudFlare SDK Migration Status
+
+ExternalDNS is currently migrating from the legacy CloudFlare Go SDK v0 to the modern v4 SDK to improve performance, reliability, and access to newer CloudFlare features. The migration status is:
+
+**✅ Fully migrated to v4 SDK:**
+
+- Zone management (listing, filtering, pagination)
+- Zone details retrieval (`GetZone`)
+- Zone ID lookup by name (`ZoneIDByName`)
+- Zone plan detection (fully v4 implementation)
+- Regional services (data localization)
+
+**🔄 Still using legacy v0 SDK:**
+
+- DNS record management (create, update, delete records)
+- Custom hostnames
+- Proxied records
+
+This mixed approach ensures continued functionality while gradually modernizing the codebase. Users should not experience any breaking changes during this transition.
+
+### SDK Dependencies
+
+ExternalDNS currently uses:
+
+- **cloudflare-go v0.115.0+**: Legacy SDK for DNS records, custom hostnames, and proxied record features
+- **cloudflare-go/v4 v4.6.0+**: Modern SDK for all zone management and regional services operations
+
+Zone management has been fully migrated to the v4 SDK, providing improved performance and reliability.
+
+Both SDKs are automatically managed as Go module dependencies and require no special configuration from users.
+
 ## Creating a Cloudflare DNS zone
 
 We highly recommend to read this tutorial if you haven't used Cloudflare before:
@@ -120,7 +151,7 @@ spec:
     spec:
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.18.0
+          image: registry.k8s.io/external-dns/external-dns:v0.19.0
           args:
             - --source=service # ingress is also possible
             - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -201,7 +232,7 @@ spec:
       serviceAccountName: external-dns
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.18.0
+          image: registry.k8s.io/external-dns/external-dns:v0.19.0
           args:
             - --source=service # ingress is also possible
             - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -353,7 +384,7 @@ The custom hostname DNS must resolve to the Cloudflare DNS record (`external-dns
 
 Requires [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) product and "SSL and Certificates" API permission.
 
-Due to a limitation within the cloudflare-go v0 API, the custom hostname page size is fixed at 50.
+**Note:** Due to using the legacy cloudflare-go v0 API for custom hostname management, the custom hostname page size is fixed at 50. This limitation will be addressed in a future migration to the v4 SDK.
 
 ## Using CRD source to manage DNS records in Cloudflare
 
diff --git a/docs/tutorials/contour.md b/docs/tutorials/contour.md
index cf52dc81..ff60cbe6 100644
--- a/docs/tutorials/contour.md
+++ b/docs/tutorials/contour.md
@@ -25,7 +25,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
@@ -98,7 +98,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md
index 0187ffdc..081f144a 100644
--- a/docs/tutorials/coredns.md
+++ b/docs/tutorials/coredns.md
@@ -133,7 +133,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=ingress
         - --provider=coredns
@@ -203,7 +203,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=ingress
         - --provider=coredns
diff --git a/docs/tutorials/dnsimple.md b/docs/tutorials/dnsimple.md
index 98ef841f..3523298d 100644
--- a/docs/tutorials/dnsimple.md
+++ b/docs/tutorials/dnsimple.md
@@ -40,7 +40,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple.
@@ -112,7 +112,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple.
diff --git a/docs/tutorials/externalname.md b/docs/tutorials/externalname.md
index 4279c08c..0ed76fc6 100644
--- a/docs/tutorials/externalname.md
+++ b/docs/tutorials/externalname.md
@@ -28,7 +28,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --log-level=debug
         - --source=service
diff --git a/docs/tutorials/gandi.md b/docs/tutorials/gandi.md
index 0636e007..2d0e6fe6 100644
--- a/docs/tutorials/gandi.md
+++ b/docs/tutorials/gandi.md
@@ -42,7 +42,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -110,7 +110,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/gke-nginx.md b/docs/tutorials/gke-nginx.md
index 89c65a6c..2fa43566 100644
--- a/docs/tutorials/gke-nginx.md
+++ b/docs/tutorials/gke-nginx.md
@@ -281,7 +281,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=ingress
         - --domain-filter=external-dns-test.gcp.zalan.do
@@ -579,7 +579,7 @@ spec:
             - --google-project=zalando-external-dns-test
             - --registry=txt
             - --txt-owner-id=my-identifier
-          image: registry.k8s.io/external-dns/external-dns:v0.18.0
+          image: registry.k8s.io/external-dns/external-dns:v0.19.0
           name: external-dns
       securityContext:
         fsGroup: 65534
diff --git a/docs/tutorials/gke.md b/docs/tutorials/gke.md
index d88c8907..6843c505 100644
--- a/docs/tutorials/gke.md
+++ b/docs/tutorials/gke.md
@@ -385,7 +385,7 @@ spec:
       serviceAccountName: external-dns
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.18.0
+          image: registry.k8s.io/external-dns/external-dns:v0.19.0
           args:
             - --source=service
             - --source=ingress
diff --git a/docs/tutorials/godaddy.md b/docs/tutorials/godaddy.md
index fad04717..ca70162b 100644
--- a/docs/tutorials/godaddy.md
+++ b/docs/tutorials/godaddy.md
@@ -64,7 +64,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -103,9 +103,6 @@ rules:
 - apiGroups: [""]
   resources: ["nodes"]
   verbs: ["list","watch"]
-- apiGroups: [""]
-  resources: ["endpoints"]
-  verbs: ["get","watch","list"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
@@ -138,7 +135,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/hostport.md b/docs/tutorials/hostport.md
index a789459e..6ac2681d 100644
--- a/docs/tutorials/hostport.md
+++ b/docs/tutorials/hostport.md
@@ -35,7 +35,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --log-level=debug
         - --source=service
@@ -104,7 +104,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --log-level=debug
         - --source=service
diff --git a/docs/tutorials/linode.md b/docs/tutorials/linode.md
index 5f417021..b110b1c4 100644
--- a/docs/tutorials/linode.md
+++ b/docs/tutorials/linode.md
@@ -41,7 +41,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -108,7 +108,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/myra.md b/docs/tutorials/myra.md
new file mode 100644
index 00000000..c25e1682
--- /dev/null
+++ b/docs/tutorials/myra.md
@@ -0,0 +1,215 @@
+# Myra ExternalDNS Webhook
+
+This guide provides quick instructions for setting up and testing the [Myra ExternalDNS Webhook](https://github.com/Myra-Security-GmbH/external-dns-myrasec-webhook) in a Kubernetes environment.
+
+## Prerequisites
+
+- Kubernetes cluster (v1.19+)
+- `kubectl` configured to access your cluster
+- Docker for building the container image
+- MyraSec API credentials (API key and secret)
+- Domain registered with MyraSec
+
+## Quick Installation
+
+### 1. Build and Push the Docker Image
+
+```bash
+# From the project root
+docker build -t myra-webhook:latest .
+
+# Tag the image for your container registry
+docker tag myra-webhook:latest YOUR_REGISTRY/myra-webhook:latest
+
+# Push to your container registry
+docker push YOUR_REGISTRY/myra-webhook:latest
+```
+
+> **Important**: The image must be pushed to a container registry accessible by your Kubernetes cluster. Update the image reference in the deployment YAML file to match your registry path.
+
+### 2. Configure API Credentials
+
+Create a secret with your MyraSec API credentials:
+
+```bash
+kubectl create secret generic myra-webhook-secrets \
+  --from-literal=myrasec-api-key=YOUR_API_KEY \
+  --from-literal=myrasec-api-secret=YOUR_API_SECRET \
+  --from-literal=domain-filter=YOUR_DOMAIN.com
+```
+
+Alternatively, apply the provided secret template after editing:
+
+```bash
+# Edit the secret file first
+vi deploy/myra-webhook-secrets.yaml
+
+# Then apply
+kubectl apply -f deploy/myra-webhook-secrets.yaml
+```
+
+### 3. Deploy the Webhook and ExternalDNS
+
+```bash
+# Apply the combined deployment
+kubectl apply -f deploy/combined-deployment.yaml
+```
+
+This deploys:
+
+- ConfigMap with webhook configuration
+- ServiceAccount, ClusterRole, and ClusterRoleBinding for RBAC
+- Deployment with two containers:
+  - myra-webhook: The webhook provider implementation
+  - external-dns: The ExternalDNS controller using the webhook provider
+
+### 4. Verify Deployment
+
+```bash
+# Check if pods are running
+kubectl get pods -l app=myra-externaldns
+
+# Check logs for the webhook container
+kubectl logs -l app=myra-externaldns -c myra-webhook
+
+# Check logs for the external-dns container
+kubectl logs -l app=myra-externaldns -c external-dns
+```
+
+## Manual Testing with NGINX Demo
+
+### 1. Deploy the NGINX Demo Application
+
+```bash
+# Edit the domain in the nginx-demo.yaml file to match your domain
+vi deploy/nginx-demo.yaml
+
+# Most important part is to set the correct domain in the external-dns.alpha.kubernetes.io/hostname annotation
+# Example:
+# annotations:
+#   external-dns.alpha.kubernetes.io/enabled: "true"
+#   external-dns.alpha.kubernetes.io/hostname: "nginx-demo.dummydomainforkubes.de"
+#   external-dns.alpha.kubernetes.io/target: "9.2.3.4"
+
+# Apply the demo resources
+kubectl apply -f deploy/nginx-demo.yaml
+```
+
+This creates:
+
+- NGINX Deployment
+- Service for the deployment
+- Ingress resource with ExternalDNS annotations
+
+### 2. Verify DNS Record Creation
+
+After deploying the demo application, ExternalDNS should automatically create DNS records in MyraSec:
+
+```bash
+# Check external-dns logs to see record creation
+kubectl logs -l app=myra-externaldns -c external-dns | grep "nginx-demo"
+
+# Verify the webhook logs
+kubectl logs -l app=myra-externaldns -c myra-webhook | grep "Created DNS record"
+```
+
+You can also verify through the MyraSec dashboard that the records were created.
+
+### 3. Testing Record Deletion
+
+To test record deletion:
+
+```bash
+# Delete the nginx-demo resources or remove annotation from ingress
+kubectl delete -f deploy/nginx-demo.yaml
+
+# Delete the ingress resource or remove annotation from ingress
+# If resource is still active, external dns might still see the record and manage it
+kubectl delete ingress nginx-demo -n default
+
+# Check external-dns logs to see record deletion
+kubectl logs -l app=myra-externaldns -c external-dns | grep "nginx-demo" | grep "delete"
+
+# Verify the webhook logs
+kubectl logs -l app=myra-externaldns -c myra-webhook | grep "Deleted DNS record"
+```
+
+## Configuration Options
+
+The webhook can be configured through the ConfigMap:
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `dry-run` | Run in dry-run mode without making actual changes | `"false"` |
+| `environment` | Environment name (affects private IP handling) | `"prod"` |
+| `log-level` | Logging level (debug, info, warn, error) | `"debug"` |
+| `ttl` | Default TTL for DNS records | `"300"` |
+| `webhook-listen-address` | Address and port for the webhook server | `":8080"` |
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Webhook not receiving requests**
+   - Ensure the `webhook-provider-url` in the external-dns args is correct
+   - Check network connectivity between containers
+
+2. **DNS records not being created**
+   - Verify MyraSec API credentials are correct
+   - Check if the domain filter is properly configured
+   - Look for error messages in the webhook and external-dns logs
+
+3. **Permissions issues**
+   - Ensure the ServiceAccount has the correct RBAC permissions
+
+### Getting Help
+
+For more detailed logs:
+
+```bash
+# Set log level to debug in the ConfigMap
+kubectl edit configmap myra-externaldns-config
+# Change log-level to "debug"
+
+# Restart the pods
+kubectl rollout restart deployment myra-externaldns
+```
+
+## Environment Configuration
+
+The webhook supports different environment configurations through the `environment` setting in the ConfigMap:
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: myra-externaldns-config
+data:
+  environment: "prod"  # Can be "prod", "staging", "dev", etc.
+```
+
+The environment setting affects how the webhook handles certain operations:
+
+| Environment | Behavior |
+|-------------|----------|
+| `prod`, `production`, `staging` | Strict mode: Skips private IP records, enforces stricter validation |
+| `dev`, `development`, `test`, etc. | Development mode: Allows private IP records, more permissive validation |
+
+To modify the environment:
+
+```bash
+# Edit the ConfigMap directly
+kubectl edit configmap myra-externaldns-config
+
+# Or apply an updated YAML file
+kubectl apply -f updated-config.yaml
+```
+
+## Advanced Configuration
+
+For production deployments, consider:
+
+1. Using a proper image registry instead of `latest` tag
+2. Setting resource limits appropriate for your environment
+3. Configuring horizontal pod autoscaling
+4. Using Helm for deployment management
diff --git a/docs/tutorials/ns1.md b/docs/tutorials/ns1.md
index 8f0e452c..695f6303 100644
--- a/docs/tutorials/ns1.md
+++ b/docs/tutorials/ns1.md
@@ -93,7 +93,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -163,7 +163,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/oracle.md b/docs/tutorials/oracle.md
index 69966eb3..932b3826 100644
--- a/docs/tutorials/oracle.md
+++ b/docs/tutorials/oracle.md
@@ -173,7 +173,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service
         - --source=ingress
diff --git a/docs/tutorials/ovh.md b/docs/tutorials/ovh.md
index 170fc4fe..e9761534 100644
--- a/docs/tutorials/ovh.md
+++ b/docs/tutorials/ovh.md
@@ -97,7 +97,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -139,9 +139,6 @@ rules:
 - apiGroups: [""]
   resources: ["nodes"]
   verbs: ["list"]
-- apiGroups: [""]
-  resources: ["endpoints"]
-  verbs: ["get","watch","list"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
@@ -174,7 +171,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/pdns.md b/docs/tutorials/pdns.md
index f6169b3a..b70e2f11 100644
--- a/docs/tutorials/pdns.md
+++ b/docs/tutorials/pdns.md
@@ -42,7 +42,7 @@ spec:
       # serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # or ingress or both
         - --provider=pdns
diff --git a/docs/tutorials/pihole.md b/docs/tutorials/pihole.md
index 6b729d47..b09addfe 100644
--- a/docs/tutorials/pihole.md
+++ b/docs/tutorials/pihole.md
@@ -87,7 +87,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         # If authentication is disabled and/or you didn't create
         # a secret, you can remove this block.
         envFrom:
diff --git a/docs/tutorials/plural.md b/docs/tutorials/plural.md
index c92ff3be..d6d69be5 100644
--- a/docs/tutorials/plural.md
+++ b/docs/tutorials/plural.md
@@ -61,7 +61,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -134,7 +134,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/rfc2136.md b/docs/tutorials/rfc2136.md
index cda6bfb4..5710812a 100644
--- a/docs/tutorials/rfc2136.md
+++ b/docs/tutorials/rfc2136.md
@@ -257,7 +257,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --registry=txt
         - --txt-prefix=external-dns-
@@ -301,7 +301,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --registry=txt
         - --txt-prefix=external-dns-
diff --git a/docs/tutorials/scaleway.md b/docs/tutorials/scaleway.md
index bb1784fc..c92891a9 100644
--- a/docs/tutorials/scaleway.md
+++ b/docs/tutorials/scaleway.md
@@ -65,7 +65,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -149,7 +149,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/transip.md b/docs/tutorials/transip.md
index bf8e8194..18006f47 100644
--- a/docs/tutorials/transip.md
+++ b/docs/tutorials/transip.md
@@ -37,7 +37,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains
@@ -111,7 +111,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.18.0
+        image: registry.k8s.io/external-dns/external-dns:v0.19.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains
diff --git a/endpoint/domain_filter.go b/endpoint/domain_filter.go
index 47402bb7..8d8aad2d 100644
--- a/endpoint/domain_filter.go
+++ b/endpoint/domain_filter.go
@@ -25,7 +25,8 @@ import (
 	"strings"
 
 	log "github.com/sirupsen/logrus"
-	"golang.org/x/net/idna"
+
+	"sigs.k8s.io/external-dns/internal/idna"
 )
 
 type MatchAllDomainFilters []DomainFilterInterface
@@ -247,9 +248,9 @@ func (df *DomainFilter) MatchParent(domain string) bool {
 }
 
 // normalizeDomain converts a domain to a canonical form, so that we can filter on it
-// it: trim "." suffix, get Unicode version of domain complient with Section 5 of RFC 5891
+// it: trim "." suffix, get Unicode version of domain compliant with Section 5 of RFC 5891
 func normalizeDomain(domain string) string {
-	s, err := idna.Lookup.ToUnicode(strings.TrimSuffix(domain, "."))
+	s, err := idna.Profile.ToUnicode(strings.TrimSuffix(domain, "."))
 	if err != nil {
 		log.Warnf(`Got error while parsing domain %s: %v`, domain, err)
 	}
diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go
index 68f82378..e060ed4e 100644
--- a/endpoint/endpoint.go
+++ b/endpoint/endpoint.go
@@ -25,6 +25,8 @@ import (
 	"strings"
 
 	log "github.com/sirupsen/logrus"
+
+	"sigs.k8s.io/external-dns/pkg/events"
 )
 
 const (
@@ -111,7 +113,7 @@ func (t Targets) Swap(i, j int) {
 	t[i], t[j] = t[j], t[i]
 }
 
-// Same compares to Targets and returns true if they are identical (case-insensitive)
+// Same compares two Targets and returns true if they are identical (case-insensitive)
 func (t Targets) Same(o Targets) bool {
 	if len(t) != len(o) {
 		return false
@@ -241,6 +243,9 @@ type Endpoint struct {
 	// ProviderSpecific stores provider specific config
 	// +optional
 	ProviderSpecific ProviderSpecific `json:"providerSpecific,omitempty"`
+	// refObject stores reference object
+	// +optional
+	refObject *events.ObjectReference
 }
 
 // NewEndpoint initialization method to be used to create an endpoint
@@ -335,6 +340,17 @@ func (e *Endpoint) WithLabel(key, value string) *Endpoint {
 	return e
 }
 
+// WithRefObject sets the reference object for the Endpoint and returns the Endpoint.
+// This can be used to associate the Endpoint with a specific Kubernetes object.
+func (e *Endpoint) WithRefObject(obj *events.ObjectReference) *Endpoint {
+	e.refObject = obj
+	return e
+}
+
+func (e *Endpoint) RefObject() *events.ObjectReference {
+	return e.refObject
+}
+
 // Key returns the EndpointKey of the Endpoint.
 func (e *Endpoint) Key() EndpointKey {
 	return EndpointKey{
@@ -354,6 +370,10 @@ func (e *Endpoint) String() string {
 	return fmt.Sprintf("%s %d IN %s %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.SetIdentifier, e.Targets, e.ProviderSpecific)
 }
 
+func (e *Endpoint) Describe() string {
+	return fmt.Sprintf("record:%s, owner:%s, type:%s, targets:%s", e.DNSName, e.SetIdentifier, e.RecordType, strings.Join(e.Targets, ", "))
+}
+
 // UniqueOrderedTargets removes duplicate targets from the Endpoint and sorts them in lexicographical order.
 func (e *Endpoint) UniqueOrderedTargets() {
 	result := make([]string, 0, len(e.Targets))
diff --git a/go.mod b/go.mod
index 498acdeb..b9af7f17 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,9 @@ module sigs.k8s.io/external-dns
 go 1.24.2
 
 require (
-	cloud.google.com/go/compute/metadata v0.7.0
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1
-	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
+	cloud.google.com/go/compute/metadata v0.8.0
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0
 	github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.1
@@ -13,23 +13,24 @@ 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.36.5
-	github.com/aws/aws-sdk-go-v2/config v1.29.17
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.70
-	github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5
-	github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0
-	github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0
-	github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7
-	github.com/aws/aws-sdk-go-v2/service/sts v1.34.0
+	github.com/aws/aws-sdk-go-v2 v1.38.1
+	github.com/aws/aws-sdk-go-v2/config v1.31.2
+	github.com/aws/aws-sdk-go-v2/credentials v1.18.6
+	github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.6
+	github.com/aws/aws-sdk-go-v2/service/dynamodb v1.49.1
+	github.com/aws/aws-sdk-go-v2/service/route53 v1.56.2
+	github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.2
+	github.com/aws/aws-sdk-go-v2/service/sts v1.38.0
+	github.com/aws/smithy-go v1.22.5
 	github.com/bodgit/tsig v1.2.2
-	github.com/cenkalti/backoff/v5 v5.0.2
-	github.com/civo/civogo v0.6.1
+	github.com/cenkalti/backoff/v5 v5.0.3
+	github.com/civo/civogo v0.6.4
 	github.com/cloudflare/cloudflare-go v0.115.0
-	github.com/cloudflare/cloudflare-go/v4 v4.6.0
+	github.com/cloudflare/cloudflare-go/v5 v5.1.0
 	github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
 	github.com/datawire/ambassador v1.12.4
 	github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace
-	github.com/digitalocean/godo v1.158.0
+	github.com/digitalocean/godo v1.163.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
@@ -38,97 +39,120 @@ require (
 	github.com/goccy/go-yaml v1.18.0
 	github.com/google/go-cmp v0.7.0
 	github.com/google/uuid v1.6.0
-	github.com/linode/linodego v1.53.0
+	github.com/linode/linodego v1.55.0
 	github.com/maxatome/go-testdeep v1.14.0
-	github.com/miekg/dns v1.1.67
+	github.com/miekg/dns v1.1.68
 	github.com/openshift/api v0.0.0-20230607130528-611114dca681
 	github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3
-	github.com/oracle/oci-go-sdk/v65 v65.95.2
+	github.com/oracle/oci-go-sdk/v65 v65.99.0
 	github.com/ovh/go-ovh v1.9.0
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/pluralsh/gqlclient v1.12.2
-	github.com/projectcontour/contour v1.32.0
-	github.com/prometheus/client_golang v1.22.0
+	github.com/projectcontour/contour v1.32.1
+	github.com/prometheus/client_golang v1.23.0
 	github.com/prometheus/client_model v0.6.2
-	github.com/prometheus/common v0.64.0
+	github.com/prometheus/common v0.65.0
 	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34
 	github.com/sirupsen/logrus v1.9.3
 	github.com/stretchr/testify v1.10.0
 	github.com/transip/gotransip/v6 v6.26.0
-	go.etcd.io/etcd/api/v3 v3.6.2
-	go.etcd.io/etcd/client/v3 v3.6.2
+	go.etcd.io/etcd/api/v3 v3.6.4
+	go.etcd.io/etcd/client/v3 v3.6.4
 	go.uber.org/ratelimit v0.3.1
-	golang.org/x/net v0.42.0
+	golang.org/x/net v0.43.0
 	golang.org/x/oauth2 v0.30.0
 	golang.org/x/sync v0.16.0
-	golang.org/x/text v0.27.0
+	golang.org/x/text v0.28.0
 	golang.org/x/time v0.12.0
-	google.golang.org/api v0.242.0
+	google.golang.org/api v0.248.0
 	gopkg.in/ns1/ns1-go.v2 v2.14.4
-	istio.io/api v1.26.2
-	istio.io/client-go v1.26.2
-	k8s.io/api v0.33.3
-	k8s.io/apimachinery v0.33.3
-	k8s.io/client-go v0.33.3
+	istio.io/api v1.27.0
+	istio.io/client-go v1.27.0
+	k8s.io/api v0.33.4
+	k8s.io/apimachinery v0.33.4
+	k8s.io/client-go v0.33.4
 	k8s.io/klog/v2 v2.130.1
 	sigs.k8s.io/controller-runtime v0.21.0
 	sigs.k8s.io/gateway-api v1.3.0
 )
 
 require (
-	cloud.google.com/go/auth v0.16.2 // indirect
+	atomicgo.dev/cursor v0.2.0 // indirect
+	atomicgo.dev/keyboard v0.2.9 // indirect
+	atomicgo.dev/schedule v0.1.0 // indirect
+	cloud.google.com/go/auth v0.16.5 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect
 	github.com/99designs/gqlgen v0.17.73 // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
 	github.com/Masterminds/semver v1.5.0 // indirect
+	github.com/alecthomas/chroma/v2 v2.20.0 // indirect
 	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
 	github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
-	github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
-	github.com/aws/smithy-go v1.22.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.30.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 // indirect
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/bahlo/generic-list-go v0.2.0 // indirect
 	github.com/benbjohnson/clock v1.3.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/buger/jsonparser v1.1.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/containerd/console v1.0.5 // indirect
 	github.com/coreos/go-semver v0.3.1 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/daveshanley/vacuum v0.17.8 // indirect
 	github.com/deepmap/oapi-codegen v1.9.1 // indirect
+	github.com/dlclark/regexp2 v1.11.5 // indirect
+	github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect
+	github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/emicklei/go-restful/v3 v3.12.1 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+	github.com/ghodss/yaml v1.0.0 // indirect
+	github.com/gizak/termui/v3 v3.1.0 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-resty/resty/v2 v2.16.5 // indirect
+	github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
+	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/gofrs/flock v0.8.1 // indirect
 	github.com/gofrs/uuid v4.4.0+incompatible // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
+	github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/gnostic-models v0.6.9 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
-	github.com/googleapis/gax-go/v2 v2.14.2 // indirect
+	github.com/googleapis/gax-go/v2 v2.15.0 // indirect
+	github.com/gookit/color v1.5.4 // indirect
 	github.com/gopherjs/gopherjs v1.17.2 // indirect
+	github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
 	github.com/hashicorp/go-uuid v1.0.3 // indirect
+	github.com/iancoleman/strcase v0.3.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jcmturner/aescts/v2 v2.0.0 // indirect
 	github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
 	github.com/jcmturner/gofork v1.7.6 // indirect
@@ -140,37 +164,67 @@ require (
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
+	github.com/lithammer/fuzzysearch v1.1.8 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/mailru/easyjson v0.9.0 // indirect
-	github.com/mattn/go-runewidth v0.0.15 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
+	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/muesli/termenv v0.16.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/nsf/termbox-go v1.1.1 // indirect
 	github.com/onsi/ginkgo v1.16.5 // indirect
 	github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect
 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
+	github.com/pb33f/doctor v0.0.32 // indirect
+	github.com/pb33f/libopenapi v0.25.2 // indirect
+	github.com/pb33f/libopenapi-validator v0.4.7 // indirect
+	github.com/pb33f/ordered-map/v2 v2.2.0 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
 	github.com/peterhellberg/link v1.1.0 // indirect
+	github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb // indirect
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/procfs v0.15.1 // indirect
-	github.com/rivo/uniseg v0.2.0 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
+	github.com/pterm/pterm v0.12.81 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/sagikazarmark/locafero v0.9.0 // indirect
+	github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
+	github.com/sasha-s/go-deadlock v0.3.5 // indirect
 	github.com/schollz/progressbar/v3 v3.8.6 // indirect
+	github.com/segmentio/ksuid v1.0.4 // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/sony/gobreaker v0.5.0 // indirect
 	github.com/sosodev/duration v1.3.1 // indirect
-	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/sourcegraph/conc v0.3.0 // indirect
+	github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect
+	github.com/speakeasy-api/jsonpath v0.6.2 // indirect
+	github.com/spf13/afero v1.14.0 // indirect
+	github.com/spf13/cast v1.8.0 // indirect
+	github.com/spf13/cobra v1.9.1 // indirect
+	github.com/spf13/pflag v1.0.7 // indirect
+	github.com/spf13/viper v1.20.1 // indirect
 	github.com/stretchr/objx v0.5.2 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/tidwall/gjson v1.14.4 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/pretty v1.2.1 // indirect
 	github.com/tidwall/sjson v1.2.5 // indirect
+	github.com/tliron/commonlog v0.2.19 // indirect
+	github.com/tliron/glsp v0.2.2 // indirect
+	github.com/tliron/kutil v0.3.26 // indirect
 	github.com/vektah/gqlparser/v2 v2.5.26 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
+	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
-	go.etcd.io/etcd/client/pkg/v3 v3.6.2 // indirect
+	go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
 	go.opentelemetry.io/otel v1.36.0 // indirect
@@ -179,15 +233,16 @@ require (
 	go.uber.org/atomic v1.10.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
-	golang.org/x/crypto v0.40.0 // indirect
-	golang.org/x/mod v0.25.0 // indirect
-	golang.org/x/sys v0.34.0 // indirect
-	golang.org/x/term v0.33.0 // indirect
-	golang.org/x/tools v0.34.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/grpc v1.73.0 // indirect
-	google.golang.org/protobuf v1.36.6 // indirect
+	golang.org/x/crypto v0.41.0 // indirect
+	golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
+	golang.org/x/mod v0.26.0 // indirect
+	golang.org/x/sys v0.35.0 // indirect
+	golang.org/x/term v0.34.0 // indirect
+	golang.org/x/tools v0.35.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
+	google.golang.org/grpc v1.74.2 // indirect
+	google.golang.org/protobuf v1.36.7 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
@@ -201,3 +256,5 @@ require (
 	sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
+
+tool github.com/daveshanley/vacuum
diff --git a/go.sum b/go.sum
index b7f9df37..cff3bd3b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,21 @@
+atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
+atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
+atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
+atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
+atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
+atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
+atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
+atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
 bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
-cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
+cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
+cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
-cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
-cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
+cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
+cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
 code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk=
 code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@@ -16,14 +24,14 @@ github.com/99designs/gqlgen v0.17.73 h1:A3Ki+rHWqKbAOlg5fxiZBnz6OjW3nwupDHEG15gE
 github.com/99designs/gqlgen v0.17.73/go.mod h1:2RyGWjy2k7W9jxrs8MOQthXGkD3L3oGr0jXW3Pu8lGg=
 github.com/Azure/azure-sdk-for-go v16.2.1+incompatible h1:KnPIugL51v3N3WwvaSmZbxukD1WuWXOiE9fRdu32f2I=
 github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 h1:ci6Yd6nysBRLEodoziB6ah1+YOzZbZk+NYneoA6q+6E=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
@@ -53,11 +61,22 @@ github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.1/go.mod h1:/lGdCgv0e1qrS4ithe2qTU
 github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
+github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
+github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
+github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
+github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
+github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
+github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
+github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
+github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
+github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
 github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
+github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
 github.com/Masterminds/sprig v2.17.1+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
 github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA=
 github.com/Masterminds/squirrel v1.2.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
@@ -83,8 +102,14 @@ github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4Rq
 github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY=
 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
+github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
 github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
 github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
+github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
+github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -110,48 +135,53 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l
 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
+github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
 github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
 github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
-github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
-github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
-github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
-github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
-github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5 h1:oUEqVqonG3xuarrsze1KVJ30KagNYDemikTbdu8KlN8=
-github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5/go.mod h1:VNM08cHlOsIbSHRqb6D/M2L4kKXfJv3A2/f0GNbOQSc=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
+github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8=
+github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
+github.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0=
+github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc=
+github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.6 h1:QR3/KSpHmOhQD1XPn8SVbYdklWPB9TwM9VebUsisRm4=
+github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.6/go.mod h1:sMmWNSeevbQ/2lFMdm7go2WZuCMaJO4HrGHlCSN60WQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
-github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 h1:A99gjqZDbdhjtjJVZrmVzVKO2+p3MSg35bDWtbMQVxw=
-github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs=
-github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0 h1:0wOCTKrmwkyC8Bk76hYH/B4IJn5MGt6gMkSXc0A2uyc=
-github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
-github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo=
-github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0 h1:UglIEyurCqfzZkjNdYAuXUGFu/FNWMKP5eorzggvXe8=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0/go.mod h1:wi1naoiPnCQG3cyjsivwPON1ZmQt/EJGxFqXzubBTAw=
-github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7 h1:1eaP4/444jrv04HhJdwTHtgnyxWgxwdLjSYBGq+oMB4=
-github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7/go.mod h1:czoZQabc2chvmV/ak4oGSNR9CbcUw2bef3tatmwtoIA=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
-github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
-github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/aws/aws-sdk-go-v2/service/dynamodb v1.49.1 h1:0RqS5X7EodJzOenoY4V3LUSp9PirELO2ZOpOZbMldco=
+github.com/aws/aws-sdk-go-v2/service/dynamodb v1.49.1/go.mod h1:VRp/OeQolnQD9GfNgdSf3kU5vbg708PF6oPHh2bq3hc=
+github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.30.0 h1:SkUalAKtprOV5y77RsO3k76cEBPhacLIo0sGL3MKjuE=
+github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.30.0/go.mod h1:fuh7P1XXoWryEkCQVxTwoaOQ/GdI3ripI9UFmHaPo0o=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
+github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.4 h1:upi++G3fQCAUBXQe58TbjXmdVPwrqMnRQMThOAIz7KM=
+github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.4/go.mod h1:swb+GqWXTZMOyVV9rVePAUu5L80+X5a+Lui1RNOyUFo=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.56.2 h1:6QKyfbweIsjt1kvE8rw+LeDxmCt1uvI8ywRe2cYOpQo=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.56.2/go.mod h1:Ro0zSeA7hRAhX04QgnUAc8MvvQO74wg/S15wzA/mxgo=
+github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.2 h1:5m5YPqPzZ2kunWiC3pld0wHcwWzx/U9/VFpWfkc1OJg=
+github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.2/go.mod h1:/ZwaUAo11yCI+/RlsS9qenibv8nlhojmhGJZwswFyGg=
+github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0=
+github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM=
+github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo=
+github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
+github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
 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=
@@ -162,32 +192,37 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
 github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/blang/semver v3.5.0+incompatible h1:CGxCgetQ64DKk7rdZ++Vfnb1+ogGNnB17OJKJXD2Cfs=
 github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/bodgit/tsig v1.2.2 h1:RgxTCr8UFUHyU4D8Ygb2UtXtS4niw4B6XYYBpgCjl0k=
 github.com/bodgit/tsig v1.2.2/go.mod h1:rIGNOLZOV/UA03fmCUtEFbpWOrIoaOuETkpaeTvnLF4=
 github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
 github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
 github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
 github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
-github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
-github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
-github.com/civo/civogo v0.6.1 h1:PFOh7rBU0vmj7LTDIv3z7l9uXG4SZyyzScCl3wyTFSc=
-github.com/civo/civogo v0.6.1/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
+github.com/civo/civogo v0.6.4 h1:f77SHuXcVuUAm1famdtN9YUMP+eA9myyxAgRmepY9uQ=
+github.com/civo/civogo v0.6.4/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
 github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
 github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
-github.com/cloudflare/cloudflare-go/v4 v4.6.0 h1:ZaWwXjHFR5NoY8UEf4QFY0g3KTi72kqqEXpajV610/o=
-github.com/cloudflare/cloudflare-go/v4 v4.6.0/go.mod h1:XcYpLe7Mf6FN87kXzEWVnJ6z+vskW/k6eUqgqfhFE9k=
+github.com/cloudflare/cloudflare-go/v5 v5.1.0 h1:vvWUtrt5ZPEBFidL2ik64QipXLZmhMBgtRTw4bYvPwE=
+github.com/cloudflare/cloudflare-go/v5 v5.1.0/go.mod h1:C6OjOlDHOk/g7lXehothXJRFZrSIJMLzOZB2SXQhcjk=
 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s=
 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@@ -198,6 +233,9 @@ github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+Bu
 github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
 github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
 github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
+github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
 github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
@@ -225,6 +263,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
@@ -237,6 +276,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/daveshanley/vacuum v0.17.8 h1:bblznbx87nBbQQuDejAXBfkH1OfyNJ2D9cB83oChZE4=
+github.com/daveshanley/vacuum v0.17.8/go.mod h1:CDa14YnFLsa1L7AKm8WjO0UfxEDpLbrgAY8o18LbQOY=
 github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
@@ -249,11 +290,11 @@ github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace h1:1SnCTPFh2AA
 github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace/go.mod h1:TK05uvk4XXfK2kdvRwfcZ1NaxjDxmm7H3aQLko0mJxA=
 github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/digitalocean/godo v1.158.0 h1:XW7UlJn2X2qH8JOm6N63Hk/PjPHjLdYgG4zPaSbmbGc=
-github.com/digitalocean/godo v1.158.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM=
+github.com/digitalocean/godo v1.163.0 h1:FKB5ESsvg0d+Rh04t8Ij7fERvgbjYysQojdnIk/Ea4c=
+github.com/digitalocean/godo v1.163.0/go.mod h1:NJ1VlXmFMSnG1GEe2rWyDZVrhR69c3nHmL0s1cSSQ6M=
+github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
+github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 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=
@@ -270,10 +311,16 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
 github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
 github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg=
+github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
+github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 h1:fuHXpEVTTk7TilRdfGRLHpiTD6tnT0ihEowCfWjlFvw=
+github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0=
 github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@@ -293,6 +340,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/envoyproxy/protoc-gen-validate v0.3.0-java.0.20200609174644-bd816e4522c1/go.mod h1:bjmEhrMDubXDd0uKxnWwRmgSsiEv2CkJliIHnj6ETm8=
 github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
 github.com/exoscale/egoscale v0.102.3 h1:DYqN2ipoLKpiFoprRGQkp2av/Ze7sUYYlGhi1N62tfY=
 github.com/exoscale/egoscale v0.102.3/go.mod h1:RPf2Gah6up+6kAEayHTQwqapzXlm93f0VQas/UEGU5c=
 github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
@@ -307,18 +356,23 @@ github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99/go.mod h1:4mP9w
 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
 github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
-github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
 github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
+github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
+github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
 github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
 github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
@@ -340,6 +394,8 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
 github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk=
 github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
 github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
@@ -401,6 +457,8 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn
 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-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
+github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
 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=
@@ -408,6 +466,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8Wd
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
+github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
 github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
 github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
@@ -437,8 +497,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
@@ -481,6 +541,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -490,8 +551,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
-github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
+github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
 github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
@@ -503,12 +564,16 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
 github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
 github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
-github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
+github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
+github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
 github.com/gookit/color v1.2.3/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
+github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
+github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
+github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
+github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
 github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -527,6 +592,8 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
+github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
 github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@@ -575,14 +642,20 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
 github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
+github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
+github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
 github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
@@ -638,7 +711,13 @@ github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
 github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
+github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -676,9 +755,13 @@ 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.53.0 h1:UWr7bUUVMtcfsuapC+6blm6+jJLPd7Tf9MZUpdOERnI=
-github.com/linode/linodego v1.53.0/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA=
+github.com/linode/linodego v1.55.0 h1:ZMTRtTP4/4zwYdOKThge04JJIih03DXPDyVlHEmI/AY=
+github.com/linode/linodego v1.55.0/go.mod h1:LoQZ8hW8ZcXh/DJdYADwPCtVP8duyckLRGXwwwh55SU=
 github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
+github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
+github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 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=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -710,9 +793,10 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
 github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
-github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@@ -723,8 +807,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.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
-github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
+github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
+github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
@@ -733,11 +817,16 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
 github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
 github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -748,6 +837,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
@@ -763,6 +854,9 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
+github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
+github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
 github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@@ -819,8 +913,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.95.2 h1:0HJ0AgpLydp/DtvYrF2d4str2BjXOVAeNbuW7E07g94=
-github.com/oracle/oci-go-sdk/v65 v65.95.2/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
+github.com/oracle/oci-go-sdk/v65 v65.99.0 h1:M/a+N0IFk+TUCqyuf0kbqp4RyAIF+H7FmOe+CEiVbhs=
+github.com/oracle/oci-go-sdk/v65 v65.99.0/go.mod h1:RGiXfpDDmRRlLtqlStTzeBjjdUNXyqm3KXKyLCm3A/Q=
 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=
@@ -829,12 +923,25 @@ github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIw
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pb33f/doctor v0.0.32 h1:IWqFATA4N9yst0fWOJSJgPh4+arZpsAcaNxORRjGiTQ=
+github.com/pb33f/doctor v0.0.32/go.mod h1:iwnSB6QrPsu5zb4MAPr3Ba7dnTQuYIaidMgCuWmAm04=
+github.com/pb33f/libopenapi v0.25.2 h1:Jqup/1IOpEB2UnPD8Saw93geSpi8h1a28NtypvpVFqg=
+github.com/pb33f/libopenapi v0.25.2/go.mod h1:IefJDi7uJpflLs2wEnkiier/Y21w+dEbOrMCP5LB2aw=
+github.com/pb33f/libopenapi-validator v0.4.7 h1:sS6RvphkhlgMdad4WutRVd/yzNu/7QE4RdUTjxp0dY4=
+github.com/pb33f/libopenapi-validator v0.4.7/go.mod h1:0G2+HeGK4Oc0ugTG+npGVHVCOPVlc60Bj4ZbVW7B+Dc=
+github.com/pb33f/ordered-map/v2 v2.2.0 h1:+6D6e0nkcEjVPh6kF48ynz2Cb+D/ECH/Q3AOunHtj7E=
+github.com/pb33f/ordered-map/v2 v2.2.0/go.mod h1:rAwLzJPAha8J3pY5otLGRbGH2L077wij3W/ftbgPwNs=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/peterhellberg/link v1.1.0 h1:s2+RH8EGuI/mI4QwrWGSYQCRz7uNgip9BaM04HKu5kc=
 github.com/peterhellberg/link v1.1.0/go.mod h1:gtSlOT4jmkY8P47hbTc8PTgiDDWpdPbFYl75keYyBB8=
+github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
+github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4=
+github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -855,8 +962,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
-github.com/projectcontour/contour v1.32.0 h1:mOy8JEa6im5FOToJJugVVJYVcNp/mkb72H3ymdTg1PM=
-github.com/projectcontour/contour v1.32.0/go.mod h1:e7mw7MqbQeyv+PYnD7CbdRTtQbBfvGYyTrEvmwSKVnk=
+github.com/projectcontour/contour v1.32.1 h1:o6dD3xEJX+mT0Rt5ljelxJQa6pZCOdIQCmc5byKQURM=
+github.com/projectcontour/contour v1.32.1/go.mod h1:e05evdFqFmaCkPct0glXbw7zXlIFWKXg3xfN7dHsi4E=
 github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
@@ -864,8 +971,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
 github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4=
-github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
-github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -882,8 +989,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -892,14 +999,22 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
 github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
-github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
+github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
+github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
+github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
+github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
+github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
+github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
+github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA=
+github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
-github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -913,15 +1028,25 @@ github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3/go.mod h1:rtQlp
 github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
+github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
+github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU=
+github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 h1:48+VFHsyVcAHIN2v1Ao9v1/RkjJS5AwctFucBrfYNIA=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34/go.mod h1:zFWiHphneiey3s8HOtAEnGrRlWivNaxW5T6d5Xfco7g=
 github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c=
 github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
+github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
 github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
 github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
@@ -948,25 +1073,40 @@ github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
 github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
 github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
 github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U=
+github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoAtcEbqXQ=
+github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
+github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
+github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 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.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 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=
 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
 github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@@ -989,6 +1129,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
@@ -1001,6 +1143,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
 github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
 github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
+github.com/tliron/commonlog v0.2.19 h1:v1mOH1TyzFLqkshR03khw7ENAZPjAyZTQBQrqN+vX9c=
+github.com/tliron/commonlog v0.2.19/go.mod h1:AcdhfcUqlAWukDrzTGyaPhUgYiNdZhS4dKzD/e0tjcY=
+github.com/tliron/glsp v0.2.2 h1:IKPfwpE8Lu8yB6Dayta+IyRMAbTVunudeauEgjXBt+c=
+github.com/tliron/glsp v0.2.2/go.mod h1:GMVWDNeODxHzmDPvYbYTCs7yHVaEATfYtXiYJ9w1nBg=
+github.com/tliron/kutil v0.3.26 h1:G+dicQLvzm3zdOMrrQFLBfHJXtk57fEu2kf1IFNyJxw=
+github.com/tliron/kutil v0.3.26/go.mod h1:1/HRVAb+fnRIRnzmhu0FPP+ZJKobrpwHStDVMuaXDzY=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550=
@@ -1036,6 +1184,9 @@ github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtX
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8=
+github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
@@ -1050,12 +1201,12 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
-go.etcd.io/etcd/api/v3 v3.6.2 h1:25aCkIMjUmiiOtnBIp6PhNj4KdcURuBak0hU2P1fgRc=
-go.etcd.io/etcd/api/v3 v3.6.2/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
-go.etcd.io/etcd/client/pkg/v3 v3.6.2 h1:zw+HRghi/G8fKpgKdOcEKpnBTE4OO39T6MegA0RopVU=
-go.etcd.io/etcd/client/pkg/v3 v3.6.2/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
-go.etcd.io/etcd/client/v3 v3.6.2 h1:RgmcLJxkpHqpFvgKNwAQHX3K+wsSARMXKgjmUSpoSKQ=
-go.etcd.io/etcd/client/v3 v3.6.2/go.mod h1:PL7e5QMKzjybn0FosgiWvCUDzvdChpo5UgGR4Sk4Gzc=
+go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
+go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
+go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
+go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
+go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
+go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
 go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
@@ -1123,16 +1274,21 @@ 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
-golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
-golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
 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=
 golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -1152,8 +1308,11 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
-golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
+golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
 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=
@@ -1193,9 +1352,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
-golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
 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=
@@ -1213,6 +1374,10 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
 golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1254,6 +1419,7 @@ golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1261,27 +1427,39 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
-golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 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-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
-golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
-golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
+golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
 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=
@@ -1291,9 +1469,12 @@ 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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
-golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
 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=
@@ -1336,8 +1517,10 @@ golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
-golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
+golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
 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=
@@ -1350,8 +1533,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.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
-google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
+google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=
+google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
 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=
@@ -1365,12 +1548,12 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn
 google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
-google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
-google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
 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=
@@ -1384,8 +1567,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.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
@@ -1395,8 +1578,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
-google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
+google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1452,23 +1635,25 @@ 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.26.2 h1:gLkGSB2nkqA/9u/tE/OMEv+U4Fhci2JZgIqjA0CxMak=
-istio.io/api v1.26.2/go.mod h1:DTVGH6CLXj5W8FF9JUD3Tis78iRgT1WeuAnxfTz21Wg=
-istio.io/client-go v1.26.2 h1:XWvQzBM69vB2xo1bzrp+CYbE8KlRAPotR4ls5Vv35EM=
-istio.io/client-go v1.26.2/go.mod h1:eAImguSJPdaDiSSS2CEsywNHE8WWfqd3WfS18Rj8ynI=
+istio.io/api v1.27.0 h1:KU1DeyIvZkY2k0pF9ZY+6D0ZVB1ZgvimaPkM55NVRWk=
+istio.io/api v1.27.0/go.mod h1:DTVGH6CLXj5W8FF9JUD3Tis78iRgT1WeuAnxfTz21Wg=
+istio.io/client-go v1.27.0 h1:G6FjXoebOpnYbgcGnBWa0I2TjOI8HJe6QydTGVPOYME=
+istio.io/client-go v1.27.0/go.mod h1:oUPY27HFv9fW32NtjxlgrRaa0dPIN6jYj/xGcjorLA0=
 k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8=
 k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78=
 k8s.io/api v0.18.4/go.mod h1:lOIQAKYgai1+vz9J7YcDZwC26Z0zQewYOGWdyIPUUQ4=
-k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8=
-k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE=
+k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk=
+k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc=
 k8s.io/apiextensions-apiserver v0.18.0/go.mod h1:18Cwn1Xws4xnWQNC00FLq1E350b9lUF+aOdIWDOZxgo=
 k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY=
 k8s.io/apiextensions-apiserver v0.18.4/go.mod h1:NYeyeYq4SIpFlPxSAB6jHPIdvu3hL0pc36wuRChybio=
+k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs=
+k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8=
 k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
 k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
 k8s.io/apimachinery v0.18.4/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
-k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
-k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
+k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s=
+k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
 k8s.io/apiserver v0.18.0/go.mod h1:3S2O6FeBBd6XTo0njUrLxiqk8GNy6wWOftjhJcXYnjw=
 k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw=
 k8s.io/apiserver v0.18.4/go.mod h1:q+zoFct5ABNnYkGIaGQ3bcbUNdmPyOCoEBcg51LChY8=
@@ -1477,8 +1662,8 @@ k8s.io/cli-runtime v0.18.4/go.mod h1:9/hS/Cuf7NVzWR5F/5tyS6xsnclxoPLVtwhnkJG1Y4g
 k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8=
 k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU=
 k8s.io/client-go v0.18.4/go.mod h1:f5sXwL4yAZRkAtzOxRWUhA/N8XzGCb+nPZI8PfobZ9g=
-k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA=
-k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
+k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw=
+k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY=
 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=
diff --git a/internal/idna/idna.go b/internal/idna/idna.go
new file mode 100644
index 00000000..9290e8a5
--- /dev/null
+++ b/internal/idna/idna.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package idna
+
+import (
+	"golang.org/x/net/idna"
+)
+
+var (
+	Profile = idna.New(
+		idna.MapForLookup(),
+		idna.Transitional(true),
+		idna.StrictDomainName(false),
+	)
+)
diff --git a/kustomize/kustomization.yaml b/kustomize/kustomization.yaml
index 395b0123..e268e7f8 100644
--- a/kustomize/kustomization.yaml
+++ b/kustomize/kustomization.yaml
@@ -3,7 +3,7 @@ kind: Kustomization
 
 images:
   - name: registry.k8s.io/external-dns/external-dns
-    newTag: v0.18.0
+    newTag: v0.19.0
 
 resources:
   - ./external-dns-deployment.yaml
diff --git a/mkdocs.yml b/mkdocs.yml
index be469143..2f0b588f 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -27,6 +27,7 @@ nav:
     - DynamoDB: docs/registry/dynamodb.md
   - Advanced Topics:
     - Initial Design: docs/initial-design.md
+    - Kubernetes Events: docs/advanced/events.md
     - Leader Election: docs/proposal/001-leader-election.md
     - Monitoring: docs/monitoring/*
     - MultiTarget: docs/proposal/multi-target.md
@@ -116,5 +117,5 @@ plugins:
       j2_block_end_string: '%]]'
       j2_variable_start_string: '[['
       j2_variable_end_string: ']]'
-      j2_comment_start_string: '[#'
-      j2_comment_end_string: '#]'
+      j2_comment_start_string: '[[#'
+      j2_comment_end_string: '#]]'
diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go
index ea809d3a..9df6218d 100644
--- a/pkg/apis/externaldns/types.go
+++ b/pkg/apis/externaldns/types.go
@@ -212,7 +212,9 @@ type Config struct {
 	TraefikDisableNew                             bool
 	NAT64Networks                                 []string
 	ExcludeUnschedulable                          bool
+	EmitEvents                                    []string
 	ForceDefaultTargets                           bool
+	sourceWrappers                                map[string]bool // map of source wrappers, e.g. "targetfilter", "nat64"
 }
 
 var defaultConfig = &Config{
@@ -271,6 +273,7 @@ var defaultConfig = &Config{
 	ExcludeDNSRecordTypes:        []string{},
 	ExcludeDomains:               []string{},
 	ExcludeTargetNets:            []string{},
+	EmitEvents:                   []string{},
 	ExcludeUnschedulable:         true,
 	ExoscaleAPIEnvironment:       "api",
 	ExoscaleAPIKey:               "",
@@ -376,6 +379,7 @@ var defaultConfig = &Config{
 	WebhookServer:                false,
 	ZoneIDFilter:                 []string{},
 	ForceDefaultTargets:          false,
+	sourceWrappers:               map[string]bool{},
 }
 
 // NewConfig returns new Config object
@@ -427,6 +431,22 @@ func (cfg *Config) ParseFlags(args []string) error {
 	return nil
 }
 
+func (cfg *Config) AddSourceWrapper(name string) {
+	if cfg.sourceWrappers == nil {
+		cfg.sourceWrappers = make(map[string]bool)
+	}
+	cfg.sourceWrappers[name] = true
+}
+
+// IsSourceWrapperInstrumented returns whether a source wrapper is enabled or not.
+func (cfg *Config) IsSourceWrapperInstrumented(name string) bool {
+	if cfg.sourceWrappers == nil {
+		return false
+	}
+	_, ok := cfg.sourceWrappers[name]
+	return ok
+}
+
 func App(cfg *Config) *kingpin.Application {
 	app := kingpin.New("external-dns", "ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.\n\nNote that all flags may be replaced with env vars - `--flag` -> `EXTERNAL_DNS_FLAG=1` or `--flag value` -> `EXTERNAL_DNS_FLAG=value`")
 	app.Version(Version)
@@ -488,6 +508,8 @@ func App(cfg *Config) *kingpin.Application {
 	app.Flag("traefik-enable-legacy", "Enable legacy listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikEnableLegacy)).BoolVar(&cfg.TraefikEnableLegacy)
 	app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew)
 
+	app.Flag("events-emit", "Events that should be emitted. Specify multiple times for multiple events support (optional, default: none, expected: RecordReady, RecordDeleted, RecordError)").Default(defaultConfig.EmitEvents...).StringsVar(&cfg.EmitEvents)
+
 	// Flags related to providers
 	providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "transip", "webhook"}
 	app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...)
@@ -504,7 +526,7 @@ func App(cfg *Config) *kingpin.Application {
 	app.Flag("google-zone-visibility", "When using the Google provider, filter for zones with this visibility (optional, options: public, private)").Default(defaultConfig.GoogleZoneVisibility).EnumVar(&cfg.GoogleZoneVisibility, "", "public", "private")
 	app.Flag("alibaba-cloud-config-file", "When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud)").Default(defaultConfig.AlibabaCloudConfigFile).StringVar(&cfg.AlibabaCloudConfigFile)
 	app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private")
-	app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
+	app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, default: any, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
 	app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter)
 	app.Flag("aws-profile", "When using the AWS provider, name of the profile to use").Default("").StringsVar(&cfg.AWSProfiles)
 	app.Flag("aws-assume-role", "When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole)
diff --git a/pkg/events/OWNERS b/pkg/events/OWNERS
new file mode 100644
index 00000000..128025e2
--- /dev/null
+++ b/pkg/events/OWNERS
@@ -0,0 +1,4 @@
+# See the OWNERS docs at https://go.k8s.io/owners
+
+labels:
+- events
diff --git a/pkg/events/controller.go b/pkg/events/controller.go
new file mode 100644
index 00000000..9c13fa0f
--- /dev/null
+++ b/pkg/events/controller.go
@@ -0,0 +1,182 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package events
+
+import (
+	"context"
+	"os"
+
+	log "github.com/sirupsen/logrus"
+	eventsv1 "k8s.io/api/events/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apimachinery/pkg/util/wait"
+	v1 "k8s.io/client-go/kubernetes/typed/events/v1"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/util/workqueue"
+)
+
+const (
+	workers          = 1
+	controllerName   = "external-dns"
+	maxTriesPerEvent = 3
+	maxQueuedEvents  = 100
+)
+
+type EventEmitter interface {
+	Add(...Event)
+}
+
+type Controller struct {
+	client          v1.EventsV1Interface
+	queue           workqueue.TypedRateLimitingInterface[any]
+	emitEvents      sets.Set[Reason]
+	maxQueuedEvents int
+	dryRun          bool
+	hostname        string
+}
+
+func NewEventController(cfg *Config) (*Controller, error) {
+	queue := workqueue.NewTypedRateLimitingQueueWithConfig[any](
+		workqueue.DefaultTypedControllerRateLimiter[any](),
+		workqueue.TypedRateLimitingQueueConfig[any]{Name: controllerName},
+	)
+	// TODO: to externalize this as similar to source.GetRestConfig
+	// TODO: instrument with metrics
+	rConfig, err := GetRestConfig(cfg.kubeConfig, cfg.apiServerURL)
+	if err != nil {
+		return nil, err
+	}
+	rConfig.Timeout = cfg.timeout
+
+	client, err := v1.NewForConfig(rConfig)
+	if err != nil {
+		return nil, err
+	}
+	hostname, _ := os.Hostname()
+	return &Controller{
+		client:          client,
+		queue:           queue,
+		emitEvents:      cfg.emitEvents,
+		maxQueuedEvents: maxQueuedEvents,
+		dryRun:          cfg.dryRun,
+		hostname:        hostname,
+	}, nil
+}
+
+func (ec *Controller) Run(ctx context.Context) {
+	if len(ec.emitEvents) == 0 {
+		return
+	}
+	go ec.run(ctx)
+}
+
+func (ec *Controller) run(ctx context.Context) {
+	log.Info("event Controller started")
+	defer log.Info("event Controller terminated")
+	defer utilruntime.HandleCrash()
+	var waitGroup wait.Group
+	for i := 0; i < workers; i++ {
+		waitGroup.StartWithContext(ctx, func(ctx context.Context) {
+			for ec.processNextWorkItem(ctx) {
+			}
+		})
+	}
+	<-ctx.Done()
+	ec.queue.ShutDownWithDrain()
+	waitGroup.Wait()
+}
+
+func (ec *Controller) processNextWorkItem(ctx context.Context) bool {
+	key, quit := ec.queue.Get()
+	if quit {
+		return false
+	}
+	defer ec.queue.Done(key)
+	event, ok := key.(*eventsv1.Event)
+	if !ok {
+		log.Errorf("failed to convert key to Event: %q", key)
+		return true
+	}
+	var dryRun []string
+	if ec.dryRun {
+		dryRun = []string{metav1.DryRunAll}
+	}
+	_, err := ec.client.Events(event.Namespace).Create(ctx, event, metav1.CreateOptions{
+		DryRun: dryRun,
+	})
+	if err != nil && !apierrors.IsNotFound(err) {
+		if ec.queue.NumRequeues(key) < maxTriesPerEvent {
+			log.Errorf("not able to create event, retrying for key/%s. %v", key, err)
+			return true
+		}
+		log.Errorf("dropping event %s/%s with key/%q after %d retries. %v", event.Namespace, event.Name, key, ec.queue.NumRequeues(key), err)
+	}
+	ec.queue.Forget(key)
+	return true
+}
+
+func (ec *Controller) Add(events ...Event) {
+	if ec.queue.Len() >= ec.maxQueuedEvents {
+		log.Warnf("event queue is full, dropping %d events", len(events))
+		return
+	}
+	for _, e := range events {
+		event := e.event()
+		if event == nil {
+			continue
+		}
+		ec.emit(event)
+	}
+}
+
+func (ec *Controller) emit(event *eventsv1.Event) {
+	if !ec.emitEvents.Has(Reason(event.Reason)) {
+		log.Debugf("skipping event %s/%s/%s with reason %s as not configured to emit", event.Kind, event.Namespace, event.Name, event.Reason)
+		return
+	}
+	event.ReportingController = controllerName + "-" + ec.hostname
+	ec.queue.Add(event)
+}
+
+// GetRestConfig TODO: copy of source.GetRestConfig, consider moving to a common package
+func GetRestConfig(kubeConfig, apiServerURL string) (*rest.Config, error) {
+	if kubeConfig == "" {
+		if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil {
+			kubeConfig = clientcmd.RecommendedHomeFile
+		}
+	}
+
+	var (
+		config *rest.Config
+		err    error
+	)
+	if kubeConfig == "" {
+		log.Debug("Using inCluster-config based on serviceaccount-token")
+		config, err = rest.InClusterConfig()
+	} else {
+		log.Debug("Using kubeConfig")
+		config, err = clientcmd.BuildConfigFromFlags(apiServerURL, kubeConfig)
+	}
+	if err != nil {
+		return nil, err
+	}
+	return config, nil
+}
diff --git a/pkg/events/fake/fake.go b/pkg/events/fake/fake.go
new file mode 100644
index 00000000..ab0bd550
--- /dev/null
+++ b/pkg/events/fake/fake.go
@@ -0,0 +1,37 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fake
+
+import (
+	"github.com/stretchr/testify/mock"
+
+	"sigs.k8s.io/external-dns/pkg/events"
+)
+
+type EventEmitter struct {
+	mock.Mock
+}
+
+func (m *EventEmitter) Add(events ...events.Event) {
+	m.Called(events[0])
+}
+
+func NewFakeEventEmitter() *EventEmitter {
+	m := &EventEmitter{}
+	m.On("Add", mock.AnythingOfType("events.Event"))
+	return m
+}
diff --git a/pkg/events/types.go b/pkg/events/types.go
new file mode 100644
index 00000000..17c3ea39
--- /dev/null
+++ b/pkg/events/types.go
@@ -0,0 +1,235 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package events
+
+import (
+	"fmt"
+	"regexp"
+	"slices"
+	"strings"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	apiv1 "k8s.io/api/core/v1"
+	eventsv1 "k8s.io/api/events/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/sets"
+	runtime "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const (
+	ActionCreate  Action = "Created"
+	ActionUpdate  Action = "Updated"
+	ActionDelete  Action = "Deleted"
+	ActionFailed  Action = "FailedSync"
+	RecordReady   Reason = "RecordReady"
+	RecordDeleted Reason = "RecordDeleted"
+	RecordError   Reason = "RecordError"
+
+	EventTypeNormal  EventType = EventType(apiv1.EventTypeNormal)
+	EventTypeWarning EventType = EventType(apiv1.EventTypeWarning)
+)
+
+var (
+	invalidChars           = regexp.MustCompile(`[^a-z0-9.\-]`)
+	startsWithAlphaNumeric = regexp.MustCompile(`^[a-z0-9]`)
+	endsWithAlphaNumeric   = regexp.MustCompile(`[a-z0-9]$`)
+)
+
+type (
+	// Action values for actions
+	Action string
+	// Reason types of Event Reasons
+	Reason string
+	// EventType values for event types
+	EventType    string
+	ConfigOption func(*Config)
+
+	Event struct {
+		ref     ObjectReference
+		message string
+		source  string
+		action  Action
+		eType   EventType
+		reason  Reason
+	}
+
+	ObjectReference struct {
+		Kind       string
+		ApiVersion string
+		Namespace  string
+		Name       string
+		UID        types.UID
+		Source     string
+	}
+
+	Config struct {
+		kubeConfig   string
+		apiServerURL string
+		timeout      time.Duration
+		emitEvents   sets.Set[Reason]
+		dryRun       bool
+	}
+)
+
+func NewObjectReference(obj runtime.Object, source string) *ObjectReference {
+	return &ObjectReference{
+		Kind:       obj.GetObjectKind().GroupVersionKind().Kind,
+		ApiVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
+		Namespace:  obj.GetNamespace(),
+		Name:       obj.GetName(),
+		UID:        obj.GetUID(),
+		Source:     source,
+	}
+}
+
+func NewEvent(obj *ObjectReference, msg string, a Action, r Reason) Event {
+	if obj == nil {
+		return Event{}
+	}
+	return Event{
+		ref:     *obj,
+		message: msg,
+		eType:   EventTypeNormal,
+		action:  a,
+		reason:  r,
+		source:  obj.Source,
+	}
+}
+
+func (e *Event) description() string {
+	return fmt.Sprintf("%s/%s/%s", e.ref.Kind, e.ref.Namespace, e.ref.Name)
+}
+
+func (e *Event) Action() Action {
+	return e.action
+}
+
+func (e *Event) Reason() Reason {
+	return e.reason
+}
+
+func (e *Event) EventType() EventType {
+	return e.eType
+}
+
+func (e *Event) event() *eventsv1.Event {
+	if e.ref.Name == "" {
+		log.Debug("skipping event for resources as the name is not generated yet")
+		return nil
+	}
+	message := e.message
+	// https://github.com/kubernetes/api/blob/7da28ad7db85e33ab8be3b89e63cad4c07b37fb2/events/v1/types.go#L77
+	if len(message) > 1024 {
+		message = message[0:1021] + "..."
+	}
+
+	timestamp := metav1.MicroTime{Time: time.Now()}
+
+	event := &eventsv1.Event{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      sanitize(e.ref.Name),
+			Namespace: e.ref.Namespace,
+		},
+		EventTime:           timestamp,
+		ReportingInstance:   controllerName + "/source/" + e.ref.Source,
+		ReportingController: controllerName,
+		Action:              string(e.action),
+		Reason:              string(e.reason),
+		Note:                message,
+		Type:                string(e.eType),
+	}
+	if e.ref.UID != "" {
+		ref := e.ref.objectRef()
+		event.Related = ref
+		event.Regarding = *ref
+	}
+	return event
+}
+
+// Sanitize input to comply with RFC 1123 subdomain naming requirements
+func sanitize(input string) string {
+	t := metav1.Time{Time: time.Now()}
+	if input == "" {
+		return fmt.Sprintf("a.%x", t.UnixNano())
+	}
+	sanitized := invalidChars.ReplaceAllString(strings.ToLower(input), "-")
+
+	// the name should start with an alphanumeric character
+	if len(sanitized) > 0 && !startsWithAlphaNumeric.MatchString(sanitized) {
+		sanitized = "a" + sanitized
+	}
+
+	// the name should end with an alphanumeric character
+	if len(sanitized) > 0 && !endsWithAlphaNumeric.MatchString(sanitized) {
+		sanitized = sanitized + "z"
+	}
+
+	sanitized = invalidChars.ReplaceAllString(sanitized, "-")
+
+	return fmt.Sprintf("%v.%x", sanitized, t.UnixNano())
+}
+
+func WithKubeConfig(kubeConfig string, url string, timeout time.Duration) ConfigOption {
+	return func(c *Config) {
+		c.kubeConfig = kubeConfig
+		c.apiServerURL = url
+		c.timeout = timeout
+	}
+}
+
+func WithDryRun(dryRun bool) ConfigOption {
+	return func(c *Config) {
+		c.dryRun = dryRun
+	}
+}
+
+func WithEmitEvents(events []string) ConfigOption {
+	return func(c *Config) {
+		if len(events) > 0 {
+			c.emitEvents = sets.New[Reason]()
+			for _, event := range events {
+				if slices.Contains([]string{string(RecordReady), string(RecordError)}, event) {
+					c.emitEvents.Insert(Reason(event))
+				}
+			}
+		}
+	}
+}
+
+func NewConfig(opts ...ConfigOption) *Config {
+	c := &Config{}
+	for _, opt := range opts {
+		opt(c)
+	}
+	return c
+}
+
+func (c *Config) IsEnabled() bool {
+	return len(c.emitEvents) > 0
+}
+
+func (r *ObjectReference) objectRef() *apiv1.ObjectReference {
+	return &apiv1.ObjectReference{
+		Kind:       r.Kind,
+		Namespace:  r.Namespace,
+		Name:       r.Name,
+		UID:        r.UID,
+		APIVersion: r.ApiVersion,
+	}
+}
diff --git a/pkg/http/http.go b/pkg/http/http.go
index b6069f1a..c52d30a8 100644
--- a/pkg/http/http.go
+++ b/pkg/http/http.go
@@ -21,14 +21,15 @@ package http
 import (
 	"fmt"
 	"net/http"
-	"strings"
 	"time"
 
 	"github.com/prometheus/client_golang/prometheus"
+
+	"sigs.k8s.io/external-dns/pkg/metrics"
 )
 
 var (
-	requestDuration = prometheus.NewSummaryVec(
+	RequestDurationMetric = metrics.NewSummaryVecWithOpts(
 		prometheus.SummaryOpts{
 			Name:        "request_duration_seconds",
 			Help:        "The HTTP request latencies in seconds.",
@@ -36,12 +37,12 @@ var (
 			ConstLabels: prometheus.Labels{"handler": "instrumented_http"},
 			Objectives:  map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
 		},
-		[]string{"scheme", "host", "path", "method", "status"},
+		[]string{metrics.LabelScheme, metrics.LabelHost, metrics.LabelPath, metrics.LabelMethod, metrics.LabelStatus},
 	)
 )
 
 func init() {
-	prometheus.MustRegister(requestDuration)
+	metrics.RegisterMetric.MustRegister(RequestDurationMetric)
 }
 
 type CustomRoundTripper struct {
@@ -62,14 +63,14 @@ func (r *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
 		status = fmt.Sprintf("%d", resp.StatusCode)
 	}
 
-	labels := prometheus.Labels{
-		"scheme": req.URL.Scheme,
-		"host":   req.URL.Host,
-		"path":   pathProcessor(req.URL.Path),
-		"method": req.Method,
-		"status": status,
-	}
-	requestDuration.With(labels).Observe(time.Since(start).Seconds())
+	RequestDurationMetric.SetWithLabels(time.Since(start).Seconds(), metrics.Labels{
+		metrics.LabelScheme: req.URL.Scheme,
+		metrics.LabelHost:   req.URL.Host,
+		metrics.LabelPath:   metrics.PathProcessor(req.URL.Path),
+		metrics.LabelMethod: req.Method,
+		metrics.LabelStatus: status,
+	})
+
 	return resp, err
 }
 
@@ -90,8 +91,3 @@ func NewInstrumentedTransport(next http.RoundTripper) http.RoundTripper {
 
 	return &CustomRoundTripper{next: next}
 }
-
-func pathProcessor(path string) string {
-	parts := strings.Split(path, "/")
-	return parts[len(parts)-1]
-}
diff --git a/pkg/metrics/labels.go b/pkg/metrics/labels.go
new file mode 100644
index 00000000..bf16550f
--- /dev/null
+++ b/pkg/metrics/labels.go
@@ -0,0 +1,31 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package metrics
+
+import (
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+const (
+	LabelScheme = "scheme"
+	LabelHost   = "host"
+	LabelPath   = "path"
+	LabelMethod = "method"
+	LabelStatus = "status"
+)
+
+type Labels = prometheus.Labels
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 73e6ea5d..93bf4ace 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -73,7 +73,7 @@ func NewMetricsRegister() *MetricRegistry {
 //	}
 func (m *MetricRegistry) MustRegister(cs IMetric) {
 	switch v := cs.(type) {
-	case CounterMetric, GaugeMetric, CounterVecMetric, GaugeVecMetric, GaugeFuncMetric:
+	case CounterMetric, GaugeMetric, SummaryVecMetric, CounterVecMetric, GaugeVecMetric, GaugeFuncMetric:
 		if _, exists := m.mName[cs.Get().FQDN]; exists {
 			return
 		} else {
@@ -85,6 +85,8 @@ func (m *MetricRegistry) MustRegister(cs IMetric) {
 			m.Registerer.MustRegister(metric.Counter)
 		case GaugeMetric:
 			m.Registerer.MustRegister(metric.Gauge)
+		case SummaryVecMetric:
+			m.Registerer.MustRegister(metric.SummaryVec)
 		case GaugeVecMetric:
 			m.Registerer.MustRegister(metric.Gauge)
 		case CounterVecMetric:
diff --git a/pkg/metrics/models.go b/pkg/metrics/models.go
index 8c8fb4d4..9998fac1 100644
--- a/pkg/metrics/models.go
+++ b/pkg/metrics/models.go
@@ -176,3 +176,36 @@ func NewGaugeFuncMetric(opts prometheus.GaugeOpts) GaugeFuncMetric {
 		GaugeFunc: prometheus.NewGaugeFunc(opts, func() float64 { return 1 }),
 	}
 }
+
+type SummaryVecMetric struct {
+	Metric
+	SummaryVec prometheus.SummaryVec
+}
+
+func (s SummaryVecMetric) Get() *Metric {
+	return &s.Metric
+}
+
+func (s SummaryVecMetric) SetWithLabels(value float64, labels prometheus.Labels) {
+	s.SummaryVec.With(labels).Observe(value)
+}
+
+func NewSummaryVecWithOpts(opts prometheus.SummaryOpts, labels []string) SummaryVecMetric {
+	opts.Namespace = Namespace
+	return SummaryVecMetric{
+		Metric: Metric{
+			Type:      "summaryVec",
+			Name:      opts.Name,
+			FQDN:      fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
+			Namespace: opts.Namespace,
+			Subsystem: opts.Subsystem,
+			Help:      opts.Help,
+		},
+		SummaryVec: *prometheus.NewSummaryVec(opts, labels),
+	}
+}
+
+func PathProcessor(path string) string {
+	parts := strings.Split(path, "/")
+	return parts[len(parts)-1]
+}
diff --git a/plan/plan.go b/plan/plan.go
index 8061a5b3..29962377 100644
--- a/plan/plan.go
+++ b/plan/plan.go
@@ -22,10 +22,11 @@ import (
 	"strings"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	log "github.com/sirupsen/logrus"
-	"golang.org/x/net/idna"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/internal/idna"
 )
 
 // PropertyComparator is used in Plan for comparing the previous and current custom annotations.
@@ -160,7 +161,7 @@ func (c *Changes) HasChanges() bool {
 	if len(c.Create) > 0 || len(c.Delete) > 0 {
 		return true
 	}
-	return !cmp.Equal(c.UpdateNew, c.UpdateOld)
+	return !cmp.Equal(c.UpdateNew, c.UpdateOld, cmpopts.IgnoreUnexported(endpoint.Endpoint{}))
 }
 
 // Calculate computes the actions needed to move current state towards desired
@@ -340,16 +341,10 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Ma
 	return filtered
 }
 
-var idnaProfile = idna.New(
-	idna.MapForLookup(),
-	idna.Transitional(true),
-	idna.StrictDomainName(false),
-)
-
 // normalizeDNSName converts a DNS name to a canonical form, so that we can use string equality
 // it: removes space, get ASCII version of dnsName complient with Section 5 of RFC 5891, ensures there is a trailing dot
 func normalizeDNSName(dnsName string) string {
-	s, err := idnaProfile.ToASCII(strings.TrimSpace(dnsName))
+	s, err := idna.Profile.ToASCII(strings.TrimSpace(dnsName))
 	if err != nil {
 		log.Warnf(`Got error while parsing DNSName %s: %v`, dnsName, err)
 	}
diff --git a/provider/aws/aws.go b/provider/aws/aws.go
index 1ddb35cc..873d860d 100644
--- a/provider/aws/aws.go
+++ b/provider/aws/aws.go
@@ -734,56 +734,59 @@ func (p *AWSProvider) submitChanges(ctx context.Context, changes Route53Changes,
 				log.Infof("Desired change: %s %s %s", c.Action, *c.ResourceRecordSet.Name, c.ResourceRecordSet.Type)
 			}
 
-			if !p.dryRun {
-				params := &route53.ChangeResourceRecordSetsInput{
-					HostedZoneId: aws.String(z),
-					ChangeBatch: &route53types.ChangeBatch{
-						Changes: b.Route53Changes(),
-					},
-				}
+			if p.dryRun {
+				log.Debug("Dry run mode, skipping change submission")
+				continue
+			}
 
-				successfulChanges := 0
+			params := &route53.ChangeResourceRecordSetsInput{
+				HostedZoneId: aws.String(z),
+				ChangeBatch: &route53types.ChangeBatch{
+					Changes: b.Route53Changes(),
+				},
+			}
 
-				client := p.clients[zones[z].profile]
-				if _, err := client.ChangeResourceRecordSets(ctx, params); err != nil {
-					log.Errorf("Failure in zone %s when submitting change batch: %v", *zones[z].zone.Name, err)
+			successfulChanges := 0
 
-					changesByOwnership := groupChangesByNameAndOwnershipRelation(b)
+			client := p.clients[zones[z].profile]
+			if _, err := client.ChangeResourceRecordSets(ctx, params); err != nil {
+				log.Errorf("Failure in zone %s when submitting change batch: %v", *zones[z].zone.Name, err)
 
-					if len(changesByOwnership) > 1 {
-						log.Debug("Trying to submit change sets one-by-one instead")
-						for _, changes := range changesByOwnership {
-							if log.Logger.IsLevelEnabled(debugLevel) {
-								for _, c := range changes {
-									log.Debugf("Desired change: %s %s %s", c.Action, *c.ResourceRecordSet.Name, c.ResourceRecordSet.Type)
-								}
-							}
-							params.ChangeBatch = &route53types.ChangeBatch{
-								Changes: changes.Route53Changes(),
-							}
-							if _, err := client.ChangeResourceRecordSets(ctx, params); err != nil {
-								failedUpdate = true
-								log.Errorf("Failed submitting change (error: %v), it will be retried in a separate change batch in the next iteration", err)
-								p.failedChangesQueue[z] = append(p.failedChangesQueue[z], changes...)
-							} else {
-								successfulChanges = successfulChanges + len(changes)
+				changesByOwnership := groupChangesByNameAndOwnershipRelation(b)
+
+				if len(changesByOwnership) > 1 {
+					log.Debug("Trying to submit change sets one-by-one instead")
+					for _, changes := range changesByOwnership {
+						if log.Logger.IsLevelEnabled(debugLevel) {
+							for _, c := range changes {
+								log.Debugf("Desired change: %s %s %s", c.Action, *c.ResourceRecordSet.Name, c.ResourceRecordSet.Type)
 							}
 						}
-					} else {
-						failedUpdate = true
+						params.ChangeBatch = &route53types.ChangeBatch{
+							Changes: changes.Route53Changes(),
+						}
+						if _, err := client.ChangeResourceRecordSets(ctx, params); err != nil {
+							failedUpdate = true
+							log.Errorf("Failed submitting change (error: %v), it will be retried in a separate change batch in the next iteration", err)
+							p.failedChangesQueue[z] = append(p.failedChangesQueue[z], changes...)
+						} else {
+							successfulChanges = successfulChanges + len(changes)
+						}
 					}
 				} else {
-					successfulChanges = len(b)
+					failedUpdate = true
 				}
+			} else {
+				successfulChanges = len(b)
+			}
 
-				if successfulChanges > 0 {
-					// z is the R53 Hosted Zone ID already as aws.StringValue
-					log.Infof("%d record(s) were successfully updated", successfulChanges)
-				}
+			if successfulChanges > 0 {
+				// z is the R53 Hosted Zone ID already as aws.StringValue
+				log.Infof("%d record(s) were successfully updated", successfulChanges)
+			}
 
-				if i != len(batchCs)-1 {
-					time.Sleep(p.batchChangeInterval)
-				}
+			if i != len(batchCs)-1 {
+				time.Sleep(p.batchChangeInterval)
 			}
 		}
 
diff --git a/provider/aws/config.go b/provider/aws/config.go
index ecc53c90..7ea2d49c 100644
--- a/provider/aws/config.go
+++ b/provider/aws/config.go
@@ -19,17 +19,15 @@ package aws
 import (
 	"context"
 	"fmt"
-	"net/http"
 
 	awsv2 "github.com/aws/aws-sdk-go-v2/aws"
+
 	"github.com/aws/aws-sdk-go-v2/aws/retry"
 	"github.com/aws/aws-sdk-go-v2/config"
 	stscredsv2 "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
 	"github.com/aws/aws-sdk-go-v2/service/sts"
 	"github.com/sirupsen/logrus"
 
-	extdnshttp "sigs.k8s.io/external-dns/pkg/http"
-
 	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
 )
 
@@ -84,8 +82,8 @@ func newV2Config(awsConfig AWSSessionConfig) (awsv2.Config, error) {
 		config.WithRetryer(func() awsv2.Retryer {
 			return retry.AddWithMaxAttempts(retry.NewStandard(), awsConfig.APIRetries)
 		}),
-		config.WithHTTPClient(extdnshttp.NewInstrumentedClient(&http.Client{})),
 		config.WithSharedConfigProfile(awsConfig.Profile),
+		config.WithAPIOptions(GetInstrumentationMiddlewares()),
 	}
 
 	cfg, err := config.LoadDefaultConfig(context.Background(), defaultOpts...)
diff --git a/provider/aws/instrumented_config.go b/provider/aws/instrumented_config.go
new file mode 100644
index 00000000..8dceed6a
--- /dev/null
+++ b/provider/aws/instrumented_config.go
@@ -0,0 +1,98 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/aws/smithy-go/middleware"
+	smithyhttp "github.com/aws/smithy-go/transport/http"
+
+	extdnshttp "sigs.k8s.io/external-dns/pkg/http"
+	"sigs.k8s.io/external-dns/pkg/metrics"
+)
+
+type requestMetrics struct {
+	StartTime time.Time
+}
+
+type requestMetricsKey struct{}
+
+func getRequestMetric(ctx context.Context) requestMetrics {
+	requestMetrics, _ := middleware.GetStackValue(ctx, requestMetricsKey{}).(requestMetrics)
+	return requestMetrics
+}
+
+func setRequestMetric(ctx context.Context, requestMetrics requestMetrics) context.Context {
+	return middleware.WithStackValue(ctx, requestMetricsKey{}, requestMetrics)
+}
+
+var initializeTimedOperationMiddleware = middleware.InitializeMiddlewareFunc("timedOperation", func(
+	ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler,
+) (middleware.InitializeOutput, middleware.Metadata, error) {
+	requestMetrics := requestMetrics{}
+	requestMetrics.StartTime = time.Now()
+	ctx = setRequestMetric(ctx, requestMetrics)
+
+	return next.HandleInitialize(ctx, in)
+})
+
+var extractAWSRequestParameters = middleware.DeserializeMiddlewareFunc("extractAWSRequestParameters", func(
+	ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler,
+) (middleware.DeserializeOutput, middleware.Metadata, error) {
+	// Call the next middleware first to get the response
+	out, metadata, err := next.HandleDeserialize(ctx, in)
+
+	requestMetrics := getRequestMetric(ctx)
+
+	labels := metrics.Labels{}
+
+	if req, ok := in.Request.(*smithyhttp.Request); ok && req != nil {
+		labels[metrics.LabelScheme] = req.URL.Scheme
+		labels[metrics.LabelHost] = req.URL.Host
+		labels[metrics.LabelPath] = metrics.PathProcessor(req.URL.Path)
+		labels[metrics.LabelMethod] = req.Method
+		labels[metrics.LabelStatus] = "unknown"
+	}
+
+	// Try to access HTTP response and status code
+	if resp, ok := out.RawResponse.(*smithyhttp.Response); ok && resp != nil {
+		labels[metrics.LabelStatus] = fmt.Sprintf("%d", resp.StatusCode)
+	}
+
+	extdnshttp.RequestDurationMetric.SetWithLabels(time.Since(requestMetrics.StartTime).Seconds(), labels)
+
+	return out, metadata, err
+})
+
+func GetInstrumentationMiddlewares() []func(*middleware.Stack) error {
+	return []func(s *middleware.Stack) error{
+		func(s *middleware.Stack) error {
+			if err := s.Initialize.Add(initializeTimedOperationMiddleware, middleware.Before); err != nil {
+				return fmt.Errorf("error adding timedOperationMiddleware: %w", err)
+			}
+
+			if err := s.Deserialize.Add(extractAWSRequestParameters, middleware.After); err != nil {
+				return fmt.Errorf("error adding extractAWSRequestParameters: %w", err)
+			}
+
+			return nil
+		},
+	}
+}
diff --git a/provider/awssd/aws_sd.go b/provider/awssd/aws_sd.go
index 510fee81..9ee0822a 100644
--- a/provider/awssd/aws_sd.go
+++ b/provider/awssd/aws_sd.go
@@ -78,7 +78,7 @@ type AWSSDProvider struct {
 	// only consider namespaces ending in this suffix
 	namespaceFilter *endpoint.DomainFilter
 	// filter namespace by type (private or public)
-	namespaceTypeFilter sdtypes.NamespaceFilter
+	namespaceTypeFilter []sdtypes.NamespaceFilter
 	// enables service without instances cleanup
 	cleanEmptyService bool
 	// filter services for removal
@@ -102,21 +102,28 @@ func NewAWSSDProvider(domainFilter *endpoint.DomainFilter, namespaceType string,
 	return p, nil
 }
 
-// newSdNamespaceFilter initialized AWS SD Namespace Filter based on given string config
-func newSdNamespaceFilter(namespaceTypeConfig string) sdtypes.NamespaceFilter {
+// newSdNamespaceFilter returns NamespaceFilter based on the given namespace type configuration.
+// If the config is "public", it filters for public namespaces; if "private", for private namespaces.
+// For any other value (including empty), it returns filters for both public and private namespaces.
+// ref: https://docs.aws.amazon.com/cloud-map/latest/api/API_ListNamespaces.html
+func newSdNamespaceFilter(namespaceTypeConfig string) []sdtypes.NamespaceFilter {
 	switch namespaceTypeConfig {
 	case sdNamespaceTypePublic:
-		return sdtypes.NamespaceFilter{
-			Name:   sdtypes.NamespaceFilterNameType,
-			Values: []string{string(sdtypes.NamespaceTypeDnsPublic)},
+		return []sdtypes.NamespaceFilter{
+			{
+				Name:   sdtypes.NamespaceFilterNameType,
+				Values: []string{string(sdtypes.NamespaceTypeDnsPublic)},
+			},
 		}
 	case sdNamespaceTypePrivate:
-		return sdtypes.NamespaceFilter{
-			Name:   sdtypes.NamespaceFilterNameType,
-			Values: []string{string(sdtypes.NamespaceTypeDnsPrivate)},
+		return []sdtypes.NamespaceFilter{
+			{
+				Name:   sdtypes.NamespaceFilterNameType,
+				Values: []string{string(sdtypes.NamespaceTypeDnsPrivate)},
+			},
 		}
 	default:
-		return sdtypes.NamespaceFilter{}
+		return []sdtypes.NamespaceFilter{}
 	}
 }
 
@@ -354,7 +361,7 @@ func (p *AWSSDProvider) ListNamespaces(ctx context.Context) ([]*sdtypes.Namespac
 	namespaces := make([]*sdtypes.NamespaceSummary, 0)
 
 	paginator := sd.NewListNamespacesPaginator(p.client, &sd.ListNamespacesInput{
-		Filters: []sdtypes.NamespaceFilter{p.namespaceTypeFilter},
+		Filters: p.namespaceTypeFilter,
 	})
 	for paginator.HasMorePages() {
 		resp, err := paginator.NextPage(ctx)
diff --git a/provider/azure/config.go b/provider/azure/config.go
index 44dde4f6..793eefe1 100644
--- a/provider/azure/config.go
+++ b/provider/azure/config.go
@@ -46,6 +46,8 @@ type config struct {
 	UseWorkloadIdentityExtension bool   `json:"useWorkloadIdentityExtension" yaml:"useWorkloadIdentityExtension"`
 	UserAssignedIdentityID       string `json:"userAssignedIdentityID"       yaml:"userAssignedIdentityID"`
 	ActiveDirectoryAuthorityHost string `json:"activeDirectoryAuthorityHost" yaml:"activeDirectoryAuthorityHost"`
+	ResourceManagerAudience      string `json:"resourceManagerAudience"      yaml:"resourceManagerAudience"`
+	ResourceManagerEndpoint      string `json:"resourceManagerEndpoint"      yaml:"resourceManagerEndpoint"`
 }
 
 func getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost string) (*config, error) {
@@ -106,7 +108,7 @@ func CustomHeaderPolicynew() policy.Policy { return &customHeaderPolicy{} }
 
 // getCredentials retrieves Azure API credentials.
 func getCredentials(cfg config, maxRetries int) (azcore.TokenCredential, *arm.ClientOptions, error) {
-	cloudCfg, err := getCloudConfiguration(cfg.Cloud)
+	cloudCfg, err := getCloudConfiguration(cfg)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to get cloud configuration: %w", err)
 	}
@@ -193,8 +195,8 @@ func getCredentials(cfg config, maxRetries int) (azcore.TokenCredential, *arm.Cl
 	return nil, nil, fmt.Errorf("no credentials provided for Azure API")
 }
 
-func getCloudConfiguration(name string) (cloud.Configuration, error) {
-	name = strings.ToUpper(name)
+func getCloudConfiguration(cfg config) (cloud.Configuration, error) {
+	name := strings.ToUpper(cfg.Cloud)
 	switch name {
 	case "AZURECLOUD", "AZUREPUBLICCLOUD", "":
 		return cloud.AzurePublic, nil
@@ -202,6 +204,16 @@ func getCloudConfiguration(name string) (cloud.Configuration, error) {
 		return cloud.AzureGovernment, nil
 	case "AZURECHINACLOUD":
 		return cloud.AzureChina, nil
+	case "AZURESTACKCLOUD":
+		return cloud.Configuration{
+			ActiveDirectoryAuthorityHost: cfg.ActiveDirectoryAuthorityHost,
+			Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
+				cloud.ResourceManager: {
+					Audience: cfg.ResourceManagerAudience,
+					Endpoint: cfg.ResourceManagerEndpoint,
+				},
+			},
+		}, nil
 	}
 	return cloud.Configuration{}, fmt.Errorf("unknown cloud name: %s", name)
 }
diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go
index 8914032c..d367d2f0 100644
--- a/provider/cloudflare/cloudflare.go
+++ b/provider/cloudflare/cloudflare.go
@@ -28,10 +28,12 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/cloudflare/cloudflare-go"
-	cloudflarev4 "github.com/cloudflare/cloudflare-go/v4"
-	"github.com/cloudflare/cloudflare-go/v4/addressing"
-	"github.com/cloudflare/cloudflare-go/v4/option"
+	cloudflarev0 "github.com/cloudflare/cloudflare-go"
+	"github.com/cloudflare/cloudflare-go/v5"
+	"github.com/cloudflare/cloudflare-go/v5/addressing"
+	"github.com/cloudflare/cloudflare-go/v5/dns"
+	"github.com/cloudflare/cloudflare-go/v5/option"
+	"github.com/cloudflare/cloudflare-go/v5/zones"
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/net/publicsuffix"
 
@@ -74,14 +76,14 @@ type DNSRecordIndex struct {
 	Content string
 }
 
-type DNSRecordsMap map[DNSRecordIndex]cloudflare.DNSRecord
+type DNSRecordsMap map[DNSRecordIndex]dns.RecordResponse
 
 // for faster getCustomHostname() lookup
 type CustomHostnameIndex struct {
 	Hostname string
 }
 
-type CustomHostnamesMap map[CustomHostnameIndex]cloudflare.CustomHostname
+type CustomHostnamesMap map[CustomHostnameIndex]cloudflarev0.CustomHostname
 
 var recordTypeProxyNotSupported = map[string]bool{
 	"LOC": true,
@@ -106,65 +108,87 @@ var recordTypeCustomHostnameSupported = map[string]bool{
 // cloudFlareDNS is the subset of the CloudFlare API that we actually use.  Add methods as required. Signatures must match exactly.
 type cloudFlareDNS interface {
 	ZoneIDByName(zoneName string) (string, error)
-	ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error)
-	ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error)
-	ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error)
-	CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error)
-	DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error
-	UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error
+	ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone]
+	GetZone(ctx context.Context, zoneID string) (*zones.Zone, error)
+	ListDNSRecords(ctx context.Context, params dns.RecordListParams) autoPager[dns.RecordResponse]
+	CreateDNSRecord(ctx context.Context, params dns.RecordNewParams) (*dns.RecordResponse, error)
+	DeleteDNSRecord(ctx context.Context, recordID string, params dns.RecordDeleteParams) error
+	UpdateDNSRecord(ctx context.Context, rc *cloudflarev0.ResourceContainer, rp cloudflarev0.UpdateDNSRecordParams) error
 	ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse]
 	CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error
 	UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error
 	DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error
-	CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error)
+	CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflarev0.CustomHostname) ([]cloudflarev0.CustomHostname, cloudflarev0.ResultInfo, error)
 	DeleteCustomHostname(ctx context.Context, zoneID string, customHostnameID string) error
-	CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error)
+	CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflarev0.CustomHostname) (*cloudflarev0.CustomHostnameResponse, error)
 }
 
 type zoneService struct {
-	service   *cloudflare.API
-	serviceV4 *cloudflarev4.Client
+	serviceV0 *cloudflarev0.API
+	service   *cloudflare.Client
 }
 
 func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
-	return z.service.ZoneIDByName(zoneName)
+	// Use v4 API to find zone by name
+	params := zones.ZoneListParams{
+		Name: cloudflare.F(zoneName),
+	}
+
+	iter := z.service.Zones.ListAutoPaging(context.Background(), params)
+	for zone := range autoPagerIterator(iter) {
+		if zone.Name == zoneName {
+			return zone.ID, nil
+		}
+	}
+
+	if err := iter.Err(); err != nil {
+		return "", fmt.Errorf("failed to list zones from CloudFlare API: %w", err)
+	}
+
+	return "", fmt.Errorf("zone %q not found in CloudFlare account - verify the zone exists and API credentials have access to it", zoneName)
 }
 
-func (z zoneService) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) {
-	return z.service.CreateDNSRecord(ctx, rc, rp)
+func (z zoneService) CreateDNSRecord(ctx context.Context, params dns.RecordNewParams) (*dns.RecordResponse, error) {
+	return z.service.DNS.Records.New(ctx, params)
 }
 
-func (z zoneService) ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error) {
-	return z.service.ListDNSRecords(ctx, rc, rp)
+func (z zoneService) ListDNSRecords(ctx context.Context, params dns.RecordListParams) autoPager[dns.RecordResponse] {
+	return z.service.DNS.Records.ListAutoPaging(ctx, params)
 }
 
-func (z zoneService) UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error {
-	_, err := z.service.UpdateDNSRecord(ctx, rc, rp)
+func (z zoneService) UpdateDNSRecord(ctx context.Context, rc *cloudflarev0.ResourceContainer, rp cloudflarev0.UpdateDNSRecordParams) error {
+	_, err := z.serviceV0.UpdateDNSRecord(ctx, rc, rp)
 	return err
 }
 
-func (z zoneService) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error {
-	return z.service.DeleteDNSRecord(ctx, rc, recordID)
+func (z zoneService) DeleteDNSRecord(ctx context.Context, recordID string, params dns.RecordDeleteParams) error {
+	_, err := z.service.DNS.Records.Delete(ctx, recordID, params)
+	return err
 }
 
-func (z zoneService) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
-	return z.service.ListZonesContext(ctx, opts...)
+func (z zoneService) ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone] {
+	return z.service.Zones.ListAutoPaging(ctx, params)
 }
 
-func (z zoneService) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) {
-	return z.service.ZoneDetails(ctx, zoneID)
+func (z zoneService) GetZone(ctx context.Context, zoneID string) (*zones.Zone, error) {
+	return z.service.Zones.Get(ctx, zones.ZoneGetParams{ZoneID: cloudflare.F(zoneID)})
 }
 
-func (z zoneService) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) {
-	return z.service.CustomHostnames(ctx, zoneID, page, filter)
+func (z zoneService) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflarev0.CustomHostname) ([]cloudflarev0.CustomHostname, cloudflarev0.ResultInfo, error) {
+	return z.serviceV0.CustomHostnames(ctx, zoneID, page, filter)
 }
 
 func (z zoneService) DeleteCustomHostname(ctx context.Context, zoneID string, customHostnameID string) error {
-	return z.service.DeleteCustomHostname(ctx, zoneID, customHostnameID)
+	return z.serviceV0.DeleteCustomHostname(ctx, zoneID, customHostnameID)
+}
+
+func (z zoneService) CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflarev0.CustomHostname) (*cloudflarev0.CustomHostnameResponse, error) {
+	return z.serviceV0.CreateCustomHostname(ctx, zoneID, ch)
 }
 
-func (z zoneService) CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error) {
-	return z.service.CreateCustomHostname(ctx, zoneID, ch)
+// listZonesV4Params returns the appropriate Zone List Params for v4 API
+func listZonesV4Params() zones.ZoneListParams {
+	return zones.ZoneListParams{}
 }
 
 type DNSRecordsConfig struct {
@@ -202,13 +226,13 @@ func (p *CloudFlareProvider) ZoneHasPaidPlan(hostname string) bool {
 		return false
 	}
 
-	zoneDetails, err := p.Client.ZoneDetails(context.Background(), zoneID)
+	zoneDetails, err := p.Client.GetZone(context.Background(), zoneID)
 	if err != nil {
 		log.Errorf("Failed to get zone %s details %v", zone, err)
 		return false
 	}
 
-	return zoneDetails.Plan.IsSubscribed
+	return zoneDetails.Plan.IsSubscribed //nolint:staticcheck // SA1019: Plan.IsSubscribed is deprecated but no replacement available yet
 }
 
 // CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
@@ -228,49 +252,52 @@ type CloudFlareProvider struct {
 // cloudFlareChange differentiates between ChangeActions
 type cloudFlareChange struct {
 	Action              changeAction
-	ResourceRecord      cloudflare.DNSRecord
+	ResourceRecord      dns.RecordResponse
 	RegionalHostname    regionalHostname
-	CustomHostnames     map[string]cloudflare.CustomHostname
+	CustomHostnames     map[string]cloudflarev0.CustomHostname
 	CustomHostnamesPrev []string
 }
 
 // RecordParamsTypes is a typeset of the possible Record Params that can be passed to cloudflare-go library
 type RecordParamsTypes interface {
-	cloudflare.UpdateDNSRecordParams | cloudflare.CreateDNSRecordParams
+	cloudflarev0.UpdateDNSRecordParams | cloudflarev0.CreateDNSRecordParams
 }
 
 // updateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
-func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams {
-	params := cloudflare.UpdateDNSRecordParams{
+func updateDNSRecordParam(cfc cloudFlareChange) cloudflarev0.UpdateDNSRecordParams {
+	priority := uint16(cfc.ResourceRecord.Priority)
+
+	params := cloudflarev0.UpdateDNSRecordParams{
 		Name:     cfc.ResourceRecord.Name,
-		TTL:      cfc.ResourceRecord.TTL,
-		Proxied:  cfc.ResourceRecord.Proxied,
-		Type:     cfc.ResourceRecord.Type,
+		TTL:      int(cfc.ResourceRecord.TTL),
+		Proxied:  &cfc.ResourceRecord.Proxied,
+		Type:     string(cfc.ResourceRecord.Type),
 		Content:  cfc.ResourceRecord.Content,
-		Priority: cfc.ResourceRecord.Priority,
-		Comment:  cloudflare.StringPtr(cfc.ResourceRecord.Comment),
+		Priority: &priority,
+		Comment:  cloudflarev0.StringPtr(cfc.ResourceRecord.Comment),
 	}
 
 	return params
 }
 
 // getCreateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
-func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordParams {
-	params := cloudflare.CreateDNSRecordParams{
-		Name:     cfc.ResourceRecord.Name,
-		TTL:      cfc.ResourceRecord.TTL,
-		Proxied:  cfc.ResourceRecord.Proxied,
-		Type:     cfc.ResourceRecord.Type,
-		Content:  cfc.ResourceRecord.Content,
-		Priority: cfc.ResourceRecord.Priority,
-		Comment:  cfc.ResourceRecord.Comment,
+func getCreateDNSRecordParam(zoneID string, cfc *cloudFlareChange) dns.RecordNewParams {
+	return dns.RecordNewParams{
+		ZoneID: cloudflare.F(zoneID),
+		Body: dns.RecordNewParamsBody{
+			Name:     cloudflare.F(cfc.ResourceRecord.Name),
+			TTL:      cloudflare.F(cfc.ResourceRecord.TTL),
+			Proxied:  cloudflare.F(cfc.ResourceRecord.Proxied),
+			Type:     cloudflare.F(dns.RecordNewParamsBodyType(cfc.ResourceRecord.Type)),
+			Content:  cloudflare.F(cfc.ResourceRecord.Content),
+			Priority: cloudflare.F(cfc.ResourceRecord.Priority),
+			Comment:  cloudflare.F(cfc.ResourceRecord.Comment),
+		},
 	}
-
-	return params
 }
 
 func convertCloudflareError(err error) error {
-	var apiErr *cloudflare.Error
+	var apiErr *cloudflarev0.Error
 	if errors.As(err, &apiErr) {
 		if apiErr.ClientRateLimited() || apiErr.StatusCode >= http.StatusInternalServerError {
 			// Handle rate limit error as a soft error
@@ -298,8 +325,8 @@ func NewCloudFlareProvider(
 ) (*CloudFlareProvider, error) {
 	// initialize via chosen auth method and returns new API object
 	var (
-		config   *cloudflare.API
-		configV4 *cloudflarev4.Client
+		config   *cloudflarev0.API
+		configV4 *cloudflare.Client
 		err      error
 	)
 	if os.Getenv("CF_API_TOKEN") != "" {
@@ -311,13 +338,13 @@ func NewCloudFlareProvider(
 			}
 			token = strings.TrimSpace(string(tokenBytes))
 		}
-		config, err = cloudflare.NewWithAPIToken(token)
-		configV4 = cloudflarev4.NewClient(
+		config, err = cloudflarev0.NewWithAPIToken(token)
+		configV4 = cloudflare.NewClient(
 			option.WithAPIToken(token),
 		)
 	} else {
-		config, err = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL"))
-		configV4 = cloudflarev4.NewClient(
+		config, err = cloudflarev0.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL"))
+		configV4 = cloudflare.NewClient(
 			option.WithAPIKey(os.Getenv("CF_API_KEY")),
 			option.WithAPIEmail(os.Getenv("CF_API_EMAIL")),
 		)
@@ -343,8 +370,8 @@ func NewCloudFlareProvider(
 }
 
 // Zones returns the list of hosted zones.
-func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, error) {
-	var result []cloudflare.Zone
+func (p *CloudFlareProvider) Zones(ctx context.Context) ([]zones.Zone, error) {
+	var result []zones.Zone
 
 	// if there is a zoneIDfilter configured
 	// && if the filter isn't just a blank string (used in tests)
@@ -352,34 +379,38 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
 		log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
 		for _, zoneID := range p.zoneIDFilter.ZoneIDs {
 			log.Debugf("looking up zone %q", zoneID)
-			detailResponse, err := p.Client.ZoneDetails(ctx, zoneID)
+			detailResponse, err := p.Client.GetZone(ctx, zoneID)
 			if err != nil {
 				log.Errorf("zone %q lookup failed, %v", zoneID, err)
-				return result, err
+				return result, convertCloudflareError(err)
 			}
 			log.WithFields(log.Fields{
 				"zoneName": detailResponse.Name,
 				"zoneID":   detailResponse.ID,
 			}).Debugln("adding zone for consideration")
-			result = append(result, detailResponse)
+			result = append(result, *detailResponse)
 		}
 		return result, nil
 	}
 
 	log.Debugln("no zoneIDFilter configured, looking at all zones")
 
-	zonesResponse, err := p.Client.ListZonesContext(ctx)
-	if err != nil {
-		return nil, convertCloudflareError(err)
-	}
-
-	for _, zone := range zonesResponse.Result {
+	params := listZonesV4Params()
+	iter := p.Client.ListZones(ctx, params)
+	for zone := range autoPagerIterator(iter) {
 		if !p.domainFilter.Match(zone.Name) {
 			log.Debugf("zone %q not in domain filter", zone.Name)
 			continue
 		}
+		log.WithFields(log.Fields{
+			"zoneName": zone.Name,
+			"zoneID":   zone.ID,
+		}).Debugln("adding zone for consideration")
 		result = append(result, zone)
 	}
+	if iter.Err() != nil {
+		return nil, convertCloudflareError(iter.Err())
+	}
 
 	return result, nil
 }
@@ -393,7 +424,7 @@ func (p *CloudFlareProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
 
 	var endpoints []*endpoint.Endpoint
 	for _, zone := range zones {
-		records, err := p.listDNSRecordsWithAutoPagination(ctx, zone.ID)
+		records, err := p.getDNSRecordsMap(ctx, zone.ID)
 		if err != nil {
 			return nil, err
 		}
@@ -508,7 +539,7 @@ func (p *CloudFlareProvider) submitCustomHostnameChanges(ctx context.Context, zo
 
 	switch change.Action {
 	case cloudFlareUpdate:
-		if recordTypeCustomHostnameSupported[change.ResourceRecord.Type] {
+		if recordTypeCustomHostnameSupported[string(change.ResourceRecord.Type)] {
 			add, remove, _ := provider.Difference(change.CustomHostnamesPrev, slices.Collect(maps.Keys(change.CustomHostnames)))
 
 			for _, changeCH := range remove {
@@ -535,7 +566,7 @@ func (p *CloudFlareProvider) submitCustomHostnameChanges(ctx context.Context, zo
 		}
 	case cloudFlareDelete:
 		for _, changeCH := range change.CustomHostnames {
-			if recordTypeCustomHostnameSupported[change.ResourceRecord.Type] && changeCH.Hostname != "" {
+			if recordTypeCustomHostnameSupported[string(change.ResourceRecord.Type)] && changeCH.Hostname != "" {
 				log.WithFields(logFields).Infof("Deleting custom hostname %q", changeCH.Hostname)
 				if ch, err := getCustomHostname(chs, changeCH.Hostname); err == nil {
 					chID := ch.ID
@@ -551,7 +582,7 @@ func (p *CloudFlareProvider) submitCustomHostnameChanges(ctx context.Context, zo
 		}
 	case cloudFlareCreate:
 		for _, changeCH := range change.CustomHostnames {
-			if recordTypeCustomHostnameSupported[change.ResourceRecord.Type] && changeCH.Hostname != "" {
+			if recordTypeCustomHostnameSupported[string(change.ResourceRecord.Type)] && changeCH.Hostname != "" {
 				log.WithFields(logFields).Infof("Creating custom hostname %q", changeCH.Hostname)
 				if ch, err := getCustomHostname(chs, changeCH.Hostname); err == nil {
 					if changeCH.CustomOriginServer == ch.CustomOriginServer {
@@ -591,7 +622,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
 	var failedZones []string
 	for zoneID, zoneChanges := range changesByZone {
 		var failedChange bool
-		resourceContainer := cloudflare.ZoneIdentifier(zoneID)
+		resourceContainer := cloudflarev0.ZoneIdentifier(zoneID)
 
 		for _, change := range zoneChanges {
 			logFields := log.Fields{
@@ -608,7 +639,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
 				continue
 			}
 
-			records, err := p.listDNSRecordsWithAutoPagination(ctx, zoneID)
+			records, err := p.getDNSRecordsMap(ctx, zoneID)
 			if err != nil {
 				return fmt.Errorf("could not fetch records from zone, %w", err)
 			}
@@ -638,7 +669,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
 					log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord)
 					continue
 				}
-				err := p.Client.DeleteDNSRecord(ctx, resourceContainer, recordID)
+				err := p.Client.DeleteDNSRecord(ctx, recordID, dns.RecordDeleteParams{ZoneID: cloudflare.F(zoneID)})
 				if err != nil {
 					failedChange = true
 					log.WithFields(logFields).Errorf("failed to delete record: %v", err)
@@ -647,8 +678,8 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
 					failedChange = true
 				}
 			} else if change.Action == cloudFlareCreate {
-				recordParam := getCreateDNSRecordParam(*change)
-				_, err := p.Client.CreateDNSRecord(ctx, resourceContainer, recordParam)
+				recordParam := getCreateDNSRecordParam(zoneID, change)
+				_, err := p.Client.CreateDNSRecord(ctx, recordParam)
 				if err != nil {
 					failedChange = true
 					log.WithFields(logFields).Errorf("failed to create record: %v", err)
@@ -709,12 +740,7 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]
 			e.DeleteProviderSpecificProperty(annotations.CloudflareCustomHostnameKey)
 		}
 
-		if p.RegionalServicesConfig.Enabled {
-			// Add default region key if not set
-			if _, ok := e.GetProviderSpecificProperty(annotations.CloudflareRegionKey); !ok {
-				e.SetProviderSpecificProperty(annotations.CloudflareRegionKey, p.RegionalServicesConfig.RegionKey)
-			}
-		}
+		p.adjustEndpointProviderSpecificRegionKeyProperty(e)
 
 		adjustedEndpoints = append(adjustedEndpoints, e)
 	}
@@ -722,7 +748,7 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]
 }
 
 // changesByZone separates a multi-zone change into a single change per zone.
-func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
+func (p *CloudFlareProvider) changesByZone(zones []zones.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
 	changes := make(map[string][]*cloudFlareChange)
 	zoneNameIDMapper := provider.ZoneIDName{}
 
@@ -743,25 +769,25 @@ func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []
 	return changes
 }
 
-func (p *CloudFlareProvider) getRecordID(records DNSRecordsMap, record cloudflare.DNSRecord) string {
-	if zoneRecord, ok := records[DNSRecordIndex{Name: record.Name, Type: record.Type, Content: record.Content}]; ok {
+func (p *CloudFlareProvider) getRecordID(records DNSRecordsMap, record dns.RecordResponse) string {
+	if zoneRecord, ok := records[DNSRecordIndex{Name: record.Name, Type: string(record.Type), Content: record.Content}]; ok {
 		return zoneRecord.ID
 	}
 	return ""
 }
 
-func getCustomHostname(chs CustomHostnamesMap, chName string) (cloudflare.CustomHostname, error) {
+func getCustomHostname(chs CustomHostnamesMap, chName string) (cloudflarev0.CustomHostname, error) {
 	if chName == "" {
-		return cloudflare.CustomHostname{}, fmt.Errorf("failed to get custom hostname: %q is empty", chName)
+		return cloudflarev0.CustomHostname{}, fmt.Errorf("failed to get custom hostname: %q is empty", chName)
 	}
 	if ch, ok := chs[CustomHostnameIndex{Hostname: chName}]; ok {
 		return ch, nil
 	}
-	return cloudflare.CustomHostname{}, fmt.Errorf("failed to get custom hostname: %q not found", chName)
+	return cloudflarev0.CustomHostname{}, fmt.Errorf("failed to get custom hostname: %q not found", chName)
 }
 
-func (p *CloudFlareProvider) newCustomHostname(customHostname string, origin string) cloudflare.CustomHostname {
-	return cloudflare.CustomHostname{
+func (p *CloudFlareProvider) newCustomHostname(customHostname string, origin string) cloudflarev0.CustomHostname {
+	return cloudflarev0.CustomHostname{
 		Hostname:           customHostname,
 		CustomOriginServer: origin,
 		SSL:                getCustomHostnamesSSLOptions(p.CustomHostnamesConfig),
@@ -769,15 +795,15 @@ func (p *CloudFlareProvider) newCustomHostname(customHostname string, origin str
 }
 
 func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoint.Endpoint, target string, current *endpoint.Endpoint) (*cloudFlareChange, error) {
-	ttl := defaultTTL
+	ttl := dns.TTL(defaultTTL)
 	proxied := shouldBeProxied(ep, p.proxiedByDefault)
 
 	if ep.RecordTTL.IsConfigured() {
-		ttl = int(ep.RecordTTL)
+		ttl = dns.TTL(ep.RecordTTL)
 	}
 
 	prevCustomHostnames := []string{}
-	newCustomHostnames := map[string]cloudflare.CustomHostname{}
+	newCustomHostnames := map[string]cloudflarev0.CustomHostname{}
 	if p.CustomHostnamesConfig.Enabled {
 		if current != nil {
 			prevCustomHostnames = getEndpointCustomHostnames(current)
@@ -798,26 +824,24 @@ func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoi
 		comment = p.DNSRecordsConfig.trimAndValidateComment(ep.DNSName, comment, p.ZoneHasPaidPlan)
 	}
 
-	priority := (*uint16)(nil)
+	var priority float64
 	if ep.RecordType == "MX" {
 		mxRecord, err := endpoint.NewMXRecord(target)
 		if err != nil {
 			return &cloudFlareChange{}, fmt.Errorf("failed to parse MX record target %q: %w", target, err)
 		} else {
-			priority = mxRecord.GetPriority()
+			priority = float64(*mxRecord.GetPriority())
 			target = *mxRecord.GetHost()
 		}
 	}
 
 	return &cloudFlareChange{
 		Action: action,
-		ResourceRecord: cloudflare.DNSRecord{
-			Name: ep.DNSName,
-			TTL:  ttl,
-			// We have to use pointers to bools now, as the upstream cloudflare-go library requires them
-			// see: https://github.com/cloudflare/cloudflare-go/pull/595
-			Proxied:  &proxied,
-			Type:     ep.RecordType,
+		ResourceRecord: dns.RecordResponse{
+			Name:     ep.DNSName,
+			TTL:      ttl,
+			Proxied:  proxied,
+			Type:     dns.RecordResponseType(ep.RecordType),
 			Content:  target,
 			Comment:  comment,
 			Priority: priority,
@@ -828,34 +852,26 @@ func (p *CloudFlareProvider) newCloudFlareChange(action changeAction, ep *endpoi
 	}, nil
 }
 
-func newDNSRecordIndex(r cloudflare.DNSRecord) DNSRecordIndex {
-	return DNSRecordIndex{Name: r.Name, Type: r.Type, Content: r.Content}
+func newDNSRecordIndex(r dns.RecordResponse) DNSRecordIndex {
+	return DNSRecordIndex{Name: r.Name, Type: string(r.Type), Content: r.Content}
 }
 
-// listDNSRecordsWithAutoPagination performs automatic pagination of results on requests to cloudflare.ListDNSRecords with custom per_page values
-func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Context, zoneID string) (DNSRecordsMap, error) {
+// getDNSRecordsMap retrieves all DNS records for a given zone and returns them as a DNSRecordsMap.
+func (p *CloudFlareProvider) getDNSRecordsMap(ctx context.Context, zoneID string) (DNSRecordsMap, error) {
 	// for faster getRecordID lookup
-	records := make(DNSRecordsMap)
-	resultInfo := cloudflare.ResultInfo{PerPage: p.DNSRecordsConfig.PerPage, Page: 1}
-	params := cloudflare.ListDNSRecordsParams{ResultInfo: resultInfo}
-	for {
-		pageRecords, resultInfo, err := p.Client.ListDNSRecords(ctx, cloudflare.ZoneIdentifier(zoneID), params)
-		if err != nil {
-			return nil, convertCloudflareError(err)
-		}
-
-		for _, r := range pageRecords {
-			records[newDNSRecordIndex(r)] = r
-		}
-		params.ResultInfo = resultInfo.Next()
-		if params.Done() {
-			break
-		}
+	recordsMap := make(DNSRecordsMap)
+	params := dns.RecordListParams{ZoneID: cloudflare.F(zoneID)}
+	iter := p.Client.ListDNSRecords(ctx, params)
+	for record := range autoPagerIterator(iter) {
+		recordsMap[newDNSRecordIndex(record)] = record
+	}
+	if iter.Err() != nil {
+		return nil, convertCloudflareError(iter.Err())
 	}
-	return records, nil
+	return recordsMap, nil
 }
 
-func newCustomHostnameIndex(ch cloudflare.CustomHostname) CustomHostnameIndex {
+func newCustomHostnameIndex(ch cloudflarev0.CustomHostname) CustomHostnameIndex {
 	return CustomHostnameIndex{Hostname: ch.Hostname}
 }
 
@@ -865,9 +881,9 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte
 		return nil, nil
 	}
 	chs := make(CustomHostnamesMap)
-	resultInfo := cloudflare.ResultInfo{Page: 1}
+	resultInfo := cloudflarev0.ResultInfo{Page: 1}
 	for {
-		pageCustomHostnameListResponse, result, err := p.Client.CustomHostnames(ctx, zoneID, resultInfo.Page, cloudflare.CustomHostname{})
+		pageCustomHostnameListResponse, result, err := p.Client.CustomHostnames(ctx, zoneID, resultInfo.Page, cloudflarev0.CustomHostname{})
 		if err != nil {
 			convertedError := convertCloudflareError(err)
 			if !errors.Is(convertedError, provider.SoftError) {
@@ -886,12 +902,12 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte
 	return chs, nil
 }
 
-func getCustomHostnamesSSLOptions(customHostnamesConfig CustomHostnamesConfig) *cloudflare.CustomHostnameSSL {
-	ssl := &cloudflare.CustomHostnameSSL{
+func getCustomHostnamesSSLOptions(customHostnamesConfig CustomHostnamesConfig) *cloudflarev0.CustomHostnameSSL {
+	ssl := &cloudflarev0.CustomHostnameSSL{
 		Type:         "dv",
 		Method:       "http",
 		BundleMethod: "ubiquitous",
-		Settings: cloudflare.CustomHostnameSSLSettings{
+		Settings: cloudflarev0.CustomHostnameSSLSettings{
 			MinTLSVersion: customHostnamesConfig.MinTLSVersion,
 		},
 	}
@@ -938,16 +954,16 @@ func (p *CloudFlareProvider) groupByNameAndTypeWithCustomHostnames(records DNSRe
 	var endpoints []*endpoint.Endpoint
 
 	// group supported records by name and type
-	groups := map[string][]cloudflare.DNSRecord{}
+	groups := map[string][]dns.RecordResponse{}
 
 	for _, r := range records {
-		if !p.SupportedAdditionalRecordTypes(r.Type) {
+		if !p.SupportedAdditionalRecordTypes(string(r.Type)) {
 			continue
 		}
 
-		groupBy := r.Name + r.Type
+		groupBy := r.Name + string(r.Type)
 		if _, ok := groups[groupBy]; !ok {
-			groups[groupBy] = []cloudflare.DNSRecord{}
+			groups[groupBy] = []dns.RecordResponse{}
 		}
 
 		groups[groupBy] = append(groups[groupBy], r)
@@ -968,20 +984,17 @@ func (p *CloudFlareProvider) groupByNameAndTypeWithCustomHostnames(records DNSRe
 		targets := make([]string, len(records))
 		for i, record := range records {
 			if records[i].Type == "MX" {
-				targets[i] = fmt.Sprintf("%v %v", *record.Priority, record.Content)
+				targets[i] = fmt.Sprintf("%v %v", record.Priority, record.Content)
 			} else {
 				targets[i] = record.Content
 			}
 		}
 		e := endpoint.NewEndpointWithTTL(
 			records[0].Name,
-			records[0].Type,
+			string(records[0].Type),
 			endpoint.TTL(records[0].TTL),
 			targets...)
-		proxied := false
-		if records[0].Proxied != nil {
-			proxied = *records[0].Proxied
-		}
+		proxied := records[0].Proxied
 		if e == nil {
 			continue
 		}
@@ -1010,3 +1023,27 @@ func (p *CloudFlareProvider) SupportedAdditionalRecordTypes(recordType string) b
 		return provider.SupportedRecordType(recordType)
 	}
 }
+
+func dnsRecordResponseFromLegacyDNSRecord(record cloudflarev0.DNSRecord) dns.RecordResponse {
+	var priority float64
+	if record.Priority != nil {
+		priority = float64(*record.Priority)
+	}
+
+	return dns.RecordResponse{
+		CreatedOn:  record.CreatedOn,
+		ModifiedOn: record.ModifiedOn,
+		Type:       dns.RecordResponseType(record.Type),
+		Name:       record.Name,
+		Content:    record.Content,
+		Meta:       record.Meta,
+		Data:       record.Data,
+		ID:         record.ID,
+		Priority:   priority,
+		TTL:        dns.TTL(record.TTL),
+		Proxied:    record.Proxied != nil && *record.Proxied,
+		Proxiable:  record.Proxiable,
+		Comment:    record.Comment,
+		Tags:       record.Tags,
+	}
+}
diff --git a/provider/cloudflare/cloudflare_regional.go b/provider/cloudflare/cloudflare_regional.go
index 9502bcc9..08372993 100644
--- a/provider/cloudflare/cloudflare_regional.go
+++ b/provider/cloudflare/cloudflare_regional.go
@@ -22,8 +22,8 @@ import (
 	"maps"
 	"slices"
 
-	cloudflarev4 "github.com/cloudflare/cloudflare-go/v4"
-	"github.com/cloudflare/cloudflare-go/v4/addressing"
+	"github.com/cloudflare/cloudflare-go/v5"
+	"github.com/cloudflare/cloudflare-go/v5/addressing"
 
 	log "github.com/sirupsen/logrus"
 
@@ -56,52 +56,52 @@ type regionalHostnameChange struct {
 }
 
 func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] {
-	return z.serviceV4.Addressing.RegionalHostnames.ListAutoPaging(ctx, params)
+	return z.service.Addressing.RegionalHostnames.ListAutoPaging(ctx, params)
 }
 
 func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error {
-	_, err := z.serviceV4.Addressing.RegionalHostnames.New(ctx, params)
+	_, err := z.service.Addressing.RegionalHostnames.New(ctx, params)
 	return err
 }
 
 func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error {
-	_, err := z.serviceV4.Addressing.RegionalHostnames.Edit(ctx, hostname, params)
+	_, err := z.service.Addressing.RegionalHostnames.Edit(ctx, hostname, params)
 	return err
 }
 
 func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error {
-	_, err := z.serviceV4.Addressing.RegionalHostnames.Delete(ctx, hostname, params)
+	_, err := z.service.Addressing.RegionalHostnames.Delete(ctx, hostname, params)
 	return err
 }
 
 // listDataLocalizationRegionalHostnamesParams is a function that returns the appropriate RegionalHostname List Param based on the zoneID
 func listDataLocalizationRegionalHostnamesParams(zoneID string) addressing.RegionalHostnameListParams {
 	return addressing.RegionalHostnameListParams{
-		ZoneID: cloudflarev4.F(zoneID),
+		ZoneID: cloudflare.F(zoneID),
 	}
 }
 
 // createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
 func createDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameNewParams {
 	return addressing.RegionalHostnameNewParams{
-		ZoneID:    cloudflarev4.F(zoneID),
-		Hostname:  cloudflarev4.F(rhc.hostname),
-		RegionKey: cloudflarev4.F(rhc.regionKey),
+		ZoneID:    cloudflare.F(zoneID),
+		Hostname:  cloudflare.F(rhc.hostname),
+		RegionKey: cloudflare.F(rhc.regionKey),
 	}
 }
 
 // updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
 func updateDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameEditParams {
 	return addressing.RegionalHostnameEditParams{
-		ZoneID:    cloudflarev4.F(zoneID),
-		RegionKey: cloudflarev4.F(rhc.regionKey),
+		ZoneID:    cloudflare.F(zoneID),
+		RegionKey: cloudflare.F(rhc.regionKey),
 	}
 }
 
 // deleteDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
 func deleteDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameDeleteParams {
 	return addressing.RegionalHostnameDeleteParams{
-		ZoneID: cloudflarev4.F(zoneID),
+		ZoneID: cloudflare.F(zoneID),
 	}
 }
 
@@ -222,13 +222,34 @@ func (p *CloudFlareProvider) addEnpointsProviderSpecificRegionKeyProperty(ctx co
 	}
 
 	for _, ep := range supportedEndpoints {
+		var regionKey string
 		if rh, found := regionalHostnames[ep.DNSName]; found {
-			ep.SetProviderSpecificProperty(annotations.CloudflareRegionKey, rh.regionKey)
+			regionKey = rh.regionKey
 		}
+		ep.SetProviderSpecificProperty(annotations.CloudflareRegionKey, regionKey)
 	}
 	return nil
 }
 
+// adjustEnpointProviderSpecificRegionKeyProperty updates the given endpoint's provider-specific
+// Cloudflare region key based on the provider's RegionalServicesConfig.
+//   - If regional services are disabled or the endpoint's record type does not
+//     support regional hostnames, the Cloudflare region key is removed.
+//   - If enabled and supported, and the key is not already set, it is initialized
+//     to the provider's default RegionKey.
+//
+// The endpoint is modified in place and any explicitly set region key is left unchanged.
+func (p *CloudFlareProvider) adjustEndpointProviderSpecificRegionKeyProperty(ep *endpoint.Endpoint) {
+	if !p.RegionalServicesConfig.Enabled || !recordTypeRegionalHostnameSupported[ep.RecordType] {
+		ep.DeleteProviderSpecificProperty(annotations.CloudflareRegionKey)
+		return
+	}
+	// Add default region key if not set
+	if _, ok := ep.GetProviderSpecificProperty(annotations.CloudflareRegionKey); !ok {
+		ep.SetProviderSpecificProperty(annotations.CloudflareRegionKey, p.RegionalServicesConfig.RegionKey)
+	}
+}
+
 // desiredRegionalHostnames builds a list of desired regional hostnames from changes.
 //
 // If there is a delete and a create or update action for the same hostname,
diff --git a/provider/coredns/coredns.go b/provider/coredns/coredns.go
index fdd52904..57ab8130 100644
--- a/provider/coredns/coredns.go
+++ b/provider/coredns/coredns.go
@@ -302,8 +302,8 @@ func (p coreDNSProvider) groupEndpoints(changes *plan.Changes) map[string][]*end
 		grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
 	}
 	for i, ep := range changes.UpdateNew {
+		log.Debugf("Updating labels (%s) with old labels (%s)", ep.Labels, changes.UpdateOld[i].Labels)
 		ep.Labels = changes.UpdateOld[i].Labels
-		log.Debugf("Updating labels (%s) with old labels(%s)", ep.Labels, changes.UpdateOld[i].Labels)
 		grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
 	}
 	return grouped
diff --git a/provider/fakes/provider.go b/provider/fakes/provider.go
new file mode 100644
index 00000000..6f6d1830
--- /dev/null
+++ b/provider/fakes/provider.go
@@ -0,0 +1,42 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fakes
+
+import (
+	"context"
+
+	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/plan"
+)
+
+type MockProvider struct{}
+
+func (m *MockProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error) {
+	return nil, nil
+}
+
+func (m *MockProvider) ApplyChanges(_ context.Context, _ *plan.Changes) error {
+	return nil
+}
+
+func (m *MockProvider) AdjustEndpoints(_ []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) {
+	return nil, nil
+}
+
+func (m *MockProvider) GetDomainFilter() endpoint.DomainFilterInterface {
+	return nil
+}
diff --git a/provider/zonefinder.go b/provider/zonefinder.go
index 08e6b316..a529e17e 100644
--- a/provider/zonefinder.go
+++ b/provider/zonefinder.go
@@ -20,7 +20,8 @@ import (
 	"strings"
 
 	log "github.com/sirupsen/logrus"
-	"golang.org/x/net/idna"
+
+	"sigs.k8s.io/external-dns/internal/idna"
 )
 
 type ZoneIDName map[string]string
@@ -48,7 +49,7 @@ func (z ZoneIDName) FindZone(hostname string) (string, string) {
 		if strings.Contains(label, "_") {
 			continue
 		}
-		convertedLabel, err := idna.Lookup.ToUnicode(label)
+		convertedLabel, err := idna.Profile.ToUnicode(label)
 		if err != nil {
 			log.Warnf("Failed to convert label %q of hostname %q to its Unicode form: %v", label, hostname, err)
 			convertedLabel = label
diff --git a/registry/txt.go b/registry/txt.go
index 32599693..b428cfe9 100644
--- a/registry/txt.go
+++ b/registry/txt.go
@@ -19,6 +19,7 @@ package registry
 import (
 	"context"
 	"errors"
+
 	"strings"
 	"time"
 
@@ -58,6 +59,53 @@ type TXTRegistry struct {
 	// encrypt text records
 	txtEncryptEnabled bool
 	txtEncryptAESKey  []byte
+
+	// existingTXTs is the TXT records that already exist in the zone so that
+	// ApplyChanges() can skip re-creating them. See the struct below for details.
+	existingTXTs *existingTXTs
+}
+
+// existingTXTs stores pre‑existing TXT records to avoid duplicate creation.
+// It relies on the fact that Records() is always called **before** ApplyChanges()
+// within a single reconciliation cycle.
+type existingTXTs struct {
+	entries map[recordKey]struct{}
+}
+
+type recordKey struct {
+	dnsName       string
+	setIdentifier string
+}
+
+func newExistingTXTs() *existingTXTs {
+	return &existingTXTs{
+		entries: make(map[recordKey]struct{}),
+	}
+}
+
+func (im *existingTXTs) add(r *endpoint.Endpoint) {
+	key := recordKey{
+		dnsName:       r.DNSName,
+		setIdentifier: r.SetIdentifier,
+	}
+	im.entries[key] = struct{}{}
+}
+
+// isAbsent returns true when there is no entry for the given name in the store.
+// This is intended for the "if absent -> create" pattern.
+func (im *existingTXTs) isAbsent(ep *endpoint.Endpoint) bool {
+	key := recordKey{
+		dnsName:       ep.DNSName,
+		setIdentifier: ep.SetIdentifier,
+	}
+	_, ok := im.entries[key]
+	return !ok
+}
+
+func (im *existingTXTs) reset() {
+	// Reset the existing TXT records for the next reconciliation loop.
+	// This is necessary because the existing TXT records are only relevant for the current reconciliation cycle.
+	im.entries = make(map[recordKey]struct{})
 }
 
 // NewTXTRegistry returns a new TXTRegistry object. When newFormatOnly is true, it will only
@@ -100,6 +148,7 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
 		excludeRecordTypes:  excludeRecordTypes,
 		txtEncryptEnabled:   txtEncryptEnabled,
 		txtEncryptAESKey:    txtEncryptAESKey,
+		existingTXTs:        newExistingTXTs(),
 	}, nil
 }
 
@@ -167,6 +216,7 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
 		}
 		labelMap[key] = labels
 		txtRecordsMap[record.DNSName] = struct{}{}
+		im.existingTXTs.add(record)
 	}
 
 	for _, ep := range endpoints {
@@ -230,6 +280,10 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
 // depending on the newFormatOnly configuration. The old format is maintained for backwards
 // compatibility but can be disabled to reduce the number of DNS records.
 func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpoint {
+	return im.generateTXTRecordWithFilter(r, func(ep *endpoint.Endpoint) bool { return true })
+}
+
+func (im *TXTRegistry) generateTXTRecordWithFilter(r *endpoint.Endpoint, filter func(*endpoint.Endpoint) bool) []*endpoint.Endpoint {
 	endpoints := make([]*endpoint.Endpoint, 0)
 
 	// Always create new format record
@@ -243,7 +297,9 @@ func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpo
 		txtNew.WithSetIdentifier(r.SetIdentifier)
 		txtNew.Labels[endpoint.OwnedRecordLabelKey] = r.DNSName
 		txtNew.ProviderSpecific = r.ProviderSpecific
-		endpoints = append(endpoints, txtNew)
+		if filter(txtNew) {
+			endpoints = append(endpoints, txtNew)
+		}
 	}
 	return endpoints
 }
@@ -251,6 +307,8 @@ func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpo
 // ApplyChanges updates dns provider with the changes
 // for each created/deleted record it will also take into account TXT records for creation/deletion
 func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
+	defer im.existingTXTs.reset() // reset existing TXTs for the next reconciliation loop
+
 	filteredChanges := &plan.Changes{
 		Create:    changes.Create,
 		UpdateNew: endpoint.FilterEndpointsByOwnerID(im.ownerID, changes.UpdateNew),
@@ -263,7 +321,7 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
 		}
 		r.Labels[endpoint.OwnerLabelKey] = im.ownerID
 
-		filteredChanges.Create = append(filteredChanges.Create, im.generateTXTRecord(r)...)
+		filteredChanges.Create = append(filteredChanges.Create, im.generateTXTRecordWithFilter(r, im.existingTXTs.isAbsent)...)
 
 		if im.cacheInterval > 0 {
 			im.addToCache(r)
diff --git a/scripts/helm-tools.sh b/scripts/helm-tools.sh
index 825a22c8..2ea42c46 100755
--- a/scripts/helm-tools.sh
+++ b/scripts/helm-tools.sh
@@ -67,8 +67,8 @@ update_schema() {
 
 diff_schema() {
   cd charts/external-dns
-  helm schema  \
-    -output diff-schema.schema.json
+  helm schema \
+    --output diff-schema.schema.json
   trap 'rm -rf -- "diff-schema.schema.json"' EXIT
   CURRENT_SCHEMA=$(cat values.schema.json)
   GENERATED_SCHEMA=$(cat diff-schema.schema.json)
diff --git a/scripts/install-tools.sh b/scripts/install-tools.sh
index f5b35aad..de45e63d 100755
--- a/scripts/install-tools.sh
+++ b/scripts/install-tools.sh
@@ -17,7 +17,7 @@
 # renovate: datasource=github-releases depName=kubernetes-sigs/controller-tools
 CONTROLLER_TOOLS_GENERATOR_VERSION=v0.17.2
 # renovate: datasource=github-releases depName=golangci/golangci-lint
-GOLANG_CI_LINTER_VERSION=v2.1.6
+GOLANG_CI_LINTER_VERSION=v2.2.2
 
 # Execute
 # scripts/install-tools.sh
@@ -32,7 +32,7 @@ cat << EOF
 Usage: $(basename "$0") <options>
     -h, --help          Display help
     --generator         Install generator
-    --golangci         Install golangci linter
+    --golangci          Install golangci linter
 EOF
 }
 
@@ -51,7 +51,7 @@ install_generator() {
   fi
   if [[ "$install" == true ]]; then
       set -ex ;\
-	    go install sigs.k8s.io/controller-tools/cmd/controller-gen@${CONTROLLER_TOOLS_GENERATOR_VERSION} ;
+      go install sigs.k8s.io/controller-tools/cmd/controller-gen@${CONTROLLER_TOOLS_GENERATOR_VERSION} ;
   fi
 
   if [[ ! -x $(which yq) ]]; then
diff --git a/scripts/releaser.sh b/scripts/releaser.sh
index e71ea4e7..547ece7c 100755
--- a/scripts/releaser.sh
+++ b/scripts/releaser.sh
@@ -30,6 +30,15 @@ function generate_changelog {
   echo "## :package: Others"
   echo
   cat "${MERGED_PRS}" | grep -v "\!" | grep -v feat[:\(] | grep -v fix[:\(] | grep -v docs[:\(]
+
+  echo
+  echo "## :package: Docker Image"
+  echo
+  echo "\`\`\`sh"
+  echo "# This pull command only works when it's released
+  echo "docker pull registry.k8s.io/external-dns/external-dns:${VERSION}"
+  echo "\`\`\`"
+
 }
 
 function create_release {
@@ -66,12 +75,14 @@ gh pr list \
   " | sort > "${MERGED_PRS}"
 
 if [ $# -ne 1 ]; then
+  export VERSION="v0.x.0"
   generate_changelog "${MERGED_PRS}"
   echo "** DRY RUN **"
   echo
-  echo "To create a release: ./releaser.sh v0.17.0"
+  echo "To create a release: ./releaser.sh v0.x.0"
 else
-  generate_changelog "${MERGED_PRS}" | gh release create "$1" -t "$1" -p -F -
+  export VERSION="$1"
+  generate_changelog "${MERGED_PRS}" | gh release create "${VERSION}" -t "${VERSION}" -p -F -
 fi
 
 rm -f "${MERGED_PRS}"
diff --git a/source/annotations/annotations.go b/source/annotations/annotations.go
index abfce135..9e1f19c2 100644
--- a/source/annotations/annotations.go
+++ b/source/annotations/annotations.go
@@ -40,19 +40,21 @@ const (
 	SetIdentifierKey = AnnotationKeyPrefix + "set-identifier"
 	AliasKey         = AnnotationKeyPrefix + "alias"
 	TargetKey        = AnnotationKeyPrefix + "target"
-	// The annotation used for figuring out which controller is responsible
+	// ControllerKey The annotation used for figuring out which controller is responsible
 	ControllerKey = AnnotationKeyPrefix + "controller"
-	// The annotation used for defining the desired hostname
+	// HostnameKey The annotation used for defining the desired hostname
 	HostnameKey = AnnotationKeyPrefix + "hostname"
-	// The annotation used for specifying whether the public or private interface address is used
+	// AccessKey The annotation used for specifying whether the public or private interface address is used
 	AccessKey = AnnotationKeyPrefix + "access"
-	// The annotation used for specifying the type of endpoints to use for headless services
+	// EndpointsTypeKey The annotation used for specifying the type of endpoints to use for headless services
 	EndpointsTypeKey = AnnotationKeyPrefix + "endpoints-type"
-	// The annotation used to determine the source of hostnames for ingresses.  This is an optional field - all
+	// Ingress the annotation used to determine if the gateway is implemented by an Ingress object
+	Ingress = AnnotationKeyPrefix + "ingress"
+	// IngressHostnameSourceKey The annotation used to determine the source of hostnames for ingresses.  This is an optional field - all
 	// available hostname sources are used if not specified.
 	IngressHostnameSourceKey = AnnotationKeyPrefix + "ingress-hostname-source"
-	// The value of the controller annotation so that we feel responsible
+	// ControllerValue The value of the controller annotation so that we feel responsible
 	ControllerValue = "dns-controller"
-	// The annotation used for defining the desired hostname
+	// InternalHostnameKey The annotation used for defining the desired hostname
 	InternalHostnameKey = AnnotationKeyPrefix + "internal-hostname"
 )
diff --git a/source/fake.go b/source/fake.go
index 2041c116..d6b1235f 100644
--- a/source/fake.go
+++ b/source/fake.go
@@ -26,7 +26,12 @@ import (
 	"math/rand"
 	"net"
 
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/pkg/events"
+	"sigs.k8s.io/external-dns/source/types"
 )
 
 // fakeSource is an implementation of Source that provides dummy endpoints for
@@ -50,7 +55,7 @@ func NewFakeSource(fqdnTemplate string) (Source, error) {
 	}, nil
 }
 
-func (sc *fakeSource) AddEventHandler(ctx context.Context, handler func()) {
+func (sc *fakeSource) AddEventHandler(_ context.Context, handler func()) {
 }
 
 // Endpoints returns endpoint objects.
@@ -58,20 +63,30 @@ func (sc *fakeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
 	endpoints := make([]*endpoint.Endpoint, 10)
 
 	for i := 0; i < 10; i++ {
-		endpoints[i], _ = sc.generateEndpoint()
+		endpoints[i] = sc.generateEndpoint()
 	}
 
 	return endpoints, nil
 }
 
-func (sc *fakeSource) generateEndpoint() (*endpoint.Endpoint, error) {
+func (sc *fakeSource) generateEndpoint() *endpoint.Endpoint {
 	ep := endpoint.NewEndpoint(
 		generateDNSName(4, sc.dnsName),
 		endpoint.RecordTypeA,
 		generateIPAddress(),
 	)
-
-	return ep, nil
+	ep.SetIdentifier = types.Fake
+	ep.WithRefObject(events.NewObjectReference(&v1.Pod{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Pod",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      types.Fake + "-" + ep.DNSName,
+			Namespace: v1.NamespaceDefault,
+		},
+	}, types.Fake))
+	return ep
 }
 
 func generateIPAddress() string {
diff --git a/source/informers/fake.go b/source/informers/fake.go
new file mode 100644
index 00000000..2eb86e32
--- /dev/null
+++ b/source/informers/fake.go
@@ -0,0 +1,110 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package informers
+
+import (
+	"github.com/stretchr/testify/mock"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	corev1lister "k8s.io/client-go/listers/core/v1"
+	discoveryv1lister "k8s.io/client-go/listers/discovery/v1"
+	"k8s.io/client-go/tools/cache"
+)
+
+type FakeServiceInformer struct {
+	mock.Mock
+}
+
+func (f *FakeServiceInformer) Informer() cache.SharedIndexInformer {
+	args := f.Called()
+	return args.Get(0).(cache.SharedIndexInformer)
+}
+
+func (f *FakeServiceInformer) Lister() corev1lister.ServiceLister {
+	return corev1lister.NewServiceLister(f.Informer().GetIndexer())
+}
+
+type FakeEndpointSliceInformer struct {
+	mock.Mock
+}
+
+func (f *FakeEndpointSliceInformer) Informer() cache.SharedIndexInformer {
+	args := f.Called()
+	return args.Get(0).(cache.SharedIndexInformer)
+}
+
+func (f *FakeEndpointSliceInformer) Lister() discoveryv1lister.EndpointSliceLister {
+	return discoveryv1lister.NewEndpointSliceLister(f.Informer().GetIndexer())
+}
+
+type FakeNodeInformer struct {
+	mock.Mock
+}
+
+func (f *FakeNodeInformer) Informer() cache.SharedIndexInformer {
+	args := f.Called()
+	return args.Get(0).(cache.SharedIndexInformer)
+}
+
+func (f *FakeNodeInformer) Lister() corev1lister.NodeLister {
+	return corev1lister.NewNodeLister(f.Informer().GetIndexer())
+}
+
+func fakeService() *corev1.Service {
+	return &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        "fake-service",
+			Namespace:   "ns",
+			Labels:      map[string]string{"env": "prod", "team": "devops"},
+			Annotations: map[string]string{"description": "Enriched service object"},
+			UID:         "1234",
+		},
+		Spec: corev1.ServiceSpec{
+			Selector:    map[string]string{"app": "demo"},
+			ExternalIPs: []string{"1.2.3.4"},
+			Ports: []corev1.ServicePort{
+				{
+					Name:       "http",
+					Port:       80,
+					TargetPort: intstr.FromInt32(8080),
+					Protocol:   corev1.ProtocolTCP,
+				},
+				{
+					Name:       "https",
+					Port:       443,
+					TargetPort: intstr.FromInt32(8443),
+					Protocol:   corev1.ProtocolTCP,
+				},
+			},
+			Type: corev1.ServiceTypeLoadBalancer,
+		},
+		Status: corev1.ServiceStatus{
+			LoadBalancer: corev1.LoadBalancerStatus{
+				Ingress: []corev1.LoadBalancerIngress{
+					{IP: "5.6.7.8", Hostname: "lb.example.com"},
+				},
+			},
+			Conditions: []metav1.Condition{
+				{
+					Type:               "Available",
+					Status:             metav1.ConditionTrue,
+					Reason:             "MinimumReplicasAvailable",
+					Message:            "Service is available",
+					LastTransitionTime: metav1.Now(),
+				},
+			},
+		},
+	}
+}
diff --git a/source/informers/transfomers.go b/source/informers/transfomers.go
new file mode 100644
index 00000000..2d3daf5b
--- /dev/null
+++ b/source/informers/transfomers.go
@@ -0,0 +1,84 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package informers
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/tools/cache"
+)
+
+type TransformOptions struct {
+	specSelector    bool
+	specExternalIps bool
+	statusLb        bool
+}
+
+func TransformerWithOptions[T metav1.Object](optFns ...func(options *TransformOptions)) cache.TransformFunc {
+	options := TransformOptions{}
+	for _, fn := range optFns {
+		fn(&options)
+	}
+	return func(obj any) (any, error) {
+		// only transform if the object is a Service at the moment
+		entity, ok := obj.(*corev1.Service)
+		if !ok {
+			return nil, nil
+		}
+		if entity.UID == "" {
+			// Pod was already transformed and we must be idempotent.
+			return entity, nil
+		}
+		svc := &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:              entity.Name,
+				Namespace:         entity.Namespace,
+				DeletionTimestamp: entity.DeletionTimestamp,
+			},
+			Spec:   corev1.ServiceSpec{},
+			Status: corev1.ServiceStatus{},
+		}
+		if options.specSelector {
+			svc.Spec.Selector = entity.Spec.Selector
+		}
+		if options.specExternalIps {
+			svc.Spec.ExternalIPs = entity.Spec.ExternalIPs
+		}
+		if options.statusLb {
+			svc.Status.LoadBalancer = entity.Status.LoadBalancer
+		}
+		return svc, nil
+	}
+}
+
+// TransformWithSpecSelector enables copying the Service's .spec.selector field.
+func TransformWithSpecSelector() func(options *TransformOptions) {
+	return func(options *TransformOptions) {
+		options.specSelector = true
+	}
+}
+
+// TransformWithSpecExternalIPs enables copying the Service's .spec.externalIPs field.
+func TransformWithSpecExternalIPs() func(options *TransformOptions) {
+	return func(options *TransformOptions) {
+		options.specExternalIps = true
+	}
+}
+
+// TransformWithStatusLoadBalancer enables copying the Service's .status.loadBalancer field.
+func TransformWithStatusLoadBalancer() func(options *TransformOptions) {
+	return func(options *TransformOptions) {
+		options.statusLb = true
+	}
+}
diff --git a/source/ingress.go b/source/ingress.go
index e0735eba..e79d50f1 100644
--- a/source/ingress.go
+++ b/source/ingress.go
@@ -31,7 +31,6 @@ import (
 	kubeinformers "k8s.io/client-go/informers"
 	netinformers "k8s.io/client-go/informers/networking/v1"
 	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/source/informers"
 
@@ -100,12 +99,7 @@ func NewIngressSource(
 	ingressInformer := informerFactory.Networking().V1().Ingresses()
 
 	// Add default resource event handlers to properly initialize informer.
-	ingressInformer.Informer().AddEventHandler(
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-			},
-		},
-	)
+	_, _ = ingressInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
 
 	informerFactory.Start(ctx.Done())
 
@@ -360,5 +354,5 @@ func (sc *ingressSource) AddEventHandler(ctx context.Context, handler func()) {
 
 	// Right now there is no way to remove event handler from informer, see:
 	// https://github.com/kubernetes/kubernetes/issues/79610
-	sc.ingressInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
+	_, _ = sc.ingressInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
 }
diff --git a/source/istio_gateway.go b/source/istio_gateway.go
index 9043cdcf..2f7ae1af 100644
--- a/source/istio_gateway.go
+++ b/source/istio_gateway.go
@@ -28,12 +28,13 @@ import (
 	istioclient "istio.io/client-go/pkg/clientset/versioned"
 	istioinformers "istio.io/client-go/pkg/informers/externalversions"
 	networkingv1beta1informer "istio.io/client-go/pkg/informers/externalversions/networking/v1beta1"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	kubeinformers "k8s.io/client-go/informers"
 	coreinformers "k8s.io/client-go/informers/core/v1"
+	netinformers "k8s.io/client-go/informers/networking/v1"
 	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/source/annotations"
@@ -43,7 +44,7 @@ import (
 
 // IstioGatewayIngressSource is the annotation used to determine if the gateway is implemented by an Ingress object
 // instead of a standard LoadBalancer service type
-const IstioGatewayIngressSource = "external-dns.alpha.kubernetes.io/ingress"
+const IstioGatewayIngressSource = annotations.Ingress
 
 // gatewaySource is an implementation of Source for Istio Gateway objects.
 // The gateway implementation uses the spec.servers.hosts values for the hostnames.
@@ -58,6 +59,7 @@ type gatewaySource struct {
 	ignoreHostnameAnnotation bool
 	serviceInformer          coreinformers.ServiceInformer
 	gatewayInformer          networkingv1beta1informer.GatewayInformer
+	ingressInformer          netinformers.IngressInformer
 }
 
 // NewIstioGatewaySource creates a new gatewaySource with the given config.
@@ -82,32 +84,31 @@ func NewIstioGatewaySource(
 	serviceInformer := informerFactory.Core().V1().Services()
 	istioInformerFactory := istioinformers.NewSharedInformerFactory(istioClient, 0)
 	gatewayInformer := istioInformerFactory.Networking().V1beta1().Gateways()
+	ingressInformer := informerFactory.Networking().V1().Ingresses()
+
+	_, _ = ingressInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
 
 	// Add default resource event handlers to properly initialize informer.
-	_, _ = serviceInformer.Informer().AddEventHandler(
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-				log.Debug("service added")
-			},
-		},
-	)
-
-	_, _ = gatewayInformer.Informer().AddEventHandler(
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-				log.Debug("gateway added")
-			},
-		},
-	)
+	_, _ = serviceInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
+	err = serviceInformer.Informer().SetTransform(informers.TransformerWithOptions[*corev1.Service](
+		informers.TransformWithSpecSelector(),
+		informers.TransformWithSpecExternalIPs(),
+		informers.TransformWithStatusLoadBalancer(),
+	))
+	if err != nil {
+		return nil, err
+	}
+
+	_, _ = gatewayInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
 
 	informerFactory.Start(ctx.Done())
 	istioInformerFactory.Start(ctx.Done())
 
 	// wait for the local cache to be populated.
-	if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil {
+	if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil {
 		return nil, err
 	}
-	if err := informers.WaitForCacheSync(context.Background(), istioInformerFactory); err != nil {
+	if err := informers.WaitForCacheSync(ctx, istioInformerFactory); err != nil {
 		return nil, err
 	}
 
@@ -121,6 +122,7 @@ func NewIstioGatewaySource(
 		ignoreHostnameAnnotation: ignoreHostnameAnnotation,
 		serviceInformer:          serviceInformer,
 		gatewayInformer:          gatewayInformer,
+		ingressInformer:          ingressInformer,
 	}, nil
 }
 
@@ -200,7 +202,7 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
 }
 
 // AddEventHandler adds an event handler that should be triggered if the watched Istio Gateway changes.
-func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) {
+func (sc *gatewaySource) AddEventHandler(_ context.Context, handler func()) {
 	log.Debug("Adding event handler for Istio Gateway")
 
 	_, _ = sc.gatewayInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
@@ -230,7 +232,7 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1beta1.Gatew
 	return filteredList, nil
 }
 
-func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1beta1.Gateway) (endpoint.Targets, error) {
+func (sc *gatewaySource) targetsFromIngress(ingressStr string, gateway *networkingv1beta1.Gateway) (endpoint.Targets, error) {
 	namespace, name, err := ParseIngress(ingressStr)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err)
@@ -241,7 +243,7 @@ func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr stri
 
 	targets := make(endpoint.Targets, 0)
 
-	ingress, err := sc.kubeClient.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{})
+	ingress, err := sc.ingressInformer.Lister().Ingresses(namespace).Get(name)
 	if err != nil {
 		log.Error(err)
 		return nil, err
@@ -264,7 +266,7 @@ func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networ
 
 	ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource]
 	if ok && ingressStr != "" {
-		return sc.targetsFromIngress(ctx, ingressStr, gateway)
+		return sc.targetsFromIngress(ingressStr, gateway)
 	}
 
 	return EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go
index d16c236b..f9311690 100644
--- a/source/istio_virtualservice.go
+++ b/source/istio_virtualservice.go
@@ -30,13 +30,13 @@ import (
 	istioclient "istio.io/client-go/pkg/clientset/versioned"
 	istioinformers "istio.io/client-go/pkg/informers/externalversions"
 	networkingv1beta1informer "istio.io/client-go/pkg/informers/externalversions/networking/v1beta1"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	kubeinformers "k8s.io/client-go/informers"
 	coreinformers "k8s.io/client-go/informers/core/v1"
+	netinformers "k8s.io/client-go/informers/networking/v1"
 	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/source/annotations"
@@ -61,6 +61,7 @@ type virtualServiceSource struct {
 	serviceInformer          coreinformers.ServiceInformer
 	vServiceInformer         networkingv1beta1informer.VirtualServiceInformer
 	gatewayInformer          networkingv1beta1informer.GatewayInformer
+	ingressInformer          netinformers.IngressInformer
 }
 
 // NewIstioVirtualServiceSource creates a new virtualServiceSource with the given config.
@@ -86,40 +87,33 @@ func NewIstioVirtualServiceSource(
 	istioInformerFactory := istioinformers.NewSharedInformerFactoryWithOptions(istioClient, 0, istioinformers.WithNamespace(namespace))
 	virtualServiceInformer := istioInformerFactory.Networking().V1beta1().VirtualServices()
 	gatewayInformer := istioInformerFactory.Networking().V1beta1().Gateways()
+	ingressInformer := informerFactory.Networking().V1().Ingresses()
+
+	_, _ = ingressInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
 
 	// Add default resource event handlers to properly initialize informer.
-	_, _ = serviceInformer.Informer().AddEventHandler(
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-				log.Debug("service added")
-			},
-		},
-	)
-
-	_, _ = virtualServiceInformer.Informer().AddEventHandler(
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-				log.Debug("virtual service added")
-			},
-		},
-	)
-
-	_, _ = gatewayInformer.Informer().AddEventHandler(
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-				log.Debug("gateway added")
-			},
-		},
-	)
+	_, _ = serviceInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
+	err = serviceInformer.Informer().SetTransform(informers.TransformerWithOptions[*corev1.Service](
+		informers.TransformWithSpecSelector(),
+		informers.TransformWithSpecExternalIPs(),
+		informers.TransformWithStatusLoadBalancer(),
+	))
+	if err != nil {
+		return nil, err
+	}
+
+	_, _ = virtualServiceInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
+
+	_, _ = gatewayInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
 
 	informerFactory.Start(ctx.Done())
 	istioInformerFactory.Start(ctx.Done())
 
 	// wait for the local cache to be populated.
-	if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil {
+	if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil {
 		return nil, err
 	}
-	if err := informers.WaitForCacheSync(context.Background(), istioInformerFactory); err != nil {
+	if err := informers.WaitForCacheSync(ctx, istioInformerFactory); err != nil {
 		return nil, err
 	}
 
@@ -134,6 +128,7 @@ func NewIstioVirtualServiceSource(
 		serviceInformer:          serviceInformer,
 		vServiceInformer:         virtualServiceInformer,
 		gatewayInformer:          gatewayInformer,
+		ingressInformer:          ingressInformer,
 	}, nil
 }
 
@@ -302,7 +297,7 @@ func (sc *virtualServiceSource) targetsFromVirtualService(ctx context.Context, v
 		if !virtualServiceBindsToGateway(vService, gw, vsHost) {
 			continue
 		}
-		tgs, err := sc.targetsFromGateway(ctx, gw)
+		tgs, err := sc.targetsFromGateway(gw)
 		if err != nil {
 			return targets, err
 		}
@@ -417,7 +412,7 @@ func virtualServiceBindsToGateway(vService *v1beta1.VirtualService, gateway *v1b
 	return false
 }
 
-func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *v1beta1.Gateway) (endpoint.Targets, error) {
+func (sc *virtualServiceSource) targetsFromIngress(ingressStr string, gateway *v1beta1.Gateway) (endpoint.Targets, error) {
 	namespace, name, err := ParseIngress(ingressStr)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err)
@@ -426,7 +421,7 @@ func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressS
 		namespace = gateway.Namespace
 	}
 
-	ingress, err := sc.kubeClient.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{})
+	ingress, err := sc.ingressInformer.Lister().Ingresses(namespace).Get(name)
 	if err != nil {
 		log.Error(err)
 		return nil, err
@@ -444,7 +439,7 @@ func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressS
 	return targets, nil
 }
 
-func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *v1beta1.Gateway) (endpoint.Targets, error) {
+func (sc *virtualServiceSource) targetsFromGateway(gateway *v1beta1.Gateway) (endpoint.Targets, error) {
 	targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations)
 	if len(targets) > 0 {
 		return targets, nil
@@ -452,7 +447,7 @@ func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway
 
 	ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource]
 	if ok && ingressStr != "" {
-		return sc.targetsFromIngress(ctx, ingressStr, gateway)
+		return sc.targetsFromIngress(ingressStr, gateway)
 	}
 
 	return EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
diff --git a/source/pod.go b/source/pod.go
index baa0e58e..b996b358 100644
--- a/source/pod.go
+++ b/source/pod.go
@@ -24,6 +24,7 @@ import (
 
 	log "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 
 	kubeinformers "k8s.io/client-go/informers"
@@ -76,6 +77,40 @@ func NewPodSource(
 	}
 
 	_, _ = podInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
+
+	if fqdnTemplate == "" {
+		// Transformer is used to reduce the memory usage of the informer.
+		// The pod informer will otherwise store a full in-memory, go-typed copy of all pod schemas in the cluster.
+		// If watchList is not used it will not prevent memory bursts on the initial informer sync.
+		// When fqdnTemplate is used the entire pod needs to be provided to the rendering call, but the informer itself becomes unneeded.
+		podInformer.Informer().SetTransform(func(i interface{}) (interface{}, error) {
+			pod, ok := i.(*corev1.Pod)
+			if !ok {
+				return nil, fmt.Errorf("object is not a pod")
+			}
+			if pod.UID == "" {
+				// Pod was already transformed and we must be idempotent.
+				return pod, nil
+			}
+			return &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					// Name/namespace must always be kept for the informer to work.
+					Name:      pod.Name,
+					Namespace: pod.Namespace,
+					// Used by the controller. This includes non-external-dns prefixed annotations.
+					Annotations: pod.Annotations,
+				},
+				Spec: corev1.PodSpec{
+					HostNetwork: pod.Spec.HostNetwork,
+					NodeName:    pod.Spec.NodeName,
+				},
+				Status: corev1.PodStatus{
+					PodIP: pod.Status.PodIP,
+				},
+			}, nil
+		})
+	}
+
 	_, _ = nodeInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
 
 	informerFactory.Start(ctx.Done())
diff --git a/source/service.go b/source/service.go
index a339b192..29447e38 100644
--- a/source/service.go
+++ b/source/service.go
@@ -29,6 +29,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	v1 "k8s.io/api/core/v1"
 	discoveryv1 "k8s.io/api/discovery/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/types"
 	kubeinformers "k8s.io/client-go/informers"
@@ -96,28 +97,9 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name
 	// Set the resync period to 0 to prevent processing when nothing has changed
 	informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace))
 	serviceInformer := informerFactory.Core().V1().Services()
-	endpointSlicesInformer := informerFactory.Discovery().V1().EndpointSlices()
-	podInformer := informerFactory.Core().V1().Pods()
 
 	// Add default resource event handlers to properly initialize informer.
-	_, _ = serviceInformer.Informer().AddEventHandler(
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-			},
-		},
-	)
-	_, _ = endpointSlicesInformer.Informer().AddEventHandler(
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-			},
-		},
-	)
-	_, _ = podInformer.Informer().AddEventHandler(
-		cache.ResourceEventHandlerFuncs{
-			AddFunc: func(obj interface{}) {
-			},
-		},
-	)
+	_, _ = serviceInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
 
 	// Transform the slice into a map so it will be way much easier and fast to filter later
 	sTypesFilter, err := newServiceTypesFilter(serviceTypeFilter)
@@ -125,30 +107,82 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name
 		return nil, err
 	}
 
-	var nodeInformer coreinformers.NodeInformer
-	if sTypesFilter.isNodeInformerRequired() {
-		nodeInformer = informerFactory.Core().V1().Nodes()
-		_, _ = nodeInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
-	}
+	var endpointSlicesInformer discoveryinformers.EndpointSliceInformer
+	var podInformer coreinformers.PodInformer
+	if sTypesFilter.isRequired(v1.ServiceTypeNodePort, v1.ServiceTypeClusterIP) {
+		endpointSlicesInformer = informerFactory.Discovery().V1().EndpointSlices()
+		podInformer = informerFactory.Core().V1().Pods()
+
+		_, _ = endpointSlicesInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
+		_, _ = podInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
 
-	// Add an indexer to the EndpointSlice informer to index by the service name label
-	err = endpointSlicesInformer.Informer().AddIndexers(cache.Indexers{
-		serviceNameIndexKey: func(obj any) ([]string, error) {
-			endpointSlice, ok := obj.(*discoveryv1.EndpointSlice)
+		// Add an indexer to the EndpointSlice informer to index by the service name label
+		err = endpointSlicesInformer.Informer().AddIndexers(cache.Indexers{
+			serviceNameIndexKey: func(obj any) ([]string, error) {
+				endpointSlice, ok := obj.(*discoveryv1.EndpointSlice)
+				if !ok {
+					// This should never happen because the Informer should only contain EndpointSlice objects
+					return nil, fmt.Errorf("expected %T but got %T instead", endpointSlice, obj)
+				}
+				serviceName := endpointSlice.Labels[discoveryv1.LabelServiceName]
+				if serviceName == "" {
+					return nil, nil
+				}
+				key := types.NamespacedName{Namespace: endpointSlice.Namespace, Name: serviceName}.String()
+				return []string{key}, nil
+			},
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		// Transformer is used to reduce the memory usage of the informer.
+		// The pod informer will otherwise store a full in-memory, go-typed copy of all pod schemas in the cluster.
+		// If watchList is not used it will not prevent memory bursts on the initial informer sync.
+		podInformer.Informer().SetTransform(func(i interface{}) (interface{}, error) {
+			pod, ok := i.(*v1.Pod)
 			if !ok {
-				// This should never happen because the Informer should only contain EndpointSlice objects
-				return nil, fmt.Errorf("expected %T but got %T instead", endpointSlice, obj)
+				return nil, fmt.Errorf("object is not a pod")
 			}
-			serviceName := endpointSlice.Labels[discoveryv1.LabelServiceName]
-			if serviceName == "" {
-				return nil, nil
+			if pod.UID == "" {
+				// Pod was already transformed and we must be idempotent.
+				return pod, nil
 			}
-			key := types.NamespacedName{Namespace: endpointSlice.Namespace, Name: serviceName}.String()
-			return []string{key}, nil
-		},
-	})
-	if err != nil {
-		return nil, err
+
+			// All pod level annotations we're interested in start with a common prefix
+			podAnnotations := map[string]string{}
+			for key, value := range pod.Annotations {
+				if strings.HasPrefix(key, annotations.AnnotationKeyPrefix) {
+					podAnnotations[key] = value
+				}
+			}
+			return &v1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					// Name/namespace must always be kept for the informer to work.
+					Name:      pod.Name,
+					Namespace: pod.Namespace,
+					// Used to match services.
+					Labels:            pod.Labels,
+					Annotations:       podAnnotations,
+					DeletionTimestamp: pod.DeletionTimestamp,
+				},
+				Spec: v1.PodSpec{
+					Hostname: pod.Spec.Hostname,
+					NodeName: pod.Spec.NodeName,
+				},
+				Status: v1.PodStatus{
+					HostIP:     pod.Status.HostIP,
+					Phase:      pod.Status.Phase,
+					Conditions: pod.Status.Conditions,
+				},
+			}, nil
+		})
+	}
+
+	var nodeInformer coreinformers.NodeInformer
+	if sTypesFilter.isRequired(v1.ServiceTypeNodePort) {
+		nodeInformer = informerFactory.Core().V1().Nodes()
+		_, _ = nodeInformer.Informer().AddEventHandler(informers.DefaultEventHandler())
 	}
 
 	informerFactory.Start(ctx.Done())
@@ -808,10 +842,10 @@ func (sc *serviceSource) AddEventHandler(_ context.Context, handler func()) {
 	// Right now there is no way to remove event handler from informer, see:
 	// https://github.com/kubernetes/kubernetes/issues/79610
 	_, _ = sc.serviceInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
-	if sc.listenEndpointEvents {
+	if sc.listenEndpointEvents && sc.serviceTypeFilter.isRequired(v1.ServiceTypeNodePort, v1.ServiceTypeClusterIP) {
 		_, _ = sc.endpointSlicesInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
 	}
-	if sc.serviceTypeFilter.isNodeInformerRequired() {
+	if sc.serviceTypeFilter.isRequired(v1.ServiceTypeNodePort) {
 		_, _ = sc.nodeInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
 	}
 }
@@ -848,12 +882,18 @@ func (sc *serviceTypes) isProcessed(serviceType v1.ServiceType) bool {
 	return !sc.enabled || sc.types[serviceType]
 }
 
-func (sc *serviceTypes) isNodeInformerRequired() bool {
-	if !sc.enabled {
+// isRequired returns true if service type filtering is disabled or if any of the provided service types are present in the filter.
+// If no options are provided, it returns true.
+func (sc *serviceTypes) isRequired(opts ...v1.ServiceType) bool {
+	if len(opts) == 0 || !sc.enabled {
 		return true
 	}
-	_, ok := sc.types[v1.ServiceTypeNodePort]
-	return ok
+	for _, opt := range opts {
+		if _, ok := sc.types[opt]; ok {
+			return true
+		}
+	}
+	return false
 }
 
 // conditionToBool converts an EndpointConditions condition to a bool value.
diff --git a/source/store.go b/source/store.go
index c77e89c3..9dbc5687 100644
--- a/source/store.go
+++ b/source/store.go
@@ -37,6 +37,8 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 	gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
 
+	"sigs.k8s.io/external-dns/source/types"
+
 	extdnshttp "sigs.k8s.io/external-dns/pkg/http"
 
 	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
@@ -332,53 +334,53 @@ func ByNames(ctx context.Context, p ClientGenerator, names []string, cfg *Config
 // because they have simpler initialization requirements.
 func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg *Config) (Source, error) {
 	switch source {
-	case "node":
+	case types.Node:
 		return buildNodeSource(ctx, p, cfg)
-	case "service":
+	case types.Service:
 		return buildServiceSource(ctx, p, cfg)
-	case "ingress":
+	case types.Ingress:
 		return buildIngressSource(ctx, p, cfg)
-	case "pod":
+	case types.Pod:
 		return buildPodSource(ctx, p, cfg)
-	case "gateway-httproute":
+	case types.GatewayHttpRoute:
 		return NewGatewayHTTPRouteSource(p, cfg)
-	case "gateway-grpcroute":
+	case types.GatewayGrpcRoute:
 		return NewGatewayGRPCRouteSource(p, cfg)
-	case "gateway-tlsroute":
+	case types.GatewayTlsRoute:
 		return NewGatewayTLSRouteSource(p, cfg)
-	case "gateway-tcproute":
+	case types.GatewayTcpRoute:
 		return NewGatewayTCPRouteSource(p, cfg)
-	case "gateway-udproute":
+	case types.GatewayUdpRoute:
 		return NewGatewayUDPRouteSource(p, cfg)
-	case "istio-gateway":
+	case types.IstioGateway:
 		return buildIstioGatewaySource(ctx, p, cfg)
-	case "istio-virtualservice":
+	case types.IstioVirtualService:
 		return buildIstioVirtualServiceSource(ctx, p, cfg)
-	case "cloudfoundry":
+	case types.Cloudfoundry:
 		return buildCloudFoundrySource(ctx, p, cfg)
-	case "ambassador-host":
+	case types.AmbassadorHost:
 		return buildAmbassadorHostSource(ctx, p, cfg)
-	case "contour-httpproxy":
+	case types.ContourHTTPProxy:
 		return buildContourHTTPProxySource(ctx, p, cfg)
-	case "gloo-proxy":
+	case types.GlooProxy:
 		return buildGlooProxySource(ctx, p, cfg)
-	case "traefik-proxy":
+	case types.TraefikProxy:
 		return buildTraefikProxySource(ctx, p, cfg)
-	case "openshift-route":
+	case types.OpenShiftRoute:
 		return buildOpenShiftRouteSource(ctx, p, cfg)
-	case "fake":
+	case types.Fake:
 		return NewFakeSource(cfg.FQDNTemplate)
-	case "connector":
+	case types.Connector:
 		return NewConnectorSource(cfg.ConnectorServer)
-	case "crd":
+	case types.CRD:
 		return buildCRDSource(ctx, p, cfg)
-	case "skipper-routegroup":
+	case types.SkipperRouteGroup:
 		return buildSkipperRouteGroupSource(ctx, cfg)
-	case "kong-tcpingress":
+	case types.KongTCPIngress:
 		return buildKongTCPIngressSource(ctx, p, cfg)
-	case "f5-virtualserver":
+	case types.F5VirtualServer:
 		return buildF5VirtualServerSource(ctx, p, cfg)
-	case "f5-transportserver":
+	case types.F5TransportServer:
 		return buildF5TransportServerSource(ctx, p, cfg)
 	}
 	return nil, ErrSourceNotFound
diff --git a/source/types/types.go b/source/types/types.go
new file mode 100644
index 00000000..bf95491b
--- /dev/null
+++ b/source/types/types.go
@@ -0,0 +1,46 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package types
+
+type Type = string
+
+const (
+	Node                Type = "node"
+	Service             Type = "service"
+	Ingress             Type = "ingress"
+	Pod                 Type = "pod"
+	GatewayHttpRoute    Type = "gateway-httproute"
+	GatewayGrpcRoute    Type = "gateway-grpcroute"
+	GatewayTlsRoute     Type = "gateway-tlsroute"
+	GatewayTcpRoute     Type = "gateway-tcproute"
+	GatewayUdpRoute     Type = "gateway-udproute"
+	IstioGateway        Type = "istio-gateway"
+	IstioVirtualService Type = "istio-virtualservice"
+	Cloudfoundry        Type = "cloudfoundry"
+	AmbassadorHost      Type = "ambassador-host"
+	ContourHTTPProxy    Type = "contour-httpproxy"
+	GlooProxy           Type = "gloo-proxy"
+	TraefikProxy        Type = "traefik-proxy"
+	OpenShiftRoute      Type = "openshift-route"
+	Fake                Type = "fake"
+	Connector           Type = "connector"
+	CRD                 Type = "crd"
+	SkipperRouteGroup   Type = "skipper-routegroup"
+	KongTCPIngress      Type = "kong-tcpingress"
+	F5VirtualServer     Type = "f5-virtualserver"
+	F5TransportServer   Type = "f5-transportserver"
+)
diff --git a/source/wrappers/dedupsource.go b/source/wrappers/dedupsource.go
index 665799b4..961ef45c 100644
--- a/source/wrappers/dedupsource.go
+++ b/source/wrappers/dedupsource.go
@@ -22,9 +22,8 @@ import (
 
 	log "github.com/sirupsen/logrus"
 
-	"sigs.k8s.io/external-dns/source"
-
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source"
 )
 
 // dedupSource is a Source that removes duplicate endpoints from its wrapped source.
@@ -39,6 +38,7 @@ func NewDedupSource(source source.Source) source.Source {
 
 // Endpoints collects endpoints from its wrapped source and returns them without duplicates.
 func (ms *dedupSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
+	log.Debug("dedupSource: collecting endpoints and removing duplicates")
 	result := []*endpoint.Endpoint{}
 	collected := map[string]bool{}
 
@@ -67,5 +67,6 @@ func (ms *dedupSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, err
 }
 
 func (ms *dedupSource) AddEventHandler(ctx context.Context, handler func()) {
+	log.Debug("dedupSource: adding event handler")
 	ms.source.AddEventHandler(ctx, handler)
 }
diff --git a/source/wrappers/multisource.go b/source/wrappers/multisource.go
index 60c28771..60e19766 100644
--- a/source/wrappers/multisource.go
+++ b/source/wrappers/multisource.go
@@ -18,12 +18,13 @@ package wrappers
 
 import (
 	"context"
+	"reflect"
 	"strings"
 
+	log "github.com/sirupsen/logrus"
+
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/source"
-
-	log "github.com/sirupsen/logrus"
 )
 
 // multiSource is a Source that merges the endpoints of its nested Sources.
@@ -35,6 +36,7 @@ type multiSource struct {
 
 // Endpoints collects endpoints of all nested Sources and returns them in a single slice.
 func (ms *multiSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
+	log.Debugf("multiSource: collecting endpoints from %d child sources and removing duplicates", len(ms.children))
 	result := []*endpoint.Endpoint{}
 	hasDefaultTargets := len(ms.defaultTargets) > 0
 
@@ -70,7 +72,9 @@ func (ms *multiSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, err
 }
 
 func (ms *multiSource) AddEventHandler(ctx context.Context, handler func()) {
+	log.Debugf("multiSource: adding event handler for %d child sources", len(ms.children))
 	for _, s := range ms.children {
+		log.Debugf("multiSource: adding event handler for child %q", reflect.TypeOf(s).String())
 		s.AddEventHandler(ctx, handler)
 	}
 }
diff --git a/source/wrappers/nat64source.go b/source/wrappers/nat64source.go
index 383bdde5..d1b13f95 100644
--- a/source/wrappers/nat64source.go
+++ b/source/wrappers/nat64source.go
@@ -21,6 +21,8 @@ import (
 	"fmt"
 	"net/netip"
 
+	log "github.com/sirupsen/logrus"
+
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/source"
 )
@@ -38,6 +40,7 @@ func NewNAT64Source(source source.Source, nat64Prefixes []string) source.Source
 
 // Endpoints collects endpoints from its wrapped source and returns them without duplicates.
 func (s *nat64Source) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
+	log.Debug("nat64Source: collecting endpoints and processing NAT64 translation")
 	parsedNAT64Prefixes := make([]netip.Prefix, 0)
 	for _, prefix := range s.nat64Prefixes {
 		pPrefix, err := netip.ParsePrefix(prefix)
@@ -109,5 +112,6 @@ func (s *nat64Source) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
 }
 
 func (s *nat64Source) AddEventHandler(ctx context.Context, handler func()) {
+	log.Debug("nat64Source: adding event handler")
 	s.source.AddEventHandler(ctx, handler)
 }
diff --git a/source/wrappers/targetfiltersource.go b/source/wrappers/targetfiltersource.go
index afc654d9..7cbbaa8c 100644
--- a/source/wrappers/targetfiltersource.go
+++ b/source/wrappers/targetfiltersource.go
@@ -21,9 +21,8 @@ import (
 
 	log "github.com/sirupsen/logrus"
 
-	"sigs.k8s.io/external-dns/source"
-
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source"
 )
 
 // targetFilterSource is a Source that removes endpoints matching the target filter from its wrapped source.
@@ -40,6 +39,7 @@ func NewTargetFilterSource(source source.Source, targetFilter endpoint.TargetFil
 // Endpoints collects endpoints from its wrapped source and returns
 // them without targets matching the target filter.
 func (ms *targetFilterSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
+	log.Debug("targetFilterSource: collecting endpoints from wrapped source and applying target filter")
 	endpoints, err := ms.source.Endpoints(ctx)
 	if err != nil {
 		return nil, err
@@ -75,7 +75,6 @@ func (ms *targetFilterSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoi
 }
 
 func (ms *targetFilterSource) AddEventHandler(ctx context.Context, handler func()) {
-	if ms.targetFilter.IsEnabled() {
-		ms.source.AddEventHandler(ctx, handler)
-	}
+	log.Debug("targetFilterSource: adding event handler")
+	ms.source.AddEventHandler(ctx, handler)
 }



Test patch:
diff --git a/charts/external-dns/tests/rbac_test.yaml b/charts/external-dns/tests/rbac_test.yaml
index c8fcaea2..4658d7ee 100644
--- a/charts/external-dns/tests/rbac_test.yaml
+++ b/charts/external-dns/tests/rbac_test.yaml
@@ -52,6 +52,50 @@ tests:
               resources: ["ingresses"]
               verbs: ["get","watch","list"]
 
+  - it: should create no RBAC rules when no sources are set
+    set:
+      sources: []
+    asserts:
+      - template: clusterrole.yaml
+        equal:
+          path: rules
+          value: null
+
+  - it: should create default RBAC rules for 'ingresses' with source 'ingress'
+    set:
+      sources:
+        - ingress
+    asserts:
+      - template: clusterrole.yaml
+        equal:
+          path: rules
+          value:
+            - apiGroups: ["extensions","networking.k8s.io"]
+              resources: ["ingresses"]
+              verbs: ["get","watch","list"]
+
+  - it: should create default RBAC rules for 'nodes', 'pods', 'services' and 'endpointslices' with source 'service'
+    set:
+      sources:
+        - service
+    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: ["discovery.k8s.io"]
+              resources: ["endpointslices"]
+              verbs: ["get","watch","list"]
+
   - it: should create default RBAC rules for 'ambassador-host'
     set:
       sources:
@@ -391,3 +435,88 @@ tests:
       - isKind:
           of: RoleBinding
         template: clusterrolebinding.yaml
+
+  - it: should create only Role when namespaced=true for istio sources
+    set:
+      namespaced: true
+      sources:
+        - istio-virtualservice
+        - istio-gateway
+    asserts:
+      - isKind:
+          of: Role
+        template: clusterrole.yaml
+      - isKind:
+          of: RoleBinding
+        template: clusterrolebinding.yaml
+      - equal:
+          path: rules
+          value:
+          - apiGroups: [""]
+            resources: ["services"]
+            verbs: ["get","watch","list"]
+          - apiGroups: ["extensions","networking.k8s.io"]
+            resources: ["ingresses"]
+            verbs: ["get","watch","list"]
+          - apiGroups: ["networking.istio.io"]
+            resources: ["gateways"]
+            verbs: ["get","watch","list"]
+          - apiGroups: ["networking.istio.io"]
+            resources: ["virtualservices"]
+            verbs: ["get","watch","list"]
+        template: clusterrole.yaml
+
+  - it: should create only ClusterRole for istio-gateway with ingress permissions
+    set:
+      namespaced: false
+      sources:
+        - istio-gateway
+    asserts:
+      - isKind:
+          of: ClusterRole
+        template: clusterrole.yaml
+      - isKind:
+          of: ClusterRoleBinding
+        template: clusterrolebinding.yaml
+      - equal:
+          path: rules
+          value:
+            - apiGroups: [""]
+              resources: ["services"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["extensions","networking.k8s.io"]
+              resources: ["ingresses"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["networking.istio.io"]
+              resources: ["gateways"]
+              verbs: ["get","watch","list"]
+        template: clusterrole.yaml
+
+  - it: should create only ClusterRole for istio-virtualservice with ingress permissions required
+    set:
+      namespaced: false
+      sources:
+        - istio-virtualservice
+    asserts:
+      - isKind:
+          of: ClusterRole
+        template: clusterrole.yaml
+      - isKind:
+          of: ClusterRoleBinding
+        template: clusterrolebinding.yaml
+      - equal:
+          path: rules
+          value:
+            - apiGroups: [""]
+              resources: ["services"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["extensions","networking.k8s.io"]
+              resources: ["ingresses"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["networking.istio.io"]
+              resources: ["gateways"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["networking.istio.io"]
+              resources: ["virtualservices"]
+              verbs: ["get","watch","list"]
+        template: clusterrole.yaml
diff --git a/controller/controller_test.go b/controller/controller_test.go
index 02cd847f..5abd5677 100644
--- a/controller/controller_test.go
+++ b/controller/controller_test.go
@@ -28,6 +28,7 @@ import (
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/internal/testutils"
 	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
+	"sigs.k8s.io/external-dns/pkg/events/fake"
 	"sigs.k8s.io/external-dns/plan"
 	"sigs.k8s.io/external-dns/provider"
 	"sigs.k8s.io/external-dns/registry"
@@ -216,6 +217,8 @@ func TestRunOnce(t *testing.T) {
 	cfg := getTestConfig()
 	provider := getTestProvider()
 
+	emitter := fake.NewFakeEventEmitter()
+
 	r, err := registry.NewNoopRegistry(provider)
 	require.NoError(t, err)
 
@@ -225,6 +228,7 @@ func TestRunOnce(t *testing.T) {
 		Registry:           r,
 		Policy:             &plan.SyncPolicy{},
 		ManagedRecordTypes: cfg.ManagedDNSRecordTypes,
+		EventEmitter:       emitter,
 	}
 
 	assert.NoError(t, ctrl.RunOnce(context.Background()))
@@ -235,6 +239,8 @@ func TestRunOnce(t *testing.T) {
 
 	testutils.TestHelperVerifyMetricsGaugeVectorWithLabels(t, 1, verifiedRecords.Gauge, map[string]string{"record_type": "a"})
 	testutils.TestHelperVerifyMetricsGaugeVectorWithLabels(t, 1, verifiedRecords.Gauge, map[string]string{"record_type": "aaaa"})
+
+	emitter.AssertNumberOfCalls(t, "Add", 6)
 }
 
 // TestRun tests that Run correctly starts and stops
diff --git a/controller/events_test.go b/controller/events_test.go
new file mode 100644
index 00000000..3f31842a
--- /dev/null
+++ b/controller/events_test.go
@@ -0,0 +1,107 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+
+	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/pkg/events"
+	"sigs.k8s.io/external-dns/pkg/events/fake"
+	"sigs.k8s.io/external-dns/plan"
+)
+
+func TestEmit_RecordReady(t *testing.T) {
+	refObj := &events.ObjectReference{}
+
+	tests := []struct {
+		name    string
+		changes plan.Changes
+		asserts func(em *fake.EventEmitter, ch plan.Changes)
+	}{
+		{
+			name: "create, update and delete endpoints",
+			changes: plan.Changes{
+				Create: []*endpoint.Endpoint{
+					endpoint.NewEndpoint("one.example.com", endpoint.RecordTypeA, "10.10.10.0").WithRefObject(refObj),
+					endpoint.NewEndpoint("two.example.com", endpoint.RecordTypeA, "10.10.10.1").WithRefObject(refObj),
+				},
+				UpdateNew: []*endpoint.Endpoint{
+					endpoint.NewEndpoint("three.example.com", endpoint.RecordTypeA, "10.10.10.2").WithRefObject(refObj),
+					endpoint.NewEndpoint("four.example.com", endpoint.RecordTypeA, "10.10.10.3").WithRefObject(refObj),
+				},
+				Delete: []*endpoint.Endpoint{
+					endpoint.NewEndpoint("five.example.com", endpoint.RecordTypeA, "192.10.10.0").WithRefObject(refObj),
+				},
+			},
+			asserts: func(em *fake.EventEmitter, ch plan.Changes) {
+				for _, ep := range ch.Create {
+					em.AssertCalled(t, "Add", events.NewEvent(ep.RefObject(), ep.Describe(), events.ActionCreate, events.RecordReady))
+				}
+				for _, ep := range ch.Delete {
+					em.AssertCalled(t, "Add", events.NewEvent(ep.RefObject(), ep.Describe(), events.ActionDelete, events.RecordDeleted))
+				}
+				em.AssertNotCalled(t, "Add", mock.MatchedBy(func(e events.Event) bool {
+					return e.EventType() == events.EventTypeWarning
+				}))
+				em.AssertNumberOfCalls(t, "Add", 5)
+			},
+		},
+		{
+			name: "delete endpoints",
+			changes: plan.Changes{
+				Create:    []*endpoint.Endpoint{},
+				UpdateNew: []*endpoint.Endpoint{},
+				Delete: []*endpoint.Endpoint{
+					endpoint.NewEndpoint("five.example.com", endpoint.RecordTypeA, "192.10.10.0").WithRefObject(refObj),
+				},
+			},
+			asserts: func(em *fake.EventEmitter, ch plan.Changes) {
+				for _, ep := range ch.Delete {
+					em.AssertCalled(t, "Add", events.NewEvent(ep.RefObject(), ep.Describe(), events.ActionDelete, events.RecordDeleted))
+				}
+				em.AssertCalled(t, "Add", mock.MatchedBy(func(e events.Event) bool {
+					return e.EventType() == events.EventTypeNormal &&
+						e.Action() == events.ActionDelete &&
+						e.Reason() == events.RecordDeleted
+				}))
+
+				em.AssertNumberOfCalls(t, "Add", 1)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			emitter := fake.NewFakeEventEmitter()
+
+			emitChangeEvent(emitter, tt.changes, events.RecordReady)
+
+			tt.asserts(emitter, tt.changes)
+			mock.AssertExpectationsForObjects(t, emitter)
+		})
+	}
+}
+
+func TestEmit_NilEmitter(t *testing.T) {
+	assert.NotPanics(t, func() {
+		emitChangeEvent(nil, plan.Changes{}, events.RecordError)
+	})
+}
diff --git a/controller/execute_test.go b/controller/execute_test.go
index 924c4c80..3b5e02e5 100644
--- a/controller/execute_test.go
+++ b/controller/execute_test.go
@@ -18,7 +18,6 @@ package controller
 
 import (
 	"bytes"
-	"context"
 	"fmt"
 	"net"
 	"net/http"
@@ -36,8 +35,8 @@ import (
 	"github.com/stretchr/testify/require"
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
-	"sigs.k8s.io/external-dns/plan"
 	"sigs.k8s.io/external-dns/provider"
+	fakeprovider "sigs.k8s.io/external-dns/provider/fakes"
 )
 
 func TestSelectRegistry(t *testing.T) {
@@ -60,7 +59,7 @@ func TestSelectRegistry(t *testing.T) {
 				ExcludeDNSRecordTypes:  []string{"TXT"},
 				TXTCacheInterval:       60,
 			},
-			provider: &MockProvider{},
+			provider: &fakeprovider.MockProvider{},
 			wantErr:  false,
 			wantType: "DynamoDBRegistry",
 		},
@@ -69,7 +68,7 @@ func TestSelectRegistry(t *testing.T) {
 			cfg: &externaldns.Config{
 				Registry: "noop",
 			},
-			provider: &MockProvider{},
+			provider: &fakeprovider.MockProvider{},
 			wantErr:  false,
 			wantType: "NoopRegistry",
 		},
@@ -84,7 +83,7 @@ func TestSelectRegistry(t *testing.T) {
 				ManagedDNSRecordTypes:  []string{"A", "CNAME"},
 				ExcludeDNSRecordTypes:  []string{"TXT"},
 			},
-			provider: &MockProvider{},
+			provider: &fakeprovider.MockProvider{},
 			wantErr:  false,
 			wantType: "TXTRegistry",
 		},
@@ -94,7 +93,7 @@ func TestSelectRegistry(t *testing.T) {
 				Registry:   "aws-sd",
 				TXTOwnerID: "owner-id",
 			},
-			provider: &MockProvider{},
+			provider: &fakeprovider.MockProvider{},
 			wantErr:  false,
 			wantType: "AWSSDRegistry",
 		},
@@ -103,7 +102,7 @@ func TestSelectRegistry(t *testing.T) {
 			cfg: &externaldns.Config{
 				Registry: "unknown",
 			},
-			provider: &MockProvider{},
+			provider: &fakeprovider.MockProvider{},
 			wantErr:  true,
 			wantType: "",
 		},
@@ -477,21 +476,47 @@ func TestBuildSource(t *testing.T) {
 	}
 }
 
-// mocks
-type MockProvider struct{}
-
-func (m *MockProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
-	return nil, nil
-}
-
-func (p *MockProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
-	return nil
-}
+func TestBuildSourceWithWrappers(t *testing.T) {
+	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusNotImplemented)
+	}))
+	defer svr.Close()
 
-func (m *MockProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) {
-	return nil, nil
-}
+	tests := []struct {
+		name    string
+		cfg     *externaldns.Config
+		asserts func(*externaldns.Config)
+	}{
+		{
+			name: "configuration with target filter wrapper",
+			cfg: &externaldns.Config{
+				APIServerURL:    svr.URL,
+				Sources:         []string{"fake"},
+				TargetNetFilter: []string{"10.0.0.0/8"},
+			},
+			asserts: func(cfg *externaldns.Config) {
+				assert.True(t, cfg.IsSourceWrapperInstrumented("target-filter"))
+			},
+		},
+		{
+			name: "configuration without target filter wrapper",
+			cfg: &externaldns.Config{
+				APIServerURL: svr.URL,
+				Sources:      []string{"fake"},
+			},
+			asserts: func(cfg *externaldns.Config) {
+				assert.True(t, cfg.IsSourceWrapperInstrumented("dedup"))
+				assert.True(t, cfg.IsSourceWrapperInstrumented("nat64"))
+				assert.False(t, cfg.IsSourceWrapperInstrumented("target-filter"))
+			},
+		},
+	}
 
-func (m *MockProvider) GetDomainFilter() endpoint.DomainFilterInterface {
-	return nil
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := buildSource(t.Context(), tt.cfg)
+			require.NoError(t, err)
+			tt.asserts(tt.cfg)
+		})
+	}
 }
diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go
index d87aaab3..31b26a15 100644
--- a/endpoint/endpoint_test.go
+++ b/endpoint/endpoint_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"sigs.k8s.io/external-dns/pkg/events"
 )
 
 func TestNewEndpoint(t *testing.T) {
@@ -968,3 +969,16 @@ func TestEndpoint_UniqueOrderedTargets(t *testing.T) {
 		})
 	}
 }
+
+func TestEndpoint_WithRefObject(t *testing.T) {
+	ep := &Endpoint{}
+	ref := &events.ObjectReference{
+		Kind:      "Service",
+		Namespace: "default",
+		Name:      "my-service",
+	}
+	result := ep.WithRefObject(ref)
+
+	assert.Equal(t, ref, ep.RefObject(), "refObject should be set")
+	assert.Equal(t, ep, result, "should return the same Endpoint pointer")
+}
diff --git a/internal/gen/docs/metrics/main_test.go b/internal/gen/docs/metrics/main_test.go
index bea1d75e..dd9884e9 100644
--- a/internal/gen/docs/metrics/main_test.go
+++ b/internal/gen/docs/metrics/main_test.go
@@ -37,7 +37,7 @@ func TestComputeMetrics(t *testing.T) {
 		t.Errorf("Expected not empty metrics registry, got %d", len(reg.Metrics))
 	}
 
-	assert.Len(t, reg.Metrics, 20)
+	assert.Len(t, reg.Metrics, 21)
 }
 
 func TestGenerateMarkdownTableRenderer(t *testing.T) {
diff --git a/internal/idna/idna_test.go b/internal/idna/idna_test.go
new file mode 100644
index 00000000..f3ae93c4
--- /dev/null
+++ b/internal/idna/idna_test.go
@@ -0,0 +1,59 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package idna
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProfileWithDefault(t *testing.T) {
+	tets := []struct {
+		input    string
+		expected string
+	}{
+		{
+			input:    "*.GÖPHER.com",
+			expected: "*.göpher.com",
+		},
+		{
+			input:    "*._abrakadabra.com",
+			expected: "*._abrakadabra.com",
+		},
+		{
+			input:    "_abrakadabra.com",
+			expected: "_abrakadabra.com",
+		},
+		{
+			input:    "*.foo.kube.example.com",
+			expected: "*.foo.kube.example.com",
+		},
+		{
+			input:    "xn--bcher-kva.example.com",
+			expected: "bücher.example.com",
+		},
+	}
+	for _, tt := range tets {
+		t.Run(strings.ToLower(tt.input), func(t *testing.T) {
+			result, err := Profile.ToUnicode(tt.input)
+			assert.NoError(t, err)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
diff --git a/internal/testutils/mock_source.go b/internal/testutils/mock_source.go
index 4819e211..99644cb0 100644
--- a/internal/testutils/mock_source.go
+++ b/internal/testutils/mock_source.go
@@ -28,10 +28,20 @@ import (
 // MockSource returns mock endpoints.
 type MockSource struct {
 	mock.Mock
+	endpoints []*endpoint.Endpoint
+}
+
+func NewMockSource(endpoints ...*endpoint.Endpoint) *MockSource {
+	m := &MockSource{
+		endpoints: endpoints,
+	}
+	m.On("Endpoints").Return(endpoints, nil)
+	m.On("AddEventHandler", mock.AnythingOfType("*context.cancelCtx")).Return()
+	return m
 }
 
 // Endpoints returns the desired mock endpoints.
-func (m *MockSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
+func (m *MockSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) {
 	args := m.Called()
 
 	endpoints := args.Get(0)
@@ -44,6 +54,10 @@ func (m *MockSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 
 // AddEventHandler adds an event handler that should be triggered if something in source changes
 func (m *MockSource) AddEventHandler(ctx context.Context, handler func()) {
+	m.Called(ctx)
+	if handler == nil {
+		return
+	}
 	go func() {
 		ticker := time.NewTicker(5 * time.Second)
 		defer ticker.Stop()
diff --git a/pkg/events/controller_test.go b/pkg/events/controller_test.go
new file mode 100644
index 00000000..3141f0a7
--- /dev/null
+++ b/pkg/events/controller_test.go
@@ -0,0 +1,180 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package events
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	v1 "k8s.io/api/core/v1"
+	eventsv1 "k8s.io/api/events/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/kubernetes/fake"
+	"k8s.io/client-go/util/workqueue"
+
+	clienttesting "k8s.io/client-go/testing"
+)
+
+func TestNewEventController_Success(t *testing.T) {
+	svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+	defer svr.Close()
+
+	mockKubeCfgDir := filepath.Join(t.TempDir(), ".kube")
+	mockKubeCfgPath := filepath.Join(mockKubeCfgDir, "config")
+	err := os.MkdirAll(mockKubeCfgDir, 0755)
+	require.NoError(t, err)
+
+	kubeCfgTemplate := `
+apiVersion: v1
+kind: Config
+clusters:
+- cluster:
+    server: %s
+  name: test-cluster
+contexts:
+- context:
+    cluster: test-cluster
+    user: test-user
+  name: test-context
+current-context: test-context
+users:
+- name: test-user
+  user:
+    token: fake-token
+`
+	err = os.WriteFile(mockKubeCfgPath, []byte(fmt.Sprintf(kubeCfgTemplate, svr.URL)), os.FileMode(0755))
+	require.NoError(t, err)
+
+	cfg := NewConfig(
+		WithKubeConfig(mockKubeCfgPath, svr.URL, 0),
+		WithEmitEvents([]string{string(RecordReady)}),
+	)
+	ctrl, err := NewEventController(cfg)
+	require.NoError(t, err)
+	require.NotNil(t, ctrl)
+	require.False(t, ctrl.dryRun)
+}
+
+func TestController_Run_NoEmitEvents(t *testing.T) {
+	kClient := fake.NewClientset()
+	ctrl := &Controller{
+		client:     kClient.EventsV1(),
+		emitEvents: sets.New[Reason](),
+	}
+
+	require.NotPanics(t, func() {
+		ctrl.Run(t.Context())
+	})
+}
+
+func TestController_Run_EmitEvents(t *testing.T) {
+	log.SetLevel(log.ErrorLevel)
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	eventCreated := make(chan struct{})
+	kubeClient := fake.NewClientset()
+	kubeClient.PrependReactor("create", "events", func(action clienttesting.Action) (bool, runtime.Object, error) {
+		eventCreated <- struct{}{}
+		return true, nil, nil
+	})
+
+	eventsClient := kubeClient.EventsV1()
+	ctrl := &Controller{
+		client:     eventsClient,
+		emitEvents: sets.New[Reason](RecordReady),
+		queue: workqueue.NewTypedRateLimitingQueueWithConfig[any](
+			workqueue.DefaultTypedControllerRateLimiter[any](),
+			workqueue.TypedRateLimitingQueueConfig[any]{Name: controllerName},
+		),
+		hostname:        controllerName,
+		maxQueuedEvents: 1,
+	}
+
+	ctrl.Run(ctx)
+
+	event := NewEvent(NewObjectReference(&v1.Pod{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Pod",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "fake-object",
+			Namespace: v1.NamespaceDefault,
+			UID:       "9de3fc19-8aeb-4e76-865d-ada955403103",
+		},
+	}, "fake-source"), "record created", ActionCreate, RecordReady)
+
+	ctrl.Add(event)
+
+	select {
+	case <-eventCreated:
+	case <-time.After(wait.ForeverTestTimeout):
+		t.Fatal("event not created")
+	}
+}
+
+func TestController_Queue_EmitEvents(t *testing.T) {
+	log.SetLevel(log.ErrorLevel)
+
+	eventsClient := fake.NewClientset().EventsV1()
+	ctrl := &Controller{
+		client:     eventsClient,
+		emitEvents: sets.New[Reason](RecordReady),
+		queue: workqueue.NewTypedRateLimitingQueueWithConfig[any](
+			workqueue.DefaultTypedControllerRateLimiter[any](),
+			workqueue.TypedRateLimitingQueueConfig[any]{Name: controllerName},
+		),
+		hostname:        controllerName,
+		maxQueuedEvents: 1,
+	}
+
+	event := NewEvent(NewObjectReference(&v1.Pod{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Pod",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "fake-object",
+			Namespace: v1.NamespaceDefault,
+			UID:       "9de3fc19-8aeb-4e76-865d-ada955403103",
+		},
+	}, "fake-source"), "record created", ActionCreate, RecordReady)
+
+	ctrl.Add(event)
+
+	queueItem, shutdown := ctrl.queue.Get()
+	require.False(t, shutdown)
+	value, ok := queueItem.(*eventsv1.Event)
+	assert.True(t, ok)
+	assert.NotNil(t, value)
+
+	assert.Contains(t, value.Name, "fake-object.")
+	assert.Contains(t, value.Reason, RecordReady)
+}
diff --git a/pkg/events/types_test.go b/pkg/events/types_test.go
new file mode 100644
index 00000000..66c7b44d
--- /dev/null
+++ b/pkg/events/types_test.go
@@ -0,0 +1,235 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package events
+
+import (
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+	apiv1 "k8s.io/api/core/v1"
+	eventsv1 "k8s.io/api/events/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+func TestSanitize(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected string // expected prefix of sanitized output
+	}{
+		{"My.Resource_1", "my.resource-1."},
+		{"!@#bad*chars", "a---bad-chars."},
+		{"-start", "a-start."},
+		{"end-", "end-z."},
+		{"-both-", "a-both-z."},
+		{"", "a."},
+		{"ALLCAPS", "allcaps."},
+		{"foo.bar", "foo.bar."},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.input, func(t *testing.T) {
+			result := sanitize(tt.input)
+			require.True(t, strings.HasPrefix(result, tt.expected), "expected prefix %q, got %q", tt.expected, result)
+			require.Greater(t, len(result), len(tt.expected))
+		})
+	}
+}
+
+func TestEvent_Reference(t *testing.T) {
+	tests := []struct {
+		kind      string
+		namespace string
+		name      string
+		expected  string
+	}{
+		{"Pod", "default", "nginx", "Pod/default/nginx"},
+		{"Service", "prod", "api", "Service/prod/api"},
+		{"", "", "", "//"},
+	}
+
+	for _, tt := range tests {
+		ev := Event{
+			ref: ObjectReference{
+				Kind:      tt.kind,
+				Namespace: tt.namespace,
+				Name:      tt.name,
+				Source:    "fake-source",
+			},
+		}
+		require.Equal(t, tt.expected, ev.description())
+	}
+}
+
+func TestEvent_NewEvents(t *testing.T) {
+	tests := []struct {
+		name    string
+		event   Event
+		asserts func(e *eventsv1.Event)
+	}{
+		{
+			name:  "empty event",
+			event: NewEvent(nil, "", ActionCreate, RecordReady),
+			asserts: func(e *eventsv1.Event) {
+				require.Nil(t, e)
+			},
+		},
+		{
+			name: "event without uuid",
+			event: NewEvent(NewObjectReference(&apiv1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "apiv1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "fake-pod",
+					Namespace: apiv1.NamespaceDefault,
+				},
+			}, "fake"), "", ActionCreate, RecordReady),
+			asserts: func(e *eventsv1.Event) {
+				require.NotNil(t, e)
+				require.Contains(t, e.Name, "fake-pod.")
+				require.Equal(t, apiv1.NamespaceDefault, e.Namespace)
+				require.Nil(t, e.Related)
+				require.Equal(t, apiv1.ObjectReference{}, e.Regarding)
+			},
+		},
+		{
+			name: "event with uuid",
+			event: NewEvent(NewObjectReference(&apiv1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "apiv1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "fake-pod",
+					Namespace: apiv1.NamespaceDefault,
+					UID:       "9de3fc19-8aeb-4e76-865d-ada955403103",
+				},
+			}, "fake"), "", ActionCreate, RecordReady),
+			asserts: func(e *eventsv1.Event) {
+				require.NotNil(t, e)
+				require.Contains(t, e.Name, "fake-pod.")
+				require.NotNil(t, e.Related)
+				require.NotNil(t, e.Regarding)
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.asserts(tt.event.event())
+		})
+	}
+}
+
+func TestEvent_Transpose(t *testing.T) {
+	ev := NewEvent(&ObjectReference{
+		Kind:      "Pod",
+		Namespace: "default",
+		Name:      "nginx",
+	}, "test message", ActionCreate, RecordReady)
+
+	event := ev.event()
+	require.NotNil(t, event)
+	require.Contains(t, event.ObjectMeta.Name, ev.ref.Name)
+	require.Equal(t, "default", event.ObjectMeta.Namespace)
+	require.Equal(t, string(ActionCreate), event.Action)
+	require.Equal(t, string(RecordReady), event.Reason)
+	require.Equal(t, "test message", event.Note)
+	require.Equal(t, apiv1.EventTypeNormal, event.Type)
+	require.Equal(t, controllerName, event.ReportingController)
+	require.Contains(t, event.ReportingInstance, controllerName+"/source/")
+
+	longMsg := strings.Repeat("a", 2000)
+	ev.message = longMsg
+	event = ev.event()
+	require.Equal(t, longMsg[:1021]+"...", event.Note)
+
+	ev.ref.Name = ""
+	require.Nil(t, ev.event())
+}
+
+func TestWithEmitEvents(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []string
+		expected sets.Set[Reason]
+		assert   func(c *Config)
+	}{
+		{
+			name:     "valid events",
+			input:    []string{string(RecordReady), string(RecordError)},
+			expected: sets.New[Reason](RecordReady, RecordError),
+			assert: func(c *Config) {
+				require.Equal(t, sets.New[Reason](RecordReady, RecordError), c.emitEvents)
+				require.True(t, c.IsEnabled())
+			},
+		},
+		{
+			name:     "invalid event",
+			input:    []string{"InvalidEvent"},
+			expected: sets.New[Reason](),
+			assert: func(c *Config) {
+				require.Equal(t, sets.New[Reason](), c.emitEvents)
+				require.False(t, c.IsEnabled())
+			},
+		},
+		{
+			name:     "mixed valid and invalid",
+			input:    []string{string(RecordReady), "InvalidEvent"},
+			expected: sets.New[Reason](RecordReady),
+			assert: func(c *Config) {
+				require.Equal(t, sets.New[Reason](RecordReady), c.emitEvents)
+				require.True(t, c.IsEnabled())
+			},
+		},
+		{
+			name:     "empty input",
+			input:    []string{},
+			expected: nil,
+			assert: func(c *Config) {
+				require.NotNil(t, c)
+				require.False(t, c.IsEnabled())
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			cfg := &Config{}
+			opt := WithEmitEvents(tt.input)
+			opt(cfg)
+			tt.assert(cfg)
+		})
+	}
+}
+
+func TestWithKubeConfig(t *testing.T) {
+	kubeConfig := "/path/to/kubeconfig"
+	apiServerURL := "https://api.server"
+	timeout := 5 * time.Second
+
+	cfg := &Config{}
+	opt := WithKubeConfig(kubeConfig, apiServerURL, timeout)
+	opt(cfg)
+
+	require.Equal(t, kubeConfig, cfg.kubeConfig)
+	require.Equal(t, apiServerURL, cfg.apiServerURL)
+	require.Equal(t, timeout, cfg.timeout)
+}
diff --git a/pkg/http/http_benchmark_test.go b/pkg/http/http_benchmark_test.go
new file mode 100644
index 00000000..efdf4e21
--- /dev/null
+++ b/pkg/http/http_benchmark_test.go
@@ -0,0 +1,97 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package http
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"sync"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+type roundTripFunc func(req *http.Request) *http.Response
+
+func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
+	return f(req), nil
+}
+
+// newTestClient returns *http.client with Transport replaced to avoid making real calls
+func newTestClient(fn roundTripFunc) *http.Client {
+	return &http.Client{
+		Transport: NewInstrumentedTransport(fn),
+	}
+}
+
+type apiUnderTest struct {
+	client  *http.Client
+	baseURL string
+}
+
+func (api *apiUnderTest) doStuff() ([]byte, error) {
+	resp, err := api.client.Get(api.baseURL + "/some/path")
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	return io.ReadAll(resp.Body)
+}
+
+func BenchmarkRoundTripper(b *testing.B) {
+	client := newTestClient(func(req *http.Request) *http.Response {
+		return &http.Response{
+			StatusCode: http.StatusOK,
+			Body:       io.NopCloser(bytes.NewBufferString(`OK`)),
+			Header:     make(http.Header),
+		}
+	})
+
+	for b.Loop() {
+		api := apiUnderTest{client, "http://example.com"}
+		body, err := api.doStuff()
+		require.NoError(b, err)
+		assert.Equal(b, []byte("OK"), body)
+	}
+}
+
+func TestRoundTripper_Concurrent(t *testing.T) {
+	client := newTestClient(func(req *http.Request) *http.Response {
+		return &http.Response{
+			StatusCode: http.StatusOK,
+			Body:       io.NopCloser(bytes.NewBufferString(`OK`)),
+			Header:     make(http.Header),
+		}
+	})
+	api := &apiUnderTest{client: client, baseURL: "http://example.com"}
+
+	const numGoroutines = 100
+	var wg sync.WaitGroup
+	wg.Add(numGoroutines)
+
+	for i := 0; i < numGoroutines; i++ {
+		go func() {
+			defer wg.Done()
+			body, err := api.doStuff()
+			assert.NoError(t, err)
+			assert.Equal(t, []byte("OK"), body)
+		}()
+	}
+	wg.Wait()
+}
diff --git a/pkg/http/http_test.go b/pkg/http/http_test.go
index cd17f387..3fc558bd 100644
--- a/pkg/http/http_test.go
+++ b/pkg/http/http_test.go
@@ -18,9 +18,12 @@ package http
 
 import (
 	"fmt"
+	"io"
 	"net/http"
+	"net/url"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -58,24 +61,97 @@ func TestNewInstrumentedClient(t *testing.T) {
 	require.True(t, ok)
 }
 
-func TestPathProcessor(t *testing.T) {
-	tests := []struct {
-		input    string
-		expected string
+func TestCancelRequest(t *testing.T) {
+	for _, tt := range []struct {
+		title              string
+		customRoundTripper CustomRoundTripper
+		request            *http.Request
 	}{
-		{"/foo/bar", "bar"},
-		{"/foo/", ""},
-		{"/", ""},
-		{"", ""},
-		{"/foo/bar/baz", "baz"},
-		{"foo/bar", "bar"},
-		{"foo", "foo"},
-		{"foo/", ""},
+		{
+			title:              "CancelRequest does nothing",
+			customRoundTripper: CustomRoundTripper{},
+			request:            &http.Request{},
+		},
+	} {
+		t.Run(tt.title, func(t *testing.T) {
+			tt.customRoundTripper.CancelRequest(tt.request)
+		})
 	}
+}
+
+type mockRoundTripper struct {
+	response *http.Response
+	error    error
+}
+
+func (mrt mockRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
+	return mrt.response, mrt.error
+}
+
+func TestRoundTrip(t *testing.T) {
+	for _, tt := range []struct {
+		title            string
+		nextRoundTripper mockRoundTripper
+		request          *http.Request
+		method           string
+		url              string
+		body             io.Reader
+
+		expectError      bool
+		expectedResponse *http.Response
+	}{
+		{
+			title:            "RoundTrip returns no error",
+			nextRoundTripper: mockRoundTripper{},
+			request: &http.Request{
+				Method: http.MethodGet,
+				URL: &url.URL{
+					Scheme: "HTTPS",
+					Host:   "test.local",
+					Path:   "/path",
+				},
+				Body: nil,
+			},
+			expectError:      false,
+			expectedResponse: nil,
+		},
+		{
+			title: "RoundTrip extracts status from request",
+			nextRoundTripper: mockRoundTripper{
+				response: &http.Response{
+					StatusCode: http.StatusOK,
+				},
+			},
+			request: &http.Request{
+				Method: http.MethodGet,
+				URL: &url.URL{
+					Scheme: "HTTPS",
+					Host:   "test.local",
+					Path:   "/path",
+				},
+				Body: nil,
+			},
+			expectError: false,
+			expectedResponse: &http.Response{
+				StatusCode: http.StatusOK,
+			},
+		},
+	} {
+		t.Run(tt.title, func(t *testing.T) {
+			req, err := http.NewRequest(tt.method, tt.url, tt.body)
+			customRoundTripper := CustomRoundTripper{
+				next: tt.nextRoundTripper,
+			}
+
+			resp, err := customRoundTripper.RoundTrip(req)
+
+			if tt.expectError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
 
-	for _, tt := range tests {
-		t.Run(tt.input, func(t *testing.T) {
-			require.Equal(t, tt.expected, pathProcessor(tt.input))
+			assert.Equal(t, tt.expectedResponse, resp)
 		})
 	}
 }
diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go
index 16cf52f5..3406577c 100644
--- a/pkg/metrics/metrics_test.go
+++ b/pkg/metrics/metrics_test.go
@@ -61,8 +61,9 @@ func TestMustRegister(t *testing.T) {
 				NewCounterWithOpts(prometheus.CounterOpts{Name: "test_counter_3"}),
 				NewCounterVecWithOpts(prometheus.CounterOpts{Name: "test_counter_vec_3"}, []string{"label"}),
 				NewGaugedVectorOpts(prometheus.GaugeOpts{Name: "test_gauge_v_3"}, []string{"label"}),
+				NewSummaryVecWithOpts(prometheus.SummaryOpts{Name: "test_summary_v_3"}, []string{"label"}),
 			},
-			expected: 4,
+			expected: 5,
 		},
 		{
 			name: "unsupported metric",
@@ -71,6 +72,14 @@ func TestMustRegister(t *testing.T) {
 			},
 			expected: 0,
 		},
+		{
+			name: "skip if metric exists",
+			metrics: []IMetric{
+				NewGaugeWithOpts(prometheus.GaugeOpts{Name: "existing_metric"}),
+				NewGaugeWithOpts(prometheus.GaugeOpts{Name: "existing_metric"}),
+			},
+			expected: 1,
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/pkg/metrics/models_test.go b/pkg/metrics/models_test.go
index 738f9f6e..8aa67b4e 100644
--- a/pkg/metrics/models_test.go
+++ b/pkg/metrics/models_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	dto "github.com/prometheus/client_model/go"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestNewGaugeWithOpts(t *testing.T) {
@@ -112,19 +113,130 @@ func TestGaugeV_SetWithLabels(t *testing.T) {
 	assert.Len(t, m.Label, 2)
 }
 
-func TestNewBuildInfoCollector(t *testing.T) {
-	metric := NewGaugeFuncMetric(prometheus.GaugeOpts{
-		Namespace: Namespace,
-		Name:      "build_info",
-		ConstLabels: prometheus.Labels{
-			"version":   "0.0.1",
-			"goversion": "1.24",
-			"arch":      "arm64",
+func TestNewGaugeFuncMetric(t *testing.T) {
+	tests := []struct {
+		name                    string
+		metricName              string
+		subSystem               string
+		constLabels             prometheus.Labels
+		expectedFqName          string
+		expectedDescString      string
+		expectedGaugeFuncReturn float64
+	}{
+		{
+			name:       "NewGaugeFuncMetric returns build_info",
+			metricName: "build_info",
+			subSystem:  "",
+			constLabels: prometheus.Labels{
+				"version":   "0.0.1",
+				"goversion": "1.24",
+				"arch":      "arm64",
+			},
+			expectedFqName:          "external_dns_build_info",
+			expectedDescString:      "version=\"0.0.1\"",
+			expectedGaugeFuncReturn: 1,
 		},
-	})
+		{
+			name:                    "NewGaugeFuncMetric subsystem alters name",
+			metricName:              "metricName",
+			subSystem:               "subSystem",
+			constLabels:             prometheus.Labels{},
+			expectedFqName:          "external_dns_subSystem_metricName",
+			expectedDescString:      "",
+			expectedGaugeFuncReturn: 1,
+		},
+		{
+			name:                    "NewGaugeFuncMetric GaugeFunc returns 1",
+			metricName:              "metricName",
+			subSystem:               "",
+			constLabels:             prometheus.Labels{},
+			expectedFqName:          "external_dns_metricName",
+			expectedDescString:      "",
+			expectedGaugeFuncReturn: 1,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			metric := NewGaugeFuncMetric(prometheus.GaugeOpts{
+				Namespace:   Namespace,
+				Name:        tt.metricName,
+				Subsystem:   tt.subSystem,
+				ConstLabels: tt.constLabels,
+			})
+
+			desc := metric.GaugeFunc.Desc()
+
+			assert.Equal(t, tt.expectedFqName, reflect.ValueOf(desc).Elem().FieldByName("fqName").String())
+			assert.Contains(t, desc.String(), tt.expectedDescString)
+
+			testRegistry := prometheus.NewRegistry()
+			err := testRegistry.Register(metric.GaugeFunc)
+			require.NoError(t, err)
+
+			metricFamily, err := testRegistry.Gather()
+			require.NoError(t, err)
+			require.Len(t, metricFamily, 1)
+
+			require.NotNil(t, metricFamily[0].Metric[0].Gauge)
+			assert.InDelta(t, tt.expectedGaugeFuncReturn, metricFamily[0].Metric[0].GetGauge().GetValue(), 0.0001)
+		})
+	}
+}
+
+func TestSummaryV_SetWithLabels(t *testing.T) {
+	opts := prometheus.SummaryOpts{
+		Name:      "test_summaryVec",
+		Namespace: "test_ns",
+		Subsystem: "test_sub",
+		Help:      "help text",
+	}
+
+	labels := Labels{}
+	sv := NewSummaryVecWithOpts(opts, []string{"label1", "label2"})
+
+	labels["label1"] = "alpha"
+	labels["label2"] = "beta"
+
+	sv.SetWithLabels(5.01, labels)
 
-	desc := metric.GaugeFunc.Desc()
+	reg := prometheus.NewRegistry()
+	reg.MustRegister(sv.SummaryVec)
+
+	metricsFamilies, err := reg.Gather()
+	assert.NoError(t, err)
+	assert.Len(t, metricsFamilies, 1)
+
+	s, err := sv.SummaryVec.GetMetricWithLabelValues("alpha", "beta")
+	assert.NoError(t, err)
+	metricsFamilies, err = reg.Gather()
 
-	assert.Equal(t, "external_dns_build_info", reflect.ValueOf(desc).Elem().FieldByName("fqName").String())
-	assert.Contains(t, desc.String(), "version=\"0.0.1\"")
+	s.Observe(5.21)
+	metricsFamilies, err = reg.Gather()
+	assert.NoError(t, err)
+
+	assert.InDelta(t, 10.22, *metricsFamilies[0].Metric[0].Summary.SampleSum, 0.01)
+	assert.Len(t, metricsFamilies[0].Metric[0].Label, 2)
+}
+
+func TestPathProcessor(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{"/foo/bar", "bar"},
+		{"/foo/", ""},
+		{"/", ""},
+		{"", ""},
+		{"/foo/bar/baz", "baz"},
+		{"foo/bar", "bar"},
+		{"foo", "foo"},
+		{"foo/", ""},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.input, func(t *testing.T) {
+			require.Equal(t, tt.expected, PathProcessor(tt.input))
+		})
+	}
 }
diff --git a/provider/aws/config_test.go b/provider/aws/config_test.go
index 00b3b46a..ec6ef021 100644
--- a/provider/aws/config_test.go
+++ b/provider/aws/config_test.go
@@ -62,6 +62,19 @@ func Test_newV2Config(t *testing.T) {
 		assert.Equal(t, "AKIAIOSFODNN7EXAMPLE", creds.AccessKeyID)
 		assert.Equal(t, "topsecret", creds.SecretAccessKey)
 	})
+
+	t.Run("should not error when AWS_CA_BUNDLE set", func(t *testing.T) {
+		// setup
+		os.Setenv("AWS_CA_BUNDLE", "../../internal/testresources/ca.pem")
+		defer os.Unsetenv("AWS_CA_BUNDLE")
+
+		// when
+		_, err := newV2Config(AWSSessionConfig{})
+		require.NoError(t, err)
+
+		// then
+		assert.NoError(t, err)
+	})
 }
 
 func prepareCredentialsFile(t *testing.T) (*os.File, error) {
diff --git a/provider/aws/instrumented_config_test.go b/provider/aws/instrumented_config_test.go
new file mode 100644
index 00000000..397b599d
--- /dev/null
+++ b/provider/aws/instrumented_config_test.go
@@ -0,0 +1,102 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"context"
+	"net/http"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/aws/smithy-go/middleware"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	smithyhttp "github.com/aws/smithy-go/transport/http"
+)
+
+func Test_GetInstrumentationMiddlewares(t *testing.T) {
+	t.Run("adds expected middlewares", func(t *testing.T) {
+		stack := middleware.NewStack("test-stack", nil)
+
+		for _, mw := range GetInstrumentationMiddlewares() {
+			err := mw(stack)
+			require.NoError(t, err)
+		}
+
+		// Check Initialize stage
+		timedOperationMiddleware, found := stack.Initialize.Get("timedOperation")
+		assert.True(t, found, "timedOperation middleware should be present in Initialize stage")
+		assert.NotNil(t, timedOperationMiddleware)
+
+		// Check Deserialize stage
+		extractAWSRequestParametersMiddleware, found := stack.Deserialize.Get("extractAWSRequestParameters")
+		assert.True(t, found, "extractAWSRequestParameters middleware should be present in Deserialize stage")
+		assert.NotNil(t, extractAWSRequestParametersMiddleware)
+	})
+}
+
+type MockInitializeHandler struct {
+	CapturedContext context.Context
+}
+
+func (mock *MockInitializeHandler) HandleInitialize(ctx context.Context, in middleware.InitializeInput) (middleware.InitializeOutput, middleware.Metadata, error) {
+	mock.CapturedContext = ctx
+
+	return middleware.InitializeOutput{}, middleware.Metadata{}, nil
+}
+
+func Test_InitializedTimedOperationMiddleware(t *testing.T) {
+	testContext := context.Background()
+	mockInitializeHandler := &MockInitializeHandler{}
+
+	_, _, err := initializeTimedOperationMiddleware.HandleInitialize(testContext, middleware.InitializeInput{}, mockInitializeHandler)
+	require.NoError(t, err)
+
+	requestMetrics := middleware.GetStackValue(mockInitializeHandler.CapturedContext, requestMetricsKey{}).(requestMetrics)
+	assert.NotNil(t, requestMetrics.StartTime)
+}
+
+type MockDeserializeHandler struct {
+}
+
+func (mock *MockDeserializeHandler) HandleDeserialize(ctx context.Context, in middleware.DeserializeInput) (middleware.DeserializeOutput, middleware.Metadata, error) {
+	return middleware.DeserializeOutput{}, middleware.Metadata{}, nil
+}
+
+func Test_ExtractAWSRequestParameters(t *testing.T) {
+	testContext := context.Background()
+	middleware.WithStackValue(testContext, requestMetricsKey{}, requestMetrics{StartTime: time.Now()})
+
+	mockDeserializeHandler := &MockDeserializeHandler{}
+
+	deserializeInput := middleware.DeserializeInput{
+		Request: &smithyhttp.Request{
+			Request: &http.Request{
+				Method: http.MethodGet,
+				URL: &url.URL{
+					Host:   "example.com",
+					Scheme: "HTTPS",
+					Path:   "/testPath",
+				},
+			},
+		},
+	}
+	_, _, err := extractAWSRequestParameters.HandleDeserialize(testContext, deserializeInput, mockDeserializeHandler)
+	require.NoError(t, err)
+}
diff --git a/provider/awssd/aws_sd_test.go b/provider/awssd/aws_sd_test.go
index 3bf59220..3cb4e758 100644
--- a/provider/awssd/aws_sd_test.go
+++ b/provider/awssd/aws_sd_test.go
@@ -254,14 +254,14 @@ func TestAWSSDProvider_ApplyChanges_Update(t *testing.T) {
 	ctx := context.Background()
 
 	// apply creates
-	provider.ApplyChanges(ctx, &plan.Changes{
+	_ = provider.ApplyChanges(ctx, &plan.Changes{
 		Create: oldEndpoints,
 	})
 
 	ctx = context.Background()
 
 	// apply update
-	provider.ApplyChanges(ctx, &plan.Changes{
+	_ = provider.ApplyChanges(ctx, &plan.Changes{
 		UpdateOld: oldEndpoints,
 		UpdateNew: newEndpoints,
 	})
@@ -306,6 +306,7 @@ func TestAWSSDProvider_ListNamespaces(t *testing.T) {
 	}{
 		{"public filter", endpoint.NewDomainFilter([]string{}), "public", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
 		{"private filter", endpoint.NewDomainFilter([]string{}), "private", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["private"])}},
+		{"optional filter", endpoint.NewDomainFilter([]string{}), "", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"]), namespaceToNamespaceSummary(namespaces["private"])}},
 		{"domain filter", endpoint.NewDomainFilter([]string{"public.com"}), "", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
 		{"non-existing domain", endpoint.NewDomainFilter([]string{"xxx.com"}), "", []*sdtypes.NamespaceSummary{}},
 	} {
@@ -913,7 +914,7 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) {
 	}
 
 	// AWS NLB instance (ALIAS)
-	provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{
+	_ = provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{
 		RecordType: endpoint.RecordTypeCNAME,
 		DNSName:    "service1.private.com.",
 		RecordTTL:  300,
@@ -927,7 +928,7 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) {
 	}
 
 	// CNAME instance
-	provider.RegisterInstance(context.Background(), services["private"]["cname-srv"], &endpoint.Endpoint{
+	_ = provider.RegisterInstance(context.Background(), services["private"]["cname-srv"], &endpoint.Endpoint{
 		RecordType: endpoint.RecordTypeCNAME,
 		DNSName:    "service2.private.com.",
 		RecordTTL:  300,
@@ -1001,7 +1002,7 @@ func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
 
 	provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
 
-	provider.DeregisterInstance(context.Background(), services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))
+	_ = provider.DeregisterInstance(context.Background(), services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))
 
 	assert.Empty(t, instances["srv1"])
 }
diff --git a/provider/azure/config_test.go b/provider/azure/config_test.go
index 3b993cfe..89542e96 100644
--- a/provider/azure/config_test.go
+++ b/provider/azure/config_test.go
@@ -45,7 +45,8 @@ func TestGetCloudConfiguration(t *testing.T) {
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
-			cloudCfg, err := getCloudConfiguration(test.cloudName)
+			cfg := config{Cloud: test.cloudName}
+			cloudCfg, err := getCloudConfiguration(cfg)
 			if err != nil {
 				t.Errorf("got unexpected err %v", err)
 			}
diff --git a/provider/cloudflare/cloudflare_regional_test.go b/provider/cloudflare/cloudflare_regional_test.go
index aee88dfe..b114adaf 100644
--- a/provider/cloudflare/cloudflare_regional_test.go
+++ b/provider/cloudflare/cloudflare_regional_test.go
@@ -23,8 +23,8 @@ import (
 	"strings"
 	"testing"
 
-	cloudflare "github.com/cloudflare/cloudflare-go"
-	"github.com/cloudflare/cloudflare-go/v4/addressing"
+	"github.com/cloudflare/cloudflare-go/v5/addressing"
+	"github.com/cloudflare/cloudflare-go/v5/dns"
 	log "github.com/sirupsen/logrus"
 
 	"github.com/stretchr/testify/assert"
@@ -32,6 +32,7 @@ import (
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/internal/testutils"
 	"sigs.k8s.io/external-dns/plan"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 func (m *mockCloudFlareClient) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] {
@@ -103,14 +104,14 @@ func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx contex
 func TestCloudflareRegionalHostnameActions(t *testing.T) {
 	tests := []struct {
 		name              string
-		records           map[string]cloudflare.DNSRecord
+		records           map[string]dns.RecordResponse
 		regionalHostnames []regionalHostname
 		endpoints         []*endpoint.Endpoint
 		want              []MockAction
 	}{
 		{
 			name:              "create",
-			records:           map[string]cloudflare.DNSRecord{},
+			records:           map[string]dns.RecordResponse{},
 			regionalHostnames: []regionalHostname{},
 			endpoints: []*endpoint.Endpoint{
 				{
@@ -130,13 +131,13 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
 					Name:     "Create",
 					ZoneId:   "001",
 					RecordId: generateDNSRecordID("A", "create.bar.com", "127.0.0.1"),
-					RecordData: cloudflare.DNSRecord{
+					RecordData: dns.RecordResponse{
 						ID:      generateDNSRecordID("A", "create.bar.com", "127.0.0.1"),
 						Type:    "A",
 						Name:    "create.bar.com",
 						Content: "127.0.0.1",
 						TTL:     1,
-						Proxied: proxyDisabled,
+						Proxied: false,
 					},
 				},
 				{
@@ -151,14 +152,14 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
 		},
 		{
 			name: "Update",
-			records: map[string]cloudflare.DNSRecord{
+			records: map[string]dns.RecordResponse{
 				"update.bar.com": {
 					ID:      generateDNSRecordID("A", "update.bar.com", "127.0.0.1"),
 					Type:    "A",
 					Name:    "update.bar.com",
 					Content: "127.0.0.1",
 					TTL:     1,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 			},
 			regionalHostnames: []regionalHostname{
@@ -185,13 +186,13 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
 					Name:     "Update",
 					ZoneId:   "001",
 					RecordId: generateDNSRecordID("A", "update.bar.com", "127.0.0.1"),
-					RecordData: cloudflare.DNSRecord{
+					RecordData: dns.RecordResponse{
 						ID:      generateDNSRecordID("A", "update.bar.com", "127.0.0.1"),
 						Type:    "A",
 						Name:    "update.bar.com",
 						Content: "127.0.0.1",
 						TTL:     1,
-						Proxied: proxyDisabled,
+						Proxied: false,
 					},
 				},
 				{
@@ -206,14 +207,14 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
 		},
 		{
 			name: "Delete",
-			records: map[string]cloudflare.DNSRecord{
+			records: map[string]dns.RecordResponse{
 				"update.bar.com": {
 					ID:      generateDNSRecordID("A", "delete.bar.com", "127.0.0.1"),
 					Type:    "A",
 					Name:    "delete.bar.com",
 					Content: "127.0.0.1",
 					TTL:     1,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 			},
 			regionalHostnames: []regionalHostname{
@@ -228,7 +229,7 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
 					Name:       "Delete",
 					ZoneId:     "001",
 					RecordId:   generateDNSRecordID("A", "delete.bar.com", "127.0.0.1"),
-					RecordData: cloudflare.DNSRecord{},
+					RecordData: dns.RecordResponse{},
 				},
 				{
 					Name:   "DeleteDataLocalizationRegionalHostname",
@@ -241,14 +242,14 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
 		},
 		{
 			name: "No change",
-			records: map[string]cloudflare.DNSRecord{
+			records: map[string]dns.RecordResponse{
 				"nochange.bar.com": {
 					ID:      generateDNSRecordID("A", "nochange.bar.com", "127.0.0.1"),
 					Type:    "A",
 					Name:    "nochange.bar.com",
 					Content: "127.0.0.1",
 					TTL:     1,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 			},
 			regionalHostnames: []regionalHostname{
@@ -281,7 +282,7 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) {
 					Zones: map[string]string{
 						"001": "bar.com",
 					},
-					Records: map[string]map[string]cloudflare.DNSRecord{
+					Records: map[string]map[string]dns.RecordResponse{
 						"001": tt.records,
 					},
 					regionalHostnames: map[string][]regionalHostname{
@@ -309,26 +310,26 @@ func TestCloudflareRegionalHostnameDefaults(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "bar.com", "127.0.0.1"),
 				Type:    "A",
 				Name:    "bar.com",
 				Content: "127.0.0.1",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 		{
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "bar.com", "127.0.0.2"),
 				Type:    "A",
 				Name:    "bar.com",
 				Content: "127.0.0.2",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 		{
@@ -829,7 +830,7 @@ func TestRecordsWithListRegionalHostnameFaillure(t *testing.T) {
 		Zones: map[string]string{
 			"rherror": "error.com",
 		},
-		Records: map[string]map[string]cloudflare.DNSRecord{
+		Records: map[string]map[string]dns.RecordResponse{
 			"rherror": {"foo.error.com": {Type: "A"}},
 		},
 	}
@@ -844,7 +845,7 @@ func TestRecordsWithListRegionalHostnameFaillure(t *testing.T) {
 func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
 	t.Parallel()
 	type fields struct {
-		Records           map[string]cloudflare.DNSRecord
+		Records           map[string]dns.RecordResponse
 		RegionalHostnames []regionalHostname
 		RegionKey         string
 	}
@@ -879,7 +880,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
 		{
 			name: "create fails",
 			fields: fields{
-				Records:           map[string]cloudflare.DNSRecord{},
+				Records:           map[string]dns.RecordResponse{},
 				RegionalHostnames: []regionalHostname{},
 				RegionKey:         "us",
 			},
@@ -902,7 +903,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
 		{
 			name: "update fails",
 			fields: fields{
-				Records: map[string]cloudflare.DNSRecord{
+				Records: map[string]dns.RecordResponse{
 					"rherror.bar.com": {
 						ID:      "123",
 						Type:    "A",
@@ -944,7 +945,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
 		{
 			name: "delete fails",
 			fields: fields{
-				Records: map[string]cloudflare.DNSRecord{
+				Records: map[string]dns.RecordResponse{
 					"rherror.bar.com": {
 						ID:      "123",
 						Type:    "A",
@@ -974,7 +975,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
 			// This should not happen in practice, but we test it to ensure we return an error.
 			name: "conflicting regional keys",
 			fields: fields{
-				Records:           map[string]cloudflare.DNSRecord{},
+				Records:           map[string]dns.RecordResponse{},
 				RegionalHostnames: []regionalHostname{},
 				RegionKey:         "us",
 			},
@@ -1008,7 +1009,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
 			t.Parallel()
 			records := tt.fields.Records
 			if records == nil {
-				records = map[string]cloudflare.DNSRecord{}
+				records = map[string]dns.RecordResponse{}
 			}
 			p := &CloudFlareProvider{
 				Client: &mockCloudFlareClient{
@@ -1016,7 +1017,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
 						"001":     "bar.com",
 						"rherror": "error.com",
 					},
-					Records: map[string]map[string]cloudflare.DNSRecord{
+					Records: map[string]map[string]dns.RecordResponse{
 						"001": records,
 					},
 					regionalHostnames: map[string][]regionalHostname{
@@ -1044,7 +1045,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) {
 func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
 	t.Parallel()
 	type fields struct {
-		Records           map[string]cloudflare.DNSRecord
+		Records           map[string]dns.RecordResponse
 		RegionalHostnames []regionalHostname
 		RegionKey         string
 	}
@@ -1060,7 +1061,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
 		{
 			name: "create dry run",
 			fields: fields{
-				Records:           map[string]cloudflare.DNSRecord{},
+				Records:           map[string]dns.RecordResponse{},
 				RegionalHostnames: []regionalHostname{},
 				RegionKey:         "us",
 			},
@@ -1083,7 +1084,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
 		{
 			name: "update fails",
 			fields: fields{
-				Records: map[string]cloudflare.DNSRecord{
+				Records: map[string]dns.RecordResponse{
 					"foo.bar.com": {
 						ID:      "123",
 						Type:    "A",
@@ -1125,7 +1126,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
 		{
 			name: "delete fails",
 			fields: fields{
-				Records: map[string]cloudflare.DNSRecord{
+				Records: map[string]dns.RecordResponse{
 					"foo.bar.com": {
 						ID:      "123",
 						Type:    "A",
@@ -1157,7 +1158,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
 			t.Parallel()
 			records := tt.fields.Records
 			if records == nil {
-				records = map[string]cloudflare.DNSRecord{}
+				records = map[string]dns.RecordResponse{}
 			}
 			p := &CloudFlareProvider{
 				DryRun: true,
@@ -1165,7 +1166,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
 					Zones: map[string]string{
 						"001": "bar.com",
 					},
-					Records: map[string]map[string]cloudflare.DNSRecord{
+					Records: map[string]map[string]dns.RecordResponse{
 						"001": records,
 					},
 					regionalHostnames: map[string][]regionalHostname{
@@ -1186,3 +1187,109 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) {
 		})
 	}
 }
+
+func TestCloudflareAdjustEndpointsRegionalServices(t *testing.T) {
+	testCases := []struct {
+		name                   string
+		recordType             string
+		regionalServicesConfig RegionalServicesConfig
+		initialRegionKey       string  // existing region key on endpoint
+		expectedRegionKey      *string // expected region key after AdjustEndpoints (nil = should not be present)
+	}{
+		// Supported types should get region key when enabled
+		{
+			name:                   "A record with regional services enabled",
+			recordType:             "A",
+			regionalServicesConfig: RegionalServicesConfig{Enabled: true, RegionKey: "us"},
+			initialRegionKey:       "",
+			expectedRegionKey:      testutils.ToPtr("us"),
+		},
+		{
+			name:                   "AAAA record with regional services enabled",
+			recordType:             "AAAA",
+			regionalServicesConfig: RegionalServicesConfig{Enabled: true, RegionKey: "us"},
+			initialRegionKey:       "",
+			expectedRegionKey:      testutils.ToPtr("us"),
+		},
+		{
+			name:                   "CNAME record with regional services enabled",
+			recordType:             "CNAME",
+			regionalServicesConfig: RegionalServicesConfig{Enabled: true, RegionKey: "us"},
+			initialRegionKey:       "",
+			expectedRegionKey:      testutils.ToPtr("us"),
+		},
+
+		// Unsupported types should NOT get region key even when enabled
+		{
+			name:                   "TXT record with regional services enabled",
+			recordType:             "TXT",
+			regionalServicesConfig: RegionalServicesConfig{Enabled: true, RegionKey: "us"},
+			initialRegionKey:       "",
+			expectedRegionKey:      nil,
+		},
+
+		// Disabled regional services should remove region key for all types
+		{
+			name:                   "A record with regional services disabled",
+			recordType:             "A",
+			regionalServicesConfig: RegionalServicesConfig{Enabled: false},
+			initialRegionKey:       "existing-region",
+			expectedRegionKey:      nil,
+		},
+		{
+			name:                   "TXT record with regional services disabled",
+			recordType:             "TXT",
+			regionalServicesConfig: RegionalServicesConfig{Enabled: false},
+			initialRegionKey:       "existing-region",
+			expectedRegionKey:      nil,
+		},
+
+		// Existing region key should be preserved when already set
+		{
+			name:                   "A record with existing custom region key",
+			recordType:             "A",
+			regionalServicesConfig: RegionalServicesConfig{Enabled: true, RegionKey: "us"},
+			initialRegionKey:       "eu",
+			expectedRegionKey:      testutils.ToPtr("eu"),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Create endpoint with initial region key if specified
+			testEndpoint := &endpoint.Endpoint{
+				RecordType: tc.recordType,
+				DNSName:    "test.bar.com",
+				Targets:    endpoint.Targets{"127.0.0.1"},
+			}
+
+			if tc.initialRegionKey != "" {
+				testEndpoint.ProviderSpecific = endpoint.ProviderSpecific{
+					endpoint.ProviderSpecificProperty{
+						Name:  annotations.CloudflareRegionKey,
+						Value: tc.initialRegionKey,
+					},
+				}
+			}
+
+			provider := &CloudFlareProvider{
+				RegionalServicesConfig: tc.regionalServicesConfig,
+			}
+
+			adjustedEndpoints, err := provider.AdjustEndpoints([]*endpoint.Endpoint{testEndpoint})
+			assert.NoError(t, err)
+			assert.Len(t, adjustedEndpoints, 1)
+
+			regionKey, exists := adjustedEndpoints[0].GetProviderSpecificProperty(annotations.CloudflareRegionKey)
+
+			if tc.expectedRegionKey != nil {
+				// Region key should be present with expected value
+				assert.True(t, exists, "Region key should be present")
+				assert.Equal(t, *tc.expectedRegionKey, regionKey, "Region key value should match expected")
+			} else {
+				// Region key should not be present
+				assert.False(t, exists, "Region key should not be present")
+			}
+		})
+	}
+}
diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go
index 79b67000..652dce0e 100644
--- a/provider/cloudflare/cloudflare_test.go
+++ b/provider/cloudflare/cloudflare_test.go
@@ -22,14 +22,19 @@ import (
 	"fmt"
 	"os"
 	"slices"
-	"sort"
 	"strings"
 	"testing"
+	"time"
 
-	"github.com/cloudflare/cloudflare-go"
+	cloudflarev0 "github.com/cloudflare/cloudflare-go"
+	"github.com/cloudflare/cloudflare-go/v5"
+	"github.com/cloudflare/cloudflare-go/v5/dns"
+	"github.com/cloudflare/cloudflare-go/v5/zones"
 	"github.com/maxatome/go-testdeep/td"
 	log "github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/internal/testutils"
 	"sigs.k8s.io/external-dns/plan"
@@ -37,40 +42,33 @@ import (
 	"sigs.k8s.io/external-dns/source/annotations"
 )
 
-// proxyEnabled and proxyDisabled are pointers to bool values used to set if a record should be proxied through Cloudflare.
-var (
-	proxyEnabled  *bool = testutils.ToPtr(true)
-	proxyDisabled *bool = testutils.ToPtr(false)
-)
-
 type MockAction struct {
 	Name             string
 	ZoneId           string
 	RecordId         string
-	RecordData       cloudflare.DNSRecord
+	RecordData       dns.RecordResponse
 	RegionalHostname regionalHostname
 }
 
 type mockCloudFlareClient struct {
-	Zones                 map[string]string
-	Records               map[string]map[string]cloudflare.DNSRecord
-	Actions               []MockAction
-	listZonesError        error
-	zoneDetailsError      error
-	listZonesContextError error
-	dnsRecordsError       error
-	customHostnames       map[string][]cloudflare.CustomHostname
-	regionalHostnames     map[string][]regionalHostname
-}
-
-var ExampleDomain = []cloudflare.DNSRecord{
+	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
+}
+
+var ExampleDomain = []dns.RecordResponse{
 	{
 		ID:      "1234567890",
 		Name:    "foobar.bar.com",
 		Type:    endpoint.RecordTypeA,
 		TTL:     120,
 		Content: "1.2.3.4",
-		Proxied: proxyDisabled,
+		Proxied: false,
 		Comment: "valid comment",
 	},
 	{
@@ -79,7 +77,7 @@ var ExampleDomain = []cloudflare.DNSRecord{
 		Type:    endpoint.RecordTypeA,
 		TTL:     120,
 		Content: "3.4.5.6",
-		Proxied: proxyDisabled,
+		Proxied: false,
 	},
 	{
 		ID:      "1231231233",
@@ -87,7 +85,7 @@ var ExampleDomain = []cloudflare.DNSRecord{
 		Type:    endpoint.RecordTypeA,
 		TTL:     1,
 		Content: "2.3.4.5",
-		Proxied: proxyDisabled,
+		Proxied: false,
 	},
 }
 
@@ -97,16 +95,16 @@ func NewMockCloudFlareClient() *mockCloudFlareClient {
 			"001": "bar.com",
 			"002": "foo.com",
 		},
-		Records: map[string]map[string]cloudflare.DNSRecord{
+		Records: map[string]map[string]dns.RecordResponse{
 			"001": {},
 			"002": {},
 		},
-		customHostnames:   map[string][]cloudflare.CustomHostname{},
+		customHostnames:   map[string][]cloudflarev0.CustomHostname{},
 		regionalHostnames: map[string][]regionalHostname{},
 	}
 }
 
-func NewMockCloudFlareClientWithRecords(records map[string][]cloudflare.DNSRecord) *mockCloudFlareClient {
+func NewMockCloudFlareClientWithRecords(records map[string][]dns.RecordResponse) *mockCloudFlareClient {
 	m := NewMockCloudFlareClient()
 
 	for zoneID, zoneRecords := range records {
@@ -120,36 +118,23 @@ func NewMockCloudFlareClientWithRecords(records map[string][]cloudflare.DNSRecor
 	return m
 }
 
-func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
+func getDNSRecordFromRecordParams(rp any) dns.RecordResponse {
 	switch params := rp.(type) {
-	case cloudflare.CreateDNSRecordParams:
-		record := cloudflare.DNSRecord{
-			ID:      params.ID,
-			Name:    params.Name,
-			TTL:     params.TTL,
-			Proxied: params.Proxied,
-			Type:    params.Type,
-			Content: params.Content,
-		}
-		if params.Type == "MX" {
-			record.Priority = params.Priority
-		}
-		return record
-	case cloudflare.UpdateDNSRecordParams:
-		record := cloudflare.DNSRecord{
+	case cloudflarev0.UpdateDNSRecordParams:
+		record := dns.RecordResponse{
 			ID:      params.ID,
 			Name:    params.Name,
-			TTL:     params.TTL,
-			Proxied: params.Proxied,
-			Type:    params.Type,
+			TTL:     dns.TTL(params.TTL),
+			Proxied: params.Proxied != nil && *params.Proxied,
+			Type:    dns.RecordResponseType(params.Type),
 			Content: params.Content,
 		}
 		if params.Type == "MX" {
-			record.Priority = params.Priority
+			record.Priority = float64(*params.Priority)
 		}
 		return record
 	default:
-		return cloudflare.DNSRecord{}
+		return dns.RecordResponse{}
 	}
 }
 
@@ -157,73 +142,54 @@ func generateDNSRecordID(rrtype string, name string, content string) string {
 	return fmt.Sprintf("%s-%s-%s", name, rrtype, content)
 }
 
-func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) {
-	recordData := getDNSRecordFromRecordParams(rp)
-	if recordData.ID == "" {
-		recordData.ID = generateDNSRecordID(recordData.Type, recordData.Name, recordData.Content)
+func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, params dns.RecordNewParams) (*dns.RecordResponse, error) {
+	body := params.Body.(dns.RecordNewParamsBody)
+
+	record := dns.RecordResponse{
+		ID:       generateDNSRecordID(body.Type.String(), body.Name.Value, body.Content.Value),
+		Name:     body.Name.Value,
+		TTL:      dns.TTL(body.TTL.Value),
+		Proxied:  body.Proxied.Value,
+		Type:     dns.RecordResponseType(body.Type.String()),
+		Content:  body.Content.Value,
+		Priority: body.Priority.Value,
 	}
+
 	m.Actions = append(m.Actions, MockAction{
 		Name:       "Create",
-		ZoneId:     rc.Identifier,
-		RecordId:   recordData.ID,
-		RecordData: recordData,
+		ZoneId:     params.ZoneID.Value,
+		RecordId:   record.ID,
+		RecordData: record,
 	})
-	if zone, ok := m.Records[rc.Identifier]; ok {
-		zone[recordData.ID] = recordData
+	if zone, ok := m.Records[params.ZoneID.Value]; ok {
+		zone[record.ID] = record
 	}
 
-	if recordData.Name == "newerror.bar.com" {
-		return cloudflare.DNSRecord{}, fmt.Errorf("failed to create record")
+	if record.Name == "newerror.bar.com" {
+		return nil, fmt.Errorf("failed to create record")
 	}
-	return cloudflare.DNSRecord{}, nil
+	return &record, nil
 }
 
-func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error) {
+func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, params dns.RecordListParams) autoPager[dns.RecordResponse] {
 	if m.dnsRecordsError != nil {
-		return nil, &cloudflare.ResultInfo{}, m.dnsRecordsError
+		return &mockAutoPager[dns.RecordResponse]{err: m.dnsRecordsError}
 	}
-	result := []cloudflare.DNSRecord{}
-	if zone, ok := m.Records[rc.Identifier]; ok {
+	iter := &mockAutoPager[dns.RecordResponse]{}
+	if zone, ok := m.Records[params.ZoneID.Value]; ok {
 		for _, record := range zone {
 			if strings.HasPrefix(record.Name, "newerror-list-") {
-				m.DeleteDNSRecord(ctx, rc, record.ID)
-				return nil, &cloudflare.ResultInfo{}, errors.New("failed to list erroring DNS record")
+				m.DeleteDNSRecord(ctx, record.ID, dns.RecordDeleteParams{ZoneID: params.ZoneID})
+				iter.err = errors.New("failed to list erroring DNS record")
+				return iter
 			}
-			result = append(result, record)
+			iter.items = append(iter.items, record)
 		}
 	}
-
-	if len(result) == 0 || rp.PerPage == 0 {
-		return result, &cloudflare.ResultInfo{Page: 1, TotalPages: 1, Count: 0, Total: 0}, nil
-	}
-
-	// if not pagination options were passed in, return the result as is
-	if rp.Page == 0 {
-		return result, &cloudflare.ResultInfo{Page: 1, TotalPages: 1, Count: len(result), Total: len(result)}, nil
-	}
-
-	// otherwise, split the result into chunks of size rp.PerPage to simulate the pagination from the API
-	chunks := [][]cloudflare.DNSRecord{}
-
-	// to ensure consistency in the multiple calls to this function, sort the result slice
-	sort.Slice(result, func(i, j int) bool { return strings.Compare(result[i].ID, result[j].ID) > 0 })
-	for rp.PerPage < len(result) {
-		result, chunks = result[rp.PerPage:], append(chunks, result[0:rp.PerPage])
-	}
-	chunks = append(chunks, result)
-
-	// return the requested page
-	partialResult := chunks[rp.Page-1]
-	return partialResult, &cloudflare.ResultInfo{
-		PerPage:    rp.PerPage,
-		Page:       rp.Page,
-		TotalPages: len(chunks),
-		Count:      len(partialResult),
-		Total:      len(result),
-	}, nil
+	return iter
 }
 
-func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error {
+func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, rc *cloudflarev0.ResourceContainer, rp cloudflarev0.UpdateDNSRecordParams) error {
 	recordData := getDNSRecordFromRecordParams(rp)
 	m.Actions = append(m.Actions, MockAction{
 		Name:       "Update",
@@ -242,13 +208,14 @@ func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, rc *cloudfla
 	return nil
 }
 
-func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error {
+func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, recordID string, params dns.RecordDeleteParams) error {
+	zoneID := params.ZoneID.String()
 	m.Actions = append(m.Actions, MockAction{
 		Name:     "Delete",
-		ZoneId:   rc.Identifier,
+		ZoneId:   zoneID,
 		RecordId: recordID,
 	})
-	if zone, ok := m.Records[rc.Identifier]; ok {
+	if zone, ok := m.Records[zoneID]; ok {
 		if _, ok := zone[recordID]; ok {
 			name := zone[recordID].Name
 			delete(zone, recordID)
@@ -261,34 +228,34 @@ func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, rc *cloudfla
 	return nil
 }
 
-func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) {
+func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflarev0.CustomHostname) ([]cloudflarev0.CustomHostname, cloudflarev0.ResultInfo, error) {
 	var err error = nil
 	perPage := 50 // cloudflare-go v0 API hardcoded
 
 	if strings.HasPrefix(zoneID, "newerror-") {
-		return nil, cloudflare.ResultInfo{}, errors.New("failed to list custom hostnames")
+		return nil, cloudflarev0.ResultInfo{}, errors.New("failed to list custom hostnames")
 	}
 	if filter.Hostname != "" {
 		err = errors.New("filters are not supported for custom hostnames mock test")
-		return nil, cloudflare.ResultInfo{}, err
+		return nil, cloudflarev0.ResultInfo{}, err
 	}
 	if page < 1 {
 		err = errors.New("incorrect page value for custom hostnames list")
-		return nil, cloudflare.ResultInfo{}, err
+		return nil, cloudflarev0.ResultInfo{}, err
 	}
 
-	result := []cloudflare.CustomHostname{}
+	result := []cloudflarev0.CustomHostname{}
 	if chs, ok := m.customHostnames[zoneID]; ok {
 		for idx := (page - 1) * perPage; idx < min(len(chs), page*perPage); idx++ {
 			ch := m.customHostnames[zoneID][idx]
 			if strings.HasPrefix(ch.Hostname, "newerror-list-") {
 				m.DeleteCustomHostname(ctx, zoneID, ch.ID)
-				return nil, cloudflare.ResultInfo{}, errors.New("failed to list erroring custom hostname")
+				return nil, cloudflarev0.ResultInfo{}, errors.New("failed to list erroring custom hostname")
 			}
 			result = append(result, ch)
 		}
 		return result,
-			cloudflare.ResultInfo{
+			cloudflarev0.ResultInfo{
 				Page:       page,
 				PerPage:    perPage,
 				Count:      len(result),
@@ -297,7 +264,7 @@ func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID strin
 			}, err
 	} else {
 		return result,
-			cloudflare.ResultInfo{
+			cloudflarev0.ResultInfo{
 				Page:       page,
 				PerPage:    perPage,
 				Count:      0,
@@ -307,17 +274,17 @@ func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID strin
 	}
 }
 
-func (m *mockCloudFlareClient) CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error) {
+func (m *mockCloudFlareClient) CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflarev0.CustomHostname) (*cloudflarev0.CustomHostnameResponse, error) {
 	if ch.Hostname == "" || ch.CustomOriginServer == "" || ch.Hostname == "newerror-create.foo.fancybar.com" {
 		return nil, fmt.Errorf("Invalid custom hostname or origin hostname")
 	}
 	if _, ok := m.customHostnames[zoneID]; !ok {
-		m.customHostnames[zoneID] = []cloudflare.CustomHostname{}
+		m.customHostnames[zoneID] = []cloudflarev0.CustomHostname{}
 	}
-	var newCustomHostname cloudflare.CustomHostname = ch
+	var newCustomHostname cloudflarev0.CustomHostname = ch
 	newCustomHostname.ID = fmt.Sprintf("ID-%s", ch.Hostname)
 	m.customHostnames[zoneID] = append(m.customHostnames[zoneID], newCustomHostname)
-	return &cloudflare.CustomHostnameResponse{}, nil
+	return &cloudflarev0.CustomHostnameResponse{}, nil
 }
 
 func (m *mockCloudFlareClient) DeleteCustomHostname(ctx context.Context, zoneID string, customHostnameID string) error {
@@ -335,57 +302,63 @@ func (m *mockCloudFlareClient) DeleteCustomHostname(ctx context.Context, zoneID
 }
 
 func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) {
+	// Simulate iterator error (line 144)
+	if m.listZonesError != nil {
+		return "", fmt.Errorf("failed to list zones from CloudFlare API: %w", m.listZonesError)
+	}
+
 	for id, name := range m.Zones {
 		if name == zoneName {
 			return id, nil
 		}
 	}
 
-	return "", errors.New("Unknown zone: " + zoneName)
+	// Use the improved error message (line 147)
+	return "", fmt.Errorf("zone %q not found in CloudFlare account - verify the zone exists and API credentials have access to it", zoneName)
 }
 
-func (m *mockCloudFlareClient) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
-	if m.listZonesContextError != nil {
-		return cloudflare.ZonesResponse{}, m.listZonesContextError
+// V4 Zone methods
+func (m *mockCloudFlareClient) ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone] {
+	if m.listZonesError != nil {
+		return &mockAutoPager[zones.Zone]{
+			err: m.listZonesError,
+		}
 	}
 
-	result := []cloudflare.Zone{}
+	var results []zones.Zone
 
-	for zoneId, zoneName := range m.Zones {
-		result = append(result, cloudflare.Zone{
-			ID:   zoneId,
+	for id, zoneName := range m.Zones {
+		results = append(results, zones.Zone{
+			ID:   id,
 			Name: zoneName,
+			Plan: zones.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")}, //nolint:SA1019 // Plan.IsSubscribed is deprecated but no replacement available yet
 		})
 	}
 
-	return cloudflare.ZonesResponse{
-		Result: result,
-		ResultInfo: cloudflare.ResultInfo{
-			Page:       1,
-			TotalPages: 1,
-		},
-	}, nil
+	return &mockAutoPager[zones.Zone]{
+		items: results,
+	}
 }
 
-func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) {
-	if m.zoneDetailsError != nil {
-		return cloudflare.Zone{}, m.zoneDetailsError
+func (m *mockCloudFlareClient) GetZone(ctx context.Context, zoneID string) (*zones.Zone, error) {
+	if m.getZoneError != nil {
+		return nil, m.getZoneError
 	}
 
 	for id, zoneName := range m.Zones {
 		if zoneID == id {
-			return cloudflare.Zone{
+			return &zones.Zone{
 				ID:   zoneID,
 				Name: zoneName,
-				Plan: cloudflare.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")},
+				Plan: zones.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")}, //nolint:SA1019 // Plan.IsSubscribed is deprecated but no replacement available yet
 			}, nil
 		}
 	}
 
-	return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
+	return nil, errors.New("Unknown zoneID: " + zoneID)
 }
 
-func getCustomHostnameIdxByID(chs []cloudflare.CustomHostname, customHostnameID string) int {
+func getCustomHostnameIdxByID(chs []cloudflarev0.CustomHostname, customHostnameID string) int {
 	for idx, ch := range chs {
 		if ch.ID == customHostnameID {
 			return idx
@@ -454,26 +427,26 @@ func TestCloudflareA(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "bar.com", "127.0.0.1"),
 				Type:    "A",
 				Name:    "bar.com",
 				Content: "127.0.0.1",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 		{
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.2"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "bar.com", "127.0.0.2"),
 				Type:    "A",
 				Name:    "bar.com",
 				Content: "127.0.0.2",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 	},
@@ -495,26 +468,26 @@ func TestCloudflareCname(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("CNAME", "cname.bar.com", "google.com"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("CNAME", "cname.bar.com", "google.com"),
 				Type:    "CNAME",
 				Name:    "cname.bar.com",
 				Content: "google.com",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 		{
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("CNAME", "cname.bar.com", "facebook.com"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("CNAME", "cname.bar.com", "facebook.com"),
 				Type:    "CNAME",
 				Name:    "cname.bar.com",
 				Content: "facebook.com",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 	},
@@ -536,28 +509,28 @@ func TestCloudflareMx(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("MX", "mx.bar.com", "google.com"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:       generateDNSRecordID("MX", "mx.bar.com", "google.com"),
 				Type:     "MX",
 				Name:     "mx.bar.com",
 				Content:  "google.com",
-				Priority: cloudflare.Uint16Ptr(10),
+				Priority: 10,
 				TTL:      1,
-				Proxied:  proxyDisabled,
+				Proxied:  false,
 			},
 		},
 		{
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("MX", "mx.bar.com", "facebook.com"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:       generateDNSRecordID("MX", "mx.bar.com", "facebook.com"),
 				Type:     "MX",
 				Name:     "mx.bar.com",
 				Content:  "facebook.com",
-				Priority: cloudflare.Uint16Ptr(20),
+				Priority: 20,
 				TTL:      1,
-				Proxied:  proxyDisabled,
+				Proxied:  false,
 			},
 		},
 	},
@@ -579,13 +552,13 @@ func TestCloudflareTxt(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("TXT", "txt.bar.com", "v=spf1 include:_spf.google.com ~all"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("TXT", "txt.bar.com", "v=spf1 include:_spf.google.com ~all"),
 				Type:    "TXT",
 				Name:    "txt.bar.com",
 				Content: "v=spf1 include:_spf.google.com ~all",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 	},
@@ -608,13 +581,13 @@ func TestCloudflareCustomTTL(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "ttl.bar.com", "127.0.0.1"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "ttl.bar.com", "127.0.0.1"),
 				Type:    "A",
 				Name:    "ttl.bar.com",
 				Content: "127.0.0.1",
 				TTL:     120,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 	},
@@ -636,13 +609,13 @@ func TestCloudflareProxiedDefault(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "bar.com", "127.0.0.1"),
 				Type:    "A",
 				Name:    "bar.com",
 				Content: "127.0.0.1",
 				TTL:     1,
-				Proxied: proxyEnabled,
+				Proxied: true,
 			},
 		},
 	},
@@ -670,13 +643,13 @@ func TestCloudflareProxiedOverrideTrue(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "bar.com", "127.0.0.1"),
 				Type:    "A",
 				Name:    "bar.com",
 				Content: "127.0.0.1",
 				TTL:     1,
-				Proxied: proxyEnabled,
+				Proxied: true,
 			},
 		},
 	},
@@ -704,13 +677,13 @@ func TestCloudflareProxiedOverrideFalse(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "bar.com", "127.0.0.1"),
 				Type:    "A",
 				Name:    "bar.com",
 				Content: "127.0.0.1",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 	},
@@ -738,13 +711,13 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "bar.com", "127.0.0.1"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "bar.com", "127.0.0.1"),
 				Type:    "A",
 				Name:    "bar.com",
 				Content: "127.0.0.1",
 				TTL:     1,
-				Proxied: proxyEnabled,
+				Proxied: true,
 			},
 		},
 	},
@@ -753,33 +726,31 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
 }
 
 func TestCloudflareSetProxied(t *testing.T) {
-	var proxied *bool = proxyEnabled
-	var notProxied *bool = proxyDisabled
 	testCases := []struct {
 		recordType string
 		domain     string
-		proxiable  *bool
+		proxiable  bool
 	}{
-		{"A", "bar.com", proxied},
-		{"CNAME", "bar.com", proxied},
-		{"TXT", "bar.com", notProxied},
-		{"MX", "bar.com", notProxied},
-		{"NS", "bar.com", notProxied},
-		{"SPF", "bar.com", notProxied},
-		{"SRV", "bar.com", notProxied},
-		{"A", "*.bar.com", proxied},
-		{"CNAME", "*.docs.bar.com", proxied},
+		{"A", "bar.com", true},
+		{"CNAME", "bar.com", true},
+		{"TXT", "bar.com", false},
+		{"MX", "bar.com", false},
+		{"NS", "bar.com", false},
+		{"SPF", "bar.com", false},
+		{"SRV", "bar.com", false},
+		{"A", "*.bar.com", true},
+		{"CNAME", "*.docs.bar.com", true},
 	}
 
 	for _, testCase := range testCases {
 		var targets endpoint.Targets
 		var content string
-		var priority *uint16
+		var priority float64
 
 		if testCase.recordType == "MX" {
 			targets = endpoint.Targets{"10 mx.example.com"}
 			content = "mx.example.com"
-			priority = cloudflare.Uint16Ptr(10)
+			priority = 10
 		} else {
 			targets = endpoint.Targets{"127.0.0.1"}
 			content = "127.0.0.1"
@@ -799,9 +770,9 @@ func TestCloudflareSetProxied(t *testing.T) {
 			},
 		}
 		expectedID := fmt.Sprintf("%s-%s-%s", testCase.domain, testCase.recordType, content)
-		recordData := cloudflare.DNSRecord{
+		recordData := dns.RecordResponse{
 			ID:      expectedID,
-			Type:    testCase.recordType,
+			Type:    dns.RecordResponseType(testCase.recordType),
 			Name:    testCase.domain,
 			Content: content,
 			TTL:     1,
@@ -841,7 +812,7 @@ func TestCloudflareZones(t *testing.T) {
 func TestCloudflareZonesFailed(t *testing.T) {
 
 	client := NewMockCloudFlareClient()
-	client.zoneDetailsError = errors.New("zone lookup failed")
+	client.getZoneError = errors.New("zone lookup failed")
 
 	provider := &CloudFlareProvider{
 		Client:       client,
@@ -877,10 +848,10 @@ func TestCloudFlareZonesWithIDFilter(t *testing.T) {
 func TestCloudflareListZonesRateLimited(t *testing.T) {
 	// Create a mock client that returns a rate limit error
 	client := NewMockCloudFlareClient()
-	client.listZonesContextError = &cloudflare.Error{
+	client.listZonesError = &cloudflarev0.Error{
 		StatusCode: 429,
 		ErrorCodes: []int{10000},
-		Type:       cloudflare.ErrorTypeRateLimit,
+		Type:       cloudflarev0.ErrorTypeRateLimit,
 	}
 	p := &CloudFlareProvider{Client: client}
 
@@ -896,7 +867,7 @@ func TestCloudflareListZonesRateLimited(t *testing.T) {
 func TestCloudflareListZonesRateLimitedStringError(t *testing.T) {
 	// Create a mock client that returns a rate limit error
 	client := NewMockCloudFlareClient()
-	client.listZonesContextError = errors.New("exceeded available rate limit retries")
+	client.listZonesError = errors.New("exceeded available rate limit retries")
 	p := &CloudFlareProvider{Client: client}
 
 	// Call the Zones function
@@ -909,10 +880,10 @@ func TestCloudflareListZonesRateLimitedStringError(t *testing.T) {
 func TestCloudflareListZoneInternalErrors(t *testing.T) {
 	// Create a mock client that returns a internal server error
 	client := NewMockCloudFlareClient()
-	client.listZonesContextError = &cloudflare.Error{
+	client.listZonesError = &cloudflarev0.Error{
 		StatusCode: 500,
 		ErrorCodes: []int{20000},
-		Type:       cloudflare.ErrorTypeService,
+		Type:       cloudflarev0.ErrorTypeService,
 	}
 	p := &CloudFlareProvider{Client: client}
 
@@ -927,7 +898,7 @@ func TestCloudflareListZoneInternalErrors(t *testing.T) {
 }
 
 func TestCloudflareRecords(t *testing.T) {
-	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
+	client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{
 		"001": ExampleDomain,
 	})
 
@@ -949,10 +920,10 @@ func TestCloudflareRecords(t *testing.T) {
 		t.Errorf("expected to fail")
 	}
 	client.dnsRecordsError = nil
-	client.listZonesContextError = &cloudflare.Error{
+	client.listZonesError = &cloudflarev0.Error{
 		StatusCode: 429,
 		ErrorCodes: []int{10000},
-		Type:       cloudflare.ErrorTypeRateLimit,
+		Type:       cloudflarev0.ErrorTypeRateLimit,
 	}
 	_, err = p.Records(ctx)
 	// Assert that a soft error was returned
@@ -960,10 +931,10 @@ func TestCloudflareRecords(t *testing.T) {
 		t.Error("expected a rate limit error")
 	}
 
-	client.listZonesContextError = &cloudflare.Error{
+	client.listZonesError = &cloudflarev0.Error{
 		StatusCode: 500,
 		ErrorCodes: []int{10000},
-		Type:       cloudflare.ErrorTypeService,
+		Type:       cloudflarev0.ErrorTypeService,
 	}
 	_, err = p.Records(ctx)
 	// Assert that a soft error was returned
@@ -971,7 +942,7 @@ func TestCloudflareRecords(t *testing.T) {
 		t.Error("expected a internal server error")
 	}
 
-	client.listZonesContextError = errors.New("failed to list zones")
+	client.listZonesError = errors.New("failed to list zones")
 	_, err = p.Records(ctx)
 	if err == nil {
 		t.Errorf("expected to fail")
@@ -1100,24 +1071,24 @@ func TestCloudflareApplyChanges(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("", "new.bar.com", "target"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("", "new.bar.com", "target"),
 				Name:    "new.bar.com",
 				Content: "target",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 		{
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("", "foobar.bar.com", "target-new"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("", "foobar.bar.com", "target-new"),
 				Name:    "foobar.bar.com",
 				Content: "target-new",
 				TTL:     1,
-				Proxied: proxyDisabled,
+				Proxied: false,
 			},
 		},
 	})
@@ -1196,29 +1167,29 @@ func TestCloudflareGetRecordID(t *testing.T) {
 		},
 	}
 
-	assert.Empty(t, p.getRecordID(recordsMap, cloudflare.DNSRecord{
+	assert.Empty(t, p.getRecordID(recordsMap, dns.RecordResponse{
 		Name:    "foo.com",
 		Type:    endpoint.RecordTypeA,
 		Content: "foobar",
 	}))
 
-	assert.Empty(t, p.getRecordID(recordsMap, cloudflare.DNSRecord{
+	assert.Empty(t, p.getRecordID(recordsMap, dns.RecordResponse{
 		Name:    "foo.com",
 		Type:    endpoint.RecordTypeCNAME,
 		Content: "fizfuz",
 	}))
 
-	assert.Equal(t, "1", p.getRecordID(recordsMap, cloudflare.DNSRecord{
+	assert.Equal(t, "1", p.getRecordID(recordsMap, dns.RecordResponse{
 		Name:    "foo.com",
 		Type:    endpoint.RecordTypeCNAME,
 		Content: "foobar",
 	}))
-	assert.Empty(t, p.getRecordID(recordsMap, cloudflare.DNSRecord{
+	assert.Empty(t, p.getRecordID(recordsMap, dns.RecordResponse{
 		Name:    "bar.de",
 		Type:    endpoint.RecordTypeA,
 		Content: "2.3.4.5",
 	}))
-	assert.Equal(t, "2", p.getRecordID(recordsMap, cloudflare.DNSRecord{
+	assert.Equal(t, "2", p.getRecordID(recordsMap, dns.RecordResponse{
 		Name:    "bar.de",
 		Type:    endpoint.RecordTypeA,
 		Content: "1.2.3.4",
@@ -1233,23 +1204,23 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
 	}
 	testCases := []struct {
 		Name              string
-		Records           []cloudflare.DNSRecord
+		Records           []dns.RecordResponse
 		ExpectedEndpoints []*endpoint.Endpoint
 	}{
 		{
 			Name:              "empty",
-			Records:           []cloudflare.DNSRecord{},
+			Records:           []dns.RecordResponse{},
 			ExpectedEndpoints: []*endpoint.Endpoint{},
 		},
 		{
 			Name: "single record - single target",
-			Records: []cloudflare.DNSRecord{
+			Records: []dns.RecordResponse{
 				{
 					Name:    "foo.com",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.1",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 			},
 			ExpectedEndpoints: []*endpoint.Endpoint{
@@ -1270,20 +1241,20 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
 		},
 		{
 			Name: "single record - multiple targets",
-			Records: []cloudflare.DNSRecord{
+			Records: []dns.RecordResponse{
 				{
 					Name:    "foo.com",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.1",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 				{
 					Name:    "foo.com",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.2",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 			},
 			ExpectedEndpoints: []*endpoint.Endpoint{
@@ -1304,34 +1275,34 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
 		},
 		{
 			Name: "multiple record - multiple targets",
-			Records: []cloudflare.DNSRecord{
+			Records: []dns.RecordResponse{
 				{
 					Name:    "foo.com",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.1",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 				{
 					Name:    "foo.com",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.2",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 				{
 					Name:    "bar.de",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.1",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 				{
 					Name:    "bar.de",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.2",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 			},
 			ExpectedEndpoints: []*endpoint.Endpoint{
@@ -1365,27 +1336,27 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
 		},
 		{
 			Name: "multiple record - mixed single/multiple targets",
-			Records: []cloudflare.DNSRecord{
+			Records: []dns.RecordResponse{
 				{
 					Name:    "foo.com",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.1",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 				{
 					Name:    "foo.com",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.2",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 				{
 					Name:    "bar.de",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.1",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 			},
 			ExpectedEndpoints: []*endpoint.Endpoint{
@@ -1419,27 +1390,27 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
 		},
 		{
 			Name: "unsupported record type",
-			Records: []cloudflare.DNSRecord{
+			Records: []dns.RecordResponse{
 				{
 					Name:    "foo.com",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.1",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 				{
 					Name:    "foo.com",
 					Type:    endpoint.RecordTypeA,
 					Content: "10.10.10.2",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 				{
 					Name:    "bar.de",
 					Type:    "NOT SUPPORTED",
 					Content: "10.10.10.1",
 					TTL:     defaultTTL,
-					Proxied: proxyDisabled,
+					Proxied: false,
 				},
 			},
 			ExpectedEndpoints: []*endpoint.Endpoint{
@@ -1478,7 +1449,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
 }
 
 func TestGroupByNameAndTypeWithCustomHostnames_MX(t *testing.T) {
-	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
+	client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{
 		"001": {
 			{
 				ID:       "mx-1",
@@ -1486,7 +1457,7 @@ func TestGroupByNameAndTypeWithCustomHostnames_MX(t *testing.T) {
 				Type:     endpoint.RecordTypeMX,
 				TTL:      3600,
 				Content:  "mail.bar.com",
-				Priority: cloudflare.Uint16Ptr(10),
+				Priority: 10,
 			},
 			{
 				ID:       "mx-2",
@@ -1494,7 +1465,7 @@ func TestGroupByNameAndTypeWithCustomHostnames_MX(t *testing.T) {
 				Type:     endpoint.RecordTypeMX,
 				TTL:      3600,
 				Content:  "mail2.bar.com",
-				Priority: cloudflare.Uint16Ptr(20),
+				Priority: 20,
 			},
 		},
 	})
@@ -1503,7 +1474,7 @@ func TestGroupByNameAndTypeWithCustomHostnames_MX(t *testing.T) {
 	}
 	ctx := context.Background()
 	chs := CustomHostnamesMap{}
-	records, err := provider.listDNSRecordsWithAutoPagination(ctx, "001")
+	records, err := provider.getDNSRecordsMap(ctx, "001")
 	assert.NoError(t, err)
 
 	endpoints := provider.groupByNameAndTypeWithCustomHostnames(records, chs)
@@ -1519,38 +1490,38 @@ func TestProviderPropertiesIdempotency(t *testing.T) {
 	testCases := []struct {
 		Name                     string
 		ProviderProxiedByDefault bool
-		RecordsAreProxied        *bool
+		RecordsAreProxied        bool
 		ShouldBeUpdated          bool
 	}{
 		{
 			Name:                     "ProxyDefault: false, ShouldBeProxied: false, ExpectUpdates: false",
 			ProviderProxiedByDefault: false,
-			RecordsAreProxied:        proxyDisabled,
+			RecordsAreProxied:        false,
 			ShouldBeUpdated:          false,
 		},
 		{
 			Name:                     "ProxyDefault: true, ShouldBeProxied: true, ExpectUpdates: false",
 			ProviderProxiedByDefault: true,
-			RecordsAreProxied:        proxyEnabled,
+			RecordsAreProxied:        true,
 			ShouldBeUpdated:          false,
 		},
 		{
 			Name:                     "ProxyDefault: true, ShouldBeProxied: false, ExpectUpdates: true",
 			ProviderProxiedByDefault: true,
-			RecordsAreProxied:        proxyDisabled,
+			RecordsAreProxied:        false,
 			ShouldBeUpdated:          true,
 		},
 		{
 			Name:                     "ProxyDefault: false, ShouldBeProxied: true, ExpectUpdates: true",
 			ProviderProxiedByDefault: false,
-			RecordsAreProxied:        proxyEnabled,
+			RecordsAreProxied:        true,
 			ShouldBeUpdated:          true,
 		},
 	}
 
 	for _, test := range testCases {
 		t.Run(test.Name, func(t *testing.T) {
-			client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
+			client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{
 				"001": {
 					{
 						ID:      "1234567890",
@@ -1617,7 +1588,7 @@ func TestProviderPropertiesIdempotency(t *testing.T) {
 }
 
 func TestCloudflareComplexUpdate(t *testing.T) {
-	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
+	client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{
 		"001": ExampleDomain,
 	})
 
@@ -1672,33 +1643,33 @@ func TestCloudflareComplexUpdate(t *testing.T) {
 			Name:     "Create",
 			ZoneId:   "001",
 			RecordId: generateDNSRecordID("A", "foobar.bar.com", "2.3.4.5"),
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      generateDNSRecordID("A", "foobar.bar.com", "2.3.4.5"),
 				Name:    "foobar.bar.com",
 				Type:    "A",
 				Content: "2.3.4.5",
 				TTL:     1,
-				Proxied: proxyEnabled,
+				Proxied: true,
 			},
 		},
 		{
 			Name:     "Update",
 			ZoneId:   "001",
 			RecordId: "1234567890",
-			RecordData: cloudflare.DNSRecord{
+			RecordData: dns.RecordResponse{
 				ID:      "1234567890",
 				Name:    "foobar.bar.com",
 				Type:    "A",
 				Content: "1.2.3.4",
 				TTL:     1,
-				Proxied: proxyEnabled,
+				Proxied: true,
 			},
 		},
 	})
 }
 
 func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
-	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
+	client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{
 		"001": {
 			{
 				ID:      "1234567890",
@@ -1706,7 +1677,7 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
 				Type:    endpoint.RecordTypeA,
 				TTL:     1,
 				Content: "1.2.3.4",
-				Proxied: proxyEnabled,
+				Proxied: true,
 			},
 		},
 	})
@@ -1916,7 +1887,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
 }
 
 func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) {
-	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
+	client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{
 		"001": {
 			{
 				ID:      "1234567890",
@@ -1924,7 +1895,7 @@ func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) {
 				Type:    endpoint.RecordTypeCNAME,
 				TTL:     1,
 				Content: "my-tunnel-guid-here.cfargotunnel.com",
-				Proxied: proxyEnabled,
+				Proxied: true,
 			},
 			{
 				ID:      "9876543210",
@@ -1943,7 +1914,7 @@ func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) {
 	changes := []*cloudFlareChange{
 		{
 			Action: cloudFlareUpdate,
-			ResourceRecord: cloudflare.DNSRecord{
+			ResourceRecord: dns.RecordResponse{
 				Name:    "my-domain-here.app",
 				Type:    endpoint.RecordTypeCNAME,
 				ID:      "1234567890",
@@ -1955,7 +1926,7 @@ func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) {
 		},
 		{
 			Action: cloudFlareUpdate,
-			ResourceRecord: cloudflare.DNSRecord{
+			ResourceRecord: dns.RecordResponse{
 				Name:    "my-domain-here.app",
 				Type:    endpoint.RecordTypeTXT,
 				ID:      "9876543210",
@@ -1977,7 +1948,7 @@ func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) {
 
 func TestCloudFlareProvider_submitChangesApex(t *testing.T) {
 	// Create a mock CloudFlare client with APEX records
-	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
+	client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{
 		"001": {
 			{
 				ID:      "1234567890",
@@ -1985,7 +1956,7 @@ func TestCloudFlareProvider_submitChangesApex(t *testing.T) {
 				Type:    endpoint.RecordTypeCNAME,
 				TTL:     1,
 				Content: "my-tunnel-guid-here.cfargotunnel.com",
-				Proxied: proxyEnabled,
+				Proxied: true,
 			},
 			{
 				ID:      "9876543210",
@@ -2006,7 +1977,7 @@ func TestCloudFlareProvider_submitChangesApex(t *testing.T) {
 	changes := []*cloudFlareChange{
 		{
 			Action: cloudFlareUpdate,
-			ResourceRecord: cloudflare.DNSRecord{
+			ResourceRecord: dns.RecordResponse{
 				Name:    "@", // APEX record
 				Type:    endpoint.RecordTypeCNAME,
 				ID:      "1234567890",
@@ -2018,7 +1989,7 @@ func TestCloudFlareProvider_submitChangesApex(t *testing.T) {
 		},
 		{
 			Action: cloudFlareUpdate,
-			ResourceRecord: cloudflare.DNSRecord{
+			ResourceRecord: dns.RecordResponse{
 				Name:    "@", // APEX record
 				Type:    endpoint.RecordTypeTXT,
 				ID:      "9876543210",
@@ -2043,8 +2014,8 @@ func TestCloudflareZoneRecordsFail(t *testing.T) {
 		Zones: map[string]string{
 			"newerror-001": "bar.com",
 		},
-		Records:         map[string]map[string]cloudflare.DNSRecord{},
-		customHostnames: map[string][]cloudflare.CustomHostname{},
+		Records:         map[string]map[string]dns.RecordResponse{},
+		customHostnames: map[string][]cloudflarev0.CustomHostname{},
 	}
 	failingProvider := &CloudFlareProvider{
 		Client:                client,
@@ -2061,7 +2032,7 @@ func TestCloudflareZoneRecordsFail(t *testing.T) {
 // TestCloudflareLongRecordsErrorLog checks if the error is logged when a record name exceeds 63 characters
 // it's not likely to happen in practice, as the Cloudflare API should reject having it
 func TestCloudflareLongRecordsErrorLog(t *testing.T) {
-	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
+	client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{
 		"001": {
 			{
 				ID:      "1234567890",
@@ -2489,7 +2460,7 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) {
 				t.Logf("corrupting custom hostname %q", chID)
 				oldIdx := getCustomHostnameIdxByID(client.customHostnames[zoneID], chID)
 				oldCh := client.customHostnames[zoneID][oldIdx]
-				ch := cloudflare.CustomHostname{
+				ch := cloudflarev0.CustomHostname{
 					Hostname:           "corrupted-newerror-getCustomHostnameOrigin.foo.fancybar.com",
 					CustomOriginServer: oldCh.CustomOriginServer,
 					SSL:                oldCh.SSL,
@@ -2497,7 +2468,7 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) {
 				client.customHostnames[zoneID][oldIdx] = ch
 			}
 		} else if tc.preApplyHook == "duplicate" { // manually inject duplicating custom hostname with the same name and origin
-			ch := cloudflare.CustomHostname{
+			ch := cloudflarev0.CustomHostname{
 				ID:                 "ID-random-123",
 				Hostname:           "a.foo.fancybar.com",
 				CustomOriginServer: "a.foo.bar.com",
@@ -2584,7 +2555,7 @@ func TestZoneHasPaidPlan(t *testing.T) {
 	assert.True(t, cfprovider.ZoneHasPaidPlan("subdomain.bar.com"))
 	assert.False(t, cfprovider.ZoneHasPaidPlan("invaliddomain"))
 
-	client.zoneDetailsError = errors.New("zone lookup failed")
+	client.getZoneError = errors.New("zone lookup failed")
 	cfproviderWithZoneError := &CloudFlareProvider{
 		Client:       client,
 		domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
@@ -2592,6 +2563,7 @@ func TestZoneHasPaidPlan(t *testing.T) {
 	}
 	assert.False(t, cfproviderWithZoneError.ZoneHasPaidPlan("subdomain.foo.com"))
 }
+
 func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) {
 	hook := testutils.LogsUnderTestWithLogLevel(log.ErrorLevel, t)
 
@@ -2760,3 +2732,711 @@ func TestCloudFlareProvider_SupportedAdditionalRecordTypes(t *testing.T) {
 		})
 	}
 }
+
+func TestCloudflareZoneChanges(t *testing.T) {
+	client := NewMockCloudFlareClient()
+	cfProvider := &CloudFlareProvider{
+		Client:       client,
+		domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
+		zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
+	}
+
+	// Test zone listing and filtering
+	zones, err := cfProvider.Zones(context.Background())
+	assert.NoError(t, err)
+	assert.Len(t, zones, 2)
+
+	// Verify zone names
+	zoneNames := make([]string, len(zones))
+	for i, zone := range zones {
+		zoneNames[i] = zone.Name
+	}
+	assert.Contains(t, zoneNames, "foo.com")
+	assert.Contains(t, zoneNames, "bar.com")
+
+	// Test zone filtering with specific zone ID
+	providerWithZoneFilter := &CloudFlareProvider{
+		Client:       client,
+		domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
+		zoneIDFilter: provider.NewZoneIDFilter([]string{"001"}),
+	}
+
+	filteredZones, err := providerWithZoneFilter.Zones(context.Background())
+	assert.NoError(t, err)
+	assert.Len(t, filteredZones, 1)
+	assert.Equal(t, "bar.com", filteredZones[0].Name) // zone 001 is bar.com
+	assert.Equal(t, "001", filteredZones[0].ID)
+
+	// Test zone changes grouping
+	changes := []*cloudFlareChange{
+		{
+			Action:         cloudFlareCreate,
+			ResourceRecord: dns.RecordResponse{Name: "test1.foo.com", Type: "A", Content: "1.2.3.4"},
+		},
+		{
+			Action:         cloudFlareCreate,
+			ResourceRecord: dns.RecordResponse{Name: "test2.foo.com", Type: "A", Content: "1.2.3.5"},
+		},
+		{
+			Action:         cloudFlareCreate,
+			ResourceRecord: dns.RecordResponse{Name: "test1.bar.com", Type: "A", Content: "1.2.3.6"},
+		},
+	}
+
+	changesByZone := cfProvider.changesByZone(zones, changes)
+	assert.Len(t, changesByZone, 2)
+	assert.Len(t, changesByZone["001"], 1) // bar.com zone (test1.bar.com)
+	assert.Len(t, changesByZone["002"], 2) // foo.com zone (test1.foo.com, test2.foo.com)
+
+	// Test paid plan detection
+	assert.False(t, cfProvider.ZoneHasPaidPlan("subdomain.foo.com")) // free plan
+	assert.True(t, cfProvider.ZoneHasPaidPlan("subdomain.bar.com"))  // paid plan
+}
+
+func TestCloudflareZoneErrors(t *testing.T) {
+	client := NewMockCloudFlareClient()
+
+	// Test list zones error
+	client.listZonesError = errors.New("failed to list zones")
+	cfProvider := &CloudFlareProvider{
+		Client: client,
+	}
+
+	zones, err := cfProvider.Zones(context.Background())
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "failed to list zones")
+	assert.Nil(t, zones)
+
+	// Test get zone error
+	client.listZonesError = nil
+	client.getZoneError = errors.New("failed to get zone")
+
+	// This should still work for listing but fail when getting individual zones
+	zones, err = cfProvider.Zones(context.Background())
+	assert.NoError(t, err) // List works, individual gets may fail internally
+	assert.NotNil(t, zones)
+}
+
+func TestCloudflareZoneFiltering(t *testing.T) {
+	client := NewMockCloudFlareClient()
+
+	// Test with domain filter only
+	cfProvider := &CloudFlareProvider{
+		Client:       client,
+		domainFilter: endpoint.NewDomainFilter([]string{"foo.com"}),
+		zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
+	}
+
+	zones, err := cfProvider.Zones(context.Background())
+	assert.NoError(t, err)
+	assert.Len(t, zones, 1)
+	assert.Equal(t, "foo.com", zones[0].Name)
+
+	// Test with zone ID filter
+	providerWithIDFilter := &CloudFlareProvider{
+		Client:       client,
+		domainFilter: endpoint.NewDomainFilter([]string{}),
+		zoneIDFilter: provider.NewZoneIDFilter([]string{"002"}),
+	}
+
+	filteredZones, err := providerWithIDFilter.Zones(context.Background())
+	assert.NoError(t, err)
+	assert.Len(t, filteredZones, 1)
+	assert.Equal(t, "foo.com", filteredZones[0].Name) // zone 002 is foo.com
+	assert.Equal(t, "002", filteredZones[0].ID)
+}
+
+func TestCloudflareZonePlanDetection(t *testing.T) {
+	client := NewMockCloudFlareClient()
+	cfProvider := &CloudFlareProvider{
+		Client:       client,
+		domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
+		zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
+	}
+
+	// Test free plan detection (foo.com)
+	assert.False(t, cfProvider.ZoneHasPaidPlan("foo.com"))
+	assert.False(t, cfProvider.ZoneHasPaidPlan("subdomain.foo.com"))
+	assert.False(t, cfProvider.ZoneHasPaidPlan("deep.subdomain.foo.com"))
+
+	// Test paid plan detection (bar.com)
+	assert.True(t, cfProvider.ZoneHasPaidPlan("bar.com"))
+	assert.True(t, cfProvider.ZoneHasPaidPlan("subdomain.bar.com"))
+	assert.True(t, cfProvider.ZoneHasPaidPlan("deep.subdomain.bar.com"))
+
+	// Test invalid domain
+	assert.False(t, cfProvider.ZoneHasPaidPlan("invalid.domain.com"))
+
+	// Test with zone error
+	client.getZoneError = errors.New("zone lookup failed")
+	providerWithError := &CloudFlareProvider{
+		Client:       client,
+		domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
+		zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
+	}
+	assert.False(t, providerWithError.ZoneHasPaidPlan("subdomain.foo.com"))
+}
+
+func TestCloudflareChangesByZone(t *testing.T) {
+	client := NewMockCloudFlareClient()
+	cfProvider := &CloudFlareProvider{
+		Client:       client,
+		domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
+		zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
+	}
+
+	zones, err := cfProvider.Zones(context.Background())
+	assert.NoError(t, err)
+	assert.Len(t, zones, 2)
+
+	// Test empty changes
+	emptyChanges := []*cloudFlareChange{}
+	changesByZone := cfProvider.changesByZone(zones, emptyChanges)
+	assert.Len(t, changesByZone, 2)       // Should return map with zones but empty slices
+	assert.Empty(t, changesByZone["001"]) // bar.com zone should have no changes
+	assert.Empty(t, changesByZone["002"]) // foo.com zone should have no changes
+
+	// Test changes for different zones
+	changes := []*cloudFlareChange{
+		{
+			Action:         cloudFlareCreate,
+			ResourceRecord: dns.RecordResponse{Name: "api.foo.com", Type: "A", Content: "1.2.3.4"},
+		},
+		{
+			Action:         cloudFlareUpdate,
+			ResourceRecord: dns.RecordResponse{Name: "www.foo.com", Type: "CNAME", Content: "foo.com"},
+		},
+		{
+			Action:         cloudFlareCreate,
+			ResourceRecord: dns.RecordResponse{Name: "mail.bar.com", Type: "MX", Content: "10 mail.bar.com"},
+		},
+		{
+			Action:         cloudFlareDelete,
+			ResourceRecord: dns.RecordResponse{Name: "old.bar.com", Type: "A", Content: "5.6.7.8"},
+		},
+	}
+
+	changesByZone = cfProvider.changesByZone(zones, changes)
+	assert.Len(t, changesByZone, 2)
+
+	// Verify bar.com zone changes (zone 001)
+	barChanges := changesByZone["001"]
+	assert.Len(t, barChanges, 2)
+	assert.Equal(t, "mail.bar.com", barChanges[0].ResourceRecord.Name)
+	assert.Equal(t, "old.bar.com", barChanges[1].ResourceRecord.Name)
+
+	// Verify foo.com zone changes (zone 002)
+	fooChanges := changesByZone["002"]
+	assert.Len(t, fooChanges, 2)
+	assert.Equal(t, "api.foo.com", fooChanges[0].ResourceRecord.Name)
+	assert.Equal(t, "www.foo.com", fooChanges[1].ResourceRecord.Name)
+}
+
+func TestConvertCloudflareError(t *testing.T) {
+	tests := []struct {
+		name            string
+		inputError      error
+		expectSoftError bool
+		description     string
+	}{
+		{
+			name:            "Rate limit error via Error type",
+			inputError:      &cloudflarev0.Error{StatusCode: 429, Type: cloudflarev0.ErrorTypeRateLimit},
+			expectSoftError: true,
+			description:     "CloudFlare API rate limit error should be converted to soft error",
+		},
+		{
+			name:            "Rate limit error via ClientRateLimited",
+			inputError:      &cloudflarev0.Error{StatusCode: 429, ErrorCodes: []int{10000}, Type: cloudflarev0.ErrorTypeRateLimit}, // Complete rate limit error
+			expectSoftError: true,
+			description:     "CloudFlare client rate limited error should be converted to soft error",
+		},
+		{
+			name:            "Server error 500",
+			inputError:      &cloudflarev0.Error{StatusCode: 500},
+			expectSoftError: true,
+			description:     "Server error (500+) should be converted to soft error",
+		},
+		{
+			name:            "Server error 502",
+			inputError:      &cloudflarev0.Error{StatusCode: 502},
+			expectSoftError: true,
+			description:     "Server error (502) should be converted to soft error",
+		},
+		{
+			name:            "Server error 503",
+			inputError:      &cloudflarev0.Error{StatusCode: 503},
+			expectSoftError: true,
+			description:     "Server error (503) should be converted to soft error",
+		},
+		{
+			name:            "Rate limit string error",
+			inputError:      errors.New("exceeded available rate limit retries"),
+			expectSoftError: true,
+			description:     "String error containing rate limit message should be converted to soft error",
+		},
+		{
+			name:            "Rate limit string error mixed case",
+			inputError:      errors.New("request failed: exceeded available rate limit retries for this operation"),
+			expectSoftError: true,
+			description:     "String error containing rate limit message should be converted to soft error regardless of context",
+		},
+		{
+			name:            "Client error 400",
+			inputError:      &cloudflarev0.Error{StatusCode: 400},
+			expectSoftError: false,
+			description:     "Client error (400) should not be converted to soft error",
+		},
+		{
+			name:            "Client error 401",
+			inputError:      &cloudflarev0.Error{StatusCode: 401},
+			expectSoftError: false,
+			description:     "Client error (401) should not be converted to soft error",
+		},
+		{
+			name:            "Client error 404",
+			inputError:      &cloudflarev0.Error{StatusCode: 404},
+			expectSoftError: false,
+			description:     "Client error (404) should not be converted to soft error",
+		},
+		{
+			name:            "Generic error",
+			inputError:      errors.New("some generic error"),
+			expectSoftError: false,
+			description:     "Generic error should not be converted to soft error",
+		},
+		{
+			name:            "Network error",
+			inputError:      errors.New("connection refused"),
+			expectSoftError: false,
+			description:     "Network error should not be converted to soft error",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := convertCloudflareError(tt.inputError)
+
+			if tt.expectSoftError {
+				assert.ErrorIs(t, result, provider.SoftError,
+					"Expected soft error for %s: %s", tt.name, tt.description)
+
+				// Verify the original error message is preserved in the soft error
+				assert.Contains(t, result.Error(), tt.inputError.Error(),
+					"Original error message should be preserved")
+			} else {
+				assert.NotErrorIs(t, result, provider.SoftError,
+					"Expected non-soft error for %s: %s", tt.name, tt.description)
+				assert.Equal(t, tt.inputError, result,
+					"Non-soft errors should be returned unchanged")
+			}
+		})
+	}
+}
+
+func TestConvertCloudflareErrorInContext(t *testing.T) {
+	tests := []struct {
+		name            string
+		setupMock       func(*mockCloudFlareClient)
+		function        func(*CloudFlareProvider) error
+		expectSoftError bool
+		description     string
+	}{
+		{
+			name: "Zones with GetZone rate limit error",
+			setupMock: func(client *mockCloudFlareClient) {
+				client.Zones = map[string]string{"zone1": "example.com"}
+				client.getZoneError = &cloudflarev0.Error{StatusCode: 429, Type: cloudflarev0.ErrorTypeRateLimit}
+			},
+			function: func(p *CloudFlareProvider) error {
+				p.zoneIDFilter.ZoneIDs = []string{"zone1"}
+				_, err := p.Zones(context.Background())
+				return err
+			},
+			expectSoftError: true,
+			description:     "Zones function should convert GetZone rate limit errors to soft errors",
+		},
+		{
+			name: "Zones with GetZone server error",
+			setupMock: func(client *mockCloudFlareClient) {
+				client.Zones = map[string]string{"zone1": "example.com"}
+				client.getZoneError = &cloudflarev0.Error{StatusCode: 500}
+			},
+			function: func(p *CloudFlareProvider) error {
+				p.zoneIDFilter.ZoneIDs = []string{"zone1"}
+				_, err := p.Zones(context.Background())
+				return err
+			},
+			expectSoftError: true,
+			description:     "Zones function should convert GetZone server errors to soft errors",
+		},
+		{
+			name: "Zones with GetZone client error",
+			setupMock: func(client *mockCloudFlareClient) {
+				client.Zones = map[string]string{"zone1": "example.com"}
+				client.getZoneError = &cloudflarev0.Error{StatusCode: 404}
+			},
+			function: func(p *CloudFlareProvider) error {
+				p.zoneIDFilter.ZoneIDs = []string{"zone1"}
+				_, err := p.Zones(context.Background())
+				return err
+			},
+			expectSoftError: false,
+			description:     "Zones function should not convert GetZone client errors to soft errors",
+		},
+		{
+			name: "Zones with ListZones rate limit error",
+			setupMock: func(client *mockCloudFlareClient) {
+				client.listZonesError = errors.New("exceeded available rate limit retries")
+			},
+			function: func(p *CloudFlareProvider) error {
+				_, err := p.Zones(context.Background())
+				return err
+			},
+			expectSoftError: true,
+			description:     "Zones function should convert ListZones rate limit string errors to soft errors",
+		},
+		{
+			name: "Zones with ListZones server error",
+			setupMock: func(client *mockCloudFlareClient) {
+				client.listZonesError = &cloudflarev0.Error{StatusCode: 503}
+			},
+			function: func(p *CloudFlareProvider) error {
+				_, err := p.Zones(context.Background())
+				return err
+			},
+			expectSoftError: true,
+			description:     "Zones function should convert ListZones server errors to soft errors",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client := NewMockCloudFlareClient()
+			tt.setupMock(client)
+
+			p := &CloudFlareProvider{
+				Client:       client,
+				zoneIDFilter: provider.ZoneIDFilter{},
+			}
+
+			err := tt.function(p)
+			assert.Error(t, err, "Expected an error from %s", tt.name)
+
+			if tt.expectSoftError {
+				assert.ErrorIs(t, err, provider.SoftError,
+					"Expected soft error for %s: %s", tt.name, tt.description)
+			} else {
+				assert.NotErrorIs(t, err, provider.SoftError,
+					"Expected non-soft error for %s: %s", tt.name, tt.description)
+			}
+		})
+	}
+}
+
+func TestCloudFlareZonesDomainFilter(t *testing.T) {
+	// Set required environment variables for CloudFlare provider
+	t.Setenv("CF_API_TOKEN", "test-token")
+
+	client := NewMockCloudFlareClient()
+
+	// Create a domain filter that only matches "bar.com"
+	// This should filter out "foo.com" and trigger the debug log
+	domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
+
+	p, err := NewCloudFlareProvider(
+		domainFilter,
+		provider.NewZoneIDFilter([]string{""}), // empty zone ID filter so it uses ListZones path
+		false,                                  // proxied
+		false,                                  // dry run
+		RegionalServicesConfig{},
+		CustomHostnamesConfig{},
+		DNSRecordsConfig{PerPage: 50},
+	)
+	require.NoError(t, err)
+
+	// Replace the real client with our mock
+	p.Client = client
+
+	// Capture debug logs to verify the filter log message
+	oldLevel := log.GetLevel()
+	log.SetLevel(log.DebugLevel)
+	defer log.SetLevel(oldLevel)
+
+	// Use a custom formatter to capture log output
+	var logOutput strings.Builder
+	log.SetOutput(&logOutput)
+	defer log.SetOutput(os.Stderr)
+
+	// Call Zones() which should trigger the domain filter logic
+	zones, err := p.Zones(context.Background())
+	require.NoError(t, err)
+
+	// Should only return the "bar.com" zone since "foo.com" is filtered out
+	assert.Len(t, zones, 1)
+	assert.Equal(t, "bar.com", zones[0].Name)
+	assert.Equal(t, "001", zones[0].ID)
+
+	// Verify that the debug log was written for the filtered zone
+	logString := logOutput.String()
+	assert.Contains(t, logString, `zone \"foo.com\" not in domain filter`)
+	assert.Contains(t, logString, "no zoneIDFilter configured, looking at all zones")
+}
+
+func TestZoneIDByNameIteratorError(t *testing.T) {
+	client := NewMockCloudFlareClient()
+
+	// Set up an error that will be returned by the ListZones iterator (line 144)
+	client.listZonesError = fmt.Errorf("CloudFlare API connection timeout")
+
+	// Call ZoneIDByName which should hit line 144 (iterator error handling)
+	zoneID, err := client.ZoneIDByName("example.com")
+
+	// Should return empty zone ID and the wrapped iterator error
+	assert.Empty(t, zoneID)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "failed to list zones from CloudFlare API")
+	assert.Contains(t, err.Error(), "CloudFlare API connection timeout")
+}
+
+func TestZoneIDByNameZoneNotFound(t *testing.T) {
+	client := NewMockCloudFlareClient()
+
+	// Set up mock to return different zones but not the one we're looking for
+	client.Zones = map[string]string{
+		"zone456": "different.com",
+		"zone789": "another.com",
+	}
+
+	// Call ZoneIDByName for a zone that doesn't exist, should hit line 147 (zone not found)
+	zoneID, err := client.ZoneIDByName("nonexistent.com")
+
+	// Should return empty zone ID and the improved error message
+	assert.Empty(t, zoneID)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), `zone "nonexistent.com" not found in CloudFlare account`)
+	assert.Contains(t, err.Error(), "verify the zone exists and API credentials have access to it")
+}
+
+func TestDnsRecordFromLegacyAPI(t *testing.T) {
+	parseTime := func(s string) time.Time {
+		parsed, err := time.Parse(time.RFC3339, s)
+		if err != nil {
+			t.Fatal("failed to parse time:", err)
+		}
+		return parsed
+	}
+
+	tests := []struct {
+		name   string
+		input  cloudflarev0.DNSRecord
+		expect dns.RecordResponse
+	}{
+		{
+			name: "All fields set",
+			input: cloudflarev0.DNSRecord{
+				CreatedOn:  parseTime("2024-06-01T12:00:00Z"),
+				ModifiedOn: parseTime("2024-06-02T12:00:00Z"),
+				Type:       "A",
+				Name:       "example.com",
+				Content:    "1.2.3.4",
+				Meta:       map[string]any{"foo": "bar"},
+				Data:       map[string]any{"baz": "qux"},
+				ID:         "record-id",
+				Priority:   testutils.ToPtr(uint16(10)),
+				TTL:        120,
+				Proxied:    testutils.ToPtr(true),
+				Proxiable:  true,
+				Comment:    "test comment",
+				Tags:       []string{"tag1", "tag2"},
+			},
+			expect: dns.RecordResponse{
+				CreatedOn:  parseTime("2024-06-01T12:00:00Z"),
+				ModifiedOn: parseTime("2024-06-02T12:00:00Z"),
+				Type:       "A",
+				Name:       "example.com",
+				Content:    "1.2.3.4",
+				Meta:       map[string]any{"foo": "bar"},
+				Data:       map[string]any{"baz": "qux"},
+				ID:         "record-id",
+				Priority:   10,
+				TTL:        120,
+				Proxied:    true,
+				Proxiable:  true,
+				Comment:    "test comment",
+				Tags:       []string{"tag1", "tag2"},
+			},
+		},
+		{
+			name: "Nil priority and proxied",
+			input: cloudflarev0.DNSRecord{
+				Type:      "TXT",
+				Name:      "txt.example.com",
+				Content:   "some text",
+				Priority:  nil,
+				TTL:       300,
+				Proxied:   nil,
+				Proxiable: false,
+				Tags:      []string(nil),
+			},
+			expect: dns.RecordResponse{
+				Type:      "TXT",
+				Name:      "txt.example.com",
+				Content:   "some text",
+				Priority:  0,
+				TTL:       300,
+				Proxied:   false,
+				Proxiable: false,
+				Tags:      []string(nil),
+			},
+		},
+		{
+			name: "Proxied false",
+			input: cloudflarev0.DNSRecord{
+				Type:     "CNAME",
+				Name:     "cname.example.com",
+				Content:  "target.example.com",
+				Proxied:  testutils.ToPtr(false),
+				TTL:      60,
+				Priority: nil,
+				Tags:     []string(nil),
+			},
+			expect: dns.RecordResponse{
+				Type:     "CNAME",
+				Name:     "cname.example.com",
+				Content:  "target.example.com",
+				Proxied:  false,
+				TTL:      60,
+				Priority: 0,
+				Tags:     []string(nil),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := dnsRecordResponseFromLegacyDNSRecord(tt.input)
+			assert.Equal(t, tt.expect, got)
+		})
+	}
+}
+
+func TestZoneService(t *testing.T) {
+	t.Parallel()
+
+	ctx, cancel := context.WithCancel(t.Context())
+	cancel()
+
+	serviceV0, err := cloudflarev0.NewWithAPIToken("fake-token")
+	require.NoError(t, err)
+
+	client := &zoneService{
+		service:   cloudflare.NewClient(),
+		serviceV0: serviceV0,
+	}
+
+	zoneID := "foo"
+	rc := cloudflarev0.ZoneIdentifier(zoneID)
+
+	t.Run("ListDNSRecord", func(t *testing.T) {
+		t.Parallel()
+		iter := client.ListDNSRecords(ctx, dns.RecordListParams{ZoneID: cloudflare.F("foo")})
+		assert.False(t, iter.Next())
+		assert.Empty(t, iter.Current())
+		assert.ErrorIs(t, iter.Err(), context.Canceled)
+	})
+
+	t.Run("CreateDNSRecord", func(t *testing.T) {
+		t.Parallel()
+		params := getCreateDNSRecordParam(zoneID, &cloudFlareChange{})
+		record, err := client.CreateDNSRecord(ctx, params)
+		assert.Empty(t, record)
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("UpdateDNSRecord", func(t *testing.T) {
+		t.Parallel()
+		params := updateDNSRecordParam(cloudFlareChange{})
+		params.ID = "1234"
+		err := client.UpdateDNSRecord(ctx, rc, params)
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("DeleteDNSRecord", func(t *testing.T) {
+		t.Parallel()
+		err := client.DeleteDNSRecord(ctx, "1234", dns.RecordDeleteParams{ZoneID: cloudflare.F("foo")})
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("ListZones", func(t *testing.T) {
+		t.Parallel()
+		iter := client.ListZones(ctx, listZonesV4Params())
+		assert.False(t, iter.Next())
+		assert.Empty(t, iter.Current())
+		assert.ErrorIs(t, iter.Err(), context.Canceled)
+	})
+
+	t.Run("GetZone", func(t *testing.T) {
+		t.Parallel()
+		zone, err := client.GetZone(ctx, zoneID)
+		assert.Nil(t, zone)
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("ListDataLocalizationRegionalHostnames", func(t *testing.T) {
+		t.Parallel()
+		params := listDataLocalizationRegionalHostnamesParams(zoneID)
+		iter := client.ListDataLocalizationRegionalHostnames(ctx, params)
+		assert.False(t, iter.Next())
+		assert.Empty(t, iter.Current())
+		assert.ErrorIs(t, iter.Err(), context.Canceled)
+	})
+
+	t.Run("CreateDataLocalizationRegionalHostname", func(t *testing.T) {
+		t.Parallel()
+		params := createDataLocalizationRegionalHostnameParams(zoneID, regionalHostnameChange{})
+		err := client.CreateDataLocalizationRegionalHostname(ctx, params)
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("DeleteDataLocalizationRegionalHostname", func(t *testing.T) {
+		t.Parallel()
+		params := deleteDataLocalizationRegionalHostnameParams(zoneID, regionalHostnameChange{})
+		err := client.DeleteDataLocalizationRegionalHostname(ctx, "foo", params)
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("UpdateDataLocalizationRegionalHostname", func(t *testing.T) {
+		t.Parallel()
+		params := updateDataLocalizationRegionalHostnameParams(zoneID, regionalHostnameChange{})
+		err := client.UpdateDataLocalizationRegionalHostname(ctx, "foo", params)
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("CustomHostnames", func(t *testing.T) {
+		t.Parallel()
+		ch, info, err := client.CustomHostnames(ctx, zoneID, 0, cloudflarev0.CustomHostname{})
+		assert.Empty(t, ch)
+		assert.Empty(t, info)
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("CreateCustomHostname", func(t *testing.T) {
+		t.Parallel()
+		resp, err := client.CreateCustomHostname(ctx, zoneID, cloudflarev0.CustomHostname{})
+		assert.Empty(t, resp)
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("DeleteCustomHostname", func(t *testing.T) {
+		t.Parallel()
+		err := client.DeleteCustomHostname(ctx, zoneID, "foo")
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+
+	t.Run("DeleteDNSRecord", func(t *testing.T) {
+		t.Parallel()
+		err := client.DeleteDNSRecord(ctx, "1234", dns.RecordDeleteParams{ZoneID: cloudflare.F("foo")})
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+}
diff --git a/provider/pihole/client_test.go b/provider/pihole/client_test.go
index 1a775056..b94545b4 100644
--- a/provider/pihole/client_test.go
+++ b/provider/pihole/client_test.go
@@ -96,6 +96,32 @@ func TestNewPiholeClient(t *testing.T) {
 	}
 }
 
+// Helper function to validate records against expected values
+func ValidateRecords(t *testing.T, records []*endpoint.Endpoint, expected [][]string, expectedCount int, recordType string) {
+	t.Helper()
+	if len(records) != expectedCount {
+		t.Fatalf("Expected %d %s records returned, got: %d", expectedCount, recordType, len(records))
+	}
+	for idx, rec := range records {
+		if rec.DNSName != expected[idx][0] {
+			t.Errorf("Got invalid DNS Name: %s, expected: %s", rec.DNSName, expected[idx][0])
+		}
+		if rec.Targets[0] != expected[idx][1] {
+			t.Errorf("Got invalid target: %s, expected: %s", rec.Targets[0], expected[idx][1])
+		}
+	}
+}
+
+// Helper function to test record retrieval for a specific type
+func CheckRecordRetrieval(t *testing.T, cl *piholeClient, recordType string, expected [][]string, expectedCount int) {
+	t.Helper()
+	records, err := cl.listRecords(context.Background(), recordType)
+	if err != nil {
+		t.Fatal(err)
+	}
+	ValidateRecords(t, records, expected, expectedCount, recordType)
+}
+
 func TestListRecords(t *testing.T) {
 	srvr := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
 		r.ParseForm()
@@ -140,76 +166,27 @@ func TestListRecords(t *testing.T) {
 	}
 
 	// Test retrieve A records unfiltered
-	arecs, err := cl.listRecords(context.Background(), endpoint.RecordTypeA)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if len(arecs) != 3 {
-		t.Fatal("Expected 3 A records returned, got:", len(arecs))
-	}
-	// Ensure records were parsed correctly
-	expected := [][]string{
+	CheckRecordRetrieval(t, cl.(*piholeClient), endpoint.RecordTypeA, [][]string{
 		{"test1.example.com", "192.168.1.1"},
 		{"test2.example.com", "192.168.1.2"},
 		{"test3.match.com", "192.168.1.3"},
-	}
-	for idx, rec := range arecs {
-		if rec.DNSName != expected[idx][0] {
-			t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
-		}
-		if rec.Targets[0] != expected[idx][1] {
-			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
-		}
-	}
+	}, 3)
 
 	// Test retrieve AAAA records unfiltered
-	arecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeAAAA)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if len(arecs) != 3 {
-		t.Fatal("Expected 3 AAAA records returned, got:", len(arecs))
-	}
-	// Ensure records were parsed correctly
-	expected = [][]string{
+	CheckRecordRetrieval(t, cl.(*piholeClient), endpoint.RecordTypeAAAA, [][]string{
 		{"test1.example.com", "fc00::1:192:168:1:1"},
 		{"test2.example.com", "fc00::1:192:168:1:2"},
 		{"test3.match.com", "fc00::1:192:168:1:3"},
-	}
-	for idx, rec := range arecs {
-		if rec.DNSName != expected[idx][0] {
-			t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
-		}
-		if rec.Targets[0] != expected[idx][1] {
-			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
-		}
-	}
+	}, 3)
 
 	// Test retrieve CNAME records unfiltered
-	cnamerecs, err := cl.listRecords(context.Background(), endpoint.RecordTypeCNAME)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if len(cnamerecs) != 3 {
-		t.Fatal("Expected 3 CAME records returned, got:", len(cnamerecs))
-	}
-	// Ensure records were parsed correctly
-	expected = [][]string{
+	CheckRecordRetrieval(t, cl.(*piholeClient), endpoint.RecordTypeCNAME, [][]string{
 		{"test4.example.com", "cname.example.com"},
 		{"test5.example.com", "cname.example.com"},
 		{"test6.match.com", "cname.example.com"},
-	}
-	for idx, rec := range cnamerecs {
-		if rec.DNSName != expected[idx][0] {
-			t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
-		}
-		if rec.Targets[0] != expected[idx][1] {
-			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
-		}
-	}
+	}, 3)
 
 	// Same tests but with a domain filter
-
 	cfg.DomainFilter = endpoint.NewDomainFilter([]string{"match.com"})
 	cl, err = newPiholeClient(cfg)
 	if err != nil {
@@ -217,68 +194,53 @@ func TestListRecords(t *testing.T) {
 	}
 
 	// Test retrieve A records filtered
-	arecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeA)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if len(arecs) != 1 {
-		t.Fatal("Expected 1 A record returned, got:", len(arecs))
-	}
-	// Ensure records were parsed correctly
-	expected = [][]string{
+	CheckRecordRetrieval(t, cl.(*piholeClient), endpoint.RecordTypeA, [][]string{
 		{"test3.match.com", "192.168.1.3"},
-	}
-	for idx, rec := range arecs {
-		if rec.DNSName != expected[idx][0] {
-			t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
-		}
-		if rec.Targets[0] != expected[idx][1] {
-			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
-		}
-	}
+	}, 1)
 
 	// Test retrieve AAAA records filtered
-	arecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeAAAA)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if len(arecs) != 1 {
-		t.Fatal("Expected 1 AAAA record returned, got:", len(arecs))
-	}
-	// Ensure records were parsed correctly
-	expected = [][]string{
+	CheckRecordRetrieval(t, cl.(*piholeClient), endpoint.RecordTypeAAAA, [][]string{
 		{"test3.match.com", "fc00::1:192:168:1:3"},
-	}
-	for idx, rec := range arecs {
-		if rec.DNSName != expected[idx][0] {
-			t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
-		}
-		if rec.Targets[0] != expected[idx][1] {
-			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
-		}
-	}
+	}, 1)
 
 	// Test retrieve CNAME records filtered
-	cnamerecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	CheckRecordRetrieval(t, cl.(*piholeClient), endpoint.RecordTypeCNAME, [][]string{
+		{"test6.match.com", "cname.example.com"},
+	}, 1)
+
+}
+
+// Helper function to test error scenarios
+func testErrorScenarios(t *testing.T, srvrErr *httptest.Server) {
+	t.Helper()
+	cfgExpired := PiholeConfig{
+		Server: srvrErr.URL,
+	}
+	clExpired, err := newPiholeClient(cfgExpired)
 	if err != nil {
 		t.Fatal(err)
 	}
-	if len(cnamerecs) != 1 {
-		t.Fatal("Expected 1 CNAME record returned, got:", len(cnamerecs))
+	//set clExpired.token to a valid token
+	clExpired.(*piholeClient).token = "expired"
+	clExpired.(*piholeClient).cfg.Password = "notcorrect"
+
+	cnamerecs, err := clExpired.listRecords(context.Background(), "notarealrecordtype")
+	if err == nil {
+		t.Fatal("Should return error, type is unknown ! ")
 	}
-	// Ensure records were parsed correctly
-	expected = [][]string{
-		{"test6.match.com", "cname.example.com"},
+	cnamerecs, err = clExpired.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	if err == nil {
+		t.Fatal("Should return error on failed auth ! ")
 	}
-	for idx, rec := range cnamerecs {
-		if rec.DNSName != expected[idx][0] {
-			t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
-		}
-		if rec.Targets[0] != expected[idx][1] {
-			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
-		}
+	clExpired.(*piholeClient).token = "correct"
+	clExpired.(*piholeClient).cfg.Password = "correct"
+	cnamerecs, err = clExpired.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	if len(cnamerecs) != 0 {
+		t.Fatal("Should return empty on missing data in response ! ")
 	}
+}
 
+func TestErrorScenarios(t *testing.T) {
 	// Test errors token
 	srvrErr := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
 		r.ParseForm()
@@ -318,6 +280,7 @@ func TestListRecords(t *testing.T) {
 		`))
 	})
 	defer srvrErr.Close()
+
 	cfgExpired := PiholeConfig{
 		Server: srvrErr.URL,
 	}
@@ -329,21 +292,23 @@ func TestListRecords(t *testing.T) {
 	clExpired.(*piholeClient).token = "expired"
 	clExpired.(*piholeClient).cfg.Password = "notcorrect"
 
-	cnamerecs, err = clExpired.listRecords(context.Background(), "notarealrecordtype")
+	_, err = clExpired.listRecords(context.Background(), "notarealrecordtype")
 	if err == nil {
 		t.Fatal("Should return error, type is unknown ! ")
 	}
-	cnamerecs, err = clExpired.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	_, err = clExpired.listRecords(context.Background(), endpoint.RecordTypeCNAME)
 	if err == nil {
 		t.Fatal("Should return error on failed auth ! ")
 	}
 	clExpired.(*piholeClient).token = "correct"
 	clExpired.(*piholeClient).cfg.Password = "correct"
-	cnamerecs, err = clExpired.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	cnamerecs, err := clExpired.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	if err != nil {
+		t.Fatal(err)
+	}
 	if len(cnamerecs) != 0 {
 		t.Fatal("Should return empty on missing data in response ! ")
 	}
-
 }
 
 func TestCreateRecord(t *testing.T) {
diff --git a/provider/zonefinder_test.go b/provider/zonefinder_test.go
index dccfe5ad..b02a0388 100644
--- a/provider/zonefinder_test.go
+++ b/provider/zonefinder_test.go
@@ -32,14 +32,18 @@ func TestZoneIDName(t *testing.T) {
 	z.Add("654321", "foo.qux.baz")
 	z.Add("987654", "エイミー.みんな")
 	z.Add("123123", "_metadata.example.com")
+	z.Add("1231231", "_foo._metadata.example.com")
 	z.Add("456456", "_metadata.エイミー.みんな")
+	z.Add("123412", "*.example.com")
 
 	assert.Equal(t, ZoneIDName{
-		"123456": "qux.baz",
-		"654321": "foo.qux.baz",
-		"987654": "エイミー.みんな",
-		"123123": "_metadata.example.com",
-		"456456": "_metadata.エイミー.みんな",
+		"123456":  "qux.baz",
+		"654321":  "foo.qux.baz",
+		"987654":  "エイミー.みんな",
+		"123123":  "_metadata.example.com",
+		"1231231": "_foo._metadata.example.com",
+		"456456":  "_metadata.エイミー.みんな",
+		"123412":  "*.example.com",
 	}, z)
 
 	// simple entry in a domain
@@ -77,8 +81,16 @@ func TestZoneIDName(t *testing.T) {
 	assert.Equal(t, "エイミー.みんな", zoneName)
 	assert.Equal(t, "987654", zoneID)
 
+	zoneID, zoneName = z.FindZone("_foo._metadata.example.com")
+	assert.Equal(t, "_foo._metadata.example.com", zoneName)
+	assert.Equal(t, "1231231", zoneID)
+
+	zoneID, zoneName = z.FindZone("*.example.com")
+	assert.Equal(t, "*.example.com", zoneName)
+	assert.Equal(t, "123412", zoneID)
+
 	hook := testutils.LogsUnderTestWithLogLevel(log.WarnLevel, t)
-	_, _ = z.FindZone("???")
+	_, _ = z.FindZone("xn--not-a-valid-punycode")
 
-	testutils.TestHelperLogContains("Failed to convert label \"???\" of hostname \"???\" to its Unicode form: idna: disallowed rune U+003F", hook, t)
+	testutils.TestHelperLogContains("Failed to convert label \"xn--not-a-valid-punycode\" of hostname \"xn--not-a-valid-punycode\" to its Unicode form: idna: invalid label", hook, t)
 }
diff --git a/registry/txt_test.go b/registry/txt_test.go
index d59ce90e..05cdd030 100644
--- a/registry/txt_test.go
+++ b/registry/txt_test.go
@@ -18,6 +18,7 @@ package registry
 
 import (
 	"context"
+	"fmt"
 	"reflect"
 	"strings"
 	"testing"
@@ -1807,3 +1808,223 @@ func TestTXTRegistryRecordsWithEmptyTargets(t *testing.T) {
 
 	testutils.TestHelperLogContains("TXT record has no targets empty-targets.test-zone.example.org", hook, t)
 }
+
+// TestTXTRegistryRecreatesMissingRecords reproduces issue #4914.
+// It verifies that External‑DNS recreates A/CNAME records that were accidentally deleted while their corresponding TXT records remain.
+// An InMemoryProvider is used because, like Route53, it throws an error when attempting to create a duplicate record.
+func TestTXTRegistryRecreatesMissingRecords(t *testing.T) {
+	ownerId := "owner"
+	tests := []struct {
+		name           string
+		desired        []*endpoint.Endpoint
+		existing       []*endpoint.Endpoint
+		expectedCreate []*endpoint.Endpoint
+	}{
+		{
+			name: "Recreate missing A record when TXT exists",
+			desired: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""),
+			},
+			existing: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("a-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+			},
+			expectedCreate: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ownerId),
+			},
+		},
+		{
+			name: "Recreate missing AAAA record when TXT exists",
+			desired: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "2001:db8::1", endpoint.RecordTypeAAAA, ""),
+			},
+			existing: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("aaaa-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+			},
+			expectedCreate: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "2001:db8::1", endpoint.RecordTypeAAAA, ownerId),
+			},
+		},
+		{
+			name: "Recreate missing CNAME record when TXT exists",
+			desired: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""),
+			},
+			existing: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("cname-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+			},
+			expectedCreate: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ownerId)},
+		},
+		{
+			name: "Recreate missing A and CNAME records when TXT exists",
+			desired: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""),
+				newEndpointWithOwner("new-record-2.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""),
+			},
+			existing: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("new-record-2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("a-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("cname-new-record-2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+			},
+			expectedCreate: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ownerId),
+				newEndpointWithOwner("new-record-2.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ownerId),
+			},
+		},
+		{
+			name: "Recreate missing A records when TXT and CNAME exists",
+			desired: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""),
+				newEndpointWithOwner("new-record-2.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""),
+			},
+			existing: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-2.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ownerId),
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("new-record-2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("a-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("cname-new-record-2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+			},
+			expectedCreate: []*endpoint.Endpoint{
+				newEndpointWithOwner("new-record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ownerId),
+			},
+		},
+		{
+			name: "Only one A record is missing among several existing records",
+			desired: []*endpoint.Endpoint{
+				newEndpointWithOwner("record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""),
+				newEndpointWithOwner("record-2.test-zone.example.org", "1.1.1.2", endpoint.RecordTypeA, ""),
+				newEndpointWithOwner("record-3.test-zone.example.org", "1.1.1.3", endpoint.RecordTypeA, ""),
+				newEndpointWithOwner("record-4.test-zone.example.org", "2001:db8::4", endpoint.RecordTypeAAAA, ""),
+				newEndpointWithOwner("record-5.test-zone.example.org", "cluster-b", endpoint.RecordTypeCNAME, ""),
+			},
+			existing: []*endpoint.Endpoint{
+				newEndpointWithOwner("record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("a-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+
+				newEndpointWithOwner("record-2.test-zone.example.org", "1.1.1.2", endpoint.RecordTypeA, ownerId),
+				newEndpointWithOwner("record-2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("a-record-2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+
+				newEndpointWithOwner("record-3.test-zone.example.org", "1.1.1.3", endpoint.RecordTypeA, ownerId),
+				newEndpointWithOwner("record-3.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("a-record-3.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+
+				newEndpointWithOwner("record-4.test-zone.example.org", "2001:db8::4", endpoint.RecordTypeAAAA, ownerId),
+				newEndpointWithOwner("record-4.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("aaaa-record-4.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+
+				newEndpointWithOwner("record-5.test-zone.example.org", "cluster-b", endpoint.RecordTypeCNAME, ownerId),
+				newEndpointWithOwner("record-5.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+				newEndpointWithOwner("cname-record-5.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+ownerId+"\"", endpoint.RecordTypeTXT, ownerId),
+			},
+			expectedCreate: []*endpoint.Endpoint{
+				newEndpointWithOwner("record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ownerId),
+			},
+		},
+		{
+			name: "Should not recreate TXT records for existing A records without owner",
+			desired: []*endpoint.Endpoint{
+				newEndpointWithOwner("record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""),
+			},
+			existing: []*endpoint.Endpoint{
+				newEndpointWithOwner("record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ownerId),
+				// Missing TXT record for the existing A record
+			},
+			expectedCreate: []*endpoint.Endpoint{},
+		},
+		{
+			name: "Should not recreate TXT records for existing A records with another owner",
+			desired: []*endpoint.Endpoint{
+				newEndpointWithOwner("record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, ""),
+			},
+			existing: []*endpoint.Endpoint{
+				// This test uses the `ownerId` variable, and "another-owner" simulates a different owner.
+				// In this case, TXT records should not be recreated.
+				newEndpointWithOwner("record-1.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, "another-owner"),
+				newEndpointWithOwner("a-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner="+"another-owner"+"\"", endpoint.RecordTypeTXT, "another-owner"),
+			},
+			expectedCreate: []*endpoint.Endpoint{},
+		},
+	}
+	for _, tt := range tests {
+		for _, setIdentifier := range []string{"", "set-identifier"} {
+			for pName, policy := range plan.Policies {
+				// Clone inputs per policy to avoid data races when using t.Parallel.
+				desired := cloneEndpointsWithOpts(tt.desired, func(e *endpoint.Endpoint) {
+					e.WithSetIdentifier(setIdentifier)
+				})
+				existing := cloneEndpointsWithOpts(tt.existing, func(e *endpoint.Endpoint) {
+					e.WithSetIdentifier(setIdentifier)
+				})
+				expectedCreate := cloneEndpointsWithOpts(tt.expectedCreate, func(e *endpoint.Endpoint) {
+					e.WithSetIdentifier(setIdentifier)
+				})
+
+				t.Run(fmt.Sprintf("%s with %s policy and setIdentifier=%s", tt.name, pName, setIdentifier), func(t *testing.T) {
+					t.Parallel()
+					ctx := context.Background()
+					p := inmemory.NewInMemoryProvider()
+
+					// Given: Register existing records
+					p.CreateZone(testZone)
+					err := p.ApplyChanges(ctx, &plan.Changes{Create: existing})
+					assert.NoError(t, err)
+
+					// The first ApplyChanges call should create the expected records.
+					// Subsequent calls are expected to be no-ops (i.e., no additional creates).
+					isCalled := false
+					p.OnApplyChanges = func(ctx context.Context, changes *plan.Changes) {
+						if isCalled {
+							assert.Empty(t, changes.Create, "ApplyChanges should not be called multiple times with new changes")
+						} else {
+							assert.True(t,
+								testutils.SameEndpoints(changes.Create, expectedCreate),
+								"Expected create changes: %v, but got: %v", expectedCreate, changes.Create,
+							)
+						}
+						assert.Empty(t, changes.UpdateNew, "UpdateNew should be empty")
+						assert.Empty(t, changes.UpdateOld, "UpdateOld should be empty")
+						assert.Empty(t, changes.Delete, "Delete should be empty")
+						isCalled = true
+					}
+
+					// When: Apply changes to recreate missing A records
+					managedRecords := []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeAAAA, endpoint.RecordTypeTXT}
+					registry, err := NewTXTRegistry(p, "", "", ownerId, time.Hour, "", managedRecords, nil, false, nil)
+					assert.NoError(t, err)
+
+					expectedRecords := append(existing, expectedCreate...)
+
+					// Simulate the reconciliation loop by executing multiple times
+					reconciliationLoops := 3
+					for i := range reconciliationLoops {
+						records, err := registry.Records(ctx)
+						assert.NoError(t, err)
+						plan := &plan.Plan{
+							Policies:       []plan.Policy{policy},
+							Current:        records,
+							Desired:        desired,
+							ManagedRecords: managedRecords,
+							OwnerID:        ownerId,
+						}
+						plan = plan.Calculate()
+						err = registry.ApplyChanges(ctx, plan.Changes)
+						assert.NoError(t, err)
+
+						// Then: Verify that the missing records are recreated or the existing records are not modified
+						records, err = p.Records(ctx)
+						assert.NoError(t, err)
+						assert.True(t, testutils.SameEndpoints(records, expectedRecords),
+							"Expected records after reconciliation loop #%d: %v, but got: %v",
+							i, expectedRecords, records,
+						)
+					}
+				})
+			}
+		}
+	}
+}
diff --git a/registry/txt_utils_test.go b/registry/txt_utils_test.go
index 5ff663c5..e37c5495 100644
--- a/registry/txt_utils_test.go
+++ b/registry/txt_utils_test.go
@@ -43,3 +43,51 @@ func newEndpointWithOwnerResource(dnsName, target, recordType, ownerID, resource
 	e.Labels[endpoint.ResourceLabelKey] = resource
 	return e
 }
+
+// This is primarily used to prevent data races when running tests in parallel (t.Parallel).
+func cloneEndpointsWithOpts(list []*endpoint.Endpoint, opt ...func(*endpoint.Endpoint)) []*endpoint.Endpoint {
+	cloned := make([]*endpoint.Endpoint, len(list))
+	for i, e := range list {
+		cloned[i] = cloneEndpointWithOpts(e, opt...)
+	}
+	return cloned
+}
+
+func cloneEndpointWithOpts(e *endpoint.Endpoint, opt ...func(*endpoint.Endpoint)) *endpoint.Endpoint {
+	targets := make(endpoint.Targets, len(e.Targets))
+	copy(targets, e.Targets)
+
+	// SameEndpoints treats nil and empty maps/slices as different.
+	// To avoid introducing unintended differences, we retain nil when original is nil.
+	var labels endpoint.Labels
+	if e.Labels != nil {
+		labels = make(endpoint.Labels, len(e.Labels))
+		for k, v := range e.Labels {
+			labels[k] = v
+		}
+	}
+
+	var providerSpecific endpoint.ProviderSpecific
+	if e.ProviderSpecific != nil {
+		providerSpecific = make(endpoint.ProviderSpecific, len(e.ProviderSpecific))
+		for i, p := range e.ProviderSpecific {
+			providerSpecific[i] = p
+		}
+	}
+
+	ttl := e.RecordTTL
+
+	ep := &endpoint.Endpoint{
+		DNSName:          e.DNSName,
+		Targets:          targets,
+		RecordType:       e.RecordType,
+		RecordTTL:        ttl,
+		Labels:           labels,
+		ProviderSpecific: providerSpecific,
+		SetIdentifier:    e.SetIdentifier,
+	}
+	for _, o := range opt {
+		o(ep)
+	}
+	return ep
+}
diff --git a/source/crd_test.go b/source/crd_test.go
index 0f92d0db..ff6cec41 100644
--- a/source/crd_test.go
+++ b/source/crd_test.go
@@ -548,7 +548,7 @@ func TestCRDSource_AddEventHandler_Add(t *testing.T) {
 
 	require.Eventually(t, func() bool {
 		return counter.Load() == 1
-	}, time.Second, 10*time.Millisecond)
+	}, 2*time.Second, 10*time.Millisecond)
 }
 
 func TestCRDSource_AddEventHandler_Update(t *testing.T) {
@@ -569,7 +569,7 @@ func TestCRDSource_AddEventHandler_Update(t *testing.T) {
 
 	require.Eventually(t, func() bool {
 		return len(watcher.Items) == 1
-	}, time.Second, 10*time.Millisecond)
+	}, 2*time.Second, 10*time.Millisecond)
 
 	modified := obj.DeepCopy()
 	modified.SetLabels(map[string]string{"new-label": "this"})
@@ -577,11 +577,11 @@ func TestCRDSource_AddEventHandler_Update(t *testing.T) {
 
 	require.Eventually(t, func() bool {
 		return len(watcher.Items) == 1
-	}, time.Second, 10*time.Millisecond)
+	}, 2*time.Second, 10*time.Millisecond)
 
 	require.Eventually(t, func() bool {
 		return counter.Load() == 2
-	}, time.Second, 10*time.Millisecond)
+	}, 2*time.Second, 10*time.Millisecond)
 }
 
 func TestCRDSource_AddEventHandler_Delete(t *testing.T) {
@@ -600,7 +600,7 @@ func TestCRDSource_AddEventHandler_Delete(t *testing.T) {
 
 	require.Eventually(t, func() bool {
 		return counter.Load() == 1
-	}, time.Second, 10*time.Millisecond)
+	}, 2*time.Second, 10*time.Millisecond)
 }
 
 func TestCRDSource_Watch(t *testing.T) {
@@ -733,7 +733,7 @@ func helperCreateWatcherWithInformer(t *testing.T) (*cachetesting.FakeController
 
 	require.Eventually(t, func() bool {
 		return cache.WaitForCacheSync(ctx.Done(), informer.HasSynced)
-	}, time.Second, 10*time.Millisecond)
+	}, 2*time.Second, 10*time.Millisecond)
 
 	cs := &crdSource{
 		informer: informer,
diff --git a/source/endpoints_test.go b/source/endpoints_test.go
index 9470db68..0182646e 100644
--- a/source/endpoints_test.go
+++ b/source/endpoints_test.go
@@ -239,6 +239,55 @@ func TestEndpointTargetsFromServices(t *testing.T) {
 			selector:  map[string]string{"app": "nginx"},
 			expected:  endpoint.Targets{},
 		},
+		{
+			name: "multiple selectors",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "fake",
+						Namespace: "default",
+					},
+					Spec: corev1.ServiceSpec{
+						Selector:    map[string]string{"app": "apache", "version": "v1"},
+						ExternalIPs: []string{"158.123.32.23"},
+					},
+				},
+			},
+			namespace: "default",
+			selector:  map[string]string{"version": "v1"},
+			expected:  endpoint.Targets{"158.123.32.23"},
+		},
+		{
+			name: "complex selectors",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "fake",
+						Namespace: "default",
+					},
+					Spec: corev1.ServiceSpec{
+						Selector: map[string]string{
+							"app":     "demo",
+							"env":     "prod",
+							"team":    "devops",
+							"version": "v1",
+							"release": "stable",
+							"track":   "daily",
+							"tier":    "backend",
+						},
+						ExternalIPs: []string{"158.123.32.23"},
+					},
+				},
+			},
+			namespace: "default",
+			selector: map[string]string{
+				"version": "v1",
+				"release": "stable",
+				"tier":    "backend",
+				"app":     "demo",
+			},
+			expected: endpoint.Targets{"158.123.32.23"},
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/source/fake_test.go b/source/fake_test.go
index ecc1aa10..7ad35a6e 100644
--- a/source/fake_test.go
+++ b/source/fake_test.go
@@ -22,9 +22,14 @@ import (
 	"regexp"
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	"sigs.k8s.io/external-dns/endpoint"
 )
 
+// Validate that FakeSource is a source
+var _ Source = &fakeSource{}
+
 func generateTestEndpoints() []*endpoint.Endpoint {
 	sc, _ := NewFakeSource("")
 
@@ -69,5 +74,12 @@ func TestFakeEndpointsResolveToIPAddresses(t *testing.T) {
 	}
 }
 
-// Validate that FakeSource is a source
-var _ Source = &fakeSource{}
+func TestFakeSource_GenerateEndpoint_RefObject(t *testing.T) {
+	sc, _ := NewFakeSource("example.com")
+	fs := sc.(*fakeSource)
+
+	ep := fs.generateEndpoint()
+	require.NotNil(t, ep, "endpoint should not be nil")
+	require.NotNil(t, ep.RefObject())
+	require.Equal(t, "Pod", ep.RefObject().Kind)
+}
diff --git a/source/informers/transformers_test.go b/source/informers/transformers_test.go
new file mode 100644
index 00000000..b8166497
--- /dev/null
+++ b/source/informers/transformers_test.go
@@ -0,0 +1,176 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package informers
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	kubeinformers "k8s.io/client-go/informers"
+	"k8s.io/client-go/kubernetes/fake"
+)
+
+func TestTransformerWithOptions_Service(t *testing.T) {
+	base := fakeService()
+
+	tests := []struct {
+		name    string
+		options []func(*TransformOptions)
+		asserts func(any)
+	}{
+		{
+			name:    "minimalistic object",
+			options: nil,
+			asserts: func(obj any) {
+				svc, ok := obj.(*corev1.Service)
+				assert.True(t, ok)
+				assert.Empty(t, svc.UID)
+				assert.NotEmpty(t, svc.Name)
+				assert.NotEmpty(t, svc.Namespace)
+			},
+		},
+		{
+			name:    "with selector",
+			options: []func(*TransformOptions){TransformWithSpecSelector()},
+			asserts: func(obj any) {
+				svc, ok := obj.(*corev1.Service)
+				assert.True(t, ok)
+				assert.NotEmpty(t, svc.Spec.Selector)
+				assert.Empty(t, svc.Spec.ExternalIPs)
+				assert.Empty(t, svc.Status.LoadBalancer.Ingress)
+			},
+		},
+		{
+			name:    "with selector",
+			options: []func(*TransformOptions){TransformWithSpecSelector()},
+			asserts: func(obj any) {
+				svc, ok := obj.(*corev1.Service)
+				assert.True(t, ok)
+				assert.NotEmpty(t, svc.Spec.Selector)
+				assert.Empty(t, svc.Spec.ExternalIPs)
+				assert.Empty(t, svc.Status.LoadBalancer.Ingress)
+			},
+		},
+		{
+			name:    "with loadBalancer",
+			options: []func(*TransformOptions){TransformWithStatusLoadBalancer()},
+			asserts: func(obj any) {
+				svc, ok := obj.(*corev1.Service)
+				assert.True(t, ok)
+				assert.Empty(t, svc.Spec.Selector)
+				assert.Empty(t, svc.Spec.ExternalIPs)
+				assert.NotEmpty(t, svc.Status.LoadBalancer.Ingress)
+			},
+		},
+		{
+			name: "all options",
+			options: []func(*TransformOptions){
+				TransformWithSpecSelector(),
+				TransformWithSpecExternalIPs(),
+				TransformWithStatusLoadBalancer(),
+			},
+			asserts: func(obj any) {
+				svc, ok := obj.(*corev1.Service)
+				assert.True(t, ok)
+				assert.NotEmpty(t, svc.Spec.Selector)
+				assert.NotEmpty(t, svc.Spec.ExternalIPs)
+				assert.NotEmpty(t, svc.Status.LoadBalancer.Ingress)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			transform := TransformerWithOptions[*corev1.Service](tt.options...)
+			got, err := transform(base)
+			require.NoError(t, err)
+			tt.asserts(got)
+		})
+	}
+
+	t.Run("non-service input", func(t *testing.T) {
+		transform := TransformerWithOptions[*corev1.Service]()
+		out, err := transform("not-a-service")
+		if err != nil {
+			t.Fatalf("unexpected error: %v", err)
+		}
+		if out != nil {
+			t.Errorf("expected nil output for non-service input, got %v", out)
+		}
+	})
+}
+
+func TestTransformer_Service_WithFakeClient(t *testing.T) {
+	t.Run("with transformer", func(t *testing.T) {
+		ctx := t.Context()
+		svc := fakeService()
+		fakeClient := fake.NewClientset()
+
+		_, err := fakeClient.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{})
+		require.NoError(t, err)
+
+		factory := kubeinformers.NewSharedInformerFactoryWithOptions(fakeClient, 0, kubeinformers.WithNamespace(svc.Namespace))
+		serviceInformer := factory.Core().V1().Services()
+		err = serviceInformer.Informer().SetTransform(TransformerWithOptions[*corev1.Service](
+			TransformWithSpecSelector(),
+			TransformWithSpecExternalIPs(),
+			TransformWithStatusLoadBalancer(),
+		))
+		require.NoError(t, err)
+
+		factory.Start(ctx.Done())
+		err = WaitForCacheSync(ctx, factory)
+		require.NoError(t, err)
+
+		got, err := serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name)
+		require.NoError(t, err)
+
+		assert.Equal(t, svc.Spec.Selector, got.Spec.Selector)
+		assert.Equal(t, svc.Spec.ExternalIPs, got.Spec.ExternalIPs)
+		assert.Equal(t, svc.Status.LoadBalancer.Ingress, got.Status.LoadBalancer.Ingress)
+		assert.NotEqual(t, svc.Annotations, got.Annotations)
+		assert.NotEqual(t, svc.Labels, got.Labels)
+	})
+
+	t.Run("without transformer", func(t *testing.T) {
+		ctx := t.Context()
+		svc := fakeService()
+		fakeClient := fake.NewClientset()
+
+		_, err := fakeClient.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{})
+		require.NoError(t, err)
+
+		factory := kubeinformers.NewSharedInformerFactoryWithOptions(fakeClient, 0, kubeinformers.WithNamespace(svc.Namespace))
+		serviceInformer := factory.Core().V1().Services()
+
+		err = serviceInformer.Informer().GetIndexer().Add(svc)
+		require.NoError(t, err)
+
+		factory.Start(ctx.Done())
+		err = WaitForCacheSync(ctx, factory)
+		require.NoError(t, err)
+
+		got, err := serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name)
+		require.NoError(t, err)
+
+		assert.Equal(t, map[string]string{"app": "demo"}, got.Spec.Selector)
+		assert.Equal(t, []string{"1.2.3.4"}, got.Spec.ExternalIPs)
+		assert.Equal(t, svc.Status.LoadBalancer.Ingress, got.Status.LoadBalancer.Ingress)
+		assert.Equal(t, svc.Annotations, got.Annotations)
+		assert.Equal(t, svc.Labels, got.Labels)
+	})
+}
diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go
index 21f57009..50c02e18 100644
--- a/source/istio_gateway_test.go
+++ b/source/istio_gateway_test.go
@@ -19,17 +19,20 @@ package source
 import (
 	"context"
 	"errors"
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
-	networkingv1alpha3api "istio.io/api/networking/v1beta1"
-	networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1beta1"
+	istionetworking "istio.io/api/networking/v1beta1"
+	networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
 	istiofake "istio.io/client-go/pkg/clientset/versioned/fake"
 	v1 "k8s.io/api/core/v1"
 	networkv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/client-go/kubernetes/fake"
 
 	"sigs.k8s.io/external-dns/endpoint"
@@ -1477,7 +1480,8 @@ func testGatewayEndpoints(t *testing.T) {
 		t.Run(ti.title, func(t *testing.T) {
 			t.Parallel()
 
-			fakeKubernetesClient := fake.NewSimpleClientset()
+			fakeKubernetesClient := fake.NewClientset()
+			targetNamespace := ti.targetNamespace
 
 			for _, lb := range ti.lbServices {
 				service := lb.Service()
@@ -1487,6 +1491,9 @@ func testGatewayEndpoints(t *testing.T) {
 
 			for _, ing := range ti.ingresses {
 				ingress := ing.Ingress()
+				if ingress.Namespace != targetNamespace {
+					targetNamespace = v1.NamespaceAll
+				}
 				_, err := fakeKubernetesClient.NetworkingV1().Ingresses(ingress.Namespace).Create(context.Background(), ingress, metav1.CreateOptions{})
 				require.NoError(t, err)
 			}
@@ -1502,7 +1509,7 @@ func testGatewayEndpoints(t *testing.T) {
 				context.TODO(),
 				fakeKubernetesClient,
 				fakeIstioClient,
-				ti.targetNamespace,
+				targetNamespace,
 				ti.annotationFilter,
 				ti.fqdnTemplate,
 				ti.combineFQDNAndAnnotation,
@@ -1522,9 +1529,212 @@ func testGatewayEndpoints(t *testing.T) {
 	}
 }
 
+func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) {
+	tests := []struct {
+		name      string
+		selectors map[string]string
+		expected  []*endpoint.Endpoint
+	}{
+		{
+			name: "gw single selector match with single service selector",
+			selectors: map[string]string{
+				"version": "v1",
+			},
+			expected: []*endpoint.Endpoint{
+				endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "gateway/default/fake-gateway"),
+			},
+		},
+		{
+			name: "gw selector match all service selectors",
+			selectors: map[string]string{
+				"app":     "demo",
+				"env":     "prod",
+				"team":    "devops",
+				"version": "v1",
+				"release": "stable",
+				"track":   "daily",
+				"tier":    "backend",
+			},
+			expected: []*endpoint.Endpoint{
+				endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "gateway/default/fake-gateway"),
+			},
+		},
+		{
+			name: "gw selector has subset of service selectors",
+			selectors: map[string]string{
+				"version": "v1",
+				"release": "stable",
+				"tier":    "backend",
+				"app":     "demo",
+			},
+			expected: []*endpoint.Endpoint{
+				endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "gateway/default/fake-gateway"),
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeKubeClient := fake.NewClientset()
+			fakeIstioClient := istiofake.NewSimpleClientset()
+
+			svc := &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "fake-service",
+					Namespace: "default",
+					UID:       types.UID(fmt.Sprintf("fake-service-uid-%d", i)),
+				},
+				Spec: v1.ServiceSpec{
+					Selector: map[string]string{
+						"app":     "demo",
+						"env":     "prod",
+						"team":    "devops",
+						"version": "v1",
+						"release": "stable",
+						"track":   "daily",
+						"tier":    "backend",
+					},
+					ExternalIPs: []string{"10.10.10.255"},
+				},
+			}
+			_, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{})
+			require.NoError(t, err)
+
+			gw := &networkingv1beta1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "fake-gateway",
+					Namespace: "default",
+				},
+				Spec: istionetworking.Gateway{
+					Servers: []*istionetworking.Server{
+						{
+							Hosts: []string{"example.org"},
+						},
+					},
+					Selector: tt.selectors,
+				},
+			}
+
+			_, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(context.Background(), gw, metav1.CreateOptions{})
+			require.NoError(t, err)
+
+			src, err := NewIstioGatewaySource(
+				t.Context(),
+				fakeKubeClient,
+				fakeIstioClient,
+				"",
+				"",
+				"",
+				false,
+				false,
+			)
+			require.NoError(t, err)
+			require.NotNil(t, src)
+
+			res, err := src.Endpoints(t.Context())
+			require.NoError(t, err)
+
+			validateEndpoints(t, res, tt.expected)
+		})
+	}
+}
+
+func TestTransformerInIstioGatewaySource(t *testing.T) {
+	svc := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "fake-service",
+			Namespace: "default",
+			Labels: map[string]string{
+				"label1": "value1",
+				"label2": "value2",
+				"label3": "value3",
+			},
+			Annotations: map[string]string{
+				"user-annotation": "value",
+				"external-dns.alpha.kubernetes.io/hostname": "test-hostname",
+				"external-dns.alpha.kubernetes.io/random":   "value",
+				"other/annotation":                          "value",
+			},
+			UID: "someuid",
+		},
+		Spec: v1.ServiceSpec{
+			Selector: map[string]string{
+				"selector":  "one",
+				"selector2": "two",
+				"selector3": "three",
+			},
+			ExternalIPs: []string{"1.2.3.4"},
+			Ports: []v1.ServicePort{
+				{
+					Name:       "http",
+					Port:       80,
+					TargetPort: intstr.FromInt32(8080),
+					Protocol:   v1.ProtocolTCP,
+				},
+				{
+					Name:       "https",
+					Port:       443,
+					TargetPort: intstr.FromInt32(8443),
+					Protocol:   v1.ProtocolTCP,
+				},
+			},
+			Type: v1.ServiceTypeLoadBalancer,
+		},
+		Status: v1.ServiceStatus{
+			LoadBalancer: v1.LoadBalancerStatus{
+				Ingress: []v1.LoadBalancerIngress{
+					{IP: "5.6.7.8", Hostname: "lb.example.com"},
+				},
+			},
+			Conditions: []metav1.Condition{
+				{
+					Type:               "Available",
+					Status:             metav1.ConditionTrue,
+					Reason:             "MinimumReplicasAvailable",
+					Message:            "Service is available",
+					LastTransitionTime: metav1.Now(),
+				},
+			},
+		},
+	}
+
+	fakeClient := fake.NewClientset()
+
+	_, err := fakeClient.CoreV1().Services(svc.Namespace).Create(context.Background(), svc, metav1.CreateOptions{})
+	require.NoError(t, err)
+
+	src, err := NewIstioGatewaySource(
+		t.Context(),
+		fakeClient,
+		istiofake.NewSimpleClientset(),
+		"",
+		"",
+		"",
+		false,
+		false)
+	require.NoError(t, err)
+	gwSource, ok := src.(*gatewaySource)
+	require.True(t, ok)
+
+	rService, err := gwSource.serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name)
+	require.NoError(t, err)
+
+	assert.Equal(t, "fake-service", rService.Name)
+	assert.Empty(t, rService.Labels)
+	assert.Empty(t, rService.Annotations)
+	assert.Empty(t, rService.UID)
+	assert.NotEmpty(t, rService.Status.LoadBalancer)
+	assert.Empty(t, rService.Status.Conditions)
+	assert.Equal(t, map[string]string{
+		"selector":  "one",
+		"selector2": "two",
+		"selector3": "three",
+	}, rService.Spec.Selector)
+}
+
 // gateway specific helper functions
 func newTestGatewaySource(loadBalancerList []fakeIngressGatewayService, ingressList []fakeIngress) (*gatewaySource, error) {
-	fakeKubernetesClient := fake.NewSimpleClientset()
+	fakeKubernetesClient := fake.NewClientset()
 	fakeIstioClient := istiofake.NewSimpleClientset()
 
 	for _, lb := range loadBalancerList {
@@ -1612,22 +1822,22 @@ type fakeGatewayConfig struct {
 	selector    map[string]string
 }
 
-func (c fakeGatewayConfig) Config() *networkingv1alpha3.Gateway {
-	gw := &networkingv1alpha3.Gateway{
+func (c fakeGatewayConfig) Config() *networkingv1beta1.Gateway {
+	gw := &networkingv1beta1.Gateway{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        c.name,
 			Namespace:   c.namespace,
 			Annotations: c.annotations,
 		},
-		Spec: networkingv1alpha3api.Gateway{
+		Spec: istionetworking.Gateway{
 			Servers:  nil,
 			Selector: c.selector,
 		},
 	}
 
-	var servers []*networkingv1alpha3api.Server
+	var servers []*istionetworking.Server
 	for _, dnsnames := range c.dnsnames {
-		servers = append(servers, &networkingv1alpha3api.Server{
+		servers = append(servers, &istionetworking.Server{
 			Hosts: dnsnames,
 		})
 	}
diff --git a/source/istio_virtualservice_test.go b/source/istio_virtualservice_test.go
index c4c01ba7..198b87cc 100644
--- a/source/istio_virtualservice_test.go
+++ b/source/istio_virtualservice_test.go
@@ -19,6 +19,7 @@ package source
 import (
 	"context"
 	"errors"
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -31,6 +32,8 @@ import (
 	v1 "k8s.io/api/core/v1"
 	networkv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/client-go/kubernetes/fake"
 
 	"sigs.k8s.io/external-dns/endpoint"
@@ -49,7 +52,7 @@ type VirtualServiceSuite struct {
 }
 
 func (suite *VirtualServiceSuite) SetupTest() {
-	fakeKubernetesClient := fake.NewSimpleClientset()
+	fakeKubernetesClient := fake.NewClientset()
 	fakeIstioClient := istiofake.NewSimpleClientset()
 	var err error
 
@@ -189,7 +192,7 @@ func TestNewIstioVirtualServiceSource(t *testing.T) {
 
 			_, err := NewIstioVirtualServiceSource(
 				context.TODO(),
-				fake.NewSimpleClientset(),
+				fake.NewClientset(),
 				istiofake.NewSimpleClientset(),
 				"",
 				ti.annotationFilter,
@@ -2212,3 +2215,217 @@ func TestVirtualServiceSourceGetGateway(t *testing.T) {
 		})
 	}
 }
+
+func TestIstioVirtualServiceSource_GWServiceSelectorMatchServiceSelector(t *testing.T) {
+	tests := []struct {
+		name      string
+		selectors map[string]string
+		expected  []*endpoint.Endpoint
+	}{
+		{
+			name: "gw single selector match with single service selector",
+			selectors: map[string]string{
+				"version": "v1",
+			},
+			expected: []*endpoint.Endpoint{
+				endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"),
+			},
+		},
+		{
+			name: "gw selector match all service selectors",
+			selectors: map[string]string{
+				"app":     "demo",
+				"env":     "prod",
+				"team":    "devops",
+				"version": "v1",
+				"release": "stable",
+				"track":   "daily",
+				"tier":    "backend",
+			},
+			expected: []*endpoint.Endpoint{
+				endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"),
+			},
+		},
+		{
+			name: "gw selector has subset of service selectors",
+			selectors: map[string]string{
+				"version": "v1",
+				"release": "stable",
+				"tier":    "backend",
+				"app":     "demo",
+			},
+			expected: []*endpoint.Endpoint{
+				endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"),
+			},
+		},
+	}
+	for i, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeKubeClient := fake.NewClientset()
+			fakeIstioClient := istiofake.NewSimpleClientset()
+
+			svc := &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "fake-service",
+					Namespace: "default",
+					UID:       types.UID(fmt.Sprintf("fake-service-uid-%d", i)),
+				},
+				Spec: v1.ServiceSpec{
+					Selector: map[string]string{
+						"app":     "demo",
+						"env":     "prod",
+						"team":    "devops",
+						"version": "v1",
+						"release": "stable",
+						"track":   "daily",
+						"tier":    "backend",
+					},
+					ExternalIPs: []string{"10.10.10.255"},
+				},
+			}
+
+			_, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(context.Background(), svc, metav1.CreateOptions{})
+			require.NoError(t, err)
+
+			gw := &networkingv1beta1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "fake-gateway",
+					Namespace: "default",
+				},
+				Spec: istionetworking.Gateway{
+					Servers: []*istionetworking.Server{
+						{
+							Hosts: []string{"example.org"},
+						},
+					},
+					Selector: tt.selectors,
+				},
+			}
+
+			_, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(context.Background(), gw, metav1.CreateOptions{})
+			require.NoError(t, err)
+
+			gwService := &networkingv1beta1.VirtualService{
+				ObjectMeta: metav1.ObjectMeta{Name: "fake-vservice", Namespace: "default"},
+				Spec: istionetworking.VirtualService{
+					Gateways: []string{gw.Namespace + "/" + gw.Name},
+					Hosts:    []string{"example.org"},
+					ExportTo: []string{"*"},
+				},
+			}
+			_, err = fakeIstioClient.NetworkingV1beta1().VirtualServices(gwService.Namespace).Create(t.Context(), gwService, metav1.CreateOptions{})
+			require.NoError(t, err)
+
+			src, err := NewIstioVirtualServiceSource(
+				t.Context(),
+				fakeKubeClient,
+				fakeIstioClient,
+				"",
+				"",
+				"",
+				false,
+				false,
+			)
+			require.NoError(t, err)
+			require.NotNil(t, src)
+
+			res, err := src.Endpoints(t.Context())
+			require.NoError(t, err)
+
+			validateEndpoints(t, res, tt.expected)
+		})
+	}
+}
+
+func TestTransformerInIstioGatewayVirtualServiceSource(t *testing.T) {
+	svc := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "fake-service",
+			Namespace: "default",
+			Labels: map[string]string{
+				"label1": "value1",
+				"label2": "value2",
+				"label3": "value3",
+			},
+			Annotations: map[string]string{
+				"user-annotation": "value",
+				"external-dns.alpha.kubernetes.io/hostname": "test-hostname",
+				"external-dns.alpha.kubernetes.io/random":   "value",
+				"other/annotation":                          "value",
+			},
+			UID: "someuid",
+		},
+		Spec: v1.ServiceSpec{
+			Selector: map[string]string{
+				"selector":  "one",
+				"selector2": "two",
+				"selector3": "three",
+			},
+			ExternalIPs: []string{"1.2.3.4"},
+			Ports: []v1.ServicePort{
+				{
+					Name:       "http",
+					Port:       80,
+					TargetPort: intstr.FromInt32(8080),
+					Protocol:   v1.ProtocolTCP,
+				},
+				{
+					Name:       "https",
+					Port:       443,
+					TargetPort: intstr.FromInt32(8443),
+					Protocol:   v1.ProtocolTCP,
+				},
+			},
+			Type: v1.ServiceTypeLoadBalancer,
+		},
+		Status: v1.ServiceStatus{
+			LoadBalancer: v1.LoadBalancerStatus{
+				Ingress: []v1.LoadBalancerIngress{
+					{IP: "5.6.7.8", Hostname: "lb.example.com"},
+				},
+			},
+			Conditions: []metav1.Condition{
+				{
+					Type:               "Available",
+					Status:             metav1.ConditionTrue,
+					Reason:             "MinimumReplicasAvailable",
+					Message:            "Service is available",
+					LastTransitionTime: metav1.Now(),
+				},
+			},
+		},
+	}
+
+	fakeClient := fake.NewClientset()
+
+	_, err := fakeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{})
+	require.NoError(t, err)
+
+	src, err := NewIstioVirtualServiceSource(
+		t.Context(),
+		fakeClient,
+		istiofake.NewSimpleClientset(),
+		"",
+		"",
+		"",
+		false,
+		false)
+	require.NoError(t, err)
+	gwSource, ok := src.(*virtualServiceSource)
+	require.True(t, ok)
+
+	rService, err := gwSource.serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name)
+	require.NoError(t, err)
+
+	assert.Equal(t, svc.Name, rService.Name)
+	assert.Empty(t, rService.Labels)
+	assert.Empty(t, rService.Annotations)
+	assert.Empty(t, rService.UID)
+	assert.NotEmpty(t, rService.Status.LoadBalancer)
+	assert.Empty(t, rService.Status.Conditions)
+	assert.Equal(t, map[string]string{
+		"selector":  "one",
+		"selector2": "two",
+		"selector3": "three",
+	}, rService.Spec.Selector)
+}
diff --git a/source/pod_test.go b/source/pod_test.go
index e6e0f8b0..835f6a22 100644
--- a/source/pod_test.go
+++ b/source/pod_test.go
@@ -27,6 +27,7 @@ import (
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	corev1lister "k8s.io/client-go/listers/core/v1"
 	"k8s.io/client-go/tools/cache"
@@ -997,3 +998,147 @@ func nodesFixturesIPv4() []*corev1.Node {
 		},
 	}
 }
+
+func TestPodTransformerInPodSource(t *testing.T) {
+	t.Run("transformer set", func(t *testing.T) {
+		ctx := t.Context()
+		fakeClient := fake.NewClientset()
+
+		pod := &v1.Pod{
+			Spec: v1.PodSpec{
+				Containers: []v1.Container{{
+					Name: "test",
+				}},
+				Hostname:    "test-hostname",
+				NodeName:    "test-node",
+				HostNetwork: true,
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "test-ns",
+				Name:      "test-name",
+				Labels: map[string]string{
+					"label1": "value1",
+					"label2": "value2",
+					"label3": "value3",
+				},
+				Annotations: map[string]string{
+					"user-annotation": "value",
+					"external-dns.alpha.kubernetes.io/hostname": "test-hostname",
+					"external-dns.alpha.kubernetes.io/random":   "value",
+					"other/annotation":                          "value",
+				},
+				UID: "someuid",
+			},
+			Status: v1.PodStatus{
+				PodIP:  "127.0.0.1",
+				HostIP: "127.0.0.2",
+				Conditions: []v1.PodCondition{{
+					Type:   v1.PodReady,
+					Status: v1.ConditionTrue,
+				}, {
+					Type:   v1.ContainersReady,
+					Status: v1.ConditionFalse,
+				}},
+			},
+		}
+
+		_, err := fakeClient.CoreV1().Pods(pod.Namespace).Create(context.Background(), pod, metav1.CreateOptions{})
+		require.NoError(t, err)
+
+		// Should not error when creating the source
+		src, err := NewPodSource(ctx, fakeClient, "", "", false, "", "", false, "", nil)
+		require.NoError(t, err)
+		ps, ok := src.(*podSource)
+		require.True(t, ok)
+
+		retrieved, err := ps.podInformer.Lister().Pods("test-ns").Get("test-name")
+		require.NoError(t, err)
+
+		// Metadata
+		assert.Equal(t, "test-name", retrieved.Name)
+		assert.Equal(t, "test-ns", retrieved.Namespace)
+		assert.Empty(t, retrieved.UID)
+		assert.Empty(t, retrieved.Labels)
+		// Filtered
+		assert.Equal(t, map[string]string{
+			"user-annotation": "value",
+			"external-dns.alpha.kubernetes.io/hostname": "test-hostname",
+			"external-dns.alpha.kubernetes.io/random":   "value",
+			"other/annotation":                          "value",
+		}, retrieved.Annotations)
+
+		// Spec
+		assert.Empty(t, retrieved.Spec.Containers)
+		assert.Empty(t, retrieved.Spec.Hostname)
+		assert.Equal(t, "test-node", retrieved.Spec.NodeName)
+		assert.True(t, retrieved.Spec.HostNetwork)
+
+		// Status
+		assert.Empty(t, retrieved.Status.ContainerStatuses)
+		assert.Empty(t, retrieved.Status.InitContainerStatuses)
+		assert.Empty(t, retrieved.Status.HostIP)
+		assert.Equal(t, "127.0.0.1", retrieved.Status.PodIP)
+		assert.Empty(t, retrieved.Status.Conditions)
+	})
+
+	t.Run("transformer is not used when fqdnTemplate is set", func(t *testing.T) {
+		ctx := t.Context()
+		fakeClient := fake.NewClientset()
+
+		pod := &v1.Pod{
+			Spec: v1.PodSpec{
+				Containers: []v1.Container{{
+					Name: "test",
+				}},
+				Hostname:    "test-hostname",
+				NodeName:    "test-node",
+				HostNetwork: true,
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "test-ns",
+				Name:      "test-name",
+				Labels: map[string]string{
+					"label1": "value1",
+					"label2": "value2",
+					"label3": "value3",
+				},
+				Annotations: map[string]string{
+					"user-annotation": "value",
+					"external-dns.alpha.kubernetes.io/hostname": "test-hostname",
+					"external-dns.alpha.kubernetes.io/random":   "value",
+					"other/annotation":                          "value",
+				},
+				UID: "someuid",
+			},
+			Status: v1.PodStatus{
+				PodIP:  "127.0.0.1",
+				HostIP: "127.0.0.2",
+				Conditions: []v1.PodCondition{{
+					Type:   v1.PodReady,
+					Status: v1.ConditionTrue,
+				}, {
+					Type:   v1.ContainersReady,
+					Status: v1.ConditionFalse,
+				}},
+			},
+		}
+
+		_, err := fakeClient.CoreV1().Pods(pod.Namespace).Create(context.Background(), pod, metav1.CreateOptions{})
+		require.NoError(t, err)
+
+		// Should not error when creating the source
+		src, err := NewPodSource(ctx, fakeClient, "", "", false, "", "template", false, "", nil)
+		require.NoError(t, err)
+		ps, ok := src.(*podSource)
+		require.True(t, ok)
+
+		retrieved, err := ps.podInformer.Lister().Pods("test-ns").Get("test-name")
+		require.NoError(t, err)
+
+		// Metadata
+		assert.Equal(t, "test-name", retrieved.Name)
+		assert.Equal(t, "test-ns", retrieved.Namespace)
+		assert.NotEmpty(t, retrieved.UID)
+		assert.NotEmpty(t, retrieved.Labels)
+	})
+}
diff --git a/source/service_test.go b/source/service_test.go
index dd21331e..26e4320f 100644
--- a/source/service_test.go
+++ b/source/service_test.go
@@ -37,6 +37,7 @@ import (
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/client-go/kubernetes/fake"
+	"sigs.k8s.io/external-dns/source/informers"
 
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/internal/testutils"
@@ -251,7 +252,7 @@ func testServiceSourceEndpoints(t *testing.T) {
 			},
 			externalIPs:        []string{},
 			lbs:                []string{"1.2.3.4"},
-			serviceTypesFilter: []string{},
+			serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer)},
 			expected: []*endpoint.Endpoint{
 				{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
 			},
@@ -296,7 +297,7 @@ func testServiceSourceEndpoints(t *testing.T) {
 			annotations:        map[string]string{},
 			externalIPs:        []string{},
 			lbs:                []string{"1.2.3.4"},
-			serviceTypesFilter: []string{},
+			serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer), string(v1.ServiceTypeNodePort)},
 			expected: []*endpoint.Endpoint{
 				{DNSName: "foo.fqdn.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
 				{DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
@@ -2498,6 +2499,7 @@ func TestHeadlessServices(t *testing.T) {
 		podsReady                []bool
 		publishNotReadyAddresses bool
 		nodes                    []v1.Node
+		serviceTypesFilter       []string
 		expected                 []*endpoint.Endpoint
 		expectError              bool
 	}{
@@ -2528,6 +2530,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
 				{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}},
@@ -2562,6 +2565,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true},
 			false,
 			[]v1.Node{},
+			[]string{string(v1.ServiceTypeClusterIP), string(v1.ServiceTypeLoadBalancer)},
 			[]*endpoint.Endpoint{
 				{DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}},
 				{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}},
@@ -2596,6 +2600,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{},
 			false,
 		},
@@ -2627,6 +2632,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)},
 				{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)},
@@ -2662,6 +2668,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}, RecordTTL: endpoint.TTL(1)},
 				{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}, RecordTTL: endpoint.TTL(1)},
@@ -2696,6 +2703,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, false},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
@@ -2729,6 +2737,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, false},
 			true,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
 				{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}},
@@ -2763,6 +2772,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
 			},
@@ -2795,6 +2805,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
 			},
@@ -2827,6 +2838,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}},
 			},
@@ -2861,6 +2873,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true, true},
 			false,
 			[]v1.Node{},
+			[]string{string(v1.ServiceTypeClusterIP)},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
 			},
@@ -2895,6 +2908,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
 			},
@@ -2939,6 +2953,7 @@ func TestHeadlessServices(t *testing.T) {
 					},
 				},
 			},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
 			},
@@ -2987,6 +3002,7 @@ func TestHeadlessServices(t *testing.T) {
 					},
 				},
 			},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::5"}},
 			},
@@ -3031,6 +3047,7 @@ func TestHeadlessServices(t *testing.T) {
 					},
 				},
 			},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
 			},
@@ -3079,6 +3096,7 @@ func TestHeadlessServices(t *testing.T) {
 					},
 				},
 			},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
@@ -3113,6 +3131,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
 			},
@@ -3146,6 +3165,7 @@ func TestHeadlessServices(t *testing.T) {
 			[]bool{true, true, true},
 			false,
 			[]v1.Node{},
+			[]string{},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
 			},
@@ -3242,7 +3262,7 @@ func TestHeadlessServices(t *testing.T) {
 				true,
 				false,
 				false,
-				[]string{},
+				tc.serviceTypesFilter,
 				tc.ignoreHostnameAnnotation,
 				labels.Everything(),
 				false,
@@ -3994,7 +4014,6 @@ func TestHeadlessServicesHostIP(t *testing.T) {
 		t.Run(tc.title, func(t *testing.T) {
 			t.Parallel()
 
-			// Create a Kubernetes testing client
 			kubernetes := fake.NewClientset()
 
 			service := &v1.Service{
@@ -4134,7 +4153,7 @@ func TestExternalServices(t *testing.T) {
 			},
 			"111.111.111.111",
 			[]string{},
-			[]string{},
+			[]string{string(v1.ServiceTypeNodePort), string(v1.ServiceTypeExternalName)},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", Targets: endpoint.Targets{"111.111.111.111"}, RecordType: endpoint.RecordTypeA},
 			},
@@ -4176,7 +4195,7 @@ func TestExternalServices(t *testing.T) {
 			},
 			"remote.example.com",
 			[]string{},
-			[]string{},
+			[]string{string(v1.ServiceTypeExternalName)},
 			[]*endpoint.Endpoint{
 				{DNSName: "service.example.org", Targets: endpoint.Targets{"remote.example.com"}, RecordType: endpoint.RecordTypeCNAME},
 			},
@@ -4371,6 +4390,8 @@ func TestNewServiceSourceInformersEnabled(t *testing.T) {
 				assert.NotNil(t, svc.serviceTypeFilter)
 				assert.False(t, svc.serviceTypeFilter.enabled)
 				assert.NotNil(t, svc.nodeInformer)
+				assert.NotNil(t, svc.serviceInformer)
+				assert.NotNil(t, svc.endpointSlicesInformer)
 			},
 		},
 		{
@@ -4380,17 +4401,49 @@ func TestNewServiceSourceInformersEnabled(t *testing.T) {
 				assert.NotNil(t, svc)
 				assert.NotNil(t, svc.serviceTypeFilter)
 				assert.True(t, svc.serviceTypeFilter.enabled)
+				assert.NotNil(t, svc.serviceInformer)
 				assert.Nil(t, svc.nodeInformer)
+				assert.NotNil(t, svc.endpointSlicesInformer)
+				assert.NotNil(t, svc.podInformer)
 			},
 		},
 		{
-			name:      "serviceTypeFilter contains NodePort",
-			svcFilter: []string{string(v1.ServiceTypeNodePort)},
+			name:      "serviceTypeFilter contains NodePort and ExternalName",
+			svcFilter: []string{string(v1.ServiceTypeNodePort), string(v1.ServiceTypeExternalName)},
 			asserts: func(svc *serviceSource) {
 				assert.NotNil(t, svc)
 				assert.NotNil(t, svc.serviceTypeFilter)
 				assert.True(t, svc.serviceTypeFilter.enabled)
+				assert.NotNil(t, svc.serviceInformer)
 				assert.NotNil(t, svc.nodeInformer)
+				assert.NotNil(t, svc.endpointSlicesInformer)
+				assert.NotNil(t, svc.podInformer)
+			},
+		},
+		{
+			name:      "serviceTypeFilter contains ExternalName",
+			svcFilter: []string{string(v1.ServiceTypeExternalName)},
+			asserts: func(svc *serviceSource) {
+				assert.NotNil(t, svc)
+				assert.NotNil(t, svc.serviceTypeFilter)
+				assert.True(t, svc.serviceTypeFilter.enabled)
+				assert.NotNil(t, svc.serviceInformer)
+				assert.Nil(t, svc.nodeInformer)
+				assert.Nil(t, svc.endpointSlicesInformer)
+				assert.Nil(t, svc.podInformer)
+			},
+		},
+		{
+			name:      "serviceTypeFilter contains LoadBalancer",
+			svcFilter: []string{string(v1.ServiceTypeLoadBalancer)},
+			asserts: func(svc *serviceSource) {
+				assert.NotNil(t, svc)
+				assert.NotNil(t, svc.serviceTypeFilter)
+				assert.True(t, svc.serviceTypeFilter.enabled)
+				assert.NotNil(t, svc.serviceInformer)
+				assert.Nil(t, svc.nodeInformer)
+				assert.Nil(t, svc.endpointSlicesInformer)
+				assert.Nil(t, svc.podInformer)
 			},
 		},
 	}
@@ -4649,6 +4702,110 @@ func TestEndpointSlicesIndexer(t *testing.T) {
 		})
 }
 
+func TestPodTransformerInServiceSource(t *testing.T) {
+	ctx := t.Context()
+	fakeClient := fake.NewClientset()
+
+	pod := &v1.Pod{
+		Spec: v1.PodSpec{
+			Containers: []v1.Container{{
+				Name: "test",
+			}},
+			Hostname: "test-hostname",
+			NodeName: "test-node",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "test-ns",
+			Name:      "test-name",
+			Labels: map[string]string{
+				"label1": "value1",
+				"label2": "value2",
+				"label3": "value3",
+			},
+			Annotations: map[string]string{
+				"user-annotation": "value",
+				"external-dns.alpha.kubernetes.io/hostname": "test-hostname",
+				"external-dns.alpha.kubernetes.io/random":   "value",
+				"other/annotation":                          "value",
+			},
+			UID: "someuid",
+		},
+		Status: v1.PodStatus{
+			PodIP:  "127.0.0.1",
+			HostIP: "127.0.0.2",
+			Conditions: []v1.PodCondition{{
+				Type:   v1.PodReady,
+				Status: v1.ConditionTrue,
+			}, {
+				Type:   v1.ContainersReady,
+				Status: v1.ConditionFalse,
+			}},
+		},
+	}
+
+	_, err := fakeClient.CoreV1().Pods(pod.Namespace).Create(context.Background(), pod, metav1.CreateOptions{})
+	require.NoError(t, err)
+
+	// Should not error when creating the source
+	src, err := NewServiceSource(
+		ctx,
+		fakeClient,
+		"",
+		"",
+		"{{.Name}}",
+		false,
+		"",
+		false,
+		false,
+		false,
+		[]string{},
+		false,
+		labels.Everything(),
+		false,
+		false,
+		false,
+	)
+	require.NoError(t, err)
+	ss, ok := src.(*serviceSource)
+	require.True(t, ok)
+
+	retrieved, err := ss.podInformer.Lister().Pods("test-ns").Get("test-name")
+	require.NoError(t, err)
+
+	// Metadata
+	assert.Equal(t, "test-name", retrieved.Name)
+	assert.Equal(t, "test-ns", retrieved.Namespace)
+	assert.Empty(t, retrieved.UID)
+	assert.Equal(t, map[string]string{
+		"label1": "value1",
+		"label2": "value2",
+		"label3": "value3",
+	}, retrieved.Labels)
+	// Filtered
+	assert.Equal(t, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": "test-hostname",
+		"external-dns.alpha.kubernetes.io/random":   "value",
+	}, retrieved.Annotations)
+
+	// Spec
+	assert.Empty(t, retrieved.Spec.Containers)
+	assert.Equal(t, "test-hostname", retrieved.Spec.Hostname)
+	assert.Equal(t, "test-node", retrieved.Spec.NodeName)
+
+	// Status
+	assert.Empty(t, retrieved.Status.ContainerStatuses)
+	assert.Empty(t, retrieved.Status.InitContainerStatuses)
+	assert.Equal(t, "127.0.0.2", retrieved.Status.HostIP)
+	assert.Empty(t, retrieved.Status.PodIP)
+	assert.ElementsMatch(t, []v1.PodCondition{{
+		Type:   v1.PodReady,
+		Status: v1.ConditionTrue,
+	}, {
+		Type:   v1.ContainersReady,
+		Status: v1.ConditionFalse,
+	}}, retrieved.Status.Conditions)
+}
+
 // createTestServicesByType creates the requested number of services per type in the given namespace.
 func createTestServicesByType(namespace string, typeCounts map[v1.ServiceType]int) []*v1.Service {
 	var services []*v1.Service
@@ -4681,32 +4838,126 @@ func createTestServicesByType(namespace string, typeCounts map[v1.ServiceType]in
 
 func TestServiceTypes_isNodeInformerRequired(t *testing.T) {
 	tests := []struct {
-		name   string
-		filter []string
-		want   bool
+		name     string
+		filter   []string
+		required []v1.ServiceType
+		want     bool
 	}{
 		{
-			name:   "NodePort type present",
-			filter: []string{string(v1.ServiceTypeNodePort)},
-			want:   true,
+			name:     "NodePort required and filter is empty",
+			filter:   []string{},
+			required: []v1.ServiceType{v1.ServiceTypeNodePort},
+			want:     true,
 		},
 		{
-			name:   "NodePort type absent, filter enabled",
-			filter: []string{string(v1.ServiceTypeLoadBalancer)},
-			want:   false,
+			name:     "NodePort type present",
+			filter:   []string{string(v1.ServiceTypeNodePort)},
+			required: []v1.ServiceType{v1.ServiceTypeNodePort},
+			want:     true,
 		},
 		{
-			name:   "NodePort and other filters present",
-			filter: []string{string(v1.ServiceTypeLoadBalancer), string(v1.ServiceTypeNodePort)},
-			want:   true,
+			name:     "NodePort type absent, filter enabled",
+			filter:   []string{string(v1.ServiceTypeLoadBalancer)},
+			required: []v1.ServiceType{v1.ServiceTypeNodePort},
+			want:     false,
+		},
+		{
+			name:     "NodePort and other filters present",
+			filter:   []string{string(v1.ServiceTypeLoadBalancer), string(v1.ServiceTypeNodePort)},
+			required: []v1.ServiceType{v1.ServiceTypeNodePort},
+			want:     true,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			filter, _ := newServiceTypesFilter(tt.filter)
-			got := filter.isNodeInformerRequired()
+			got := filter.isRequired(tt.required...)
 			assert.Equal(t, tt.want, got)
 		})
 	}
 }
+
+func TestServiceSource_AddEventHandler(t *testing.T) {
+	var fakeServiceInformer *informers.FakeServiceInformer
+	var fakeEdpInformer *informers.FakeEndpointSliceInformer
+	var fakeNodeInformer *informers.FakeNodeInformer
+	tests := []struct {
+		name    string
+		filter  []string
+		times   int
+		asserts func(t *testing.T, s *serviceSource)
+	}{
+		{
+			name:   "AddEventHandler should trigger all event handlers when empty filter is provided",
+			filter: []string{},
+			times:  3,
+			asserts: func(t *testing.T, s *serviceSource) {
+				fakeServiceInformer.AssertNumberOfCalls(t, "Informer", 1)
+				fakeEdpInformer.AssertNumberOfCalls(t, "Informer", 1)
+				fakeNodeInformer.AssertNumberOfCalls(t, "Informer", 1)
+			},
+		},
+		{
+			name:   "AddEventHandler should trigger only service event handler",
+			filter: []string{string(v1.ServiceTypeExternalName), string(v1.ServiceTypeLoadBalancer)},
+			times:  1,
+			asserts: func(t *testing.T, s *serviceSource) {
+				fakeServiceInformer.AssertNumberOfCalls(t, "Informer", 1)
+				fakeEdpInformer.AssertNumberOfCalls(t, "Informer", 0)
+				fakeNodeInformer.AssertNumberOfCalls(t, "Informer", 0)
+			},
+		},
+		{
+			name:   "AddEventHandler should configure only service event handler",
+			filter: []string{string(v1.ServiceTypeExternalName), string(v1.ServiceTypeLoadBalancer), string(v1.ServiceTypeClusterIP)},
+			times:  2,
+			asserts: func(t *testing.T, s *serviceSource) {
+				fakeServiceInformer.AssertNumberOfCalls(t, "Informer", 1)
+				fakeEdpInformer.AssertNumberOfCalls(t, "Informer", 1)
+				fakeNodeInformer.AssertNumberOfCalls(t, "Informer", 0)
+			},
+		},
+		{
+			name:   "AddEventHandler should configure all service event handlers",
+			filter: []string{string(v1.ServiceTypeNodePort)},
+			times:  3,
+			asserts: func(t *testing.T, s *serviceSource) {
+				fakeServiceInformer.AssertNumberOfCalls(t, "Informer", 1)
+				fakeEdpInformer.AssertNumberOfCalls(t, "Informer", 1)
+				fakeNodeInformer.AssertNumberOfCalls(t, "Informer", 1)
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeServiceInformer = new(informers.FakeServiceInformer)
+			infSvc := testInformer{}
+			fakeServiceInformer.On("Informer").Return(&infSvc)
+
+			fakeEdpInformer = new(informers.FakeEndpointSliceInformer)
+			infEdp := testInformer{}
+			fakeEdpInformer.On("Informer").Return(&infEdp)
+
+			fakeNodeInformer = new(informers.FakeNodeInformer)
+			infNode := testInformer{}
+			fakeNodeInformer.On("Informer").Return(&infNode)
+
+			filter, _ := newServiceTypesFilter(tt.filter)
+
+			svcSource := &serviceSource{
+				endpointSlicesInformer: fakeEdpInformer,
+				serviceInformer:        fakeServiceInformer,
+				nodeInformer:           fakeNodeInformer,
+				serviceTypeFilter:      filter,
+				listenEndpointEvents:   true,
+			}
+
+			svcSource.AddEventHandler(t.Context(), func() {})
+
+			assert.Equal(t, tt.times, infSvc.times+infEdp.times+infNode.times)
+
+			tt.asserts(t, svcSource)
+		})
+	}
+}
diff --git a/source/store_test.go b/source/store_test.go
index 3b9c3849..77e29fa5 100644
--- a/source/store_test.go
+++ b/source/store_test.go
@@ -21,7 +21,7 @@ import (
 	"errors"
 	"testing"
 
-	cfclient "github.com/cloudfoundry-community/go-cfclient"
+	"github.com/cloudfoundry-community/go-cfclient"
 	openshift "github.com/openshift/client-go/route/clientset/versioned"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/suite"
@@ -34,6 +34,7 @@ import (
 	fakeDynamic "k8s.io/client-go/dynamic/fake"
 	"k8s.io/client-go/kubernetes"
 	fakeKube "k8s.io/client-go/kubernetes/fake"
+	"sigs.k8s.io/external-dns/source/types"
 	gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
 )
 
@@ -168,7 +169,10 @@ func (suite *ByNamesTestSuite) TestAllInitialized() {
 			}: "IngressRouteUDPList",
 		}), nil)
 
-	sources, err := ByNames(context.TODO(), mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-httpproxy", "kong-tcpingress", "f5-virtualserver", "f5-transportserver", "traefik-proxy", "fake"}, &Config{})
+	sources, err := ByNames(context.TODO(), mockClientGenerator, []string{
+		types.Service, types.Ingress, types.IstioGateway, types.ContourHTTPProxy,
+		types.KongTCPIngress, types.F5VirtualServer, types.F5TransportServer, types.TraefikProxy, types.Fake,
+	}, &Config{})
 	suite.NoError(err, "should not generate errors")
 	suite.Len(sources, 9, "should generate all nine sources")
 }
@@ -177,7 +181,7 @@ func (suite *ByNamesTestSuite) TestOnlyFake() {
 	mockClientGenerator := new(MockClientGenerator)
 	mockClientGenerator.On("KubeClient").Return(fakeKube.NewSimpleClientset(), nil)
 
-	sources, err := ByNames(context.TODO(), mockClientGenerator, []string{"fake"}, &Config{})
+	sources, err := ByNames(context.TODO(), mockClientGenerator, []string{types.Fake}, &Config{})
 	suite.NoError(err, "should not generate errors")
 	suite.Len(sources, 1, "should generate fake source")
 	suite.Nil(mockClientGenerator.kubeClient, "client should not be created")
@@ -197,9 +201,9 @@ func (suite *ByNamesTestSuite) TestKubeClientFails() {
 	mockClientGenerator.On("KubeClient").Return(nil, errors.New("foo"))
 
 	sourcesDependentOnKubeClient := []string{
-		"node", "service", "ingress", "pod", "istio-gateway", "istio-virtualservice",
-		"ambassador-host", "gloo-proxy", "traefik-proxy", "crd", "kong-tcpingress",
-		"f5-virtualserver", "f5-transportserver",
+		types.Node, types.Service, types.Ingress, types.Pod, types.IstioGateway, types.IstioVirtualService,
+		types.AmbassadorHost, types.GlooProxy, types.TraefikProxy, types.CRD, types.KongTCPIngress,
+		types.F5VirtualServer, types.F5TransportServer,
 	}
 
 	for _, source := range sourcesDependentOnKubeClient {
@@ -214,7 +218,7 @@ func (suite *ByNamesTestSuite) TestIstioClientFails() {
 	mockClientGenerator.On("IstioClient").Return(nil, errors.New("foo"))
 	mockClientGenerator.On("DynamicKubernetesClient").Return(nil, errors.New("foo"))
 
-	sourcesDependentOnIstioClient := []string{"istio-gateway", "istio-virtualservice"}
+	sourcesDependentOnIstioClient := []string{types.IstioGateway, types.IstioVirtualService}
 
 	for _, source := range sourcesDependentOnIstioClient {
 		_, err := ByNames(context.TODO(), mockClientGenerator, []string{source}, &Config{})
@@ -228,8 +232,10 @@ func (suite *ByNamesTestSuite) TestDynamicKubernetesClientFails() {
 	mockClientGenerator.On("IstioClient").Return(istiofake.NewSimpleClientset(), nil)
 	mockClientGenerator.On("DynamicKubernetesClient").Return(nil, errors.New("foo"))
 
-	sourcesDependentOnDynamicKubernetesClient := []string{"ambassador-host", "contour-httpproxy", "gloo-proxy", "traefik-proxy",
-		"kong-tcpingress", "f5-virtualserver", "f5-transportserver"}
+	sourcesDependentOnDynamicKubernetesClient := []string{
+		types.AmbassadorHost, types.ContourHTTPProxy, types.GlooProxy, types.TraefikProxy,
+		types.KongTCPIngress, types.F5VirtualServer, types.F5TransportServer,
+	}
 
 	for _, source := range sourcesDependentOnDynamicKubernetesClient {
 		_, err := ByNames(context.TODO(), mockClientGenerator, []string{source}, &Config{})
diff --git a/source/wrappers/dedupsource_test.go b/source/wrappers/dedupsource_test.go
index 07a13f40..3f9ac58b 100644
--- a/source/wrappers/dedupsource_test.go
+++ b/source/wrappers/dedupsource_test.go
@@ -144,3 +144,27 @@ func testDedupEndpoints(t *testing.T) {
 		})
 	}
 }
+
+func TestDedupSource_AddEventHandler(t *testing.T) {
+	tests := []struct {
+		title string
+		input []string
+		times int
+	}{
+		{
+			title: "should add event handler",
+			times: 1,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.title, func(t *testing.T) {
+			mockSource := testutils.NewMockSource()
+
+			src := NewDedupSource(mockSource)
+			src.AddEventHandler(t.Context(), func() {})
+
+			mockSource.AssertNumberOfCalls(t, "AddEventHandler", tt.times)
+		})
+	}
+}
diff --git a/source/wrappers/multisource_test.go b/source/wrappers/multisource_test.go
index 06626a08..ea45093f 100644
--- a/source/wrappers/multisource_test.go
+++ b/source/wrappers/multisource_test.go
@@ -269,3 +269,43 @@ func testMultiSourceEndpointsDefaultTargets(t *testing.T) {
 		src.AssertExpectations(t)
 	})
 }
+
+func TestMultiSource_AddEventHandler(t *testing.T) {
+	tests := []struct {
+		title   string
+		sources []source.Source
+		times   int
+	}{
+		{
+			title:   "should not add event handler when sources are empty",
+			sources: []source.Source{},
+			times:   0,
+		},
+		{
+			title: "should add event handler when sources not empty",
+			sources: []source.Source{
+				testutils.NewMockSource(),
+				testutils.NewMockSource(),
+				testutils.NewMockSource(),
+			},
+			times: 3,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.title, func(t *testing.T) {
+			src := NewMultiSource(tt.sources, []string{}, true)
+			src.AddEventHandler(t.Context(), func() {})
+
+			count := 0
+
+			for _, mockSource := range tt.sources {
+				mSource := mockSource.(*testutils.MockSource)
+				mSource.AssertNumberOfCalls(t, "AddEventHandler", 1)
+				count += 1
+			}
+
+			assert.Equal(t, tt.times, count)
+		})
+	}
+}
diff --git a/source/wrappers/nat64source_test.go b/source/wrappers/nat64source_test.go
index 3a0d15f1..2401d3f3 100644
--- a/source/wrappers/nat64source_test.go
+++ b/source/wrappers/nat64source_test.go
@@ -89,3 +89,33 @@ func testNat64Source(t *testing.T) {
 		})
 	}
 }
+
+func TestNat64Source_AddEventHandler(t *testing.T) {
+	tests := []struct {
+		title string
+		input []string
+		times int
+	}{
+		{
+			title: "should add event handler when prefixes are provided",
+			input: []string{"2001:DB8::/96"},
+			times: 1,
+		},
+		{
+			title: "should add event handler when prefixes not provided",
+			input: []string{},
+			times: 1,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.title, func(t *testing.T) {
+			mockSource := testutils.NewMockSource()
+
+			src := NewNAT64Source(mockSource, tt.input)
+			src.AddEventHandler(t.Context(), func() {})
+
+			mockSource.AssertNumberOfCalls(t, "AddEventHandler", tt.times)
+		})
+	}
+}
diff --git a/source/wrappers/targetfiltersource_test.go b/source/wrappers/targetfiltersource_test.go
index e0e01d74..7298c1e0 100644
--- a/source/wrappers/targetfiltersource_test.go
+++ b/source/wrappers/targetfiltersource_test.go
@@ -19,9 +19,10 @@ package wrappers
 import (
 	"testing"
 
-	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/net/context"
+
+	"sigs.k8s.io/external-dns/internal/testutils"
 	"sigs.k8s.io/external-dns/source"
 
 	"sigs.k8s.io/external-dns/endpoint"
@@ -47,26 +48,6 @@ func (m *mockTargetNetFilter) IsEnabled() bool {
 	return true
 }
 
-// echoSource is a Source that returns the endpoints passed in on creation.
-type echoSource struct {
-	mock.Mock
-	endpoints []*endpoint.Endpoint
-}
-
-func (e *echoSource) AddEventHandler(ctx context.Context, handler func()) {
-	e.Called(ctx)
-}
-
-// Endpoints returns all the endpoints passed in on creation
-func (e *echoSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
-	return e.endpoints, nil
-}
-
-// NewEchoSource creates a new echoSource.
-func NewEchoSource(endpoints []*endpoint.Endpoint) source.Source {
-	return &echoSource{endpoints: endpoints}
-}
-
 func TestEchoSourceReturnGivenSources(t *testing.T) {
 	startEndpoints := []*endpoint.Endpoint{{
 		DNSName:    "foo.bar.com",
@@ -75,7 +56,7 @@ func TestEchoSourceReturnGivenSources(t *testing.T) {
 		RecordTTL:  endpoint.TTL(300),
 		Labels:     endpoint.Labels{},
 	}}
-	e := NewEchoSource(startEndpoints)
+	e := testutils.NewMockSource(startEndpoints...)
 
 	endpoints, err := e.Endpoints(context.Background())
 	if err != nil {
@@ -143,7 +124,7 @@ func TestTargetFilterSourceEndpoints(t *testing.T) {
 		t.Run(tt.title, func(t *testing.T) {
 			t.Parallel()
 
-			echo := NewEchoSource(tt.endpoints)
+			echo := testutils.NewMockSource(tt.endpoints...)
 			src := NewTargetFilterSource(echo, tt.filters)
 
 			endpoints, err := src.Endpoints(context.Background())
@@ -225,7 +206,7 @@ func TestTargetFilterConcreteTargetFilter(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.title, func(t *testing.T) {
-			echo := NewEchoSource(tt.endpoints)
+			echo := testutils.NewMockSource(tt.endpoints...)
 			src := NewTargetFilterSource(echo, tt.filters)
 
 			endpoints, err := src.Endpoints(context.Background())
@@ -248,20 +229,16 @@ func TestTargetFilterSource_AddEventHandler(t *testing.T) {
 			times:   1,
 		},
 		{
-			title:   "should not add event handler if target filter is disabled",
+			title:   "should add event handler if target filter is disabled",
 			filters: endpoint.NewTargetNetFilterWithExclusions([]string{}, []string{}),
-			times:   0,
+			times:   1,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.title, func(t *testing.T) {
-			echo := NewEchoSource([]*endpoint.Endpoint{})
-
-			m := echo.(*echoSource)
-			m.On("AddEventHandler", t.Context()).Return()
-
-			src := NewTargetFilterSource(echo, tt.filters)
+			m := testutils.NewMockSource()
+			src := NewTargetFilterSource(m, tt.filters)
 			src.AddEventHandler(t.Context(), func() {})
 
 			m.AssertNumberOfCalls(t, "AddEventHandler", tt.times)