Repository: kubernetes-sigs/external-dns

Issue:
RFC2136 provider broken after release 0.16.0 or 0.16.1
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!

-->

**What happened**:

After upgrading [external-dns Helm chart](https://github.com/bitnami/charts/tree/main/bitnami/external-dns) from Bitnami from v8.7.7 to v8.7.8, ExternalDNS can no longuer create or remove DNS records in our Active Directory.
This upgrade bumps the version of ExternalDNS from v0.15.0 to v0.16.1.

I already opened [a PR](https://github.com/bitnami/charts/issues/32644) on their repository but to them the issue seems to be in ExternalDNS itself.

**What you expected to happen**:

DNS entries are correctly created/remove in/from Active Directory.

**How to reproduce it (as minimally and precisely as possible)**:

Deploy the [external-dns Helm chart](https://github.com/bitnami/charts/tree/main/bitnami/external-dns) from Bitnami using this `values.yaml` file :

```
domainFilters:
  - example.com
policy: sync
provider: rfc2136
rfc2136:
  host: dc01.example.com
  kerberosConfig: |
    [logging]
        default = FILE:/var/log/krb5libs.log
        kdc = FILE:/var/log/krb5kdc.log
        admin_server = FILE:/var/log/kadmind.log

    [libdefaults]
        dns_lookup_realm = false
        ticket_lifetime = 24h
        renew_lifetime = 7d
        forwardable = true
        rdns = false
        pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt
        default_ccache_name = KEYRING:persistent:%{uid}

        default_realm = EXAMPLE.COM

    [realms]
        EXAMPLE.COM = {
          kdc = dc01.example.com
          admin_server = dc01.example.com
        }

    [domain_realm]
        example.com = EXAMPLE.COM
        .example.com = EXAMPLE.COM
  kerberosRealm: example.com
  rfc3645Enabled: true
  secretName: external-dns-kerberos-credentials
  tsigAxfr: true
  zones:
    - example.com
sources:
  - ingress
  - service
  - traefik-proxy
traefik:
  disableLegacy: true
txtPrefix: external-dns.
```

**Anything else we need to know?**:

The following error is returned by ExternalDNS :

```
level=info msg="Instantiating new Kubernetes client"
level=info msg="Using inCluster-config based on serviceaccount-token"
level=info msg="Created Kubernetes client https://10.21.0.1:443"
level=info msg="Using inCluster-config based on serviceaccount-token"
level=info msg="Created Dynamic Kubernetes client https://10.21.0.1:443"
level=info msg="Configured RFC2136 with zones '[example.com[]' and nameservers '[dc01.example.com]'"
level=info msg="Removing RR: test.example.com 0 A 10.22.0.1"
level=info msg="Removing RR: test.example.com 0 AAAA 2a0b:4780:0:8::10:0"
level=info msg="Removing RR: external-dns.test.example.com 0 TXT \"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/default/test-app-websecure\""
level=info msg="Removing RR: external-dns.a-test.example.com 0 TXT \"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/default/test-app-websecure\""
level=info msg="Removing RR: external-dns.aaaa-test.example.com 0 TXT \"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/default/test-app-websecure\""
level=error msg="RFC2136 delete record failed: [Root cause: Networking_Error] Networking_Error: AS Exchange Error: failed sending AS_REQ to KDC: failed to communicate with KDC. Attempts made with UDP (no KDCs defined in configuration for realm EXAMPLE.COM.) and then TCP (no KDCs defined in configuration for realm EXAMPLE.COM.)"
level=fatal msg="Failed to do run once: RFC2136 had errors in one or more of its batches: [[Root cause: Networking_Error] Networking_Error: AS Exchange Error: failed sending AS_REQ to KDC: failed to communicate with KDC. Attempts made with UDP (no KDCs defined in configuration for realm EXAMPLE.COM.) and then TCP (no KDCs defined in configuration for realm EXAMPLE.COM.)]"
```

**Environment**:

- External-DNS version (use `external-dns --version`): 0.16.1
- DNS provider: RFC2136 GSS-TSIG (Active Directory)
- Others:


Discussion:
@buroa: Same here.@buroa: https://github.com/kubernetes-sigs/external-dns/commit/781eb5c8efa5f24c5f90bfee300840dd071cf235

@Jeremy-Boyle I think this broke some setups.@d4rkfella: Having the exact same issue.@geekifier: Not sure if this issue has the same cause as the one below, but v0.16.x also broke my setup using Kerberos.


> level=error msg="RFC2136 create record failed: [Root cause: KDC_Error] KDC_Error: AS Exchange Error: kerberos error response from KDC: KRB Error: (6) KDC_ERR_C_PRINCIPAL_UNKNOWN Client not found in Kerberos database"
> level=fatal msg="Failed to do run once: RFC2136 had errors in one or more of its batches: [[Root cause: KDC_Error] KDC_Error: AS Exchange Error: kerberos error response from KDC: KRB Error: (6) KDC_ERR_C_PRINCIPAL_UNKNOWN Client not found in Kerberos database]"


Downgrading to `v0.15.1` resolves the issue.

When I ran the diff between the log output of both versions, there are some differences in the nameserver and zone fields. Especially the zone field seems to have an empty array (?) appended to the zone name (might just be a log formatting issue).

**Broken Log Output:**

```
RFC2136Host:[sambadc03.fqdn.domain.name[]
RFC2136Port:53
RFC2136Zone:[fqdn.domain.name[]
RFC2136Insecure:false
RFC2136GSSTSIG:true
RFC2136CreatePTR:false
RFC2136KerberosRealm:fqdn.domain.name
RFC2136KerberosUsername:svcKubernetes
RFC2136KerberosPassword:******
RFC2136TSIGKeyName:
RFC2136TSIGSecret:
RFC2136TSIGSecretAlg:
RFC2136TAXFR:true
RFC2136MinTTL:0s
RFC2136LoadBalancingStrategy:disabled
RFC2136BatchChangeSize:50
RFC2136UseTLS:false
RFC2136SkipTLSVerify:false

> level=info msg="Configured RFC2136 with zones '[fqdn.domain.name[]' and nameservers '[sambadc03.fqdn.domain.name]'"
```
**Working Log Output:**

```
RFC2136Host:sambadc03.fqdn.domain.name
RFC2136Port:53
RFC2136Zone:[fqdn.domain.name[]
RFC2136Insecure:false
RFC2136GSSTSIG:true
RFC2136CreatePTR:false
RFC2136KerberosRealm:fqdn.domain.name
RFC2136KerberosUsername:svcKubernetes
RFC2136KerberosPassword:******
RFC2136TSIGKeyName:
RFC2136TSIGSecret:
RFC2136TSIGSecretAlg:
RFC2136TAXFR:true
RFC2136MinTTL:0s
RFC2136BatchChangeSize:50
RFC2136UseTLS:false
RFC2136SkipTLSVerify:false

level=info msg="Configured RFC2136 with zone '[fqdn.domain.name]' and nameserver 'sambadc03.fqdn.domain.name:53'"
```

**Pod Config**

```yaml
Args:
      --log-level=info
      --log-format=text
      --interval=1m
      --source=service
      --source=ingress
      --policy=sync
      --registry=txt
      --txt-owner-id=tinykube01
      --txt-prefix=k8s
      --domain-filter=fqdn.domain.name
      --provider=rfc2136
      --rfc2136-gss-tsig
      --rfc2136-host=sambadc03.fqdn.domain.name
      --rfc2136-port=53
      --rfc2136-zone=fqdn.domain.name
      --rfc2136-tsig-axfr
    Environment:
      EXTERNAL_DNS_RFC2136_KERBEROS_USERNAME:  <set to the key 'EXTERNAL_DNS_RFC2136_KERBEROS_USERNAME' in secret 'internal-dns-secret'>  Optional: false
      EXTERNAL_DNS_RFC2136_KERBEROS_PASSWORD:  <set to the key 'EXTERNAL_DNS_RFC2136_KERBEROS_PASSWORD' in secret 'internal-dns-secret'>  Optional: false
      EXTERNAL_DNS_RFC2136_KERBEROS_REALM:     <set to the key 'EXTERNAL_DNS_RFC2136_KERBEROS_REALM' in secret 'internal-dns-secret'>     Optional: false
    Mounts:
      /etc/krb5.conf from kerberos-config-volume (rw,path="krb5.conf")
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-p7g8g (ro)

```@fmunteanu: For reference, I'm running on official Helm chart `1.15.2` release without any issues. The little mess started with `1.16.0` release, annotations are also [partially fixed](https://github.com/axivo/k3s-cluster/discussions/478) into OCI `v0.16.1` version, from an user report (I asked him to open an issue):

```
helm show chart kubernetes-sigs/external-dns --version 1.15.2 | grep appVersion
appVersion: 0.15.1
helm get manifest external-dns -n kube-system | grep app.kubernetes.io/version | head -n 1
    app.kubernetes.io/version: "0.15.1"
```

I opened a new issue, see https://github.com/kubernetes-sigs/external-dns/issues/5256.@ivankatliarchuk: Anyone have a working home lab open sourced, the one I could deploy to minikube/kind and try out at some point. A little bit occupied with other stuff but may find some time@geekifier: I tried to do some more debugging with the latest chart release, which fixed the schema validation issues.

I messed with the host options, trying to see if enabling the new load balancing features might change some of the behaviors behind the scenes.

The debug logs reflect that the client is sending `DOMAIN.TLD.` (period at the end) as the REALM, whereas the server does not expect a period for the Kerberos *realm* (not the domain). I am passing in the correct REALM using `EXTERNAL_DNS_RFC2136_KERBEROS_REALM` in env.

> CRealm in response does not match what was requested. Requested: **SECRETDOMAIN.COM.**; Reply: **SECRETDOMAIN.COM**

Now, that could be just some issue with the logging from the upstream (gokrb?) library, but if indeed the `.` is appended to the realm, that might explain the issue.

Here's the log in question

>time="2025-04-15T19:30:07Z" level=debug msg="RemoveRecord.ep=k8sa-echo-internal.SECRETDOMAIN.COM 0 IN TXT  \"heritage=external-dns,external-dns/owner=tinykube01,external-dns/resource=ingress/default/echo-internal\" []"
time="2025-04-15T19:30:07Z" level=info msg="Removing RR: k8sa-echo-internal.SECRETDOMAIN.COM 0 TXT \"heritage=external-dns,external-dns/owner=tinykube01,external-dns/resource=ingress/default/echo-internal\""
time="2025-04-15T19:30:07Z" level=debug msg=SendMessage
time="2025-04-15T19:30:07Z" level=debug msg="Sending message to nameserver: sambadc03.SECRETDOMAIN.COM:53"
time="2025-04-15T19:30:07Z" level=error msg="RFC2136 delete record failed: [Root cause: KRBMessage_Handling_Error] KRBMessage_Handling_Error: AS Exchange Error: AS_REP is not valid or client password/keytab incorrect < KRBMessage_Handling_Error: CRealm in response does not match what was requested. Requested: SECRETDOMAIN.COM.; Reply: SECRETDOMAIN.COM"
time="2025-04-15T19:30:07Z" level=fatal msg="Failed to do run once: RFC2136 had errors in one or more of its batches: [[Root cause: KDC_Error[] KDC_Error: AS Exchange Error: kerberos error response from KDC: KRB Error: (6) KDC_ERR_C_PRINCIPAL_UNKNOWN Client not found in Kerberos database [Root cause: KRBMessage_Handling_Error] KRBMessage_Handling_Error: AS Exchange Error: AS_REP is not valid or client password/keytab incorrect < KRBMessage_Handling_Error: CRealm in response does not match what was requested. Requested: SECRETDOMAIN.COM.; Reply: SECRETDOMAIN.COM]"

Once again, reverting to v0.15.x fixes this behavior.@stovemeerkat: I just ran into the same issue. Like @geekifier I quickly noticed that the logs show  the realm `DOMAIN.TLD.` (with a trailing dot), even if a different realm is explicitely passed using the `--rfc2136-kerberos-realm` argument or the `EXTERNAL_DNS_RFC2136_KERBEROS_REALM` env.

I think that is down to this change, which effetively overwrites the configured realm befor trying to apply changes: https://github.com/kubernetes-sigs/external-dns/commit/7fb529965ddef54bf5489b4bc7984d20409ba3a6#diff-ccdb1859307bef674a422d4608d6dea501398216a0c18202c4d2fa160b9986a1R267

In my opinion, this definitely should not happen when the realm is explicitely set using CLI args or env variables.@ValeriiVozniuk: The issue is present even in simple configuration with config like this
```
domainFilters:
- domain.com
nodeSelector:
  kubernetes.io/os: linux
provider: rfc2136
rfc2136:
  host: 10.16.5.1
  port: 53
  zones:
  - domain.com
```
Log:
```
time="2025-04-23T08:07:31Z" level=debug msg=SendMessage
time="2025-04-23T08:07:31Z" level=debug msg="Sending message to nameserver: 10.16.5.1:53"
time="2025-04-23T08:07:31Z" level=warning msg="warn in dns.Client.Exchange: dns: bad signature"
time="2025-04-23T08:07:31Z" level=error msg="RFC2136 create record failed: dns: bad signature"
time="2025-04-23T08:07:31Z" level=fatal msg="Failed to do run once: RFC2136 had errors in one or more of its batches: [dns: bad signature]"
```
Seems "dns: bad signature" warning was silently promoted to error. Rolling back to 0.15.1 fixes the issue@mloiseleur: @Jeremy-Boyle Do you think you can open a PR to fix this trailing dot issue ? Do you need some help ? or should we revert the multiple hosts feature ?@Jeremy-Boyle: ~~I'll take a look but the trailing dot issue, is likely not the issue.~~

I cant remember where but gokrb (bodgit/tsig uses) will not work correctly without it, or it will add it anyways.

Here we are also adding a trailing dot. which is in both versions, ill take a look however. 

https://github.com/kubernetes-sigs/external-dns/blob/8d8f81369f02beeba479944e26f55dfbcb53195d/provider/rfc2136/rfc2136.go#L122-L130

https://github.com/kubernetes-sigs/external-dns/blob/c55b1a1127df024b95f927869a1bcd662ec4337a/provider/rfc2136/rfc2136.go#L105-L113@Jeremy-Boyle: I think i might have found the issue and where its happening. 

If we take a look at 0.15.1

https://github.com/kubernetes-sigs/external-dns/blob/c55b1a1127df024b95f927869a1bcd662ec4337a/provider/rfc2136/rfc2136.go#L312-L333

You can see
```go
func (r rfc2136Provider)
```

When i made my changes i needed to make the rfc2136Provider a pointer to handle the updates for lastErr and for the mutex. 

Which means when:

https://github.com/kubernetes-sigs/external-dns/blob/8d8f81369f02beeba479944e26f55dfbcb53195d/provider/rfc2136/rfc2136.go#L362-L383

This now runs it updates the provider causing issues talking with kerb. 

Specifically:
```go
r.krb5Realm = strings.ToUpper(zone)

----
// Set zone to root if no set
if len(zoneNames) == 0 {
	zoneNames = append(zoneNames, ".")
}

// Sort zones
sort.Slice(zoneNames, func(i, j int) bool {
	return len(strings.Split(zoneNames[i], ".")) > len(strings.Split(zoneNames[j], "."))
})
```

Since this appends the "." to the zones, you run into a condition where the credentials will ether fail or you'll get the bad DNS because its updating before or after you get the credentials. But i'm still looking into the fix, i'll start a PR once i understand better . 


However, debugging this from master vs 0.15.1 tag, In 0.15.1, the code below never actually does anything because its already created the provider and has a local copy, I think we can commit this out, but ill make sure and test it. I don't think this should ever change unless we add support for multiple realms. Which external-dns doesn't currently.

```go
r.krb5Realm = strings.ToUpper(zone)
```@Jeremy-Boyle: @mloiseleur commenting out the following seems to fix the issue. 

Im not exactly sure what happened, and why it was only a issue once we released, previously it was working and would get credentials and set up kerb before the memory was updated hence why i never caught it.


Commenting out below in ApplyChanges, seems to fix the provider.
```go
//r.krb5Realm = strings.ToUpper(zone)
```

Before I make the MR do we want to cherry pick this fix off 0.16.x, or will we make a new release off main ? @mloiseleur: @Jeremy-Boyle You can do it on _master_ branch, we will release soon (https://github.com/kubernetes-sigs/external-dns/issues/5362)@ivankatliarchuk: Anyone have time to review PR and ideally validate is working?

https://github.com/kubernetes-sigs/external-dns/pull/5385
@geekifier: All of the issues I listed above are fixed in #5385.

Solution patch:
diff --git a/.github/ISSUE_TEMPLATE/create-release.md b/.github/ISSUE_TEMPLATE/create-release.md
new file mode 100644
index 00000000..349753ee
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/create-release.md
@@ -0,0 +1,24 @@
+---
+name: Create Release
+about: Release template to track the next release
+title: Release x.y
+labels: area/release
+assignees: ''
+
+---
+
+This Issue tracks the next `external-dns` release. Please follow the guideline below. If anything is missing or unclear, please add a comment to this issue so this can be improved after the release.
+
+#### Preparation Tasks
+
+- [ ] Release [steps](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/release.md#steps)
+
+#### Release Execution
+
+- [ ] Branch out from the default branch and run scripts/version-updater.sh to update the image tag used in the kustomization.yaml and in documentation.
+- [ ] Create the PR with this version change.
+- [ ] Create an issue to release the corresponding Helm chart via the chart release process (below) assigned to a chart maintainer
+
+#### After Release Tasks
+
+- [ ] Announce release on `#external-dns` in Slack
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 0faa3d06..1f46ff48 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -4,6 +4,9 @@ on:
   push:
     tags:
       - "v*"
+  # See https://docs.github.com/fr/webhooks/webhook-events-and-payloads#workflow_dispatch
+  # Can be used to update doc with latest tag
+  workflow_dispatch:
 
 permissions: {}
 
@@ -35,5 +38,9 @@ jobs:
 
       - name: build and push
         run: |
-          mike deploy ${{ github.ref_name }} latest --push --update-aliases
+          VERSION="${{ github.ref_name }}"
+          if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then
+            VERSION="latest"
+          fi
+          mike deploy $VERSION --push --update-aliases
           mike set-default --push latest
diff --git a/.github/workflows/json-yaml-validate.yml b/.github/workflows/json-yaml-validate.yml
index 7cf8d250..aa861c42 100644
--- a/.github/workflows/json-yaml-validate.yml
+++ b/.github/workflows/json-yaml-validate.yml
@@ -17,7 +17,9 @@ jobs:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
       - name: json-yaml-validate
-        uses: GrantBirki/json-yaml-validate@v3.3.0
+        uses: GrantBirki/json-yaml-validate@v3.2.1 # pin @v3.2.1 bug in 3.3.0: https://github.com/GrantBirki/json-yaml-validate/issues/86
         with:
+          # ref: https://github.com/GrantBirki/json-yaml-validate?tab=readme-ov-file#inputs-
           comment: "true" # enable comment mode
           yaml_exclude_regex: "(charts/external-dns/templates.*|mkdocs.yml)"
+          allow_multiple_documents: "true"
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 8e784de6..c1b26d40 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -55,3 +55,8 @@ jobs:
       uses: stoplightio/spectral-action@2ad0b9302e32a77c1caccf474a9b2191a8060d83 # v0.8.11
       with:
         file_glob: 'api/*.yaml'
+
+    - uses: actions/setup-python@v3
+    # https://github.com/pre-commit/action
+    - name: Verify with pre-commit
+      uses: pre-commit/action@v3.0.1
diff --git a/.golangci.yml b/.golangci.yml
index 17669081..7b12c19c 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -14,9 +14,12 @@ linters:
     - unconvert
     - unused
     - whitespace
+    - decorder # Check declaration order and count of types, constants, variables and functions. https://golangci-lint.run/usage/linters/#decorder
+    - tagalign # Check that struct tags are well aligned. https://golangci-lint.run/usage/linters/#tagalign
     - predeclared # Find code that shadows one of Go's predeclared identifiers
     - sloglint # Ensure consistent code style when using log/slog
     - asciicheck  # Checks that all code identifiers does not have non-ASCII symbols in the name
+    - nilnil # Checks that there is no simultaneous return of nil error and an nil value. ref: https://golangci-lint.run/usage/linters/#nilnil
   settings:
     exhaustive:
       default-signifies-exhaustive: false
@@ -51,6 +54,10 @@ linters:
           - varcheck
           - whitespace
         path: _test\.go
+      # TODO: skiip as will require design changes
+      - linters:
+          - nilnil
+        path: istio_virtualservice.go|fqdn.go|cloudflare.go
     paths:
       - endpoint/zz_generated.deepcopy.go
       - third_party$
diff --git a/Makefile b/Makefile
index d8b7a74e..c0c15cd2 100644
--- a/Makefile
+++ b/Makefile
@@ -200,5 +200,12 @@ helm-lint:
 	scripts/helm-tools.sh --docs
 
 .PHONY: go-dependency
-go-dependency: ## Dependency maintanance
+#? go-dependency: Dependency maintanance
+go-dependency:
 	go mod tidy
+
+.PHONY: mkdocs-serve
+#? mkdocs-serve: Run the builtin development server for mkdocs
+mkdocs-serve:
+	@$(info "contribute to documentation docs/contributing/dev-guide.md")
+	@mkdocs serve
diff --git a/README.md b/README.md
index 45841e9a..b4eebfd1 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ This README is a part of the complete documentation, available [here](https://ku
 
 Inspired by [Kubernetes DNS](https://github.com/kubernetes/dns), Kubernetes' cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers.
 Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the [Kubernetes API](https://kubernetes.io/docs/api/) to determine a desired list of DNS records.
-*Unlike* KubeDNS, however, it's not a DNS server itself, but merely configures other DNS providers accordingly—e.g. [AWS Route 53](https://aws.amazon.com/route53/) or [Google Cloud DNS](https://cloud.google.com/dns/docs/).
+_Unlike_ KubeDNS, however, it's not a DNS server itself, but merely configures other DNS providers accordingly—e.g. [AWS Route 53](https://aws.amazon.com/route53/) or [Google Cloud DNS](https://cloud.google.com/dns/docs/).
 
 In a broader sense, ExternalDNS allows you to control DNS records dynamically via Kubernetes resources in a DNS provider-agnostic way.
 
@@ -77,7 +77,7 @@ Note that all flags can be replaced with environment variables; for instance,
 
 ## New providers
 
-No new provider will be added to ExternalDNS *in-tree*.
+No new provider will be added to ExternalDNS _in-tree_.
 
 ExternalDNS has introduced a webhook system, which can be used to add a new provider.
 See PR #3063 for all the discussions about it.
@@ -116,7 +116,7 @@ ExternalDNS supports multiple DNS providers which have been implemented by the [
 Maintaining all of those in a central repository is a challenge, which introduces lots of toil and potential risks.
 
 This mean that `external-dns` has begun the process to move providers out of tree. See #4347 for more details.
-Those who are interested can create a webhook provider based on an *in-tree* provider and after submit a PR to reference it here.
+Those who are interested can create a webhook provider based on an _in-tree_ provider and after submit a PR to reference it here.
 
 We define the following stability levels for providers:
 
@@ -126,34 +126,34 @@ We define the following stability levels for providers:
 
 The following table clarifies the current status of the providers according to the aforementioned stability levels:
 
-| Provider | Status | Maintainers |
-| -------- | ------ | ----------- |
-| Google Cloud DNS | Stable | |
-| AWS Route 53 | Stable | |
-| AWS Cloud Map | Beta | |
-| Akamai Edge DNS | Beta | |
-| AzureDNS | Stable | |
-| Civo | Alpha | @alejandrojnm |
-| CloudFlare | Beta | |
-| DigitalOcean | Alpha | |
-| DNSimple | Alpha | |
-| PowerDNS | Alpha | |
-| CoreDNS | Alpha | |
-| Exoscale | Alpha | |
-| Oracle Cloud Infrastructure DNS | Alpha | |
-| Linode DNS | Alpha | |
-| RFC2136 | Alpha | |
-| NS1 | Alpha | |
-| TransIP | Alpha | |
-| OVHcloud | Beta | @rbeuque74 |
-| Scaleway DNS | Alpha | @Sh4d1 |
-| UltraDNS | Alpha | |
-| GoDaddy | Alpha | |
-| Gandi | Alpha | @packi |
-| IBMCloud | Alpha | @hughhuangzh |
-| TencentCloud | Alpha | @Hyzhou |
-| Plural | Alpha | @michaeljguarino |
-| Pi-hole | Alpha | @tinyzimmer |
+| Provider                        | Status | Maintainers      |
+| ------------------------------- | ------ | ---------------- |
+| Google Cloud DNS                | Stable |                  |
+| AWS Route 53                    | Stable |                  |
+| AWS Cloud Map                   | Beta   |                  |
+| Akamai Edge DNS                 | Beta   |                  |
+| AzureDNS                        | Stable |                  |
+| Civo                            | Alpha  | @alejandrojnm    |
+| CloudFlare                      | Beta   |                  |
+| DigitalOcean                    | Alpha  |                  |
+| DNSimple                        | Alpha  |                  |
+| PowerDNS                        | Alpha  |                  |
+| CoreDNS                         | Alpha  |                  |
+| Exoscale                        | Alpha  |                  |
+| Oracle Cloud Infrastructure DNS | Alpha  |                  |
+| Linode DNS                      | Alpha  |                  |
+| RFC2136                         | Alpha  |                  |
+| NS1                             | Alpha  |                  |
+| TransIP                         | Alpha  |                  |
+| OVHcloud                        | Beta   | @rbeuque74       |
+| Scaleway DNS                    | Alpha  | @Sh4d1           |
+| UltraDNS                        | Alpha  |                  |
+| GoDaddy                         | Alpha  |                  |
+| Gandi                           | Alpha  | @packi           |
+| IBMCloud                        | Alpha  | @hughhuangzh     |
+| TencentCloud                    | Alpha  | @Hyzhou          |
+| Plural                          | Alpha  | @michaeljguarino |
+| Pi-hole                         | Alpha  | @tinyzimmer      |
 
 ## Kubernetes version compatibility
 
@@ -220,7 +220,7 @@ The following tutorials are provided:
 
 ### Running Locally
 
-See the [contributor guide](docs/contributing/getting-started.md) for details on compiling
+See the [contributor guide](docs/contributing/dev-guide.md) for details on compiling
 from source.
 
 #### Setup Steps
@@ -244,7 +244,7 @@ Optionally, you can customize the TTL value of the resulting DNS record by using
 kubectl annotate service nginx "external-dns.alpha.kubernetes.io/ttl=10"
 ```
 
-For more details on configuring TTL, see [here](docs/ttl.md).
+For more details on configuring TTL, see [here](docs/advanced/ttl.md).
 
 Use the internal-hostname annotation to create DNS records with ClusterIP as the target.
 
@@ -289,7 +289,7 @@ The **tutorials** section contains examples, including Ingress resources, and sh
 
 # Note
 
-If using a txt registry and attempting to use a CNAME the `--txt-prefix` must be set to avoid conflicts.  Changing `--txt-prefix` will result in lost ownership over previously created records.
+If using a txt registry and attempting to use a CNAME the `--txt-prefix` must be set to avoid conflicts. Changing `--txt-prefix` will result in lost ownership over previously created records.
 
 If `externalIPs` list is defined for a `LoadBalancer` service, this list will be used instead of an assigned load balancer IP to create a DNS record.
 It's useful when you run bare metal Kubernetes clusters behind NAT or in a similar setup, where a load balancer IP differs from a public IP (e.g. with [MetalLB](https://metallb.universe.tf)).
diff --git a/controller/execute.go b/controller/execute.go
index 4113fd64..6ae1cf5b 100644
--- a/controller/execute.go
+++ b/controller/execute.go
@@ -186,9 +186,9 @@ func Execute() {
 		}
 		p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, cfg.AWSSDCreateTag, sd.NewFromConfig(aws.CreateDefaultV2Config(cfg)))
 	case "azure-dns", "azure":
-		p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.DryRun)
+		p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.AzureMaxRetriesCount, cfg.DryRun)
 	case "azure-private-dns":
-		p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.DryRun)
+		p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.AzureMaxRetriesCount, cfg.DryRun)
 	case "ultradns":
 		p, err = ultradns.NewUltraDNSProvider(domainFilter, cfg.DryRun)
 	case "civo":
@@ -199,12 +199,15 @@ func Execute() {
 			zoneIDFilter,
 			cfg.CloudflareProxied,
 			cfg.DryRun,
-			cfg.CloudflareDNSRecordsPerPage,
 			cfg.CloudflareRegionKey,
 			cloudflare.CustomHostnamesConfig{
 				Enabled:              cfg.CloudflareCustomHostnames,
 				MinTLSVersion:        cfg.CloudflareCustomHostnamesMinTLSVersion,
 				CertificateAuthority: cfg.CloudflareCustomHostnamesCertificateAuthority,
+			},
+			cloudflare.DNSRecordsConfig{
+				PerPage: cfg.CloudflareDNSRecordsPerPage,
+				Comment: cfg.CloudflareDNSRecordsComment,
 			})
 	case "google":
 		p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun)
diff --git a/docs/20190708-external-dns-incubator.md b/docs/20190708-external-dns-incubator.md
index f86dad5f..2a49c40f 100644
--- a/docs/20190708-external-dns-incubator.md
+++ b/docs/20190708-external-dns-incubator.md
@@ -2,16 +2,15 @@
 
 <!-- TOC depthFrom:1 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
 
-- [Move ExternalDNS out of Kubernetes incubator]((#move-externaldns-out-of-kubernetes-incubator))
-  - [Summary](#summary)
-  - [Motivation](#motivation)
-    - [Goals](#goals)
-  - [Proposal](#proposal)
-  - [Details](#details)
-    - [Graduation Criteria](#graduation-criteria)
-      - [Maintainers](#maintainers)
-    - [Release process, artifacts](#release-process-artifacts)
-    - [Risks and Mitigations](#risks-and-mitigations)
+- [Summary](#summary)
+- [Motivation](#motivation)
+  - [Goals](#goals)
+- [Proposal](#proposal)
+- [Details](#details)
+  - [Graduation Criteria](#graduation-criteria)
+    - [Maintainers](#maintainers)
+  - [Release process, artifacts](#release-process-artifacts)
+  - [Risks and Mitigations](#risks-and-mitigations)
 
 <!-- /TOC -->
 
@@ -150,9 +149,11 @@ The following are risks that were identified:
 We think that the following actions will constitute appropriate mitigations:
 
 - Decoupling the providers via an API will allow us to resolve the problem of the providers. Being the project already more than 2 years old and given that there are 18 providers implemented, we possess enough information to define an API that we can be stable in a short timeframe.
+
   - Once this is stable, the problem of testing the providers can be deferred to be a provider's responsibility. This will also reduce the scope of External DNS core code, which means that there will be no need for a further increase of the maintaining team.
 
 - We added integration testing for the main cloud providers to the roadmap for the 1.0 release to make sure that we cover the mostly used ones.
+
   - We believe that this item should be tackled independently from the decoupling of providers as it would be capable of generating value independently from the result of the decoupling efforts.
 
 - With the move to the Kubernetes incubation, we hope that we will be able to access the testing resources of the Kubernetes project.
diff --git a/docs/advanced/fqdn-templating.md b/docs/advanced/fqdn-templating.md
index 531eb8bc..16607f33 100644
--- a/docs/advanced/fqdn-templating.md
+++ b/docs/advanced/fqdn-templating.md
@@ -68,6 +68,9 @@ The template uses the following data from the source object (e.g., a `Service` o
 | Function     | Description                                                                              |
 |:-------------|:-----------------------------------------------------------------------------------------|
 | `trimPrefix`  | Function from the `strings` package. Returns `string` without the provided leading prefix. |
+| `replace`     | Function that performs a simple replacement of all `old` string with `new` in the source string. |
+| `isIPv4`      | Function that checks if a string is a valid IPv4 address. |
+| `isIPv6`      | Function that checks if a string is a valid IPv6 address (including IPv4-mapped IPv6). |
 
 ---
 
@@ -304,3 +307,12 @@ args:
 ```
 
 By setting the hostname annotation in the ingress resource, ExternalDNS constructs the FQDN accordingly. This approach allows for dynamic DNS entries without hardcoding hostnames.
+
+### Using a Node's Addresses for FQDNs
+
+```yml
+args:
+  - --fqdn-template="{{range .Status.Addresses}}{{if and (eq .Type \"ExternalIP\") (isIPv4 .Address)}}{{.Address | replace \".\" \"-\"}}{{break}}{{end}}{{end}}.example.com
+```
+
+This is a complex template that iternates through a list of a Node's Addresses and creates a FQDN with public IPv4 addresses.
diff --git a/docs/contributing/dev-guide.md b/docs/contributing/dev-guide.md
index 2747c7c7..a8863225 100644
--- a/docs/contributing/dev-guide.md
+++ b/docs/contributing/dev-guide.md
@@ -304,3 +304,20 @@ Install required dependencies. In order to not to break system packages, we are
 $$ ...
 $$ Serving on http://127.0.0.1:8000/
 ```
+
+### How to add an example snippet
+
+Let's say we are improving tutorial location in `docs/tutorials/aws.md`.
+
+1. Add a snippet to `docs/snippets/aws/<snippet-name>.<snippet-extension>`
+2. Add snippet to a markdown file `docs/tutorials/aws.md`
+
+[[% raw %]]
+
+````md
+  ```extension
+    [[% include 'snippets/aws/<snippet-name>.<snippet-extension>' %]]
+  ```
+````
+
+[[% endraw %]]
diff --git a/docs/contributing/sources-and-providers.md b/docs/contributing/sources-and-providers.md
index 9d9677cc..77e32bbe 100644
--- a/docs/contributing/sources-and-providers.md
+++ b/docs/contributing/sources-and-providers.md
@@ -26,7 +26,7 @@ All sources live in package `source`.
 * `ContourIngressRouteSource`: collects all Contour IngressRoutes and returns them as Endpoint objects. The desired DNS name corresponds to the `virtualhost.fqdn` listed within the spec of each IngressRoute object.
 * `FakeSource`: returns a random list of Endpoints for the purpose of testing providers without having access to a Kubernetes cluster.
 * `ConnectorSource`: returns a list of Endpoint objects which are served by a tcp server configured through `connector-source-server` flag.
-* `CRDSource`: returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to [CRD source](crd-source.md) documentation.
+* `CRDSource`: returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to [CRD source](../sources/crd.md) documentation.
 * `EmptySource`: returns an empty list of Endpoint objects for the purpose of testing and cleaning out entries.
 
 ## Providers
diff --git a/docs/deprecation.md b/docs/deprecation.md
index adc98ff3..66db1c8b 100644
--- a/docs/deprecation.md
+++ b/docs/deprecation.md
@@ -43,7 +43,7 @@ The proposal must clearly outline the rationale for inclusion, the impact on use
 
 The proposal must be formalized by submitting a `docs/proposal/EDP-XXX.md` document in a Pull Request. Pull request must be labeled with `kind/proposal`.
 
-The proposal template location is [here](docs/proposal/design-template.md). The template is quite complete, one can remove any unnecessary or irrelevant section on a specific proposal.
+The proposal template location is [here](./proposal/design-template.md). The template is quite complete, one can remove any unnecessary or irrelevant section on a specific proposal.
 
 ## Deprecation Process
 
diff --git a/docs/faq.md b/docs/faq.md
index 76256bd7..e94b8e6c 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -193,7 +193,7 @@ $ docker run \
   -e EXTERNAL_DNS_SOURCE=$'service\ningress' \
   -e EXTERNAL_DNS_PROVIDER=google \
   -e EXTERNAL_DNS_DOMAIN_FILTER=$'foo.com\nbar.com' \
-  registry.k8s.io/external-dns/external-dns:v0.16.1
+  registry.k8s.io/external-dns/external-dns:v0.17.0
 time="2017-08-08T14:10:26Z" level=info msg="config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ...
 ```
 
diff --git a/docs/flags.md b/docs/flags.md
index 06af5277..00a9abda 100644
--- a/docs/flags.md
+++ b/docs/flags.md
@@ -19,7 +19,7 @@
 | `--skipper-routegroup-groupversion="zalando.org/v1"` | The resource version for skipper routegroup |
 | `--[no-]always-publish-not-ready-addresses` | Always publish also not ready addresses for headless services (optional) |
 | `--annotation-filter=""` | Filter resources queried for endpoints by annotation, using label selector semantics |
-| `--[no-]combine-fqdn-annotation` | Combine FQDN template and Annotations instead of overwriting |
+| `--[no-]combine-fqdn-annotation` | Combine FQDN template and Annotations instead of overwriting (default: false) |
 | `--compatibility=` | Process annotation semantics from legacy implementations (optional, options: mate, molecule, kops-dns-controller) |
 | `--connector-source-server="localhost:8080"` | The server to connect for connector source, valid only when using connector source |
 | `--crd-source-apiversion="externaldns.k8s.io/v1alpha1"` | API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source |
@@ -86,6 +86,7 @@
 | `--azure-subscription-id=""` | When using the Azure provider, override the Azure subscription to use (optional) |
 | `--azure-user-assigned-identity-client-id=""` | When using the Azure provider, override the client id of user assigned identity in config file (optional) |
 | `--azure-zones-cache-duration=0s` | When using the Azure provider, set the zones list cache TTL (0s to disable). |
+| `--azure-maxretries-count=3` | When using the Azure provider, set the number of retries for API calls (When less than 0, it disables retries). (optional) |
 | `--tencent-cloud-config-file="/etc/kubernetes/tencent-cloud.json"` | When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud) |
 | `--tencent-cloud-zone-type=` | When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private) |
 | `--[no-]cloudflare-proxied` | When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled) |
@@ -94,6 +95,7 @@
 | `--cloudflare-custom-hostnames-certificate-authority=google` | When using the Cloudflare provider with the Custom Hostnames, specify which Cerrtificate Authority will be used by default. (default: google, options: google, ssl_com, lets_encrypt) |
 | `--cloudflare-dns-records-per-page=100` | When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100) |
 | `--cloudflare-region-key=CLOUDFLARE-REGION-KEY` | When using the Cloudflare provider, specify the region (default: earth) |
+| `--cloudflare-record-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (default: '') |
 | `--coredns-prefix="/skydns/"` | When using the CoreDNS provider, specify the prefix name |
 | `--akamai-serviceconsumerdomain=""` | When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified) |
 | `--akamai-client-token=""` | When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified) |
diff --git a/docs/registry/dynamodb.md b/docs/registry/dynamodb.md
index ba9f76b7..0e34ea55 100644
--- a/docs/registry/dynamodb.md
+++ b/docs/registry/dynamodb.md
@@ -81,7 +81,7 @@ spec:
     spec:
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.16.1
+          image: registry.k8s.io/external-dns/external-dns:v0.17.0
           args:
             - --source=service
             - --source=ingress
diff --git a/docs/release.md b/docs/release.md
index aa7dd6ac..35bdd59a 100644
--- a/docs/release.md
+++ b/docs/release.md
@@ -14,7 +14,7 @@ A new staging image is released weekly and can be found at [gcr.io/k8s-staging-e
 Example command to fetch `10` most recent staging images:
 
 ```sh
-export EXT_DNS_VERSION="v0.16.1"
+export EXT_DNS_VERSION="v0.17.0"
 curl -sLk https://gcr.io/v2/k8s-staging-external-dns/external-dns/tags/list | jq | grep "$EXT_DNS_VERSION" | tail -n 10
 ```
 
diff --git a/docs/scripts/requirements.txt b/docs/scripts/requirements.txt
index 5564c82c..27086cd3 100644
--- a/docs/scripts/requirements.txt
+++ b/docs/scripts/requirements.txt
@@ -1,5 +1,6 @@
 mkdocs-git-revision-date-localized-plugin == 1.2.4
 mkdocs == 1.5.3
+mkdocs-macros-plugin==1.3.7
 mkdocs-material == 9.5.17
 mkdocs-literate-nav == 0.6.1
 mkdocs-same-dir == 0.1.3
diff --git a/docs/snippets/digitalocean/deploy-nginx.yaml b/docs/snippets/digitalocean/deploy-nginx.yaml
new file mode 100644
index 00000000..6c0dc1a2
--- /dev/null
+++ b/docs/snippets/digitalocean/deploy-nginx.yaml
@@ -0,0 +1,35 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+      - image: nginx
+        name: nginx
+        ports:
+        - containerPort: 80
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nginx
+  annotations:
+    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
+spec:
+  selector:
+    app: nginx
+  type: LoadBalancer
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 80
diff --git a/docs/snippets/digitalocean/extdns-with-rbac.yaml b/docs/snippets/digitalocean/extdns-with-rbac.yaml
new file mode 100644
index 00000000..ccb60383
--- /dev/null
+++ b/docs/snippets/digitalocean/extdns-with-rbac.yaml
@@ -0,0 +1,64 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: external-dns
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: external-dns
+rules:
+- apiGroups: [""]
+  resources: ["services","endpoints","pods"]
+  verbs: ["get","watch","list"]
+- apiGroups: ["extensions","networking.k8s.io"]
+  resources: ["ingresses"]
+  verbs: ["get","watch","list"]
+- apiGroups: [""]
+  resources: ["nodes"]
+  verbs: ["list"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: external-dns-viewer
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: external-dns
+subjects:
+- kind: ServiceAccount
+  name: external-dns
+  namespace: default
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: external-dns
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: external-dns
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      labels:
+        app: external-dns
+    spec:
+      serviceAccountName: external-dns
+      containers:
+      - name: external-dns
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
+        args:
+        - --source=service # ingress is also possible
+        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
+        - --provider=digitalocean
+        env:
+        - name: DO_TOKEN
+          valueFrom:
+            secretKeyRef:
+              name: DO_TOKEN
+              key: DO_TOKEN
diff --git a/docs/snippets/digitalocean/extdns-without-rbac.yaml b/docs/snippets/digitalocean/extdns-without-rbac.yaml
new file mode 100644
index 00000000..696e1b08
--- /dev/null
+++ b/docs/snippets/digitalocean/extdns-without-rbac.yaml
@@ -0,0 +1,29 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: external-dns
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: external-dns
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      labels:
+        app: external-dns
+    spec:
+      containers:
+      - name: external-dns
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
+        args:
+        - --source=service # ingress is also possible
+        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
+        - --provider=digitalocean
+        env:
+        - name: DO_TOKEN
+          valueFrom:
+            secretKeyRef:
+              name: DO_TOKEN
+              key: DO_TOKEN
diff --git a/docs/snippets/exoscale/extdns.yaml b/docs/snippets/exoscale/extdns.yaml
new file mode 100644
index 00000000..48ae63fa
--- /dev/null
+++ b/docs/snippets/exoscale/extdns.yaml
@@ -0,0 +1,30 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: external-dns
+spec:
+  strategy:
+    type: Recreate
+  selector:
+    matchLabels:
+      app: external-dns
+  template:
+    metadata:
+      labels:
+        app: external-dns
+    spec:
+      # Only use if you're also using RBAC
+      # serviceAccountName: external-dns
+      containers:
+      - name: external-dns
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
+        args:
+        - --source=ingress # or service or both
+        - --provider=exoscale
+        - --domain-filter={{ my-domain }}
+        - --policy=sync # if you want DNS entries to get deleted as well
+        - --txt-owner-id={{ owner-id-for-this-external-dns }}
+        - --exoscale-apikey={{ api-key}}
+        - --exoscale-apisecret={{ api-secret }}
+        # - --exoscale-apizone={{ api-zone }}
+        # - --exoscale-apienv={{ api-env }}
diff --git a/docs/snippets/exoscale/how-to-test.yaml b/docs/snippets/exoscale/how-to-test.yaml
new file mode 100644
index 00000000..e302c63d
--- /dev/null
+++ b/docs/snippets/exoscale/how-to-test.yaml
@@ -0,0 +1,50 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: nginx
+  annotations:
+    external-dns.alpha.kubernetes.io/target: {{ Elastic-IP-address }}
+spec:
+  ingressClassName: nginx
+  rules:
+  - host: via-ingress.example.com
+    http:
+      paths:
+      - backend:
+          service:
+            name: "nginx"
+            port:
+              number: 80
+        path: /
+        pathType: Prefix
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nginx
+spec:
+  ports:
+  - port: 80
+    targetPort: 80
+  selector:
+    app: nginx
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx
+spec:
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+      - image: nginx
+        name: nginx
+        ports:
+        - containerPort: 80
diff --git a/docs/snippets/exoscale/rbac.yaml b/docs/snippets/exoscale/rbac.yaml
new file mode 100644
index 00000000..574b3356
--- /dev/null
+++ b/docs/snippets/exoscale/rbac.yaml
@@ -0,0 +1,34 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: external-dns
+  namespace: default
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: external-dns
+rules:
+- apiGroups: [""]
+  resources: ["services","endpoints","pods"]
+  verbs: ["get","watch","list"]
+- apiGroups: ["extensions","networking.k8s.io"]
+  resources: ["ingresses"]
+  verbs: ["get","watch","list"]
+- apiGroups: [""]
+  resources: ["nodes"]
+  verbs: ["list"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: external-dns-viewer
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: external-dns
+subjects:
+- kind: ServiceAccount
+  name: external-dns
+  namespace: default
diff --git a/docs/snippets/security-context/extdns-limited-privilege.yaml b/docs/snippets/security-context/extdns-limited-privilege.yaml
new file mode 100644
index 00000000..6256c86e
--- /dev/null
+++ b/docs/snippets/security-context/extdns-limited-privilege.yaml
@@ -0,0 +1,26 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: external-dns
+spec:
+  strategy:
+    type: Recreate
+  selector:
+    matchLabels:
+      app: external-dns
+  template:
+    metadata:
+      labels:
+        app: external-dns
+    spec:
+      containers:
+      - name: external-dns
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
+        args:
+        - ... # your arguments here
+        securityContext:
+          runAsNonRoot: true
+          runAsUser: 65534
+          readOnlyRootFilesystem: true
+          capabilities:
+            drop: ["ALL"]
diff --git a/docs/sources/about.md b/docs/sources/about.md
index 5a7a6d3b..1421b9a9 100644
--- a/docs/sources/about.md
+++ b/docs/sources/about.md
@@ -1,26 +1,30 @@
 # About
 
-| Source                          | Resources                                                                     | annotation-filter | label-filter |
-|---------------------------------|-------------------------------------------------------------------------------|-------------------|--------------|
-| ambassador-host                 | Host.getambassador.io                                                         | Yes               | Yes          |
-| connector                       |                                                                               |                   |              |
-| contour-httpproxy               | HttpProxy.projectcontour.io                                                   | Yes               |              |
-| cloudfoundry                    |                                                                               |                   |              |
-| crd                             | DNSEndpoint.externaldns.k8s.io                                                | Yes               | Yes          |
-| f5-virtualserver                | VirtualServer.cis.f5.com                                                      | Yes               |              |
-| [gateway-grpcroute](gateway.md) | GRPCRoute.gateway.networking.k8s.io                                           | Yes               | Yes          |
-| [gateway-httproute](gateway.md) | HTTPRoute.gateway.networking.k8s.io                                           | Yes               | Yes          |
-| [gateway-tcproute](gateway.md)  | TCPRoute.gateway.networking.k8s.io                                            | Yes               | Yes          |
-| [gateway-tlsroute](gateway.md)  | TLSRoute.gateway.networking.k8s.io                                            | Yes               | Yes          |
-| [gateway-udproute](gateway.md)  | UDPRoute.gateway.networking.k8s.io                                            | Yes               | Yes          |
-| gloo-proxy                      | Proxy.gloo.solo.io                                                            |                   |              |
-| [ingress](ingress.md)           | Ingress.networking.k8s.io                                                     | Yes               | Yes          |
-| istio-gateway                   | Gateway.networking.istio.io                                                   | Yes               |              |
-| istio-virtualservice            | VirtualService.networking.istio.io                                            | Yes               |              |
-| kong-tcpingress                 | TCPIngress.configuration.konghq.com                                           | Yes               |              |
-| node                            | Node                                                                          | Yes               | Yes          |
-| openshift-route                 | Route.route.openshift.io                                                      | Yes               | Yes          |
-| pod                             | Pod                                                                           |                   |              |
-| [service](service.md)           | Service                                                                       | Yes               | Yes          |
-| skipper-routegroup              | RouteGroup.zalando.org                                                        | Yes               |              |
-| traefik-proxy                   | IngressRoute.traefik.io IngressRouteTCP.traefik.io IngressRouteUDP.traefik.io | Yes               |              |
+A source in ExternalDNS defines where DNS records are discovered from within your infrastructure. Each source corresponds to a specific Kubernetes resource or external system that declares DNS names.
+
+ExternalDNS watches the specified sources for hostname information and uses it to create, update, or delete DNS records accordingly. Multiple sources can be configured simultaneously to support diverse environments.
+
+| Source                                  | Resources                                                                     | annotation-filter | label-filter |
+| --------------------------------------- | ----------------------------------------------------------------------------- | ----------------- | ------------ |
+| ambassador-host                         | Host.getambassador.io                                                         | Yes               | Yes          |
+| connector                               |                                                                               |                   |              |
+| contour-httpproxy                       | HttpProxy.projectcontour.io                                                   | Yes               |              |
+| cloudfoundry                            |                                                                               |                   |              |
+| [crd](crd.md)                           | DNSEndpoint.externaldns.k8s.io                                                | Yes               | Yes          |
+| [f5-virtualserver](f5-virtualserver.md) | VirtualServer.cis.f5.com                                                      | Yes               |              |
+| [gateway-grpcroute](gateway.md)         | GRPCRoute.gateway.networking.k8s.io                                           | Yes               | Yes          |
+| [gateway-httproute](gateway.md)         | HTTPRoute.gateway.networking.k8s.io                                           | Yes               | Yes          |
+| [gateway-tcproute](gateway.md)          | TCPRoute.gateway.networking.k8s.io                                            | Yes               | Yes          |
+| [gateway-tlsroute](gateway.md)          | TLSRoute.gateway.networking.k8s.io                                            | Yes               | Yes          |
+| [gateway-udproute](gateway.md)          | UDPRoute.gateway.networking.k8s.io                                            | Yes               | Yes          |
+| [gloo-proxy](gloo-proxy.md)             | Proxy.gloo.solo.io                                                            |                   |              |
+| [ingress](ingress.md)                   | Ingress.networking.k8s.io                                                     | Yes               | Yes          |
+| [istio-gateway](istio.md)               | Gateway.networking.istio.io                                                   | Yes               |              |
+| [istio-virtualservice](istio.md)        | VirtualService.networking.istio.io                                            | Yes               |              |
+| [kong-tcpingress](kong.md)              | TCPIngress.configuration.konghq.com                                           | Yes               |              |
+| [node](nodes.md)                        | Node                                                                          | Yes               | Yes          |
+| [openshift-route](openshift.md)         | Route.route.openshift.io                                                      | Yes               | Yes          |
+| [pod](pod.md)                           | Pod                                                                           |                   |              |
+| [service](service.md)                   | Service                                                                       | Yes               | Yes          |
+| skipper-routegroup                      | RouteGroup.zalando.org                                                        | Yes               |              |
+| [traefik-proxy](traefik-proxy.md)       | IngressRoute.traefik.io IngressRouteTCP.traefik.io IngressRouteUDP.traefik.io | Yes               |              |
diff --git a/docs/sources/crd.md b/docs/sources/crd.md
index bd21ef00..a5bf3f1b 100644
--- a/docs/sources/crd.md
+++ b/docs/sources/crd.md
@@ -1,6 +1,6 @@
 # CRD Source
 
-CRD source provides a generic mechanism to manage DNS records in your favourite DNS provider supported by external-dns.
+CRD source provides a generic mechanism to manage DNS records in your favorite DNS provider supported by external-dns.
 
 ## Details
 
diff --git a/docs/sources/gateway-api.md b/docs/sources/gateway-api.md
index b8fe6167..6e03e960 100644
--- a/docs/sources/gateway-api.md
+++ b/docs/sources/gateway-api.md
@@ -88,7 +88,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         # Add desired Gateway API Route sources.
         - --source=gateway-httproute
diff --git a/docs/sources/gloo-proxy.md b/docs/sources/gloo-proxy.md
index c187ed2b..ae4b6f55 100644
--- a/docs/sources/gloo-proxy.md
+++ b/docs/sources/gloo-proxy.md
@@ -24,7 +24,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=gloo-proxy
         - --gloo-namespace=custom-gloo-system # gloo system namespace. Specify multiple times for multiple namespaces. Omit to use the default (gloo-system)
@@ -93,7 +93,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=gloo-proxy
         - --gloo-namespace=custom-gloo-system # gloo system namespace. Specify multiple times for multiple namespaces. Omit to use the default (gloo-system)
diff --git a/docs/sources/istio.md b/docs/sources/istio.md
index 2b32f594..fc792d64 100644
--- a/docs/sources/istio.md
+++ b/docs/sources/istio.md
@@ -29,7 +29,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
@@ -100,7 +100,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
diff --git a/docs/sources/kong.md b/docs/sources/kong.md
index 02061133..26e50ba8 100644
--- a/docs/sources/kong.md
+++ b/docs/sources/kong.md
@@ -24,7 +24,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=kong-tcpingress
         - --provider=aws
@@ -89,7 +89,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=kong-tcpingress
         - --provider=aws
diff --git a/docs/sources/mx-record.md b/docs/sources/mx-record.md
index 3abf618a..7b0c91c7 100644
--- a/docs/sources/mx-record.md
+++ b/docs/sources/mx-record.md
@@ -1,6 +1,6 @@
 # MX record with CRD source
 
-You can create and manage MX records with the help of [CRD source](../contributing/crd-source.md)
+You can create and manage MX records with the help of [CRD source](../sources/crd.md)
 and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, `google` and `digitalocean` providers.
 
 In order to start managing MX records you need to set the `--managed-record-types=MX` flag.
diff --git a/docs/sources/nodes.md b/docs/sources/nodes.md
index b3ede960..ac0d65a1 100644
--- a/docs/sources/nodes.md
+++ b/docs/sources/nodes.md
@@ -24,7 +24,7 @@ spec:
   serviceAccountName: external-dns
   containers:
   - name: external-dns
-    image: registry.k8s.io/external-dns/external-dns:v0.16.1 # update this to the desired external-dns version
+    image: registry.k8s.io/external-dns/external-dns:v0.17.0 # update this to the desired external-dns version
     args:
     - --source=node # will use nodes as source
     - --provider=aws
@@ -61,7 +61,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=node # will use nodes as source
         - --provider=aws
@@ -132,7 +132,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=node # will use nodes as source
         - --provider=aws
diff --git a/docs/sources/ns-record.md b/docs/sources/ns-record.md
index 9baba533..d842b032 100644
--- a/docs/sources/ns-record.md
+++ b/docs/sources/ns-record.md
@@ -1,6 +1,6 @@
 # NS record with CRD source
 
-You can create NS records with the help of [CRD source](../contributing/crd-source.md)
+You can create NS records with the help of [CRD source](../sources/crd.md)
 and `DNSEndpoint` CRD.
 
 In order to start managing NS records you need to set the `--managed-record-types=NS` flag.
diff --git a/docs/sources/openshift.md b/docs/sources/openshift.md
index 8d720aac..eb6885f6 100644
--- a/docs/sources/openshift.md
+++ b/docs/sources/openshift.md
@@ -72,7 +72,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=openshift-route
         - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
@@ -140,7 +140,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=openshift-route
         - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
diff --git a/docs/sources/traefik-proxy.md b/docs/sources/traefik-proxy.md
index a5079bdf..c842bc12 100644
--- a/docs/sources/traefik-proxy.md
+++ b/docs/sources/traefik-proxy.md
@@ -24,7 +24,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=traefik-proxy
         - --provider=aws
@@ -87,7 +87,7 @@ spec:
       containers:
       - name: external-dns
         # update this to the desired external-dns version
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=traefik-proxy
         - --provider=aws
diff --git a/docs/sources/txt-record.md b/docs/sources/txt-record.md
index 89ba458b..31f6aa9f 100644
--- a/docs/sources/txt-record.md
+++ b/docs/sources/txt-record.md
@@ -1,6 +1,6 @@
 # Creating TXT record with CRD source
 
-You can create and manage TXT records with the help of [CRD source](../contributing/crd-source.md)
+You can create and manage TXT records with the help of [CRD source](../sources/crd.md)
 and `DNSEndpoint` CRD. Currently, this feature is only supported by `digitalocean` providers.
 
 In order to start managing TXT records you need to set the `--managed-record-types=TXT` flag.
diff --git a/docs/tutorials/akamai-edgedns.md b/docs/tutorials/akamai-edgedns.md
index cd0faeff..9ffa6a21 100644
--- a/docs/tutorials/akamai-edgedns.md
+++ b/docs/tutorials/akamai-edgedns.md
@@ -104,7 +104,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service  # or ingress or both
         - --provider=akamai
@@ -190,7 +190,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service  # or ingress or both
         - --provider=akamai
diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md
index 445728d2..d70ed2b8 100644
--- a/docs/tutorials/alibabacloud.md
+++ b/docs/tutorials/alibabacloud.md
@@ -113,7 +113,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
@@ -187,7 +187,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
diff --git a/docs/tutorials/aws-public-private-route53.md b/docs/tutorials/aws-public-private-route53.md
index 90729bf3..79a5bdb1 100644
--- a/docs/tutorials/aws-public-private-route53.md
+++ b/docs/tutorials/aws-public-private-route53.md
@@ -242,7 +242,7 @@ spec:
         - --txt-owner-id=external-dns
         - --ingress-class=external-ingress
         - --aws-zone-type=public
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         name: external-dns-public
 ```
 
@@ -280,7 +280,7 @@ spec:
         - --txt-owner-id=dev.k8s.nexus
         - --ingress-class=internal-ingress
         - --aws-zone-type=private
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         name: external-dns-private
 ```
 
diff --git a/docs/tutorials/aws-sd.md b/docs/tutorials/aws-sd.md
index 8623904b..26375c8a 100644
--- a/docs/tutorials/aws-sd.md
+++ b/docs/tutorials/aws-sd.md
@@ -159,7 +159,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         env:
           - name: AWS_REGION
             value: us-east-1 # put your CloudMap NameSpace region
@@ -226,7 +226,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         env:
           - name: AWS_REGION
             value: us-east-1 # put your CloudMap NameSpace region
diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md
index 8c2c1f01..524f5c82 100644
--- a/docs/tutorials/aws.md
+++ b/docs/tutorials/aws.md
@@ -453,7 +453,7 @@ spec:
     spec:
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.16.1
+          image: registry.k8s.io/external-dns/external-dns:v0.17.0
           args:
             - --source=service
             - --source=ingress
@@ -999,7 +999,7 @@ A simple way to implement randomised startup is with an init container:
     spec:
       initContainers:
       - name: init-jitter
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         command:
         - /bin/sh
         - -c
diff --git a/docs/tutorials/azure-private-dns.md b/docs/tutorials/azure-private-dns.md
index 38ba121b..984b78d0 100644
--- a/docs/tutorials/azure-private-dns.md
+++ b/docs/tutorials/azure-private-dns.md
@@ -108,6 +108,7 @@ $ az role assignment create --role "Private DNS Zone Contributor" --assignee <ap
 ## Throttling
 
 When the ExternalDNS managed zones list doesn't change frequently, one can set `--azure-zones-cache-duration` (zones list cache time-to-live). The zones list cache is disabled by default, with a value of 0s.
+Also, one can leverage the built-in retry policies of the Azure SDK. The flag --azure-maxretries-count can be specified in the manifest yaml to configure behavior. The default value of Azure SDK retry is 3.
 
 ## Deploy ExternalDNS
 
@@ -143,7 +144,7 @@ spec:
     spec:
       containers:
       - name: externaldns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
@@ -151,6 +152,7 @@ spec:
         - --provider=azure-private-dns
         - --azure-resource-group=externaldns
         - --azure-subscription-id=<use the id of your subscription>
+        - --azure-maxretries-count=1  # (optional) specifies the maxRetires value to be used by the Azure SDK. Default is 3.
         volumeMounts:
         - name: azure-config-file
           mountPath: /etc/kubernetes
@@ -215,7 +217,7 @@ spec:
       serviceAccountName: externaldns
       containers:
       - name: externaldns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
@@ -223,6 +225,7 @@ spec:
         - --provider=azure-private-dns
         - --azure-resource-group=externaldns
         - --azure-subscription-id=<use the id of your subscription>
+        - --azure-maxretries-count=1  # (optional) specifies the maxRetires value to be used by the Azure SDK. Default is 3.
         volumeMounts:
         - name: azure-config-file
           mountPath: /etc/kubernetes
@@ -287,7 +290,7 @@ spec:
       serviceAccountName: externaldns
       containers:
       - name: externaldns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
@@ -295,6 +298,7 @@ spec:
         - --provider=azure-private-dns
         - --azure-resource-group=externaldns
         - --azure-subscription-id=<use the id of your subscription>
+        - --azure-maxretries-count=1  # (optional) specifies the maxRetires value to be used by the Azure SDK. Default is 3.
         volumeMounts:
         - name: azure-config-file
           mountPath: /etc/kubernetes
diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md
index df6d95ff..0e537622 100644
--- a/docs/tutorials/azure.md
+++ b/docs/tutorials/azure.md
@@ -493,6 +493,7 @@ NOTE: make sure the pod is restarted whenever you make a configuration change.
 ## Throttling
 
 When the ExternalDNS managed zones list doesn't change frequently, one can set `--azure-zones-cache-duration` (zones list cache time-to-live). The zones list cache is disabled by default, with a value of 0s.
+Also, one can leverage the built-in retry policies of the Azure SDK with a tunable maxRetries value. Environment variable AZURE_SDK_MAX_RETRIES can be specified in the manifest yaml to configure behavior. The defualt value of Azure SDK retry is 3.
 
 ## Ingress used with ExternalDNS
 
@@ -533,13 +534,14 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
         - --provider=azure
         - --azure-resource-group=MyDnsResourceGroup # (optional) use the DNS zones from the tutorial's resource group
+        - --azure-maxretries-count=1  # (optional) specifies the maxRetires value to be used by the Azure SDK. Default is 3.
         volumeMounts:
         - name: azure-config-file
           mountPath: /etc/kubernetes
@@ -601,7 +603,7 @@ spec:
       serviceAccountName: external-dns
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.16.1
+          image: registry.k8s.io/external-dns/external-dns:v0.17.0
           args:
             - --source=service
             - --source=ingress
@@ -609,6 +611,7 @@ spec:
             - --provider=azure
             - --azure-resource-group=MyDnsResourceGroup # (optional) use the DNS zones from the tutorial's resource group
             - --txt-prefix=externaldns-
+            - --azure-maxretries-count=1  # (optional) specifies the maxRetires value to be used by the Azure SDK. Default is 3.
           volumeMounts:
             - name: azure-config-file
               mountPath: /etc/kubernetes
@@ -673,13 +676,14 @@ spec:
       serviceAccountName: external-dns
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.16.1
+          image: registry.k8s.io/external-dns/external-dns:v0.17.0
           args:
             - --source=service
             - --source=ingress
             - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
             - --provider=azure
             - --azure-resource-group=MyDnsResourceGroup # (optional) use the DNS zones from the tutorial's resource group
+            - --azure-maxretries-count=1  # (optional) specifies the maxRetires value to be used by the Azure SDK. Default is 3.
           volumeMounts:
             - name: azure-config-file
               mountPath: /etc/kubernetes
diff --git a/docs/tutorials/civo.md b/docs/tutorials/civo.md
index 6fc723ed..024be720 100644
--- a/docs/tutorials/civo.md
+++ b/docs/tutorials/civo.md
@@ -40,7 +40,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -104,7 +104,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md
index 55509609..abfb95ee 100644
--- a/docs/tutorials/cloudflare.md
+++ b/docs/tutorials/cloudflare.md
@@ -120,7 +120,7 @@ spec:
     spec:
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.16.1
+          image: registry.k8s.io/external-dns/external-dns:v0.17.0
           args:
             - --source=service # ingress is also possible
             - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -129,6 +129,7 @@ spec:
             - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...)
             - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request
             - --cloudflare-region-key="eu" # (optional) configure which region can decrypt HTTPS requests
+            - --cloudflare-record-comment="provisioned by external-dns" # (optional) configure comments for provisioned records; <=100 chars for free zones; <=500 chars for paid zones
          env:
             - name: CF_API_KEY
               valueFrom:
@@ -196,7 +197,7 @@ spec:
       serviceAccountName: external-dns
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.16.1
+          image: registry.k8s.io/external-dns/external-dns:v0.17.0
           args:
             - --source=service # ingress is also possible
             - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -205,6 +206,7 @@ spec:
             - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...)
             - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request
             - --cloudflare-region-key="eu" # (optional) configure which region can decrypt HTTPS requests
+            - --cloudflare-record-comment="provisioned by external-dns" # (optional) configure comments for provisioned records; <=100 chars for free zones; <=500 chars for paid zones
           env:
             - name: CF_API_KEY
               valueFrom:
diff --git a/docs/tutorials/contour.md b/docs/tutorials/contour.md
index 8ad7e974..db3a7050 100644
--- a/docs/tutorials/contour.md
+++ b/docs/tutorials/contour.md
@@ -25,7 +25,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
@@ -95,7 +95,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md
index 8d8901d8..bd036721 100644
--- a/docs/tutorials/coredns.md
+++ b/docs/tutorials/coredns.md
@@ -133,7 +133,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=ingress
         - --provider=coredns
@@ -200,7 +200,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=ingress
         - --provider=coredns
diff --git a/docs/tutorials/digitalocean.md b/docs/tutorials/digitalocean.md
index b46f1e57..3ffbd17e 100644
--- a/docs/tutorials/digitalocean.md
+++ b/docs/tutorials/digitalocean.md
@@ -52,103 +52,13 @@ env:
 ### Manifest (for clusters without RBAC enabled)
 
 ```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: external-dns
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: external-dns
-  strategy:
-    type: Recreate
-  template:
-    metadata:
-      labels:
-        app: external-dns
-    spec:
-      containers:
-      - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
-        args:
-        - --source=service # ingress is also possible
-        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
-        - --provider=digitalocean
-        env:
-        - name: DO_TOKEN
-          valueFrom:
-            secretKeyRef:
-              name: DO_TOKEN
-              key: DO_TOKEN
+[[% include 'digitalocean/extdns-without-rbac.yaml' %]]
 ```
 
 ### Manifest (for clusters with RBAC enabled)
 
 ```yaml
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: external-dns
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  name: external-dns
-rules:
-- apiGroups: [""]
-  resources: ["services","endpoints","pods"]
-  verbs: ["get","watch","list"]
-- apiGroups: ["extensions","networking.k8s.io"]
-  resources: ["ingresses"]
-  verbs: ["get","watch","list"]
-- apiGroups: [""]
-  resources: ["nodes"]
-  verbs: ["list"]
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  name: external-dns-viewer
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: external-dns
-subjects:
-- kind: ServiceAccount
-  name: external-dns
-  namespace: default
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: external-dns
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: external-dns
-  strategy:
-    type: Recreate
-  template:
-    metadata:
-      labels:
-        app: external-dns
-    spec:
-      serviceAccountName: external-dns
-      containers:
-      - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
-        args:
-        - --source=service # ingress is also possible
-        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
-        - --provider=digitalocean
-        env:
-        - name: DO_TOKEN
-          valueFrom:
-            secretKeyRef:
-              name: DO_TOKEN
-              key: DO_TOKEN
+[[% include 'digitalocean/extdns-with-rbac.yaml' %]]
 ```
 
 ## Deploying an Nginx Service
@@ -156,40 +66,7 @@ spec:
 Create a service file called 'nginx.yaml' with the following contents:
 
 ```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: nginx
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: nginx
-  template:
-    metadata:
-      labels:
-        app: nginx
-    spec:
-      containers:
-      - image: nginx
-        name: nginx
-        ports:
-        - containerPort: 80
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: nginx
-  annotations:
-    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
-spec:
-  selector:
-    app: nginx
-  type: LoadBalancer
-  ports:
-    - protocol: TCP
-      port: 80
-      targetPort: 80
+[[% include 'digitalocean/deploy-nginx.yaml' %]]
 ```
 
 Note the annotation on the service; use the same hostname as the DigitalOcean DNS zone created above.
diff --git a/docs/tutorials/dnsimple.md b/docs/tutorials/dnsimple.md
index df85e789..dbc6ec83 100644
--- a/docs/tutorials/dnsimple.md
+++ b/docs/tutorials/dnsimple.md
@@ -40,7 +40,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple.
@@ -109,7 +109,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple.
diff --git a/docs/tutorials/exoscale.md b/docs/tutorials/exoscale.md
index eaf91642..ea80414a 100644
--- a/docs/tutorials/exoscale.md
+++ b/docs/tutorials/exoscale.md
@@ -21,36 +21,7 @@ Deploying external DNS for Exoscale is actually nearly identical to deploying
 it for other providers. This is what a sample `deployment.yaml` looks like:
 
 ```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: external-dns
-spec:
-  strategy:
-    type: Recreate
-  selector:
-    matchLabels:
-      app: external-dns
-  template:
-    metadata:
-      labels:
-        app: external-dns
-    spec:
-      # Only use if you're also using RBAC
-      # serviceAccountName: external-dns
-      containers:
-      - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
-        args:
-        - --source=ingress # or service or both
-        - --provider=exoscale
-        - --domain-filter={{ my-domain }}
-        - --policy=sync # if you want DNS entries to get deleted as well
-        - --txt-owner-id={{ owner-id-for-this-external-dns }}
-        - --exoscale-apikey={{ api-key}}
-        - --exoscale-apisecret={{ api-secret }}
-        # - --exoscale-apizone={{ api-zone }}
-        # - --exoscale-apienv={{ api-env }}
+[[% include 'exoscale/extdns.yaml' %]]
 ```
 
 Optional arguments `--exoscale-apizone` and `--exoscale-apienv` define [Exoscale API Zone](https://community.exoscale.com/documentation/platform/exoscale-datacenter-zones/)
@@ -61,43 +32,7 @@ Optional arguments `--exoscale-apizone` and `--exoscale-apienv` define [Exoscale
 If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns:
 
 ```yaml
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: external-dns
-  namespace: default
-
----
-
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  name: external-dns
-rules:
-- apiGroups: [""]
-  resources: ["services","endpoints","pods"]
-  verbs: ["get","watch","list"]
-- apiGroups: ["extensions","networking.k8s.io"]
-  resources: ["ingresses"]
-  verbs: ["get","watch","list"]
-- apiGroups: [""]
-  resources: ["nodes"]
-  verbs: ["list"]
-
----
-
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  name: external-dns-viewer
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: external-dns
-subjects:
-- kind: ServiceAccount
-  name: external-dns
-  namespace: default
+[[% include 'exoscale/rbac.yaml' %]]
 ```
 
 ## Testing and Verification
@@ -107,59 +42,7 @@ subjects:
 Spin up a simple nginx HTTP server with the following spec (`kubectl apply -f`):
 
 ```yaml
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
-  name: nginx
-  annotations:
-    external-dns.alpha.kubernetes.io/target: {{ Elastic-IP-address }}
-spec:
-  ingressClassName: nginx
-  rules:
-  - host: via-ingress.example.com
-    http:
-      paths:
-      - backend:
-          service:
-            name: "nginx"
-            port:
-              number: 80
-        path: /
-        pathType: Prefix
-
----
-
-apiVersion: v1
-kind: Service
-metadata:
-  name: nginx
-spec:
-  ports:
-  - port: 80
-    targetPort: 80
-  selector:
-    app: nginx
-
----
-
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: nginx
-spec:
-  selector:
-    matchLabels:
-      app: nginx
-  template:
-    metadata:
-      labels:
-        app: nginx
-    spec:
-      containers:
-      - image: nginx
-        name: nginx
-        ports:
-        - containerPort: 80
+[[% include 'exoscale/how-to-test.yaml' %]]
 ```
 
 **Important!**: Don't run dig, nslookup or similar immediately (until you've
diff --git a/docs/tutorials/externalname.md b/docs/tutorials/externalname.md
index bca73743..82e55c37 100644
--- a/docs/tutorials/externalname.md
+++ b/docs/tutorials/externalname.md
@@ -28,7 +28,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --log-level=debug
         - --source=service
diff --git a/docs/tutorials/gandi.md b/docs/tutorials/gandi.md
index 83337dc2..368be5ce 100644
--- a/docs/tutorials/gandi.md
+++ b/docs/tutorials/gandi.md
@@ -42,7 +42,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -107,7 +107,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/gke-nginx.md b/docs/tutorials/gke-nginx.md
index 5f043d8e..1f60386e 100644
--- a/docs/tutorials/gke-nginx.md
+++ b/docs/tutorials/gke-nginx.md
@@ -278,7 +278,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=ingress
         - --domain-filter=external-dns-test.gcp.zalan.do
@@ -573,7 +573,7 @@ spec:
             - --google-project=zalando-external-dns-test
             - --registry=txt
             - --txt-owner-id=my-identifier
-          image: registry.k8s.io/external-dns/external-dns:v0.16.1
+          image: registry.k8s.io/external-dns/external-dns:v0.17.0
           name: external-dns
       securityContext:
         fsGroup: 65534
diff --git a/docs/tutorials/gke.md b/docs/tutorials/gke.md
index a04b8639..4af49c26 100644
--- a/docs/tutorials/gke.md
+++ b/docs/tutorials/gke.md
@@ -382,7 +382,7 @@ spec:
       serviceAccountName: external-dns
       containers:
         - name: external-dns
-          image: registry.k8s.io/external-dns/external-dns:v0.16.1
+          image: registry.k8s.io/external-dns/external-dns:v0.17.0
           args:
             - --source=service
             - --source=ingress
diff --git a/docs/tutorials/godaddy.md b/docs/tutorials/godaddy.md
index c35e8618..36ac5d9e 100644
--- a/docs/tutorials/godaddy.md
+++ b/docs/tutorials/godaddy.md
@@ -64,7 +64,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -135,7 +135,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/hostport.md b/docs/tutorials/hostport.md
index 5566fc5d..2c8d71d3 100644
--- a/docs/tutorials/hostport.md
+++ b/docs/tutorials/hostport.md
@@ -35,7 +35,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --log-level=debug
         - --source=service
@@ -101,7 +101,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --log-level=debug
         - --source=service
diff --git a/docs/tutorials/ibmcloud.md b/docs/tutorials/ibmcloud.md
index 83e3047b..6d303365 100644
--- a/docs/tutorials/ibmcloud.md
+++ b/docs/tutorials/ibmcloud.md
@@ -77,7 +77,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -150,7 +150,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/linode.md b/docs/tutorials/linode.md
index 33242904..4c7be710 100644
--- a/docs/tutorials/linode.md
+++ b/docs/tutorials/linode.md
@@ -41,7 +41,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -105,7 +105,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/ns1.md b/docs/tutorials/ns1.md
index 62434e14..c32c7c98 100644
--- a/docs/tutorials/ns1.md
+++ b/docs/tutorials/ns1.md
@@ -93,7 +93,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -160,7 +160,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/oracle.md b/docs/tutorials/oracle.md
index 7124695b..497a3dab 100644
--- a/docs/tutorials/oracle.md
+++ b/docs/tutorials/oracle.md
@@ -170,7 +170,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
diff --git a/docs/tutorials/ovh.md b/docs/tutorials/ovh.md
index 9f61d902..c53d8d4e 100644
--- a/docs/tutorials/ovh.md
+++ b/docs/tutorials/ovh.md
@@ -97,7 +97,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -171,7 +171,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/pdns.md b/docs/tutorials/pdns.md
index 8d31424c..8a8cc64a 100644
--- a/docs/tutorials/pdns.md
+++ b/docs/tutorials/pdns.md
@@ -42,7 +42,7 @@ spec:
       # serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # or ingress or both
         - --provider=pdns
diff --git a/docs/tutorials/pihole.md b/docs/tutorials/pihole.md
index adf493e4..5c8400bc 100644
--- a/docs/tutorials/pihole.md
+++ b/docs/tutorials/pihole.md
@@ -84,7 +84,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         # If authentication is disabled and/or you didn't create
         # a secret, you can remove this block.
         envFrom:
diff --git a/docs/tutorials/plural.md b/docs/tutorials/plural.md
index 7a270736..b381d3d0 100644
--- a/docs/tutorials/plural.md
+++ b/docs/tutorials/plural.md
@@ -61,7 +61,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -131,7 +131,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/rfc2136.md b/docs/tutorials/rfc2136.md
index dd95ea63..d7fdc0b4 100644
--- a/docs/tutorials/rfc2136.md
+++ b/docs/tutorials/rfc2136.md
@@ -257,7 +257,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --registry=txt
         - --txt-prefix=external-dns-
@@ -301,7 +301,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --registry=txt
         - --txt-prefix=external-dns-
diff --git a/docs/tutorials/scaleway.md b/docs/tutorials/scaleway.md
index abf0bea8..366a3ca4 100644
--- a/docs/tutorials/scaleway.md
+++ b/docs/tutorials/scaleway.md
@@ -65,7 +65,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
@@ -146,7 +146,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
diff --git a/docs/tutorials/security-context.md b/docs/tutorials/security-context.md
index 01293bb8..8c25a435 100644
--- a/docs/tutorials/security-context.md
+++ b/docs/tutorials/security-context.md
@@ -3,30 +3,5 @@
 You can run ExternalDNS with reduced privileges since `v0.5.6` using the following `SecurityContext`.
 
 ```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: external-dns
-spec:
-  strategy:
-    type: Recreate
-  selector:
-    matchLabels:
-      app: external-dns
-  template:
-    metadata:
-      labels:
-        app: external-dns
-    spec:
-      containers:
-      - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
-        args:
-        - ... # your arguments here
-        securityContext:
-          runAsNonRoot: true
-          runAsUser: 65534
-          readOnlyRootFilesystem: true
-          capabilities:
-            drop: ["ALL"]
+[[% include 'security-context/limited-privilege.yaml' %]]
 ```
diff --git a/docs/tutorials/tencentcloud.md b/docs/tutorials/tencentcloud.md
index 6b25927a..60726c02 100644
--- a/docs/tutorials/tencentcloud.md
+++ b/docs/tutorials/tencentcloud.md
@@ -133,7 +133,7 @@ spec:
         - --policy=sync # set `upsert-only` would prevent ExternalDNS from deleting any records
         - --tencent-cloud-zone-type=private # only look at private hosted zones. set `public` to use the public dns service.
         - --tencent-cloud-config-file=/etc/kubernetes/tencent-cloud.json
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         imagePullPolicy: Always
         name: external-dns
         resources: {}
diff --git a/docs/tutorials/transip.md b/docs/tutorials/transip.md
index f5b50ecf..82be662d 100644
--- a/docs/tutorials/transip.md
+++ b/docs/tutorials/transip.md
@@ -37,7 +37,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains
@@ -108,7 +108,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service # ingress is also possible
         - --domain-filter=example.com # (optional) limit to only example.com domains
diff --git a/docs/tutorials/ultradns.md b/docs/tutorials/ultradns.md
index bc24fa42..b83d1531 100644
--- a/docs/tutorials/ultradns.md
+++ b/docs/tutorials/ultradns.md
@@ -45,7 +45,7 @@ spec:
     spec:
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress # ingress is also possible
@@ -117,7 +117,7 @@ spec:
       serviceAccountName: external-dns
       containers:
       - name: external-dns
-        image: registry.k8s.io/external-dns/external-dns:v0.16.1
+        image: registry.k8s.io/external-dns/external-dns:v0.17.0
         args:
         - --source=service
         - --source=ingress
diff --git a/go.mod b/go.mod
index 15623faa..cb505369 100644
--- a/go.mod
+++ b/go.mod
@@ -3,15 +3,15 @@ module sigs.k8s.io/external-dns
 go 1.24.2
 
 require (
-	cloud.google.com/go/compute/metadata v0.6.0
+	cloud.google.com/go/compute/metadata v0.7.0
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0
-	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.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.19.1
 	github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.7.2
 	github.com/IBM/go-sdk-core/v5 v5.19.1
-	github.com/IBM/networking-go-sdk v0.51.4
+	github.com/IBM/networking-go-sdk v0.51.5
 	github.com/Yamashou/gqlgenc v0.32.1
 	github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2
 	github.com/alecthomas/kingpin/v2 v2.4.0
@@ -26,12 +26,12 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sts v1.33.19
 	github.com/bodgit/tsig v1.2.2
 	github.com/cenkalti/backoff/v5 v5.0.2
-	github.com/civo/civogo v0.4.1
+	github.com/civo/civogo v0.5.1
 	github.com/cloudflare/cloudflare-go v0.115.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.146.0
+	github.com/digitalocean/godo v1.148.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
@@ -47,34 +47,34 @@ require (
 	github.com/onsi/ginkgo v1.16.5
 	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.90.0
+	github.com/oracle/oci-go-sdk/v65 v65.91.0
 	github.com/ovh/go-ovh v1.7.0
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/pluralsh/gqlclient v1.12.2
-	github.com/projectcontour/contour v1.31.0
+	github.com/projectcontour/contour v1.32.0
 	github.com/prometheus/client_golang v1.22.0
 	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33
 	github.com/sirupsen/logrus v1.9.3
 	github.com/stretchr/testify v1.10.0
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1162
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1165
 	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1145
 	github.com/transip/gotransip/v6 v6.26.0
 	github.com/ultradns/ultradns-sdk-go v1.3.7
-	go.etcd.io/etcd/client/v3 v3.5.21
+	go.etcd.io/etcd/client/v3 v3.6.0
 	go.uber.org/ratelimit v0.3.1
 	golang.org/x/net v0.40.0
 	golang.org/x/oauth2 v0.30.0
 	golang.org/x/sync v0.14.0
 	golang.org/x/text v0.25.0
 	golang.org/x/time v0.11.0
-	google.golang.org/api v0.232.0
+	google.golang.org/api v0.233.0
 	gopkg.in/ns1/ns1-go.v2 v2.14.3
 	istio.io/api v1.26.0
 	istio.io/client-go v1.26.0
-	k8s.io/api v0.33.0
-	k8s.io/apimachinery v0.33.0
-	k8s.io/client-go v0.33.0
+	k8s.io/api v0.33.1
+	k8s.io/apimachinery v0.33.1
+	k8s.io/client-go v0.33.1
 	k8s.io/klog/v2 v2.130.1
 	sigs.k8s.io/gateway-api v1.3.0
 )
@@ -136,6 +136,7 @@ require (
 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
 	github.com/googleapis/gax-go/v2 v2.14.1 // indirect
 	github.com/gopherjs/gopherjs v1.17.2 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -170,7 +171,7 @@ require (
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.63.0 // indirect
+	github.com/prometheus/common v0.64.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/schollz/progressbar/v3 v3.8.6 // indirect
@@ -184,8 +185,8 @@ require (
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
 	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
-	go.etcd.io/etcd/api/v3 v3.5.21 // indirect
-	go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
+	go.etcd.io/etcd/api/v3 v3.6.0 // indirect
+	go.etcd.io/etcd/client/pkg/v3 v3.6.0 // indirect
 	go.mongodb.org/mongo-driver v1.17.2 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
@@ -201,8 +202,8 @@ require (
 	golang.org/x/term v0.32.0 // indirect
 	golang.org/x/tools v0.32.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/grpc v1.72.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect
+	google.golang.org/grpc v1.72.1 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
diff --git a/go.sum b/go.sum
index 50bdfba5..a31ea75b 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
 cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
 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.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
-cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+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=
 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=
@@ -18,8 +18,8 @@ github.com/Azure/azure-sdk-for-go v16.2.1+incompatible h1:KnPIugL51v3N3WwvaSmZbx
 github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 h1:j8BorDEigD8UFOSZQiSqAMOOleyQOOQPnUAwV+Ls1gA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
 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=
@@ -55,8 +55,8 @@ github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.7.2 h1:eW5o8NpblAyqPjwOlZ+XISdhlYynjf7
 github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.7.2/go.mod h1:HulyrJLLc9FSZlwKQ9vu5Jq83thNlUfg1afonOdhrRA=
 github.com/IBM/go-sdk-core/v5 v5.19.1 h1:sleVks1O4XjgF4YEGvyDh6PZbP6iZhlTPeDkQc8nWDs=
 github.com/IBM/go-sdk-core/v5 v5.19.1/go.mod h1:Q3BYO6iDA2zweQPDGbNTtqft5tDcEpm6RTuqMlPcvbw=
-github.com/IBM/networking-go-sdk v0.51.4 h1:rkbR+gUkksLKjNYL5YEWEAMv3qddR0mUkxObDMa4l/s=
-github.com/IBM/networking-go-sdk v0.51.4/go.mod h1:gjCFEp+UVP7FUlcq2C1RaoZAXFcD39CQdlUk7uVKko4=
+github.com/IBM/networking-go-sdk v0.51.5 h1:75lKAx17y++hirXK5GcEM23mTRhHnhsv6gmhz70ex1Q=
+github.com/IBM/networking-go-sdk v0.51.5/go.mod h1:wyEnRnBnROgGmSn5UrryycIrbBujHKXf0PmI1NSwcjY=
 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/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
@@ -188,8 +188,8 @@ 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.4.1 h1:C+lwZ7hBqKy6eKy6qgviuselF0V5Z/um0x7X/eLEQ64=
-github.com/civo/civogo v0.4.1/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
+github.com/civo/civogo v0.5.1 h1:WH6v+47uJybpJOtf+/Jopjp2R5lLwi4/PANp714d+zc=
+github.com/civo/civogo v0.5.1/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=
@@ -258,8 +258,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/digitalocean/godo v1.146.0 h1:0CDU1/vxvlvMFjIOk1UfC8Ku6pwfZFOM1Uf/ZEke4oc=
-github.com/digitalocean/godo v1.146.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM=
+github.com/digitalocean/godo v1.148.0 h1:th91q+6bZY+Slgs9eZxBupa2+aUUYn1qT7gPICFmtPA=
+github.com/digitalocean/godo v1.148.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM=
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
 github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY=
 github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8=
@@ -556,6 +556,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
 github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
@@ -848,8 +850,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.90.0 h1:XGoPkq0oThnIzmreyWOG3PkP3r1I1JrEWxOlGocJXt0=
-github.com/oracle/oci-go-sdk/v65 v65.90.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
+github.com/oracle/oci-go-sdk/v65 v65.91.0 h1:maO6AxKxVfszH0X4tbbtN21jOk03lCRR3IqiA8/FzZc=
+github.com/oracle/oci-go-sdk/v65 v65.91.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
 github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw=
 github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
 github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso=
@@ -884,8 +886,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.31.0 h1:y6PNQTqPAa0mInTld50bxdDuf69PZmxCP3RobfjiSNk=
-github.com/projectcontour/contour v1.31.0/go.mod h1:nigS15X+bsL/vYLYe7vBzUriIKjR8XrOeQAKqXypWZg=
+github.com/projectcontour/contour v1.32.0 h1:mOy8JEa6im5FOToJJugVVJYVcNp/mkb72H3ymdTg1PM=
+github.com/projectcontour/contour v1.32.0/go.mod h1:e7mw7MqbQeyv+PYnD7CbdRTtQbBfvGYyTrEvmwSKVnk=
 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=
@@ -911,8 +913,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
-github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
-github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
+github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
+github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -925,8 +927,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
-github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
+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/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@@ -1019,12 +1021,12 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1136/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1145/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1162 h1:bscCBygP9JRl6iNabF+vmBOhY+xayFFGYV5Wa0NzH0A=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1162/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 h1:kMIdSU5IvpOROh27ToVQ3hlm6ym3lCRs9tnGCOBoZqk=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136/go.mod h1:FpyIz3mymKaExVs6Fz27kxDBS42jqZn7vbACtxdeEH4=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1165/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166 h1:WgdCsNxde/flDGxOy+dXZqm2wrUQA/4kzaaY69XC/2A=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1165 h1:bHT5sLou90UwrVm7Km2nQ7hyBk0+LK4xi7JOZXoDQ2c=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1165/go.mod h1:6pN5Nh0PuYJ+XHkjiV7SQqXgc1gecB9FHd456W9ZNdE=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1145 h1:K5N0Uxqm9kM7KU6DFBekCTKbldlXq6UD1ekOyXn4zEc=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.1145/go.mod h1:mgwxarWzLxIylWtHmkoMg0dX8aqhO5lwfrHkK4IGLKE=
 github.com/terra-farm/udnssdk v1.3.5 h1:MNR3adfuuEK/l04+jzo8WW/0fnorY+nW515qb3vEr6I=
@@ -1081,12 +1083,12 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
-go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
-go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
-go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
-go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
-go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
-go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
+go.etcd.io/etcd/api/v3 v3.6.0 h1:vdbkcUBGLf1vfopoGE/uS3Nv0KPyIpUV/HM6w9yx2kM=
+go.etcd.io/etcd/api/v3 v3.6.0/go.mod h1:Wt5yZqEmxgTNJGHob7mTVBJDZNXiHPtXTcPab37iFOw=
+go.etcd.io/etcd/client/pkg/v3 v3.6.0 h1:nchnPqpuxvv3UuGGHaz0DQKYi5EIW5wOYsgUNRc365k=
+go.etcd.io/etcd/client/pkg/v3 v3.6.0/go.mod h1:Jv5SFWMnGvIBn8o3OaBq/PnT0jjsX8iNokAUessNjoA=
+go.etcd.io/etcd/client/v3 v3.6.0 h1:/yjKzD+HW5v/3DVj9tpwFxzNbu8hjcKID183ug9duWk=
+go.etcd.io/etcd/client/v3 v3.6.0/go.mod h1:Jzk/Knqe06pkOZPHXsQ0+vNDvMQrgIqJ0W8DwPdMJMg=
 go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
@@ -1383,8 +1385,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.232.0 h1:qGnmaIMf7KcuwHOlF3mERVzChloDYwRfOJOrHt8YC3I=
-google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY=
+google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI=
+google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c=
 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=
@@ -1400,8 +1402,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
 google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
 google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -1415,8 +1417,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.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
-google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
+google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
 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=
@@ -1490,16 +1492,16 @@ istio.io/client-go v1.26.0/go.mod h1:1nRkn8XmI8AZVKElaLsC79jrVqS+PPIu1EczNvmGst8
 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.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
-k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
+k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
+k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
 k8s.io/apiextensions-apiserver v0.18.0/go.mod h1:18Cwn1Xws4xnWQNC00FLq1E350b9lUF+aOdIWDOZxgo=
 k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY=
 k8s.io/apiextensions-apiserver v0.18.4/go.mod h1:NYeyeYq4SIpFlPxSAB6jHPIdvu3hL0pc36wuRChybio=
 k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
 k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
 k8s.io/apimachinery v0.18.4/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
-k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
-k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
+k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
+k8s.io/apimachinery v0.33.1/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=
@@ -1508,8 +1510,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.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
-k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
+k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
+k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
 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/mkdocs.yml b/mkdocs.yml
index ed0bbd5e..4bc0c173 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -35,7 +35,7 @@ nav:
     - FQDN Templating: docs/advanced/fqdn-templating.md
   - Contributing:
       - Kubernetes Contributions: CONTRIBUTING.md
-      - Release: docs/release.
+      - Release: docs/release.md
       - Deprecation Policy: docs/deprecation.md
       - docs/contributing/*
 
@@ -105,3 +105,14 @@ plugins:
   - git-revision-date-localized:
       type: date
       fallback_to_build_date: true
+  # https://mkdocs-macros-plugin.readthedocs.io/en/latest/
+  - macros:
+      include_dir: docs/snippets
+      # required, as default jinja markers are {{ and }}
+      # ref: https://mkdocs-macros-plugin.readthedocs.io/en/latest/rendering/#solution-5-altering-the-syntax-of-jinja2-for-mkdocs-macros
+      j2_block_start_string: '[[%'
+      j2_block_end_string: '%]]'
+      j2_variable_start_string: '[['
+      j2_variable_end_string: ']]'
+      j2_comment_start_string: '[#'
+      j2_comment_end_string: '#]'
diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go
index b8e8d5c9..fefdfaf7 100644
--- a/pkg/apis/externaldns/types.go
+++ b/pkg/apis/externaldns/types.go
@@ -106,12 +106,15 @@ type Config struct {
 	AzureUserAssignedIdentityClientID             string
 	AzureActiveDirectoryAuthorityHost             string
 	AzureZonesCacheDuration                       time.Duration
+	AzureMaxRetriesCount                          int
 	CloudflareProxied                             bool
 	CloudflareCustomHostnames                     bool
+	CloudflareDNSRecordsPerPage                   int
+	CloudflareDNSRecordsComment                   string
 	CloudflareCustomHostnamesMinTLSVersion        string
 	CloudflareCustomHostnamesCertificateAuthority string
-	CloudflareDNSRecordsPerPage                   int
 	CloudflareRegionKey                           string
+	CloudflareRecordComment                       string
 	CoreDNSPrefix                                 string
 	AkamaiServiceConsumerDomain                   string
 	AkamaiClientToken                             string
@@ -247,6 +250,7 @@ var defaultConfig = &Config{
 	AzureResourceGroup:          "",
 	AzureSubscriptionID:         "",
 	AzureZonesCacheDuration:     0 * time.Second,
+	AzureMaxRetriesCount:        3,
 	CFAPIEndpoint:               "",
 	CFPassword:                  "",
 	CFUsername:                  "",
@@ -456,7 +460,7 @@ func App(cfg *Config) *kingpin.Application {
 	// Flags related to processing source
 	app.Flag("always-publish-not-ready-addresses", "Always publish also not ready addresses for headless services (optional)").BoolVar(&cfg.AlwaysPublishNotReadyAddresses)
 	app.Flag("annotation-filter", "Filter resources queried for endpoints by annotation, using label selector semantics").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
-	app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation)
+	app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting (default: false)").BoolVar(&cfg.CombineFQDNAndAnnotation)
 	app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule, kops-dns-controller)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule", "kops-dns-controller")
 	app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer)
 	app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion)
@@ -527,6 +531,7 @@ func App(cfg *Config) *kingpin.Application {
 	app.Flag("azure-subscription-id", "When using the Azure provider, override the Azure subscription to use (optional)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID)
 	app.Flag("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)").Default("").StringVar(&cfg.AzureUserAssignedIdentityClientID)
 	app.Flag("azure-zones-cache-duration", "When using the Azure provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AzureZonesCacheDuration.String()).DurationVar(&cfg.AzureZonesCacheDuration)
+	app.Flag("azure-maxretries-count", "When using the Azure provider, set the number of retries for API calls (When less than 0, it disables retries). (optional)").Default(strconv.Itoa(defaultConfig.AzureMaxRetriesCount)).IntVar(&cfg.AzureMaxRetriesCount)
 	app.Flag("tencent-cloud-config-file", "When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud)").Default(defaultConfig.TencentCloudConfigFile).StringVar(&cfg.TencentCloudConfigFile)
 	app.Flag("tencent-cloud-zone-type", "When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private)").Default(defaultConfig.TencentCloudZoneType).EnumVar(&cfg.TencentCloudZoneType, "", "public", "private")
 
@@ -536,6 +541,8 @@ func App(cfg *Config) *kingpin.Application {
 	app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Cerrtificate Authority will be used by default. (default: google, options: google, ssl_com, lets_encrypt)").Default("google").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt")
 	app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage)
 	app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the region (default: earth)").StringVar(&cfg.CloudflareRegionKey)
+	app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment)
+
 	app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)
 	app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain)
 	app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
diff --git a/pkg/tlsutils/tlsconfig.go b/pkg/tlsutils/tlsconfig.go
index 5275fad1..e6e4373b 100644
--- a/pkg/tlsutils/tlsconfig.go
+++ b/pkg/tlsutils/tlsconfig.go
@@ -35,14 +35,10 @@ func CreateTLSConfig(prefix string) (*tls.Config, error) {
 	serverName := os.Getenv(fmt.Sprintf("%s_TLS_SERVER_NAME", prefix))
 	isInsecureStr := strings.ToLower(os.Getenv(fmt.Sprintf("%s_TLS_INSECURE", prefix)))
 	isInsecure := isInsecureStr == "true" || isInsecureStr == "yes" || isInsecureStr == "1"
-	tlsConfig, err := NewTLSConfig(certFile, keyFile, caFile, serverName, isInsecure, defaultMinVersion)
-	if err != nil {
-		return nil, err
-	}
-	return tlsConfig, nil
+	return NewTLSConfig(certFile, keyFile, caFile, serverName, isInsecure, defaultMinVersion)
 }
 
-// NewTLSConfig creates a tls.Config instance from directly-passed parameters, loading the ca, cert, and key from disk
+// NewTLSConfig creates a tls.Config instance from directly passed parameters, loading the ca, cert, and key from disk
 func NewTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool, minVersion uint16) (*tls.Config, error) {
 	if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
 		return nil, errors.New("either both cert and key or none must be provided")
@@ -55,15 +51,21 @@ func NewTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool, m
 		}
 		certificates = append(certificates, cert)
 	}
-	roots, err := loadRoots(caPath)
-	if err != nil {
-		return nil, err
+	// If rootCAs is nil, TLS uses the host's root CA set.
+	var rootCAs *x509.CertPool
+	var err error
+
+	if caPath != "" {
+		rootCAs, err = loadRoots(caPath)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	return &tls.Config{
 		MinVersion:         minVersion,
 		Certificates:       certificates,
-		RootCAs:            roots,
+		RootCAs:            rootCAs,
 		InsecureSkipVerify: insecure,
 		ServerName:         serverName,
 	}, nil
@@ -71,10 +73,6 @@ func NewTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool, m
 
 // loads CA cert
 func loadRoots(caPath string) (*x509.CertPool, error) {
-	if caPath == "" {
-		return nil, nil
-	}
-
 	roots := x509.NewCertPool()
 	pem, err := os.ReadFile(caPath)
 	if err != nil {
diff --git a/provider/alibabacloud/alibaba_cloud.go b/provider/alibabacloud/alibaba_cloud.go
index 16318385..9ff690c3 100644
--- a/provider/alibabacloud/alibaba_cloud.go
+++ b/provider/alibabacloud/alibaba_cloud.go
@@ -85,13 +85,13 @@ type AlibabaCloudProvider struct {
 }
 
 type alibabaCloudConfig struct {
-	RegionID        string    `json:"regionId" yaml:"regionId"`
-	AccessKeyID     string    `json:"accessKeyId" yaml:"accessKeyId"`
+	RegionID        string    `json:"regionId"        yaml:"regionId"`
+	AccessKeyID     string    `json:"accessKeyId"     yaml:"accessKeyId"`
 	AccessKeySecret string    `json:"accessKeySecret" yaml:"accessKeySecret"`
-	VPCID           string    `json:"vpcId" yaml:"vpcId"`
-	RoleName        string    `json:"-" yaml:"-"` // For ECS RAM role only
-	StsToken        string    `json:"-" yaml:"-"`
-	ExpireTime      time.Time `json:"-" yaml:"-"`
+	VPCID           string    `json:"vpcId"           yaml:"vpcId"`
+	RoleName        string    `json:"-"               yaml:"-"` // For ECS RAM role only
+	StsToken        string    `json:"-"               yaml:"-"`
+	ExpireTime      time.Time `json:"-"               yaml:"-"`
 }
 
 // NewAlibabaCloudProvider creates a new Alibaba Cloud provider.
diff --git a/provider/azure/azure.go b/provider/azure/azure.go
index c71e638e..0d68fe47 100644
--- a/provider/azure/azure.go
+++ b/provider/azure/azure.go
@@ -63,17 +63,19 @@ type AzureProvider struct {
 	zonesClient                  ZonesClient
 	zonesCache                   *zonesCache[dns.Zone]
 	recordSetsClient             RecordSetsClient
+	maxRetriesCount              int
 }
 
 // NewAzureProvider creates a new Azure provider.
 //
 // Returns the provider or an error if a provider could not be created.
-func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, dryRun bool) (*AzureProvider, error) {
+func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, maxRetriesCount int, dryRun bool) (*AzureProvider, error) {
 	cfg, err := getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
 	}
-	cred, clientOpts, err := getCredentials(*cfg)
+
+	cred, clientOpts, err := getCredentials(*cfg, maxRetriesCount)
 	if err != nil {
 		return nil, fmt.Errorf("failed to get credentials: %w", err)
 	}
@@ -97,6 +99,7 @@ func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zon
 		zonesClient:                  zonesClient,
 		zonesCache:                   &zonesCache[dns.Zone]{duration: zonesCacheDuration},
 		recordSetsClient:             recordSetsClient,
+		maxRetriesCount:              maxRetriesCount,
 	}, nil
 }
 
diff --git a/provider/azure/azure_private_dns.go b/provider/azure/azure_private_dns.go
index d31fa20a..c199d95a 100644
--- a/provider/azure/azure_private_dns.go
+++ b/provider/azure/azure_private_dns.go
@@ -58,17 +58,19 @@ type AzurePrivateDNSProvider struct {
 	zonesClient                  PrivateZonesClient
 	zonesCache                   *zonesCache[privatedns.PrivateZone]
 	recordSetsClient             PrivateRecordSetsClient
+	maxRetriesCount              int
 }
 
 // NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
 //
 // Returns the provider or an error if a provider could not be created.
-func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, dryRun bool) (*AzurePrivateDNSProvider, error) {
+func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, maxRetriesCount int, dryRun bool) (*AzurePrivateDNSProvider, error) {
 	cfg, err := getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
 	}
-	cred, clientOpts, err := getCredentials(*cfg)
+
+	cred, clientOpts, err := getCredentials(*cfg, maxRetriesCount)
 	if err != nil {
 		return nil, fmt.Errorf("failed to get credentials: %w", err)
 	}
@@ -92,6 +94,7 @@ func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainF
 		zonesClient:                  zonesClient,
 		zonesCache:                   &zonesCache[privatedns.PrivateZone]{duration: zonesCacheDuration},
 		recordSetsClient:             recordSetsClient,
+		maxRetriesCount:              maxRetriesCount,
 	}, nil
 }
 
diff --git a/provider/azure/config.go b/provider/azure/config.go
index ac24f0b4..e64f3857 100644
--- a/provider/azure/config.go
+++ b/provider/azure/config.go
@@ -17,30 +17,34 @@ limitations under the License.
 package azure
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
+	"net/http"
 	"os"
 	"strings"
 
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
 	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+	"github.com/google/uuid"
 	log "github.com/sirupsen/logrus"
 )
 
 // config represents common config items for Azure DNS and Azure Private DNS
 type config struct {
-	Cloud                        string `json:"cloud" yaml:"cloud"`
-	TenantID                     string `json:"tenantId" yaml:"tenantId"`
-	SubscriptionID               string `json:"subscriptionId" yaml:"subscriptionId"`
-	ResourceGroup                string `json:"resourceGroup" yaml:"resourceGroup"`
-	Location                     string `json:"location" yaml:"location"`
-	ClientID                     string `json:"aadClientId" yaml:"aadClientId"`
-	ClientSecret                 string `json:"aadClientSecret" yaml:"aadClientSecret"`
-	UseManagedIdentityExtension  bool   `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
+	Cloud                        string `json:"cloud"                        yaml:"cloud"`
+	TenantID                     string `json:"tenantId"                     yaml:"tenantId"`
+	SubscriptionID               string `json:"subscriptionId"               yaml:"subscriptionId"`
+	ResourceGroup                string `json:"resourceGroup"                yaml:"resourceGroup"`
+	Location                     string `json:"location"                     yaml:"location"`
+	ClientID                     string `json:"aadClientId"                  yaml:"aadClientId"`
+	ClientSecret                 string `json:"aadClientSecret"              yaml:"aadClientSecret"`
+	UseManagedIdentityExtension  bool   `json:"useManagedIdentityExtension"  yaml:"useManagedIdentityExtension"`
 	UseWorkloadIdentityExtension bool   `json:"useWorkloadIdentityExtension" yaml:"useWorkloadIdentityExtension"`
-	UserAssignedIdentityID       string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
+	UserAssignedIdentityID       string `json:"userAssignedIdentityID"       yaml:"userAssignedIdentityID"`
 	ActiveDirectoryAuthorityHost string `json:"activeDirectoryAuthorityHost" yaml:"activeDirectoryAuthorityHost"`
 }
 
@@ -72,15 +76,57 @@ func getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityCl
 	return cfg, nil
 }
 
+// ctxKey is a type for context keys
+// This is used to avoid collisions with other packages that may use the same key in the context.
+type ctxKey string
+
+const (
+	// Context key for request ID
+	clientRequestIDKey ctxKey = "client-request-id"
+	// Azure API Headers
+	msRequestIDHeader          = "x-ms-request-id"
+	msCorrelationRequestHeader = "x-ms-correlation-request-id"
+	msClientRequestIDHeader    = "x-ms-client-request-id"
+)
+
+// customHeaderPolicy adds UUID to request headers
+type customHeaderPolicy struct{}
+
+func (p *customHeaderPolicy) Do(req *policy.Request) (*http.Response, error) {
+	id := req.Raw().Header.Get(msClientRequestIDHeader)
+	if id == "" {
+		id = uuid.New().String()
+		req.Raw().Header.Set(msClientRequestIDHeader, id)
+		newCtx := context.WithValue(req.Raw().Context(), clientRequestIDKey, id)
+		*req.Raw() = *req.Raw().WithContext(newCtx)
+	}
+	return req.Next()
+}
+func CustomHeaderPolicynew() policy.Policy { return &customHeaderPolicy{} }
+
 // getCredentials retrieves Azure API credentials.
-func getCredentials(cfg config) (azcore.TokenCredential, *arm.ClientOptions, error) {
+func getCredentials(cfg config, maxRetries int) (azcore.TokenCredential, *arm.ClientOptions, error) {
 	cloudCfg, err := getCloudConfiguration(cfg.Cloud)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to get cloud configuration: %w", err)
 	}
 	clientOpts := azcore.ClientOptions{
 		Cloud: cloudCfg,
+		Retry: policy.RetryOptions{
+			MaxRetries: int32(maxRetries),
+		},
+		Logging: policy.LogOptions{
+			AllowedHeaders: []string{
+				msRequestIDHeader,
+				msCorrelationRequestHeader,
+				msClientRequestIDHeader,
+			},
+		},
+		PerCallPolicies: []policy.Policy{
+			CustomHeaderPolicynew(),
+		},
 	}
+	log.Debugf("Configured Azure client with maxRetries: %d", clientOpts.Retry.MaxRetries)
 	armClientOpts := &arm.ClientOptions{
 		ClientOptions: clientOpts,
 	}
diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go
index e89d82cf..29dd8000 100644
--- a/provider/cloudflare/cloudflare.go
+++ b/provider/cloudflare/cloudflare.go
@@ -28,13 +28,14 @@ import (
 	"strconv"
 	"strings"
 
-	cloudflare "github.com/cloudflare/cloudflare-go"
+	"github.com/cloudflare/cloudflare-go"
 	log "github.com/sirupsen/logrus"
+	"golang.org/x/net/publicsuffix"
 
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/plan"
 	"sigs.k8s.io/external-dns/provider"
-	"sigs.k8s.io/external-dns/source"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 const (
@@ -46,6 +47,10 @@ const (
 	cloudFlareUpdate = "UPDATE"
 	// defaultTTL 1 = automatic
 	defaultTTL = 1
+
+	// Cloudflare tier limitations https://developers.cloudflare.com/dns/manage-dns-records/reference/record-attributes/#availability
+	freeZoneMaxCommentLength = 100
+	paidZoneMaxCommentLength = 500
 )
 
 // We have to use pointers to bools now, as the upstream cloudflare-go library requires them
@@ -190,6 +195,45 @@ func (z zoneService) CreateCustomHostname(ctx context.Context, zoneID string, ch
 	return z.service.CreateCustomHostname(ctx, zoneID, ch)
 }
 
+type DNSRecordsConfig struct {
+	PerPage int
+	Comment string
+}
+
+func (c *DNSRecordsConfig) trimAndValidateComment(dnsName, comment string, paidZone func(string) bool) string {
+	if len(comment) > freeZoneMaxCommentLength {
+		if !paidZone(dnsName) {
+			log.Warnf("DNS record comment is invalid. Trimming comment of %s. To avoid endless syncs, please set it to less than %d chars.", dnsName, freeZoneMaxCommentLength)
+			return comment[:freeZoneMaxCommentLength]
+		} else if len(comment) > paidZoneMaxCommentLength {
+			log.Warnf("DNS record comment is invalid. Trimming comment of %s. To avoid endless syncs, please set it to less than %d chars.", dnsName, paidZoneMaxCommentLength)
+			return comment[:paidZoneMaxCommentLength]
+		}
+	}
+	return comment
+}
+
+func (p *CloudFlareProvider) ZoneHasPaidPlan(hostname string) bool {
+	zone, err := publicsuffix.EffectiveTLDPlusOne(hostname)
+	if err != nil {
+		log.Errorf("Failed to get effective TLD+1 for hostname %s %v", hostname, err)
+		return false
+	}
+	zoneID, err := p.Client.ZoneIDByName(zone)
+	if err != nil {
+		log.Errorf("Failed to get zone %s by name %v", zone, err)
+		return false
+	}
+
+	zoneDetails, err := p.Client.ZoneDetails(context.Background(), zoneID)
+	if err != nil {
+		log.Errorf("Failed to get zone %s details %v", zone, err)
+		return false
+	}
+
+	return zoneDetails.Plan.IsSubscribed
+}
+
 // CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
 type CloudFlareProvider struct {
 	provider.BaseProvider
@@ -198,9 +242,9 @@ type CloudFlareProvider struct {
 	domainFilter          endpoint.DomainFilter
 	zoneIDFilter          provider.ZoneIDFilter
 	proxiedByDefault      bool
-	CustomHostnamesConfig CustomHostnamesConfig
 	DryRun                bool
-	DNSRecordsPerPage     int
+	CustomHostnamesConfig CustomHostnamesConfig
+	DNSRecordsConfig      DNSRecordsConfig
 	RegionKey             string
 }
 
@@ -257,7 +301,7 @@ func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordPar
 }
 
 // NewCloudFlareProvider initializes a new CloudFlare DNS based Provider.
-func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, proxiedByDefault bool, dryRun bool, dnsRecordsPerPage int, regionKey string, customHostnamesConfig CustomHostnamesConfig) (*CloudFlareProvider, error) {
+func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, proxiedByDefault bool, dryRun bool, regionKey string, customHostnamesConfig CustomHostnamesConfig, dnsRecordsConfig DNSRecordsConfig) (*CloudFlareProvider, error) {
 	// initialize via chosen auth method and returns new API object
 	var (
 		config *cloudflare.API
@@ -287,8 +331,8 @@ func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter prov
 		proxiedByDefault:      proxiedByDefault,
 		CustomHostnamesConfig: customHostnamesConfig,
 		DryRun:                dryRun,
-		DNSRecordsPerPage:     dnsRecordsPerPage,
 		RegionKey:             regionKey,
+		DNSRecordsConfig:      dnsRecordsConfig,
 	}, nil
 }
 
@@ -735,17 +779,17 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]
 		if proxied {
 			e.RecordTTL = 0
 		}
-		e.SetProviderSpecificProperty(source.CloudflareProxiedKey, strconv.FormatBool(proxied))
+		e.SetProviderSpecificProperty(annotations.CloudflareProxiedKey, strconv.FormatBool(proxied))
 
 		if p.CustomHostnamesConfig.Enabled {
 			// sort custom hostnames in annotation to properly detect changes
 			if customHostnames := getEndpointCustomHostnames(e); len(customHostnames) > 1 {
 				sort.Strings(customHostnames)
-				e.SetProviderSpecificProperty(source.CloudflareCustomHostnameKey, strings.Join(customHostnames, ","))
+				e.SetProviderSpecificProperty(annotations.CloudflareCustomHostnameKey, strings.Join(customHostnames, ","))
 			}
 		} else {
 			// ignore custom hostnames annotations if not enabled
-			e.DeleteProviderSpecificProperty(source.CloudflareCustomHostnameKey)
+			e.DeleteProviderSpecificProperty(annotations.CloudflareCustomHostnameKey)
 		}
 
 		adjustedEndpoints = append(adjustedEndpoints, e)
@@ -825,6 +869,18 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
 			RegionKey: regionKey,
 		}
 	}
+
+	// Load comment from program flag
+	comment := p.DNSRecordsConfig.Comment
+	if val, ok := ep.GetProviderSpecificProperty(annotations.CloudflareRecordCommentKey); ok {
+		// Replace comment with Ingress annotation
+		comment = val
+	}
+
+	if len(comment) > freeZoneMaxCommentLength {
+		comment = p.DNSRecordsConfig.trimAndValidateComment(ep.DNSName, comment, p.ZoneHasPaidPlan)
+	}
+
 	return &cloudFlareChange{
 		Action: action,
 		ResourceRecord: cloudflare.DNSRecord{
@@ -835,6 +891,7 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, ep *endpoint.End
 			Proxied: &proxied,
 			Type:    ep.RecordType,
 			Content: target,
+			Comment: comment,
 		},
 		RegionalHostname:    regionalHostname,
 		CustomHostnamesPrev: prevCustomHostnames,
@@ -850,7 +907,7 @@ func newDNSRecordIndex(r cloudflare.DNSRecord) DNSRecordIndex {
 func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Context, zoneID string) (DNSRecordsMap, error) {
 	// for faster getRecordID lookup
 	records := make(DNSRecordsMap)
-	resultInfo := cloudflare.ResultInfo{PerPage: p.DNSRecordsPerPage, Page: 1}
+	resultInfo := cloudflare.ResultInfo{PerPage: p.DNSRecordsConfig.PerPage, Page: 1}
 	params := cloudflare.ListDNSRecordsParams{ResultInfo: resultInfo}
 	for {
 		pageRecords, resultInfo, err := p.Client.ListDNSRecords(ctx, cloudflare.ZoneIdentifier(zoneID), params)
@@ -927,10 +984,10 @@ func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool {
 	proxied := proxiedByDefault
 
 	for _, v := range ep.ProviderSpecific {
-		if v.Name == source.CloudflareProxiedKey {
+		if v.Name == annotations.CloudflareProxiedKey {
 			b, err := strconv.ParseBool(v.Value)
 			if err != nil {
-				log.Errorf("Failed to parse annotation [%q]: %v", source.CloudflareProxiedKey, err)
+				log.Errorf("Failed to parse annotation [%q]: %v", annotations.CloudflareProxiedKey, err)
 			} else {
 				proxied = b
 			}
@@ -950,7 +1007,7 @@ func getRegionKey(endpoint *endpoint.Endpoint, defaultRegionKey string) string {
 	}
 
 	for _, v := range endpoint.ProviderSpecific {
-		if v.Name == source.CloudflareRegionKey {
+		if v.Name == annotations.CloudflareRegionKey {
 			return v.Value
 		}
 	}
@@ -959,7 +1016,7 @@ func getRegionKey(endpoint *endpoint.Endpoint, defaultRegionKey string) string {
 
 func getEndpointCustomHostnames(ep *endpoint.Endpoint) []string {
 	for _, v := range ep.ProviderSpecific {
-		if v.Name == source.CloudflareCustomHostnameKey {
+		if v.Name == annotations.CloudflareCustomHostnameKey {
 			customHostnames := strings.Split(v.Value, ",")
 			return customHostnames
 		}
@@ -1014,11 +1071,15 @@ func groupByNameAndTypeWithCustomHostnames(records DNSRecordsMap, chs CustomHost
 		if e == nil {
 			continue
 		}
-		e = e.WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(proxied))
-		// noop (customHostnames is empty) if the custom hostnames feature is not in use
+		e = e.WithProviderSpecific(annotations.CloudflareProxiedKey, strconv.FormatBool(proxied))
+		// noop (customHostnames is empty) if custom hostnames feature is not in use
 		if customHostnames, ok := customHostnames[records[0].Name]; ok {
 			sort.Strings(customHostnames)
-			e = e.WithProviderSpecific(source.CloudflareCustomHostnameKey, strings.Join(customHostnames, ","))
+			e = e.WithProviderSpecific(annotations.CloudflareCustomHostnameKey, strings.Join(customHostnames, ","))
+		}
+
+		if records[0].Comment != "" {
+			e = e.WithProviderSpecific(annotations.CloudflareRecordCommentKey, records[0].Comment)
 		}
 
 		endpoints = append(endpoints, e)
diff --git a/provider/coredns/coredns.go b/provider/coredns/coredns.go
index 18dc9cea..21fb2929 100644
--- a/provider/coredns/coredns.go
+++ b/provider/coredns/coredns.go
@@ -18,8 +18,6 @@ package coredns
 
 import (
 	"context"
-	"crypto/tls"
-	"crypto/x509"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -32,6 +30,8 @@ import (
 	log "github.com/sirupsen/logrus"
 	etcdcv3 "go.etcd.io/etcd/client/v3"
 
+	"sigs.k8s.io/external-dns/pkg/tlsutils"
+
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/plan"
 	"sigs.k8s.io/external-dns/provider"
@@ -153,50 +153,6 @@ func (c etcdClient) DeleteService(key string) error {
 	return err
 }
 
-// loads TLS artifacts and builds tls.Config object
-func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) (*tls.Config, error) {
-	if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
-		return nil, errors.New("either both cert and key or none must be provided")
-	}
-	var certificates []tls.Certificate
-	if certPath != "" {
-		cert, err := tls.LoadX509KeyPair(certPath, keyPath)
-		if err != nil {
-			return nil, fmt.Errorf("could not load TLS cert: %w", err)
-		}
-		certificates = append(certificates, cert)
-	}
-	roots, err := loadRoots(caPath)
-	if err != nil {
-		return nil, err
-	}
-
-	return &tls.Config{
-		Certificates:       certificates,
-		RootCAs:            roots,
-		InsecureSkipVerify: insecure,
-		ServerName:         serverName,
-	}, nil
-}
-
-// loads CA cert
-func loadRoots(caPath string) (*x509.CertPool, error) {
-	if caPath == "" {
-		return nil, nil
-	}
-
-	roots := x509.NewCertPool()
-	pem, err := os.ReadFile(caPath)
-	if err != nil {
-		return nil, fmt.Errorf("error reading %s: %w", caPath, err)
-	}
-	ok := roots.AppendCertsFromPEM(pem)
-	if !ok {
-		return nil, fmt.Errorf("could not read root certs: %w", err)
-	}
-	return roots, nil
-}
-
 // builds etcd client config depending on connection scheme and TLS parameters
 func getETCDConfig() (*etcdcv3.Config, error) {
 	etcdURLsStr := os.Getenv("ETCD_URLS")
@@ -210,16 +166,11 @@ func getETCDConfig() (*etcdcv3.Config, error) {
 	if strings.HasPrefix(firstURL, "http://") {
 		return &etcdcv3.Config{Endpoints: etcdURLs, Username: etcdUsername, Password: etcdPassword}, nil
 	} else if strings.HasPrefix(firstURL, "https://") {
-		caFile := os.Getenv("ETCD_CA_FILE")
-		certFile := os.Getenv("ETCD_CERT_FILE")
-		keyFile := os.Getenv("ETCD_KEY_FILE")
-		serverName := os.Getenv("ETCD_TLS_SERVER_NAME")
-		isInsecureStr := strings.ToLower(os.Getenv("ETCD_TLS_INSECURE"))
-		isInsecure := isInsecureStr == "true" || isInsecureStr == "yes" || isInsecureStr == "1"
-		tlsConfig, err := newTLSConfig(certFile, keyFile, caFile, serverName, isInsecure)
+		tlsConfig, err := tlsutils.CreateTLSConfig("ETCD")
 		if err != nil {
 			return nil, err
 		}
+		log.Debug("using TLS for etcd")
 		return &etcdcv3.Config{
 			Endpoints: etcdURLs,
 			TLS:       tlsConfig,
@@ -231,7 +182,7 @@ func getETCDConfig() (*etcdcv3.Config, error) {
 	}
 }
 
-// newETCDClient is an etcd client constructor
+// the newETCDClient is an etcd client constructor
 func newETCDClient() (coreDNSClient, error) {
 	cfg, err := getETCDConfig()
 	if err != nil {
@@ -283,7 +234,7 @@ func findLabelInTargets(targets []string, label string) (string, bool) {
 
 // Records returns all DNS records found in CoreDNS etcd backend. Depending on the record fields
 // it may be mapped to one or two records of type A, CNAME, TXT, A+TXT, CNAME+TXT
-func (p coreDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
+func (p coreDNSProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error) {
 	var result []*endpoint.Endpoint
 	services, err := p.client.GetServices(p.coreDNSPrefix)
 	if err != nil {
diff --git a/provider/ibmcloud/ibmcloud.go b/provider/ibmcloud/ibmcloud.go
index 85bc8ae7..980789c8 100644
--- a/provider/ibmcloud/ibmcloud.go
+++ b/provider/ibmcloud/ibmcloud.go
@@ -186,11 +186,11 @@ type IBMCloudProvider struct {
 }
 
 type ibmcloudConfig struct {
-	Endpoint   string `json:"endpoint" yaml:"endpoint"`
-	APIKey     string `json:"apiKey" yaml:"apiKey"`
+	Endpoint   string `json:"endpoint"    yaml:"endpoint"`
+	APIKey     string `json:"apiKey"      yaml:"apiKey"`
 	CRN        string `json:"instanceCrn" yaml:"instanceCrn"`
-	IAMURL     string `json:"iamUrl" yaml:"iamUrl"`
-	InstanceID string `json:"-" yaml:"-"`
+	IAMURL     string `json:"iamUrl"      yaml:"iamUrl"`
+	InstanceID string `json:"-"           yaml:"-"`
 }
 
 // ibmcloudChange differentiates between ChangActions
diff --git a/provider/ovh/ovh.go b/provider/ovh/ovh.go
index f7b67a65..b6c340b6 100644
--- a/provider/ovh/ovh.go
+++ b/provider/ovh/ovh.go
@@ -195,7 +195,7 @@ func planChangesByZoneName(zones []string, changes *plan.Changes) map[string]*pl
 	return output
 }
 
-func (p OVHProvider) computeSingleZoneChanges(_ context.Context, zoneName string, existingRecords []ovhRecord, changes *plan.Changes) []ovhChange {
+func (p OVHProvider) computeSingleZoneChanges(_ context.Context, zoneName string, existingRecords []ovhRecord, changes *plan.Changes) ([]ovhChange, error) {
 	allChanges := []ovhChange{}
 	var computedChanges []ovhChange
 
@@ -204,14 +204,21 @@ func (p OVHProvider) computeSingleZoneChanges(_ context.Context, zoneName string
 	computedChanges, existingRecords = p.newOvhChangeCreateDelete(ovhDelete, changes.Delete, zoneName, existingRecords)
 	allChanges = append(allChanges, computedChanges...)
 
-	computedChanges = p.newOvhChangeUpdate(changes.UpdateOld, changes.UpdateNew, zoneName, existingRecords)
+	var err error
+	computedChanges, err = p.newOvhChangeUpdate(changes.UpdateOld, changes.UpdateNew, zoneName, existingRecords)
+	if err != nil {
+		return nil, err
+	}
 	allChanges = append(allChanges, computedChanges...)
 
-	return allChanges
+	return allChanges, nil
 }
 
 func (p *OVHProvider) handleSingleZoneUpdate(ctx context.Context, zoneName string, existingRecords []ovhRecord, changes *plan.Changes) error {
-	allChanges := p.computeSingleZoneChanges(ctx, zoneName, existingRecords, changes)
+	allChanges, err := p.computeSingleZoneChanges(ctx, zoneName, existingRecords, changes)
+	if err != nil {
+		return err
+	}
 	log.Infof("OVH: %q: %d changes will be done", zoneName, len(allChanges))
 
 	eg, ctxErrGroup := errgroup.WithContext(ctx)
@@ -222,7 +229,7 @@ func (p *OVHProvider) handleSingleZoneUpdate(ctx context.Context, zoneName strin
 		})
 	}
 
-	err := eg.Wait()
+	err = eg.Wait()
 
 	// do not refresh zone if errors: some records might haven't been processed yet, hence the zone will be in an inconsistent state
 	// if modification of the zone was in error, invalidating the cache to make sure next run will start freshly
@@ -554,7 +561,11 @@ func convertDNSNameIntoSubDomain(DNSName string, zoneName string) string {
 	return strings.TrimSuffix(DNSName, "."+zoneName)
 }
 
-func (p OVHProvider) newOvhChangeUpdate(endpointsOld []*endpoint.Endpoint, endpointsNew []*endpoint.Endpoint, zone string, existingRecords []ovhRecord) []ovhChange {
+func normalizeDNSName(dnsName string) string {
+	return strings.TrimSpace(strings.ToLower(dnsName))
+}
+
+func (p OVHProvider) newOvhChangeUpdate(endpointsOld []*endpoint.Endpoint, endpointsNew []*endpoint.Endpoint, zone string, existingRecords []ovhRecord) ([]ovhChange, error) {
 	zoneNameIDMapper := provider.ZoneIDName{}
 	zoneNameIDMapper.Add(zone, zone)
 
@@ -564,16 +575,16 @@ func (p OVHProvider) newOvhChangeUpdate(endpointsOld []*endpoint.Endpoint, endpo
 
 	for _, e := range endpointsOld {
 		sub := convertDNSNameIntoSubDomain(e.DNSName, zone)
-		oldEndpointByTypeAndName[e.RecordType+"//"+sub] = e
+		oldEndpointByTypeAndName[normalizeDNSName(e.RecordType+"//"+sub)] = e
 	}
 	for _, e := range endpointsNew {
 		sub := convertDNSNameIntoSubDomain(e.DNSName, zone)
-		newEndpointByTypeAndName[e.RecordType+"//"+sub] = e
+		newEndpointByTypeAndName[normalizeDNSName(e.RecordType+"//"+sub)] = e
 	}
 
 	for id := range oldEndpointByTypeAndName {
 		for _, record := range existingRecords {
-			if id == record.FieldType+"//"+record.SubDomain {
+			if id == normalizeDNSName(record.FieldType+"//"+record.SubDomain) {
 				oldRecordsInZone[id] = append(oldRecordsInZone[id], record)
 			}
 		}
@@ -583,7 +594,10 @@ func (p OVHProvider) newOvhChangeUpdate(endpointsOld []*endpoint.Endpoint, endpo
 
 	for id := range oldEndpointByTypeAndName {
 		oldRecords := slices.Clone(oldRecordsInZone[id])
-		endpointsNew := newEndpointByTypeAndName[id]
+		endpointsNew, ok := newEndpointByTypeAndName[id]
+		if !ok {
+			return nil, errors.New("unrecoverable error: couldn't find the matching record in the update.New")
+		}
 
 		var toInsertTarget []string
 
@@ -668,7 +682,7 @@ func (p OVHProvider) newOvhChangeUpdate(endpointsOld []*endpoint.Endpoint, endpo
 		}
 	}
 
-	return changes
+	return changes, nil
 }
 
 func (c *ovhChange) String() string {
diff --git a/provider/rfc2136/rfc2136.go b/provider/rfc2136/rfc2136.go
index 8b1b58d5..f12bfabb 100644
--- a/provider/rfc2136/rfc2136.go
+++ b/provider/rfc2136/rfc2136.go
@@ -82,9 +82,6 @@ type rfc2136Provider struct {
 	// Random number generator for random load balancing
 	randGen *rand.Rand
 
-	// Store TSIG credentials for each nameserver
-	credentials map[string]*gss.Client
-
 	// Last error encountered
 	lastErr error
 }
@@ -152,7 +149,6 @@ func NewRfc2136Provider(hosts []string, port int, zoneNames []string, insecure b
 		tlsConfig:             tlsConfig,
 		loadBalancingStrategy: loadBalancingStrategy,
 		randGen:               rand.New(rand.NewSource(time.Now().UnixNano())),
-		credentials:           make(map[string]*gss.Client),
 		counter:               0,
 		lastErr:               nil,
 	}
@@ -174,11 +170,6 @@ func NewRfc2136Provider(hosts []string, port int, zoneNames []string, insecure b
 
 // KeyData will return TKEY name and TSIG handle to use for followon actions with a secure connection
 func (r *rfc2136Provider) KeyData(nameserver string) (keyName string, handle *gss.Client, err error) {
-	// Check if we already have credentials for this nameserver
-	if existingHandle, ok := r.credentials[nameserver]; ok {
-		return nameserver, existingHandle, nil
-	}
-
 	handle, err = gss.NewClient(new(dns.Client))
 	if err != nil {
 		return keyName, handle, err
@@ -189,9 +180,6 @@ func (r *rfc2136Provider) KeyData(nameserver string) (keyName string, handle *gs
 		return keyName, handle, err
 	}
 
-	// Store the credentials for this nameserver
-	r.credentials[nameserver] = handle
-
 	return keyName, handle, nil
 }
 
@@ -380,7 +368,6 @@ func (r *rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Change
 			}
 
 			zone := findMsgZone(ep, r.zoneNames)
-			r.krb5Realm = strings.ToUpper(zone)
 			m[zone].SetUpdate(zone)
 
 			r.AddRecord(m[zone], ep)
@@ -419,7 +406,6 @@ func (r *rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Change
 			}
 
 			zone := findMsgZone(ep, r.zoneNames)
-			r.krb5Realm = strings.ToUpper(zone)
 			m[zone].SetUpdate(zone)
 
 			r.UpdateRecord(m[zone], changes.UpdateOld[i], ep)
@@ -457,7 +443,6 @@ func (r *rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Change
 			}
 
 			zone := findMsgZone(ep, r.zoneNames)
-			r.krb5Realm = strings.ToUpper(zone)
 			m[zone].SetUpdate(zone)
 
 			r.RemoveRecord(m[zone], ep)
diff --git a/provider/tencentcloud/tencent_cloud.go b/provider/tencentcloud/tencent_cloud.go
index e52a4df8..f4435a01 100644
--- a/provider/tencentcloud/tencent_cloud.go
+++ b/provider/tencentcloud/tencent_cloud.go
@@ -75,10 +75,10 @@ type TencentCloudProvider struct {
 }
 
 type tencentCloudConfig struct {
-	RegionId         string `json:"regionId" yaml:"regionId"`
-	SecretId         string `json:"secretId" yaml:"secretId"`
-	SecretKey        string `json:"secretKey" yaml:"secretKey"`
-	VPCId            string `json:"vpcId" yaml:"vpcId"`
+	RegionId         string `json:"regionId"         yaml:"regionId"`
+	SecretId         string `json:"secretId"         yaml:"secretId"`
+	SecretKey        string `json:"secretKey"        yaml:"secretKey"`
+	VPCId            string `json:"vpcId"            yaml:"vpcId"`
 	InternetEndpoint bool   `json:"internetEndpoint" yaml:"internetEndpoint"`
 }
 
diff --git a/scripts/releaser.sh b/scripts/releaser.sh
index 2295b833..89a71e32 100755
--- a/scripts/releaser.sh
+++ b/scripts/releaser.sh
@@ -1,43 +1,72 @@
 #!/bin/bash
 set -e
 
-current_tag="${GITHUB_REF#refs/tags/}"
-start_ref="HEAD"
+
 
 function generate_changelog {
-  # Find the previous release on the same branch, skipping prereleases if the
-  # current tag is a full release
-  previous_tag=""
-  while [[ -z $previous_tag || ( $previous_tag == *-* && $current_tag != *-* ) ]]; do
-    previous_tag="$(git describe --tags "$start_ref"^ --abbrev=0 --match "v*")"
-    start_ref="$previous_tag"
-  done
-
-  git log "$previous_tag".. --reverse --merges --oneline --grep='Merge pull request #' | \
-    while read -r sha title; do
-      pr_num="$(grep -o '#[[:digit:]]\+' <<<"$title")"
-      pr_desc="$(git show -s --format=%b "$sha" | sed -n '1,/^$/p' | tr $'\n' ' ')"
-      pr_author="$(gh pr view "$pr_num" | grep "author:" | awk '{ print $2 }' | tr $'\n' ' ')"
-      printf "* %s (%s) @%s\n\n" "$pr_desc" "$pr_num" "$pr_author"
-    done
-
-  git log "$previous_tag".. --reverse --oneline --grep='(#' | \
-    while read -r sha title; do
-      pr_num="$(grep -o '#[[:digit:]]\+' <<<"$title")"
-      pr_desc="$(git show -s --format=%s "$sha")"
-      pr_author="$(gh pr view "$pr_num" | grep "author:" | awk '{ print $2 }' | tr $'\n' ' ')"
-      printf "* %s (%s) @%s\n\n" "$pr_desc" "$pr_num" "$pr_author"
-    done
+  MERGED_PRS="$1"
+
+  echo
+  echo "## :rocket: Features"
+  echo
+  cat "${MERGED_PRS}" | grep feat
+
+  echo
+  echo "## :bug: Bug fixes"
+  echo
+  cat "${MERGED_PRS}" | grep fix
+
+  echo
+  echo "## :memo: Documentation"
+  echo
+  cat "${MERGED_PRS}" | grep doc
+
+  echo
+  echo "## :package: Others"
+  echo
+  cat "${MERGED_PRS}" | grep -v feat | grep -v fix | grep -v doc
 }
 
 function create_release {
-  generate_changelog | sort | gh release create "$1" -t "$1" -F -
+  generate_changelog | sort # | gh release create "$1" -t "$1" -F -
+}
+
+function latest_release {
+  gh release list -L 10 --json name,isLatest --jq '.[] | select(.isLatest)|.name'
+}
+
+function latest_release_date {
+  gh release list -L 10 --json name,isLatest,publishedAt --jq '.[] | select(.isLatest)|.publishedAt'
+}
+
+function latest_release_ts {
+  gh release list -L 10 --json name,isLatest,publishedAt --jq '.[] | select(.isLatest)|.publishedAt | fromdateiso8601'
 }
 
 if [ $# -ne 1 ]; then
-    echo "$0: usage: releaser [release number]"
-    echo "example: ./releaser.sh v0.7.5"
-    exit 1
+    echo "** DRY RUN **"
+fi
+
+printf "Latest release: %s (%s)\n" $(latest_release) $(latest_release_date)
+
+TIMESTAMP=$(latest_release_ts)
+MERGED_PRS=$(mktemp)
+gh pr list \
+  --state merged \
+  --json author,number,mergeCommit,mergedAt,url,title \
+  --limit 999 \
+  --jq ".[] |
+    select (.mergedAt | fromdateiso8601 > ${TIMESTAMP}) | \
+    \"- \(.title) by @\(.author.login) in #\(.number)\"
+  " | sort > "${MERGED_PRS}"
+
+if [ $# -ne 1 ]; then
+  generate_changelog "${MERGED_PRS}"
+  echo "** DRY RUN **"
+  echo
+  echo "To create a release: ./releaser.sh v0.17.0"
+else
+  generate_changelog "${MERGED_PRS}" | gh release create "$1" -t "$1" -F -
 fi
 
-create_release "$1"
+rm -f "${MERGED_PRS}"
diff --git a/source/ambassador_host.go b/source/ambassador_host.go
index 4dde2eaf..077a3f58 100644
--- a/source/ambassador_host.go
+++ b/source/ambassador_host.go
@@ -38,6 +38,7 @@ import (
 	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 // ambHostAnnotation is the annotation in the Host that maps to a Service
@@ -152,7 +153,7 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
 			continue
 		}
 
-		targets := getTargetsFromTargetAnnotation(host.Annotations)
+		targets := annotations.TargetsFromTargetAnnotation(host.Annotations)
 		if len(targets) == 0 {
 			targets, err = sc.targetsFromAmbassadorLoadBalancer(ctx, service)
 			if err != nil {
@@ -187,8 +188,8 @@ func (sc *ambassadorHostSource) endpointsFromHost(host *ambassador.Host, targets
 	var endpoints []*endpoint.Endpoint
 
 	resource := fmt.Sprintf("host/%s/%s", host.Namespace, host.Name)
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(host.Annotations)
-	ttl := getTTLFromAnnotations(host.Annotations, resource)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(host.Annotations)
+	ttl := annotations.TTLFromAnnotations(host.Annotations, resource)
 
 	if host.Spec != nil {
 		hostname := host.Spec.Hostname
diff --git a/source/annotations/annotations.go b/source/annotations/annotations.go
new file mode 100644
index 00000000..5c98516e
--- /dev/null
+++ b/source/annotations/annotations.go
@@ -0,0 +1,55 @@
+/*
+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 annotations
+
+import (
+	"math"
+)
+
+const (
+	// CloudflareProxiedKey The annotation used for determining if traffic will go through Cloudflare
+	CloudflareProxiedKey        = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
+	CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname"
+	CloudflareRegionKey         = "external-dns.alpha.kubernetes.io/cloudflare-region-key"
+	CloudflareRecordCommentKey  = "external-dns.alpha.kubernetes.io/cloudflare-record-comment"
+
+	AWSPrefix        = "external-dns.alpha.kubernetes.io/aws-"
+	SCWPrefix        = "external-dns.alpha.kubernetes.io/scw-"
+	IBMCloudPrefix   = "external-dns.alpha.kubernetes.io/ibmcloud-"
+	WebhookPrefix    = "external-dns.alpha.kubernetes.io/webhook-"
+	CloudflarePrefix = "external-dns.alpha.kubernetes.io/cloudflare-"
+
+	TtlKey     = "external-dns.alpha.kubernetes.io/ttl"
+	ttlMinimum = 1
+	ttlMaximum = math.MaxInt32
+
+	SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier"
+	AliasKey         = "external-dns.alpha.kubernetes.io/alias"
+	TargetKey        = "external-dns.alpha.kubernetes.io/target"
+	// The annotation used for figuring out which controller is responsible
+	ControllerKey = "external-dns.alpha.kubernetes.io/controller"
+	// The annotation used for defining the desired hostname
+	HostnameKey = "external-dns.alpha.kubernetes.io/hostname"
+	// The annotation used for specifying whether the public or private interface address is used
+	AccessKey = "external-dns.alpha.kubernetes.io/access"
+	// The annotation used for specifying the type of endpoints to use for headless services
+	EndpointsTypeKey = "external-dns.alpha.kubernetes.io/endpoints-type"
+	// The annotation used to determine the source of hostnames for ingresses.  This is an optional field - all
+	// available hostname sources are used if not specified.
+	IngressHostnameSourceKey = "external-dns.alpha.kubernetes.io/ingress-hostname-source"
+	// The value of the controller annotation so that we feel responsible
+	ControllerValue = "dns-controller"
+	// The annotation used for defining the desired hostname
+	InternalHostnameKey = "external-dns.alpha.kubernetes.io/internal-hostname"
+)
diff --git a/source/annotations/processors.go b/source/annotations/processors.go
new file mode 100644
index 00000000..dd704ab5
--- /dev/null
+++ b/source/annotations/processors.go
@@ -0,0 +1,125 @@
+/*
+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 annotations
+
+import (
+	"strconv"
+	"strings"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+
+	"sigs.k8s.io/external-dns/endpoint"
+)
+
+func hasAliasFromAnnotations(annotations map[string]string) bool {
+	aliasAnnotation, ok := annotations[AliasKey]
+	return ok && aliasAnnotation == "true"
+}
+
+// TTLFromAnnotations extracts the TTL from the annotations of the given resource.
+func TTLFromAnnotations(annotations map[string]string, resource string) endpoint.TTL {
+	ttlNotConfigured := endpoint.TTL(0)
+	ttlAnnotation, ok := annotations[TtlKey]
+	if !ok {
+		return ttlNotConfigured
+	}
+	ttlValue, err := parseTTL(ttlAnnotation)
+	if err != nil {
+		log.Warnf("%s: %q is not a valid TTL value: %v", resource, ttlAnnotation, err)
+		return ttlNotConfigured
+	}
+	if ttlValue < ttlMinimum || ttlValue > ttlMaximum {
+		log.Warnf("TTL value %q must be between [%d, %d]", ttlValue, ttlMinimum, ttlMaximum)
+		return ttlNotConfigured
+	}
+	return endpoint.TTL(ttlValue)
+}
+
+// parseTTL parses TTL from string, returning duration in seconds.
+// parseTTL supports both integers like "600" and durations based
+// on Go Duration like "10m", hence "600" and "10m" represent the same value.
+//
+// Note: for durations like "1.5s" the fraction is omitted (resulting in 1 second for the example).
+func parseTTL(s string) (int64, error) {
+	ttlDuration, errDuration := time.ParseDuration(s)
+	if errDuration != nil {
+		ttlInt, err := strconv.ParseInt(s, 10, 64)
+		if err != nil {
+			return 0, errDuration
+		}
+		return ttlInt, nil
+	}
+
+	return int64(ttlDuration.Seconds()), nil
+}
+
+// ParseFilter parses an annotation filter string into a labels.Selector.
+// Returns nil if the annotation filter is invalid.
+func ParseFilter(annotationFilter string) (labels.Selector, error) {
+	labelSelector, err := metav1.ParseToLabelSelector(annotationFilter)
+	if err != nil {
+		return nil, err
+	}
+	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+	return selector, nil
+}
+
+// TargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
+// Returns empty endpoints array if none are found.
+func TargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets {
+	var targets endpoint.Targets
+	// Get the desired hostname of the ingress from the annotation.
+	targetAnnotation, ok := annotations[TargetKey]
+	if ok && targetAnnotation != "" {
+		// splits the hostname annotation and removes the trailing periods
+		targetsList := SplitHostnameAnnotation(targetAnnotation)
+		for _, targetHostname := range targetsList {
+			targetHostname = strings.TrimSuffix(targetHostname, ".")
+			targets = append(targets, targetHostname)
+		}
+	}
+	return targets
+}
+
+// HostnamesFromAnnotations extracts the hostnames from the given annotations map.
+// It returns a slice of hostnames if the HostnameKey annotation is present, otherwise it returns nil.
+func HostnamesFromAnnotations(input map[string]string) []string {
+	return extractHostnamesFromAnnotations(input, HostnameKey)
+}
+
+// InternalHostnamesFromAnnotations extracts the internal hostnames from the given annotations map.
+// It returns a slice of internal hostnames if the InternalHostnameKey annotation is present, otherwise it returns nil.
+func InternalHostnamesFromAnnotations(input map[string]string) []string {
+	return extractHostnamesFromAnnotations(input, InternalHostnameKey)
+}
+
+// SplitHostnameAnnotation splits a comma-separated hostname annotation string into a slice of hostnames.
+// It trims any leading or trailing whitespace and removes any spaces within the anno
+func SplitHostnameAnnotation(input string) []string {
+	return strings.Split(strings.TrimSpace(strings.ReplaceAll(input, " ", "")), ",")
+}
+
+func extractHostnamesFromAnnotations(input map[string]string, key string) []string {
+	annotation, ok := input[key]
+	if !ok {
+		return nil
+	}
+	return SplitHostnameAnnotation(annotation)
+}
diff --git a/source/annotations/provider_specific.go b/source/annotations/provider_specific.go
new file mode 100644
index 00000000..99d932c3
--- /dev/null
+++ b/source/annotations/provider_specific.go
@@ -0,0 +1,86 @@
+/*
+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 annotations
+
+import (
+	"fmt"
+	"strings"
+
+	"sigs.k8s.io/external-dns/endpoint"
+)
+
+func ProviderSpecificAnnotations(annotations map[string]string) (endpoint.ProviderSpecific, string) {
+	providerSpecificAnnotations := endpoint.ProviderSpecific{}
+
+	if hasAliasFromAnnotations(annotations) {
+		providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
+			Name:  "alias",
+			Value: "true",
+		})
+	}
+	setIdentifier := ""
+	for k, v := range annotations {
+		if k == SetIdentifierKey {
+			setIdentifier = v
+		} else if strings.HasPrefix(k, AWSPrefix) {
+			attr := strings.TrimPrefix(k, AWSPrefix)
+			providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
+				Name:  fmt.Sprintf("aws/%s", attr),
+				Value: v,
+			})
+		} else if strings.HasPrefix(k, SCWPrefix) {
+			attr := strings.TrimPrefix(k, SCWPrefix)
+			providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
+				Name:  fmt.Sprintf("scw/%s", attr),
+				Value: v,
+			})
+		} else if strings.HasPrefix(k, IBMCloudPrefix) {
+			attr := strings.TrimPrefix(k, IBMCloudPrefix)
+			providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
+				Name:  fmt.Sprintf("ibmcloud-%s", attr),
+				Value: v,
+			})
+		} else if strings.HasPrefix(k, WebhookPrefix) {
+			// Support for wildcard annotations for webhook providers
+			attr := strings.TrimPrefix(k, WebhookPrefix)
+			providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
+				Name:  fmt.Sprintf("webhook/%s", attr),
+				Value: v,
+			})
+		} else if strings.HasPrefix(k, CloudflarePrefix) {
+			if strings.Contains(k, CloudflareCustomHostnameKey) {
+				providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
+					Name:  CloudflareCustomHostnameKey,
+					Value: v,
+				})
+			} else if strings.Contains(k, CloudflareProxiedKey) {
+				providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
+					Name:  CloudflareProxiedKey,
+					Value: v,
+				})
+			} else if strings.Contains(k, CloudflareRegionKey) {
+				providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
+					Name:  CloudflareRegionKey,
+					Value: v,
+				})
+			} else if strings.Contains(k, CloudflareRecordCommentKey) {
+				providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
+					Name:  CloudflareRecordCommentKey,
+					Value: v,
+				})
+			}
+		}
+	}
+	return providerSpecificAnnotations, setIdentifier
+}
diff --git a/source/compatibility.go b/source/compatibility.go
index afb1de3e..fc54552f 100644
--- a/source/compatibility.go
+++ b/source/compatibility.go
@@ -55,8 +55,8 @@ func legacyEndpointsFromMateService(svc *v1.Service) []*endpoint.Endpoint {
 	var endpoints []*endpoint.Endpoint
 
 	// Get the desired hostname of the service from the annotation.
-	hostname, exists := svc.Annotations[mateAnnotationKey]
-	if !exists {
+	hostname, ok := svc.Annotations[mateAnnotationKey]
+	if !ok {
 		return nil
 	}
 
@@ -159,7 +159,7 @@ func legacyEndpointsFromDNSControllerNodePortService(svc *v1.Service, sc *servic
 			for _, address := range node.Status.Addresses {
 				recordType := suitableType(address.Address)
 				// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
-				if isExternal && (address.Type == v1.NodeExternalIP || (address.Type == v1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA)) {
+				if isExternal && (address.Type == v1.NodeExternalIP || (sc.exposeInternalIPv6 && address.Type == v1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA)) {
 					endpoints = append(endpoints, endpoint.NewEndpoint(hostname, recordType, address.Address))
 				}
 				if isInternal && address.Type == v1.NodeInternalIP {
diff --git a/source/contour_httpproxy.go b/source/contour_httpproxy.go
index 82bdaee4..0d872d5e 100644
--- a/source/contour_httpproxy.go
+++ b/source/contour_httpproxy.go
@@ -36,6 +36,7 @@ import (
 	"sigs.k8s.io/external-dns/source/fqdn"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 // HTTPProxySource is an implementation of Source for ProjectContour HTTPProxy objects.
@@ -187,9 +188,9 @@ func (sc *httpProxySource) endpointsFromTemplate(httpProxy *projectcontour.HTTPP
 
 	resource := fmt.Sprintf("HTTPProxy/%s/%s", httpProxy.Namespace, httpProxy.Name)
 
-	ttl := getTTLFromAnnotations(httpProxy.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(httpProxy.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(httpProxy.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(httpProxy.Annotations)
 	if len(targets) == 0 {
 		for _, lb := range httpProxy.Status.LoadBalancer.Ingress {
 			if lb.IP != "" {
@@ -201,7 +202,7 @@ func (sc *httpProxySource) endpointsFromTemplate(httpProxy *projectcontour.HTTPP
 		}
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(httpProxy.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(httpProxy.Annotations)
 
 	var endpoints []*endpoint.Endpoint
 	for _, hostname := range hostnames {
@@ -242,9 +243,9 @@ func (sc *httpProxySource) filterByAnnotations(httpProxies []*projectcontour.HTT
 func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTPProxy) ([]*endpoint.Endpoint, error) {
 	resource := fmt.Sprintf("HTTPProxy/%s/%s", httpProxy.Namespace, httpProxy.Name)
 
-	ttl := getTTLFromAnnotations(httpProxy.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(httpProxy.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(httpProxy.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(httpProxy.Annotations)
 
 	if len(targets) == 0 {
 		for _, lb := range httpProxy.Status.LoadBalancer.Ingress {
@@ -257,7 +258,7 @@ func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTP
 		}
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(httpProxy.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(httpProxy.Annotations)
 
 	var endpoints []*endpoint.Endpoint
 
@@ -269,7 +270,7 @@ func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTP
 
 	// Skip endpoints if we do not want entries from annotations
 	if !sc.ignoreHostnameAnnotation {
-		hostnameList := getHostnamesFromAnnotations(httpProxy.Annotations)
+		hostnameList := annotations.HostnamesFromAnnotations(httpProxy.Annotations)
 		for _, hostname := range hostnameList {
 			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
 		}
diff --git a/source/endpoints.go b/source/endpoints.go
new file mode 100644
index 00000000..c2b31872
--- /dev/null
+++ b/source/endpoints.go
@@ -0,0 +1,111 @@
+/*
+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 source
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/labels"
+	coreinformers "k8s.io/client-go/informers/core/v1"
+
+	"sigs.k8s.io/external-dns/endpoint"
+)
+
+// endpointsForHostname returns the endpoint objects for each host-target combination.
+func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint {
+	var (
+		endpoints    []*endpoint.Endpoint
+		aTargets     endpoint.Targets
+		aaaaTargets  endpoint.Targets
+		cnameTargets endpoint.Targets
+	)
+
+	for _, t := range targets {
+		switch suitableType(t) {
+		case endpoint.RecordTypeA:
+			aTargets = append(aTargets, t)
+		case endpoint.RecordTypeAAAA:
+			aaaaTargets = append(aaaaTargets, t)
+		default:
+			cnameTargets = append(cnameTargets, t)
+		}
+	}
+
+	if len(aTargets) > 0 {
+		epA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, ttl, aTargets...)
+		if epA != nil {
+			epA.ProviderSpecific = providerSpecific
+			epA.SetIdentifier = setIdentifier
+			if resource != "" {
+				epA.Labels[endpoint.ResourceLabelKey] = resource
+			}
+			endpoints = append(endpoints, epA)
+		}
+	}
+
+	if len(aaaaTargets) > 0 {
+		epAAAA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeAAAA, ttl, aaaaTargets...)
+		if epAAAA != nil {
+			epAAAA.ProviderSpecific = providerSpecific
+			epAAAA.SetIdentifier = setIdentifier
+			if resource != "" {
+				epAAAA.Labels[endpoint.ResourceLabelKey] = resource
+			}
+			endpoints = append(endpoints, epAAAA)
+		}
+	}
+
+	if len(cnameTargets) > 0 {
+		epCNAME := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeCNAME, ttl, cnameTargets...)
+		if epCNAME != nil {
+			epCNAME.ProviderSpecific = providerSpecific
+			epCNAME.SetIdentifier = setIdentifier
+			if resource != "" {
+				epCNAME.Labels[endpoint.ResourceLabelKey] = resource
+			}
+			endpoints = append(endpoints, epCNAME)
+		}
+	}
+
+	return endpoints
+}
+
+func EndpointTargetsFromServices(svcInformer coreinformers.ServiceInformer, namespace string, selector map[string]string) (endpoint.Targets, error) {
+	targets := endpoint.Targets{}
+
+	services, err := svcInformer.Lister().Services(namespace).List(labels.Everything())
+	if err != nil {
+		return nil, fmt.Errorf("failed to list labels for services in namespace %q: %w", namespace, err)
+	}
+
+	for _, service := range services {
+		if !MatchesServiceSelector(selector, service.Spec.Selector) {
+			continue
+		}
+
+		if len(service.Spec.ExternalIPs) > 0 {
+			targets = append(targets, service.Spec.ExternalIPs...)
+			continue
+		}
+
+		for _, lb := range service.Status.LoadBalancer.Ingress {
+			if lb.IP != "" {
+				targets = append(targets, lb.IP)
+			} else if lb.Hostname != "" {
+				targets = append(targets, lb.Hostname)
+			}
+		}
+	}
+	return targets, nil
+}
diff --git a/source/f5_transportserver.go b/source/f5_transportserver.go
index 7a055982..c9417d5b 100644
--- a/source/f5_transportserver.go
+++ b/source/f5_transportserver.go
@@ -38,6 +38,7 @@ import (
 	f5 "github.com/F5Networks/k8s-bigip-ctlr/v2/config/apis/cis/v1"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 var f5TransportServerGVR = schema.GroupVersionResource{
@@ -150,9 +151,9 @@ func (ts *f5TransportServerSource) endpointsFromTransportServers(transportServer
 
 		resource := fmt.Sprintf("f5-transportserver/%s/%s", transportServer.Namespace, transportServer.Name)
 
-		ttl := getTTLFromAnnotations(transportServer.Annotations, resource)
+		ttl := annotations.TTLFromAnnotations(transportServer.Annotations, resource)
 
-		targets := getTargetsFromTargetAnnotation(transportServer.Annotations)
+		targets := annotations.TargetsFromTargetAnnotation(transportServer.Annotations)
 		if len(targets) == 0 && transportServer.Spec.VirtualServerAddress != "" {
 			targets = append(targets, transportServer.Spec.VirtualServerAddress)
 		}
diff --git a/source/f5_virtualserver.go b/source/f5_virtualserver.go
index 1e98dc33..9ff80b90 100644
--- a/source/f5_virtualserver.go
+++ b/source/f5_virtualserver.go
@@ -39,6 +39,7 @@ import (
 	f5 "github.com/F5Networks/k8s-bigip-ctlr/v2/config/apis/cis/v1"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 var f5VirtualServerGVR = schema.GroupVersionResource{
@@ -156,9 +157,9 @@ func (vs *f5VirtualServerSource) endpointsFromVirtualServers(virtualServers []*f
 
 		resource := fmt.Sprintf("f5-virtualserver/%s/%s", virtualServer.Namespace, virtualServer.Name)
 
-		ttl := getTTLFromAnnotations(virtualServer.Annotations, resource)
+		ttl := annotations.TTLFromAnnotations(virtualServer.Annotations, resource)
 
-		targets := getTargetsFromTargetAnnotation(virtualServer.Annotations)
+		targets := annotations.TargetsFromTargetAnnotation(virtualServer.Annotations)
 		if len(targets) == 0 && virtualServer.Spec.VirtualServerAddress != "" {
 			targets = append(targets, virtualServer.Spec.VirtualServerAddress)
 		}
diff --git a/source/fqdn/fqdn.go b/source/fqdn/fqdn.go
index 50fc16f6..d7ba044a 100644
--- a/source/fqdn/fqdn.go
+++ b/source/fqdn/fqdn.go
@@ -17,6 +17,7 @@ limitations under the License.
 package fqdn
 
 import (
+	"net/netip"
 	"strings"
 	"text/template"
 )
@@ -27,6 +28,34 @@ func ParseTemplate(fqdnTemplate string) (tmpl *template.Template, err error) {
 	}
 	funcs := template.FuncMap{
 		"trimPrefix": strings.TrimPrefix,
+		"replace":    replace,
+		"isIPv6":     isIPv6String,
+		"isIPv4":     isIPv4String,
 	}
 	return template.New("endpoint").Funcs(funcs).Parse(fqdnTemplate)
 }
+
+// replace all instances of oldValue with newValue in target string.
+// adheres to syntax from https://masterminds.github.io/sprig/strings.html.
+func replace(oldValue, newValue, target string) string {
+	return strings.ReplaceAll(target, oldValue, newValue)
+}
+
+// isIPv6String reports whether the target string is an IPv6 address,
+// including IPv4-mapped IPv6 addresses.
+func isIPv6String(target string) bool {
+	netIP, err := netip.ParseAddr(target)
+	if err != nil {
+		return false
+	}
+	return netIP.Is6()
+}
+
+// isIPv4String reports whether the target string is an IPv4 address.
+func isIPv4String(target string) bool {
+	netIP, err := netip.ParseAddr(target)
+	if err != nil {
+		return false
+	}
+	return netIP.Is4()
+}
diff --git a/source/gateway.go b/source/gateway.go
index c903ccae..1bba6c58 100644
--- a/source/gateway.go
+++ b/source/gateway.go
@@ -40,6 +40,7 @@ import (
 	informers_v1beta1 "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions/apis/v1beta1"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 	"sigs.k8s.io/external-dns/source/fqdn"
 )
 
@@ -237,8 +238,8 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo
 		// Create endpoints from hostnames and targets.
 		var routeEndpoints []*endpoint.Endpoint
 		resource := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name)
-		providerSpecific, setIdentifier := getProviderSpecificAnnotations(annots)
-		ttl := getTTLFromAnnotations(annots, resource)
+		providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(annots)
+		ttl := annotations.TTLFromAnnotations(annots, resource)
 		for host, targets := range hostTargets {
 			routeEndpoints = append(routeEndpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
 		}
@@ -375,7 +376,7 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar
 				if !ok {
 					continue
 				}
-				override := getTargetsFromTargetAnnotation(gw.gateway.Annotations)
+				override := annotations.TargetsFromTargetAnnotation(gw.gateway.Annotations)
 				hostTargets[host] = append(hostTargets[host], override...)
 				if len(override) == 0 {
 					for _, addr := range gw.gateway.Status.Addresses {
@@ -405,7 +406,7 @@ func (c *gatewayRouteResolver) hosts(rt gatewayRoute) ([]string, error) {
 	// TODO: The ignore-hostname-annotation flag help says "valid only when using fqdn-template"
 	// but other sources don't check if fqdn-template is set. Which should it be?
 	if !c.src.ignoreHostnameAnnotation {
-		hostnames = append(hostnames, getHostnamesFromAnnotations(rt.Metadata().Annotations)...)
+		hostnames = append(hostnames, annotations.HostnamesFromAnnotations(rt.Metadata().Annotations)...)
 	}
 	// TODO: The combine-fqdn-annotation flag is similarly vague.
 	if c.src.fqdnTemplate != nil && (len(hostnames) == 0 || c.src.combineFQDNAnnotation) {
diff --git a/source/gloo_proxy.go b/source/gloo_proxy.go
index e851b287..5c3e957a 100644
--- a/source/gloo_proxy.go
+++ b/source/gloo_proxy.go
@@ -30,6 +30,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 var (
@@ -134,7 +135,7 @@ func (gs *glooSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
 			}
 			log.Debugf("Gloo: Find %s proxy", proxy.Metadata.Name)
 
-			proxyTargets := getTargetsFromTargetAnnotation(proxy.Metadata.Annotations)
+			proxyTargets := annotations.TargetsFromTargetAnnotation(proxy.Metadata.Annotations)
 			if len(proxyTargets) == 0 {
 				proxyTargets, err = gs.proxyTargets(ctx, proxy.Metadata.Name, ns)
 				if err != nil {
@@ -165,8 +166,8 @@ func (gs *glooSource) generateEndpointsFromProxy(ctx context.Context, proxy *pro
 			if err != nil {
 				return nil, err
 			}
-			ttl := getTTLFromAnnotations(ants, resource)
-			providerSpecific, setIdentifier := getProviderSpecificAnnotations(ants)
+			ttl := annotations.TTLFromAnnotations(ants, resource)
+			providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ants)
 			for _, domain := range virtualHost.Domains {
 				endpoints = append(endpoints, endpointsForHostname(strings.TrimSuffix(domain, "."), targets, ttl, providerSpecific, setIdentifier, "")...)
 			}
diff --git a/source/ingress.go b/source/ingress.go
index 1a63b99a..9a2eec16 100644
--- a/source/ingress.go
+++ b/source/ingress.go
@@ -34,6 +34,7 @@ import (
 	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 	"sigs.k8s.io/external-dns/source/fqdn"
 )
 
@@ -185,14 +186,14 @@ func (sc *ingressSource) endpointsFromTemplate(ing *networkv1.Ingress) ([]*endpo
 
 	resource := fmt.Sprintf("ingress/%s/%s", ing.Namespace, ing.Name)
 
-	ttl := getTTLFromAnnotations(ing.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(ing.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(ing.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(ing.Annotations)
 	if len(targets) == 0 {
 		targets = targetsFromIngressStatus(ing.Status)
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ing.Annotations)
 
 	var endpoints []*endpoint.Endpoint
 	for _, hostname := range hostnames {
@@ -273,15 +274,15 @@ func (sc *ingressSource) filterByIngressClass(ingresses []*networkv1.Ingress) ([
 func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool) []*endpoint.Endpoint {
 	resource := fmt.Sprintf("ingress/%s/%s", ing.Namespace, ing.Name)
 
-	ttl := getTTLFromAnnotations(ing.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(ing.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(ing.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(ing.Annotations)
 
 	if len(targets) == 0 {
 		targets = targetsFromIngressStatus(ing.Status)
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ing.Annotations)
 
 	// Gather endpoints defined on hosts sections of the ingress
 	var definedHostsEndpoints []*endpoint.Endpoint
@@ -310,7 +311,7 @@ func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool,
 	// Gather endpoints defined on annotations in the ingress
 	var annotationEndpoints []*endpoint.Endpoint
 	if !ignoreHostnameAnnotation {
-		for _, hostname := range getHostnamesFromAnnotations(ing.Annotations) {
+		for _, hostname := range annotations.HostnamesFromAnnotations(ing.Annotations) {
 			annotationEndpoints = append(annotationEndpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
 		}
 	}
diff --git a/source/istio_gateway.go b/source/istio_gateway.go
index 734cdef6..546c211e 100644
--- a/source/istio_gateway.go
+++ b/source/istio_gateway.go
@@ -38,6 +38,7 @@ import (
 	"sigs.k8s.io/external-dns/source/fqdn"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 // IstioGatewayIngressSource is the annotation used to determine if the gateway is implemented by an Ingress object
@@ -202,11 +203,7 @@ func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) {
 
 // filterByAnnotations filters a list of configs by a given annotation selector.
 func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gateway) ([]*networkingv1alpha3.Gateway, error) {
-	labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
-	if err != nil {
-		return nil, err
-	}
-	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
+	selector, err := annotations.ParseFilter(sc.annotationFilter)
 	if err != nil {
 		return nil, err
 	}
@@ -228,21 +225,8 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gate
 	return filteredList, nil
 }
 
-func parseIngress(ingress string) (namespace, name string, err error) {
-	parts := strings.Split(ingress, "/")
-	if len(parts) == 2 {
-		namespace, name = parts[0], parts[1]
-	} else if len(parts) == 1 {
-		name = parts[0]
-	} else {
-		err = fmt.Errorf("invalid ingress name (name or namespace/name) found %q", ingress)
-	}
-
-	return
-}
-
 func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
-	namespace, name, err := parseIngress(ingressStr)
+	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)
 	}
@@ -265,44 +249,24 @@ func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr stri
 	return
 }
 
-func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
-	targets = getTargetsFromTargetAnnotation(gateway.Annotations)
+func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) {
+	targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations)
 	if len(targets) > 0 {
-		return
+		return targets, nil
 	}
 
 	ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource]
 	if ok && ingressStr != "" {
-		targets, err = sc.targetsFromIngress(ctx, ingressStr, gateway)
-		return
+		return sc.targetsFromIngress(ctx, ingressStr, gateway)
 	}
 
-	services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(labels.Everything())
-	if err != nil {
-		log.Error(err)
-		return
-	}
-
-	for _, service := range services {
-		if !gatewaySelectorMatchesServiceSelector(gateway.Spec.Selector, service.Spec.Selector) {
-			continue
-		}
-
-		if len(service.Spec.ExternalIPs) > 0 {
-			targets = append(targets, service.Spec.ExternalIPs...)
-			continue
-		}
+	targets, err := EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
 
-		for _, lb := range service.Status.LoadBalancer.Ingress {
-			if lb.IP != "" {
-				targets = append(targets, lb.IP)
-			} else if lb.Hostname != "" {
-				targets = append(targets, lb.Hostname)
-			}
-		}
+	if err != nil {
+		return nil, err
 	}
 
-	return
+	return targets, nil
 }
 
 // endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object
@@ -312,9 +276,9 @@ func (sc *gatewaySource) endpointsFromGateway(ctx context.Context, hostnames []s
 
 	resource := fmt.Sprintf("gateway/%s/%s", gateway.Namespace, gateway.Name)
 
-	ttl := getTTLFromAnnotations(gateway.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(gateway.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(gateway.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations)
 	if len(targets) == 0 {
 		targets, err = sc.targetsFromGateway(ctx, gateway)
 		if err != nil {
@@ -322,7 +286,7 @@ func (sc *gatewaySource) endpointsFromGateway(ctx context.Context, hostnames []s
 		}
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(gateway.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(gateway.Annotations)
 
 	for _, host := range hostnames {
 		endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
@@ -354,7 +318,7 @@ func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1alpha3.Gatewa
 	}
 
 	if !sc.ignoreHostnameAnnotation {
-		hostnames = append(hostnames, getHostnamesFromAnnotations(gateway.Annotations)...)
+		hostnames = append(hostnames, annotations.HostnamesFromAnnotations(gateway.Annotations)...)
 	}
 
 	return hostnames, nil
diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go
index 1c71f8cb..7e76f597 100644
--- a/source/istio_virtualservice.go
+++ b/source/istio_virtualservice.go
@@ -17,6 +17,7 @@ limitations under the License.
 package source
 
 import (
+	"cmp"
 	"context"
 	"fmt"
 	"sort"
@@ -37,6 +38,7 @@ import (
 	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 	"sigs.k8s.io/external-dns/source/fqdn"
 )
 
@@ -192,7 +194,7 @@ func (sc *virtualServiceSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
 }
 
 // AddEventHandler adds an event handler that should be triggered if the watched Istio VirtualService changes.
-func (sc *virtualServiceSource) AddEventHandler(ctx context.Context, handler func()) {
+func (sc *virtualServiceSource) AddEventHandler(_ context.Context, handler func()) {
 	log.Debug("Adding event handler for Istio VirtualService")
 
 	sc.virtualserviceInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
@@ -209,21 +211,19 @@ func (sc *virtualServiceSource) getGateway(_ context.Context, gatewayStr string,
 		log.Debugf("Failed parsing gatewayStr %s of VirtualService %s/%s", gatewayStr, virtualService.Namespace, virtualService.Name)
 		return nil, err
 	}
-	if namespace == "" {
-		namespace = virtualService.Namespace
-	}
+	namespace = cmp.Or(namespace, virtualService.Namespace)
 
 	gateway, err := sc.gatewayInformer.Lister().Gateways(namespace).Get(name)
 	if errors.IsNotFound(err) {
 		log.Warnf("VirtualService (%s/%s) references non-existent gateway: %s ", virtualService.Namespace, virtualService.Name, gatewayStr)
-		return nil, nil
+		return gateway, nil
 	} else if err != nil {
 		log.Errorf("Failed retrieving gateway %s referenced by VirtualService %s/%s: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err)
 		return nil, err
 	}
 	if gateway == nil {
 		log.Debugf("Gateway %s referenced by VirtualService %s/%s not found: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err)
-		return nil, nil
+		return gateway, nil
 	}
 	return gateway, nil
 }
@@ -236,9 +236,9 @@ func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtu
 
 	resource := fmt.Sprintf("virtualservice/%s/%s", virtualService.Namespace, virtualService.Name)
 
-	ttl := getTTLFromAnnotations(virtualService.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(virtualService.Annotations, resource)
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(virtualService.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(virtualService.Annotations)
 
 	var endpoints []*endpoint.Endpoint
 	for _, hostname := range hostnames {
@@ -253,11 +253,7 @@ func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtu
 
 // filterByAnnotations filters a list of configs by a given annotation selector.
 func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkingv1alpha3.VirtualService) ([]*networkingv1alpha3.VirtualService, error) {
-	labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
-	if err != nil {
-		return nil, err
-	}
-	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
+	selector, err := annotations.ParseFilter(sc.annotationFilter)
 	if err != nil {
 		return nil, err
 	}
@@ -293,17 +289,17 @@ func (sc *virtualServiceSource) targetsFromVirtualService(ctx context.Context, v
 	var targets []string
 	// for each host we need to iterate through the gateways because each host might match for only one of the gateways
 	for _, gateway := range virtualService.Spec.Gateways {
-		gateway, err := sc.getGateway(ctx, gateway, virtualService)
+		gw, err := sc.getGateway(ctx, gateway, virtualService)
 		if err != nil {
 			return nil, err
 		}
-		if gateway == nil {
+		if gw == nil {
 			continue
 		}
-		if !virtualServiceBindsToGateway(virtualService, gateway, vsHost) {
+		if !virtualServiceBindsToGateway(virtualService, gw, vsHost) {
 			continue
 		}
-		tgs, err := sc.targetsFromGateway(ctx, gateway)
+		tgs, err := sc.targetsFromGateway(ctx, gw)
 		if err != nil {
 			return targets, err
 		}
@@ -322,11 +318,11 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context,
 
 	resource := fmt.Sprintf("virtualservice/%s/%s", virtualservice.Namespace, virtualservice.Name)
 
-	ttl := getTTLFromAnnotations(virtualservice.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(virtualservice.Annotations, resource)
 
-	targetsFromAnnotation := getTargetsFromTargetAnnotation(virtualservice.Annotations)
+	targetsFromAnnotation := annotations.TargetsFromTargetAnnotation(virtualservice.Annotations)
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(virtualservice.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(virtualservice.Annotations)
 
 	for _, host := range virtualservice.Spec.Hosts {
 		if host == "" || host == "*" {
@@ -354,7 +350,7 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context,
 
 	// Skip endpoints if we do not want entries from annotations
 	if !sc.ignoreHostnameAnnotation {
-		hostnameList := getHostnamesFromAnnotations(virtualservice.Annotations)
+		hostnameList := annotations.HostnamesFromAnnotations(virtualservice.Annotations)
 		for _, hostname := range hostnameList {
 			targets := targetsFromAnnotation
 			if len(targets) == 0 {
@@ -433,7 +429,7 @@ func parseGateway(gateway string) (namespace, name string, err error) {
 }
 
 func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
-	namespace, name, err := parseIngress(ingressStr)
+	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)
 	}
@@ -457,7 +453,7 @@ func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressS
 }
 
 func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
-	targets = getTargetsFromTargetAnnotation(gateway.Annotations)
+	targets = annotations.TargetsFromTargetAnnotation(gateway.Annotations)
 	if len(targets) > 0 {
 		return
 	}
diff --git a/source/kong_tcpingress.go b/source/kong_tcpingress.go
index f710a80d..f65e178e 100644
--- a/source/kong_tcpingress.go
+++ b/source/kong_tcpingress.go
@@ -37,6 +37,7 @@ import (
 	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 var kongGroupdVersionResource = schema.GroupVersionResource{
@@ -126,7 +127,7 @@ func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
 
 	var endpoints []*endpoint.Endpoint
 	for _, tcpIngress := range tcpIngresses {
-		targets := getTargetsFromTargetAnnotation(tcpIngress.Annotations)
+		targets := annotations.TargetsFromTargetAnnotation(tcpIngress.Annotations)
 		if len(targets) == 0 {
 			for _, lb := range tcpIngress.Status.LoadBalancer.Ingress {
 				if lb.IP != "" {
@@ -162,11 +163,7 @@ func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
 
 // filterByAnnotations filters a list of TCPIngresses by a given annotation selector.
 func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress) ([]*TCPIngress, error) {
-	labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
-	if err != nil {
-		return nil, err
-	}
-	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
+	selector, err := annotations.ParseFilter(sc.annotationFilter)
 	if err != nil {
 		return nil, err
 	}
@@ -176,7 +173,7 @@ func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress)
 		return tcpIngresses, nil
 	}
 
-	filteredList := []*TCPIngress{}
+	var filteredList []*TCPIngress
 
 	for _, tcpIngress := range tcpIngresses {
 		// include TCPIngress if its annotations match the selector
@@ -194,12 +191,12 @@ func (sc *kongTCPIngressSource) endpointsFromTCPIngress(tcpIngress *TCPIngress,
 
 	resource := fmt.Sprintf("tcpingress/%s/%s", tcpIngress.Namespace, tcpIngress.Name)
 
-	ttl := getTTLFromAnnotations(tcpIngress.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(tcpIngress.Annotations, resource)
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(tcpIngress.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(tcpIngress.Annotations)
 
 	if !sc.ignoreHostnameAnnotation {
-		hostnameList := getHostnamesFromAnnotations(tcpIngress.Annotations)
+		hostnameList := annotations.HostnamesFromAnnotations(tcpIngress.Annotations)
 		for _, hostname := range hostnameList {
 			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
 		}
diff --git a/source/node.go b/source/node.go
index 02a7ee8f..3f76e665 100644
--- a/source/node.go
+++ b/source/node.go
@@ -23,7 +23,6 @@ import (
 
 	log "github.com/sirupsen/logrus"
 	v1 "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"
@@ -31,6 +30,7 @@ import (
 	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 	"sigs.k8s.io/external-dns/source/fqdn"
 )
 
@@ -43,7 +43,7 @@ type nodeSource struct {
 	nodeInformer         coreinformers.NodeInformer
 	labelSelector        labels.Selector
 	excludeUnschedulable bool
-	exposeInternalIPV6   bool
+	exposeInternalIPv6   bool
 }
 
 // NewNodeSource creates a new nodeSource with the given config.
@@ -81,7 +81,7 @@ func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotat
 		nodeInformer:         nodeInformer,
 		labelSelector:        labelSelector,
 		excludeUnschedulable: excludeUnschedulable,
-		exposeInternalIPV6:   exposeInternalIPv6,
+		exposeInternalIPv6:   exposeInternalIPv6,
 	}, nil
 }
 
@@ -116,7 +116,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
 
 		log.Debugf("creating endpoint for node %s", node.Name)
 
-		ttl := getTTLFromAnnotations(node.Annotations, fmt.Sprintf("node/%s", node.Name))
+		ttl := annotations.TTLFromAnnotations(node.Annotations, fmt.Sprintf("node/%s", node.Name))
 
 		// create new endpoint with the information we already have
 		ep := &endpoint.Endpoint{
@@ -139,7 +139,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
 			log.Debugf("not applying template for %s", node.Name)
 		}
 
-		addrs := getTargetsFromTargetAnnotation(node.Annotations)
+		addrs := annotations.TargetsFromTargetAnnotation(node.Annotations)
 		if len(addrs) == 0 {
 			addrs, err = ns.nodeAddresses(node)
 			if err != nil {
@@ -193,7 +193,7 @@ func (ns *nodeSource) nodeAddresses(node *v1.Node) ([]string, error) {
 	}
 
 	if len(addresses[v1.NodeExternalIP]) > 0 {
-		if ns.exposeInternalIPV6 {
+		if ns.exposeInternalIPv6 {
 			log.Warn(warningMsg)
 			return append(addresses[v1.NodeExternalIP], internalIpv6Addresses...), nil
 		}
@@ -209,11 +209,7 @@ func (ns *nodeSource) nodeAddresses(node *v1.Node) ([]string, error) {
 
 // filterByAnnotations filters a list of nodes by a given annotation selector.
 func (ns *nodeSource) filterByAnnotations(nodes []*v1.Node) ([]*v1.Node, error) {
-	labelSelector, err := metav1.ParseToLabelSelector(ns.annotationFilter)
-	if err != nil {
-		return nil, err
-	}
-	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
+	selector, err := annotations.ParseFilter(ns.annotationFilter)
 	if err != nil {
 		return nil, err
 	}
diff --git a/source/openshift_route.go b/source/openshift_route.go
index c2f01091..d500cd22 100644
--- a/source/openshift_route.go
+++ b/source/openshift_route.go
@@ -29,11 +29,11 @@ import (
 	routeInformer "github.com/openshift/client-go/route/informers/externalversions/route/v1"
 	log "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 	"sigs.k8s.io/external-dns/source/fqdn"
 )
 
@@ -177,15 +177,15 @@ func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routev1.Route) ([]*en
 
 	resource := fmt.Sprintf("route/%s/%s", ocpRoute.Namespace, ocpRoute.Name)
 
-	ttl := getTTLFromAnnotations(ocpRoute.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(ocpRoute.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(ocpRoute.Annotations)
 	if len(targets) == 0 {
 		targetsFromRoute, _ := ors.getTargetsFromRouteStatus(ocpRoute.Status)
 		targets = targetsFromRoute
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ocpRoute.Annotations)
 
 	var endpoints []*endpoint.Endpoint
 	for _, hostname := range hostnames {
@@ -195,11 +195,7 @@ func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routev1.Route) ([]*en
 }
 
 func (ors *ocpRouteSource) filterByAnnotations(ocpRoutes []*routev1.Route) ([]*routev1.Route, error) {
-	labelSelector, err := metav1.ParseToLabelSelector(ors.annotationFilter)
-	if err != nil {
-		return nil, err
-	}
-	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
+	selector, err := annotations.ParseFilter(ors.annotationFilter)
 	if err != nil {
 		return nil, err
 	}
@@ -227,16 +223,16 @@ func (ors *ocpRouteSource) endpointsFromOcpRoute(ocpRoute *routev1.Route, ignore
 
 	resource := fmt.Sprintf("route/%s/%s", ocpRoute.Namespace, ocpRoute.Name)
 
-	ttl := getTTLFromAnnotations(ocpRoute.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(ocpRoute.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(ocpRoute.Annotations)
 	targetsFromRoute, host := ors.getTargetsFromRouteStatus(ocpRoute.Status)
 
 	if len(targets) == 0 {
 		targets = targetsFromRoute
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ocpRoute.Annotations)
 
 	if host != "" {
 		endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
@@ -244,7 +240,7 @@ func (ors *ocpRouteSource) endpointsFromOcpRoute(ocpRoute *routev1.Route, ignore
 
 	// Skip endpoints if we do not want entries from annotations
 	if !ignoreHostnameAnnotation {
-		hostnameList := getHostnamesFromAnnotations(ocpRoute.Annotations)
+		hostnameList := annotations.HostnamesFromAnnotations(ocpRoute.Annotations)
 		for _, hostname := range hostnameList {
 			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
 		}
diff --git a/source/pod.go b/source/pod.go
index 8c77e6ed..f9326371 100644
--- a/source/pod.go
+++ b/source/pod.go
@@ -28,6 +28,8 @@ import (
 	coreinformers "k8s.io/client-go/informers/core/v1"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/cache"
+
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 type podSource struct {
@@ -93,10 +95,10 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 			continue
 		}
 
-		targets := getTargetsFromTargetAnnotation(pod.Annotations)
+		targets := annotations.TargetsFromTargetAnnotation(pod.Annotations)
 
 		if domainAnnotation, ok := pod.Annotations[internalHostnameAnnotationKey]; ok {
-			domainList := splitHostnameAnnotation(domainAnnotation)
+			domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
 			for _, domain := range domainList {
 				if len(targets) == 0 {
 					addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
@@ -109,7 +111,7 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 		}
 
 		if domainAnnotation, ok := pod.Annotations[hostnameAnnotationKey]; ok {
-			domainList := splitHostnameAnnotation(domainAnnotation)
+			domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
 			for _, domain := range domainList {
 				if len(targets) == 0 {
 					node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
@@ -130,14 +132,14 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 
 		if ps.compatibility == "kops-dns-controller" {
 			if domainAnnotation, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok {
-				domainList := splitHostnameAnnotation(domainAnnotation)
+				domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
 				for _, domain := range domainList {
 					addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
 				}
 			}
 
 			if domainAnnotation, ok := pod.Annotations[kopsDNSControllerHostnameAnnotationKey]; ok {
-				domainList := splitHostnameAnnotation(domainAnnotation)
+				domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
 				for _, domain := range domainList {
 					node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
 					for _, address := range node.Status.Addresses {
diff --git a/source/service.go b/source/service.go
index 733512e2..c341d3f8 100644
--- a/source/service.go
+++ b/source/service.go
@@ -33,6 +33,8 @@ import (
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/cache"
 
+	"sigs.k8s.io/external-dns/source/annotations"
+
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/source/fqdn"
 )
@@ -63,10 +65,11 @@ type serviceSource struct {
 	nodeInformer                   coreinformers.NodeInformer
 	serviceTypeFilter              map[string]struct{}
 	labelSelector                  labels.Selector
+	exposeInternalIPv6             bool
 }
 
 // NewServiceSource creates a new serviceSource with the given config.
-func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal, publishHostIP, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool, labelSelector labels.Selector, resolveLoadBalancerHostname, listenEndpointEvents bool) (Source, error) {
+func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal, publishHostIP, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool, labelSelector labels.Selector, resolveLoadBalancerHostname, listenEndpointEvents bool, exposeInternalIPv6 bool) (Source, error) {
 	tmpl, err := fqdn.ParseTemplate(fqdnTemplate)
 	if err != nil {
 		return nil, err
@@ -139,6 +142,7 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name
 		labelSelector:                  labelSelector,
 		resolveLoadBalancerHostname:    resolveLoadBalancerHostname,
 		listenEndpointEvents:           listenEndpointEvents,
+		exposeInternalIPv6:             exposeInternalIPv6,
 	}, nil
 }
 
@@ -308,7 +312,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
 			}
 
 			for _, headlessDomain := range headlessDomains {
-				targets := getTargetsFromTargetAnnotation(pod.Annotations)
+				targets := annotations.TargetsFromTargetAnnotation(pod.Annotations)
 				if len(targets) == 0 {
 					if endpointsType == EndpointsTypeNodeExternalIP {
 						node, err := sc.nodeInformer.Lister().Get(pod.Spec.NodeName)
@@ -317,7 +321,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
 							return endpoints
 						}
 						for _, address := range node.Status.Addresses {
-							if address.Type == v1.NodeExternalIP || (address.Type == v1.NodeInternalIP && suitableType(address.Address) == endpoint.RecordTypeAAAA) {
+							if address.Type == v1.NodeExternalIP || (sc.exposeInternalIPv6 && address.Type == v1.NodeInternalIP && suitableType(address.Address) == endpoint.RecordTypeAAAA) {
 								targets = append(targets, address.Address)
 								log.Debugf("Generating matching endpoint %s with NodeExternalIP %s", headlessDomain, address.Address)
 							}
@@ -387,7 +391,7 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.End
 		return nil, err
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(svc.Annotations)
 
 	var endpoints []*endpoint.Endpoint
 	for _, hostname := range hostnames {
@@ -402,16 +406,16 @@ func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint {
 	var endpoints []*endpoint.Endpoint
 	// Skip endpoints if we do not want entries from annotations
 	if !sc.ignoreHostnameAnnotation {
-		providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
+		providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(svc.Annotations)
 		var hostnameList []string
 		var internalHostnameList []string
 
-		hostnameList = getHostnamesFromAnnotations(svc.Annotations)
+		hostnameList = annotations.HostnamesFromAnnotations(svc.Annotations)
 		for _, hostname := range hostnameList {
 			endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, false)...)
 		}
 
-		internalHostnameList = getInternalHostnamesFromAnnotations(svc.Annotations)
+		internalHostnameList = annotations.InternalHostnamesFromAnnotations(svc.Annotations)
 		for _, hostname := range internalHostnameList {
 			endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, true)...)
 		}
@@ -471,9 +475,9 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
 
 	resource := fmt.Sprintf("service/%s/%s", svc.Namespace, svc.Name)
 
-	ttl := getTTLFromAnnotations(svc.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(svc.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(svc.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(svc.Annotations)
 
 	if len(targets) == 0 {
 		switch svc.Spec.Type {
diff --git a/source/skipper_routegroup.go b/source/skipper_routegroup.go
index 98b05ccb..60105538 100644
--- a/source/skipper_routegroup.go
+++ b/source/skipper_routegroup.go
@@ -36,6 +36,7 @@ import (
 	log "github.com/sirupsen/logrus"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 	"sigs.k8s.io/external-dns/source/fqdn"
 )
 
@@ -304,15 +305,15 @@ func (sc *routeGroupSource) endpointsFromTemplate(rg *routeGroup) ([]*endpoint.E
 	resource := fmt.Sprintf("routegroup/%s/%s", rg.Metadata.Namespace, rg.Metadata.Name)
 
 	// error handled in endpointsFromRouteGroup(), otherwise duplicate log
-	ttl := getTTLFromAnnotations(rg.Metadata.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(rg.Metadata.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(rg.Metadata.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(rg.Metadata.Annotations)
 
 	if len(targets) == 0 {
 		targets = targetsFromRouteGroupStatus(rg.Status)
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(rg.Metadata.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(rg.Metadata.Annotations)
 
 	var endpoints []*endpoint.Endpoint
 	// splits the FQDN template and removes the trailing periods
@@ -330,9 +331,9 @@ func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.
 
 	resource := fmt.Sprintf("routegroup/%s/%s", rg.Metadata.Namespace, rg.Metadata.Name)
 
-	ttl := getTTLFromAnnotations(rg.Metadata.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(rg.Metadata.Annotations, resource)
 
-	targets := getTargetsFromTargetAnnotation(rg.Metadata.Annotations)
+	targets := annotations.TargetsFromTargetAnnotation(rg.Metadata.Annotations)
 	if len(targets) == 0 {
 		for _, lb := range rg.Status.LoadBalancer.RouteGroup {
 			if lb.IP != "" {
@@ -344,7 +345,7 @@ func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.
 		}
 	}
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(rg.Metadata.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(rg.Metadata.Annotations)
 
 	for _, src := range rg.Spec.Hosts {
 		if src == "" {
@@ -355,7 +356,7 @@ func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.
 
 	// Skip endpoints if we do not want entries from annotations
 	if !sc.ignoreHostnameAnnotation {
-		hostnameList := getHostnamesFromAnnotations(rg.Metadata.Annotations)
+		hostnameList := annotations.HostnamesFromAnnotations(rg.Metadata.Annotations)
 		for _, hostname := range hostnameList {
 			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
 		}
diff --git a/source/source.go b/source/source.go
index fbcfd61b..414ff1ad 100644
--- a/source/source.go
+++ b/source/source.go
@@ -20,68 +20,37 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"math"
-	"net/netip"
 	"reflect"
-	"strconv"
 	"strings"
 	"text/template"
 	"time"
 	"unicode"
 
-	log "github.com/sirupsen/logrus"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 const (
-	// The annotation used for figuring out which controller is responsible
-	controllerAnnotationKey = "external-dns.alpha.kubernetes.io/controller"
-	// The annotation used for defining the desired hostname
-	hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname"
-	// The annotation used for specifying whether the public or private interface address is used
-	accessAnnotationKey = "external-dns.alpha.kubernetes.io/access"
-	// The annotation used for specifying the type of endpoints to use for headless services
-	endpointsTypeAnnotationKey = "external-dns.alpha.kubernetes.io/endpoints-type"
-	// The annotation used for defining the desired ingress/service target
-	targetAnnotationKey = "external-dns.alpha.kubernetes.io/target"
-	// The annotation used for defining the desired DNS record TTL
-	ttlAnnotationKey = "external-dns.alpha.kubernetes.io/ttl"
-	// The annotation used for switching to the alias record types e. g. AWS Alias records instead of a normal CNAME
-	aliasAnnotationKey = "external-dns.alpha.kubernetes.io/alias"
-	// The annotation used to determine the source of hostnames for ingresses.  This is an optional field - all
-	// available hostname sources are used if not specified.
-	ingressHostnameSourceKey = "external-dns.alpha.kubernetes.io/ingress-hostname-source"
-	// The value of the controller annotation so that we feel responsible
-	controllerAnnotationValue = "dns-controller"
-	// The annotation used for defining the desired hostname
-	internalHostnameAnnotationKey = "external-dns.alpha.kubernetes.io/internal-hostname"
-)
+	controllerAnnotationKey       = annotations.ControllerKey
+	hostnameAnnotationKey         = annotations.HostnameKey
+	accessAnnotationKey           = annotations.AccessKey
+	endpointsTypeAnnotationKey    = annotations.EndpointsTypeKey
+	targetAnnotationKey           = annotations.TargetKey
+	ttlAnnotationKey              = annotations.TtlKey
+	aliasAnnotationKey            = annotations.AliasKey
+	ingressHostnameSourceKey      = annotations.IngressHostnameSourceKey
+	controllerAnnotationValue     = annotations.ControllerValue
+	internalHostnameAnnotationKey = annotations.InternalHostnameKey
 
-const (
 	EndpointsTypeNodeExternalIP = "NodeExternalIP"
 	EndpointsTypeHostIP         = "HostIP"
 )
 
-// Provider-specific annotations
-const (
-	// The annotation used for determining if traffic will go through Cloudflare
-	CloudflareProxiedKey        = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
-	CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname"
-	CloudflareRegionKey         = "external-dns.alpha.kubernetes.io/cloudflare-region-key"
-
-	SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier"
-)
-
-const (
-	ttlMinimum = 1
-	ttlMaximum = math.MaxInt32
-)
-
 // Source defines the interface Endpoint sources should implement.
 type Source interface {
 	Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error)
@@ -89,43 +58,6 @@ type Source interface {
 	AddEventHandler(context.Context, func())
 }
 
-func getTTLFromAnnotations(ants map[string]string, resource string) endpoint.TTL {
-	ttlNotConfigured := endpoint.TTL(0)
-	ttlAnnotation, ok := ants[ttlAnnotationKey]
-	if !ok {
-		return ttlNotConfigured
-	}
-	ttlValue, err := parseTTL(ttlAnnotation)
-	if err != nil {
-		log.Warnf("%s: \"%v\" is not a valid TTL value: %v", resource, ttlAnnotation, err)
-		return ttlNotConfigured
-	}
-	if ttlValue < ttlMinimum || ttlValue > ttlMaximum {
-		log.Warnf("TTL value %q must be between [%d, %d]", ttlValue, ttlMinimum, ttlMaximum)
-		return ttlNotConfigured
-	}
-	return endpoint.TTL(ttlValue)
-}
-
-// parseTTL parses TTL from string, returning duration in seconds.
-// parseTTL supports both integers like "600" and durations based
-// on Go Duration like "10m", hence "600" and "10m" represent the same value.
-//
-// Note: for durations like "1.5s" the fraction is omitted (resulting in 1 second
-// for the example).
-func parseTTL(s string) (ttlSeconds int64, err error) {
-	ttlDuration, errDuration := time.ParseDuration(s)
-	if errDuration != nil {
-		ttlInt, err := strconv.ParseInt(s, 10, 64)
-		if err != nil {
-			return 0, errDuration
-		}
-		return ttlInt, nil
-	}
-
-	return int64(ttlDuration.Seconds()), nil
-}
-
 type kubeObject interface {
 	runtime.Object
 	metav1.Object
@@ -145,188 +77,14 @@ func execTemplate(tmpl *template.Template, obj kubeObject) (hostnames []string,
 	return hostnames, nil
 }
 
-func getHostnamesFromAnnotations(annotations map[string]string) []string {
-	hostnameAnnotation, exists := annotations[hostnameAnnotationKey]
-	if !exists {
-		return nil
-	}
-	return splitHostnameAnnotation(hostnameAnnotation)
-}
-
-func getAccessFromAnnotations(annotations map[string]string) string {
-	return annotations[accessAnnotationKey]
+func getAccessFromAnnotations(input map[string]string) string {
+	return input[accessAnnotationKey]
 }
 
 func getEndpointsTypeFromAnnotations(annotations map[string]string) string {
 	return annotations[endpointsTypeAnnotationKey]
 }
 
-func getInternalHostnamesFromAnnotations(ants map[string]string) []string {
-	internalHostnameAnnotation, ok := ants[internalHostnameAnnotationKey]
-	if !ok {
-		return nil
-	}
-	return splitHostnameAnnotation(internalHostnameAnnotation)
-}
-
-func splitHostnameAnnotation(annotation string) []string {
-	return strings.Split(strings.ReplaceAll(annotation, " ", ""), ",")
-}
-
-func getAliasFromAnnotations(ants map[string]string) bool {
-	aliasAnnotation, exists := ants[aliasAnnotationKey]
-	return exists && aliasAnnotation == "true"
-}
-
-func getProviderSpecificAnnotations(ants map[string]string) (endpoint.ProviderSpecific, string) {
-	providerSpecificAnnotations := endpoint.ProviderSpecific{}
-
-	if v, exists := ants[CloudflareProxiedKey]; exists {
-		providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
-			Name:  CloudflareProxiedKey,
-			Value: v,
-		})
-	}
-	if v, exists := ants[CloudflareCustomHostnameKey]; exists {
-		providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
-			Name:  CloudflareCustomHostnameKey,
-			Value: v,
-		})
-	}
-	if v, exists := ants[CloudflareRegionKey]; exists {
-		providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
-			Name:  CloudflareRegionKey,
-			Value: v,
-		})
-	}
-	if getAliasFromAnnotations(ants) {
-		providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
-			Name:  "alias",
-			Value: "true",
-		})
-	}
-	setIdentifier := ""
-	for k, v := range ants {
-		if k == SetIdentifierKey {
-			setIdentifier = v
-		} else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/aws-") {
-			attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/aws-")
-			providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
-				Name:  fmt.Sprintf("aws/%s", attr),
-				Value: v,
-			})
-		} else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/scw-") {
-			attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/scw-")
-			providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
-				Name:  fmt.Sprintf("scw/%s", attr),
-				Value: v,
-			})
-		} else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-") {
-			attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-")
-			providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
-				Name:  fmt.Sprintf("ibmcloud-%s", attr),
-				Value: v,
-			})
-		} else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/webhook-") {
-			// Support for wildcard annotations for webhook providers
-			attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/webhook-")
-			providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
-				Name:  fmt.Sprintf("webhook/%s", attr),
-				Value: v,
-			})
-		}
-	}
-	return providerSpecificAnnotations, setIdentifier
-}
-
-// getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
-// Returns empty endpoints array if none are found.
-func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets {
-	var targets endpoint.Targets
-
-	// Get the desired hostname of the ingress from the annotation.
-	targetAnnotation, exists := annotations[targetAnnotationKey]
-	if exists && targetAnnotation != "" {
-		// splits the hostname annotation and removes the trailing periods
-		targetsList := strings.Split(strings.ReplaceAll(targetAnnotation, " ", ""), ",")
-		for _, targetHostname := range targetsList {
-			targetHostname = strings.TrimSuffix(targetHostname, ".")
-			targets = append(targets, targetHostname)
-		}
-	}
-	return targets
-}
-
-// suitableType returns the DNS resource record type suitable for the target.
-// In this case type A/AAAA for IPs and type CNAME for everything else.
-func suitableType(target string) string {
-	netIP, err := netip.ParseAddr(target)
-	if err == nil && netIP.Is4() {
-		return endpoint.RecordTypeA
-	} else if err == nil && netIP.Is6() {
-		return endpoint.RecordTypeAAAA
-	}
-	return endpoint.RecordTypeCNAME
-}
-
-// endpointsForHostname returns the endpoint objects for each host-target combination.
-func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint {
-	var endpoints []*endpoint.Endpoint
-
-	var aTargets endpoint.Targets
-	var aaaaTargets endpoint.Targets
-	var cnameTargets endpoint.Targets
-
-	for _, t := range targets {
-		switch suitableType(t) {
-		case endpoint.RecordTypeA:
-			aTargets = append(aTargets, t)
-		case endpoint.RecordTypeAAAA:
-			aaaaTargets = append(aaaaTargets, t)
-		default:
-			cnameTargets = append(cnameTargets, t)
-		}
-	}
-
-	if len(aTargets) > 0 {
-		epA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, ttl, aTargets...)
-		if epA != nil {
-			epA.ProviderSpecific = providerSpecific
-			epA.SetIdentifier = setIdentifier
-			if resource != "" {
-				epA.Labels[endpoint.ResourceLabelKey] = resource
-			}
-			endpoints = append(endpoints, epA)
-		}
-	}
-
-	if len(aaaaTargets) > 0 {
-		epAAAA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeAAAA, ttl, aaaaTargets...)
-		if epAAAA != nil {
-			epAAAA.ProviderSpecific = providerSpecific
-			epAAAA.SetIdentifier = setIdentifier
-			if resource != "" {
-				epAAAA.Labels[endpoint.ResourceLabelKey] = resource
-			}
-			endpoints = append(endpoints, epAAAA)
-		}
-	}
-
-	if len(cnameTargets) > 0 {
-		epCNAME := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeCNAME, ttl, cnameTargets...)
-		if epCNAME != nil {
-			epCNAME.ProviderSpecific = providerSpecific
-			epCNAME.SetIdentifier = setIdentifier
-			if resource != "" {
-				epCNAME.Labels[endpoint.ResourceLabelKey] = resource
-			}
-			endpoints = append(endpoints, epCNAME)
-		}
-	}
-
-	return endpoints
-}
-
 func getLabelSelector(annotationFilter string) (labels.Selector, error) {
 	labelSelector, err := metav1.ParseToLabelSelector(annotationFilter)
 	if err != nil {
diff --git a/source/store.go b/source/store.go
index 8a9149ba..f8ef39b5 100644
--- a/source/store.go
+++ b/source/store.go
@@ -273,7 +273,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg
 		if err != nil {
 			return nil, err
 		}
-		return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents)
+		return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, cfg.ExposeInternalIPv6)
 	case "ingress":
 		client, err := p.KubeClient()
 		if err != nil {
diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go
index 1fd919a4..212b604c 100644
--- a/source/traefik_proxy.go
+++ b/source/traefik_proxy.go
@@ -38,6 +38,7 @@ import (
 	"k8s.io/client-go/tools/cache"
 
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 var (
@@ -170,7 +171,7 @@ func NewTraefikSource(ctx context.Context, dynamicKubeClient dynamic.Interface,
 	}, nil
 }
 
-func (ts *traefikSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
+func (ts *traefikSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) {
 	var endpoints []*endpoint.Endpoint
 
 	if ts.ingressRouteInformer != nil {
@@ -225,54 +226,18 @@ func (ts *traefikSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
 
 // ingressRouteEndpoints extracts endpoints from all IngressRoute objects
 func (ts *traefikSource) ingressRouteEndpoints() ([]*endpoint.Endpoint, error) {
-	var endpoints []*endpoint.Endpoint
-
-	irs, err := ts.ingressRouteInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything())
-	if err != nil {
-		return nil, err
-	}
-
-	var ingressRoutes []*IngressRoute
-	for _, ingressRouteObj := range irs {
-		unstructuredHost, ok := ingressRouteObj.(*unstructured.Unstructured)
-		if !ok {
-			return nil, errors.New("could not convert IngressRoute object to unstructured")
-		}
-
-		ingressRoute := &IngressRoute{}
-		err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil)
-		if err != nil {
-			return nil, err
-		}
-		ingressRoutes = append(ingressRoutes, ingressRoute)
-	}
-
-	ingressRoutes, err = ts.filterIngressRouteByAnnotation(ingressRoutes)
-	if err != nil {
-		return nil, fmt.Errorf("failed to filter IngressRoute: %w", err)
-	}
-
-	for _, ingressRoute := range ingressRoutes {
-		var targets endpoint.Targets
-
-		targets = append(targets, getTargetsFromTargetAnnotation(ingressRoute.Annotations)...)
-
-		fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name)
-
-		ingressEndpoints, err := ts.endpointsFromIngressRoute(ingressRoute, targets)
-		if err != nil {
-			return nil, err
-		}
-		if len(ingressEndpoints) == 0 {
-			log.Debugf("No endpoints could be generated from Host %s", fullname)
-			continue
-		}
-
-		log.Debugf("Endpoints generated from IngressRoute: %s: %v", fullname, ingressEndpoints)
-		endpoints = append(endpoints, ingressEndpoints...)
-	}
-
-	return endpoints, nil
+	return extractEndpoints[IngressRoute](
+		ts.ingressRouteInformer.Lister(),
+		ts.namespace,
+		func(u *unstructured.Unstructured) (*IngressRoute, error) {
+			typed := &IngressRoute{}
+			return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil)
+		},
+		ts.filterIngressRouteByAnnotation,
+		func(r *IngressRoute, targets endpoint.Targets) []*endpoint.Endpoint {
+			return ts.endpointsFromIngressRoute(r, targets)
+		},
+	)
 }
 
 // ingressRouteTCPEndpoints extracts endpoints from all IngressRouteTCP objects
@@ -307,14 +272,11 @@ func (ts *traefikSource) ingressRouteTCPEndpoints() ([]*endpoint.Endpoint, error
 	for _, ingressRouteTCP := range ingressRouteTCPs {
 		var targets endpoint.Targets
 
-		targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteTCP.Annotations)...)
+		targets = append(targets, annotations.TargetsFromTargetAnnotation(ingressRouteTCP.Annotations)...)
 
 		fullname := fmt.Sprintf("%s/%s", ingressRouteTCP.Namespace, ingressRouteTCP.Name)
 
-		ingressEndpoints, err := ts.endpointsFromIngressRouteTCP(ingressRouteTCP, targets)
-		if err != nil {
-			return nil, err
-		}
+		ingressEndpoints := ts.endpointsFromIngressRouteTCP(ingressRouteTCP, targets)
 		if len(ingressEndpoints) == 0 {
 			log.Debugf("No endpoints could be generated from Host %s", fullname)
 			continue
@@ -329,320 +291,104 @@ func (ts *traefikSource) ingressRouteTCPEndpoints() ([]*endpoint.Endpoint, error
 
 // ingressRouteUDPEndpoints extracts endpoints from all IngressRouteUDP objects
 func (ts *traefikSource) ingressRouteUDPEndpoints() ([]*endpoint.Endpoint, error) {
-	var endpoints []*endpoint.Endpoint
-
-	irs, err := ts.ingressRouteUdpInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything())
-	if err != nil {
-		return nil, err
-	}
-
-	var ingressRouteUDPs []*IngressRouteUDP
-	for _, ingressRouteUDPObj := range irs {
-		unstructuredHost, ok := ingressRouteUDPObj.(*unstructured.Unstructured)
-		if !ok {
-			return nil, errors.New("could not convert IngressRouteUDP object to unstructured")
-		}
-
-		ingressRoute := &IngressRouteUDP{}
-		err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil)
-		if err != nil {
-			return nil, err
-		}
-		ingressRouteUDPs = append(ingressRouteUDPs, ingressRoute)
-	}
-
-	ingressRouteUDPs, err = ts.filterIngressRouteUdpByAnnotations(ingressRouteUDPs)
-	if err != nil {
-		return nil, fmt.Errorf("failed to filter IngressRouteUDP: %w", err)
-	}
-
-	for _, ingressRouteUDP := range ingressRouteUDPs {
-		var targets endpoint.Targets
-
-		targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteUDP.Annotations)...)
-
-		fullname := fmt.Sprintf("%s/%s", ingressRouteUDP.Namespace, ingressRouteUDP.Name)
-
-		ingressEndpoints, err := ts.endpointsFromIngressRouteUDP(ingressRouteUDP, targets)
-		if err != nil {
-			return nil, err
-		}
-		if len(ingressEndpoints) == 0 {
-			log.Debugf("No endpoints could be generated from Host %s", fullname)
-			continue
-		}
-
-		log.Debugf("Endpoints generated from IngressRouteUDP: %s: %v", fullname, ingressEndpoints)
-		endpoints = append(endpoints, ingressEndpoints...)
-	}
-
-	return endpoints, nil
+	return extractEndpoints[IngressRouteUDP](
+		ts.ingressRouteUdpInformer.Lister(),
+		ts.namespace,
+		func(u *unstructured.Unstructured) (*IngressRouteUDP, error) {
+			typed := &IngressRouteUDP{}
+			return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil)
+		},
+		ts.filterIngressRouteUdpByAnnotations,
+		ts.endpointsFromIngressRouteUDP,
+	)
 }
 
 // oldIngressRouteEndpoints extracts endpoints from all IngressRoute objects
 func (ts *traefikSource) oldIngressRouteEndpoints() ([]*endpoint.Endpoint, error) {
-	var endpoints []*endpoint.Endpoint
-
-	irs, err := ts.oldIngressRouteInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything())
-	if err != nil {
-		return nil, err
-	}
-
-	var ingressRoutes []*IngressRoute
-	for _, ingressRouteObj := range irs {
-		unstructuredHost, ok := ingressRouteObj.(*unstructured.Unstructured)
-		if !ok {
-			return nil, errors.New("could not convert IngressRoute object to unstructured")
-		}
-
-		ingressRoute := &IngressRoute{}
-		err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil)
-		if err != nil {
-			return nil, err
-		}
-		ingressRoutes = append(ingressRoutes, ingressRoute)
-	}
-
-	ingressRoutes, err = ts.filterIngressRouteByAnnotation(ingressRoutes)
-	if err != nil {
-		return nil, fmt.Errorf("failed to filter IngressRoute: %w", err)
-	}
-
-	for _, ingressRoute := range ingressRoutes {
-		var targets endpoint.Targets
-
-		targets = append(targets, getTargetsFromTargetAnnotation(ingressRoute.Annotations)...)
-
-		fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name)
-
-		ingressEndpoints, err := ts.endpointsFromIngressRoute(ingressRoute, targets)
-		if err != nil {
-			return nil, err
-		}
-		if len(ingressEndpoints) == 0 {
-			log.Debugf("No endpoints could be generated from Host %s", fullname)
-			continue
-		}
-
-		log.Debugf("Endpoints generated from IngressRoute: %s: %v", fullname, ingressEndpoints)
-		endpoints = append(endpoints, ingressEndpoints...)
-	}
-
-	return endpoints, nil
+	return extractEndpoints[IngressRoute](
+		ts.oldIngressRouteInformer.Lister(),
+		ts.namespace,
+		func(u *unstructured.Unstructured) (*IngressRoute, error) {
+			typed := &IngressRoute{}
+			return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil)
+		},
+		ts.filterIngressRouteByAnnotation,
+		func(r *IngressRoute, targets endpoint.Targets) []*endpoint.Endpoint {
+			return ts.endpointsFromIngressRoute(r, targets)
+		},
+	)
 }
 
 // oldIngressRouteTCPEndpoints extracts endpoints from all IngressRouteTCP objects
 func (ts *traefikSource) oldIngressRouteTCPEndpoints() ([]*endpoint.Endpoint, error) {
-	var endpoints []*endpoint.Endpoint
-
-	irs, err := ts.oldIngressRouteTcpInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything())
-	if err != nil {
-		return nil, err
-	}
-
-	var ingressRouteTCPs []*IngressRouteTCP
-	for _, ingressRouteTCPObj := range irs {
-		unstructuredHost, ok := ingressRouteTCPObj.(*unstructured.Unstructured)
-		if !ok {
-			return nil, errors.New("could not convert IngressRouteTCP object to unstructured")
-		}
-
-		ingressRouteTCP := &IngressRouteTCP{}
-		err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRouteTCP, nil)
-		if err != nil {
-			return nil, err
-		}
-		ingressRouteTCPs = append(ingressRouteTCPs, ingressRouteTCP)
-	}
-
-	ingressRouteTCPs, err = ts.filterIngressRouteTcpByAnnotations(ingressRouteTCPs)
-	if err != nil {
-		return nil, fmt.Errorf("failed to filter IngressRouteTCP: %w", err)
-	}
-
-	for _, ingressRouteTCP := range ingressRouteTCPs {
-		var targets endpoint.Targets
-
-		targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteTCP.Annotations)...)
-
-		fullname := fmt.Sprintf("%s/%s", ingressRouteTCP.Namespace, ingressRouteTCP.Name)
-
-		ingressEndpoints, err := ts.endpointsFromIngressRouteTCP(ingressRouteTCP, targets)
-		if err != nil {
-			return nil, err
-		}
-		if len(ingressEndpoints) == 0 {
-			log.Debugf("No endpoints could be generated from Host %s", fullname)
-			continue
-		}
-
-		log.Debugf("Endpoints generated from IngressRouteTCP: %s: %v", fullname, ingressEndpoints)
-		endpoints = append(endpoints, ingressEndpoints...)
-	}
-
-	return endpoints, nil
+	return extractEndpoints[IngressRouteTCP](
+		ts.oldIngressRouteTcpInformer.Lister(),
+		ts.namespace,
+		func(u *unstructured.Unstructured) (*IngressRouteTCP, error) {
+			typed := &IngressRouteTCP{}
+			return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil)
+		},
+		ts.filterIngressRouteTcpByAnnotations,
+		ts.endpointsFromIngressRouteTCP,
+	)
 }
 
 // oldIngressRouteUDPEndpoints extracts endpoints from all IngressRouteUDP objects
 func (ts *traefikSource) oldIngressRouteUDPEndpoints() ([]*endpoint.Endpoint, error) {
-	var endpoints []*endpoint.Endpoint
-
-	irs, err := ts.oldIngressRouteUdpInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything())
-	if err != nil {
-		return nil, err
-	}
-
-	var ingressRouteUDPs []*IngressRouteUDP
-	for _, ingressRouteUDPObj := range irs {
-		unstructuredHost, ok := ingressRouteUDPObj.(*unstructured.Unstructured)
-		if !ok {
-			return nil, errors.New("could not convert IngressRouteUDP object to unstructured")
-		}
-
-		ingressRoute := &IngressRouteUDP{}
-		err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil)
-		if err != nil {
-			return nil, err
-		}
-		ingressRouteUDPs = append(ingressRouteUDPs, ingressRoute)
-	}
-
-	ingressRouteUDPs, err = ts.filterIngressRouteUdpByAnnotations(ingressRouteUDPs)
-	if err != nil {
-		return nil, fmt.Errorf("failed to filter IngressRouteUDP: %w", err)
-	}
-
-	for _, ingressRouteUDP := range ingressRouteUDPs {
-		var targets endpoint.Targets
-
-		targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteUDP.Annotations)...)
-
-		fullname := fmt.Sprintf("%s/%s", ingressRouteUDP.Namespace, ingressRouteUDP.Name)
-
-		ingressEndpoints, err := ts.endpointsFromIngressRouteUDP(ingressRouteUDP, targets)
-		if err != nil {
-			return nil, err
-		}
-		if len(ingressEndpoints) == 0 {
-			log.Debugf("No endpoints could be generated from Host %s", fullname)
-			continue
-		}
-
-		log.Debugf("Endpoints generated from IngressRouteUDP: %s: %v", fullname, ingressEndpoints)
-		endpoints = append(endpoints, ingressEndpoints...)
-	}
-
-	return endpoints, nil
+	return extractEndpoints[IngressRouteUDP](
+		ts.oldIngressRouteUdpInformer.Lister(),
+		ts.namespace,
+		func(u *unstructured.Unstructured) (*IngressRouteUDP, error) {
+			typed := &IngressRouteUDP{}
+			return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil)
+		},
+		ts.filterIngressRouteUdpByAnnotations,
+		ts.endpointsFromIngressRouteUDP,
+	)
 }
 
 // filterIngressRouteByAnnotation filters a list of IngressRoute by a given annotation selector.
-func (ts *traefikSource) filterIngressRouteByAnnotation(ingressRoutes []*IngressRoute) ([]*IngressRoute, error) {
-	labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter)
-	if err != nil {
-		return nil, err
-	}
-	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	// empty filter returns original list
-	if selector.Empty() {
-		return ingressRoutes, nil
-	}
-
-	filteredList := []*IngressRoute{}
-
-	for _, ingressRoute := range ingressRoutes {
-		// include IngressRoute if its annotations match the selector
-		if selector.Matches(labels.Set(ingressRoute.Annotations)) {
-			filteredList = append(filteredList, ingressRoute)
-		}
-	}
-
-	return filteredList, nil
+func (ts *traefikSource) filterIngressRouteByAnnotation(input []*IngressRoute) ([]*IngressRoute, error) {
+	return filterResourcesByAnnotations(input, ts.annotationFilter, func(ir *IngressRoute) map[string]string {
+		return ir.Annotations
+	})
 }
 
 // filterIngressRouteTcpByAnnotations filters a list of IngressRouteTCP by a given annotation selector.
-func (ts *traefikSource) filterIngressRouteTcpByAnnotations(ingressRoutes []*IngressRouteTCP) ([]*IngressRouteTCP, error) {
-	labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter)
-	if err != nil {
-		return nil, err
-	}
-	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	// empty filter returns original list
-	if selector.Empty() {
-		return ingressRoutes, nil
-	}
-
-	var filteredList []*IngressRouteTCP
-
-	for _, ingressRoute := range ingressRoutes {
-		// include IngressRoute if its annotations match the selector
-		if selector.Matches(labels.Set(ingressRoute.Annotations)) {
-			filteredList = append(filteredList, ingressRoute)
-		}
-	}
-
-	return filteredList, nil
+func (ts *traefikSource) filterIngressRouteTcpByAnnotations(input []*IngressRouteTCP) ([]*IngressRouteTCP, error) {
+	return filterResourcesByAnnotations(input, ts.annotationFilter, func(ir *IngressRouteTCP) map[string]string {
+		return ir.Annotations
+	})
 }
 
 // filterIngressRouteUdpByAnnotations filters a list of IngressRoute by a given annotation selector.
-func (ts *traefikSource) filterIngressRouteUdpByAnnotations(ingressRoutes []*IngressRouteUDP) ([]*IngressRouteUDP, error) {
-	labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter)
-	if err != nil {
-		return nil, err
-	}
-	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	// empty filter returns original list
-	if selector.Empty() {
-		return ingressRoutes, nil
-	}
-
-	var filteredList []*IngressRouteUDP
-
-	for _, ingressRoute := range ingressRoutes {
-		// include IngressRoute if its annotations match the selector
-		if selector.Matches(labels.Set(ingressRoute.Annotations)) {
-			filteredList = append(filteredList, ingressRoute)
-		}
-	}
-
-	return filteredList, nil
+func (ts *traefikSource) filterIngressRouteUdpByAnnotations(input []*IngressRouteUDP) ([]*IngressRouteUDP, error) {
+	return filterResourcesByAnnotations(input, ts.annotationFilter, func(ir *IngressRouteUDP) map[string]string {
+		return ir.Annotations
+	})
 }
 
 // endpointsFromIngressRoute extracts the endpoints from a IngressRoute object
-func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *IngressRoute, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
+func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *IngressRoute, targets endpoint.Targets) []*endpoint.Endpoint {
 	var endpoints []*endpoint.Endpoint
 
 	resource := fmt.Sprintf("ingressroute/%s/%s", ingressRoute.Namespace, ingressRoute.Name)
 
-	ttl := getTTLFromAnnotations(ingressRoute.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(ingressRoute.Annotations, resource)
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ingressRoute.Annotations)
 
 	if !ts.ignoreHostnameAnnotation {
-		hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations)
+		hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations)
 		for _, hostname := range hostnameList {
 			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
 		}
 	}
 
 	for _, route := range ingressRoute.Spec.Routes {
-		match := route.Match
-
-		for _, hostEntry := range traefikHostExtractor.FindAllString(match, -1) {
+		for _, hostEntry := range traefikHostExtractor.FindAllString(route.Match, -1) {
 			for _, host := range traefikValueProcessor.FindAllString(hostEntry, -1) {
-				host = strings.TrimPrefix(host, "`")
-				host = strings.TrimSuffix(host, "`")
+				host = strings.Trim(host, "`")
 
 				// Checking for host = * is required, as Host(`*`) can be set
 				if host != "*" && host != "" {
@@ -652,34 +398,30 @@ func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *IngressRoute, t
 		}
 	}
 
-	return endpoints, nil
+	return endpoints
 }
 
 // endpointsFromIngressRouteTCP extracts the endpoints from a IngressRouteTCP object
-func (ts *traefikSource) endpointsFromIngressRouteTCP(ingressRoute *IngressRouteTCP, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
+func (ts *traefikSource) endpointsFromIngressRouteTCP(ingressRoute *IngressRouteTCP, targets endpoint.Targets) []*endpoint.Endpoint {
 	var endpoints []*endpoint.Endpoint
 
 	resource := fmt.Sprintf("ingressroutetcp/%s/%s", ingressRoute.Namespace, ingressRoute.Name)
 
-	ttl := getTTLFromAnnotations(ingressRoute.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(ingressRoute.Annotations, resource)
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ingressRoute.Annotations)
 
 	if !ts.ignoreHostnameAnnotation {
-		hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations)
+		hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations)
 		for _, hostname := range hostnameList {
 			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
 		}
 	}
 
 	for _, route := range ingressRoute.Spec.Routes {
-		match := route.Match
-
-		for _, hostEntry := range traefikHostExtractor.FindAllString(match, -1) {
+		for _, hostEntry := range traefikHostExtractor.FindAllString(route.Match, -1) {
 			for _, host := range traefikValueProcessor.FindAllString(hostEntry, -1) {
-				host = strings.TrimPrefix(host, "`")
-				host = strings.TrimSuffix(host, "`")
-
+				host = strings.Trim(host, "`")
 				// Checking for host = * is required, as HostSNI(`*`) can be set
 				// in the case of TLS passthrough
 				if host != "*" && host != "" {
@@ -689,27 +431,27 @@ func (ts *traefikSource) endpointsFromIngressRouteTCP(ingressRoute *IngressRoute
 		}
 	}
 
-	return endpoints, nil
+	return endpoints
 }
 
 // endpointsFromIngressRouteUDP extracts the endpoints from a IngressRouteUDP object
-func (ts *traefikSource) endpointsFromIngressRouteUDP(ingressRoute *IngressRouteUDP, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
+func (ts *traefikSource) endpointsFromIngressRouteUDP(ingressRoute *IngressRouteUDP, targets endpoint.Targets) []*endpoint.Endpoint {
 	var endpoints []*endpoint.Endpoint
 
 	resource := fmt.Sprintf("ingressrouteudp/%s/%s", ingressRoute.Namespace, ingressRoute.Name)
 
-	ttl := getTTLFromAnnotations(ingressRoute.Annotations, resource)
+	ttl := annotations.TTLFromAnnotations(ingressRoute.Annotations, resource)
 
-	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations)
+	providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ingressRoute.Annotations)
 
 	if !ts.ignoreHostnameAnnotation {
-		hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations)
+		hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations)
 		for _, hostname := range hostnameList {
 			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
 		}
 	}
 
-	return endpoints, nil
+	return endpoints
 }
 
 func (ts *traefikSource) AddEventHandler(ctx context.Context, handler func()) {
@@ -1092,3 +834,119 @@ func (in *IngressRouteUDPList) DeepCopyObject() runtime.Object {
 	}
 	return nil
 }
+
+// extractEndpoints is a generic function that extracts endpoints from Kubernetes resources.
+// It performs the following steps:
+// 1. Lists all objects in the specified namespace using the provided informer.
+// 2. Converts the unstructured objects to the desired type using the convertFunc.
+// 3. Filters the converted objects based on the provided filterFunc.
+// 4. Generates endpoints for each filtered object using the generateEndpoints function.
+// Returns a list of generated endpoints or an error if any step fails.
+func extractEndpoints[T any](
+	informer cache.GenericLister,
+	namespace string,
+	convertFunc func(*unstructured.Unstructured) (*T, error),
+	filterFunc func([]*T) ([]*T, error),
+	generateEndpoints func(*T, endpoint.Targets) []*endpoint.Endpoint,
+) ([]*endpoint.Endpoint, error) {
+	var endpoints []*endpoint.Endpoint
+
+	objs, err := informer.ByNamespace(namespace).List(labels.Everything())
+	if err != nil {
+		return nil, err
+	}
+
+	var typedObjs []*T
+	for _, obj := range objs {
+		unstructuredObj, ok := obj.(*unstructured.Unstructured)
+		if !ok {
+			return nil, errors.New("failed to cast to unstructured.Unstructured")
+		}
+
+		typed, err := convertFunc(unstructuredObj)
+		if err != nil {
+			return nil, err
+		}
+		typedObjs = append(typedObjs, typed)
+	}
+
+	typedObjs, err = filterFunc(typedObjs)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, item := range typedObjs {
+		targets := annotations.TargetsFromTargetAnnotation(getAnnotations(item))
+
+		name := getObjectFullName(item)
+		ingressEndpoints := generateEndpoints(item, targets)
+
+		if len(ingressEndpoints) == 0 {
+			log.Debugf("No endpoints could be generated from Host %s", name)
+			continue
+		}
+
+		log.Debugf("Endpoints generated from %s: %v", name, ingressEndpoints)
+		endpoints = append(endpoints, ingressEndpoints...)
+	}
+
+	return endpoints, nil
+}
+
+// filterResourcesByAnnotations filters a list of resources based on a given annotation selector.
+// It performs the following steps:
+// 1. Parses the annotation filter into a label selector.
+// 2. Converts the label selector into a Kubernetes selector.
+// 3. If the selector is empty, returns the original list of resources.
+// 4. Iterates through the resources and matches their annotations against the selector.
+// 5. Returns the filtered list of resources or an error if any step fails.
+func filterResourcesByAnnotations[T any](resources []*T, annotationFilter string, getAnnotations func(*T) map[string]string) ([]*T, error) {
+	labelSelector, err := metav1.ParseToLabelSelector(annotationFilter)
+	if err != nil {
+		return nil, err
+	}
+	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	if selector.Empty() {
+		return resources, nil
+	}
+
+	var filteredList []*T
+	for _, resource := range resources {
+		annotations := getAnnotations(resource)
+		if selector.Matches(labels.Set(annotations)) {
+			filteredList = append(filteredList, resource)
+		}
+	}
+
+	return filteredList, nil
+}
+
+func getAnnotations(obj interface{}) map[string]string {
+	switch o := obj.(type) {
+	case *IngressRouteUDP:
+		return o.Annotations
+	case *IngressRoute:
+		return o.Annotations
+	case *IngressRouteTCP:
+		return o.Annotations
+	default:
+		return nil
+	}
+}
+
+func getObjectFullName(obj interface{}) string {
+	switch o := obj.(type) {
+	case *IngressRouteUDP:
+		return fmt.Sprintf("%s/%s", o.Namespace, o.Name)
+	case *IngressRoute:
+		return fmt.Sprintf("%s/%s", o.Namespace, o.Name)
+	case *IngressRouteTCP:
+		return fmt.Sprintf("%s/%s", o.Namespace, o.Name)
+	default:
+		return ""
+	}
+}
diff --git a/source/utils.go b/source/utils.go
new file mode 100644
index 00000000..2dc04cf9
--- /dev/null
+++ b/source/utils.go
@@ -0,0 +1,67 @@
+/*
+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 source
+
+import (
+	"fmt"
+	"net/netip"
+	"strings"
+
+	"sigs.k8s.io/external-dns/endpoint"
+)
+
+// suitableType returns the DNS resource record type suitable for the target.
+// In this case type A/AAAA for IPs and type CNAME for everything else.
+func suitableType(target string) string {
+	netIP, err := netip.ParseAddr(target)
+	if err != nil {
+		return endpoint.RecordTypeCNAME
+	}
+	switch {
+	case netIP.Is4():
+		return endpoint.RecordTypeA
+	case netIP.Is6():
+		return endpoint.RecordTypeAAAA
+	default:
+		return endpoint.RecordTypeCNAME
+	}
+}
+
+// ParseIngress parses an ingress string in the format "namespace/name" or "name".
+// It returns the namespace and name extracted from the string, or an error if the format is invalid.
+// If the namespace is not provided, it defaults to an empty string.
+func ParseIngress(ingress string) (namespace, name string, err error) {
+	parts := strings.Split(ingress, "/")
+	if len(parts) == 2 {
+		namespace, name = parts[0], parts[1]
+	} else if len(parts) == 1 {
+		name = parts[0]
+	} else {
+		err = fmt.Errorf("invalid ingress name (name or namespace/name) found %q", ingress)
+	}
+
+	return
+}
+
+// MatchesServiceSelector checks if all key-value pairs in the selector map
+// are present and match the corresponding key-value pairs in the svcSelector map.
+// It returns true if all pairs match, otherwise it returns false.
+func MatchesServiceSelector(selector, svcSelector map[string]string) bool {
+	for k, v := range selector {
+		if lbl, ok := svcSelector[k]; !ok || lbl != v {
+			return false
+		}
+	}
+	return true
+}



Test patch:
diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go
index 6c805ecf..6776b81a 100644
--- a/endpoint/endpoint_test.go
+++ b/endpoint/endpoint_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package endpoint
 
 import (
+	"fmt"
 	"reflect"
 	"testing"
 
@@ -38,6 +39,39 @@ func TestNewEndpoint(t *testing.T) {
 	}
 }
 
+func TestNewTargets(t *testing.T) {
+	cases := []struct {
+		name     string
+		input    []string
+		expected Targets
+	}{
+		{
+			name:     "no targets",
+			input:    []string{},
+			expected: Targets{},
+		},
+		{
+			name:     "single target",
+			input:    []string{"1.2.3.4"},
+			expected: Targets{"1.2.3.4"},
+		},
+		{
+			name:     "multiple targets",
+			input:    []string{"example.com", "8.8.8.8", "::0001"},
+			expected: Targets{"example.com", "8.8.8.8", "::0001"},
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			Targets := NewTargets(c.input...)
+			changedTarget := Targets.String()
+			assert.Equal(t, c.expected.String(), changedTarget)
+
+		})
+	}
+}
+
 func TestTargetsSame(t *testing.T) {
 	tests := []Targets{
 		{""},
@@ -155,6 +189,249 @@ func TestIsLess(t *testing.T) {
 	}
 }
 
+func TestGetProviderSpecificProperty(t *testing.T) {
+	e := &Endpoint{
+		ProviderSpecific: []ProviderSpecificProperty{
+			ProviderSpecificProperty{
+				Name:  "name",
+				Value: "value",
+			},
+		},
+	}
+
+	t.Run("key is not present in provider specific", func(t *testing.T) {
+		val, ok := e.GetProviderSpecificProperty("hello")
+		assert.Equal(t, false, ok)
+		assert.Empty(t, val)
+	})
+
+	t.Run("key is present in provider specific", func(t *testing.T) {
+		val, ok := e.GetProviderSpecificProperty("name")
+		assert.Equal(t, true, ok)
+		assert.NotEmpty(t, val)
+
+	})
+}
+
+func TestSetProviderSpecficProperty(t *testing.T) {
+	cases := []struct {
+		name               string
+		endpoint           Endpoint
+		key                string
+		value              string
+		expectedIdentifier string
+		expected           []ProviderSpecificProperty
+	}{
+		{
+			name:     "endpoint is empty",
+			endpoint: Endpoint{},
+			key:      "key1",
+			value:    "value1",
+			expected: []ProviderSpecificProperty{
+				ProviderSpecificProperty{
+					Name:  "key1",
+					Value: "value1",
+				},
+			},
+		},
+		{
+			name: "name and key are not matching",
+			endpoint: Endpoint{
+				DNSName:       "example.org",
+				RecordTTL:     TTL(0),
+				RecordType:    RecordTypeA,
+				SetIdentifier: "newIdentifier",
+				Targets: Targets{
+					"example.org", "example.com", "1.2.4.5",
+				},
+				ProviderSpecific: []ProviderSpecificProperty{
+					ProviderSpecificProperty{
+						Name:  "name1",
+						Value: "value1",
+					},
+				},
+			},
+			expectedIdentifier: "newIdentifier",
+			key:                "name2",
+			value:              "value2",
+
+			expected: []ProviderSpecificProperty{
+				ProviderSpecificProperty{
+					Name:  "name1",
+					Value: "value1",
+				},
+				ProviderSpecificProperty{
+					Name:  "name2",
+					Value: "value2",
+				},
+			},
+		},
+		{
+			name: "some keys are matching and some are not matching ",
+			endpoint: Endpoint{
+				DNSName:       "example.org",
+				RecordTTL:     TTL(0),
+				RecordType:    RecordTypeA,
+				SetIdentifier: "newIdentifier",
+				Targets: Targets{
+					"example.org", "example.com", "1.2.4.5",
+				},
+				ProviderSpecific: []ProviderSpecificProperty{
+					ProviderSpecificProperty{
+						Name:  "name1",
+						Value: "value1",
+					},
+					ProviderSpecificProperty{
+						Name:  "name2",
+						Value: "value2",
+					},
+					ProviderSpecificProperty{
+						Name:  "name3",
+						Value: "value3",
+					},
+				},
+			},
+			key:                "name2",
+			value:              "value2",
+			expectedIdentifier: "newIdentifier",
+			expected: []ProviderSpecificProperty{
+				ProviderSpecificProperty{
+					Name:  "name1",
+					Value: "value1",
+				},
+				ProviderSpecificProperty{
+					Name:  "name2",
+					Value: "value2",
+				},
+				ProviderSpecificProperty{
+					Name:  "name3",
+					Value: "value3",
+				},
+			},
+		},
+		{
+			name: "name and key are not matching",
+			endpoint: Endpoint{
+				DNSName:       "example.org",
+				RecordTTL:     TTL(0),
+				RecordType:    RecordTypeA,
+				SetIdentifier: "identifier",
+				Targets: Targets{
+					"example.org", "example.com", "1.2.4.5",
+				},
+				ProviderSpecific: []ProviderSpecificProperty{
+					ProviderSpecificProperty{
+						Name:  "name1",
+						Value: "value1",
+					},
+				},
+			},
+			key:                "name1",
+			value:              "value2",
+			expectedIdentifier: "identifier",
+			expected: []ProviderSpecificProperty{
+				ProviderSpecificProperty{
+					Name:  "name1",
+					Value: "value2",
+				},
+			},
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			c.endpoint.WithProviderSpecific(c.key, c.value)
+			expectedString := fmt.Sprintf("%s %d IN %s %s %s %s", c.endpoint.DNSName, c.endpoint.RecordTTL, c.endpoint.RecordType, c.endpoint.SetIdentifier, c.endpoint.Targets, c.endpoint.ProviderSpecific)
+			identifier := c.endpoint.WithSetIdentifier(c.endpoint.SetIdentifier)
+			assert.Equal(t, c.expectedIdentifier, identifier.SetIdentifier)
+			assert.Equal(t, expectedString, c.endpoint.String())
+			if !reflect.DeepEqual([]ProviderSpecificProperty(c.endpoint.ProviderSpecific), c.expected) {
+				t.Errorf("unexpected ProviderSpecific:\nGot:      %#v\nExpected: %#v", c.endpoint.ProviderSpecific, c.expected)
+			}
+		})
+	}
+}
+
+func TestDeleteProviderSpecificProperty(t *testing.T) {
+	cases := []struct {
+		name     string
+		endpoint Endpoint
+		key      string
+		expected []ProviderSpecificProperty
+	}{
+		{
+			name: "name and key are not matching",
+			endpoint: Endpoint{
+				ProviderSpecific: []ProviderSpecificProperty{
+					ProviderSpecificProperty{
+						Name:  "name1",
+						Value: "value1",
+					},
+				},
+			},
+			key: "name2",
+			expected: []ProviderSpecificProperty{
+				ProviderSpecificProperty{
+					Name:  "name1",
+					Value: "value1",
+				},
+			},
+		},
+		{
+			name: "some keys are matching and some keys are not matching",
+			endpoint: Endpoint{
+				ProviderSpecific: []ProviderSpecificProperty{
+					ProviderSpecificProperty{
+						Name:  "name1",
+						Value: "value1",
+					},
+					ProviderSpecificProperty{
+						Name:  "name2",
+						Value: "value2",
+					},
+					ProviderSpecificProperty{
+						Name:  "name3",
+						Value: "value3",
+					},
+				},
+			},
+			key: "name2",
+			expected: []ProviderSpecificProperty{
+				ProviderSpecificProperty{
+					Name:  "name1",
+					Value: "value1",
+				},
+				ProviderSpecificProperty{
+					Name:  "name3",
+					Value: "value3",
+				},
+			},
+		},
+		{
+			name: "name and key are matching",
+			endpoint: Endpoint{
+				ProviderSpecific: []ProviderSpecificProperty{
+					ProviderSpecificProperty{
+						Name:  "name1",
+						Value: "value1",
+					},
+				},
+			},
+			key:      "name1",
+			expected: []ProviderSpecificProperty{},
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			c.endpoint.DeleteProviderSpecificProperty(c.key)
+			if !reflect.DeepEqual([]ProviderSpecificProperty(c.endpoint.ProviderSpecific), c.expected) {
+				t.Errorf("unexpected ProviderSpecific:\nGot:      %#v\nExpected: %#v", c.endpoint.ProviderSpecific, c.expected)
+			}
+		})
+	}
+}
+
 func TestFilterEndpointsByOwnerIDWithRecordTypeA(t *testing.T) {
 	foo1 := &Endpoint{
 		DNSName:    "foo.com",
diff --git a/internal/testutils/endpoint_test.go b/internal/testutils/endpoint_test.go
index efa472f8..8ddb1f75 100644
--- a/internal/testutils/endpoint_test.go
+++ b/internal/testutils/endpoint_test.go
@@ -18,9 +18,12 @@ package testutils
 
 import (
 	"fmt"
+	"net/netip"
+	"reflect"
 	"sort"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"sigs.k8s.io/external-dns/endpoint"
 )
 
@@ -78,3 +81,391 @@ func TestExampleSameEndpoints(t *testing.T) {
 	// example.org 0 IN   load-balancer.org [{foo bar}]
 	// example.org 0 IN TXT  load-balancer.org []
 }
+
+func makeEndpoint(DNSName string) *endpoint.Endpoint {
+	return &endpoint.Endpoint{
+		DNSName:       DNSName,
+		Targets:       endpoint.Targets{"target.com"},
+		RecordType:    "A",
+		SetIdentifier: "set1",
+		RecordTTL:     300,
+		Labels: map[string]string{
+			endpoint.OwnerLabelKey:       "owner",
+			endpoint.ResourceLabelKey:    "resource",
+			endpoint.OwnedRecordLabelKey: "owned",
+		},
+		ProviderSpecific: endpoint.ProviderSpecific{
+			{Name: "key", Value: "val"},
+		},
+	}
+}
+
+func TestSameEndpoint(t *testing.T) {
+	tests := []struct {
+		name           string
+		a              *endpoint.Endpoint
+		b              *endpoint.Endpoint
+		isSameEndpoint bool
+	}{
+		{
+			name:           "DNSName is not equal",
+			a:              &endpoint.Endpoint{DNSName: "example.org"},
+			b:              &endpoint.Endpoint{DNSName: "example.com"},
+			isSameEndpoint: false,
+		},
+		{
+			name: "All fields are equal",
+			a: &endpoint.Endpoint{
+				DNSName:       "example.org",
+				Targets:       endpoint.Targets{"lb.example.com"},
+				RecordType:    "A",
+				SetIdentifier: "set-1",
+				RecordTTL:     300,
+				Labels: map[string]string{
+					endpoint.OwnerLabelKey:       "owner-1",
+					endpoint.ResourceLabelKey:    "resource-1",
+					endpoint.OwnedRecordLabelKey: "owned-true",
+				},
+				ProviderSpecific: endpoint.ProviderSpecific{
+					{Name: "key1", Value: "val1"},
+				},
+			},
+			b: &endpoint.Endpoint{
+				DNSName:       "example.org",
+				Targets:       endpoint.Targets{"lb.example.com"},
+				RecordType:    "A",
+				SetIdentifier: "set-1",
+				RecordTTL:     300,
+				Labels: map[string]string{
+					endpoint.OwnerLabelKey:       "owner-1",
+					endpoint.ResourceLabelKey:    "resource-1",
+					endpoint.OwnedRecordLabelKey: "owned-true",
+				},
+				ProviderSpecific: endpoint.ProviderSpecific{
+					{Name: "key1", Value: "val1"},
+				},
+			},
+			isSameEndpoint: true,
+		},
+		{
+			name:           "Different Targets",
+			a:              &endpoint.Endpoint{DNSName: "example.org", Targets: endpoint.Targets{"a.com"}},
+			b:              &endpoint.Endpoint{DNSName: "example.org", Targets: endpoint.Targets{"b.com"}},
+			isSameEndpoint: false,
+		},
+		{
+			name:           "Different RecordType",
+			a:              &endpoint.Endpoint{DNSName: "example.org", RecordType: "A"},
+			b:              &endpoint.Endpoint{DNSName: "example.org", RecordType: "CNAME"},
+			isSameEndpoint: false,
+		},
+		{
+			name:           "Different SetIdentifier",
+			a:              &endpoint.Endpoint{DNSName: "example.org", SetIdentifier: "id1"},
+			b:              &endpoint.Endpoint{DNSName: "example.org", SetIdentifier: "id2"},
+			isSameEndpoint: false,
+		},
+		{
+			name: "Different OwnerLabelKey",
+			a: &endpoint.Endpoint{
+				DNSName: "example.org",
+				Labels: map[string]string{
+					endpoint.OwnerLabelKey: "owner1",
+				},
+			},
+			b: &endpoint.Endpoint{
+				DNSName: "example.org",
+				Labels: map[string]string{
+					endpoint.OwnerLabelKey: "owner2",
+				},
+			},
+			isSameEndpoint: false,
+		},
+		{
+			name:           "Different RecordTTL",
+			a:              &endpoint.Endpoint{DNSName: "example.org", RecordTTL: 300},
+			b:              &endpoint.Endpoint{DNSName: "example.org", RecordTTL: 400},
+			isSameEndpoint: false,
+		},
+		{
+			name: "Different ProviderSpecific",
+			a: &endpoint.Endpoint{
+				DNSName: "example.org",
+				ProviderSpecific: endpoint.ProviderSpecific{
+					{Name: "key1", Value: "val1"},
+				},
+			},
+			b: &endpoint.Endpoint{
+				DNSName: "example.org",
+				ProviderSpecific: endpoint.ProviderSpecific{
+					{Name: "key1", Value: "val2"},
+				},
+			},
+			isSameEndpoint: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			isSameEndpoint := SameEndpoint(tt.a, tt.b)
+			assert.Equal(t, tt.isSameEndpoint, isSameEndpoint)
+		})
+	}
+}
+func TestSameEndpoints(t *testing.T) {
+	tests := []struct {
+		name string
+		a, b []*endpoint.Endpoint
+		want bool
+	}{
+		{
+			name: "Both slices nil",
+			a:    nil,
+			b:    nil,
+			want: true,
+		},
+		{
+			name: "One nil, one empty",
+			a:    []*endpoint.Endpoint{},
+			b:    nil,
+			want: true,
+		},
+		{
+			name: "Different lengths",
+			a:    []*endpoint.Endpoint{makeEndpoint("a.com")},
+			b:    []*endpoint.Endpoint{},
+			want: false,
+		},
+		{
+			name: "Same endpoints in same order",
+			a:    []*endpoint.Endpoint{makeEndpoint("a.com"), makeEndpoint("b.com")},
+			b:    []*endpoint.Endpoint{makeEndpoint("a.com"), makeEndpoint("b.com")},
+			want: true,
+		},
+		{
+			name: "Same endpoints in different order",
+			a:    []*endpoint.Endpoint{makeEndpoint("b.com"), makeEndpoint("a.com")},
+			b:    []*endpoint.Endpoint{makeEndpoint("a.com"), makeEndpoint("b.com")},
+			want: true,
+		},
+		{
+			name: "One endpoint differs",
+			a:    []*endpoint.Endpoint{makeEndpoint("a.com"), makeEndpoint("b.com")},
+			b:    []*endpoint.Endpoint{makeEndpoint("a.com"), makeEndpoint("c.com")},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			isSameEndpoints := SameEndpoints(tt.a, tt.b)
+			assert.Equal(t, tt.want, isSameEndpoints)
+		})
+	}
+}
+
+func TestSameEndpointLabel(t *testing.T) {
+	tests := []struct {
+		name string
+		a    []*endpoint.Endpoint
+		b    []*endpoint.Endpoint
+		want bool
+	}{
+		{
+			name: "length of a and b are not same",
+			a:    []*endpoint.Endpoint{makeEndpoint("a.com")},
+			b:    []*endpoint.Endpoint{makeEndpoint("b.com"), makeEndpoint("c.com")},
+			want: false,
+		},
+		{
+			name: "endpoint's labels are same in a and b",
+			a:    []*endpoint.Endpoint{makeEndpoint("a.com"), makeEndpoint("c.com")},
+			b:    []*endpoint.Endpoint{makeEndpoint("b.com"), makeEndpoint("c.com")},
+			want: true,
+		},
+		{
+			name: "endpoint's labels are not same in a and b",
+			a: []*endpoint.Endpoint{
+				{
+					DNSName: "a.com",
+					Labels: endpoint.Labels{
+						endpoint.OwnerLabelKey:    "owner1",
+						endpoint.ResourceLabelKey: "resource1",
+					},
+				},
+				{
+					DNSName: "b.com",
+					Labels: endpoint.Labels{
+						endpoint.OwnerLabelKey:    "owner2",
+						endpoint.ResourceLabelKey: "resource2",
+					},
+				},
+			},
+			b: []*endpoint.Endpoint{
+				{
+					DNSName: "a.com",
+					Labels: endpoint.Labels{
+						endpoint.OwnerLabelKey:    "owner",
+						endpoint.ResourceLabelKey: "resource",
+					},
+				},
+				{
+					DNSName: "b.com",
+					Labels: endpoint.Labels{
+						endpoint.OwnerLabelKey:    "owner1",
+						endpoint.ResourceLabelKey: "resource1",
+					},
+				},
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			isSameEndpointLabels := SameEndpointLabels(tt.a, tt.b)
+			assert.Equal(t, tt.want, isSameEndpointLabels)
+		})
+	}
+}
+
+func TestSamePlanChanges(t *testing.T) {
+	tests := []struct {
+		name string
+		a    map[string][]*endpoint.Endpoint
+		b    map[string][]*endpoint.Endpoint
+		want bool
+	}{
+		{
+			name: "endpoints with all operations in a and b are same",
+			a: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("a.com")},
+				"Delete":    {makeEndpoint("b.com")},
+				"UpdateOld": {makeEndpoint("a.com")},
+				"UpdateNew": {makeEndpoint("c.com")},
+			},
+			b: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("a.com")},
+				"Delete":    {makeEndpoint("b.com")},
+				"UpdateOld": {makeEndpoint("a.com")},
+				"UpdateNew": {makeEndpoint("c.com")},
+			},
+			want: true,
+		},
+		{
+			name: "endpoints for create operations in a and b are not same",
+			a: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("a.com")},
+				"Delete":    {makeEndpoint("b.com")},
+				"UpdateOld": {makeEndpoint("a.com")},
+				"UpdateNew": {makeEndpoint("c.com")},
+			},
+			b: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("x.com")},
+				"Delete":    {makeEndpoint("b.com")},
+				"UpdateOld": {makeEndpoint("a.com")},
+				"UpdateNew": {makeEndpoint("c.com")},
+			},
+			want: false,
+		},
+		{
+			name: "endpoints for delete operations in a and b are not same",
+			a: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("a.com")},
+				"Delete":    {makeEndpoint("b.com")},
+				"UpdateOld": {makeEndpoint("a.com")},
+				"UpdateNew": {makeEndpoint("c.com")},
+			},
+			b: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("a.com")},
+				"Delete":    {makeEndpoint("g.com")},
+				"UpdateOld": {makeEndpoint("a.com")},
+				"UpdateNew": {makeEndpoint("c.com")},
+			},
+			want: false,
+		},
+		{
+			name: "endpoints for updateOld operations in a and b are not same",
+			a: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("a.com")},
+				"Delete":    {makeEndpoint("b.com")},
+				"UpdateOld": {makeEndpoint("b.com")},
+				"UpdateNew": {makeEndpoint("c.com")},
+			},
+			b: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("a.com")},
+				"Delete":    {makeEndpoint("b.com")},
+				"UpdateOld": {makeEndpoint("c.com")},
+				"UpdateNew": {makeEndpoint("c.com")},
+			},
+			want: false,
+		},
+		{
+			name: "endpoints for updateNew operations in a and b are same",
+			a: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("a.com")},
+				"Delete":    {makeEndpoint("b.com")},
+				"UpdateOld": {makeEndpoint("a.com")},
+				"UpdateNew": {makeEndpoint("d.com")},
+			},
+			b: map[string][]*endpoint.Endpoint{
+				"Create":    {makeEndpoint("a.com")},
+				"Delete":    {makeEndpoint("b.com")},
+				"UpdateOld": {makeEndpoint("a.com")},
+				"UpdateNew": {makeEndpoint("c.com")},
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			checkPlanChanges := SamePlanChanges(tt.a, tt.b)
+			assert.Equal(t, tt.want, checkPlanChanges)
+		})
+	}
+}
+func TestNewTargetsFromAddr(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []netip.Addr
+		expected endpoint.Targets
+	}{
+		{
+			name:     "empty slice",
+			input:    []netip.Addr{},
+			expected: endpoint.Targets{},
+		},
+		{
+			name: "single IPv4 address",
+			input: []netip.Addr{
+				netip.MustParseAddr("192.0.2.1"),
+			},
+			expected: endpoint.Targets{"192.0.2.1"},
+		},
+		{
+			name: "multiple IP addresses",
+			input: []netip.Addr{
+				netip.MustParseAddr("192.0.2.1"),
+				netip.MustParseAddr("2001:db8::1"),
+			},
+			expected: endpoint.Targets{"192.0.2.1", "2001:db8::1"},
+		},
+		{
+			name: "IPv6 address only",
+			input: []netip.Addr{
+				netip.MustParseAddr("::1"),
+			},
+			expected: endpoint.Targets{"::1"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := NewTargetsFromAddr(tt.input)
+			if !reflect.DeepEqual(got, tt.expected) {
+				t.Errorf("NewTargetsFromAddr() = %v, want %v", got, tt.expected)
+			}
+		})
+	}
+}
diff --git a/internal/testutils/env.go b/internal/testutils/env.go
new file mode 100644
index 00000000..7f90597a
--- /dev/null
+++ b/internal/testutils/env.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testutils
+
+import (
+	"testing"
+)
+
+func TestHelperEnvSetter(t *testing.T, envs map[string]string) {
+	t.Helper()
+
+	for name, value := range envs {
+		t.Setenv(name, value)
+	}
+}
diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go
index b9b257c8..305ca9cd 100644
--- a/pkg/apis/externaldns/types_test.go
+++ b/pkg/apis/externaldns/types_test.go
@@ -73,11 +73,13 @@ var (
 		AzureConfigFile:                        "/etc/kubernetes/azure.json",
 		AzureResourceGroup:                     "",
 		AzureSubscriptionID:                    "",
+		AzureMaxRetriesCount:                   3,
 		CloudflareProxied:                      false,
 		CloudflareCustomHostnames:              false,
 		CloudflareCustomHostnamesMinTLSVersion: "1.0",
 		CloudflareCustomHostnamesCertificateAuthority: "google",
 		CloudflareDNSRecordsPerPage:                   100,
+		CloudflareDNSRecordsComment:                   "",
 		CloudflareRegionKey:                           "",
 		CoreDNSPrefix:                                 "/skydns/",
 		AkamaiServiceConsumerDomain:                   "",
@@ -183,6 +185,7 @@ var (
 		AzureConfigFile:                        "azure.json",
 		AzureResourceGroup:                     "arg",
 		AzureSubscriptionID:                    "arg",
+		AzureMaxRetriesCount:                   4,
 		CloudflareProxied:                      true,
 		CloudflareCustomHostnames:              true,
 		CloudflareCustomHostnamesMinTLSVersion: "1.3",
@@ -296,6 +299,7 @@ func TestParseFlags(t *testing.T) {
 				"--azure-config-file=azure.json",
 				"--azure-resource-group=arg",
 				"--azure-subscription-id=arg",
+				"--azure-maxretries-count=4",
 				"--cloudflare-proxied",
 				"--cloudflare-custom-hostnames",
 				"--cloudflare-custom-hostnames-min-tls-version=1.3",
@@ -427,6 +431,7 @@ func TestParseFlags(t *testing.T) {
 				"EXTERNAL_DNS_AZURE_CONFIG_FILE":                                 "azure.json",
 				"EXTERNAL_DNS_AZURE_RESOURCE_GROUP":                              "arg",
 				"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID":                             "arg",
+				"EXTERNAL_DNS_AZURE_MAXRETRIES_COUNT":                            "4",
 				"EXTERNAL_DNS_CLOUDFLARE_PROXIED":                                "1",
 				"EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES":                       "1",
 				"EXTERNAL_DNS_CLOUDFLARE_CUSTOM_HOSTNAMES_MIN_TLS_VERSION":       "1.3",
diff --git a/provider/azure/azure_privatedns_test.go b/provider/azure/azure_privatedns_test.go
index cc922349..2f980ccd 100644
--- a/provider/azure/azure_privatedns_test.go
+++ b/provider/azure/azure_privatedns_test.go
@@ -224,13 +224,13 @@ func createPrivateMockRecordSetMultiWithTTL(name, recordType string, ttl int64,
 }
 
 // newMockedAzurePrivateDNSProvider creates an AzureProvider comprising the mocked clients for zones and recordsets
-func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, zones []*privatedns.PrivateZone, recordSets []*privatedns.RecordSet) (*AzurePrivateDNSProvider, error) {
+func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, zones []*privatedns.PrivateZone, recordSets []*privatedns.RecordSet, maxRetriesCount int) (*AzurePrivateDNSProvider, error) {
 	zonesClient := newMockPrivateZonesClient(zones)
 	recordSetsClient := newMockPrivateRecordSectsClient(recordSets)
-	return newAzurePrivateDNSProvider(domainFilter, zoneNameFilter, zoneIDFilter, dryRun, resourceGroup, &zonesClient, &recordSetsClient), nil
+	return newAzurePrivateDNSProvider(domainFilter, zoneNameFilter, zoneIDFilter, dryRun, resourceGroup, &zonesClient, &recordSetsClient, maxRetriesCount), nil
 }
 
-func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, privateZonesClient PrivateZonesClient, privateRecordsClient PrivateRecordSetsClient) *AzurePrivateDNSProvider {
+func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, privateZonesClient PrivateZonesClient, privateRecordsClient PrivateRecordSetsClient, maxRetriesCount int) *AzurePrivateDNSProvider {
 	return &AzurePrivateDNSProvider{
 		domainFilter:     domainFilter,
 		zoneNameFilter:   zoneNameFilter,
@@ -240,6 +240,7 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilt
 		zonesClient:      privateZonesClient,
 		zonesCache:       &zonesCache[privatedns.PrivateZone]{duration: 0},
 		recordSetsClient: privateRecordsClient,
+		maxRetriesCount:  maxRetriesCount,
 	}
 }
 
@@ -259,7 +260,7 @@ func TestAzurePrivateDNSRecord(t *testing.T) {
 			createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
 			createPrivateMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
 			createPrivateMockRecordSetWithTTL("mail", endpoint.RecordTypeMX, "10 example.com", 4000),
-		})
+		}, 3)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -298,7 +299,7 @@ func TestAzurePrivateDNSMultiRecord(t *testing.T) {
 			createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
 			createPrivateMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
 			createPrivateMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"),
-		})
+		}, 3)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -378,6 +379,7 @@ func testAzurePrivateDNSApplyChangesInternal(t *testing.T, dryRun bool, client P
 		"group",
 		&zonesClient,
 		client,
+		3,
 	)
 
 	createRecords := []*endpoint.Endpoint{
@@ -450,7 +452,7 @@ func TestAzurePrivateDNSNameFilter(t *testing.T) {
 			createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
 			createPrivateMockRecordSetWithTTL("mail.nginx", endpoint.RecordTypeMX, "20 example.com", recordTTL),
 			createPrivateMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
-		})
+		}, 3)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -505,6 +507,7 @@ func testAzurePrivateDNSApplyChangesInternalZoneName(t *testing.T, dryRun bool,
 		"group",
 		&zonesClient,
 		client,
+		3,
 	)
 
 	createRecords := []*endpoint.Endpoint{
diff --git a/provider/azure/azure_test.go b/provider/azure/azure_test.go
index a27f2eb1..e7807d9c 100644
--- a/provider/azure/azure_test.go
+++ b/provider/azure/azure_test.go
@@ -237,13 +237,13 @@ func createMockRecordSetMultiWithTTL(name, recordType string, ttl int64, values
 }
 
 // newMockedAzureProvider creates an AzureProvider comprising the mocked clients for zones and recordsets
-func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zones []*dns.Zone, recordSets []*dns.RecordSet) (*AzureProvider, error) {
+func newMockedAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zones []*dns.Zone, recordSets []*dns.RecordSet, maxRetriesCount int) (*AzureProvider, error) {
 	zonesClient := newMockZonesClient(zones)
 	recordSetsClient := newMockRecordSetsClient(recordSets)
-	return newAzureProvider(domainFilter, zoneNameFilter, zoneIDFilter, dryRun, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost, &zonesClient, &recordSetsClient), nil
+	return newAzureProvider(domainFilter, zoneNameFilter, zoneIDFilter, dryRun, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost, &zonesClient, &recordSetsClient, maxRetriesCount), nil
 }
 
-func newAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesClient ZonesClient, recordsClient RecordSetsClient) *AzureProvider {
+func newAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesClient ZonesClient, recordsClient RecordSetsClient, maxRetriesCount int) *AzureProvider {
 	return &AzureProvider{
 		domainFilter:                 domainFilter,
 		zoneNameFilter:               zoneNameFilter,
@@ -255,6 +255,7 @@ func newAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoin
 		zonesClient:                  zonesClient,
 		zonesCache:                   &zonesCache[dns.Zone]{duration: 0},
 		recordSetsClient:             recordsClient,
+		maxRetriesCount:              maxRetriesCount,
 	}
 }
 
@@ -280,7 +281,7 @@ func TestAzureRecord(t *testing.T) {
 			createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
 			createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
 			createMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com"),
-		})
+		}, 3)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -325,7 +326,7 @@ func TestAzureMultiRecord(t *testing.T) {
 			createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL),
 			createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
 			createMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"),
-		})
+		}, 3)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -415,6 +416,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC
 		"",
 		&zonesClient,
 		client,
+		3,
 	)
 
 	createRecords := []*endpoint.Endpoint{
@@ -497,7 +499,7 @@ func TestAzureNameFilter(t *testing.T) {
 			createMockRecordSetWithTTL("mail.nginx", endpoint.RecordTypeMX, "20 example.com", recordTTL),
 			createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10),
 			createMockRecordSetWithTTL("hack", endpoint.RecordTypeNS, "ns1.example.com.", 3600),
-		})
+		}, 3)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -555,6 +557,7 @@ func testAzureApplyChangesInternalZoneName(t *testing.T, dryRun bool, client Rec
 		"",
 		&zonesClient,
 		client,
+		3,
 	)
 
 	createRecords := []*endpoint.Endpoint{
diff --git a/provider/azure/config_test.go b/provider/azure/config_test.go
index 956599a9..8e7f9c6c 100644
--- a/provider/azure/config_test.go
+++ b/provider/azure/config_test.go
@@ -17,11 +17,19 @@ limitations under the License.
 package azure
 
 import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
 	"path"
 	"runtime"
+	"strconv"
+	"strings"
 	"testing"
 
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+	azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -59,3 +67,268 @@ func TestOverrideConfiguration(t *testing.T) {
 	assert.Equal(t, cfg.ResourceGroup, "rg-override")
 	assert.Equal(t, cfg.ActiveDirectoryAuthorityHost, "aad-endpoint-override")
 }
+
+// Test for custom header policy
+type transportFunc func(*http.Request) (*http.Response, error)
+
+func (f transportFunc) Do(req *http.Request) (*http.Response, error) {
+	return f(req)
+}
+
+func TestCustomHeaderPolicyWithRetries(t *testing.T) {
+	// Set up test environment
+	defaultRetries := 3
+	flagValue := "-6"
+	isSet := true
+
+	retries, err := parseMaxRetries(flagValue, defaultRetries)
+	if err != nil {
+		t.Fatalf("Failed to parse retries: %v", err)
+	}
+	maxRetries := int32(retries)
+	if !isSet || (isSet && flagValue == "0") {
+		// Use default if flag not provided OR if flag is "0"
+		maxRetries = int32(defaultRetries)
+		t.Logf("Using default value: %d (flag provided: %v, value: %q)",
+			defaultRetries, isSet, flagValue)
+	} else {
+		// Flag was provided with non-zero value
+		retries, err := parseMaxRetries(flagValue, defaultRetries)
+		if err != nil {
+			t.Fatalf("Failed to parse retries: %v", err)
+		}
+		maxRetries = int32(retries)
+		t.Logf("Using provided flag value: %d", retries)
+	}
+
+	var attempt int32
+	var firstRequestID string
+
+	// Create mock transport that simulates 429 responses
+	mockTransport := transportFunc(func(req *http.Request) (*http.Response, error) {
+		attempt++
+
+		// Get the request ID from header
+		requestID := req.Header.Get("x-ms-client-request-id")
+		if requestID == "" {
+			t.Fatalf("Request ID missing on attempt %d", attempt)
+		}
+
+		// On first attempt, store the request ID
+		if attempt == 1 {
+			firstRequestID = requestID
+			t.Logf("Initial request ID: %s", firstRequestID)
+		} else {
+			// On subsequent attempts, verify it matches the first request ID
+			if requestID != firstRequestID {
+				t.Fatalf("Request ID changed on retry %d: got %s, want %s",
+					attempt, requestID, firstRequestID)
+			} else {
+				t.Logf("Request ID preserved on attempt %d: %s", attempt, requestID)
+			}
+		}
+
+		// Verify the ID is also in the context
+		if ctxID, ok := req.Context().Value(clientRequestIDKey).(string); !ok || ctxID != requestID {
+			t.Errorf("Context ID mismatch on attempt %d: got %v, want %s",
+				attempt, ctxID, requestID)
+		}
+
+		// Return 429 for all but the last attempt
+		if maxRetries < 0 || attempt <= maxRetries {
+			t.Logf("Attempt %d: THROTTLED (429) - Request ID: %s", attempt, requestID)
+			return &http.Response{
+				StatusCode: http.StatusTooManyRequests,
+				Body:       io.NopCloser(strings.NewReader("Too many requests")),
+				Request:    req,
+				Header: http.Header{
+					"x-ms-client-request-id": []string{requestID},
+					"Retry-After":            []string{"1"},
+				},
+			}, nil
+		}
+
+		// Return 200 on final attempt
+		t.Logf("Attempt %d: SUCCESS (200) - Request ID: %s", attempt, requestID)
+		return &http.Response{
+			StatusCode: http.StatusOK,
+			Body:       io.NopCloser(strings.NewReader("Success")),
+			Request:    req,
+			Header: http.Header{
+				"x-ms-client-request-id": []string{requestID},
+			},
+		}, nil
+	})
+
+	// Create pipeline with retry policy and custom header policy
+	mockPipeline := azruntime.NewPipeline(
+		"testmodule",
+		"1.0",
+		azruntime.PipelineOptions{
+			PerCall: []policy.Policy{
+				CustomHeaderPolicynew(),
+			},
+		},
+		&policy.ClientOptions{
+			Retry: policy.RetryOptions{
+				MaxRetries: maxRetries,
+			},
+			Transport: mockTransport,
+		},
+	)
+	// Create request and execute
+	req, err := azruntime.NewRequest(context.Background(), http.MethodGet, "https://example.com")
+	if err != nil {
+		t.Fatalf("Failed to create request: %v", err)
+	}
+
+	resp, err := mockPipeline.Do(req)
+	if err != nil {
+		t.Fatalf("Request failed: %v", err)
+	}
+	defer resp.Body.Close()
+
+	// Verify we got the expected number of attempts
+	var expectedAttempts int32
+	if maxRetries < 0 {
+		expectedAttempts = 1 // For negative retries, only one attempt should be made
+	} else {
+		expectedAttempts = maxRetries + 1 // For zero or positive retries, attempts = retries + 1
+	}
+
+	if attempt != expectedAttempts {
+		t.Errorf("Wrong number of attempts: got %d, want %d", attempt, expectedAttempts)
+	}
+
+	t.Logf("Test completed with %d attempts, all with request ID: %s", attempt, firstRequestID)
+}
+
+func TestMaxRetriesCount(t *testing.T) {
+	defaultRetries := 3
+
+	tests := []struct {
+		name        string
+		input       string
+		isSet       bool // indicates if flag was provided
+		expected    int
+		shouldError bool
+		description string
+	}{
+		{
+			name:        "FlagNotProvided",
+			input:       "",
+			isSet:       false,
+			expected:    defaultRetries,
+			shouldError: false,
+			description: "When flag is not provided, should use default value",
+		},
+		{
+			name:        "FlagProvidedEmpty",
+			input:       "",
+			isSet:       true,
+			expected:    0,
+			shouldError: true,
+			description: "When flag is provided but empty, should error",
+		},
+		{
+			name:        "ValidPositive",
+			input:       "5",
+			isSet:       true,
+			expected:    5,
+			shouldError: false,
+			description: "Valid positive number should be accepted",
+		},
+		{
+			name:        "ZeroRetries",
+			input:       "0",
+			isSet:       true,
+			expected:    0,
+			shouldError: false,
+			description: "Zero should be accepted and handled by SDK",
+		},
+		{
+			name:        "NegativeRetries",
+			input:       "-2",
+			isSet:       true,
+			expected:    -2,
+			shouldError: false,
+			description: "Negative values should be accepted and  handled by SDK",
+		},
+		{
+			name:        "InvalidString",
+			input:       "abc",
+			isSet:       true,
+			expected:    0,
+			shouldError: true,
+			description: "Non-numeric string should error",
+		},
+		{
+			name:        "Whitespace",
+			input:       "   ",
+			isSet:       true,
+			expected:    0,
+			shouldError: true,
+			description: "Whitespace should error",
+		},
+		{
+			name:        "SpecialChars",
+			input:       "@#$%",
+			isSet:       true,
+			expected:    0,
+			shouldError: true,
+			description: "Special characters should error",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Logf("=== Test Case: %s ===", tt.name)
+			t.Logf("Description: %s", tt.description)
+			t.Logf("Input: %q (flag provided: %v)", tt.input, tt.isSet)
+
+			// Handle flag not provided case
+			if !tt.isSet {
+				t.Logf("Using default value: %d", defaultRetries)
+				return
+			}
+
+			retries, err := parseMaxRetries(tt.input, defaultRetries)
+
+			// Check error condition
+			if tt.shouldError {
+				if err == nil {
+					t.Errorf("Expected error for input %q but got none", tt.input)
+				} else {
+					t.Logf("Got expected error: %v", err)
+				}
+			} else {
+				if err != nil {
+					t.Errorf("Unexpected error: %v", err)
+				}
+				if retries != tt.expected {
+					t.Errorf("Got %d retries, want %d", retries, tt.expected)
+				} else {
+					t.Logf("Got expected value: %d", retries)
+				}
+			}
+		})
+	}
+}
+
+// Helper function to parse max retries value
+func parseMaxRetries(value string, defaultValue int) (int, error) {
+	// Trim whitespace
+	value = strings.TrimSpace(value)
+
+	// Empty string or whitespace should error
+	if value == "" {
+		return 0, fmt.Errorf("retry count must be provided when flag is set")
+	}
+
+	retries, err := strconv.Atoi(value)
+	if err != nil {
+		return 0, fmt.Errorf("invalid retry count %q: %v", value, err)
+	}
+
+	return retries, nil
+}
diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go
index b5c05cea..6bd93411 100644
--- a/provider/cloudflare/cloudflare_test.go
+++ b/provider/cloudflare/cloudflare_test.go
@@ -35,6 +35,7 @@ import (
 	"sigs.k8s.io/external-dns/internal/testutils"
 	"sigs.k8s.io/external-dns/plan"
 	"sigs.k8s.io/external-dns/provider"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 type MockAction struct {
@@ -65,6 +66,7 @@ var ExampleDomain = []cloudflare.DNSRecord{
 		TTL:     120,
 		Content: "1.2.3.4",
 		Proxied: proxyDisabled,
+		Comment: "valid comment",
 	},
 	{
 		ID:      "2345678901",
@@ -420,6 +422,7 @@ func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) (
 			return cloudflare.Zone{
 				ID:   zoneID,
 				Name: zoneName,
+				Plan: cloudflare.ZonePlan{IsSubscribed: strings.HasSuffix(zoneName, "bar.com")},
 			}, nil
 		}
 	}
@@ -979,8 +982,8 @@ func TestCloudflareRecords(t *testing.T) {
 
 	// Set DNSRecordsPerPage to 1 test the pagination behaviour
 	p := &CloudFlareProvider{
-		Client:            client,
-		DNSRecordsPerPage: 1,
+		Client:           client,
+		DNSRecordsConfig: DNSRecordsConfig{PerPage: 1},
 	}
 	ctx := context.Background()
 
@@ -1096,9 +1099,10 @@ func TestCloudflareProvider(t *testing.T) {
 				provider.NewZoneIDFilter([]string{""}),
 				false,
 				true,
-				5000,
 				"",
-				CustomHostnamesConfig{Enabled: false})
+				CustomHostnamesConfig{Enabled: false},
+				DNSRecordsConfig{PerPage: 5000, Comment: ""},
+			)
 			if err != nil && !tc.ShouldFail {
 				t.Errorf("should not fail, %s", err)
 			}
@@ -1764,9 +1768,10 @@ func TestCloudFlareProvider_Region(t *testing.T) {
 		provider.ZoneIDFilter{},
 		true,
 		false,
-		50,
 		"us",
-		CustomHostnamesConfig{Enabled: false})
+		CustomHostnamesConfig{Enabled: false},
+		DNSRecordsConfig{PerPage: 50, Comment: ""},
+	)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1779,28 +1784,144 @@ func TestCloudFlareProvider_Region(t *testing.T) {
 func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
 	_ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx")
 	_ = os.Setenv("CF_API_EMAIL", "test@test.com")
-	provider, err := NewCloudFlareProvider(
+
+	p, err := NewCloudFlareProvider(
 		endpoint.NewDomainFilter([]string{"example.com"}),
 		provider.ZoneIDFilter{},
 		true,
 		false,
-		50,
 		"us",
-		CustomHostnamesConfig{Enabled: false})
+		CustomHostnamesConfig{Enabled: false},
+		DNSRecordsConfig{PerPage: 50},
+	)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	endpoint := &endpoint.Endpoint{
+	ep := &endpoint.Endpoint{
 		DNSName:    "example.com",
 		RecordType: "A",
 		Targets:    []string{"192.0.2.1"},
 	}
 
-	change := provider.newCloudFlareChange(cloudFlareCreate, endpoint, endpoint.Targets[0], nil)
+	change := p.newCloudFlareChange(cloudFlareCreate, ep, ep.Targets[0], nil)
 	if change.RegionalHostname.RegionKey != "us" {
 		t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.RegionKey)
 	}
+
+	var freeValidCommentBuilder strings.Builder
+	for range freeZoneMaxCommentLength {
+		freeValidCommentBuilder.WriteString("x")
+	}
+
+	var freeInvalidCommentBuilder strings.Builder
+	for range freeZoneMaxCommentLength + 1 {
+		freeInvalidCommentBuilder.WriteString("x")
+	}
+
+	var paidValidCommentBuilder strings.Builder
+	for range paidZoneMaxCommentLength {
+		paidValidCommentBuilder.WriteString("x")
+	}
+	var paidInvalidCommentBuilder strings.Builder
+	for range paidZoneMaxCommentLength + 1 {
+		paidInvalidCommentBuilder.WriteString("x")
+	}
+
+	paidProvider, err := NewCloudFlareProvider(
+		endpoint.NewDomainFilter([]string{"bar.com"}),
+		provider.ZoneIDFilter{},
+		true,
+		false,
+		"us",
+		CustomHostnamesConfig{Enabled: false},
+		DNSRecordsConfig{PerPage: 50, Comment: paidValidCommentBuilder.String()},
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	paidProvider.Client = NewMockCloudFlareClient()
+	commentTestCases := []struct {
+		name     string
+		provider *CloudFlareProvider
+		endpoint *endpoint.Endpoint
+		expected int
+	}{
+		{
+			name:     "For free Zone respecting comment length, expect no trimming",
+			provider: p,
+			endpoint: &endpoint.Endpoint{
+				DNSName:    "example.com",
+				RecordType: "A",
+				Targets:    []string{"192.0.2.1"},
+				ProviderSpecific: endpoint.ProviderSpecific{
+					{
+						Name:  annotations.CloudflareRecordCommentKey,
+						Value: freeValidCommentBuilder.String(),
+					},
+				},
+			},
+			expected: len(freeValidCommentBuilder.String()),
+		},
+		{
+			name:     "For free Zones not respecting comment length, expect trimmed comments",
+			provider: p,
+			endpoint: &endpoint.Endpoint{
+				DNSName:    "example.com",
+				RecordType: "A",
+				Targets:    []string{"192.0.2.1"},
+				ProviderSpecific: endpoint.ProviderSpecific{
+					{
+						Name:  annotations.CloudflareRecordCommentKey,
+						Value: freeInvalidCommentBuilder.String(),
+					},
+				},
+			},
+			expected: freeZoneMaxCommentLength,
+		},
+		{
+			name:     "For paid Zones respecting comment length, expect no trimming",
+			provider: paidProvider,
+			endpoint: &endpoint.Endpoint{
+				DNSName:    "bar.com",
+				RecordType: "A",
+				Targets:    []string{"192.0.2.1"},
+				ProviderSpecific: endpoint.ProviderSpecific{
+					{
+						Name:  annotations.CloudflareRecordCommentKey,
+						Value: paidValidCommentBuilder.String(),
+					},
+				},
+			},
+			expected: len(paidValidCommentBuilder.String()),
+		},
+		{
+			name:     "For paid Zones not respecting comment length, expect trimmed comments",
+			provider: paidProvider,
+			endpoint: &endpoint.Endpoint{
+				DNSName:    "bar.com",
+				RecordType: "A",
+				Targets:    []string{"192.0.2.1"},
+				ProviderSpecific: endpoint.ProviderSpecific{
+					{
+						Name:  annotations.CloudflareRecordCommentKey,
+						Value: paidInvalidCommentBuilder.String(),
+					},
+				},
+			},
+			expected: paidZoneMaxCommentLength,
+		},
+	}
+
+	for _, test := range commentTestCases {
+		t.Run(test.name, func(t *testing.T) {
+			change := test.provider.newCloudFlareChange(cloudFlareCreate, test.endpoint, test.endpoint.Targets[0], nil)
+			if len(change.ResourceRecord.Comment) != test.expected {
+				t.Errorf("expected comment to be %d characters long, but got %d", test.expected, len(change.ResourceRecord.Comment))
+			}
+		})
+	}
 }
 
 func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) {
@@ -3195,3 +3316,24 @@ func Test_dataLocalizationRegionalHostnamesChanges(t *testing.T) {
 		})
 	}
 }
+
+func TestZoneHasPaidPlan(t *testing.T) {
+	client := NewMockCloudFlareClient()
+	cfprovider := &CloudFlareProvider{
+		Client:       client,
+		domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
+		zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
+	}
+
+	assert.Equal(t, false, cfprovider.ZoneHasPaidPlan("subdomain.foo.com"))
+	assert.Equal(t, true, cfprovider.ZoneHasPaidPlan("subdomain.bar.com"))
+	assert.Equal(t, false, cfprovider.ZoneHasPaidPlan("invaliddomain"))
+
+	client.zoneDetailsError = errors.New("zone lookup failed")
+	cfproviderWithZoneError := &CloudFlareProvider{
+		Client:       client,
+		domainFilter: endpoint.NewDomainFilter([]string{"foo.com", "bar.com"}),
+		zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
+	}
+	assert.Equal(t, false, cfproviderWithZoneError.ZoneHasPaidPlan("subdomain.foo.com"))
+}
diff --git a/provider/coredns/coredns_test.go b/provider/coredns/coredns_test.go
index baf5652f..92b781e8 100644
--- a/provider/coredns/coredns_test.go
+++ b/provider/coredns/coredns_test.go
@@ -18,13 +18,14 @@ package coredns
 
 import (
 	"context"
-	"os"
 	"reflect"
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	etcdcv3 "go.etcd.io/etcd/client/v3"
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/internal/testutils"
 	"sigs.k8s.io/external-dns/plan"
 
 	"github.com/stretchr/testify/require"
@@ -87,37 +88,45 @@ func TestETCDConfig(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			closer := envSetter(tt.input)
+			testutils.TestHelperEnvSetter(t, tt.input)
 			cfg, _ := getETCDConfig()
 			if !reflect.DeepEqual(cfg, tt.want) {
 				t.Errorf("unexpected config. Got %v, want %v", cfg, tt.want)
 			}
-			t.Cleanup(closer)
 		})
 	}
-
 }
 
-func envSetter(envs map[string]string) (closer func()) {
-	originalEnvs := map[string]string{}
+func TestEtcdHttpsProtocol(t *testing.T) {
+	envs := map[string]string{
+		"ETCD_URLS": "https://example.com:2379",
+	}
+	testutils.TestHelperEnvSetter(t, envs)
 
-	for name, value := range envs {
-		if originalValue, ok := os.LookupEnv(name); ok {
-			originalEnvs[name] = originalValue
-		}
-		_ = os.Setenv(name, value)
+	cfg, err := getETCDConfig()
+	assert.NoError(t, err)
+	assert.NotNil(t, cfg)
+}
+
+func TestEtcdHttpsIncorrectConfigError(t *testing.T) {
+	envs := map[string]string{
+		"ETCD_URLS":     "https://example.com:2379",
+		"ETCD_KEY_FILE": "incorrect-path-to-etcd-tls-key",
 	}
+	testutils.TestHelperEnvSetter(t, envs)
 
-	return func() {
-		for name := range envs {
-			origValue, has := originalEnvs[name]
-			if has {
-				_ = os.Setenv(name, origValue)
-			} else {
-				_ = os.Unsetenv(name)
-			}
-		}
+	_, err := getETCDConfig()
+	assert.Errorf(t, err, "Error creating TLS config: either both cert and key or none must be provided")
+}
+
+func TestEtcdUnsupportedProtocolError(t *testing.T) {
+	envs := map[string]string{
+		"ETCD_URLS": "jdbc:ftp:RemoteHost=MyFTPServer",
 	}
+	testutils.TestHelperEnvSetter(t, envs)
+
+	_, err := getETCDConfig()
+	assert.Errorf(t, err, "etcd URLs must start with either http:// or https://")
 }
 
 func TestAServiceTranslation(t *testing.T) {
diff --git a/provider/digitalocean/digital_ocean_test.go b/provider/digitalocean/digital_ocean_test.go
index 7088b092..4fd6ec05 100644
--- a/provider/digitalocean/digital_ocean_test.go
+++ b/provider/digitalocean/digital_ocean_test.go
@@ -73,11 +73,11 @@ func (m *mockDigitalOceanClient) CreateRecord(context.Context, string, *godo.Dom
 }
 
 func (m *mockDigitalOceanClient) Delete(context.Context, string) (*godo.Response, error) {
-	return nil, nil
+	return nil, fmt.Errorf("failed to delete domain")
 }
 
 func (m *mockDigitalOceanClient) DeleteRecord(ctx context.Context, domain string, id int) (*godo.Response, error) {
-	return nil, nil
+	return nil, fmt.Errorf("failed to delete record")
 }
 
 func (m *mockDigitalOceanClient) EditRecord(ctx context.Context, domain string, id int, editRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
@@ -160,11 +160,11 @@ func (m *mockDigitalOceanRecordsFail) CreateRecord(context.Context, string, *god
 }
 
 func (m *mockDigitalOceanRecordsFail) Delete(context.Context, string) (*godo.Response, error) {
-	return nil, nil
+	return nil, fmt.Errorf("failed to delete record")
 }
 
 func (m *mockDigitalOceanRecordsFail) DeleteRecord(ctx context.Context, domain string, id int) (*godo.Response, error) {
-	return nil, nil
+	return nil, fmt.Errorf("failed to delete record")
 }
 
 func (m *mockDigitalOceanRecordsFail) EditRecord(ctx context.Context, domain string, id int, editRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
diff --git a/provider/ns1/ns1_test.go b/provider/ns1/ns1_test.go
index 6d529879..21e21fcb 100644
--- a/provider/ns1/ns1_test.go
+++ b/provider/ns1/ns1_test.go
@@ -39,15 +39,15 @@ type MockNS1DomainClient struct {
 }
 
 func (m *MockNS1DomainClient) CreateRecord(r *dns.Record) (*http.Response, error) {
-	return nil, nil
+	return &http.Response{}, nil
 }
 
 func (m *MockNS1DomainClient) DeleteRecord(zone string, domain string, t string) (*http.Response, error) {
-	return nil, nil
+	return &http.Response{}, nil
 }
 
 func (m *MockNS1DomainClient) UpdateRecord(r *dns.Record) (*http.Response, error) {
-	return nil, nil
+	return &http.Response{}, nil
 }
 
 func (m *MockNS1DomainClient) GetZone(zone string) (*dns.Zone, *http.Response, error) {
@@ -81,16 +81,16 @@ func (m *MockNS1DomainClient) ListZones() ([]*dns.Zone, *http.Response, error) {
 
 type MockNS1GetZoneFail struct{}
 
-func (m *MockNS1GetZoneFail) CreateRecord(r *dns.Record) (*http.Response, error) {
-	return nil, nil
+func (m *MockNS1GetZoneFail) CreateRecord(_ *dns.Record) (*http.Response, error) {
+	return &http.Response{}, nil
 }
 
-func (m *MockNS1GetZoneFail) DeleteRecord(zone string, domain string, t string) (*http.Response, error) {
-	return nil, nil
+func (m *MockNS1GetZoneFail) DeleteRecord(_ string, _ string, _ string) (*http.Response, error) {
+	return &http.Response{}, nil
 }
 
 func (m *MockNS1GetZoneFail) UpdateRecord(r *dns.Record) (*http.Response, error) {
-	return nil, nil
+	return &http.Response{}, nil
 }
 
 func (m *MockNS1GetZoneFail) GetZone(zone string) (*dns.Zone, *http.Response, error) {
@@ -107,20 +107,20 @@ func (m *MockNS1GetZoneFail) ListZones() ([]*dns.Zone, *http.Response, error) {
 
 type MockNS1ListZonesFail struct{}
 
-func (m *MockNS1ListZonesFail) CreateRecord(r *dns.Record) (*http.Response, error) {
-	return nil, nil
+func (m *MockNS1ListZonesFail) CreateRecord(_ *dns.Record) (*http.Response, error) {
+	return &http.Response{}, nil
 }
 
-func (m *MockNS1ListZonesFail) DeleteRecord(zone string, domain string, t string) (*http.Response, error) {
-	return nil, nil
+func (m *MockNS1ListZonesFail) DeleteRecord(_ string, _ string, _ string) (*http.Response, error) {
+	return &http.Response{}, nil
 }
 
 func (m *MockNS1ListZonesFail) UpdateRecord(r *dns.Record) (*http.Response, error) {
-	return nil, nil
+	return &http.Response{}, nil
 }
 
 func (m *MockNS1ListZonesFail) GetZone(zone string) (*dns.Zone, *http.Response, error) {
-	return &dns.Zone{}, nil, nil
+	return &dns.Zone{}, &http.Response{}, nil
 }
 
 func (m *MockNS1ListZonesFail) ListZones() ([]*dns.Zone, *http.Response, error) {
diff --git a/provider/ovh/ovh_test.go b/provider/ovh/ovh_test.go
index 2e69d7a5..4b53db20 100644
--- a/provider/ovh/ovh_test.go
+++ b/provider/ovh/ovh_test.go
@@ -328,7 +328,8 @@ func TestOvhComputeChanges(t *testing.T) {
 	}
 
 	provider := &OVHProvider{client: nil, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
-	ovhChanges := provider.computeSingleZoneChanges(t.Context(), "example.net", existingRecords, &changes)
+	ovhChanges, err := provider.computeSingleZoneChanges(t.Context(), "example.net", existingRecords, &changes)
+	td.CmpNoError(t, err)
 	td.Cmp(t, ovhChanges, []ovhChange{
 		{
 			Action: ovhCreate,
diff --git a/provider/pdns/pdns_test.go b/provider/pdns/pdns_test.go
index 773d4897..c8a22165 100644
--- a/provider/pdns/pdns_test.go
+++ b/provider/pdns/pdns_test.go
@@ -690,7 +690,7 @@ func (c *PDNSAPIClientStub) ListZone(zoneID string) (pgo.Zone, *http.Response, e
 }
 
 func (c *PDNSAPIClientStub) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
-	return nil, nil
+	return &http.Response{}, nil
 }
 
 /******************************************************************************/
@@ -721,7 +721,7 @@ func (c *PDNSAPIClientStubEmptyZones) ListZone(zoneID string) (pgo.Zone, *http.R
 
 func (c *PDNSAPIClientStubEmptyZones) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
 	c.patchedZones = append(c.patchedZones, zoneStruct)
-	return nil, nil
+	return &http.Response{}, nil
 }
 
 /******************************************************************************/
diff --git a/provider/pihole/clientV6_test.go b/provider/pihole/clientV6_test.go
index 69757179..ba140382 100644
--- a/provider/pihole/clientV6_test.go
+++ b/provider/pihole/clientV6_test.go
@@ -61,7 +61,7 @@ func TestIsValidIPv6(t *testing.T) {
 	}{
 		{"2001:0db8:85a3:0000:0000:8a2e:0370:7334", true},
 		{"2001:db8:85a3::8a2e:370:7334", true},
-		//IPV6 dual, the format is y:y:y:y:y:y:x.x.x.x.
+		//IPv6 dual, the format is y:y:y:y:y:y:x.x.x.x.
 		{"::ffff:192.168.20.3", true},
 		{"::1", true},
 		{"::", true},
diff --git a/provider/rfc2136/rfc2136_test.go b/provider/rfc2136/rfc2136_test.go
index 8bdff70c..970ff2c4 100644
--- a/provider/rfc2136/rfc2136_test.go
+++ b/provider/rfc2136/rfc2136_test.go
@@ -886,10 +886,25 @@ func contains(arr []*endpoint.Endpoint, name string) bool {
 	return false
 }
 
+// TestCreateRfc2136StubProviderWithHosts validates the stub provider initializes with multiple nameservers.
+func TestCreateRfc2136StubProviderWithHosts(t *testing.T) {
+	stub := newStub()
+	provider, err := createRfc2136StubProviderWithHosts(stub)
+	assert.NoError(t, err)
+
+	rawProvider, ok := provider.(*rfc2136Provider)
+	assert.True(t, ok, "expected provider to be of type *rfc2136Provider")
+
+	assert.Equal(t, 3, len(rawProvider.nameservers))
+	assert.Equal(t, "rfc2136-host1:0", rawProvider.nameservers[0])
+	assert.Equal(t, "rfc2136-host2:0", rawProvider.nameservers[1])
+	assert.Equal(t, "rfc2136-host3:0", rawProvider.nameservers[2])
+}
+
 // TestRoundRobinLoadBalancing tests the round-robin load balancing strategy.
 func TestRoundRobinLoadBalancing(t *testing.T) {
 	stub := newStubLB("round-robin", []string{"rfc2136-host1", "rfc2136-host2", "rfc2136-host3"})
-	_, err := createRfc2136StubProviderWithHosts(stub)
+	_, err := createRfc2136StubProviderWithStrategy(stub, "round-robin")
 	assert.NoError(t, err)
 
 	m := new(dns.Msg)
@@ -908,7 +923,7 @@ func TestRoundRobinLoadBalancing(t *testing.T) {
 // TestRandomLoadBalancing tests the random load balancing strategy.
 func TestRandomLoadBalancing(t *testing.T) {
 	stub := newStubLB("random", []string{"rfc2136-host1", "rfc2136-host2", "rfc2136-host3"})
-	_, err := createRfc2136StubProvider(stub)
+	_, err := createRfc2136StubProviderWithStrategy(stub, "random")
 	assert.NoError(t, err)
 
 	m := new(dns.Msg)
diff --git a/provider/scaleway/scaleway_test.go b/provider/scaleway/scaleway_test.go
index 560687c7..45f09b32 100644
--- a/provider/scaleway/scaleway_test.go
+++ b/provider/scaleway/scaleway_test.go
@@ -106,8 +106,8 @@ func (m *mockScalewayDomain) ListDNSZoneRecords(req *domain.ListDNSZoneRecordsRe
 	}, nil
 }
 
-func (m *mockScalewayDomain) UpdateDNSZoneRecords(req *domain.UpdateDNSZoneRecordsRequest, opts ...scw.RequestOption) (*domain.UpdateDNSZoneRecordsResponse, error) {
-	return nil, nil
+func (m *mockScalewayDomain) UpdateDNSZoneRecords(_ *domain.UpdateDNSZoneRecordsRequest, _ ...scw.RequestOption) (*domain.UpdateDNSZoneRecordsResponse, error) {
+	return &domain.UpdateDNSZoneRecordsResponse{}, nil
 }
 
 func TestScalewayProvider_NewScalewayProvider(t *testing.T) {
diff --git a/provider/ultradns/ultradns_test.go b/provider/ultradns/ultradns_test.go
index 2d027ccc..380650e3 100644
--- a/provider/ultradns/ultradns_test.go
+++ b/provider/ultradns/ultradns_test.go
@@ -65,11 +65,11 @@ type mockUltraDNSRecord struct {
 	client *udnssdk.Client
 }
 
-func (m *mockUltraDNSRecord) Create(k udnssdk.RRSetKey, rrset udnssdk.RRSet) (*http.Response, error) {
-	return nil, nil
+func (m *mockUltraDNSRecord) Create(_ udnssdk.RRSetKey, _ udnssdk.RRSet) (*http.Response, error) {
+	return &http.Response{}, nil
 }
 
-func (m *mockUltraDNSRecord) Select(k udnssdk.RRSetKey) ([]udnssdk.RRSet, error) {
+func (m *mockUltraDNSRecord) Select(_ udnssdk.RRSetKey) ([]udnssdk.RRSet, error) {
 	return []udnssdk.RRSet{{
 		OwnerName: "test-ultradns-provider.com.",
 		RRType:    endpoint.RecordTypeA,
@@ -83,11 +83,11 @@ func (m *mockUltraDNSRecord) SelectWithOffset(k udnssdk.RRSetKey, offset int) ([
 }
 
 func (m *mockUltraDNSRecord) Update(udnssdk.RRSetKey, udnssdk.RRSet) (*http.Response, error) {
-	return nil, nil
+	return &http.Response{}, nil
 }
 
 func (m *mockUltraDNSRecord) Delete(k udnssdk.RRSetKey) (*http.Response, error) {
-	return nil, nil
+	return &http.Response{}, nil
 }
 
 func (m *mockUltraDNSRecord) SelectWithOffsetWithLimit(k udnssdk.RRSetKey, offset int, limit int) (rrsets []udnssdk.RRSet, ResultInfo udnssdk.ResultInfo, resp *http.Response, err error) {
diff --git a/provider/zone_id_filter_test.go b/provider/zone_id_filter_test.go
index 41e31388..65d6e206 100644
--- a/provider/zone_id_filter_test.go
+++ b/provider/zone_id_filter_test.go
@@ -28,6 +28,11 @@ type zoneIDFilterTest struct {
 	expected     bool
 }
 
+type zoneIdFilterTestIsConfigured struct {
+	zoneIDFilter []string
+	expected     bool
+}
+
 func TestZoneIDFilterMatch(t *testing.T) {
 	zone := "/hostedzone/ZTST1"
 
@@ -37,6 +42,11 @@ func TestZoneIDFilterMatch(t *testing.T) {
 			zone,
 			true,
 		},
+		{
+			[]string{""},
+			zone,
+			true,
+		},
 		{
 			[]string{"/hostedzone/ZTST1"},
 			zone,
@@ -77,3 +87,31 @@ func TestZoneIDFilterMatch(t *testing.T) {
 		assert.Equal(t, tt.expected, zoneIDFilter.Match(tt.zone))
 	}
 }
+
+func TestZoneIDFilterIsConfigured(t *testing.T) {
+	for _, tt := range []zoneIdFilterTestIsConfigured{
+		{
+			[]string{""},
+			false,
+		},
+		{
+			[]string{},
+			false,
+		},
+		{
+			[]string{"/hostedzone/ZTST2"},
+			true,
+		},
+		{
+			[]string{"/hostedzone/ZTST2", "hostedzone/ZTST2"},
+			true,
+		},
+		{
+			[]string{"/ZSTS2"},
+			true,
+		},
+	} {
+		zoneIDFilter := NewZoneIDFilter(tt.zoneIDFilter)
+		assert.Equal(t, tt.expected, zoneIDFilter.IsConfigured())
+	}
+}
diff --git a/source/ambassador_host_test.go b/source/ambassador_host_test.go
index 970bb2a1..d6246d85 100644
--- a/source/ambassador_host_test.go
+++ b/source/ambassador_host_test.go
@@ -33,6 +33,7 @@ import (
 	fakeDynamic "k8s.io/client-go/dynamic/fake"
 	fakeKube "k8s.io/client-go/kubernetes/fake"
 	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 const defaultAmbassadorNamespace = "ambassador"
@@ -246,8 +247,8 @@ func TestAmbassadorHostSource(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "basic-host",
 					Annotations: map[string]string{
-						ambHostAnnotation:    hostAnnotation,
-						CloudflareProxiedKey: "true",
+						ambHostAnnotation:                hostAnnotation,
+						annotations.CloudflareProxiedKey: "true",
 					},
 				},
 				Spec: &ambassador.HostSpec{
diff --git a/source/annotations/processors_test.go b/source/annotations/processors_test.go
new file mode 100644
index 00000000..d423edc7
--- /dev/null
+++ b/source/annotations/processors_test.go
@@ -0,0 +1,350 @@
+/*
+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 annotations
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/labels"
+	"sigs.k8s.io/external-dns/endpoint"
+)
+
+func TestParseAnnotationFilter(t *testing.T) {
+	tests := []struct {
+		name             string
+		annotationFilter string
+		expectedSelector labels.Selector
+		expectError      bool
+	}{
+		{
+			name:             "valid annotation filter",
+			annotationFilter: "key1=value1,key2=value2",
+			expectedSelector: labels.Set{"key1": "value1", "key2": "value2"}.AsSelector(),
+			expectError:      false,
+		},
+		{
+			name:             "invalid annotation filter",
+			annotationFilter: "key1==value1",
+			expectedSelector: labels.Set{"key1": "value1"}.AsSelector(),
+			expectError:      false,
+		},
+		{
+			name:             "empty annotation filter",
+			annotationFilter: "",
+			expectedSelector: labels.Set{}.AsSelector(),
+			expectError:      false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			selector, err := ParseFilter(tt.annotationFilter)
+			if tt.expectError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.expectedSelector, selector)
+			}
+		})
+	}
+}
+
+func TestTargetsFromTargetAnnotation(t *testing.T) {
+	tests := []struct {
+		name        string
+		annotations map[string]string
+		expected    endpoint.Targets
+	}{
+		{
+			name:        "no target annotation",
+			annotations: map[string]string{},
+			expected:    endpoint.Targets(nil),
+		},
+		{
+			name: "single target annotation",
+			annotations: map[string]string{
+				TargetKey: "example.com",
+			},
+			expected: endpoint.Targets{"example.com"},
+		},
+		{
+			name: "multiple target annotations",
+			annotations: map[string]string{
+				TargetKey: "example.com,example.org",
+			},
+			expected: endpoint.Targets{"example.com", "example.org"},
+		},
+		{
+			name: "target annotation with trailing periods",
+			annotations: map[string]string{
+				TargetKey: "example.com.,example.org.",
+			},
+			expected: endpoint.Targets{"example.com", "example.org"},
+		},
+		{
+			name: "target annotation with spaces",
+			annotations: map[string]string{
+				TargetKey: " example.com , example.org ",
+			},
+			expected: endpoint.Targets{"example.com", "example.org"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := TargetsFromTargetAnnotation(tt.annotations)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestTTLFromAnnotations(t *testing.T) {
+	tests := []struct {
+		name        string
+		annotations map[string]string
+		resource    string
+		expectedTTL endpoint.TTL
+	}{
+		{
+			name:        "no TTL annotation",
+			annotations: map[string]string{},
+			resource:    "test-resource",
+			expectedTTL: endpoint.TTL(0),
+		},
+		{
+			name: "valid TTL annotation",
+			annotations: map[string]string{
+				TtlKey: "600",
+			},
+			resource:    "test-resource",
+			expectedTTL: endpoint.TTL(600),
+		},
+		{
+			name: "invalid TTL annotation",
+			annotations: map[string]string{
+				TtlKey: "invalid",
+			},
+			resource:    "test-resource",
+			expectedTTL: endpoint.TTL(0),
+		},
+		{
+			name: "TTL annotation out of range",
+			annotations: map[string]string{
+				TtlKey: "999999",
+			},
+			resource:    "test-resource",
+			expectedTTL: endpoint.TTL(999999),
+		},
+		{
+			name:        "TTL annotation not present",
+			annotations: map[string]string{"foo": "bar"},
+			expectedTTL: endpoint.TTL(0),
+		},
+		{
+			name:        "TTL annotation value is not a number",
+			annotations: map[string]string{TtlKey: "foo"},
+			expectedTTL: endpoint.TTL(0),
+		},
+		{
+			name:        "TTL annotation value is empty",
+			annotations: map[string]string{TtlKey: ""},
+			expectedTTL: endpoint.TTL(0),
+		},
+		{
+			name:        "TTL annotation value is negative number",
+			annotations: map[string]string{TtlKey: "-1"},
+			expectedTTL: endpoint.TTL(0),
+		},
+		{
+			name:        "TTL annotation value is too high",
+			annotations: map[string]string{TtlKey: fmt.Sprintf("%d", 1<<32)},
+			expectedTTL: endpoint.TTL(0),
+		},
+		{
+			name:        "TTL annotation value is set correctly using integer",
+			annotations: map[string]string{TtlKey: "60"},
+			expectedTTL: endpoint.TTL(60),
+		},
+		{
+			name:        "TTL annotation value is set correctly using duration (whole)",
+			annotations: map[string]string{TtlKey: "10m"},
+			expectedTTL: endpoint.TTL(600),
+		},
+		{
+			name:        "TTL annotation value is set correctly using duration (fractional)",
+			annotations: map[string]string{TtlKey: "20.5s"},
+			expectedTTL: endpoint.TTL(20),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ttl := TTLFromAnnotations(tt.annotations, tt.resource)
+			assert.Equal(t, tt.expectedTTL, ttl)
+		})
+	}
+}
+
+func TestGetAliasFromAnnotations(t *testing.T) {
+	tests := []struct {
+		name        string
+		annotations map[string]string
+		expected    bool
+	}{
+		{
+			name:        "alias annotation exists and is true",
+			annotations: map[string]string{AliasKey: "true"},
+			expected:    true,
+		},
+		{
+			name:        "alias annotation exists and is false",
+			annotations: map[string]string{AliasKey: "false"},
+			expected:    false,
+		},
+		{
+			name:        "alias annotation does not exist",
+			annotations: map[string]string{},
+			expected:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := hasAliasFromAnnotations(tt.annotations)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestHostnamesFromAnnotations(t *testing.T) {
+	tests := []struct {
+		name        string
+		annotations map[string]string
+		expected    []string
+	}{
+		{
+			name:        "no hostname annotation",
+			annotations: map[string]string{},
+			expected:    nil,
+		},
+		{
+			name: "single hostname annotation",
+			annotations: map[string]string{
+				HostnameKey: "example.com",
+			},
+			expected: []string{"example.com"},
+		},
+		{
+			name: "multiple hostname annotations",
+			annotations: map[string]string{
+				HostnameKey: "example.com,example.org",
+			},
+			expected: []string{"example.com", "example.org"},
+		},
+		{
+			name: "hostname annotation with spaces",
+			annotations: map[string]string{
+				HostnameKey: " example.com , example.org ",
+			},
+			expected: []string{"example.com", "example.org"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := HostnamesFromAnnotations(tt.annotations)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestSplitHostnameAnnotation(t *testing.T) {
+	tests := []struct {
+		name       string
+		annotation string
+		expected   []string
+	}{
+		{
+			name:       "empty annotation",
+			annotation: "",
+			expected:   []string{""},
+		},
+		{
+			name:       "single hostname",
+			annotation: "example.com",
+			expected:   []string{"example.com"},
+		},
+		{
+			name:       "multiple hostnames",
+			annotation: "example.com,example.org",
+			expected:   []string{"example.com", "example.org"},
+		},
+		{
+			name:       "hostnames with spaces",
+			annotation: " example.com , example.org ",
+			expected:   []string{"example.com", "example.org"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := SplitHostnameAnnotation(tt.annotation)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestInternalHostnamesFromAnnotations(t *testing.T) {
+	tests := []struct {
+		name        string
+		annotations map[string]string
+		expected    []string
+	}{
+		{
+			name:        "no internal hostname annotation",
+			annotations: map[string]string{},
+			expected:    nil,
+		},
+		{
+			name: "single internal hostname annotation",
+			annotations: map[string]string{
+				InternalHostnameKey: "internal.example.com",
+			},
+			expected: []string{"internal.example.com"},
+		},
+		{
+			name: "multiple internal hostname annotations",
+			annotations: map[string]string{
+				InternalHostnameKey: "internal.example.com,internal.example.org",
+			},
+			expected: []string{"internal.example.com", "internal.example.org"},
+		},
+		{
+			name: "internal hostname annotation with spaces",
+			annotations: map[string]string{
+				InternalHostnameKey: " internal.example.com , internal.example.org ",
+			},
+			expected: []string{"internal.example.com", "internal.example.org"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := InternalHostnamesFromAnnotations(tt.annotations)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
diff --git a/source/annotations/provider_specific_test.go b/source/annotations/provider_specific_test.go
new file mode 100644
index 00000000..e9fd7633
--- /dev/null
+++ b/source/annotations/provider_specific_test.go
@@ -0,0 +1,348 @@
+/*
+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 annotations
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"sigs.k8s.io/external-dns/endpoint"
+)
+
+func TestProviderSpecificAnnotations(t *testing.T) {
+	tests := []struct {
+		name          string
+		annotations   map[string]string
+		expected      endpoint.ProviderSpecific
+		setIdentifier string
+	}{
+		{
+			name:          "no annotations",
+			annotations:   map[string]string{},
+			expected:      endpoint.ProviderSpecific{},
+			setIdentifier: "",
+		},
+		{
+			name: "Cloudflare proxied annotation",
+			annotations: map[string]string{
+				CloudflareProxiedKey: "true",
+			},
+			expected: endpoint.ProviderSpecific{
+				{Name: CloudflareProxiedKey, Value: "true"},
+			},
+			setIdentifier: "",
+		},
+		{
+			name: "Cloudflare custom hostname annotation",
+			annotations: map[string]string{
+				CloudflareCustomHostnameKey: "custom.example.com",
+			},
+			expected: endpoint.ProviderSpecific{
+				{Name: CloudflareCustomHostnameKey, Value: "custom.example.com"},
+			},
+			setIdentifier: "",
+		},
+		{
+			name: "AWS annotation",
+			annotations: map[string]string{
+				"external-dns.alpha.kubernetes.io/aws-weight": "100",
+			},
+			expected: endpoint.ProviderSpecific{
+				{Name: "aws/weight", Value: "100"},
+			},
+			setIdentifier: "",
+		},
+		{
+			name: "Set identifier annotation",
+			annotations: map[string]string{
+				SetIdentifierKey: "identifier",
+			},
+			expected:      endpoint.ProviderSpecific{},
+			setIdentifier: "identifier",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, setIdentifier := ProviderSpecificAnnotations(tt.annotations)
+			assert.Equal(t, tt.expected, result)
+			assert.Equal(t, tt.setIdentifier, setIdentifier)
+		})
+	}
+}
+
+func TestGetProviderSpecificCloudflareAnnotations(t *testing.T) {
+	for _, tc := range []struct {
+		title         string
+		annotations   map[string]string
+		expectedKey   string
+		expectedValue bool
+	}{
+		{
+			title:         "Cloudflare proxied annotation is set correctly to true",
+			annotations:   map[string]string{CloudflareProxiedKey: "true"},
+			expectedKey:   CloudflareProxiedKey,
+			expectedValue: true,
+		},
+		{
+			title:         "Cloudflare proxied annotation is set correctly to false",
+			annotations:   map[string]string{CloudflareProxiedKey: "false"},
+			expectedKey:   CloudflareProxiedKey,
+			expectedValue: false,
+		},
+		{
+			title: "Cloudflare proxied annotation among another annotations is set correctly to true",
+			annotations: map[string]string{
+				"random annotation 1": "random value 1",
+				CloudflareProxiedKey:  "false",
+				"random annotation 2": "random value 2",
+			},
+			expectedKey:   CloudflareProxiedKey,
+			expectedValue: false,
+		},
+	} {
+		t.Run(tc.title, func(t *testing.T) {
+			providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations)
+			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
+				if providerSpecificAnnotation.Name == tc.expectedKey {
+					assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value)
+					return
+				}
+			}
+			t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue)
+		})
+	}
+
+	for _, tc := range []struct {
+		title         string
+		annotations   map[string]string
+		expectedKey   string
+		expectedValue string
+	}{
+		{
+			title:         "Cloudflare region key annotation is set correctly",
+			annotations:   map[string]string{CloudflareRegionKey: "us"},
+			expectedKey:   CloudflareRegionKey,
+			expectedValue: "us",
+		},
+		{
+			title: "Cloudflare region key annotation among another annotations is set correctly",
+			annotations: map[string]string{
+				"random annotation 1": "random value 1",
+				CloudflareRegionKey:   "us",
+				"random annotation 2": "random value 2",
+			},
+			expectedKey:   CloudflareRegionKey,
+			expectedValue: "us",
+		},
+		{
+			title: "Cloudflare DNS record comment annotation is set correctly",
+			annotations: map[string]string{
+				CloudflareRecordCommentKey: "comment",
+			},
+			expectedKey:   CloudflareRecordCommentKey,
+			expectedValue: "comment",
+		},
+	} {
+		t.Run(tc.title, func(t *testing.T) {
+			providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations)
+			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
+				if providerSpecificAnnotation.Name == tc.expectedKey {
+					assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value)
+					return
+				}
+			}
+			t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue)
+		})
+	}
+
+	for _, tc := range []struct {
+		title         string
+		annotations   map[string]string
+		expectedKey   string
+		expectedValue string
+	}{
+		{
+			title:         "Cloudflare custom hostname annotation is set correctly",
+			annotations:   map[string]string{CloudflareCustomHostnameKey: "a.foo.fancybar.com"},
+			expectedKey:   CloudflareCustomHostnameKey,
+			expectedValue: "a.foo.fancybar.com",
+		},
+		{
+			title: "Cloudflare custom hostname annotation among another annotations is set correctly",
+			annotations: map[string]string{
+				"random annotation 1":       "random value 1",
+				CloudflareCustomHostnameKey: "a.foo.fancybar.com",
+				"random annotation 2":       "random value 2"},
+			expectedKey:   CloudflareCustomHostnameKey,
+			expectedValue: "a.foo.fancybar.com",
+		},
+	} {
+		t.Run(tc.title, func(t *testing.T) {
+			providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations)
+			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
+				if providerSpecificAnnotation.Name == tc.expectedKey {
+					assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value)
+					return
+				}
+			}
+			t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %s", tc.expectedKey, tc.expectedValue)
+		})
+	}
+}
+
+func TestGetProviderSpecificAliasAnnotations(t *testing.T) {
+	for _, tc := range []struct {
+		title         string
+		annotations   map[string]string
+		expectedKey   string
+		expectedValue bool
+	}{
+		{
+			title:         "alias annotation is set correctly to true",
+			annotations:   map[string]string{AliasKey: "true"},
+			expectedKey:   AliasKey,
+			expectedValue: true,
+		},
+		{
+			title: "alias annotation among another annotations is set correctly to true",
+			annotations: map[string]string{
+				"random annotation 1": "random value 1",
+				AliasKey:              "true",
+				"random annotation 2": "random value 2",
+			},
+			expectedKey:   AliasKey,
+			expectedValue: true,
+		},
+	} {
+		t.Run(tc.title, func(t *testing.T) {
+			providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations)
+			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
+				if providerSpecificAnnotation.Name == "alias" {
+					assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value)
+					return
+				}
+			}
+			t.Errorf("provider specific annotation alias is not set correctly to %v", tc.expectedValue)
+		})
+	}
+
+	for _, tc := range []struct {
+		title       string
+		annotations map[string]string
+	}{
+		{
+			title:       "alias annotation is set to false",
+			annotations: map[string]string{AliasKey: "false"},
+		},
+		{
+			title: "alias annotation is not set",
+			annotations: map[string]string{
+				"random annotation 1": "random value 1",
+				"random annotation 2": "random value 2",
+			},
+		},
+	} {
+		t.Run(tc.title, func(t *testing.T) {
+			providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations)
+			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
+				if providerSpecificAnnotation.Name == "alias" {
+					t.Error("provider specific annotation alias is not expected to be set")
+				}
+			}
+
+		})
+	}
+}
+
+func TestGetProviderSpecificIdentifierAnnotations(t *testing.T) {
+	for _, tc := range []struct {
+		title              string
+		annotations        map[string]string
+		expectedResult     map[string]string
+		expectedIdentifier string
+	}{
+		{
+			title: "aws- provider specific annotations are set correctly",
+			annotations: map[string]string{
+				"external-dns.alpha.kubernetes.io/aws-annotation-1": "value 1",
+				SetIdentifierKey: "id1",
+				"external-dns.alpha.kubernetes.io/aws-annotation-2": "value 2",
+			},
+			expectedResult: map[string]string{
+				"aws/annotation-1": "value 1",
+				"aws/annotation-2": "value 2",
+			},
+			expectedIdentifier: "id1",
+		},
+		{
+			title: "scw- provider specific annotations are set correctly",
+			annotations: map[string]string{
+				"external-dns.alpha.kubernetes.io/scw-annotation-1": "value 1",
+				SetIdentifierKey: "id1",
+				"external-dns.alpha.kubernetes.io/scw-annotation-2": "value 2",
+			},
+			expectedResult: map[string]string{
+				"scw/annotation-1": "value 1",
+				"scw/annotation-2": "value 2",
+			},
+			expectedIdentifier: "id1",
+		},
+		{
+			title: "ibmcloud- provider specific annotations are set correctly",
+			annotations: map[string]string{
+				"external-dns.alpha.kubernetes.io/ibmcloud-annotation-1": "value 1",
+				SetIdentifierKey: "id1",
+				"external-dns.alpha.kubernetes.io/ibmcloud-annotation-2": "value 2",
+			},
+			expectedResult: map[string]string{
+				"ibmcloud-annotation-1": "value 1",
+				"ibmcloud-annotation-2": "value 2",
+			},
+			expectedIdentifier: "id1",
+		},
+		{
+			title: "webhook- provider specific annotations are set correctly",
+			annotations: map[string]string{
+				"external-dns.alpha.kubernetes.io/webhook-annotation-1": "value 1",
+				SetIdentifierKey: "id1",
+				"external-dns.alpha.kubernetes.io/webhook-annotation-2": "value 2",
+			},
+			expectedResult: map[string]string{
+				"webhook/annotation-1": "value 1",
+				"webhook/annotation-2": "value 2",
+			},
+			expectedIdentifier: "id1",
+		},
+	} {
+		t.Run(tc.title, func(t *testing.T) {
+			providerSpecificAnnotations, identifier := ProviderSpecificAnnotations(tc.annotations)
+			assert.Equal(t, tc.expectedIdentifier, identifier)
+			for expectedAnnotationKey, expectedAnnotationValue := range tc.expectedResult {
+				expectedResultFound := false
+				for _, providerSpecificAnnotation := range providerSpecificAnnotations {
+					if providerSpecificAnnotation.Name == expectedAnnotationKey {
+						assert.Equal(t, expectedAnnotationValue, providerSpecificAnnotation.Value)
+						expectedResultFound = true
+						break
+					}
+				}
+				if !expectedResultFound {
+					t.Errorf("provider specific annotation %s has not been set", expectedAnnotationKey)
+				}
+			}
+		})
+	}
+}
diff --git a/source/endpoints_test.go b/source/endpoints_test.go
new file mode 100644
index 00000000..a90b8656
--- /dev/null
+++ b/source/endpoints_test.go
@@ -0,0 +1,255 @@
+/*
+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 source
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	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"
+	"sigs.k8s.io/external-dns/endpoint"
+)
+
+func TestEndpointsForHostname(t *testing.T) {
+	tests := []struct {
+		name             string
+		hostname         string
+		targets          endpoint.Targets
+		ttl              endpoint.TTL
+		providerSpecific endpoint.ProviderSpecific
+		setIdentifier    string
+		resource         string
+		expected         []*endpoint.Endpoint
+	}{
+		{
+			name:     "A record targets",
+			hostname: "example.com",
+			targets:  endpoint.Targets{"192.0.2.1", "192.0.2.2"},
+			ttl:      endpoint.TTL(300),
+			providerSpecific: endpoint.ProviderSpecific{
+				{Name: "provider", Value: "value"},
+			},
+			setIdentifier: "identifier",
+			resource:      "resource",
+			expected: []*endpoint.Endpoint{
+				{
+					DNSName:          "example.com",
+					Targets:          endpoint.Targets{"192.0.2.1", "192.0.2.2"},
+					RecordType:       endpoint.RecordTypeA,
+					RecordTTL:        endpoint.TTL(300),
+					ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}},
+					SetIdentifier:    "identifier",
+					Labels:           map[string]string{endpoint.ResourceLabelKey: "resource"},
+				},
+			},
+		},
+		{
+			name:     "AAAA record targets",
+			hostname: "example.com",
+			targets:  endpoint.Targets{"2001:db8::1", "2001:db8::2"},
+			ttl:      endpoint.TTL(300),
+			providerSpecific: endpoint.ProviderSpecific{
+				{Name: "provider", Value: "value"},
+			},
+			setIdentifier: "identifier",
+			resource:      "resource",
+			expected: []*endpoint.Endpoint{
+				{
+					DNSName:          "example.com",
+					Targets:          endpoint.Targets{"2001:db8::1", "2001:db8::2"},
+					RecordType:       endpoint.RecordTypeAAAA,
+					RecordTTL:        endpoint.TTL(300),
+					ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}},
+					SetIdentifier:    "identifier",
+					Labels:           map[string]string{endpoint.ResourceLabelKey: "resource"},
+				},
+			},
+		},
+		{
+			name:     "CNAME record targets",
+			hostname: "example.com",
+			targets:  endpoint.Targets{"cname.example.com"},
+			ttl:      endpoint.TTL(300),
+			providerSpecific: endpoint.ProviderSpecific{
+				{Name: "provider", Value: "value"},
+			},
+			setIdentifier: "identifier",
+			resource:      "resource",
+			expected: []*endpoint.Endpoint{
+				{
+					DNSName:          "example.com",
+					Targets:          endpoint.Targets{"cname.example.com"},
+					RecordType:       endpoint.RecordTypeCNAME,
+					RecordTTL:        endpoint.TTL(300),
+					ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}},
+					SetIdentifier:    "identifier",
+					Labels:           map[string]string{endpoint.ResourceLabelKey: "resource"},
+				},
+			},
+		},
+		{
+			name:             "No targets",
+			hostname:         "example.com",
+			targets:          endpoint.Targets{},
+			ttl:              endpoint.TTL(300),
+			providerSpecific: endpoint.ProviderSpecific{},
+			setIdentifier:    "",
+			resource:         "",
+			expected:         []*endpoint.Endpoint(nil),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := endpointsForHostname(tt.hostname, tt.targets, tt.ttl, tt.providerSpecific, tt.setIdentifier, tt.resource)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestEndpointTargetsFromServices(t *testing.T) {
+	tests := []struct {
+		name      string
+		services  []*corev1.Service
+		namespace string
+		selector  map[string]string
+		expected  endpoint.Targets
+		wantErr   bool
+	}{
+		{
+			name:      "no services",
+			services:  []*corev1.Service{},
+			namespace: "default",
+			selector:  map[string]string{"app": "nginx"},
+			expected:  endpoint.Targets{},
+			wantErr:   false,
+		},
+		{
+			name: "matching service with external IPs",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "svc1",
+						Namespace: "default",
+					},
+					Spec: corev1.ServiceSpec{
+						Selector:    map[string]string{"app": "nginx"},
+						ExternalIPs: []string{"192.0.2.1", "158.123.32.23"},
+					},
+				},
+			},
+			namespace: "default",
+			selector:  map[string]string{"app": "nginx"},
+			expected:  endpoint.Targets{"192.0.2.1", "158.123.32.23"},
+			wantErr:   false,
+		},
+		{
+			name: "matching service with load balancer IP",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "svc2",
+						Namespace: "default",
+					},
+					Spec: corev1.ServiceSpec{
+						Selector: map[string]string{"app": "nginx"},
+					},
+					Status: corev1.ServiceStatus{
+						LoadBalancer: corev1.LoadBalancerStatus{
+							Ingress: []corev1.LoadBalancerIngress{
+								{IP: "192.0.2.2"},
+							},
+						},
+					},
+				},
+			},
+			namespace: "default",
+			selector:  map[string]string{"app": "nginx"},
+			expected:  endpoint.Targets{"192.0.2.2"},
+			wantErr:   false,
+		},
+		{
+			name: "matching service with load balancer hostname",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "svc3",
+						Namespace: "default",
+					},
+					Spec: corev1.ServiceSpec{
+						Selector: map[string]string{"app": "nginx"},
+					},
+					Status: corev1.ServiceStatus{
+						LoadBalancer: corev1.LoadBalancerStatus{
+							Ingress: []corev1.LoadBalancerIngress{
+								{Hostname: "lb.example.com"},
+							},
+						},
+					},
+				},
+			},
+			namespace: "default",
+			selector:  map[string]string{"app": "nginx"},
+			expected:  endpoint.Targets{"lb.example.com"},
+			wantErr:   false,
+		},
+		{
+			name: "no matching services",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "svc4",
+						Namespace: "default",
+					},
+					Spec: corev1.ServiceSpec{
+						Selector: map[string]string{"app": "apache"},
+					},
+				},
+			},
+			namespace: "default",
+			selector:  map[string]string{"app": "nginx"},
+			expected:  endpoint.Targets{},
+			wantErr:   false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client := fake.NewSimpleClientset()
+			informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(client, 0,
+				kubeinformers.WithNamespace(tt.namespace))
+			serviceInformer := informerFactory.Core().V1().Services()
+
+			for _, svc := range tt.services {
+				_, err := client.CoreV1().Services(tt.namespace).Create(context.Background(), svc, metav1.CreateOptions{})
+				assert.NoError(t, err)
+
+				err = serviceInformer.Informer().GetIndexer().Add(svc)
+				assert.NoError(t, err)
+			}
+
+			result, err := EndpointTargetsFromServices(serviceInformer, tt.namespace, tt.selector)
+			if tt.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.expected, result)
+			}
+		})
+	}
+}
diff --git a/source/fqdn/fqdn_test.go b/source/fqdn/fqdn_test.go
index d135bad8..3aa23807 100644
--- a/source/fqdn/fqdn_test.go
+++ b/source/fqdn/fqdn_test.go
@@ -60,6 +60,31 @@ func TestParseTemplate(t *testing.T) {
 			expectError:      false,
 			annotationFilter: "kubernetes.io/ingress.class=nginx",
 		},
+		{
+			name:         "replace template function",
+			expectError:  false,
+			fqdnTemplate: "{{\"hello.world\" | replace \".\" \"-\"}}.ext-dns.test.com",
+		},
+		{
+			name:         "isIPv4 template function with valid IPv4",
+			expectError:  false,
+			fqdnTemplate: "{{if isIPv4 \"192.168.1.1\"}}valid{{else}}invalid{{end}}.ext-dns.test.com",
+		},
+		{
+			name:         "isIPv4 template function with invalid IPv4",
+			expectError:  false,
+			fqdnTemplate: "{{if isIPv4 \"not.an.ip.addr\"}}valid{{else}}invalid{{end}}.ext-dns.test.com",
+		},
+		{
+			name:         "isIPv6 template function with valid IPv6",
+			expectError:  false,
+			fqdnTemplate: "{{if isIPv6 \"2001:db8::1\"}}valid{{else}}invalid{{end}}.ext-dns.test.com",
+		},
+		{
+			name:         "isIPv6 template function with invalid IPv6",
+			expectError:  false,
+			fqdnTemplate: "{{if isIPv6 \"not:ipv6:addr\"}}valid{{else}}invalid{{end}}.ext-dns.test.com",
+		},
 	} {
 		t.Run(tt.name, func(t *testing.T) {
 			_, err := ParseTemplate(tt.fqdnTemplate)
@@ -71,3 +96,166 @@ func TestParseTemplate(t *testing.T) {
 		})
 	}
 }
+
+func TestFqdnTemplate(t *testing.T) {
+	tests := []struct {
+		name          string
+		fqdnTemplate  string
+		expectedError bool
+	}{
+		{
+			name:          "empty template",
+			fqdnTemplate:  "",
+			expectedError: false,
+		},
+		{
+			name:          "valid template",
+			fqdnTemplate:  "{{ .Name }}.example.com",
+			expectedError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tmpl, err := ParseTemplate(tt.fqdnTemplate)
+			if tt.expectedError {
+				assert.Error(t, err)
+				assert.Nil(t, tmpl)
+			} else {
+				assert.NoError(t, err)
+				if tt.fqdnTemplate == "" {
+					assert.Nil(t, tmpl)
+				} else {
+					assert.NotNil(t, tmpl)
+				}
+			}
+		})
+	}
+}
+
+func TestReplace(t *testing.T) {
+	for _, tt := range []struct {
+		name     string
+		oldValue string
+		newValue string
+		target   string
+		expected string
+	}{
+		{
+			name:     "simple replacement",
+			oldValue: "old",
+			newValue: "new",
+			target:   "old-value",
+			expected: "new-value",
+		},
+		{
+			name:     "multiple replacements",
+			oldValue: ".",
+			newValue: "-",
+			target:   "hello.world.com",
+			expected: "hello-world-com",
+		},
+		{
+			name:     "no replacement needed",
+			oldValue: "x",
+			newValue: "y",
+			target:   "hello-world",
+			expected: "hello-world",
+		},
+		{
+			name:     "empty strings",
+			oldValue: "",
+			newValue: "",
+			target:   "test",
+			expected: "test",
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			result := replace(tt.oldValue, tt.newValue, tt.target)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsIPv6String(t *testing.T) {
+	for _, tt := range []struct {
+		name     string
+		input    string
+		expected bool
+	}{
+		{
+			name:     "valid IPv6",
+			input:    "2001:db8::1",
+			expected: true,
+		},
+		{
+			name:     "valid IPv6 with multiple segments",
+			input:    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+			expected: true,
+		},
+		{
+			name:     "valid IPv4-mapped IPv6",
+			input:    "::ffff:192.168.1.1",
+			expected: true,
+		},
+		{
+			name:     "invalid IPv6",
+			input:    "not:ipv6:addr",
+			expected: false,
+		},
+		{
+			name:     "IPv4 address",
+			input:    "192.168.1.1",
+			expected: false,
+		},
+		{
+			name:     "empty string",
+			input:    "",
+			expected: false,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			result := isIPv6String(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsIPv4String(t *testing.T) {
+	for _, tt := range []struct {
+		name     string
+		input    string
+		expected bool
+	}{
+		{
+			name:     "valid IPv4",
+			input:    "192.168.1.1",
+			expected: true,
+		},
+		{
+			name:     "invalid IPv4",
+			input:    "256.256.256.256",
+			expected: false,
+		},
+		{
+			name:     "IPv6 address",
+			input:    "2001:db8::1",
+			expected: false,
+		},
+		{
+			name:     "invalid format",
+			input:    "not.an.ip",
+			expected: false,
+		},
+		{
+			name:     "empty string",
+			input:    "",
+			expected: false,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			result := isIPv4String(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
diff --git a/source/gateway_httproute_test.go b/source/gateway_httproute_test.go
index 0308080b..a30bf0f3 100644
--- a/source/gateway_httproute_test.go
+++ b/source/gateway_httproute_test.go
@@ -28,6 +28,7 @@ import (
 	kubefake "k8s.io/client-go/kubernetes/fake"
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/internal/testutils"
+	"sigs.k8s.io/external-dns/source/annotations"
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 	v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 	gatewayfake "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/fake"
@@ -1138,8 +1139,8 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
 					Name:      "provider-annotations",
 					Namespace: "default",
 					Annotations: map[string]string{
-						SetIdentifierKey:   "test-set-identifier",
-						aliasAnnotationKey: "true",
+						annotations.SetIdentifierKey: "test-set-identifier",
+						aliasAnnotationKey:           "true",
 					},
 				},
 				Spec: v1.HTTPRouteSpec{
diff --git a/source/node_test.go b/source/node_test.go
index ee400d4d..ddfef776 100644
--- a/source/node_test.go
+++ b/source/node_test.go
@@ -66,6 +66,11 @@ func testNodeSourceNewNodeSource(t *testing.T) {
 			expectError:  false,
 			fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
 		},
+		{
+			title:        "complex template",
+			expectError:  false,
+			fqdnTemplate: "{{range .Status.Addresses}}{{if and (eq .Type \"ExternalIP\") (isIPv4 .Address)}}{{.Address | replace \".\" \"-\"}}{{break}}{{end}}{{end}}.ext-dns.test.com",
+		},
 		{
 			title:            "non-empty annotation filter label",
 			expectError:      false,
diff --git a/source/service_test.go b/source/service_test.go
index 4b199916..1eaebfe5 100644
--- a/source/service_test.go
+++ b/source/service_test.go
@@ -33,6 +33,7 @@ import (
 
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/internal/testutils"
+	"sigs.k8s.io/external-dns/source/annotations"
 )
 
 type ServiceSuite struct {
@@ -81,6 +82,7 @@ func (suite *ServiceSuite) SetupTest() {
 		labels.Everything(),
 		false,
 		false,
+		false,
 	)
 	suite.NoError(err, "should initialize service source")
 }
@@ -163,6 +165,7 @@ func testServiceSourceNewServiceSource(t *testing.T) {
 				labels.Everything(),
 				false,
 				false,
+				false,
 			)
 
 			if ti.expectError {
@@ -1135,6 +1138,7 @@ func testServiceSourceEndpoints(t *testing.T) {
 				sourceLabel,
 				tc.resolveLoadBalancerHostname,
 				false,
+				false,
 			)
 
 			require.NoError(t, err)
@@ -1264,8 +1268,8 @@ func testMultipleServicesEndpoints(t *testing.T) {
 			map[string]string{},
 			"",
 			map[string]map[string]string{
-				"1.2.3.5":  {hostnameAnnotationKey: "foo.example.org", SetIdentifierKey: "a"},
-				"10.1.1.3": {hostnameAnnotationKey: "foo.example.org", SetIdentifierKey: "b"},
+				"1.2.3.5":  {hostnameAnnotationKey: "foo.example.org", annotations.SetIdentifierKey: "a"},
+				"10.1.1.3": {hostnameAnnotationKey: "foo.example.org", annotations.SetIdentifierKey: "b"},
 			},
 			[]string{},
 			[]*endpoint.Endpoint{
@@ -1350,6 +1354,7 @@ func testMultipleServicesEndpoints(t *testing.T) {
 				labels.Everything(),
 				false,
 				false,
+				false,
 			)
 			require.NoError(t, err)
 
@@ -1654,6 +1659,7 @@ func TestClusterIpServices(t *testing.T) {
 				labelSelector,
 				false,
 				false,
+				false,
 			)
 			require.NoError(t, err)
 
@@ -1685,6 +1691,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 		compatibility            string
 		fqdnTemplate             string
 		ignoreHostnameAnnotation bool
+		exposeInternalIPv6       bool
 		labels                   map[string]string
 		annotations              map[string]string
 		lbs                      []string
@@ -2206,12 +2213,13 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 			}},
 		},
 		{
-			title:            "node port services annotated with external DNS Controller annotations return an endpoint in compatibility mode",
-			svcNamespace:     "testing",
-			svcName:          "foo",
-			svcType:          v1.ServiceTypeNodePort,
-			svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
-			compatibility:    "kops-dns-controller",
+			title:              "node port services annotated with external DNS Controller annotations return an endpoint in compatibility mode with exposeInternalIPv6 flag set",
+			svcNamespace:       "testing",
+			svcName:            "foo",
+			svcType:            v1.ServiceTypeNodePort,
+			svcTrafficPolicy:   v1.ServiceExternalTrafficPolicyTypeCluster,
+			compatibility:      "kops-dns-controller",
+			exposeInternalIPv6: true,
 			annotations: map[string]string{
 				kopsDNSControllerHostnameAnnotationKey: "foo.example.org., bar.example.org",
 			},
@@ -2373,6 +2381,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
 				labels.Everything(),
 				false,
 				false,
+				tc.exposeInternalIPv6,
 			)
 			require.NoError(t, err)
 
@@ -2402,6 +2411,7 @@ func TestHeadlessServices(t *testing.T) {
 		compatibility            string
 		fqdnTemplate             string
 		ignoreHostnameAnnotation bool
+		exposeInternalIPv6       bool
 		labels                   map[string]string
 		svcAnnotations           map[string]string
 		podAnnotations           map[string]string
@@ -2427,6 +2437,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2460,6 +2471,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2493,6 +2505,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			true,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2522,6 +2535,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2556,6 +2570,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2590,6 +2605,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2622,6 +2638,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2655,6 +2672,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2686,6 +2704,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2717,6 +2736,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2748,6 +2768,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2781,6 +2802,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey: "service.example.org",
@@ -2814,6 +2836,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey:      "service.example.org",
@@ -2849,7 +2872,7 @@ func TestHeadlessServices(t *testing.T) {
 			false,
 		},
 		{
-			"annotated Headless services return IPv6 targets from node external IP if endpoints-type annotation is set",
+			"annotated Headless services return only external IPv6 targets from node IP if endpoints-type annotation is set and exposeInternalIPv6 flag is unset",
 			"",
 			"testing",
 			"foo",
@@ -2857,6 +2880,55 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
+			map[string]string{"component": "foo"},
+			map[string]string{
+				hostnameAnnotationKey:      "service.example.org",
+				endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP,
+			},
+			map[string]string{},
+			v1.ClusterIPNone,
+			[]string{"2001:db8::1"},
+			[]string{""},
+			map[string]string{
+				"component": "foo",
+			},
+			[]string{},
+			[]string{"foo"},
+			[]string{"", "", ""},
+			[]bool{true, true, true},
+			false,
+			[]v1.Node{
+				{
+					Status: v1.NodeStatus{
+						Addresses: []v1.NodeAddress{
+							{
+								Type:    v1.NodeInternalIP,
+								Address: "2001:db8::4",
+							},
+							{
+								Type:    v1.NodeExternalIP,
+								Address: "2001:db8::5",
+							},
+						},
+					},
+				},
+			},
+			[]*endpoint.Endpoint{
+				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::5"}},
+			},
+			false,
+		},
+		{
+			"annotated Headless services return IPv6 targets from node external IP if endpoints-type annotation is set and exposeInternalIPv6 flag set",
+			"",
+			"testing",
+			"foo",
+			v1.ServiceTypeClusterIP,
+			"",
+			"",
+			false,
+			true,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey:      "service.example.org",
@@ -2892,7 +2964,7 @@ func TestHeadlessServices(t *testing.T) {
 			false,
 		},
 		{
-			"annotated Headless services return dual-stack targets from node external IP if endpoints-type annotation is set",
+			"annotated Headless services return dual-stack targets from node external IP if endpoints-type annotation is set and exposeInternalIPv6 flag set",
 			"",
 			"testing",
 			"foo",
@@ -2900,6 +2972,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			true,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey:      "service.example.org",
@@ -2948,6 +3021,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey:      "service.example.org",
@@ -2980,6 +3054,7 @@ func TestHeadlessServices(t *testing.T) {
 			"",
 			"",
 			false,
+			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
 				hostnameAnnotationKey:      "service.example.org",
@@ -3102,6 +3177,7 @@ func TestHeadlessServices(t *testing.T) {
 				labels.Everything(),
 				false,
 				false,
+				tc.exposeInternalIPv6,
 			)
 			require.NoError(t, err)
 
@@ -3562,6 +3638,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
 				labels.Everything(),
 				false,
 				false,
+				false,
 			)
 			require.NoError(t, err)
 
@@ -3741,6 +3818,7 @@ func TestExternalServices(t *testing.T) {
 				labels.Everything(),
 				false,
 				false,
+				false,
 			)
 			require.NoError(t, err)
 
@@ -3797,6 +3875,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
 		labels.Everything(),
 		false,
 		false,
+		false,
 	)
 	require.NoError(b, err)
 
diff --git a/source/source_test.go b/source/source_test.go
index 413995a6..fcc73574 100644
--- a/source/source_test.go
+++ b/source/source_test.go
@@ -17,339 +17,57 @@ limitations under the License.
 package source
 
 import (
-	"fmt"
-	"strconv"
+	"context"
+	"reflect"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
-
-	"sigs.k8s.io/external-dns/endpoint"
 )
 
-func TestGetTTLFromAnnotations(t *testing.T) {
-	for _, tc := range []struct {
-		title       string
-		annotations map[string]string
-		expectedTTL endpoint.TTL
-	}{
-		{
-			title:       "TTL annotation not present",
-			annotations: map[string]string{"foo": "bar"},
-			expectedTTL: endpoint.TTL(0),
-		},
-		{
-			title:       "TTL annotation value is not a number",
-			annotations: map[string]string{ttlAnnotationKey: "foo"},
-			expectedTTL: endpoint.TTL(0),
-		},
-		{
-			title:       "TTL annotation value is empty",
-			annotations: map[string]string{ttlAnnotationKey: ""},
-			expectedTTL: endpoint.TTL(0),
-		},
-		{
-			title:       "TTL annotation value is negative number",
-			annotations: map[string]string{ttlAnnotationKey: "-1"},
-			expectedTTL: endpoint.TTL(0),
-		},
-		{
-			title:       "TTL annotation value is too high",
-			annotations: map[string]string{ttlAnnotationKey: fmt.Sprintf("%d", 1<<32)},
-			expectedTTL: endpoint.TTL(0),
-		},
-		{
-			title:       "TTL annotation value is set correctly using integer",
-			annotations: map[string]string{ttlAnnotationKey: "60"},
-			expectedTTL: endpoint.TTL(60),
-		},
-		{
-			title:       "TTL annotation value is set correctly using duration (whole)",
-			annotations: map[string]string{ttlAnnotationKey: "10m"},
-			expectedTTL: endpoint.TTL(600),
-		},
-		{
-			title:       "TTL annotation value is set correctly using duration (fractional)",
-			annotations: map[string]string{ttlAnnotationKey: "20.5s"},
-			expectedTTL: endpoint.TTL(20),
-		},
-	} {
-		t.Run(tc.title, func(t *testing.T) {
-			ttl := getTTLFromAnnotations(tc.annotations, "resource/test")
-			assert.Equal(t, tc.expectedTTL, ttl)
-		})
-	}
+type mockInformerFactory struct {
+	syncResults map[reflect.Type]bool
 }
 
-func TestSuitableType(t *testing.T) {
-	for _, tc := range []struct {
-		target, recordType, expected string
-	}{
-		{"8.8.8.8", "", "A"},
-		{"2001:db8::1", "", "AAAA"},
-		{"::ffff:c0a8:101", "", "AAAA"},
-		{"foo.example.org", "", "CNAME"},
-		{"bar.eu-central-1.elb.amazonaws.com", "", "CNAME"},
-	} {
-
-		recordType := suitableType(tc.target)
-
-		if recordType != tc.expected {
-			t.Errorf("expected %s, got %s", tc.expected, recordType)
-		}
-	}
+func (m *mockInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
+	return m.syncResults
 }
 
-func TestGetProviderSpecificCloudflareAnnotations(t *testing.T) {
-	for _, tc := range []struct {
-		title         string
-		annotations   map[string]string
-		expectedKey   string
-		expectedValue bool
-	}{
-		{
-			title:         "Cloudflare proxied annotation is set correctly to true",
-			annotations:   map[string]string{CloudflareProxiedKey: "true"},
-			expectedKey:   CloudflareProxiedKey,
-			expectedValue: true,
-		},
-		{
-			title:         "Cloudflare proxied annotation is set correctly to false",
-			annotations:   map[string]string{CloudflareProxiedKey: "false"},
-			expectedKey:   CloudflareProxiedKey,
-			expectedValue: false,
-		},
-		{
-			title: "Cloudflare proxied annotation among another annotations is set correctly to true",
-			annotations: map[string]string{
-				"random annotation 1": "random value 1",
-				CloudflareProxiedKey:  "false",
-				"random annotation 2": "random value 2",
-			},
-			expectedKey:   CloudflareProxiedKey,
-			expectedValue: false,
-		},
-	} {
-		t.Run(tc.title, func(t *testing.T) {
-			providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
-			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
-				if providerSpecificAnnotation.Name == tc.expectedKey {
-					assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value)
-					return
-				}
-			}
-			t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue)
-		})
-	}
-
-	for _, tc := range []struct {
-		title         string
-		annotations   map[string]string
-		expectedKey   string
-		expectedValue string
-	}{
-		{
-			title:         "Cloudflare custom hostname annotation is set correctly",
-			annotations:   map[string]string{CloudflareCustomHostnameKey: "a.foo.fancybar.com"},
-			expectedKey:   CloudflareCustomHostnameKey,
-			expectedValue: "a.foo.fancybar.com",
-		},
-		{
-			title: "Cloudflare custom hostname annotation among another annotations is set correctly",
-			annotations: map[string]string{
-				"random annotation 1":       "random value 1",
-				CloudflareCustomHostnameKey: "a.foo.fancybar.com",
-				"random annotation 2":       "random value 2"},
-			expectedKey:   CloudflareCustomHostnameKey,
-			expectedValue: "a.foo.fancybar.com",
-		},
-	} {
-		t.Run(tc.title, func(t *testing.T) {
-			providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
-			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
-				if providerSpecificAnnotation.Name == tc.expectedKey {
-					assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value)
-					return
-				}
-			}
-			t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %s", tc.expectedKey, tc.expectedValue)
-		})
-	}
-
-	for _, tc := range []struct {
-		title         string
-		annotations   map[string]string
-		expectedKey   string
-		expectedValue string
+func TestWaitForCacheSync(t *testing.T) {
+	tests := []struct {
+		name        string
+		syncResults map[reflect.Type]bool
+		expectError bool
 	}{
 		{
-			title:         "Cloudflare region key annotation is set correctly",
-			annotations:   map[string]string{CloudflareRegionKey: "us"},
-			expectedKey:   CloudflareRegionKey,
-			expectedValue: "us",
-		},
-		{
-			title: "Cloudflare region key annotation among another annotations is set correctly",
-			annotations: map[string]string{
-				"random annotation 1": "random value 1",
-				CloudflareRegionKey:   "us",
-				"random annotation 2": "random value 2",
-			},
-			expectedKey:   CloudflareRegionKey,
-			expectedValue: "us",
+			name:        "all caches synced",
+			syncResults: map[reflect.Type]bool{reflect.TypeOf(""): true},
+			expectError: false,
 		},
-	} {
-		t.Run(tc.title, func(t *testing.T) {
-			providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
-			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
-				if providerSpecificAnnotation.Name == tc.expectedKey {
-					assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value)
-					return
-				}
-			}
-			t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue)
-		})
-	}
-}
-
-func TestGetProviderSpecificAliasAnnotations(t *testing.T) {
-	for _, tc := range []struct {
-		title         string
-		annotations   map[string]string
-		expectedKey   string
-		expectedValue bool
-	}{
 		{
-			title:         "alias annotation is set correctly to true",
-			annotations:   map[string]string{aliasAnnotationKey: "true"},
-			expectedKey:   aliasAnnotationKey,
-			expectedValue: true,
+			name:        "some caches not synced",
+			syncResults: map[reflect.Type]bool{reflect.TypeOf(""): false},
+			expectError: true,
 		},
 		{
-			title: "alias annotation among another annotations is set correctly to true",
-			annotations: map[string]string{
-				"random annotation 1": "random value 1",
-				aliasAnnotationKey:    "true",
-				"random annotation 2": "random value 2",
-			},
-			expectedKey:   aliasAnnotationKey,
-			expectedValue: true,
+			name:        "context timeout",
+			syncResults: map[reflect.Type]bool{reflect.TypeOf(""): false},
+			expectError: true,
 		},
-	} {
-		t.Run(tc.title, func(t *testing.T) {
-			providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
-			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
-				if providerSpecificAnnotation.Name == "alias" {
-					assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value)
-					return
-				}
-			}
-			t.Errorf("provider specific annotation alias is not set correctly to %v", tc.expectedValue)
-		})
 	}
 
-	for _, tc := range []struct {
-		title       string
-		annotations map[string]string
-	}{
-		{
-			title:       "alias annotation is set to false",
-			annotations: map[string]string{aliasAnnotationKey: "false"},
-		},
-		{
-			title: "alias annotation is not set",
-			annotations: map[string]string{
-				"random annotation 1": "random value 1",
-				"random annotation 2": "random value 2",
-			},
-		},
-	} {
-		t.Run(tc.title, func(t *testing.T) {
-			providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
-			for _, providerSpecificAnnotation := range providerSpecificAnnotations {
-				if providerSpecificAnnotation.Name == "alias" {
-					t.Error("provider specific annotation alias is not expected to be set")
-				}
-			}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+			defer cancel()
 
-		})
-	}
-}
+			factory := &mockInformerFactory{syncResults: tt.syncResults}
+			err := waitForCacheSync(ctx, factory)
 
-func TestGetProviderSpecificIdentifierAnnotations(t *testing.T) {
-	for _, tc := range []struct {
-		title              string
-		annotations        map[string]string
-		expectedResult     map[string]string
-		expectedIdentifier string
-	}{
-		{
-			title: "aws- provider specific annotations are set correctly",
-			annotations: map[string]string{
-				"external-dns.alpha.kubernetes.io/aws-annotation-1": "value 1",
-				SetIdentifierKey: "id1",
-				"external-dns.alpha.kubernetes.io/aws-annotation-2": "value 2",
-			},
-			expectedResult: map[string]string{
-				"aws/annotation-1": "value 1",
-				"aws/annotation-2": "value 2",
-			},
-			expectedIdentifier: "id1",
-		},
-		{
-			title: "scw- provider specific annotations are set correctly",
-			annotations: map[string]string{
-				"external-dns.alpha.kubernetes.io/scw-annotation-1": "value 1",
-				SetIdentifierKey: "id1",
-				"external-dns.alpha.kubernetes.io/scw-annotation-2": "value 2",
-			},
-			expectedResult: map[string]string{
-				"scw/annotation-1": "value 1",
-				"scw/annotation-2": "value 2",
-			},
-			expectedIdentifier: "id1",
-		},
-		{
-			title: "ibmcloud- provider specific annotations are set correctly",
-			annotations: map[string]string{
-				"external-dns.alpha.kubernetes.io/ibmcloud-annotation-1": "value 1",
-				SetIdentifierKey: "id1",
-				"external-dns.alpha.kubernetes.io/ibmcloud-annotation-2": "value 2",
-			},
-			expectedResult: map[string]string{
-				"ibmcloud-annotation-1": "value 1",
-				"ibmcloud-annotation-2": "value 2",
-			},
-			expectedIdentifier: "id1",
-		},
-		{
-			title: "webhook- provider specific annotations are set correctly",
-			annotations: map[string]string{
-				"external-dns.alpha.kubernetes.io/webhook-annotation-1": "value 1",
-				SetIdentifierKey: "id1",
-				"external-dns.alpha.kubernetes.io/webhook-annotation-2": "value 2",
-			},
-			expectedResult: map[string]string{
-				"webhook/annotation-1": "value 1",
-				"webhook/annotation-2": "value 2",
-			},
-			expectedIdentifier: "id1",
-		},
-	} {
-		t.Run(tc.title, func(t *testing.T) {
-			providerSpecificAnnotations, identifier := getProviderSpecificAnnotations(tc.annotations)
-			assert.Equal(t, tc.expectedIdentifier, identifier)
-			for expectedAnnotationKey, expectedAnnotationValue := range tc.expectedResult {
-				expectedResultFound := false
-				for _, providerSpecificAnnotation := range providerSpecificAnnotations {
-					if providerSpecificAnnotation.Name == expectedAnnotationKey {
-						assert.Equal(t, expectedAnnotationValue, providerSpecificAnnotation.Value)
-						expectedResultFound = true
-						break
-					}
-				}
-				if !expectedResultFound {
-					t.Errorf("provider specific annotation %s has not been set", expectedAnnotationKey)
-				}
+			if tt.expectError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
 			}
 		})
 	}
diff --git a/source/traefik_proxy_test.go b/source/traefik_proxy_test.go
index 88cefb51..4cd1cec1 100644
--- a/source/traefik_proxy_test.go
+++ b/source/traefik_proxy_test.go
@@ -19,9 +19,12 @@ package source
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"testing"
 
+	"github.com/stretchr/testify/mock"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/tools/cache"
 
 	"github.com/stretchr/testify/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -1731,3 +1734,80 @@ func TestTraefikAPIGroupDisableFlags(t *testing.T) {
 		})
 	}
 }
+
+func TestAddEventHandler_AllBranches(t *testing.T) {
+	ctx := context.Background()
+	handlerCalled := false
+	handler := func() { handlerCalled = true }
+
+	inf := testInformer{}
+	fakeInformer := new(FakeInformer)
+	fakeInformer.On("Informer").Return(&inf)
+
+	cases := []struct {
+		name string
+		ts   *traefikSource
+		want int
+	}{
+		{"all nil", &traefikSource{}, 0},
+		{"all set", &traefikSource{
+			ingressRouteInformer:       fakeInformer,
+			oldIngressRouteInformer:    fakeInformer,
+			ingressRouteTcpInformer:    fakeInformer,
+			oldIngressRouteTcpInformer: fakeInformer,
+			ingressRouteUdpInformer:    fakeInformer,
+			oldIngressRouteUdpInformer: fakeInformer,
+		}, 6},
+		{"some set", &traefikSource{
+			ingressRouteInformer:       fakeInformer,
+			oldIngressRouteInformer:    fakeInformer,
+			ingressRouteTcpInformer:    nil,
+			oldIngressRouteTcpInformer: fakeInformer,
+			ingressRouteUdpInformer:    nil,
+			oldIngressRouteUdpInformer: nil,
+		}, 3},
+	}
+
+	for _, test := range cases {
+		t.Run(test.name, func(t *testing.T) {
+			test.ts.AddEventHandler(ctx, handler)
+			assert.Equal(t, test.want, inf.times)
+			assert.False(t, handlerCalled)
+
+			if test.want > 0 {
+				fakeInformer.AssertExpectations(t)
+				fakeInformer.AssertCalled(t, "Informer")
+			} else {
+				fakeInformer.AssertNotCalled(t, "Informer")
+			}
+			// reset the call count
+			inf.times = 0
+		})
+	}
+}
+
+type FakeInformer struct {
+	mock.Mock
+	informer cache.SharedIndexInformer
+	lister   cache.GenericLister
+}
+
+func (f *FakeInformer) Informer() cache.SharedIndexInformer {
+	args := f.Called()
+	return args.Get(0).(cache.SharedIndexInformer)
+}
+
+func (f *FakeInformer) Lister() cache.GenericLister {
+	return f.lister
+}
+
+type testInformer struct {
+	cache.SharedIndexInformer
+
+	times int
+}
+
+func (t *testInformer) AddEventHandler(_ cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) {
+	t.times += 1
+	return nil, fmt.Errorf("not implemented")
+}
diff --git a/source/utils_test.go b/source/utils_test.go
new file mode 100644
index 00000000..ba6447c2
--- /dev/null
+++ b/source/utils_test.go
@@ -0,0 +1,151 @@
+/*
+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 source
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"sigs.k8s.io/external-dns/endpoint"
+)
+
+func TestSuitableType(t *testing.T) {
+	tests := []struct {
+		name     string
+		target   string
+		expected string
+	}{
+		{
+			name:     "valid IPv4 address",
+			target:   "192.168.1.1",
+			expected: endpoint.RecordTypeA,
+		},
+		{
+			name:     "valid IPv6 address",
+			target:   "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+			expected: endpoint.RecordTypeAAAA,
+		},
+		{
+			name:     "invalid IP address, should return CNAME",
+			target:   "example.com",
+			expected: endpoint.RecordTypeCNAME,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := suitableType(tt.target)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestParseIngress(t *testing.T) {
+	tests := []struct {
+		name      string
+		ingress   string
+		wantNS    string
+		wantName  string
+		wantError bool
+	}{
+		{
+			name:      "valid namespace and name",
+			ingress:   "default/test-ingress",
+			wantNS:    "default",
+			wantName:  "test-ingress",
+			wantError: false,
+		},
+		{
+			name:      "only name provided",
+			ingress:   "test-ingress",
+			wantNS:    "",
+			wantName:  "test-ingress",
+			wantError: false,
+		},
+		{
+			name:      "invalid format",
+			ingress:   "default/test/ingress",
+			wantNS:    "",
+			wantName:  "",
+			wantError: true,
+		},
+		{
+			name:      "empty string",
+			ingress:   "",
+			wantNS:    "",
+			wantName:  "",
+			wantError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotNS, gotName, err := ParseIngress(tt.ingress)
+			if tt.wantError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tt.wantNS, gotNS)
+			assert.Equal(t, tt.wantName, gotName)
+		})
+	}
+}
+
+func TestSelectorMatchesService(t *testing.T) {
+	tests := []struct {
+		name        string
+		selector    map[string]string
+		svcSelector map[string]string
+		expected    bool
+	}{
+		{
+			name:        "all key-value pairs match",
+			selector:    map[string]string{"app": "nginx", "env": "prod"},
+			svcSelector: map[string]string{"app": "nginx", "env": "prod"},
+			expected:    true,
+		},
+		{
+			name:        "one key-value pair does not match",
+			selector:    map[string]string{"app": "nginx", "env": "prod"},
+			svcSelector: map[string]string{"app": "nginx", "env": "dev"},
+			expected:    false,
+		},
+		{
+			name:        "key not present in svcSelector",
+			selector:    map[string]string{"app": "nginx", "env": "prod"},
+			svcSelector: map[string]string{"app": "nginx"},
+			expected:    false,
+		},
+		{
+			name:        "empty selector",
+			selector:    map[string]string{},
+			svcSelector: map[string]string{"app": "nginx", "env": "prod"},
+			expected:    true,
+		},
+		{
+			name:        "empty svcSelector",
+			selector:    map[string]string{"app": "nginx", "env": "prod"},
+			svcSelector: map[string]string{},
+			expected:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := MatchesServiceSelector(tt.selector, tt.svcSelector)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}