Repository: kubernetes-sigs/external-dns

Issue:
Remove entries which caused provider API request failure in a batch change requst.
<!-- Please only use this template for submitting enhancement requests. This can be something like a new provider or a new gateway.  -->

**What would you like to be added**:
I'm using Google provider and have an issue while updating DNS records via `external-dns`. After a few investigations, we found that there were some errors returned from `googleapi` and told us it failed to update the record.
`time="2023-01-11T06:29:22Z" level=error msg="googleapi: Error 409: The resource 'entity.change.additions[2]' named 'xxxxxxxxxxx. (TXT)' already exists, alreadyExists"`

However, changes are separated into batches before sending to the provider. If one entry in the batch is not valid, it'll cause the whole batch to fail to update the provider.

Here is the log we found on GCP that shows the request containing batch changes failed.

```
{
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "status": {
      "code": 6
    },
    "authenticationInfo": {
      "principalEmail": "sa@xxxxxx",
      "serviceAccountDelegationInfo": [
        {
          "principalSubject": "serviceAccount:sa@xxx"
        }
      ],
      "principalSubject": "serviceAccount:sa@xxx"
    },
    "requestMetadata": {
      "callerIp": "gce-internal-ip",
      "callerSuppliedUserAgent": "google-api-go-client/0.5,gzip(gfe),gzip(gfe)",
      "requestAttributes": {
        "time": "2023-01-11T06:20:10.361036Z",
        "auth": {}
      },
      "destinationAttributes": {}
    },
    "serviceName": "dns.googleapis.com",
    "methodName": "dns.changes.create",
    "authorizationInfo": [
      {
        "permission": "dns.resourceRecordSets.create",
        "granted": true,
        "resourceAttributes": {}
      }
    ],
    "resourceName": "managedZones/example-com",
    "request": {
      "@type": "type.googleapis.com/cloud.dns.api.ChangesCreateRequest",
      "managedZone": "example-com",
      "change": {
        "deletions": [..........],
        "additions": [..........]
      },
      "project": "dnszone"
    },
    "response": {
      "@type": "type.googleapis.com/cloud.dns.api.ChangesCreateResponse"
    }
  },
  "insertId": "ak3c2te8h1pw",
  "resource": {
    "type": "dns_managed_zone",
    "labels": {
      "zone_name": "example-com",
      "project_id": "dnszone",
      "location": "global"
    }
  },
  "timestamp": "2023-01-11T06:20:10.330139Z",
  "severity": "ERROR",
  "logName": "",
  "receiveTimestamp": "2023-01-11T06:20:11.371460049Z"
}
```

So I'm here to request a feature that `external-dns` could identify the entries that break the request and remove them in the next loop. Maybe have another loop to handle the invalid entries.

**Why is this needed**:
With this feature, an invalid record won't block the other records.


Discussion:
@amitai-devops: I have the same issue@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@awx-fuyuanchu: bump. any thoughts?@awx-fuyuanchu: /remove-lifecycle stale@szuecs: This works mean that we would have state, which is bad in general.
I think we do something in aws provider to work around the problem. I am not sure anymore but I think we split the change in half and try both and one will succeed and the other fail. Next iterations should fix the next quarter so we converge to a good state. People tried multiple times to fallback to single entry changes but this will quickly consume all api quotas and you end up being rate limited. I think we should do a binary search style apply in general.
I can review a pr, if someone creates a change.@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@lmoze-windscribe: /remove-lifecycle stale@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@lmoze-windscribe: /remove-lifecycle rotten@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@lmoze-windscribe: /remove-lifecycle rotten@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@lmoze-windscribe: /remove-lifecycle stale@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@lmoze-windscribe: /remove-lifecycle stale@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@dnraitzyk: Adding to this, if external-dns implemented per record logic to handle failures rather than retrying the entire batch (at least for GCP) it would go a long way to fix this.@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@lmoze-windscribe: /remove-lifecycle rotten@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Reopen this issue with `/reopen`
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/close not-planned

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-ci-robot: @k8s-triage-robot: Closing this issue, marking it as "Not Planned".

<details>

In response to [this](https://github.com/kubernetes-sigs/external-dns/issues/3307#issuecomment-3448731510):

>The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.
>
>This bot triages issues according to the following rules:
>- After 90d of inactivity, `lifecycle/stale` is applied
>- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
>- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed
>
>You can:
>- Reopen this issue with `/reopen`
>- Mark this issue as fresh with `/remove-lifecycle rotten`
>- Offer to help out with [Issue Triage][1]
>
>Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).
>
>/close not-planned
>
>[1]: https://www.kubernetes.dev/docs/guide/issue-triage/


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>

Solution patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9edbb7cc..b04a0f2f 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -25,7 +25,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: Set up Go 1.x
       uses: actions/setup-go@v5
@@ -34,7 +34,7 @@ jobs:
         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..c45fc4f8 100644
--- a/.github/workflows/codeql-analysis.yaml
+++ b/.github/workflows/codeql-analysis.yaml
@@ -26,7 +26,7 @@ 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
       with:
diff --git a/.github/workflows/dependency-update.yaml b/.github/workflows/dependency-update.yaml
index e24f540f..fecbb72c 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.5
+        uses: renovatebot/github-action@v43.0.9
         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..027ed867 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -18,7 +18,7 @@ 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
 
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..91245a4d 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
 
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index c89e3db8..8389cabf 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
@@ -48,13 +48,14 @@ 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@6416fd018ae38e60136775066eb3e98172143141 # v0.8.13
-      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
     # https://github.com/pre-commit/action
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..174a7df8 100644
--- a/.github/workflows/staging-image-tester.yaml
+++ b/.github/workflows/staging-image-tester.yaml
@@ -21,7 +21,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: Set up Go 1.x
       uses: actions/setup-go@v5
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 74c6c5cb..bbd0396d 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,7 @@ from the usage of any externally developed 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               |
@@ -162,13 +163,18 @@ 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))
+
+| 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
 
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..82076a3b
--- /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/flags.md b/docs/flags.md
index 59f92888..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) |
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/tutorials/azure.md b/docs/tutorials/azure.md
index 7e031d60..be23b117 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.
 
diff --git a/docs/tutorials/godaddy.md b/docs/tutorials/godaddy.md
index fad04717..66b36ebf 100644
--- a/docs/tutorials/godaddy.md
+++ b/docs/tutorials/godaddy.md
@@ -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
diff --git a/docs/tutorials/ovh.md b/docs/tutorials/ovh.md
index 170fc4fe..58e5f067 100644
--- a/docs/tutorials/ovh.md
+++ b/docs/tutorials/ovh.md
@@ -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
diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go
index 68f82378..dbeabcef 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 (
@@ -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 50708ad3..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.6
-	github.com/aws/aws-sdk-go-v2/config v1.29.18
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.71
-	github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.6
-	github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.1
-	github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1
-	github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.8
-	github.com/aws/aws-sdk-go-v2/service/sts v1.34.1
+	github.com/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.3
-	github.com/civo/civogo v0.6.2
+	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.160.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,17 +39,17 @@ 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.96.0
+	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.65.0
 	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34
@@ -58,77 +59,100 @@ require (
 	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.243.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.3 // 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.33 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect
+	github.com/aws/aws-sdk-go-v2/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.1 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.18 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
-	github.com/aws/smithy-go v1.22.4 // indirect
+	github.com/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.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,35 +164,65 @@ 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.4 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.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
+	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-20250715232539-7130f93afb79 // indirect
-	google.golang.org/grpc v1.73.0 // indirect
-	google.golang.org/protobuf v1.36.6 // 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 5c773b78..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.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
-cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
+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.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQGdU=
-github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
-github.com/aws/aws-sdk-go-v2/config v1.29.18 h1:x4T1GRPnqKV8HMJOMtNktbpQMl3bIsfx8KbqmveUO2I=
-github.com/aws/aws-sdk-go-v2/config v1.29.18/go.mod h1:bvz8oXugIsH8K7HLhBv06vDqnFv3NsGDt2Znpk7zmOU=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.71 h1:r2w4mQWnrTMJjOyIsZtGp3R3XGY3nqHn8C26C2lQWgA=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.71/go.mod h1:E7VF3acIup4GB5ckzbKFrCK0vTvEQxOxgdq4U3vcMCY=
-github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.6 h1:gBfrCR6IwAhmx+oCf9i9FJo1+Cxx5f0In+PaYQbkqbU=
-github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.6/go.mod h1:zAO6MqUum/2yfE/Ig1LPPtzCBudQtrGBaz1gcNzgAoY=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 h1:D9ixiWSG4lyUBL2DDNK924Px9V/NBVpML90MHqyTADY=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33/go.mod h1:caS/m4DI+cij2paz3rtProRBI4s/+TCiWoaWZuQ9010=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs=
+github.com/aws/aws-sdk-go-v2 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.1 h1:UoEWyfuQ/yNOuDENk5nn+AgNCH2Y5yzQEv6YbTyhIV8=
-github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.1/go.mod h1:K1I47BjiTRX00pBxfJLYK80QFRcf6blev2wbjgC5Cyc=
-github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.1 h1:WD2RDt93+IgNvlxEKkx/b3BQrpw5G/YpDHvGXweO5wE=
-github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.1/go.mod h1:8ZWruWnVWtJwjSHEtMWFcI1W6L6PD6i+uKCJ9EiJBbE=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
-github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.18 h1:QnGWwpTiazs1Y74RwA8VUfAtKuJQbnQ98DBFnSywj0s=
-github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.18/go.mod h1:gWOI6Vb0Bbmsi0Ejvtt3RkwKpdoa/SOYTVUlzqYPRLc=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 h1:vvbXsA2TVO80/KT7ZqCbx934dt6PY+vQ8hZpUZ/cpYg=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18/go.mod h1:m2JJHledjBGNMsLOF1g9gbAxprzq3KjC8e4lxtn+eWg=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 h1:R3nSX1hguRy6MnknHiepSvqnnL8ansFwK2hidPesAYU=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1/go.mod h1:fmSiB4OAghn85lQgk7XN9l9bpFg5Bm1v3HuaXKytPEw=
-github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.8 h1:PPQUm3zG6XzctspDTWC6vO3DvP/RZ+04RB11r98yb6E=
-github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.8/go.mod h1:C1n2zhotURaNj/BNgdPdhXh/i6V53rI3RmVEaNDakSM=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 h1:rGtWqkQbPk7Bkwuv3NzpE/scwwL9sC1Ul3tn9x83DUI=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.6/go.mod h1:u4ku9OLv4TO4bCPdxf4fA1upaMaJmP9ZijGk3AAOC6Q=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 h1:OV/pxyXh+eMA0TExHEC4jyWdumLxNbzz1P0zJoezkJc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4/go.mod h1:8Mm5VGYwtm+r305FfPSuc+aFkrypeylGYhFim6XEPoc=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 h1:aUrLQwJfZtwv3/ZNG2xRtEen+NqI3iesuacjP51Mv1s=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.1/go.mod h1:3wFBZKoWnX3r+Sm7in79i54fBmNfwhdNdQuscCw7QIk=
-github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
-github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/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,11 +192,16 @@ 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=
@@ -180,14 +215,14 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
 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.2 h1:tQegf+coNxIKhLjOo5bwAV04CPSk6ealSod55XHb7cw=
-github.com/civo/civogo v0.6.2/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.160.0 h1:3Wa6mOzv1m5DZQDANAk8u6v4DIUm5x2i4tZ7ke28lhs=
-github.com/digitalocean/godo v1.160.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=
@@ -509,6 +570,10 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC
 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.96.0 h1:ew0WavsB6N/I6etYCC160cD5qDXbek/1xZgujqTzork=
-github.com/oracle/oci-go-sdk/v65 v65.96.0/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=
@@ -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=
@@ -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.243.0 h1:sw+ESIJ4BVnlJcWu9S+p2Z6Qq1PjG77T8IJ1xtp4jZQ=
-google.golang.org/api v0.243.0/go.mod h1:GE4QtYfaybx1KmeHMdBnNnyLzBZCVihGBXAmJu/uUr8=
+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=
@@ -1369,8 +1552,8 @@ google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuO
 google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
 google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
 google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/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/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go
index c4df4e35..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...)
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 cf7db885..29962377 100644
--- a/plan/plan.go
+++ b/plan/plan.go
@@ -22,6 +22,7 @@ import (
 	"strings"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	log "github.com/sirupsen/logrus"
 
 	"sigs.k8s.io/external-dns/endpoint"
@@ -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
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/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 0b432b3f..144947a9 100644
--- a/provider/cloudflare/cloudflare.go
+++ b/provider/cloudflare/cloudflare.go
@@ -28,11 +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"
-	"github.com/cloudflare/cloudflare-go/v4/zones"
+	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"
 
@@ -75,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,
@@ -109,31 +110,31 @@ type cloudFlareDNS interface {
 	ZoneIDByName(zoneName string) (string, error)
 	ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone]
 	GetZone(ctx context.Context, zoneID string) (*zones.Zone, error)
-	ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error)
-	CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error)
-	DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error
-	UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error
+	ListDNSRecords(ctx context.Context, rc *cloudflarev0.ResourceContainer, rp cloudflarev0.ListDNSRecordsParams) ([]dns.RecordResponse, *cloudflarev0.ResultInfo, error)
+	CreateDNSRecord(ctx context.Context, params dns.RecordNewParams) (*dns.RecordResponse, error)
+	DeleteDNSRecord(ctx context.Context, rc *cloudflarev0.ResourceContainer, recordID string) 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) {
 	// Use v4 API to find zone by name
 	params := zones.ZoneListParams{
-		Name: cloudflarev4.F(zoneName),
+		Name: cloudflare.F(zoneName),
 	}
 
-	iter := z.serviceV4.Zones.ListAutoPaging(context.Background(), params)
+	iter := z.service.Zones.ListAutoPaging(context.Background(), params)
 	for zone := range autoPagerIterator(iter) {
 		if zone.Name == zoneName {
 			return zone.ID, nil
@@ -147,41 +148,46 @@ func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
 	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, rc *cloudflarev0.ResourceContainer, rp cloudflarev0.ListDNSRecordsParams) ([]dns.RecordResponse, *cloudflarev0.ResultInfo, error) {
+	records, info, err := z.serviceV0.ListDNSRecords(ctx, rc, rp)
+	convertedRecords := make([]dns.RecordResponse, 0, len(records))
+	for _, record := range records {
+		convertedRecords = append(convertedRecords, dnsRecordResponseFromLegacyDNSRecord(record))
+	}
+	return convertedRecords, info, err
 }
 
-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, rc *cloudflarev0.ResourceContainer, recordID string) error {
+	return z.serviceV0.DeleteDNSRecord(ctx, rc, recordID)
 }
 
 func (z zoneService) ListZones(ctx context.Context, params zones.ZoneListParams) autoPager[zones.Zone] {
-	return z.serviceV4.Zones.ListAutoPaging(ctx, params)
+	return z.service.Zones.ListAutoPaging(ctx, params)
 }
 
 func (z zoneService) GetZone(ctx context.Context, zoneID string) (*zones.Zone, error) {
-	return z.serviceV4.Zones.Get(ctx, zones.ZoneGetParams{ZoneID: cloudflarev4.F(zoneID)})
+	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 cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error) {
-	return z.service.CreateCustomHostname(ctx, zoneID, ch)
+func (z zoneService) CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflarev0.CustomHostname) (*cloudflarev0.CustomHostnameResponse, error) {
+	return z.serviceV0.CreateCustomHostname(ctx, zoneID, ch)
 }
 
 // listZonesV4Params returns the appropriate Zone List Params for v4 API
@@ -250,49 +256,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
@@ -320,8 +329,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") != "" {
@@ -333,13 +342,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")),
 		)
@@ -534,7 +543,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 {
@@ -561,7 +570,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
@@ -577,7 +586,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 {
@@ -617,7 +626,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{
@@ -673,8 +682,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)
@@ -769,25 +778,25 @@ func (p *CloudFlareProvider) changesByZone(zones []zones.Zone, changeSet []*clou
 	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),
@@ -795,15 +804,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)
@@ -824,26 +833,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,
@@ -854,18 +861,18 @@ 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) {
 	// for faster getRecordID lookup
 	records := make(DNSRecordsMap)
-	resultInfo := cloudflare.ResultInfo{PerPage: p.DNSRecordsConfig.PerPage, Page: 1}
-	params := cloudflare.ListDNSRecordsParams{ResultInfo: resultInfo}
+	resultInfo := cloudflarev0.ResultInfo{PerPage: p.DNSRecordsConfig.PerPage, Page: 1}
+	params := cloudflarev0.ListDNSRecordsParams{ResultInfo: resultInfo}
 	for {
-		pageRecords, resultInfo, err := p.Client.ListDNSRecords(ctx, cloudflare.ZoneIdentifier(zoneID), params)
+		pageRecords, resultInfo, err := p.Client.ListDNSRecords(ctx, cloudflarev0.ZoneIdentifier(zoneID), params)
 		if err != nil {
 			return nil, convertCloudflareError(err)
 		}
@@ -881,7 +888,7 @@ func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Contex
 	return records, nil
 }
 
-func newCustomHostnameIndex(ch cloudflare.CustomHostname) CustomHostnameIndex {
+func newCustomHostnameIndex(ch cloudflarev0.CustomHostname) CustomHostnameIndex {
 	return CustomHostnameIndex{Hostname: ch.Hostname}
 }
 
@@ -891,9 +898,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) {
@@ -912,12 +919,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,
 		},
 	}
@@ -964,16 +971,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)
@@ -994,20 +1001,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
 		}
@@ -1036,3 +1040,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..9a78851d 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),
 	}
 }
 
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/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
index aed7b3ae..2eb86e32 100644
--- a/source/informers/fake.go
+++ b/source/informers/fake.go
@@ -15,6 +15,9 @@ 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"
@@ -58,3 +61,50 @@ func (f *FakeNodeInformer) Informer() 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 a2a8f8f7..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"
@@ -134,6 +135,48 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name
 		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 {
+				return nil, fmt.Errorf("object is not a pod")
+			}
+			if pod.UID == "" {
+				// Pod was already transformed and we must be idempotent.
+				return pod, nil
+			}
+
+			// 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
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/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/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..28ec48f3 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"
@@ -103,14 +103,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 +130,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 +151,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 +185,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 +206,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 +228,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 +241,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 +281,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 +309,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 +829,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 +844,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 +879,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 +902,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 +944,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 +974,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 +1008,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 +1016,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 +1044,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 +1060,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 +1083,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 +1125,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 +1157,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 +1165,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{
diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go
index ccc3d005..fbe7ede5 100644
--- a/provider/cloudflare/cloudflare_test.go
+++ b/provider/cloudflare/cloudflare_test.go
@@ -25,9 +25,11 @@ import (
 	"sort"
 	"strings"
 	"testing"
+	"time"
 
-	"github.com/cloudflare/cloudflare-go"
-	"github.com/cloudflare/cloudflare-go/v4/zones"
+	cloudflarev0 "github.com/cloudflare/cloudflare-go"
+	"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"
@@ -40,39 +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
+	Records           map[string]map[string]dns.RecordResponse
 	Actions           []MockAction
 	listZonesError    error // For v4 ListZones
 	getZoneError      error // For v4 GetZone
 	dnsRecordsError   error
-	customHostnames   map[string][]cloudflare.CustomHostname
+	customHostnames   map[string][]cloudflarev0.CustomHostname
 	regionalHostnames map[string][]regionalHostname
 }
 
-var ExampleDomain = []cloudflare.DNSRecord{
+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",
 	},
 	{
@@ -81,7 +77,7 @@ var ExampleDomain = []cloudflare.DNSRecord{
 		Type:    endpoint.RecordTypeA,
 		TTL:     120,
 		Content: "3.4.5.6",
-		Proxied: proxyDisabled,
+		Proxied: false,
 	},
 	{
 		ID:      "1231231233",
@@ -89,7 +85,7 @@ var ExampleDomain = []cloudflare.DNSRecord{
 		Type:    endpoint.RecordTypeA,
 		TTL:     1,
 		Content: "2.3.4.5",
-		Proxied: proxyDisabled,
+		Proxied: false,
 	},
 }
 
@@ -99,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 {
@@ -122,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{}
 	}
 }
 
@@ -159,53 +142,61 @@ 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, rc *cloudflarev0.ResourceContainer, rp cloudflarev0.ListDNSRecordsParams) ([]dns.RecordResponse, *cloudflarev0.ResultInfo, error) {
 	if m.dnsRecordsError != nil {
-		return nil, &cloudflare.ResultInfo{}, m.dnsRecordsError
+		return nil, &cloudflarev0.ResultInfo{}, m.dnsRecordsError
 	}
-	result := []cloudflare.DNSRecord{}
+	result := []dns.RecordResponse{}
 	if zone, ok := m.Records[rc.Identifier]; 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")
+				return nil, &cloudflarev0.ResultInfo{}, errors.New("failed to list erroring DNS record")
 			}
 			result = append(result, record)
 		}
 	}
 
 	if len(result) == 0 || rp.PerPage == 0 {
-		return result, &cloudflare.ResultInfo{Page: 1, TotalPages: 1, Count: 0, Total: 0}, nil
+		return result, &cloudflarev0.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
+		return result, &cloudflarev0.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{}
+	chunks := [][]dns.RecordResponse{}
 
 	// 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 })
@@ -216,7 +207,7 @@ func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, rc *cloudflar
 
 	// return the requested page
 	partialResult := chunks[rp.Page-1]
-	return partialResult, &cloudflare.ResultInfo{
+	return partialResult, &cloudflarev0.ResultInfo{
 		PerPage:    rp.PerPage,
 		Page:       rp.Page,
 		TotalPages: len(chunks),
@@ -225,7 +216,7 @@ func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, rc *cloudflar
 	}, nil
 }
 
-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",
@@ -244,7 +235,7 @@ 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, rc *cloudflarev0.ResourceContainer, recordID string) error {
 	m.Actions = append(m.Actions, MockAction{
 		Name:     "Delete",
 		ZoneId:   rc.Identifier,
@@ -263,34 +254,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),
@@ -299,7 +290,7 @@ func (m *mockCloudFlareClient) CustomHostnames(ctx context.Context, zoneID strin
 			}, err
 	} else {
 		return result,
-			cloudflare.ResultInfo{
+			cloudflarev0.ResultInfo{
 				Page:       page,
 				PerPage:    perPage,
 				Count:      0,
@@ -309,17 +300,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 {
@@ -393,7 +384,7 @@ func (m *mockCloudFlareClient) GetZone(ctx context.Context, zoneID string) (*zon
 	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
@@ -462,26 +453,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,
 			},
 		},
 	},
@@ -503,26 +494,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,
 			},
 		},
 	},
@@ -544,28 +535,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,
 			},
 		},
 	},
@@ -587,13 +578,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,
 			},
 		},
 	},
@@ -616,13 +607,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,
 			},
 		},
 	},
@@ -644,13 +635,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,
 			},
 		},
 	},
@@ -678,13 +669,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,
 			},
 		},
 	},
@@ -712,13 +703,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,
 			},
 		},
 	},
@@ -746,13 +737,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,
 			},
 		},
 	},
@@ -761,33 +752,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"
@@ -807,9 +796,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,
@@ -885,10 +874,10 @@ func TestCloudFlareZonesWithIDFilter(t *testing.T) {
 func TestCloudflareListZonesRateLimited(t *testing.T) {
 	// Create a mock client that returns a rate limit error
 	client := NewMockCloudFlareClient()
-	client.listZonesError = &cloudflare.Error{
+	client.listZonesError = &cloudflarev0.Error{
 		StatusCode: 429,
 		ErrorCodes: []int{10000},
-		Type:       cloudflare.ErrorTypeRateLimit,
+		Type:       cloudflarev0.ErrorTypeRateLimit,
 	}
 	p := &CloudFlareProvider{Client: client}
 
@@ -917,10 +906,10 @@ func TestCloudflareListZonesRateLimitedStringError(t *testing.T) {
 func TestCloudflareListZoneInternalErrors(t *testing.T) {
 	// Create a mock client that returns a internal server error
 	client := NewMockCloudFlareClient()
-	client.listZonesError = &cloudflare.Error{
+	client.listZonesError = &cloudflarev0.Error{
 		StatusCode: 500,
 		ErrorCodes: []int{20000},
-		Type:       cloudflare.ErrorTypeService,
+		Type:       cloudflarev0.ErrorTypeService,
 	}
 	p := &CloudFlareProvider{Client: client}
 
@@ -935,7 +924,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,
 	})
 
@@ -957,10 +946,10 @@ func TestCloudflareRecords(t *testing.T) {
 		t.Errorf("expected to fail")
 	}
 	client.dnsRecordsError = nil
-	client.listZonesError = &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
@@ -968,10 +957,10 @@ func TestCloudflareRecords(t *testing.T) {
 		t.Error("expected a rate limit error")
 	}
 
-	client.listZonesError = &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
@@ -1108,24 +1097,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,
 			},
 		},
 	})
@@ -1204,29 +1193,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",
@@ -1241,23 +1230,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{
@@ -1278,20 +1267,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{
@@ -1312,34 +1301,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{
@@ -1373,27 +1362,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{
@@ -1427,27 +1416,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{
@@ -1486,7 +1475,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",
@@ -1494,7 +1483,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",
@@ -1502,7 +1491,7 @@ func TestGroupByNameAndTypeWithCustomHostnames_MX(t *testing.T) {
 				Type:     endpoint.RecordTypeMX,
 				TTL:      3600,
 				Content:  "mail2.bar.com",
-				Priority: cloudflare.Uint16Ptr(20),
+				Priority: 20,
 			},
 		},
 	})
@@ -1527,38 +1516,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",
@@ -1625,7 +1614,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,
 	})
 
@@ -1680,33 +1669,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",
@@ -1714,7 +1703,7 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
 				Type:    endpoint.RecordTypeA,
 				TTL:     1,
 				Content: "1.2.3.4",
-				Proxied: proxyEnabled,
+				Proxied: true,
 			},
 		},
 	})
@@ -1924,7 +1913,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",
@@ -1932,7 +1921,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",
@@ -1951,7 +1940,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",
@@ -1963,7 +1952,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",
@@ -1985,7 +1974,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",
@@ -1993,7 +1982,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",
@@ -2014,7 +2003,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",
@@ -2026,7 +2015,7 @@ func TestCloudFlareProvider_submitChangesApex(t *testing.T) {
 		},
 		{
 			Action: cloudFlareUpdate,
-			ResourceRecord: cloudflare.DNSRecord{
+			ResourceRecord: dns.RecordResponse{
 				Name:    "@", // APEX record
 				Type:    endpoint.RecordTypeTXT,
 				ID:      "9876543210",
@@ -2051,8 +2040,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,
@@ -2069,7 +2058,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",
@@ -2497,7 +2486,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,
@@ -2505,7 +2494,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",
@@ -2808,15 +2797,15 @@ func TestCloudflareZoneChanges(t *testing.T) {
 	changes := []*cloudFlareChange{
 		{
 			Action:         cloudFlareCreate,
-			ResourceRecord: cloudflare.DNSRecord{Name: "test1.foo.com", Type: "A", Content: "1.2.3.4"},
+			ResourceRecord: dns.RecordResponse{Name: "test1.foo.com", Type: "A", Content: "1.2.3.4"},
 		},
 		{
 			Action:         cloudFlareCreate,
-			ResourceRecord: cloudflare.DNSRecord{Name: "test2.foo.com", Type: "A", Content: "1.2.3.5"},
+			ResourceRecord: dns.RecordResponse{Name: "test2.foo.com", Type: "A", Content: "1.2.3.5"},
 		},
 		{
 			Action:         cloudFlareCreate,
-			ResourceRecord: cloudflare.DNSRecord{Name: "test1.bar.com", Type: "A", Content: "1.2.3.6"},
+			ResourceRecord: dns.RecordResponse{Name: "test1.bar.com", Type: "A", Content: "1.2.3.6"},
 		},
 	}
 
@@ -2937,19 +2926,19 @@ func TestCloudflareChangesByZone(t *testing.T) {
 	changes := []*cloudFlareChange{
 		{
 			Action:         cloudFlareCreate,
-			ResourceRecord: cloudflare.DNSRecord{Name: "api.foo.com", Type: "A", Content: "1.2.3.4"},
+			ResourceRecord: dns.RecordResponse{Name: "api.foo.com", Type: "A", Content: "1.2.3.4"},
 		},
 		{
 			Action:         cloudFlareUpdate,
-			ResourceRecord: cloudflare.DNSRecord{Name: "www.foo.com", Type: "CNAME", Content: "foo.com"},
+			ResourceRecord: dns.RecordResponse{Name: "www.foo.com", Type: "CNAME", Content: "foo.com"},
 		},
 		{
 			Action:         cloudFlareCreate,
-			ResourceRecord: cloudflare.DNSRecord{Name: "mail.bar.com", Type: "MX", Content: "10 mail.bar.com"},
+			ResourceRecord: dns.RecordResponse{Name: "mail.bar.com", Type: "MX", Content: "10 mail.bar.com"},
 		},
 		{
 			Action:         cloudFlareDelete,
-			ResourceRecord: cloudflare.DNSRecord{Name: "old.bar.com", Type: "A", Content: "5.6.7.8"},
+			ResourceRecord: dns.RecordResponse{Name: "old.bar.com", Type: "A", Content: "5.6.7.8"},
 		},
 	}
 
@@ -2978,31 +2967,31 @@ func TestConvertCloudflareError(t *testing.T) {
 	}{
 		{
 			name:            "Rate limit error via Error type",
-			inputError:      &cloudflare.Error{StatusCode: 429, Type: cloudflare.ErrorTypeRateLimit},
+			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:      &cloudflare.Error{StatusCode: 429, ErrorCodes: []int{10000}, Type: cloudflare.ErrorTypeRateLimit}, // Complete rate limit error
+			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:      &cloudflare.Error{StatusCode: 500},
+			inputError:      &cloudflarev0.Error{StatusCode: 500},
 			expectSoftError: true,
 			description:     "Server error (500+) should be converted to soft error",
 		},
 		{
 			name:            "Server error 502",
-			inputError:      &cloudflare.Error{StatusCode: 502},
+			inputError:      &cloudflarev0.Error{StatusCode: 502},
 			expectSoftError: true,
 			description:     "Server error (502) should be converted to soft error",
 		},
 		{
 			name:            "Server error 503",
-			inputError:      &cloudflare.Error{StatusCode: 503},
+			inputError:      &cloudflarev0.Error{StatusCode: 503},
 			expectSoftError: true,
 			description:     "Server error (503) should be converted to soft error",
 		},
@@ -3020,19 +3009,19 @@ func TestConvertCloudflareError(t *testing.T) {
 		},
 		{
 			name:            "Client error 400",
-			inputError:      &cloudflare.Error{StatusCode: 400},
+			inputError:      &cloudflarev0.Error{StatusCode: 400},
 			expectSoftError: false,
 			description:     "Client error (400) should not be converted to soft error",
 		},
 		{
 			name:            "Client error 401",
-			inputError:      &cloudflare.Error{StatusCode: 401},
+			inputError:      &cloudflarev0.Error{StatusCode: 401},
 			expectSoftError: false,
 			description:     "Client error (401) should not be converted to soft error",
 		},
 		{
 			name:            "Client error 404",
-			inputError:      &cloudflare.Error{StatusCode: 404},
+			inputError:      &cloudflarev0.Error{StatusCode: 404},
 			expectSoftError: false,
 			description:     "Client error (404) should not be converted to soft error",
 		},
@@ -3083,7 +3072,7 @@ func TestConvertCloudflareErrorInContext(t *testing.T) {
 			name: "Zones with GetZone rate limit error",
 			setupMock: func(client *mockCloudFlareClient) {
 				client.Zones = map[string]string{"zone1": "example.com"}
-				client.getZoneError = &cloudflare.Error{StatusCode: 429, Type: cloudflare.ErrorTypeRateLimit}
+				client.getZoneError = &cloudflarev0.Error{StatusCode: 429, Type: cloudflarev0.ErrorTypeRateLimit}
 			},
 			function: func(p *CloudFlareProvider) error {
 				p.zoneIDFilter.ZoneIDs = []string{"zone1"}
@@ -3097,7 +3086,7 @@ func TestConvertCloudflareErrorInContext(t *testing.T) {
 			name: "Zones with GetZone server error",
 			setupMock: func(client *mockCloudFlareClient) {
 				client.Zones = map[string]string{"zone1": "example.com"}
-				client.getZoneError = &cloudflare.Error{StatusCode: 500}
+				client.getZoneError = &cloudflarev0.Error{StatusCode: 500}
 			},
 			function: func(p *CloudFlareProvider) error {
 				p.zoneIDFilter.ZoneIDs = []string{"zone1"}
@@ -3111,7 +3100,7 @@ func TestConvertCloudflareErrorInContext(t *testing.T) {
 			name: "Zones with GetZone client error",
 			setupMock: func(client *mockCloudFlareClient) {
 				client.Zones = map[string]string{"zone1": "example.com"}
-				client.getZoneError = &cloudflare.Error{StatusCode: 404}
+				client.getZoneError = &cloudflarev0.Error{StatusCode: 404}
 			},
 			function: func(p *CloudFlareProvider) error {
 				p.zoneIDFilter.ZoneIDs = []string{"zone1"}
@@ -3136,7 +3125,7 @@ func TestConvertCloudflareErrorInContext(t *testing.T) {
 		{
 			name: "Zones with ListZones server error",
 			setupMock: func(client *mockCloudFlareClient) {
-				client.listZonesError = &cloudflare.Error{StatusCode: 503}
+				client.listZonesError = &cloudflarev0.Error{StatusCode: 503}
 			},
 			function: func(p *CloudFlareProvider) error {
 				_, err := p.Zones(context.Background())
@@ -3254,3 +3243,106 @@ func TestZoneIDByNameZoneNotFound(t *testing.T) {
 	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)
+		})
+	}
+}
diff --git a/provider/zonefinder_test.go b/provider/zonefinder_test.go
index 4eddd36c..b02a0388 100644
--- a/provider/zonefinder_test.go
+++ b/provider/zonefinder_test.go
@@ -34,6 +34,7 @@ func TestZoneIDName(t *testing.T) {
 	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",
@@ -42,6 +43,7 @@ func TestZoneIDName(t *testing.T) {
 		"123123":  "_metadata.example.com",
 		"1231231": "_foo._metadata.example.com",
 		"456456":  "_metadata.エイミー.みんな",
+		"123412":  "*.example.com",
 	}, z)
 
 	// simple entry in a domain
@@ -83,8 +85,12 @@ func TestZoneIDName(t *testing.T) {
 	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/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 07df803b..26e4320f 100644
--- a/source/service_test.go
+++ b/source/service_test.go
@@ -4702,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
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)