Repository: kubernetes-sigs/external-dns

Issue:
new pi-hole server v6 results in 404 error
<!-- 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**: external-dns started reporting a 404 error.
```
time="2025-02-21T11:11:49Z" level=debug msg="Fetching new token from https://<pi-hole-server-address>/admin/index.php?login"
time="2025-02-21T11:11:49Z" level=fatal msg="received non-200 status code from request: 404 Not Found"
```

**What you expected to happen**: 
A non-error

**How to reproduce it (as minimally and precisely as possible)**:
Use pi-hole docker image `2025.02.0` or higher

**Anything else we need to know?**:
Started happening since (I think) my pi-hole server got updated to docker tag `2025.02.0`. [Which now is based on the new pi-hole server v6](https://github.com/pi-hole/docker-pi-hole/releases/tag/2025.02.0).

It appears that pi-hole now uses the endpoint `/api/auth` for authentication instead: https://docs.pi-hole.net/api/auth/

**Environment**:

- External-DNS version (use `external-dns --version`): helm chart `1.15.2` reports image `registry.k8s.io/external-dns/external-dns:v0.15.1`
- DNS provider: pi-hole
- Others: pi-hole docker image `2025.02.3`


Hints from issue comments:
@ivankatliarchuk: Hi. 

We recently had quite few pi-hole related issues. This is open-source project If you have time would you mind update a [dev guide](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/pihole.md) how to configure lab with external-dns and pi-hole? I think the actual how to install pi-hole is missing.

https://github.com/kubernetes-sigs/external-dns/issues/5044#issuecomment-2661574576@ivankatliarchuk: /help@k8s-ci-robot: @ivankatliarchuk: 
	This request has been marked as needing help from a contributor.

### Guidelines
Please ensure that the issue body includes answers to the following questions:
- Why are we solving this issue?
- To address this issue, are there any code changes? If there are code changes, what needs to be done in the code and what places can the assignee treat as reference points?
- Does this issue have zero to low barrier of entry?
- How can the assignee reach out to you for help?


For more details on the requirements of such an issue, please see [here](https://git.k8s.io/community/contributors/guide/help-wanted.md) and ensure that they are met.

If this request no longer meets these requirements, the label can be removed
by commenting with the `/remove-help` command.


<details>

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

>/help


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>@thebigbone: are there any current workarounds to it?@xorinzor: @thebigbone sticking with pi-hole v5 until `external-dns` is updated to work with pi-hole v6.@ivankatliarchuk: It's an open source project. So someone could submit a pull request. My understanding it should support old and new pi-hole versions at least for a while.@thebigbone: > It's an open source project. So someone could submit a pull request. My understanding it should support old and new pi-hole versions at least for a while.

you're right. I am no expert in k8s but I will still look into it and hopefully atleast figure out what's wrong.@masterkain: well what's wrong is that pihole 6 changed api endpoints so we need to update the integration https://github.com/kubernetes-sigs/external-dns/blob/master/provider/pihole/client.go#L278
I have some busy weeks ahead but I might try updating some stuff@aztechian: Just adding some info here to save others some research time:
https://discourse.pi-hole.net/t/scope-of-the-v6-api-includes-dns-cname-crud/73373/8

It's unfortunately more than just the auth part. The old PHP scripts that were getting called are (likely) no longer available either. There is now a REST API, but it's a significant rework of the current pihole provider.

For any maintainers, I'm curious how you might tackle this:
1. wrap determining version in the current pihole provider and it supports both v5 and v6
2. create a new v6 provider and users need to choose it if they run pihole v6

I can confirm that testing CURL commands on my v6 install (I naively upgraded recently 😢) did work for adding/deleting A records with something like:

GET current list
```
curl -H "X-FTL-SID: xxxxxxxxxxx" http://pi.hole/api/config/dns/hosts | jq
```

Add A record
```
curl -H "X-FTL-SID: xxxxxxxxxxx" -X PUT http://pi.hole/api/config/dns/hosts/192.168.254.0%20testing.example.com
```

Delete A Record
```
curl -H "X-FTL-SID: xxxxxxxxxxx" -X DELETE http://pi.hole/api/config/dns/hosts/192.168.254.0%20testing.example.com
```

> [!NOTE]
> Note, that it needs the full IP address + hostname combination for DELETE. In other words 
> ```
> curl -H "X-FTL-SID: xxxxxxxxxxx" -X DELETE http://pi.hole/api/config/dns/hosts/testing.example.com
> ```
> won't work.


On the plus side, the REST+JSON api lends itself nicely to go's structs and parsing, so that would be cleaner in v6. And, the API docs are self-hosted by the pihole at http://pi.hole/api/docs @tJouve: Hello, I can start working on this one

WIP => https://github.com/tJouve/external-dns/tree/feat/piholev6
@ivankatliarchuk: There is no specific endpiont to find out the pi-hole version if aunautrhosed. One could do 

First probe to validate where or not auth is required and is v6
```
GET /api/auth/
```

If not found, it's an older version. Do
```
/admin/index.php?login
```@ivankatliarchuk: Probably no point to support V5 and V6, as V5 is supported on older versions of external-dns@tJouve: @ivankatliarchuk so you think i can drop support for the previous version ? My plan was to add a config to specify the api version, with default 5. If 6 is specified I use the V6 client.@ivankatliarchuk: Is quite expensive to support both versions aka bee backward compatible. Having a flag for that, sounds reasonable as well. There is another [option to create a pi-hole webhook](https://github.com/kubernetes-sigs/external-dns/issues/4347) but not sure if someone would trust that provider in home network.

I let you to decide.



@xorinzor: Using a configurable flag for now seems like the better option IMO. Things might change at the kubernetes side whereas v5 will stay working the same way, in which case users would be forced to upgrade.

You could mark v5 as deprecated and remove it in a later version of external-dns, but for now the better path is probably to use a configurable api version, defaulting to `v5`.@divStar: Is there something I can do to actively help you @tJouve ? I have a bit of spare time and I am a bit familiar with Go (though by no means a lot). I am also in the process of setting up my homelab cluster and would love for it to communicate with my PiHole, which turned out to be V6.

If there's something I can do (review, implement something, test), let me know.@tJouve: @divStar  Sure, I can add you on my fork. 
Task list : 

- [ ] Unit test
- [ ] Review
- [ ] Finish implementation
- [ ] Refactoring
- [ ] Integration testing
- [ ] Documentation

@divStar: Sorry, the invitation didn't seem to go through:

<img width="961" alt="Image" src="https://github.com/user-attachments/assets/20c9343f-5882-4cd7-bad7-6120d9d6e99f" />

I started working on it though so no hurry. If for whatever reason the invitation does not work, I can open a pull request your way anyway if need be (though it'd require more Git juggling).@QuadmanSWE: I would love to test out any WIP version that has v6 support.@crutonjohn: just wanting to confirm with any maintainers as to what the preferred approach is. I didn't really see any confirmation one way or the other w/r/t keeping this provider in-tree vs going with a webhook provider. If there is some tangible amount of effort that needs to be done in order to support both v5 and v6, would it be of any advantage to instead convert this into a webhook for v6? this could be considered a decent opportunity to move this out-of-tree, gives a clear jumping off point to adopt a webhook for v6, and allows for deprecation of the in-tree provider.

pardon if I missed the context for this somewhere. I just don't want people to spend cycles on in-tree if the desire isnt there. @ivankatliarchuk: Shifting away from the provider is likely a strategic long-term goal. However, practically speaking, we should prioritize fixing existing issues before making that transition.

Personally, I'm advocating for moving Pi-hole from Alpha to Beta, especially for home network, there are no providers with beta stage. For that, the webhook provider needs to be extremely reliable and secure. I have concerns about achieving this with a webhook, given the potential for sources from less reputable third parties

At the moment we have zero in-tree providers that true open source https://github.com/kubernetes-sigs/external-dns?tab=readme-ov-file#status-of-in-tree-providers

Related discussion/issue https://github.com/kubernetes-sigs/external-dns/issues/4347@divStar: (A user's perspective)
Perhaps once the process is clear on how to have 3rd-party providers, where they come from, where their source code is and how they interact with external-dns itself, we should separate the providers and external-dns itself, but I'd say it shouldn't be part of this v5 to V6 switch.

I already tested the implementation for PiHole V6 and while it might not be perfectly tested, it runs fine in my setup.

The refactoring of providers should be a separate issue once it becomes mandatory in my humble opinion.@Jeansen: @tJouve I tried your fork. Looks good to me, so far. Great work!@jkossis: @tJouve any chance you're considering putting up a PR with your work? Could be a really useful contribution for the upstream.@ivankatliarchuk: Hey. May I ask to review and test the pull request https://github.com/kubernetes-sigs/external-dns/pull/5226@tradenet: > Hey. May I ask to review and test the pull request [#5226](https://github.com/kubernetes-sigs/external-dns/pull/5226)

It would be nice. Please and thank you.

Solution patch:
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index d9d65969..6bdcae2f 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -9,8 +9,12 @@ jobs:
     name: Markdown, Go and OAS
     runs-on: ubuntu-latest
     permissions:
-      contents: read  #  to fetch code (actions/checkout)
+      # Required: allow read access to the content for analysis.
+      contents: read
+      # For OAS check
       checks: write
+      # For go lang linter
+      pull-requests: read
     steps:
 
     - name: Check out code into the Go module directory
@@ -40,11 +44,11 @@ jobs:
 
     # https://github.com/golangci/golangci-lint-action?tab=readme-ov-file#verify
     - name: Verify linter configuration and Lint go code
-      uses: golangci/golangci-lint-action@v6
+      uses: golangci/golangci-lint-action@v7
       with:
         verify: true
         args: --timeout=30m
-        version: v1.64
+        version: v2.0
 
       # Run Spectral
     - name: Lint OpenAPI spec
diff --git a/.golangci.yml b/.golangci.yml
index 69572734..99b3dde0 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,69 +1,70 @@
-linters-settings:
-  exhaustive:
-    default-signifies-exhaustive: false
-  goimports:
-    local-prefixes: sigs.k8s.io/external-dns
-  misspell:
-    locale: US
-  revive:
-    ignore-generated-header: true
-    rules:
-      - name: confusing-naming
-        disabled: true
-
+version: "2"
 linters:
-  # please, do not use `enable-all`: it's deprecated and will be removed soon.
-  # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
-  disable-all: true
+  default: none
   enable:
     - dogsled
-    - gofmt
-    - goimports
     - goprintffuncname
     - govet
     - ineffassign
     - misspell
+    - revive
     - rowserrcheck
-    - typecheck
+    - staticcheck
     - unconvert
-    - whitespace
-    - revive
     - unused
-    - gosimple
-    - staticcheck
-
-issues:
-  # Excluding configuration per-path, per-linter, per-text and per-source
-  exclude-files:
-    - endpoint/zz_generated.deepcopy.go
-  exclude-rules:
-    - path: _test\.go
-      linters:
-        - deadcode
-        - depguard
-        - dogsled
-        - gofmt
-        - goimports
-        - goprintffuncname
-        - gosimple
-        - govet
-        - ineffassign
-        - misspell
-        - nolintlint
-        - rowserrcheck
-        - staticcheck
-        - structcheck
-        - stylecheck
-        - typecheck
-        - unconvert
-        - unused
-        - varcheck
-        - whitespace
-    - path: source/ambassador_host.go
-      linters: [ typecheck ]
-    - path: source/contour_httpproxy.go
-      linters: [ typecheck ]
-    - path: source/f5_virtualserver.go
-      linters: [ typecheck ]
-    - path: source/kong_tcpingress.go
-      linters: [ typecheck ]
+    - whitespace
+  settings:
+    exhaustive:
+      default-signifies-exhaustive: false
+    misspell:
+      locale: US
+    revive:
+      rules:
+        - name: confusing-naming
+          disabled: true
+  exclusions:
+    generated: lax
+    presets:
+      - comments
+      - common-false-positives
+      - legacy
+      - std-error-handling
+    rules:
+      - linters:
+          - deadcode
+          - depguard
+          - dogsled
+          - goprintffuncname
+          - govet
+          - ineffassign
+          - misspell
+          - nolintlint
+          - rowserrcheck
+          - staticcheck
+          - structcheck
+          - unconvert
+          - unused
+          - varcheck
+          - whitespace
+        path: _test\.go
+    paths:
+      - endpoint/zz_generated.deepcopy.go
+      - third_party$
+      - builtin$
+      - examples$
+formatters:
+  enable:
+    - gofmt
+    - goimports
+  settings:
+    goimports:
+      local-prefixes:
+        - sigs.k8s.io/external-dns
+  exclusions:
+    generated: lax
+    paths:
+      - endpoint/zz_generated.deepcopy.go
+      - third_party$
+      - builtin$
+      - examples$
+      - _test\.go
diff --git a/Makefile b/Makefile
index 9b2a4a6c..8b0bd52f 100644
--- a/Makefile
+++ b/Makefile
@@ -37,7 +37,7 @@ endif
 
 #? golangci-lint: Install golangci-lint tool
 golangci-lint:
-	@command -v golangci-lint > /dev/null || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.5
+	@command -v golangci-lint > /dev/null || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.0.2
 
 #? golangci-lint-verify: Verify golangci-lint configuration
 golangci-lint-verify: golangci-lint
diff --git a/controller/execute.go b/controller/execute.go
index 4114cb61..4113fd64 100644
--- a/controller/execute.go
+++ b/controller/execute.go
@@ -301,6 +301,7 @@ func Execute() {
 				TLSInsecureSkipVerify: cfg.PiholeTLSInsecureSkipVerify,
 				DomainFilter:          domainFilter,
 				DryRun:                cfg.DryRun,
+				APIVersion:            cfg.PiholeApiVersion,
 			},
 		)
 	case "ibmcloud":
diff --git a/docs/flags.md b/docs/flags.md
index b5079aa9..055b9a0a 100644
--- a/docs/flags.md
+++ b/docs/flags.md
@@ -153,6 +153,7 @@
 | `--pihole-server=""` | When using the Pihole provider, the base URL of the Pihole web server (required when --provider=pihole) |
 | `--pihole-password=""` | When using the Pihole provider, the password to the server if it is protected |
 | `--[no-]pihole-tls-skip-verify` | When using the Pihole provider, disable verification of any TLS certificates |
+| `--pihole-api-version="5"` | When using the Pihole provider, specify the pihole API version (default: 5, options: 5, 6) |
 | `--plural-cluster=""` | When using the plural provider, specify the cluster name you're running with |
 | `--plural-provider=""` | When using the plural provider, specify the provider name you're running with |
 | `--policy=sync` | Modify how DNS records are synchronized between sources and providers (default: sync, options: sync, upsert-only, create-only) |
diff --git a/docs/tutorials/pihole.md b/docs/tutorials/pihole.md
index 5091b4d1..adf493e4 100644
--- a/docs/tutorials/pihole.md
+++ b/docs/tutorials/pihole.md
@@ -6,12 +6,16 @@ There is a pseudo-API exposed that ExternalDNS is able to use to manage these re
 
 __NOTE:__ Your Pi-hole must be running [version 5.9 or newer](https://pi-hole.net/blog/2022/02/12/pi-hole-ftl-v5-14-web-v5-11-and-core-v5-9-released).
 
+__NOTE:__ Provider for Pi-hole version prior to 6.0 is now deprecated and will be removed in future release.
+
+__NOTE:__ Since Pi-hole version 6, you should use the flag *--pihole-api-version=6*
+
 ## Deploy ExternalDNS
 
 You can skip to the [manifest](#externaldns-manifest) if authentication is disabled on your Pi-hole instance or you don't want to use secrets.
 
 If your Pi-hole server's admin dashboard is protected by a password, you'll likely want to create a secret first containing its value.
-This is optional since you _do_ retain the option to pass it as a flag with `--pihole-password`.
+This is optional since you *do* retain the option to pass it as a flag with `--pihole-password`.
 
 You can create the secret with:
 
@@ -98,6 +102,8 @@ spec:
         # the policy to upsert-only so they do not get deleted.
         - --policy=upsert-only
         - --provider=pihole
+        # Switch to pihole V6 API
+        - --pihole-api-version=6
         # Change this to the actual address of your Pi-hole web server
         - --pihole-server=http://pihole-web.pihole.svc.cluster.local
       securityContext:
@@ -109,6 +115,7 @@ spec:
 - `--pihole-server (env: EXTERNAL_DNS_PIHOLE_SERVER)` - The address of the Pi-hole web server
 - `--pihole-password (env: EXTERNAL_DNS_PIHOLE_PASSWORD)` - The password to the Pi-hole web server (if enabled)
 - `--pihole-tls-skip-verify (env: EXTERNAL_DNS_PIHOLE_TLS_SKIP_VERIFY)` - Skip verification of any TLS certificates served by the Pi-hole web server.
+- `--pihole-api-version (env: EXTERNAL_DNS_PIHOLE_API_VERSION)` - Specify the pihole API version (default is 5. Eligible values are 5 or 6).
 
 ## Verify ExternalDNS Works
 
@@ -181,7 +188,7 @@ spec:
 
 You can then query your Pi-hole to see if the record was created.
 
-_Change `@192.168.100.2` to the actual address of your DNS server_
+Change *@192.168.100.2* to the actual address of your DNS server
 
 ```bash
 $ dig +short @192.168.100.2  nginx.external-dns-test.homelab.com
diff --git a/internal/gen/docs/flags/main.go b/internal/gen/docs/flags/main.go
index 72238db7..dba89ee7 100644
--- a/internal/gen/docs/flags/main.go
+++ b/internal/gen/docs/flags/main.go
@@ -55,7 +55,7 @@ func main() {
 	flags := computeFlags()
 	content, err := flags.generateMarkdownTable()
 	if err != nil {
-		_ = fmt.Errorf("failed to generate markdown file '%s': %v\n", path, err.Error())
+		_ = fmt.Errorf("failed to generate markdown file '%s': %v", path, err.Error())
 	}
 	content = content + "\n"
 	_ = utils.WriteToFile(path, content)
diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go
index 88739d6a..d4f0601f 100644
--- a/pkg/apis/externaldns/types.go
+++ b/pkg/apis/externaldns/types.go
@@ -203,6 +203,7 @@ type Config struct {
 	PiholeServer                                  string
 	PiholePassword                                string `secure:"yes"`
 	PiholeTLSInsecureSkipVerify                   bool
+	PiholeApiVersion                              string
 	PluralCluster                                 string
 	PluralProvider                                string
 	WebhookProviderURL                            string
@@ -365,6 +366,7 @@ var defaultConfig = &Config{
 	PiholeServer:                                  "",
 	PiholePassword:                                "",
 	PiholeTLSInsecureSkipVerify:                   false,
+	PiholeApiVersion:                              "5",
 	PluralCluster:                                 "",
 	PluralProvider:                                "",
 	WebhookProviderURL:                            "http://localhost:8888",
@@ -600,6 +602,7 @@ func App(cfg *Config) *kingpin.Application {
 	app.Flag("pihole-server", "When using the Pihole provider, the base URL of the Pihole web server (required when --provider=pihole)").Default(defaultConfig.PiholeServer).StringVar(&cfg.PiholeServer)
 	app.Flag("pihole-password", "When using the Pihole provider, the password to the server if it is protected").Default(defaultConfig.PiholePassword).StringVar(&cfg.PiholePassword)
 	app.Flag("pihole-tls-skip-verify", "When using the Pihole provider, disable verification of any TLS certificates").BoolVar(&cfg.PiholeTLSInsecureSkipVerify)
+	app.Flag("pihole-api-version", "When using the Pihole provider, specify the pihole API version (default: 5, options: 5, 6)").Default(defaultConfig.PiholeApiVersion).StringVar(&cfg.PiholeApiVersion)
 
 	// Flags related to the Plural provider
 	app.Flag("plural-cluster", "When using the plural provider, specify the cluster name you're running with").Default(defaultConfig.PluralCluster).StringVar(&cfg.PluralCluster)
diff --git a/pkg/rfc2317/arpa.go b/pkg/rfc2317/arpa.go
index bd7cc88e..f11bac10 100644
--- a/pkg/rfc2317/arpa.go
+++ b/pkg/rfc2317/arpa.go
@@ -72,17 +72,18 @@ func CidrToInAddr(cidr string) (string, error) {
 	}
 
 	// Handle IPv4 Class-full and IPv6:
-	if total == 32 {
+	switch total {
+	case 32:
 		if bits%8 != 0 {
 			return "", fmt.Errorf("IPv4 mask must be multiple of 8 bits")
 		}
 		toTrim = (total - bits) / 8
-	} else if total == 128 {
+	case 128:
 		if bits%4 != 0 {
 			return "", fmt.Errorf("IPv6 mask must be multiple of 4 bits")
 		}
 		toTrim = (total - bits) / 4
-	} else {
+	default:
 		return "", fmt.Errorf("invalid address (not IPv4 or IPv6): %v", cidr)
 	}
 
diff --git a/provider/akamai/akamai.go b/provider/akamai/akamai.go
index 290b423a..bd36fd66 100644
--- a/provider/akamai/akamai.go
+++ b/provider/akamai/akamai.go
@@ -321,11 +321,12 @@ func newAkamaiRecordset(dnsName, recordType string, ttl int, targets []string) d
 // cleanTargets preps recordset rdata if necessary for EdgeDNS
 func cleanTargets(rtype string, targets ...string) []string {
 	log.Debugf("Targets to clean: [%v]", targets)
-	if rtype == "CNAME" || rtype == "SRV" {
+	switch rtype {
+	case "CNAME", "SRV":
 		for idx, target := range targets {
 			targets[idx] = strings.TrimSuffix(target, ".")
 		}
-	} else if rtype == "TXT" {
+	case "TXT":
 		for idx, target := range targets {
 			log.Debugf("TXT data to clean: [%s]", target)
 			// need to embed text data in quotes. Make sure not piling on
@@ -359,7 +360,7 @@ func trimTxtRdata(rdata []string, rtype string) []string {
 func ttlAsInt(src endpoint.TTL) int {
 	var temp interface{} = int64(src)
 	temp64 := temp.(int64)
-	var ttl int = edgeDNSRecordTTL
+	var ttl = edgeDNSRecordTTL
 	if temp64 > 0 && temp64 <= int64(maxInt) {
 		ttl = int(temp64)
 	}
diff --git a/provider/alibabacloud/alibaba_cloud.go b/provider/alibabacloud/alibaba_cloud.go
index b89b8354..5183d443 100644
--- a/provider/alibabacloud/alibaba_cloud.go
+++ b/provider/alibabacloud/alibaba_cloud.go
@@ -492,7 +492,7 @@ func (p *AlibabaCloudProvider) escapeTXTRecordValue(value string) string {
 
 func (p *AlibabaCloudProvider) unescapeTXTRecordValue(value string) string {
 	if strings.HasPrefix(value, "heritage=") {
-		return fmt.Sprintf("\"%s\"", strings.Replace(value, ";", ",", -1))
+		return fmt.Sprintf("\"%s\"", strings.ReplaceAll(value, ";", ","))
 	}
 	return value
 }
diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go
index db8a3c13..a163c4c7 100644
--- a/provider/cloudflare/cloudflare.go
+++ b/provider/cloudflare/cloudflare.go
@@ -673,7 +673,7 @@ func (p *CloudFlareProvider) listDNSRecordsWithAutoPagination(ctx context.Contex
 			records[newDNSRecordIndex(r)] = r
 		}
 		params.ResultInfo = resultInfo.Next()
-		if params.ResultInfo.Done() {
+		if params.Done() {
 			break
 		}
 	}
diff --git a/provider/gandi/gandi.go b/provider/gandi/gandi.go
index bc06d862..63b193f8 100644
--- a/provider/gandi/gandi.go
+++ b/provider/gandi/gandi.go
@@ -54,7 +54,7 @@ type GandiProvider struct {
 func NewGandiProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*GandiProvider, error) {
 	key, ok_key := os.LookupEnv("GANDI_KEY")
 	pat, ok_pat := os.LookupEnv("GANDI_PAT")
-	if !(ok_key || ok_pat) {
+	if !ok_key && !ok_pat {
 		return nil, errors.New("no environment variable GANDI_KEY or GANDI_PAT provided")
 	}
 	if ok_key {
diff --git a/provider/ovh/ovh.go b/provider/ovh/ovh.go
index 22437697..5b7eb8ee 100644
--- a/provider/ovh/ovh.go
+++ b/provider/ovh/ovh.go
@@ -588,7 +588,7 @@ func (p OVHProvider) newOvhChangeUpdate(endpointsOld []*endpoint.Endpoint, endpo
 		toInsertTarget := []string{}
 
 		for _, target := range endpointsNew.Targets {
-			var toDelete int = -1
+			var toDelete = -1
 
 			for i, record := range oldRecords {
 				if target == record.Target {
diff --git a/provider/pihole/client.go b/provider/pihole/client.go
index 0d1c7152..cccf8523 100644
--- a/provider/pihole/client.go
+++ b/provider/pihole/client.go
@@ -60,10 +60,8 @@ func newPiholeClient(cfg PiholeConfig) (piholeAPI, error) {
 	}
 
 	// Setup a persistent cookiejar for storing PHP session information
-	jar, err := cookiejar.New(&cookiejar.Options{})
-	if err != nil {
-		return nil, err
-	}
+	// This call will never return an error
+	jar, _ := cookiejar.New(&cookiejar.Options{})
 	// Setup an HTTP client using the cookiejar
 	httpClient := &http.Client{
 		Jar: jar,
diff --git a/provider/pihole/clientV6.go b/provider/pihole/clientV6.go
new file mode 100644
index 00000000..ae6ae399
--- /dev/null
+++ b/provider/pihole/clientV6.go
@@ -0,0 +1,406 @@
+/*
+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 pihole
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"github.com/linki/instrumented_http"
+	log "github.com/sirupsen/logrus"
+
+	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/provider"
+)
+
+const (
+	contentTypeJSON = "application/json"
+	apiAuthPath     = "/api/auth"
+	apiConfigDNS    = "/api/config/dns"
+)
+
+// piholeClient implements the piholeAPI.
+type piholeClientV6 struct {
+	cfg        PiholeConfig
+	httpClient *http.Client
+	token      string
+}
+
+// newPiholeClient creates a new Pihole API V6 client.
+func newPiholeClientV6(cfg PiholeConfig) (piholeAPI, error) {
+	if cfg.Server == "" {
+		return nil, ErrNoPiholeServer
+	}
+
+	// Setup an HTTP client
+	httpClient := &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: cfg.TLSInsecureSkipVerify,
+			},
+		},
+	}
+	cl := instrumented_http.NewClient(httpClient, &instrumented_http.Callbacks{})
+
+	p := &piholeClientV6{
+		cfg:        cfg,
+		httpClient: cl,
+	}
+
+	if cfg.Password != "" {
+		if err := p.retrieveNewToken(context.Background()); err != nil {
+			return nil, err
+		}
+	}
+
+	return p, nil
+}
+
+func (p *piholeClientV6) getConfigValue(ctx context.Context, rtype string) ([]string, error) {
+	apiUrl, err := p.urlForRecordType(rtype)
+	if err != nil {
+		return nil, err
+	}
+
+	log.Debugf("Listing %s records from %s", rtype, apiUrl)
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiUrl, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	jRes, err := p.do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse JSON response
+	var apiResponse ApiRecordsResponse
+	if err := json.Unmarshal(jRes, &apiResponse); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal error response: %w", err)
+	}
+
+	// Pi-Hole does not allow for a record to have multiple targets.
+	var results []string
+	if endpoint.RecordTypeCNAME == rtype {
+		results = apiResponse.Config.DNS.CnameRecords
+	} else {
+		results = apiResponse.Config.DNS.Hosts
+	}
+
+	return results, nil
+}
+
+func (p *piholeClientV6) listRecords(ctx context.Context, rtype string) ([]*endpoint.Endpoint, error) {
+	out := make([]*endpoint.Endpoint, 0)
+	results, err := p.getConfigValue(ctx, rtype)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, rec := range results {
+		recs := strings.FieldsFunc(rec, func(r rune) bool {
+			return r == ' ' || r == ','
+		})
+		if len(recs) < 2 {
+			log.Warnf("skipping record %s: invalid format", rec)
+			continue
+		}
+		var DNSName, Target string
+		var Ttl endpoint.TTL = 0
+		// A/AAAA record format is target(IP) DNSName
+		DNSName, Target = recs[1], recs[0]
+
+		switch rtype {
+		case endpoint.RecordTypeA:
+			if strings.Contains(Target, ":") {
+				continue
+			}
+		case endpoint.RecordTypeAAAA:
+			if strings.Contains(Target, ".") {
+				continue
+			}
+		case endpoint.RecordTypeCNAME:
+			// CNAME format is DNSName,target
+			DNSName, Target = recs[0], recs[1]
+			if len(recs) == 3 { // TTL is present
+				// Parse string to int64 first
+				if ttlInt, err := strconv.ParseInt(recs[2], 10, 64); err == nil {
+					Ttl = endpoint.TTL(ttlInt)
+				} else {
+					log.Warnf("failed to parse TTL value '%s': %v; using a TTL of %d", recs[2], err, Ttl)
+				}
+			}
+		}
+
+		out = append(out, &endpoint.Endpoint{
+			DNSName:    DNSName,
+			Targets:    []string{Target},
+			RecordTTL:  Ttl,
+			RecordType: rtype,
+		})
+	}
+	return out, nil
+}
+
+func (p *piholeClientV6) createRecord(ctx context.Context, ep *endpoint.Endpoint) error {
+	return p.apply(ctx, http.MethodPut, ep)
+}
+
+func (p *piholeClientV6) deleteRecord(ctx context.Context, ep *endpoint.Endpoint) error {
+	return p.apply(ctx, http.MethodDelete, ep)
+}
+
+func (p *piholeClientV6) aRecordsScript() string {
+	return fmt.Sprintf("%s"+apiConfigDNS+"/hosts", p.cfg.Server)
+}
+
+func (p *piholeClientV6) cnameRecordsScript() string {
+	return fmt.Sprintf("%s"+apiConfigDNS+"/cnameRecords", p.cfg.Server)
+}
+
+func (p *piholeClientV6) urlForRecordType(rtype string) (string, error) {
+	switch rtype {
+	case endpoint.RecordTypeA, endpoint.RecordTypeAAAA:
+		return p.aRecordsScript(), nil
+	case endpoint.RecordTypeCNAME:
+		return p.cnameRecordsScript(), nil
+	default:
+		return "", fmt.Errorf("unsupported record type: %s", rtype)
+	}
+}
+
+// ApiAuthResponse Define a struct to match the JSON response /auth/app structure
+type ApiAuthResponse struct {
+	Session struct {
+		Valid    bool   `json:"valid"`
+		TOTP     bool   `json:"totp"`
+		SID      string `json:"sid"`
+		CSRF     string `json:"csrf"`
+		Validity int    `json:"validity"`
+		Message  string `json:"message"`
+	} `json:"session"`
+	Took float64 `json:"took"`
+}
+
+// ApiErrorResponse Define struct to match the JSON structure
+type ApiErrorResponse struct {
+	Error struct {
+		Key     string `json:"key"`
+		Message string `json:"message"`
+		Hint    string `json:"hint"`
+	} `json:"error"`
+	Took float64 `json:"took"`
+}
+
+// ApiRecordsResponse Define struct to match JSON structure
+type ApiRecordsResponse struct {
+	Config struct {
+		DNS struct {
+			Hosts        []string `json:"hosts"`
+			CnameRecords []string `json:"cnameRecords"`
+		} `json:"dns"`
+	} `json:"config"`
+	Took float64 `json:"took"`
+}
+
+func (p *piholeClientV6) generateApiUrl(baseUrl, params string) string {
+	return fmt.Sprintf("%s/%s", baseUrl, url.PathEscape(params))
+}
+
+func (p *piholeClientV6) apply(ctx context.Context, action string, ep *endpoint.Endpoint) error {
+	if !p.cfg.DomainFilter.Match(ep.DNSName) {
+		log.Debugf("Skipping : %s %s that does not match domain filter", action, ep.DNSName)
+		return nil
+	}
+	apiUrl, err := p.urlForRecordType(ep.RecordType)
+	if err != nil {
+		log.Warnf("Skipping : unsupported endpoint %s %s %v", ep.DNSName, ep.RecordType, ep.Targets)
+		return nil
+	}
+
+	if len(ep.Targets) == 0 {
+		log.Infof("Skipping : missing targets  %s %s %s", action, ep.DNSName, ep.RecordType)
+		return nil
+	}
+
+	if p.cfg.DryRun {
+		log.Infof("DRY RUN: %s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, ep.Targets[0])
+		return nil
+	}
+
+	log.Infof("%s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, ep.Targets[0])
+
+	// Get the current record
+	if strings.Contains(ep.DNSName, "*") {
+		return provider.NewSoftError(errors.New("UNSUPPORTED: Pihole DNS names cannot return wildcard"))
+	}
+
+	switch ep.RecordType {
+	case endpoint.RecordTypeA, endpoint.RecordTypeAAAA:
+		apiUrl = p.generateApiUrl(apiUrl, fmt.Sprintf("%s %s", ep.Targets, ep.DNSName))
+	case endpoint.RecordTypeCNAME:
+		if ep.RecordTTL.IsConfigured() {
+			apiUrl = p.generateApiUrl(apiUrl, fmt.Sprintf("%s,%s,%d", ep.DNSName, ep.Targets, ep.RecordTTL))
+		} else {
+			apiUrl = p.generateApiUrl(apiUrl, fmt.Sprintf("%s,%s", ep.DNSName, ep.Targets))
+		}
+	}
+
+	req, err := http.NewRequestWithContext(ctx, action, apiUrl, nil)
+	if err != nil {
+		return err
+	}
+
+	_, err = p.do(req)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (p *piholeClientV6) retrieveNewToken(ctx context.Context) error {
+	if p.cfg.Password == "" {
+		return nil
+	}
+
+	apiUrl := fmt.Sprintf("%s"+apiAuthPath, p.cfg.Server)
+	log.Debugf("Fetching new token from %s", apiUrl)
+
+	// Define the JSON payload
+	jsonData := []byte(`{"password":"` + p.cfg.Password + `"}`)
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiUrl, bytes.NewBuffer(jsonData))
+	if err != nil {
+		return err
+	}
+
+	jRes, err := p.do(req)
+	if err != nil {
+		return err
+	}
+
+	// Parse JSON response
+	var apiResponse ApiAuthResponse
+	if err := json.Unmarshal(jRes, &apiResponse); err != nil {
+		log.Errorf("Auth Query : failed to unmarshal error response: %v", err)
+	} else {
+		// Set the token
+		if apiResponse.Session.SID != "" {
+			p.token = apiResponse.Session.SID
+		}
+	}
+	return err
+}
+
+func (p *piholeClientV6) checkTokenValidity(ctx context.Context) (bool, error) {
+	if p.token == "" {
+		return false, nil
+	}
+
+	apiUrl := fmt.Sprintf("%s"+apiAuthPath, p.cfg.Server)
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiUrl, nil)
+	if err != nil {
+		return false, nil
+	}
+	req.Header.Add("content-type", contentTypeJSON)
+	if p.token != "" {
+		req.Header.Add("X-FTL-SID", p.token)
+	}
+	res, err := p.httpClient.Do(req)
+	if err != nil {
+		return false, err
+	}
+
+	jRes, err := io.ReadAll(res.Body)
+	defer res.Body.Close()
+	if err != nil {
+		return false, err
+	}
+
+	// Parse JSON response
+	var apiResponse ApiAuthResponse
+	if err := json.Unmarshal(jRes, &apiResponse); err != nil {
+		return false, fmt.Errorf("failed to unmarshal error response: %w", err)
+	}
+	return apiResponse.Session.Valid, nil
+}
+
+func (p *piholeClientV6) do(req *http.Request) ([]byte, error) {
+	req.Header.Add("content-type", contentTypeJSON)
+	if p.token != "" {
+		req.Header.Add("X-FTL-SID", p.token)
+	}
+	res, err := p.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	jRes, err := io.ReadAll(res.Body)
+	defer res.Body.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	if res.StatusCode != http.StatusOK &&
+		res.StatusCode != http.StatusCreated &&
+		res.StatusCode != http.StatusNoContent {
+		// Parse JSON response
+		var apiError ApiErrorResponse
+		if err := json.Unmarshal(jRes, &apiError); err != nil {
+			return nil, fmt.Errorf("failed to unmarshal error response: %w", err)
+		}
+		log.Debugf("Error on request %s", req.Body)
+		if res.StatusCode == http.StatusUnauthorized && p.token != "" {
+			tryCount := 1
+			maxRetries := 3
+			// Try to fetch a new token and redo the request.
+			for tryCount <= maxRetries {
+				valid, err := p.checkTokenValidity(req.Context())
+				if err != nil {
+					return nil, err
+				}
+				if !valid {
+					log.Debugf("Pihole token has expired, fetching a new one. Try (%d/%d)", tryCount, maxRetries)
+					if err := p.retrieveNewToken(req.Context()); err != nil {
+						return nil, err
+					}
+					tryCount++
+					continue
+				}
+				break
+			}
+			if tryCount > maxRetries {
+				return nil, errors.New("max tries reached for token renewal")
+			}
+			return p.do(req)
+		}
+		return nil, fmt.Errorf("received %d status code from request: [%s] %s (%s) - %fs", res.StatusCode, apiError.Error.Key, apiError.Error.Message, apiError.Error.Hint, apiError.Took)
+	}
+	return jRes, nil
+}
diff --git a/provider/pihole/pihole.go b/provider/pihole/pihole.go
index 312054e1..ee0bbab8 100644
--- a/provider/pihole/pihole.go
+++ b/provider/pihole/pihole.go
@@ -47,6 +47,8 @@ type PiholeConfig struct {
 	DomainFilter endpoint.DomainFilter
 	// Do nothing and log what would have changed to stdout.
 	DryRun bool
+	// PiHole API version =<5 or >=6, default is 5
+	APIVersion string
 }
 
 // Helper struct for de-duping DNS entry updates.
@@ -57,7 +59,14 @@ type piholeEntryKey struct {
 
 // NewPiholeProvider initializes a new Pi-hole Local DNS based Provider.
 func NewPiholeProvider(cfg PiholeConfig) (*PiholeProvider, error) {
-	api, err := newPiholeClient(cfg)
+	var api piholeAPI
+	var err error
+	switch cfg.APIVersion {
+	case "6":
+		api, err = newPiholeClientV6(cfg)
+	default:
+		api, err = newPiholeClient(cfg)
+	}
 	if err != nil {
 		return nil, err
 	}
diff --git a/provider/plural/plural.go b/provider/plural/plural.go
index a73c2905..9f5a7715 100644
--- a/provider/plural/plural.go
+++ b/provider/plural/plural.go
@@ -48,7 +48,7 @@ func NewPluralProvider(cluster, provider string) (*PluralProvider, error) {
 	endpoint := os.Getenv("PLURAL_ENDPOINT")
 
 	if token == "" {
-		return nil, fmt.Errorf("No plural access token provided, you must set the PLURAL_ACCESS_TOKEN env var")
+		return nil, fmt.Errorf("no plural access token provided, you must set the PLURAL_ACCESS_TOKEN env var")
 	}
 
 	config := &Config{
diff --git a/provider/provider.go b/provider/provider.go
index 317a643b..2fc03d28 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -30,7 +30,7 @@ import (
 // SoftError is an error, that provider will only log as error instead
 // of fatal. It is meant for error propagation from providers to tell
 // that this is a transient error.
-var SoftError error = errors.New("soft error")
+var SoftError error = errors.New("soft error") //nolint:staticcheck
 
 // NewSoftErrorf creates a SoftError with formats according to a format specifier and returns the string as a
 func NewSoftErrorf(format string, a ...any) error {
diff --git a/provider/tencentcloud/cloudapi/tencentapi.go b/provider/tencentcloud/cloudapi/tencentapi.go
index 1c8338e0..7cc6e6d0 100644
--- a/provider/tencentcloud/cloudapi/tencentapi.go
+++ b/provider/tencentcloud/cloudapi/tencentapi.go
@@ -238,13 +238,14 @@ func (api *defaultTencentAPIService) ModifyRecord(request *dnspod.ModifyRecordRe
 func dealWithError(action Action, request string, err error) bool {
 	log.Errorf("dealWithError %s/%s request: %s, error: %s.", action.Service, action.Name, request, err.Error())
 	if sdkError, ok := err.(*errors.TencentCloudSDKError); ok {
-		if sdkError.Code == "RequestLimitExceeded" {
+		switch sdkError.Code {
+		case "RequestLimitExceeded":
 			return true
-		} else if sdkError.Code == "InternalError" || sdkError.Code == "ClientError.HttpStatusCodeError" {
+		case "InternalError", "ClientError.HttpStatusCodeError":
 			return false
-		} else if sdkError.Code == "ClientError.NetworkError" {
+		case "ClientError.NetworkError":
 			return false
-		} else if sdkError.Code == "AuthFailure.UnauthorizedOperation" || sdkError.Code == "UnauthorizedOperation.CamNoAuth" {
+		case "AuthFailure.UnauthorizedOperation", "UnauthorizedOperation.CamNoAuth":
 			return false
 		}
 		return false
diff --git a/provider/ultradns/ultradns.go b/provider/ultradns/ultradns.go
index 6208f7a6..9303813c 100644
--- a/provider/ultradns/ultradns.go
+++ b/provider/ultradns/ultradns.go
@@ -292,9 +292,10 @@ func (p *UltraDNSProvider) submitChanges(ctx context.Context, changes []*UltraDN
 
 	for zoneName, changes := range zoneChanges {
 		for _, change := range changes {
-			if change.ResourceRecordSetUltraDNS.RRType == "CNAME" {
+			switch change.ResourceRecordSetUltraDNS.RRType {
+			case "CNAME":
 				cnameownerName = change.ResourceRecordSetUltraDNS.OwnerName
-			} else if change.ResourceRecordSetUltraDNS.RRType == "TXT" {
+			case "TXT":
 				txtownerName = change.ResourceRecordSetUltraDNS.OwnerName
 			}
 
diff --git a/source/compatibility.go b/source/compatibility.go
index 1953b76c..d7f4351c 100644
--- a/source/compatibility.go
+++ b/source/compatibility.go
@@ -89,7 +89,7 @@ func legacyEndpointsFromMoleculeService(svc *v1.Service) []*endpoint.Endpoint {
 		return nil
 	}
 
-	hostnameList := strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")
+	hostnameList := strings.Split(strings.ReplaceAll(hostnameAnnotation, " ", ""), ",")
 
 	for _, hostname := range hostnameList {
 		// Create a corresponding endpoint for each configured external entrypoint.
@@ -145,9 +145,9 @@ func legacyEndpointsFromDNSControllerNodePortService(svc *v1.Service, sc *servic
 
 	var hostnameList []string
 	if isExternal {
-		hostnameList = strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")
+		hostnameList = strings.Split(strings.ReplaceAll(hostnameAnnotation, " ", ""), ",")
 	} else {
-		hostnameList = strings.Split(strings.Replace(internalHostnameAnnotation, " ", "", -1), ",")
+		hostnameList = strings.Split(strings.ReplaceAll(internalHostnameAnnotation, " ", ""), ",")
 	}
 
 	for _, hostname := range hostnameList {
@@ -186,10 +186,10 @@ func legacyEndpointsFromDNSControllerLoadBalancerService(svc *v1.Service) []*end
 
 	var hostnameList []string
 	if hasExternal {
-		hostnameList = append(hostnameList, strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")...)
+		hostnameList = append(hostnameList, strings.Split(strings.ReplaceAll(hostnameAnnotation, " ", ""), ",")...)
 	}
 	if hasInternal {
-		hostnameList = append(hostnameList, strings.Split(strings.Replace(internalHostnameAnnotation, " ", "", -1), ",")...)
+		hostnameList = append(hostnameList, strings.Split(strings.ReplaceAll(internalHostnameAnnotation, " ", ""), ",")...)
 	}
 
 	for _, hostname := range hostnameList {
diff --git a/source/crd.go b/source/crd.go
index d15b176e..70a72407 100644
--- a/source/crd.go
+++ b/source/crd.go
@@ -96,7 +96,7 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiS
 	scheme := runtime.NewScheme()
 	addKnownTypes(scheme, groupVersion)
 
-	config.ContentConfig.GroupVersion = &groupVersion
+	config.GroupVersion = &groupVersion
 	config.APIPath = "/apis"
 	config.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)}
 
@@ -184,7 +184,7 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 		crdEndpoints := []*endpoint.Endpoint{}
 		for _, ep := range dnsEndpoint.Spec.Endpoints {
 			if (ep.RecordType == "CNAME" || ep.RecordType == "A" || ep.RecordType == "AAAA") && len(ep.Targets) < 1 {
-				log.Warnf("Endpoint %s with DNSName %s has an empty list of targets", dnsEndpoint.ObjectMeta.Name, ep.DNSName)
+				log.Warnf("Endpoint %s with DNSName %s has an empty list of targets", dnsEndpoint.Name, ep.DNSName)
 				continue
 			}
 
@@ -200,7 +200,7 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 				}
 			}
 			if illegalTarget {
-				log.Warnf("Endpoint %s with DNSName %s has an illegal target. The subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com')", dnsEndpoint.ObjectMeta.Name, ep.DNSName)
+				log.Warnf("Endpoint %s with DNSName %s has an illegal target. The subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com')", dnsEndpoint.Name, ep.DNSName)
 				continue
 			}
 
@@ -231,7 +231,7 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 
 func (cs *crdSource) setResourceLabel(crd *endpoint.DNSEndpoint, endpoints []*endpoint.Endpoint) {
 	for _, ep := range endpoints {
-		ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("crd/%s/%s", crd.ObjectMeta.Namespace, crd.ObjectMeta.Name)
+		ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("crd/%s/%s", crd.Namespace, crd.Name)
 	}
 }
 
diff --git a/source/pod.go b/source/pod.go
index 85f7a83d..3a34a985 100644
--- a/source/pod.go
+++ b/source/pod.go
@@ -152,7 +152,7 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 		}
 
 		if ps.podSourceDomain != "" {
-			domain := pod.ObjectMeta.Name + "." + ps.podSourceDomain
+			domain := pod.Name + "." + ps.podSourceDomain
 			if len(targets) == 0 {
 				addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
 			}
diff --git a/source/service.go b/source/service.go
index 1e6ed212..e0f38c69 100644
--- a/source/service.go
+++ b/source/service.go
@@ -694,7 +694,7 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, hostname stri
 			// take the service name from the K8s Service object
 			// it is safe to use since it is DNS compatible
 			// see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
-			serviceName := svc.ObjectMeta.Name
+			serviceName := svc.Name
 
 			// figure out the protocol
 			protocol := strings.ToLower(string(port.Protocol))
diff --git a/source/skipper_routegroup.go b/source/skipper_routegroup.go
index d47682ba..943b9886 100644
--- a/source/skipper_routegroup.go
+++ b/source/skipper_routegroup.go
@@ -315,7 +315,7 @@ func (sc *routeGroupSource) endpointsFromTemplate(rg *routeGroup) ([]*endpoint.E
 
 	var endpoints []*endpoint.Endpoint
 	// splits the FQDN template and removes the trailing periods
-	hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",")
+	hostnameList := strings.Split(strings.ReplaceAll(hostnames, " ", ""), ",")
 	for _, hostname := range hostnameList {
 		hostname = strings.TrimSuffix(hostname, ".")
 		endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
diff --git a/source/source.go b/source/source.go
index 2f84dc81..4641c056 100644
--- a/source/source.go
+++ b/source/source.go
@@ -179,7 +179,7 @@ func getInternalHostnamesFromAnnotations(annotations map[string]string) []string
 }
 
 func splitHostnameAnnotation(annotation string) []string {
-	return strings.Split(strings.Replace(annotation, " ", "", -1), ",")
+	return strings.Split(strings.ReplaceAll(annotation, " ", ""), ",")
 }
 
 func getAliasFromAnnotations(annotations map[string]string) bool {
@@ -251,7 +251,7 @@ func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targ
 	targetAnnotation, exists := annotations[targetAnnotationKey]
 	if exists && targetAnnotation != "" {
 		// splits the hostname annotation and removes the trailing periods
-		targetsList := strings.Split(strings.Replace(targetAnnotation, " ", "", -1), ",")
+		targetsList := strings.Split(strings.ReplaceAll(targetAnnotation, " ", ""), ",")
 		for _, targetHostname := range targetsList {
 			targetHostname = strings.TrimSuffix(targetHostname, ".")
 			targets = append(targets, targetHostname)



Test patch:
diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go
index 9ce16a6b..22973250 100644
--- a/pkg/apis/externaldns/types_test.go
+++ b/pkg/apis/externaldns/types_test.go
@@ -128,6 +128,7 @@ var (
 		IBMCloudConfigFile:                            "/etc/kubernetes/ibmcloud.json",
 		TencentCloudConfigFile:                        "/etc/kubernetes/tencent-cloud.json",
 		TencentCloudZoneType:                          "",
+		PiholeApiVersion:                              "5",
 		WebhookProviderURL:                            "http://localhost:8888",
 		WebhookProviderReadTimeout:                    5 * time.Second,
 		WebhookProviderWriteTimeout:                   10 * time.Second,
@@ -242,6 +243,7 @@ var (
 		IBMCloudConfigFile:                            "ibmcloud.json",
 		TencentCloudConfigFile:                        "tencent-cloud.json",
 		TencentCloudZoneType:                          "private",
+		PiholeApiVersion:                              "6",
 		WebhookProviderURL:                            "http://localhost:8888",
 		WebhookProviderReadTimeout:                    5 * time.Second,
 		WebhookProviderWriteTimeout:                   10 * time.Second,
@@ -352,6 +354,7 @@ func TestParseFlags(t *testing.T) {
 				"--aws-sd-create-tag=key1=value1",
 				"--aws-sd-create-tag=key2=value2",
 				"--no-aws-evaluate-target-health",
+				"--pihole-api-version=6",
 				"--policy=upsert-only",
 				"--registry=noop",
 				"--txt-owner-id=owner-1",
@@ -474,6 +477,7 @@ func TestParseFlags(t *testing.T) {
 				"EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP":                            "true",
 				"EXTERNAL_DNS_AWS_SD_CREATE_TAG":                                 "key1=value1\nkey2=value2",
 				"EXTERNAL_DNS_DYNAMODB_TABLE":                                    "custom-table",
+				"EXTERNAL_DNS_PIHOLE_API_VERSION":                                "6",
 				"EXTERNAL_DNS_POLICY":                                            "upsert-only",
 				"EXTERNAL_DNS_REGISTRY":                                          "noop",
 				"EXTERNAL_DNS_TXT_OWNER_ID":                                      "owner-1",
diff --git a/provider/pihole/clientV6_test.go b/provider/pihole/clientV6_test.go
new file mode 100644
index 00000000..664d88aa
--- /dev/null
+++ b/provider/pihole/clientV6_test.go
@@ -0,0 +1,846 @@
+/*
+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 pihole
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"strconv"
+	"strings"
+	"testing"
+
+	"sigs.k8s.io/external-dns/endpoint"
+)
+
+func newTestServerV6(t *testing.T, hdlr http.HandlerFunc) *httptest.Server {
+	t.Helper()
+	svr := httptest.NewServer(hdlr)
+	return svr
+}
+
+func TestNewPiholeClientV6(t *testing.T) {
+	// Test correct error on no server provided
+	_, err := newPiholeClientV6(PiholeConfig{APIVersion: "6"})
+	if err == nil {
+		t.Error("Expected error from config with no server")
+	} else if err != ErrNoPiholeServer {
+		t.Error("Expected ErrNoPiholeServer, got", err)
+	}
+
+	// Test new client with no password. Should create the client cleanly.
+	cl, err := newPiholeClientV6(PiholeConfig{
+		Server:     "test",
+		APIVersion: "6",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if _, ok := cl.(*piholeClientV6); !ok {
+		t.Error("Did not create a new pihole client")
+	}
+
+	// Create a test server
+	srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/api/auth" && r.Method == "POST" {
+			var requestData map[string]string
+			json.NewDecoder(r.Body).Decode(&requestData)
+			defer r.Body.Close()
+
+			w.Header().Set("Content-Type", "application/json")
+
+			if requestData["password"] != "correct" {
+				// Return unsuccessful authentication response
+				w.WriteHeader(http.StatusUnauthorized)
+				w.Write([]byte(`{
+				"session": {
+					"valid": false,
+					"totp": false,
+					"sid": null,
+					"validity": -1,
+					"message": "password incorrect"
+				},
+				"took": 0.2
+			}`))
+				return
+			}
+
+			// Return successful authentication response
+			w.Write([]byte(`{
+			"session": {
+				"valid": true,
+				"totp": false,
+				"sid": "supersecret",
+				"csrf": "csrfvalue",
+				"validity": 1800,
+				"message": "password correct"
+			},
+			"took": 0.18
+		}`))
+		} else {
+			http.NotFound(w, r)
+		}
+	})
+	defer srvr.Close()
+
+	// Test invalid password
+	_, err = newPiholeClientV6(
+		PiholeConfig{Server: srvr.URL, APIVersion: "6", Password: "wrong"},
+	)
+	if err == nil {
+		t.Error("Expected error for creating client with invalid password")
+	}
+
+	// Test correct password
+	cl, err = newPiholeClientV6(
+		PiholeConfig{Server: srvr.URL, APIVersion: "6", Password: "correct"},
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if cl.(*piholeClientV6).token != "supersecret" {
+		t.Error("Parsed invalid token from login response:", cl.(*piholeClient).token)
+	}
+}
+
+func TestListRecordsV6(t *testing.T) {
+	// Create a test server
+	srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/api/config/dns/hosts" && r.Method == "GET" {
+
+			w.WriteHeader(http.StatusOK)
+			w.Header().Set("Content-Type", "application/json")
+
+			// Return A records
+			w.Write([]byte(`{
+				"config": {
+					"dns": {
+						"hosts": [
+							"192.168.178.33 service1.example.com",
+							"192.168.178.34 service2.example.com",
+							"192.168.178.34 service3.example.com",
+							"fc00::1:192:168:1:1 service4.example.com",
+							"fc00::1:192:168:1:2 service5.example.com",
+							"fc00::1:192:168:1:3 service6.example.com"
+						]
+					}
+				},
+				"took": 5
+			}`))
+		} else if r.URL.Path == "/api/config/dns/cnameRecords" && r.Method == "GET" {
+
+			w.WriteHeader(http.StatusOK)
+			w.Header().Set("Content-Type", "application/json")
+
+			// Return A records
+			w.Write([]byte(`{
+				"config": {
+					"dns": {
+						"cnameRecords": [
+							"source1.example.com,target1.domain.com,1000",
+							"source2.example.com,target2.domain.com,50",
+							"source3.example.com,target3.domain.com"
+						]
+					}
+				},
+				"took": 5
+			}`))
+		} else {
+			http.NotFound(w, r)
+		}
+	})
+	defer srvr.Close()
+
+	// Create a client
+	cfg := PiholeConfig{
+		Server:     srvr.URL,
+		APIVersion: "6",
+	}
+	cl, err := newPiholeClientV6(cfg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Test retrieve A records unfiltered
+	arecs, err := cl.listRecords(context.Background(), endpoint.RecordTypeA)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(arecs) != 3 {
+		t.Fatal("Expected 3 A records returned, got:", len(arecs))
+	}
+	// Ensure records were parsed correctly
+	expected := [][]string{
+		{"service1.example.com", "192.168.178.33"},
+		{"service2.example.com", "192.168.178.34"},
+		{"service3.example.com", "192.168.178.34"},
+	}
+	for idx, rec := range arecs {
+		if rec.DNSName != expected[idx][0] {
+			t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
+		}
+		if rec.Targets[0] != expected[idx][1] {
+			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
+		}
+	}
+
+	// Test retrieve AAAA records unfiltered
+	arecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeAAAA)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(arecs) != 3 {
+		t.Fatal("Expected 3 AAAA records returned, got:", len(arecs))
+	}
+	// Ensure records were parsed correctly
+	expected = [][]string{
+		{"service4.example.com", "fc00::1:192:168:1:1"},
+		{"service5.example.com", "fc00::1:192:168:1:2"},
+		{"service6.example.com", "fc00::1:192:168:1:3"},
+	}
+	for idx, rec := range arecs {
+		if rec.DNSName != expected[idx][0] {
+			t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
+		}
+		if rec.Targets[0] != expected[idx][1] {
+			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
+		}
+	}
+
+	// Test retrieve CNAME records unfiltered
+	cnamerecs, err := cl.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(cnamerecs) != 3 {
+		t.Fatal("Expected 3 CAME records returned, got:", len(cnamerecs))
+	}
+	// Ensure records were parsed correctly
+	expected = [][]string{
+		{"source1.example.com", "target1.domain.com", "1000"},
+		{"source2.example.com", "target2.domain.com", "50"},
+		{"source3.example.com", "target3.domain.com"},
+	}
+	for idx, rec := range cnamerecs {
+		if rec.DNSName != expected[idx][0] {
+			t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
+		}
+		if rec.Targets[0] != expected[idx][1] {
+			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
+		}
+		if len(expected[idx]) == 3 {
+			expectedTTL, _ := strconv.ParseInt(expected[idx][2], 10, 64)
+			if int64(rec.RecordTTL) != expectedTTL {
+				t.Error("Got invalid TTL:", rec.RecordTTL, "expected:", expected[idx][2])
+			}
+		}
+	}
+
+	// Note: filtered tests are not needed since A/AAAA records are tested filtered already
+	// and cnameRecords have their own element
+
+	// unsupported type
+	_, err = cl.listRecords(context.Background(), endpoint.RecordTypeNAPTR)
+	if err == nil || err.Error() != fmt.Sprintf("unsupported record type: %s", endpoint.RecordTypeNAPTR) {
+		t.Fatal("Expected error for using unsupported record type")
+	}
+}
+func TestErrorsV6(t *testing.T) {
+	//Error test cases
+
+	// Create a client
+	cfgErrURL := PiholeConfig{
+		Server:     "not an url",
+		APIVersion: "6",
+	}
+	clErrURL, _ := newPiholeClientV6(cfgErrURL)
+
+	_, err := clErrURL.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	if err == nil {
+		t.Fatal("Expected error for using invalid URL")
+	}
+	_, err = clErrURL.listRecords(nil, endpoint.RecordTypeCNAME)
+	if err == nil {
+		t.Fatal("Expected error for nil context")
+	}
+	// Unmarshalling error
+	srvrErrJson := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Set("Content-Type", "application/json")
+
+		// Return A records
+		w.Write([]byte(`I am not JSON`))
+	})
+	defer srvrErrJson.Close()
+	// Create a client
+	cfgErr := PiholeConfig{
+		Server:     srvrErrJson.URL,
+		APIVersion: "6",
+	}
+	clErr, _ := newPiholeClientV6(cfgErr)
+
+	resp, err := clErr.listRecords(context.Background(), endpoint.RecordTypeA)
+	if err == nil {
+		t.Fatal(err)
+	}
+
+	if !strings.HasPrefix(err.Error(), "failed to unmarshal error response:") {
+		t.Fatal("Expected unmarshalling error, got:", err)
+	}
+
+	// bad record format return by server
+	srvrErr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/api/config/dns/hosts" && r.Method == "GET" {
+			w.WriteHeader(http.StatusOK)
+			w.Header().Set("Content-Type", "application/json")
+
+			// Return A records
+			w.Write([]byte(`{
+				"config": {
+					"dns": {
+						"hosts": [
+							"192.168.178.33"
+						]
+					}
+				},
+				"took": 5
+			}`))
+		} else if r.URL.Path == "/api/config/dns/cnameRecords" && r.Method == "GET" {
+			w.WriteHeader(http.StatusOK)
+			w.Header().Set("Content-Type", "application/json")
+
+			// Return A records
+			w.Write([]byte(`{
+				"config": {
+					"dns": {
+						"cnameRecords": [
+							"source1.example.com,target1.domain.com,100",
+							"source2.example.com,target2.domain.com,not_an_integer"
+						]
+					}
+				},
+				"took": 5
+			}`))
+		} else {
+			http.NotFound(w, r)
+		}
+	})
+	defer srvrErr.Close()
+
+	// Create a client
+	cfgErr = PiholeConfig{
+		Server:     srvrErr.URL,
+		APIVersion: "6",
+	}
+	clErr, _ = newPiholeClientV6(cfgErr)
+
+	resp, err = clErr.listRecords(context.Background(), endpoint.RecordTypeA)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(resp) != 0 {
+		t.Fatal("Expected no records returned, got:", len(resp))
+	}
+	resp, err = clErr.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(resp) != 2 {
+		t.Fatal("Expected one records returned, got:", len(resp))
+	}
+	if resp[1].RecordTTL != 0 {
+		t.Fatal("Expected no TTL returned, got:", resp[0].RecordTTL)
+	}
+
+}
+
+func TestTokenValidity(t *testing.T) {
+	srvok := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/api/auth" && r.Method == "GET" {
+			w.WriteHeader(http.StatusOK)
+			w.Header().Set("Content-Type", "application/json")
+
+			// Return bad content
+			w.Write([]byte(`{
+			"session": {
+				"valid": true,
+				"totp": false,
+				"sid": "supersecret",
+				"csrf": "csrfvalue",
+				"validity": 1800,
+				"message": "password correct"
+			},
+			"took": 0.17
+			}`))
+		}
+	})
+	// Create a client
+	cfgOK := PiholeConfig{
+		Server:     srvok.URL,
+		APIVersion: "6",
+	}
+	clOK, err := newPiholeClientV6(cfgOK)
+	clOK.(*piholeClientV6).token = "valid"
+	validity, err := clOK.(*piholeClientV6).checkTokenValidity(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !validity {
+		t.Fatal("Should be valid")
+	}
+
+	// Create a test server
+	srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+
+		if r.URL.Path == "/api/auth" && r.Method == "GET" {
+			w.WriteHeader(http.StatusOK)
+			w.Header().Set("Content-Type", "application/json")
+
+			// Return bad content
+			w.Write([]byte(`Not a JSON`))
+		}
+	})
+	defer srvr.Close()
+	//
+	// Create a client
+	cfg := PiholeConfig{
+		Server:     srvr.URL,
+		APIVersion: "6",
+	}
+	cl, err := newPiholeClientV6(cfg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	validity, err = cl.(*piholeClientV6).checkTokenValidity(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if validity {
+		t.Fatal("Should be invalid : no token")
+	}
+	// Test token validity
+	cl.(*piholeClientV6).token = "valid"
+
+	validity, err = cl.(*piholeClientV6).checkTokenValidity(nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if validity {
+		t.Fatal("Should be invalid : nil context")
+	}
+
+	validity, err = cl.(*piholeClientV6).checkTokenValidity(context.Background())
+	if err == nil {
+		t.Fatal("Should be invalid : failed to unmarshal error")
+	}
+	if !strings.HasPrefix(err.Error(), "failed to unmarshal error response") {
+		t.Fatal("Expected unmarshalling error, got:", err)
+	}
+	if validity {
+		t.Fatal("Should be invalid : unmarshalling error")
+	}
+}
+
+func TestDo(t *testing.T) {
+
+	srvDo := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/api/auth/ok" && r.Method == "GET" {
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(http.StatusOK)
+			// Return bad content
+			w.Write([]byte(`{
+			"session": {
+				"valid": true,
+				"totp": false,
+				"sid": "supersecret",
+				"csrf": "csrfvalue",
+				"validity": 1800,
+				"message": "password correct"
+			},
+			"took": 0.16
+			}`))
+		} else if r.URL.Path == "/api/auth" && r.Method == "POST" {
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(http.StatusOK)
+			// Return bad content
+			w.Write([]byte(`{
+			"session": {
+				"valid": false,
+				"totp": false,
+				"sid": "",
+				"csrf": "csrfvalue",
+				"validity": 1800,
+				"message": "password correct"
+			},
+			"took": 0.15
+			}`))
+		} else if r.URL.Path == "/api/auth" && r.Method == "GET" {
+			w.WriteHeader(http.StatusUnauthorized)
+			// Return bad content
+			w.Write([]byte(`{
+			"error": {
+				"key": "401",
+				"message": "Expired token",
+				"hint": "Expired token"
+			},
+			"took": 0.14
+			}`))
+		} else if r.URL.Path == "/api/auth/418" && r.Method == "GET" {
+			w.WriteHeader(http.StatusTeapot)
+			// Return bad content
+			w.Write([]byte(`{
+			"error": {
+				"key": "418",
+				"message": "I'm a teapot",
+				"hint": "It is a teapot"
+			},
+			"took": 0.13
+			}`))
+		} else if r.URL.Path == "/api/auth/nojson" && r.Method == "GET" {
+			// Return bad content
+			w.WriteHeader(http.StatusTeapot)
+			w.Write([]byte(`Not a JSON`))
+		} else if r.URL.Path == "/api/auth/401" && r.Method == "GET" {
+			w.WriteHeader(http.StatusUnauthorized)
+			// Return bad content
+			w.Write([]byte(`{
+			"error": {
+				"key": "401",
+				"message": "Expired token",
+				"hint": "Expired token"
+			},
+			"took": 0.10
+			}`))
+		}
+	})
+	defer srvDo.Close()
+
+	// Create a client
+	cfg := PiholeConfig{
+		Server:     srvDo.URL,
+		APIVersion: "6",
+	}
+	cl, err := newPiholeClientV6(cfg)
+	cl.(*piholeClientV6).token = "valid"
+
+	rq, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srvDo.URL+"/api/auth/ok", nil)
+	resp, err := cl.(*piholeClientV6).do(rq)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(resp) == 0 {
+		t.Fatal("Should have a response")
+	}
+	// Test not handled error code
+	rq, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, srvDo.URL+"/api/auth/418", nil)
+	resp, err = cl.(*piholeClientV6).do(rq)
+	if resp != nil {
+		t.Fatal(err)
+	}
+	if err == nil {
+		t.Fatal("Should have an error")
+	}
+	if !strings.HasPrefix(err.Error(), "received 418 status code from request") {
+		t.Fatal("Expected error for unexpected status code, got:", err)
+	}
+	// Test error on non JSON response
+	rq, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, srvDo.URL+"/api/auth/nojson", nil)
+	resp, err = cl.(*piholeClientV6).do(rq)
+	if resp != nil {
+		t.Fatal(err)
+	}
+	if err == nil {
+		t.Fatal("Should have an error")
+	}
+	if !strings.HasPrefix(err.Error(), "failed to unmarshal error response") {
+		t.Fatal("Expected error for unmarshal", err)
+	}
+	// Test Unauthorized retry failed
+	rq, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, srvDo.URL+"/api/auth/401", nil)
+	resp, err = cl.(*piholeClientV6).do(rq)
+	if resp != nil {
+		t.Fatal(err)
+	}
+	if err == nil {
+		t.Fatal("Should have an error")
+	}
+	if !strings.HasPrefix(err.Error(), "max tries reached for token renewal") {
+		t.Fatal("Expected error for max tries reached", err)
+	}
+}
+
+func TestDoRetryOne(t *testing.T) {
+	nbCall := 0
+	srvRetry := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/api/auth" && r.Method == "GET" {
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(http.StatusOK)
+			// Return bad content
+			w.Write([]byte(`{
+			"session": {
+				"valid": true,
+				"totp": false,
+				"sid": "123465468",
+				"csrf": "csrfvalue",
+				"validity": 1800,
+				"message": "password correct"
+			},
+			"took": 0.24
+			}`))
+		} else if r.URL.Path == "/api/auth/401" && r.Method == "GET" {
+			if nbCall == 0 {
+				w.WriteHeader(http.StatusUnauthorized)
+				// Return bad content
+				w.Write([]byte(`{
+				"error": {
+					"key": "401",
+					"message": "Expired token",
+					"hint": "Expired token"
+				},
+				"took": 0.25
+				}`))
+			} else {
+				w.WriteHeader(http.StatusOK)
+				// Return bad content
+				w.Write([]byte(`Success`))
+			}
+			nbCall += 1
+		}
+	})
+	defer srvRetry.Close()
+	// Create a client
+	cfgRetryOK := PiholeConfig{
+		Server:     srvRetry.URL,
+		APIVersion: "6",
+	}
+	clRetryOK, err := newPiholeClientV6(cfgRetryOK)
+	clRetryOK.(*piholeClientV6).token = "valid"
+	// Test Unauthorized refresh OK
+	rq, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srvRetry.URL+"/api/auth/401", nil)
+	resp, err := clRetryOK.(*piholeClientV6).do(rq)
+	if err != nil {
+		t.Fatal("Should succeed", err)
+	}
+	if string(resp) != "Success" {
+		t.Fatal("Should have a response")
+	}
+
+}
+
+func TestCreateRecordV6(t *testing.T) {
+	var ep *endpoint.Endpoint
+	srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+		if r.Method == "PUT" && (r.URL.Path == "/api/config/dns/hosts/192.168.1.1 test.example.com" ||
+			r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:1 test.example.com" ||
+			r.URL.Path == "/api/config/dns/cnameRecords/source1.example.com,target1.domain.com" ||
+			r.URL.Path == "/api/config/dns/cnameRecords/source2.example.com,target2.domain.com,500") {
+
+			// Return A records
+			w.WriteHeader(http.StatusCreated)
+		} else {
+			http.NotFound(w, r)
+		}
+	})
+	defer srvr.Close()
+
+	// Create a client
+	cfg := PiholeConfig{
+		Server:       srvr.URL,
+		APIVersion:   "6",
+		DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
+	}
+	cl, err := newPiholeClientV6(cfg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Test create A record
+	ep = &endpoint.Endpoint{
+		DNSName:    "test.example.com",
+		Targets:    []string{"192.168.1.1"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	if err := cl.createRecord(context.Background(), ep); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test create AAAA record
+	ep = &endpoint.Endpoint{
+		DNSName:    "test.example.com",
+		Targets:    []string{"fc00::1:192:168:1:1"},
+		RecordType: endpoint.RecordTypeAAAA,
+	}
+	if err := cl.createRecord(context.Background(), ep); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test create CNAME record
+	ep = &endpoint.Endpoint{
+		DNSName:    "source1.example.com",
+		Targets:    []string{"target1.domain.com"},
+		RecordType: endpoint.RecordTypeCNAME,
+	}
+	if err := cl.createRecord(context.Background(), ep); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test create CNAME record with TTL
+	ep = &endpoint.Endpoint{
+		DNSName:    "source2.example.com",
+		Targets:    []string{"target2.domain.com"},
+		RecordTTL:  endpoint.TTL(500),
+		RecordType: endpoint.RecordTypeCNAME,
+	}
+	if err := cl.createRecord(context.Background(), ep); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test create a wildcard record and ensure it fails
+	ep = &endpoint.Endpoint{
+		DNSName:    "*.example.com",
+		Targets:    []string{"192.168.1.1"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	if err := cl.createRecord(context.Background(), ep); err == nil {
+		t.Fatal(err)
+	}
+
+	// Skip not matching domain
+	ep = &endpoint.Endpoint{
+		DNSName:    "foo.bar.com",
+		Targets:    []string{"192.168.1.1"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	err = cl.createRecord(context.Background(), ep)
+	if err != nil {
+		t.Fatal("Should not return error on non filtered domain")
+	}
+
+	// Not supported type
+	ep = &endpoint.Endpoint{
+		DNSName:    "test.example.com",
+		Targets:    []string{"192.168.1.1"},
+		RecordType: "not a type",
+	}
+	err = cl.createRecord(context.Background(), ep)
+	if err != nil {
+		t.Fatal("Should not return error on unsupported type")
+	}
+
+	// Create a client
+	cfgDr := PiholeConfig{
+		Server:       srvr.URL,
+		APIVersion:   "6",
+		DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
+		DryRun:       true,
+	}
+	clDr, err := newPiholeClientV6(cfgDr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Skip Dry Run
+	ep = &endpoint.Endpoint{
+		DNSName:    "test.example.com",
+		Targets:    []string{"192.168.1.1"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	err = clDr.createRecord(context.Background(), ep)
+	if err != nil {
+		t.Fatal("Should not return error on dry run")
+	}
+	// skip missing targets
+	ep = &endpoint.Endpoint{
+		DNSName:    "test.example.com",
+		Targets:    []string{},
+		RecordType: endpoint.RecordTypeA,
+	}
+	err = clDr.createRecord(context.Background(), ep)
+	if err != nil {
+		t.Fatal("Should not return error on missing targets")
+	}
+}
+
+func TestDeleteRecordV6(t *testing.T) {
+	var ep *endpoint.Endpoint
+	srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
+		if r.Method == "DELETE" && (r.URL.Path == "/api/config/dns/hosts/192.168.1.1 test.example.com" ||
+			r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:1 test.example.com" ||
+			r.URL.Path == "/api/config/dns/cnameRecords/source1.example.com,target1.domain.com" ||
+			r.URL.Path == "/api/config/dns/cnameRecords/source2.example.com,target2.domain.com,500") {
+
+			// Return A records
+			w.WriteHeader(http.StatusNoContent)
+		} else {
+			http.NotFound(w, r)
+		}
+	})
+	defer srvr.Close()
+
+	// Create a client
+	cfg := PiholeConfig{
+		Server:     srvr.URL,
+		APIVersion: "6",
+	}
+	cl, err := newPiholeClientV6(cfg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Test delete A record
+	ep = &endpoint.Endpoint{
+		DNSName:    "test.example.com",
+		Targets:    []string{"192.168.1.1"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	if err := cl.deleteRecord(context.Background(), ep); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test delete AAAA record
+	ep = &endpoint.Endpoint{
+		DNSName:    "test.example.com",
+		Targets:    []string{"fc00::1:192:168:1:1"},
+		RecordType: endpoint.RecordTypeAAAA,
+	}
+	if err := cl.deleteRecord(context.Background(), ep); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test delete CNAME record
+	ep = &endpoint.Endpoint{
+		DNSName:    "source1.example.com",
+		Targets:    []string{"target1.domain.com"},
+		RecordType: endpoint.RecordTypeCNAME,
+	}
+	if err := cl.deleteRecord(context.Background(), ep); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test delete CNAME record with TTL
+	ep = &endpoint.Endpoint{
+		DNSName:    "source2.example.com",
+		Targets:    []string{"target2.domain.com"},
+		RecordTTL:  endpoint.TTL(500),
+		RecordType: endpoint.RecordTypeCNAME,
+	}
+	if err := cl.deleteRecord(context.Background(), ep); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/provider/pihole/client_test.go b/provider/pihole/client_test.go
index 92b0a875..7782706a 100644
--- a/provider/pihole/client_test.go
+++ b/provider/pihole/client_test.go
@@ -277,6 +277,72 @@ func TestListRecords(t *testing.T) {
 			t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
 		}
 	}
+
+	// Test errors token
+	srvrErr := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
+		r.ParseForm()
+		pw := r.Form.Get("pw")
+		if pw != "" {
+			if pw != "correct" {
+				// Pihole actually server side renders the fact that you failed, normal 200
+				w.Write([]byte("Invalid"))
+				return
+			}
+		}
+		if strings.Contains(r.URL.Path, "admin/scripts/pi-hole/php/customcname.php") && r.Form.Get("token") == "correct" {
+			w.Write([]byte(`
+				{
+					"nodata": [
+						["nodata", "no"]
+					]
+				}
+				`))
+			return
+		}
+		if strings.Contains(r.URL.Path, "admin/index.php?login") {
+			w.Write([]byte(`
+			<!doctype html>
+			<html lang="en">
+				<body>
+					<div id="token" hidden>supersecret</div>
+				</body>
+			</html>
+			`))
+		}
+		// Token Expired
+		w.Write([]byte(`
+		{
+			"auth": "expired"
+		}
+		`))
+	})
+	defer srvrErr.Close()
+	cfgExpired := PiholeConfig{
+		Server: srvrErr.URL,
+	}
+	clExpired, err := newPiholeClient(cfgExpired)
+	if err != nil {
+		t.Fatal(err)
+	}
+	//set clExpired.token to a valid token
+	clExpired.(*piholeClient).token = "expired"
+	clExpired.(*piholeClient).cfg.Password = "notcorrect"
+
+	cnamerecs, err = clExpired.listRecords(context.Background(), "notarealrecordtype")
+	if err == nil {
+		t.Fatal("Should return error, type is unknown ! ")
+	}
+	cnamerecs, err = clExpired.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	if err == nil {
+		t.Fatal("Should return error on failed auth ! ")
+	}
+	clExpired.(*piholeClient).token = "correct"
+	clExpired.(*piholeClient).cfg.Password = "correct"
+	cnamerecs, err = clExpired.listRecords(context.Background(), endpoint.RecordTypeCNAME)
+	if len(cnamerecs) != 0 {
+		t.Fatal("Should return empty on missing data in response ! ")
+	}
+
 }
 
 func TestCreateRecord(t *testing.T) {
@@ -317,7 +383,8 @@ func TestCreateRecord(t *testing.T) {
 
 	// Create a client
 	cfg := PiholeConfig{
-		Server: srvr.URL,
+		Server:       srvr.URL,
+		DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
 	}
 	cl, err := newPiholeClient(cfg)
 	if err != nil {
@@ -360,9 +427,51 @@ func TestCreateRecord(t *testing.T) {
 		Targets:    []string{"192.168.1.1"},
 		RecordType: endpoint.RecordTypeA,
 	}
+	cl.(*piholeClient).token = "correct"
 	if err := cl.createRecord(context.Background(), ep); err == nil {
 		t.Fatal(err)
 	}
+
+	// Skip not matching domain
+	ep = &endpoint.Endpoint{
+		DNSName:    "foo.bar.com",
+		Targets:    []string{"192.168.1.1"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	if err := cl.createRecord(context.Background(), ep); err != nil {
+		t.Fatal("Should not return error on non filtered domain")
+	}
+
+	// Not supported type
+	ep = &endpoint.Endpoint{
+		DNSName:    "test.example.com",
+		Targets:    []string{"192.168.1.1"},
+		RecordType: "not a type",
+	}
+	if err := cl.createRecord(context.Background(), ep); err != nil {
+		t.Fatal("Should not return error on unsupported type")
+	}
+
+	// Create a client
+	cfgDr := PiholeConfig{
+		Server:       srvr.URL,
+		DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
+		DryRun:       true,
+	}
+	clDr, err := newPiholeClient(cfgDr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Skip Dry Run
+	ep = &endpoint.Endpoint{
+		DNSName:    "test.example.com",
+		Targets:    []string{"192.168.1.1"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	if err := clDr.createRecord(context.Background(), ep); err != nil {
+		t.Fatal("Should not return error on dry run")
+	}
+
 }
 
 func TestDeleteRecord(t *testing.T) {
diff --git a/provider/pihole/piholeV6_test.go b/provider/pihole/piholeV6_test.go
new file mode 100644
index 00000000..22e0a200
--- /dev/null
+++ b/provider/pihole/piholeV6_test.go
@@ -0,0 +1,434 @@
+/*
+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 pihole
+
+import (
+	"context"
+	"errors"
+	"reflect"
+	"testing"
+
+	"sigs.k8s.io/external-dns/endpoint"
+	"sigs.k8s.io/external-dns/plan"
+)
+
+type testPiholeClientV6 struct {
+	endpoints []*endpoint.Endpoint
+	requests  *requestTrackerV6
+	trigger   string
+}
+
+func (t *testPiholeClientV6) listRecords(_ context.Context, rtype string) ([]*endpoint.Endpoint, error) {
+	out := make([]*endpoint.Endpoint, 0)
+	if t.trigger == "AERROR" {
+		return nil, errors.New("AERROR")
+	}
+	if t.trigger == "AAAAERROR" {
+		return nil, errors.New("AAAAERROR")
+	}
+	if t.trigger == "CNAMEERROR" {
+		return nil, errors.New("CNAMEERROR")
+	}
+	for _, ep := range t.endpoints {
+		if ep.RecordType == rtype {
+			out = append(out, ep)
+		}
+	}
+	return out, nil
+}
+
+func (t *testPiholeClientV6) createRecord(_ context.Context, ep *endpoint.Endpoint) error {
+	t.endpoints = append(t.endpoints, ep)
+	t.requests.createRequests = append(t.requests.createRequests, ep)
+	return nil
+}
+
+func (t *testPiholeClientV6) deleteRecord(_ context.Context, ep *endpoint.Endpoint) error {
+	newEPs := make([]*endpoint.Endpoint, 0)
+	for _, existing := range t.endpoints {
+		if existing.DNSName != ep.DNSName && existing.Targets[0] != ep.Targets[0] {
+			newEPs = append(newEPs, existing)
+		}
+	}
+	t.endpoints = newEPs
+	t.requests.deleteRequests = append(t.requests.deleteRequests, ep)
+	return nil
+}
+
+type requestTrackerV6 struct {
+	createRequests []*endpoint.Endpoint
+	deleteRequests []*endpoint.Endpoint
+}
+
+func (r *requestTrackerV6) clear() {
+	r.createRequests = nil
+	r.deleteRequests = nil
+}
+
+func TestErrorHandling(t *testing.T) {
+	requests := requestTrackerV6{}
+	p := &PiholeProvider{
+		api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests},
+	}
+
+	p.api.(*testPiholeClientV6).trigger = "AERROR"
+	_, err := p.Records(context.Background())
+	if err.Error() != "AERROR" {
+		t.Fatal(err)
+	}
+
+	p.api.(*testPiholeClientV6).trigger = "AAAAERROR"
+	_, err = p.Records(context.Background())
+	if err.Error() != "AAAAERROR" {
+		t.Fatal(err)
+	}
+
+	p.api.(*testPiholeClientV6).trigger = "CNAMEERROR"
+	_, err = p.Records(context.Background())
+	if err.Error() != "CNAMEERROR" {
+		t.Fatal(err)
+	}
+
+}
+
+func TestNewPiholeProviderV6(t *testing.T) {
+	// Test invalid configuration
+	_, err := NewPiholeProvider(PiholeConfig{APIVersion: "7"})
+	if err == nil {
+		t.Error("Expected error from invalid configuration")
+	}
+	// Test valid configuration
+	_, err = NewPiholeProvider(PiholeConfig{Server: "test.example.com", APIVersion: "6"})
+	if err != nil {
+		t.Error("Expected no error from valid configuration, got:", err)
+	}
+}
+
+func TestProviderV6(t *testing.T) {
+	requests := requestTrackerV6{}
+	p := &PiholeProvider{
+		api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests},
+	}
+
+	records, err := p.Records(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(records) != 0 {
+		t.Fatal("Expected empty list of records, got:", records)
+	}
+
+	// Populate the provider with records
+	records = []*endpoint.Endpoint{
+		{
+			DNSName:    "test1.example.com",
+			Targets:    []string{"192.168.1.1"},
+			RecordType: endpoint.RecordTypeA,
+		},
+		{
+			DNSName:    "test2.example.com",
+			Targets:    []string{"192.168.1.2"},
+			RecordType: endpoint.RecordTypeA,
+		},
+		{
+			DNSName:    "test3.example.com",
+			Targets:    []string{"192.168.1.3"},
+			RecordType: endpoint.RecordTypeA,
+		},
+		{
+			DNSName:    "test1.example.com",
+			Targets:    []string{"fc00::1:192:168:1:1"},
+			RecordType: endpoint.RecordTypeAAAA,
+		},
+		{
+			DNSName:    "test2.example.com",
+			Targets:    []string{"fc00::1:192:168:1:2"},
+			RecordType: endpoint.RecordTypeAAAA,
+		},
+		{
+			DNSName:    "test3.example.com",
+			Targets:    []string{"fc00::1:192:168:1:3"},
+			RecordType: endpoint.RecordTypeAAAA,
+		},
+	}
+	if err := p.ApplyChanges(context.Background(), &plan.Changes{
+		Create: records,
+	}); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test records are correct on retrieval
+
+	newRecords, err := p.Records(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(newRecords) != 6 {
+		t.Fatal("Expected list of 6 records, got:", records)
+	}
+	if len(requests.createRequests) != 6 {
+		t.Fatal("Expected 6 create requests, got:", requests.createRequests)
+	}
+	if len(requests.deleteRequests) != 0 {
+		t.Fatal("Expected no delete requests, got:", requests.deleteRequests)
+	}
+
+	for idx, record := range records {
+		if newRecords[idx].DNSName != record.DNSName {
+			t.Error("DNS Name malformed on retrieval, got:", newRecords[idx].DNSName, "expected:", record.DNSName)
+		}
+		if newRecords[idx].Targets[0] != record.Targets[0] {
+			t.Error("Targets malformed on retrieval, got:", newRecords[idx].Targets, "expected:", record.Targets)
+		}
+
+		if !reflect.DeepEqual(requests.createRequests[idx], record) {
+			t.Error("Unexpected create request, got:", newRecords[idx].DNSName, "expected:", record.DNSName)
+		}
+	}
+
+	requests.clear()
+
+	// Test delete a record
+
+	records = []*endpoint.Endpoint{
+		{
+			DNSName:    "test1.example.com",
+			Targets:    []string{"192.168.1.1"},
+			RecordType: endpoint.RecordTypeA,
+		},
+		{
+			DNSName:    "test2.example.com",
+			Targets:    []string{"192.168.1.2"},
+			RecordType: endpoint.RecordTypeA,
+		},
+		{
+			DNSName:    "test1.example.com",
+			Targets:    []string{"fc00::1:192:168:1:1"},
+			RecordType: endpoint.RecordTypeAAAA,
+		},
+		{
+			DNSName:    "test2.example.com",
+			Targets:    []string{"fc00::1:192:168:1:2"},
+			RecordType: endpoint.RecordTypeAAAA,
+		},
+	}
+	recordToDeleteA := endpoint.Endpoint{
+		DNSName:    "test3.example.com",
+		Targets:    []string{"192.168.1.3"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	if err := p.ApplyChanges(context.Background(), &plan.Changes{
+		Delete: []*endpoint.Endpoint{
+			&recordToDeleteA,
+		},
+	}); err != nil {
+		t.Fatal(err)
+	}
+	recordToDeleteAAAA := endpoint.Endpoint{
+		DNSName:    "test3.example.com",
+		Targets:    []string{"fc00::1:192:168:1:3"},
+		RecordType: endpoint.RecordTypeAAAA,
+	}
+	if err := p.ApplyChanges(context.Background(), &plan.Changes{
+		Delete: []*endpoint.Endpoint{
+			&recordToDeleteAAAA,
+		},
+	}); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test records are updated
+	newRecords, err = p.Records(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(newRecords) != 4 {
+		t.Fatal("Expected list of 4 records, got:", records)
+	}
+	if len(requests.createRequests) != 0 {
+		t.Fatal("Expected no create requests, got:", requests.createRequests)
+	}
+	if len(requests.deleteRequests) != 2 {
+		t.Fatal("Expected 2 delete request, got:", requests.deleteRequests)
+	}
+
+	for idx, record := range records {
+		if newRecords[idx].DNSName != record.DNSName {
+			t.Error("DNS Name malformed on retrieval, got:", newRecords[idx].DNSName, "expected:", record.DNSName)
+		}
+		if newRecords[idx].Targets[0] != record.Targets[0] {
+			t.Error("Targets malformed on retrieval, got:", newRecords[idx].Targets, "expected:", record.Targets)
+		}
+	}
+
+	if !reflect.DeepEqual(requests.deleteRequests[0], &recordToDeleteA) {
+		t.Error("Unexpected delete request, got:", requests.deleteRequests[0], "expected:", recordToDeleteA)
+	}
+	if !reflect.DeepEqual(requests.deleteRequests[1], &recordToDeleteAAAA) {
+		t.Error("Unexpected delete request, got:", requests.deleteRequests[1], "expected:", recordToDeleteAAAA)
+	}
+
+	requests.clear()
+
+	// Test update a record
+
+	records = []*endpoint.Endpoint{
+		{
+			DNSName:    "test1.example.com",
+			Targets:    []string{"192.168.1.1"},
+			RecordType: endpoint.RecordTypeA,
+		},
+		{
+			DNSName:    "test2.example.com",
+			Targets:    []string{"10.0.0.1"},
+			RecordType: endpoint.RecordTypeA,
+		},
+		{
+			DNSName:    "test1.example.com",
+			Targets:    []string{"fc00::1:192:168:1:1"},
+			RecordType: endpoint.RecordTypeAAAA,
+		},
+		{
+			DNSName:    "test2.example.com",
+			Targets:    []string{"fc00::1:10:0:0:1"},
+			RecordType: endpoint.RecordTypeAAAA,
+		},
+	}
+	if err := p.ApplyChanges(context.Background(), &plan.Changes{
+		UpdateOld: []*endpoint.Endpoint{
+			{
+				DNSName:    "test1.example.com",
+				Targets:    []string{"192.168.1.1"},
+				RecordType: endpoint.RecordTypeA,
+			},
+			{
+				DNSName:    "test2.example.com",
+				Targets:    []string{"192.168.1.2"},
+				RecordType: endpoint.RecordTypeA,
+			},
+			{
+				DNSName:    "test1.example.com",
+				Targets:    []string{"fc00::1:192:168:1:1"},
+				RecordType: endpoint.RecordTypeAAAA,
+			},
+			{
+				DNSName:    "test2.example.com",
+				Targets:    []string{"fc00::1:192:168:1:2"},
+				RecordType: endpoint.RecordTypeAAAA,
+			},
+		},
+		UpdateNew: []*endpoint.Endpoint{
+			{
+				DNSName:    "test1.example.com",
+				Targets:    []string{"192.168.1.1"},
+				RecordType: endpoint.RecordTypeA,
+			},
+			{
+				DNSName:    "test2.example.com",
+				Targets:    []string{"10.0.0.1"},
+				RecordType: endpoint.RecordTypeA,
+			},
+			{
+				DNSName:    "test1.example.com",
+				Targets:    []string{"fc00::1:192:168:1:1"},
+				RecordType: endpoint.RecordTypeAAAA,
+			},
+			{
+				DNSName:    "test2.example.com",
+				Targets:    []string{"fc00::1:10:0:0:1"},
+				RecordType: endpoint.RecordTypeAAAA,
+			},
+		},
+	}); err != nil {
+		t.Fatal(err)
+	}
+
+	// Test records are updated
+	newRecords, err = p.Records(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(newRecords) != 4 {
+		t.Fatal("Expected list of 4 records, got:", newRecords)
+	}
+	if len(requests.createRequests) != 2 {
+		t.Fatal("Expected 2 create request, got:", requests.createRequests)
+	}
+	if len(requests.deleteRequests) != 2 {
+		t.Fatal("Expected 2 delete request, got:", requests.deleteRequests)
+	}
+
+	for idx, record := range records {
+		if newRecords[idx].DNSName != record.DNSName {
+			t.Error("DNS Name malformed on retrieval, got:", newRecords[idx].DNSName, "expected:", record.DNSName)
+		}
+		if newRecords[idx].Targets[0] != record.Targets[0] {
+			t.Error("Targets malformed on retrieval, got:", newRecords[idx].Targets, "expected:", record.Targets)
+		}
+	}
+
+	expectedCreateA := endpoint.Endpoint{
+		DNSName:    "test2.example.com",
+		Targets:    []string{"10.0.0.1"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	expectedDeleteA := endpoint.Endpoint{
+		DNSName:    "test2.example.com",
+		Targets:    []string{"192.168.1.2"},
+		RecordType: endpoint.RecordTypeA,
+	}
+	expectedCreateAAAA := endpoint.Endpoint{
+		DNSName:    "test2.example.com",
+		Targets:    []string{"fc00::1:10:0:0:1"},
+		RecordType: endpoint.RecordTypeAAAA,
+	}
+	expectedDeleteAAAA := endpoint.Endpoint{
+		DNSName:    "test2.example.com",
+		Targets:    []string{"fc00::1:192:168:1:2"},
+		RecordType: endpoint.RecordTypeAAAA,
+	}
+
+	for _, request := range requests.createRequests {
+		switch request.RecordType {
+		case endpoint.RecordTypeA:
+			if !reflect.DeepEqual(request, &expectedCreateA) {
+				t.Error("Unexpected create request, got:", request, "expected:", &expectedCreateA)
+			}
+		case endpoint.RecordTypeAAAA:
+			if !reflect.DeepEqual(request, &expectedCreateAAAA) {
+				t.Error("Unexpected create request, got:", request, "expected:", &expectedCreateAAAA)
+			}
+		default:
+		}
+	}
+
+	for _, request := range requests.deleteRequests {
+		switch request.RecordType {
+		case endpoint.RecordTypeA:
+			if !reflect.DeepEqual(request, &expectedDeleteA) {
+				t.Error("Unexpected delete request, got:", request, "expected:", &expectedDeleteA)
+			}
+		case endpoint.RecordTypeAAAA:
+			if !reflect.DeepEqual(request, &expectedDeleteAAAA) {
+				t.Error("Unexpected delete request, got:", request, "expected:", &expectedDeleteAAAA)
+			}
+		default:
+		}
+	}
+
+	requests.clear()
+}