Repository: kubernetes-sigs/cloud-provider-azure
Pull request #7688 (2024-11-26 02:59:43)

[release-1.31]refactor config in azure
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?
/kind cleanup

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

#### What this PR does / why we need it:
[release-1.31]refactor config in azure
#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

#### Special notes for your reviewer:
cherrypick of #7533
#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
NONE
```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:

<!--
This section can be blank if this pull request does not require a release note.

When adding links which point to resources within git repositories, like
KEPs or supporting documentation, please reference a specific commit and avoid
linking directly to the master branch. This ensures that links reference a
specific point in time, rather than a document that may change over time.

See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files

Please use the following format for linking documentation:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cloud-provider-azure/pull/7688#" title="Author self-approved">MartinForReal</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcloud-provider-azure).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/cloud-provider-azure/blob/release-1.31/OWNERS)~~ [MartinForReal]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/go.mod b/go.mod
index 20f604c48..adf8e4808 100644
--- a/go.mod
+++ b/go.mod
@@ -49,8 +49,8 @@ require (
 	k8s.io/klog/v2 v2.130.1
 	k8s.io/kubelet v0.31.3
 	k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
-	sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.1.20
-	sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.1.7
+	sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.1.21
+	sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.1.8
 	sigs.k8s.io/yaml v1.4.0
 )
 
diff --git a/go.sum b/go.sum
index 91a089241..1a3e30499 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 github.com/Azure/azure-kusto-go v0.16.1 h1:vCBWcQghmC1qIErUUgVNWHxGhZVStu1U/hki6iBA14k=
 github.com/Azure/azure-kusto-go v0.16.1/go.mod h1:9F2zvXH8B6eWzgI1S4k1ZXAIufnBZ1bv1cW1kB1n3D0=
 github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg=
@@ -79,8 +81,12 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8het
 github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
 github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
+github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
+github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -89,8 +95,12 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
 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/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
+github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
 github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
@@ -110,6 +120,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
 github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
+github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
 github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
 github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -118,6 +130,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
 github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -143,6 +157,7 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
 github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -165,6 +180,7 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
@@ -173,6 +189,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
+github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
 github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -181,8 +198,10 @@ github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9q
 github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
 github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -200,6 +219,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
 github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
 github.com/mattn/go-ieproxy v0.0.11 h1:MQ/5BuGSgDAHZOJe6YY80IF2UVCfGkwfo6AeD7HtHYo=
 github.com/mattn/go-ieproxy v0.0.11/go.mod h1:/NsJd+kxZBmjMc5hrJCKMbP57B84rvq9BiDRbtO9AS0=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
 github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
 github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -207,12 +228,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
 github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
 github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y=
 github.com/onsi/gomega v1.36.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -220,6 +245,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
 github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
 github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
@@ -230,6 +256,7 @@ 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/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
 github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -250,6 +277,7 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -263,6 +291,7 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7
 github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -306,6 +335,7 @@ go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQD
 go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
 go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
 go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
@@ -329,6 +359,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -366,6 +397,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
 golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -419,6 +451,7 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -442,6 +475,8 @@ k8s.io/component-helpers v0.31.3 h1:0zGPD2PrekhFWgmz85XxlMEl7dfhlKC1tERZDe3onQc=
 k8s.io/component-helpers v0.31.3/go.mod h1:HZ1HZx2TKXM7xSUV2cR9L5yDoyZPhhHQNaE3BPBLPUQ=
 k8s.io/controller-manager v0.31.3 h1:TyUav69iNYwLGwA96JDhusoZoGRdh1sdrLjXmWTcPgs=
 k8s.io/controller-manager v0.31.3/go.mod h1:yuhec+dbXmBz+4c32kxJxmcauB+1pjO2ttfYODWuv18=
+k8s.io/cri-api v0.31.3/go.mod h1:Po3TMAYH/+KrZabi7QiwQI4a692oZcUOUThd/rqwxrI=
+k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kms v0.32.0-alpha.0 h1:l253yOB/0UhS7iLAzAw7aqaRB4Fw6I2fJtNtag98JF0=
@@ -454,10 +489,10 @@ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1
 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
-sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.1.20 h1:eZAfeLlBujLvViWXN/r+b+Tg8XUdssePRWr7N8dG0qs=
-sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.1.20/go.mod h1:U1hh84S6bNTLk2RQAF7ZlmaXm1g9XTHYi2gXZxv2TfQ=
-sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.1.7 h1:svbrF0iYij+ip8o87/FkJC4GW5gPHKQG8AGCKZqSNp4=
-sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.1.7/go.mod h1:/Lw2NMdjFcIZESlZUSjBpcnzS+fY33uUoeo6+I6XyUI=
+sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.1.21 h1:HQk15tR9x2Vzeo+7kP66fN0zvI/0wjaD+/FHOfztng8=
+sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.1.21/go.mod h1:WSsOhr3Kr9BqIb88mgoEvcreu7vZujsQ31x3HXM+X6Y=
+sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.1.8 h1:fwg1J+nRRXkcA0lIJ/a4fJPg7tNggRH1pPOukthuAGw=
+sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.1.8/go.mod h1:kUW9qMqi412c5+nXdYA00oSudWC9nVUCM4v1k3HtgZo=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
 sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
diff --git a/pkg/credentialprovider/azure_credentials.go b/pkg/credentialprovider/azure_credentials.go
index 2ffdceee7..50cf7d57b 100644
--- a/pkg/credentialprovider/azure_credentials.go
+++ b/pkg/credentialprovider/azure_credentials.go
@@ -59,13 +59,13 @@ type CredentialProvider interface {
 
 // acrProvider implements the credential provider interface for Azure Container Registry.
 type acrProvider struct {
-	config         *providerconfig.AzureAuthConfig
+	config         *providerconfig.AzureClientConfig
 	environment    *azclient.Environment
 	credential     azcore.TokenCredential
 	registryMirror map[string]string // Registry mirror relation: source registry -> target registry
 }
 
-func NewAcrProvider(config *providerconfig.AzureAuthConfig, environment *azclient.Environment, credential azcore.TokenCredential) CredentialProvider {
+func NewAcrProvider(config *providerconfig.AzureClientConfig, environment *azclient.Environment, credential azcore.TokenCredential) CredentialProvider {
 	return &acrProvider{
 		config:      config,
 		credential:  credential,
@@ -78,7 +78,7 @@ func NewAcrProviderFromConfig(configFile string, registryMirrorStr string) (Cred
 	if len(configFile) == 0 {
 		return nil, errors.New("no azure credential file is provided")
 	}
-	config, err := configloader.Load[providerconfig.AzureAuthConfig](context.Background(), nil, &configloader.FileLoaderConfig{FilePath: configFile})
+	config, err := configloader.Load[providerconfig.AzureClientConfig](context.Background(), nil, &configloader.FileLoaderConfig{FilePath: configFile})
 	if err != nil {
 		return nil, fmt.Errorf("failed to load config: %w", err)
 	}
diff --git a/pkg/provider/azure.go b/pkg/provider/azure.go
index b6c4ba090..1d3fe4af5 100644
--- a/pkg/provider/azure.go
+++ b/pkg/provider/azure.go
@@ -107,8 +107,7 @@ var (
 // See https://kubernetes.io/docs/reference/using-api/deprecation-policy/#deprecating-a-flag-or-cli
 // for more details.
 type Config struct {
-	ratelimitconfig.AzureAuthConfig              `json:",inline" yaml:",inline"`
-	ratelimitconfig.CloudProviderRateLimitConfig `json:",inline" yaml:",inline"`
+	ratelimitconfig.AzureClientConfig `json:",inline" yaml:",inline"`
 
 	// The cloud configure type for Azure cloud provider. Supported values are file, secret and merge.
 	CloudConfigType configloader.CloudConfigType `json:"cloudConfigType,omitempty" yaml:"cloudConfigType,omitempty"`
@@ -189,8 +188,6 @@ type Config struct {
 	// when setting AzureAuthConfig.Cloud with "AZURESTACKCLOUD" to customize ARM endpoints
 	// while the cluster is not running on AzureStack.
 	DisableAzureStackCloud bool `json:"disableAzureStackCloud,omitempty" yaml:"disableAzureStackCloud,omitempty"`
-	// Enable exponential backoff to manage resource request retries
-	CloudProviderBackoff bool `json:"cloudProviderBackoff,omitempty" yaml:"cloudProviderBackoff,omitempty"`
 	// Use instance metadata service where possible
 	UseInstanceMetadata bool `json:"useInstanceMetadata,omitempty" yaml:"useInstanceMetadata,omitempty"`
 
@@ -208,42 +205,7 @@ type Config struct {
 
 	// Maximum allowed LoadBalancer Rule Count is the limit enforced by Azure Load balancer
 	MaximumLoadBalancerRuleCount int `json:"maximumLoadBalancerRuleCount,omitempty" yaml:"maximumLoadBalancerRuleCount,omitempty"`
-	// Backoff retry limit
-	CloudProviderBackoffRetries int `json:"cloudProviderBackoffRetries,omitempty" yaml:"cloudProviderBackoffRetries,omitempty"`
-	// Backoff duration
-	CloudProviderBackoffDuration int `json:"cloudProviderBackoffDuration,omitempty" yaml:"cloudProviderBackoffDuration,omitempty"`
-	// NonVmssUniformNodesCacheTTLInSeconds sets the Cache TTL for NonVmssUniformNodesCacheTTLInSeconds
-	// if not set, will use default value
-	NonVmssUniformNodesCacheTTLInSeconds int `json:"nonVmssUniformNodesCacheTTLInSeconds,omitempty" yaml:"nonVmssUniformNodesCacheTTLInSeconds,omitempty"`
-	// VmssCacheTTLInSeconds sets the cache TTL for VMSS
-	VmssCacheTTLInSeconds int `json:"vmssCacheTTLInSeconds,omitempty" yaml:"vmssCacheTTLInSeconds,omitempty"`
-	// VmssVirtualMachinesCacheTTLInSeconds sets the cache TTL for vmssVirtualMachines
-	VmssVirtualMachinesCacheTTLInSeconds int `json:"vmssVirtualMachinesCacheTTLInSeconds,omitempty" yaml:"vmssVirtualMachinesCacheTTLInSeconds,omitempty"`
-
-	// VmssFlexCacheTTLInSeconds sets the cache TTL for VMSS Flex
-	VmssFlexCacheTTLInSeconds int `json:"vmssFlexCacheTTLInSeconds,omitempty" yaml:"vmssFlexCacheTTLInSeconds,omitempty"`
-	// VmssFlexVMCacheTTLInSeconds sets the cache TTL for vmss flex vms
-	VmssFlexVMCacheTTLInSeconds int `json:"vmssFlexVMCacheTTLInSeconds,omitempty" yaml:"vmssFlexVMCacheTTLInSeconds,omitempty"`
-
-	// VmCacheTTLInSeconds sets the cache TTL for vm
-	VMCacheTTLInSeconds int `json:"vmCacheTTLInSeconds,omitempty" yaml:"vmCacheTTLInSeconds,omitempty"`
-	// LoadBalancerCacheTTLInSeconds sets the cache TTL for load balancer
-	LoadBalancerCacheTTLInSeconds int `json:"loadBalancerCacheTTLInSeconds,omitempty" yaml:"loadBalancerCacheTTLInSeconds,omitempty"`
-	// NsgCacheTTLInSeconds sets the cache TTL for network security group
-	NsgCacheTTLInSeconds int `json:"nsgCacheTTLInSeconds,omitempty" yaml:"nsgCacheTTLInSeconds,omitempty"`
-	// RouteTableCacheTTLInSeconds sets the cache TTL for route table
-	RouteTableCacheTTLInSeconds int `json:"routeTableCacheTTLInSeconds,omitempty" yaml:"routeTableCacheTTLInSeconds,omitempty"`
-	// PlsCacheTTLInSeconds sets the cache TTL for private link service resource
-	PlsCacheTTLInSeconds int `json:"plsCacheTTLInSeconds,omitempty" yaml:"plsCacheTTLInSeconds,omitempty"`
-	// AvailabilitySetsCacheTTLInSeconds sets the cache TTL for VMAS
-	AvailabilitySetsCacheTTLInSeconds int `json:"availabilitySetsCacheTTLInSeconds,omitempty" yaml:"availabilitySetsCacheTTLInSeconds,omitempty"`
-	// PublicIPCacheTTLInSeconds sets the cache TTL for public ip
-	PublicIPCacheTTLInSeconds int `json:"publicIPCacheTTLInSeconds,omitempty" yaml:"publicIPCacheTTLInSeconds,omitempty"`
-	// RouteUpdateWaitingInSeconds is the delay time for waiting route updates to take effect. This waiting delay is added
-	// because the routes are not taken effect when the async route updating operation returns success. Default is 30 seconds.
-	RouteUpdateWaitingInSeconds int `json:"routeUpdateWaitingInSeconds,omitempty" yaml:"routeUpdateWaitingInSeconds,omitempty"`
-	// The user agent for Azure customer usage attribution
-	UserAgent string `json:"userAgent,omitempty" yaml:"userAgent,omitempty"`
+
 	// LoadBalancerBackendPoolConfigurationType defines how vms join the load balancer backend pools. Supported values
 	// are `nodeIPConfiguration`, `nodeIP` and `podIP`.
 	// `nodeIPConfiguration`: vm network interfaces will be attached to the inbound backend pool of the load balancer (default);
@@ -268,9 +230,6 @@ type Config struct {
 	// there must be one configuration named "<clustername>" or an error will be reported.
 	MultipleStandardLoadBalancerConfigurations []MultipleStandardLoadBalancerConfiguration `json:"multipleStandardLoadBalancerConfigurations,omitempty" yaml:"multipleStandardLoadBalancerConfigurations,omitempty"`
 
-	// DisableAPICallCache disables the cache for Azure API calls. It is for ARG support and not all resources will be disabled.
-	DisableAPICallCache bool `json:"disableAPICallCache,omitempty" yaml:"disableAPICallCache,omitempty"`
-
 	// RouteUpdateIntervalInSeconds is the interval for updating routes. Default is 30 seconds.
 	RouteUpdateIntervalInSeconds int `json:"routeUpdateIntervalInSeconds,omitempty" yaml:"routeUpdateIntervalInSeconds,omitempty"`
 	// LoadBalancerBackendPoolUpdateIntervalInSeconds is the interval for updating load balancer backend pool of local services. Default is 30 seconds.
@@ -671,7 +630,7 @@ func (az *Cloud) InitializeCloudFromConfig(ctx context.Context, config *Config,
 			return err
 		}
 	}
-	servicePrincipalToken, err := ratelimitconfig.GetServicePrincipalToken(&config.AzureAuthConfig, env, env.ServiceManagementEndpoint)
+	servicePrincipalToken, err := ratelimitconfig.GetServicePrincipalToken(&config.AzureClientConfig, env, env.ServiceManagementEndpoint)
 	if errors.Is(err, ratelimitconfig.ErrorNoAuth) {
 		// Only controller-manager would lazy-initialize from secret, and credentials are required for such case.
 		if fromSecret {
@@ -698,7 +657,7 @@ func (az *Cloud) InitializeCloudFromConfig(ctx context.Context, config *Config,
 	}
 
 	var authProvider *azclient.AuthProvider
-	authProvider, err = azclient.NewAuthProvider(&az.ARMClientConfig, &az.AzureAuthConfig.AzureAuthConfig)
+	authProvider, err = azclient.NewAuthProvider(&az.ARMClientConfig, &az.AzureClientConfig.AzureAuthConfig)
 	if err != nil {
 		return err
 	}
@@ -916,11 +875,11 @@ func (az *Cloud) getAuthTokenInMultiTenantEnv(_ *adal.ServicePrincipalToken, aut
 	var multiTenantOAuthToken adal.MultitenantOAuthTokenProvider
 	var networkResourceServicePrincipalToken adal.OAuthTokenProvider
 	if az.Config.UsesNetworkResourceInDifferentTenant() {
-		multiTenantOAuthToken, err = ratelimitconfig.GetMultiTenantServicePrincipalToken(&az.Config.AzureAuthConfig, &az.Environment, authProvider)
+		multiTenantOAuthToken, err = ratelimitconfig.GetMultiTenantServicePrincipalToken(&az.Config.AzureClientConfig, &az.Environment, authProvider)
 		if err != nil {
 			return nil, nil, err
 		}
-		networkResourceServicePrincipalToken, err = ratelimitconfig.GetNetworkResourceServicePrincipalToken(&az.Config.AzureAuthConfig, &az.Environment, authProvider)
+		networkResourceServicePrincipalToken, err = ratelimitconfig.GetNetworkResourceServicePrincipalToken(&az.Config.AzureClientConfig, &az.Environment, authProvider)
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/pkg/provider/azure_fakes.go b/pkg/provider/azure_fakes.go
index 0b1db9dfd..7aa9e2ba4 100644
--- a/pkg/provider/azure_fakes.go
+++ b/pkg/provider/azure_fakes.go
@@ -79,7 +79,7 @@ func NewTestFlexScaleSet(ctrl *gomock.Controller) (*FlexScaleSet, error) {
 func GetTestCloud(ctrl *gomock.Controller) (az *Cloud) {
 	az = &Cloud{
 		Config: Config{
-			AzureAuthConfig: config.AzureAuthConfig{
+			AzureClientConfig: config.AzureClientConfig{
 				ARMClientConfig: azclient.ARMClientConfig{
 					TenantID: "TenantID",
 				},
diff --git a/pkg/provider/config/azure_auth.go b/pkg/provider/config/azure_auth.go
index 84858ba97..8ad090e0c 100644
--- a/pkg/provider/config/azure_auth.go
+++ b/pkg/provider/config/azure_auth.go
@@ -47,10 +47,16 @@ const (
 	maxReadLength = 10 * 1 << 20 // 10MB
 )
 
-// AzureAuthConfig holds auth related part of cloud config
-type AzureAuthConfig struct {
-	azclient.ARMClientConfig `json:",inline" yaml:",inline"`
-	azclient.AzureAuthConfig `json:",inline" yaml:",inline"`
+// AzureClientConfig holds azure client related part of cloud config
+type AzureClientConfig struct {
+	azclient.ARMClientConfig     `json:",inline" yaml:",inline"`
+	azclient.AzureAuthConfig     `json:",inline" yaml:",inline"`
+	CloudProviderRateLimitConfig `json:",inline" yaml:",inline"`
+	CloudProviderCacheConfig     `json:",inline" yaml:",inline"`
+	// Backoff retry limit
+	CloudProviderBackoffRetries int `json:"cloudProviderBackoffRetries,omitempty" yaml:"cloudProviderBackoffRetries,omitempty"`
+	// Backoff duration
+	CloudProviderBackoffDuration int `json:"cloudProviderBackoffDuration,omitempty" yaml:"cloudProviderBackoffDuration,omitempty"`
 
 	// The ID of the Azure Subscription that the cluster is deployed in
 	SubscriptionID string `json:"subscriptionId,omitempty" yaml:"subscriptionId,omitempty"`
@@ -70,7 +76,7 @@ type AzureAuthConfig struct {
 // If NetworkResourceTenantID and NetworkResourceSubscriptionID are specified to have different values than TenantID and SubscriptionID, network resources are deployed in different AAD Tenant and Subscription than those for the cluster,
 // than only azure clients except VM/VMSS and network resource ones use this method to fetch Token.
 // For tokens for VM/VMSS and network resource ones, please check GetMultiTenantServicePrincipalToken and GetNetworkResourceServicePrincipalToken.
-func GetServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment, resource string) (*adal.ServicePrincipalToken, error) {
+func GetServicePrincipalToken(config *AzureClientConfig, env *azure.Environment, resource string) (*adal.ServicePrincipalToken, error) {
 	logger := klog.Background().WithName("GetServicePrincipalToken")
 	var tenantID string
 	if strings.EqualFold(config.IdentitySystem, consts.ADFSIdentitySystem) {
@@ -179,7 +185,7 @@ func GetServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment, r
 // PrimaryToken of the returned multi-tenant token is for the AAD Tenant specified by TenantID, and AuxiliaryToken of the returned multi-tenant token is for the AAD Tenant specified by NetworkResourceTenantID.
 //
 // Azure VM/VMSS clients use this multi-tenant token, in order to operate those VM/VMSS in AAD Tenant specified by TenantID, and meanwhile in their payload they are referencing network resources (e.g. Load Balancer, Network Security Group, etc.) in AAD Tenant specified by NetworkResourceTenantID.
-func GetMultiTenantServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment, authProvider *azclient.AuthProvider) (adal.MultitenantOAuthTokenProvider, error) {
+func GetMultiTenantServicePrincipalToken(config *AzureClientConfig, env *azure.Environment, authProvider *azclient.AuthProvider) (adal.MultitenantOAuthTokenProvider, error) {
 	logger := klog.Background().WithName("GetMultiTenantServicePrincipalToken")
 
 	err := config.ValidateForMultiTenant()
@@ -241,7 +247,7 @@ func GetMultiTenantServicePrincipalToken(config *AzureAuthConfig, env *azure.Env
 // and this method creates a new service principal token for network resources tenant based on the configuration.
 //
 // Azure network resource (Load Balancer, Public IP, Route Table, Network Security Group and their sub level resources) clients use this multi-tenant token, in order to operate resources in AAD Tenant specified by NetworkResourceTenantID.
-func GetNetworkResourceServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment, authProvider *azclient.AuthProvider) (adal.OAuthTokenProvider, error) {
+func GetNetworkResourceServicePrincipalToken(config *AzureClientConfig, env *azure.Environment, authProvider *azclient.AuthProvider) (adal.OAuthTokenProvider, error) {
 	logger := klog.Background().WithName("GetNetworkResourceServicePrincipalToken")
 
 	err := config.ValidateForMultiTenant()
@@ -320,8 +326,8 @@ func ParseAzureEnvironment(cloudName, resourceManagerEndpoint, identitySystem st
 }
 
 // ParseAzureAuthConfig returns a parsed configuration for an Azure cloudprovider config file
-func ParseAzureAuthConfig(configReader io.Reader) (*AzureAuthConfig, *azure.Environment, error) {
-	var config AzureAuthConfig
+func ParseAzureAuthConfig(configReader io.Reader) (*AzureClientConfig, *azure.Environment, error) {
+	var config AzureClientConfig
 
 	if configReader == nil {
 		return nil, nil, errors.New("nil config is provided")
@@ -351,14 +357,14 @@ func ParseAzureAuthConfig(configReader io.Reader) (*AzureAuthConfig, *azure.Envi
 // UsesNetworkResourceInDifferentTenant determines whether the AzureAuthConfig indicates to use network resources in
 // different AAD Tenant than those for the cluster. Return true when NetworkResourceTenantID is specified  and not equal
 // to one defined in global configs
-func (config *AzureAuthConfig) UsesNetworkResourceInDifferentTenant() bool {
+func (config *AzureClientConfig) UsesNetworkResourceInDifferentTenant() bool {
 	return len(config.NetworkResourceTenantID) > 0 && !strings.EqualFold(config.NetworkResourceTenantID, config.TenantID)
 }
 
 // UsesNetworkResourceInDifferentSubscription determines whether the AzureAuthConfig indicates to use network resources
 // in different Subscription than those for the cluster. Return true when NetworkResourceSubscriptionID is specified
 // and not equal to one defined in global configs
-func (config *AzureAuthConfig) UsesNetworkResourceInDifferentSubscription() bool {
+func (config *AzureClientConfig) UsesNetworkResourceInDifferentSubscription() bool {
 	return len(config.NetworkResourceSubscriptionID) > 0 && !strings.EqualFold(config.NetworkResourceSubscriptionID, config.SubscriptionID)
 }
 
@@ -375,7 +381,7 @@ func azureStackOverrides(env *azure.Environment, resourceManagerEndpoint, identi
 }
 
 // ValidateForMultiTenant checks configuration for the scenario of using network resource in different tenant
-func (config *AzureAuthConfig) ValidateForMultiTenant() error {
+func (config *AzureClientConfig) ValidateForMultiTenant() error {
 	if !config.UsesNetworkResourceInDifferentTenant() {
 		return fmt.Errorf("NetworkResourceTenantID must be configured")
 	}
diff --git a/pkg/provider/config/azure_cache.go b/pkg/provider/config/azure_cache.go
new file mode 100644
index 000000000..68d150d09
--- /dev/null
+++ b/pkg/provider/config/azure_cache.go
@@ -0,0 +1,52 @@
+/*
+Copyright 2020 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 config
+
+type CloudProviderCacheConfig struct {
+	// DisableAPICallCache disables the cache for Azure API calls. It is for ARG support and not all resources will be disabled.
+	DisableAPICallCache bool `json:"disableAPICallCache,omitempty" yaml:"disableAPICallCache,omitempty"`
+	// NonVmssUniformNodesCacheTTLInSeconds sets the Cache TTL for NonVmssUniformNodesCacheTTLInSeconds
+	// if not set, will use default value
+	NonVmssUniformNodesCacheTTLInSeconds int `json:"nonVmssUniformNodesCacheTTLInSeconds,omitempty" yaml:"nonVmssUniformNodesCacheTTLInSeconds,omitempty"`
+	// VmssCacheTTLInSeconds sets the cache TTL for VMSS
+	VmssCacheTTLInSeconds int `json:"vmssCacheTTLInSeconds,omitempty" yaml:"vmssCacheTTLInSeconds,omitempty"`
+	// VmssVirtualMachinesCacheTTLInSeconds sets the cache TTL for vmssVirtualMachines
+	VmssVirtualMachinesCacheTTLInSeconds int `json:"vmssVirtualMachinesCacheTTLInSeconds,omitempty" yaml:"vmssVirtualMachinesCacheTTLInSeconds,omitempty"`
+
+	// VmssFlexCacheTTLInSeconds sets the cache TTL for VMSS Flex
+	VmssFlexCacheTTLInSeconds int `json:"vmssFlexCacheTTLInSeconds,omitempty" yaml:"vmssFlexCacheTTLInSeconds,omitempty"`
+	// VmssFlexVMCacheTTLInSeconds sets the cache TTL for vmss flex vms
+	VmssFlexVMCacheTTLInSeconds int `json:"vmssFlexVMCacheTTLInSeconds,omitempty" yaml:"vmssFlexVMCacheTTLInSeconds,omitempty"`
+
+	// VmCacheTTLInSeconds sets the cache TTL for vm
+	VMCacheTTLInSeconds int `json:"vmCacheTTLInSeconds,omitempty" yaml:"vmCacheTTLInSeconds,omitempty"`
+	// LoadBalancerCacheTTLInSeconds sets the cache TTL for load balancer
+	LoadBalancerCacheTTLInSeconds int `json:"loadBalancerCacheTTLInSeconds,omitempty" yaml:"loadBalancerCacheTTLInSeconds,omitempty"`
+	// NsgCacheTTLInSeconds sets the cache TTL for network security group
+	NsgCacheTTLInSeconds int `json:"nsgCacheTTLInSeconds,omitempty" yaml:"nsgCacheTTLInSeconds,omitempty"`
+	// RouteTableCacheTTLInSeconds sets the cache TTL for route table
+	RouteTableCacheTTLInSeconds int `json:"routeTableCacheTTLInSeconds,omitempty" yaml:"routeTableCacheTTLInSeconds,omitempty"`
+	// PlsCacheTTLInSeconds sets the cache TTL for private link service resource
+	PlsCacheTTLInSeconds int `json:"plsCacheTTLInSeconds,omitempty" yaml:"plsCacheTTLInSeconds,omitempty"`
+	// AvailabilitySetsCacheTTLInSeconds sets the cache TTL for VMAS
+	AvailabilitySetsCacheTTLInSeconds int `json:"availabilitySetsCacheTTLInSeconds,omitempty" yaml:"availabilitySetsCacheTTLInSeconds,omitempty"`
+	// PublicIPCacheTTLInSeconds sets the cache TTL for public ip
+	PublicIPCacheTTLInSeconds int `json:"publicIPCacheTTLInSeconds,omitempty" yaml:"publicIPCacheTTLInSeconds,omitempty"`
+	// RouteUpdateWaitingInSeconds is the delay time for waiting route updates to take effect. This waiting delay is added
+	// because the routes are not taken effect when the async route updating operation returns success. Default is 30 seconds.
+	RouteUpdateWaitingInSeconds int `json:"routeUpdateWaitingInSeconds,omitempty" yaml:"routeUpdateWaitingInSeconds,omitempty"`
+}
diff --git a/pkg/util/collectionutil/map.go b/pkg/util/collectionutil/map.go
index 0d55042f5..d21484492 100644
--- a/pkg/util/collectionutil/map.go
+++ b/pkg/util/collectionutil/map.go
@@ -16,6 +16,14 @@ limitations under the License.
 
 package fnutil
 
+func Keys[K comparable, V any](m map[K]V) []K {
+	rv := make([]K, 0, len(m))
+	for k := range m {
+		rv = append(rv, k)
+	}
+	return rv
+}
+
 func Values[K comparable, V any](m map[K]V) []V {
 	rv := make([]V, 0, len(m))
 	for _, v := range m {
diff --git a/pkg/util/iputil/bits.go b/pkg/util/iputil/bits.go
new file mode 100644
index 000000000..dbad47343
--- /dev/null
+++ b/pkg/util/iputil/bits.go
@@ -0,0 +1,37 @@
+/*
+Copyright 2024 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 iputil
+
+// setBitAt sets the bit at the i-th position in the byte slice to the given value.
+// Panics if the index is out of bounds.
+// For example,
+// - setBitAt([0x00, 0x00], 8, 1) returns [0x00, 0b1000_0000].
+// - setBitAt([0xff, 0xff], 0, 0) returns [0b0111_1111, 0xff].
+func setBitAt(bytes []byte, i int, bit uint8) {
+	if bit == 1 {
+		bytes[i/8] |= 1 << (7 - i%8)
+	} else {
+		bytes[i/8] &^= 1 << (7 - i%8)
+	}
+}
+
+// bitAt returns the bit at the i-th position in the byte slice.
+// The return value is either 0 or 1 as uint8.
+// Panics if the index is out of bounds.
+func bitAt(bytes []byte, i int) uint8 {
+	return bytes[i/8] >> (7 - i%8) & 1
+}
diff --git a/pkg/util/iputil/prefix.go b/pkg/util/iputil/prefix.go
index 77395c37a..2c4a8e029 100644
--- a/pkg/util/iputil/prefix.go
+++ b/pkg/util/iputil/prefix.go
@@ -17,8 +17,10 @@ limitations under the License.
 package iputil
 
 import (
+	"bytes"
 	"fmt"
 	"net/netip"
+	"sort"
 )
 
 // IsPrefixesAllowAll returns true if one of the prefixes allows all addresses.
@@ -61,9 +63,108 @@ func GroupPrefixesByFamily(vs []netip.Prefix) ([]netip.Prefix, []netip.Prefix) {
 	return v4, v6
 }
 
-// AggregatePrefixes aggregates prefixes.
-// Overlapping prefixes are merged.
+// ContainsPrefix checks if prefix p fully contains prefix o.
+// It returns true if o is a subset of p, meaning all addresses in o are also in p.
+// This is true when p overlaps with o and p has fewer or equal number of bits than o.
+func ContainsPrefix(p netip.Prefix, o netip.Prefix) bool {
+	return p.Bits() <= o.Bits() && p.Overlaps(o)
+}
+
+// mergeAdjacentPrefixes attempts to merge two adjacent prefixes into a single prefix.
+// It returns the merged prefix and a boolean indicating success.
+// Note: This function only merges adjacent prefixes, not overlapping ones.
+func mergeAdjacentPrefixes(p1, p2 netip.Prefix) (netip.Prefix, bool) {
+	// Merge neighboring prefixes if possible
+	if p1.Bits() != p2.Bits() || p1.Bits() == 0 {
+		return netip.Prefix{}, false
+	}
+
+	var (
+		bits    = p1.Bits()
+		p1Bytes = p1.Addr().AsSlice()
+		p2Bytes = p2.Addr().AsSlice()
+	)
+	if bitAt(p1Bytes, bits-1) == 0 {
+		setBitAt(p1Bytes, bits-1, 1)
+	} else {
+		setBitAt(p2Bytes, bits-1, 1)
+	}
+	if !bytes.Equal(p1Bytes, p2Bytes) {
+		return netip.Prefix{}, false
+	}
+
+	rv, _ := p1.Addr().Prefix(bits - 1)
+	return rv, true
+}
+
+// aggregatePrefixesForSingleIPFamily merges overlapping or adjacent prefixes into a single prefix.
+// The input prefixes must be the same IP family (IPv4 or IPv6).
+// For example,
+// - [192.168.0.0/32, 192.168.0.1/32] -> [192.168.0.0/31] (adjacent)
+// - [192.168.0.0/24, 192.168.0.1/32] -> [192.168.1.0/24] (overlapping)
+func aggregatePrefixesForSingleIPFamily(prefixes []netip.Prefix) []netip.Prefix {
+	if len(prefixes) <= 1 {
+		return prefixes
+	}
+
+	sort.Slice(prefixes, func(i, j int) bool {
+		addrCmp := prefixes[i].Addr().Compare(prefixes[j].Addr())
+		if addrCmp == 0 {
+			return prefixes[i].Bits() < prefixes[j].Bits()
+		}
+		return addrCmp < 0
+	})
+
+	var rv = []netip.Prefix{prefixes[0]}
+
+	for i := 1; i < len(prefixes); i++ {
+		last, p := rv[len(rv)-1], prefixes[i]
+		if ContainsPrefix(last, p) {
+			// Skip overlapping prefixes
+			continue
+		}
+		rv = append(rv, p)
+
+		// Merge adjacent prefixes if possible
+		for len(rv) >= 2 {
+			// Merge the last two prefixes if they are adjacent
+			p, ok := mergeAdjacentPrefixes(rv[len(rv)-2], rv[len(rv)-1])
+			if !ok {
+				break
+			}
+
+			// Replace the last two prefixes with the merged prefix
+			rv = rv[:len(rv)-2]
+			rv = append(rv, p)
+		}
+	}
+	return rv
+}
+
+// AggregatePrefixes merges overlapping or adjacent prefixes into a single prefix.
+// It combines prefixes that can be represented by a larger, more inclusive prefix.
+//
+// Examples:
+//   - Adjacent:    [192.168.0.0/32, 192.168.0.1/32] -> [192.168.0.0/31]
+//   - Overlapping: [192.168.0.0/24, 192.168.0.1/32] -> [192.168.0.0/24]
 func AggregatePrefixes(prefixes []netip.Prefix) []netip.Prefix {
+	var (
+		v4, v6 = GroupPrefixesByFamily(prefixes)
+	)
+
+	return append(aggregatePrefixesForSingleIPFamily(v4), aggregatePrefixesForSingleIPFamily(v6)...)
+}
+
+// AggregatePrefixesWithPrefixTree merges overlapping or adjacent prefixes into a single prefix.
+//
+// This function uses a prefix tree to aggregate the input prefixes. While it achieves
+// the same result as AggregatePrefixes, it is less efficient. For better performance,
+// use AggregatePrefixes instead.
+//
+// Examples:
+//   - Adjacent:   [192.168.0.0/32, 192.168.0.1/32] -> [192.168.0.0/31]
+//   - Overlapping: [192.168.0.0/24, 192.168.0.1/32] -> [192.168.0.0/24]
+func AggregatePrefixesWithPrefixTree(prefixes []netip.Prefix) []netip.Prefix {
 	var (
 		v4, v6 = GroupPrefixesByFamily(prefixes)
 		v4Tree = newPrefixTreeForIPv4()
diff --git a/pkg/util/iputil/prefix_tree.go b/pkg/util/iputil/prefix_tree.go
index 2399c09eb..a7515b42f 100644
--- a/pkg/util/iputil/prefix_tree.go
+++ b/pkg/util/iputil/prefix_tree.go
@@ -29,10 +29,26 @@ type prefixTreeNode struct {
 	r *prefixTreeNode // right child node
 }
 
-// pruneToRoot prunes the tree to the root.
-// If a node's left and right children are both masked,
-// it is masked and its children are pruned.
-// This is done recursively up to the root.
+// pruneToRoot  checks if the current node and its sibling are masked,
+// and if so, marks their parent as masked and removes both children.
+// This process is repeated up the tree until a node with an unmasked sibling is found.
+//
+// The process can be visualized as follows:
+//
+//	Before:           After:
+//	   P                 P (masked)
+//	  / \               / \
+//	 A   B     ->      X   X
+//	(M) (M)
+//
+// Where:
+//
+//	P: Parent node
+//	A, B: Child nodes
+//	M: Masked
+//	X: Removed
+//
+// This method helps to optimize the tree structure by condensing fully masked subtrees.
 func (n *prefixTreeNode) pruneToRoot() {
 	var node = n
 	for node.p != nil {
@@ -49,6 +65,31 @@ func (n *prefixTreeNode) pruneToRoot() {
 	}
 }
 
+// prefixTree represents a tree structure for storing and managing IP prefixes.
+// It efficiently handles prefix aggregation, merging of overlapping prefixes,
+// and collapsing of neighboring prefixes.
+//
+// The tree is structured as follows:
+// - Each node represents a bit in the IP address
+// - Left child represents a 0 bit, right child represents a 1 bit
+// - Masked nodes indicate the end of a prefix
+// - Unused branches are represented by nil pointers
+//
+// Example tree for 128.0.0.0/4 (binary 1000 0000):
+//
+//	    0 (0.0.0.0/0)
+//	   / \
+//	  X   1 (128.0.0.0/1)
+//	     / \
+//	    0   X
+//	   / \
+//	  0   X
+//	 / \
+//	0*  X
+//
+// Where:
+// * denotes a masked node (prefix end)
+// X denotes an unused branch (nil pointer)
 type prefixTree struct {
 	maxBits int
 	root    *prefixTreeNode
diff --git a/vendor/modules.txt b/vendor/modules.txt
index c4fb89255..9b32823ff 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1534,7 +1534,7 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/client
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/client/metrics
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/common/metrics
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client/proto/client
-# sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.1.20
+# sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.1.21
 ## explicit; go 1.23.1
 sigs.k8s.io/cloud-provider-azure/pkg/azclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/accountclient
@@ -1589,7 +1589,7 @@ sigs.k8s.io/cloud-provider-azure/pkg/azclient/virtualmachinescalesetvmclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/virtualnetworkclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/virtualnetworklinkclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/virtualnetworklinkclient/mock_virtualnetworklinkclient
-# sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.1.7
+# sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader v0.1.8
 ## explicit; go 1.23.1
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader
 # sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd


Test patch:
diff --git a/pkg/credentialprovider/azure_credentials_test.go b/pkg/credentialprovider/azure_credentials_test.go
index 2dd8d6918..887834c27 100644
--- a/pkg/credentialprovider/azure_credentials_test.go
+++ b/pkg/credentialprovider/azure_credentials_test.go
@@ -45,7 +45,7 @@ func TestGetCredentials(t *testing.T) {
 		"*.azurecr.us",
 	}
 
-	provider := NewAcrProvider(&config.AzureAuthConfig{
+	provider := NewAcrProvider(&config.AzureClientConfig{
 		AzureAuthConfig: azclient.AzureAuthConfig{
 			AADClientID:     "foo",
 			AADClientSecret: "bar",
@@ -213,7 +213,7 @@ func TestProcessImageWithMirrorMapping(t *testing.T) {
 
 func TestParseACRLoginServerFromImage(t *testing.T) {
 
-	providerInterface := NewAcrProvider(&config.AzureAuthConfig{
+	providerInterface := NewAcrProvider(&config.AzureClientConfig{
 		AzureAuthConfig: azclient.AzureAuthConfig{
 			AADClientID:     "foo",
 			AADClientSecret: "bar",
diff --git a/pkg/provider/azure_test.go b/pkg/provider/azure_test.go
index 69ed7018c..88cb4f8a2 100644
--- a/pkg/provider/azure_test.go
+++ b/pkg/provider/azure_test.go
@@ -2796,7 +2796,7 @@ func TestInitializeCloudFromConfig(t *testing.T) {
 	assert.Equal(t, expectedErr, err)
 
 	config = Config{
-		AzureAuthConfig: providerconfig.AzureAuthConfig{
+		AzureClientConfig: providerconfig.AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				Cloud: "AZUREPUBLICCLOUD",
 			},
diff --git a/pkg/provider/config/azure_auth_test.go b/pkg/provider/config/azure_auth_test.go
index 7d1e3ace9..bd4676a47 100644
--- a/pkg/provider/config/azure_auth_test.go
+++ b/pkg/provider/config/azure_auth_test.go
@@ -35,7 +35,7 @@ import (
 )
 
 var (
-	CrossTenantNetworkResourceNegativeConfig = []*AzureAuthConfig{
+	CrossTenantNetworkResourceNegativeConfig = []*AzureClientConfig{
 		{
 			// missing NetworkResourceTenantID
 			ARMClientConfig: azclient.ARMClientConfig{
@@ -105,12 +105,12 @@ func TestGetServicePrincipalToken(t *testing.T) {
 		tests := []struct {
 			Name   string
 			Type   IDType
-			Config *AzureAuthConfig
+			Config *AzureClientConfig
 		}{
 			{
 				Name: "client id",
 				Type: ClientID,
-				Config: &AzureAuthConfig{
+				Config: &AzureClientConfig{
 					AzureAuthConfig: azclient.AzureAuthConfig{
 						UseManagedIdentityExtension: true,
 						UserAssignedIdentityID:      "00000000-0000-0000-0000-000000000000",
@@ -120,7 +120,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 			{
 				Name: "client id with SP (SP config should be ignored)",
 				Type: ClientID,
-				Config: &AzureAuthConfig{
+				Config: &AzureClientConfig{
 					ARMClientConfig: azclient.ARMClientConfig{
 						TenantID: "TenantID",
 					},
@@ -135,7 +135,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 			{
 				Name: "resource id",
 				Type: ResourceID,
-				Config: &AzureAuthConfig{
+				Config: &AzureClientConfig{
 					AzureAuthConfig: azclient.AzureAuthConfig{
 						UseManagedIdentityExtension: true,
 						UserAssignedIdentityID:      "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ua",
@@ -145,7 +145,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 			{
 				Name: "resource id with SP (SP config should be ignored)",
 				Type: ResourceID,
-				Config: &AzureAuthConfig{
+				Config: &AzureClientConfig{
 					ARMClientConfig: azclient.ARMClientConfig{
 						TenantID: "TenantID",
 					},
@@ -189,11 +189,11 @@ func TestGetServicePrincipalToken(t *testing.T) {
 	t.Run("setup with MSI (system managed identity)", func(t *testing.T) {
 		tests := []struct {
 			Name   string
-			Config *AzureAuthConfig
+			Config *AzureClientConfig
 		}{
 			{
 				Name: "default",
-				Config: &AzureAuthConfig{
+				Config: &AzureClientConfig{
 					AzureAuthConfig: azclient.AzureAuthConfig{
 						UseManagedIdentityExtension: true,
 					},
@@ -201,7 +201,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 			},
 			{
 				Name: "with SP (SP config should be ignored",
-				Config: &AzureAuthConfig{
+				Config: &AzureClientConfig{
 					ARMClientConfig: azclient.ARMClientConfig{
 						TenantID: "TenantID",
 					},
@@ -234,7 +234,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with workload identity", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID: "TenantID",
 			},
@@ -270,7 +270,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with SP with password", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID: "TenantID",
 			},
@@ -294,7 +294,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with SP with certificate", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID: "TenantID",
 			},
@@ -320,7 +320,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with SP with certificate has no password", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID: "TenantID",
 			},
@@ -345,7 +345,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with SP with certificate has multi public key", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID: "TenantID",
 			},
@@ -372,7 +372,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with SP with certificate has no public key", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID: "TenantID",
 			},
@@ -389,7 +389,7 @@ func TestGetServicePrincipalToken(t *testing.T) {
 
 func TestGetMultiTenantServicePrincipalToken(t *testing.T) {
 	t.Run("setup with SP with password", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID:                "TenantID",
 				NetworkResourceTenantID: "NetworkResourceTenantID",
@@ -415,7 +415,7 @@ func TestGetMultiTenantServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with SP with certificate", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID:                "TenantID",
 				NetworkResourceTenantID: "NetworkResourceTenantID",
@@ -445,7 +445,7 @@ func TestGetMultiTenantServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with SP with certificate has no public key", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID:                "TenantID",
 				NetworkResourceTenantID: "NetworkResourceTenantID",
@@ -467,7 +467,7 @@ func TestGetMultiTenantServicePrincipalToken(t *testing.T) {
 			networkToken         = "network-token"
 		)
 		var (
-			cfg = &AzureAuthConfig{
+			cfg = &AzureClientConfig{
 				AzureAuthConfig: azclient.AzureAuthConfig{
 					UseManagedIdentityExtension: true,
 				},
@@ -504,7 +504,7 @@ func TestGetMultiTenantServicePrincipalToken(t *testing.T) {
 func TestGetNetworkResourceServicePrincipalToken(t *testing.T) {
 
 	t.Run("setup with SP with password", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID:                "TenantID",
 				NetworkResourceTenantID: "NetworkResourceTenantID",
@@ -530,7 +530,7 @@ func TestGetNetworkResourceServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with SP with certificate", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID:                "TenantID",
 				NetworkResourceTenantID: "NetworkResourceTenantID",
@@ -560,7 +560,7 @@ func TestGetNetworkResourceServicePrincipalToken(t *testing.T) {
 	})
 
 	t.Run("setup with SP with certificate has no public key", func(t *testing.T) {
-		config := &AzureAuthConfig{
+		config := &AzureClientConfig{
 			ARMClientConfig: azclient.ARMClientConfig{
 				TenantID:                "TenantID",
 				NetworkResourceTenantID: "NetworkResourceTenantID",
@@ -583,7 +583,7 @@ func TestGetNetworkResourceServicePrincipalToken(t *testing.T) {
 			networkToken         = "network-token"
 		)
 		var (
-			cfg = &AzureAuthConfig{
+			cfg = &AzureClientConfig{
 				AzureAuthConfig: azclient.AzureAuthConfig{
 					UseManagedIdentityExtension: true,
 				},
@@ -681,7 +681,7 @@ func TestAzureStackOverrides(t *testing.T) {
 }
 
 func TestUsesNetworkResourceInDifferentTenant(t *testing.T) {
-	config := &AzureAuthConfig{
+	config := &AzureClientConfig{
 		ARMClientConfig: azclient.ARMClientConfig{
 			TenantID:                "TenantID",
 			NetworkResourceTenantID: "NetworkResourceTenantID",
diff --git a/pkg/util/iputil/bits_test.go b/pkg/util/iputil/bits_test.go
new file mode 100644
index 000000000..351e83eb8
--- /dev/null
+++ b/pkg/util/iputil/bits_test.go
@@ -0,0 +1,103 @@
+/*
+Copyright 2024 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 iputil
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_bitAt(t *testing.T) {
+	bytes := []byte{0b1010_1010, 0b0101_0101}
+	assert.Equal(t, uint8(1), bitAt(bytes, 0))
+	assert.Equal(t, uint8(0), bitAt(bytes, 1))
+	assert.Equal(t, uint8(1), bitAt(bytes, 2))
+	assert.Equal(t, uint8(0), bitAt(bytes, 3))
+
+	assert.Equal(t, uint8(1), bitAt(bytes, 4))
+	assert.Equal(t, uint8(0), bitAt(bytes, 5))
+	assert.Equal(t, uint8(1), bitAt(bytes, 6))
+	assert.Equal(t, uint8(0), bitAt(bytes, 7))
+
+	assert.Equal(t, uint8(0), bitAt(bytes, 8))
+	assert.Equal(t, uint8(1), bitAt(bytes, 9))
+	assert.Equal(t, uint8(0), bitAt(bytes, 10))
+	assert.Equal(t, uint8(1), bitAt(bytes, 11))
+
+	assert.Equal(t, uint8(0), bitAt(bytes, 12))
+	assert.Equal(t, uint8(1), bitAt(bytes, 13))
+	assert.Equal(t, uint8(0), bitAt(bytes, 14))
+	assert.Equal(t, uint8(1), bitAt(bytes, 15))
+
+	assert.Panics(t, func() { bitAt(bytes, 16) })
+}
+
+func Test_setBitAt(t *testing.T) {
+	tests := []struct {
+		name     string
+		initial  []byte
+		index    int
+		bit      uint8
+		expected []byte
+	}{
+		{
+			name:     "Set first bit to 1",
+			initial:  []byte{0b0000_0000},
+			index:    0,
+			bit:      1,
+			expected: []byte{0b1000_0000},
+		},
+		{
+			name:     "Set last bit to 1",
+			initial:  []byte{0b0000_0000},
+			index:    7,
+			bit:      1,
+			expected: []byte{0b0000_0001},
+		},
+		{
+			name:     "Set middle bit to 1",
+			initial:  []byte{0b0000_0000},
+			index:    4,
+			bit:      1,
+			expected: []byte{0b0000_1000},
+		},
+		{
+			name:     "Set bit to 0",
+			initial:  []byte{0b1111_1111},
+			index:    3,
+			bit:      0,
+			expected: []byte{0b1110_1111},
+		},
+		{
+			name:     "Set bit in second byte",
+			initial:  []byte{0b0000_0000, 0b0000_0000},
+			index:    9,
+			bit:      1,
+			expected: []byte{0b0000_0000, 0b0100_0000},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			setBitAt(tt.initial, tt.index, tt.bit)
+			assert.Equal(t, tt.expected, tt.initial)
+		})
+	}
+
+	assert.Panics(t, func() { setBitAt([]byte{0x00}, 8, 1) })
+}
diff --git a/pkg/util/iputil/prefix_test.go b/pkg/util/iputil/prefix_test.go
index 30661c4a4..0875e5eb0 100644
--- a/pkg/util/iputil/prefix_test.go
+++ b/pkg/util/iputil/prefix_test.go
@@ -23,6 +23,8 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+
+	fnutil "sigs.k8s.io/cloud-provider-azure/pkg/util/collectionutil"
 )
 
 func TestIsPrefixesAllowAll(t *testing.T) {
@@ -266,80 +268,250 @@ func TestAggregatePrefixes(t *testing.T) {
 				return tt.Output[i].String() < tt.Output[j].String()
 			})
 			assert.Equal(t, tt.Output, got)
+
+			{
+				// Test the prefix tree implementation
+				var got = AggregatePrefixesWithPrefixTree(tt.Input)
+
+				sort.Slice(got, func(i, j int) bool {
+					return got[i].String() < got[j].String()
+				})
+				sort.Slice(tt.Output, func(i, j int) bool {
+					return tt.Output[i].String() < tt.Output[j].String()
+				})
+				assert.Equal(t, tt.Output, got)
+			}
 		})
 	}
 }
 
-func BenchmarkAggregatePrefixes(b *testing.B) {
-	fixtureIPv4Prefixes := func(n int64) []netip.Prefix {
-		prefixes := make([]netip.Prefix, 0, n)
-		for i := int64(0); i < n; i++ {
-			addr := netip.AddrFrom4([4]byte{
-				byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i),
-			})
-			prefix, err := addr.Prefix(32)
-			assert.NoError(b, err)
-			prefixes = append(prefixes, prefix)
-		}
-
-		return prefixes
+func TestContainsPrefix(t *testing.T) {
+	tests := []struct {
+		name     string
+		p        netip.Prefix
+		o        netip.Prefix
+		expected bool
+	}{
+		{
+			name:     "IPv4: Exact match",
+			p:        netip.MustParsePrefix("192.168.0.0/24"),
+			o:        netip.MustParsePrefix("192.168.0.0/24"),
+			expected: true,
+		},
+		{
+			name:     "IPv4: Larger contains smaller",
+			p:        netip.MustParsePrefix("192.168.0.0/16"),
+			o:        netip.MustParsePrefix("192.168.1.0/24"),
+			expected: true,
+		},
+		{
+			name:     "IPv4: Smaller doesn't contain larger",
+			p:        netip.MustParsePrefix("192.168.1.0/24"),
+			o:        netip.MustParsePrefix("192.168.0.0/16"),
+			expected: false,
+		},
+		{
+			name:     "IPv4: Non-overlapping",
+			p:        netip.MustParsePrefix("192.168.0.0/24"),
+			o:        netip.MustParsePrefix("192.169.0.0/24"),
+			expected: false,
+		},
+		{
+			name:     "IPv6: Exact match",
+			p:        netip.MustParsePrefix("2001:db8::/32"),
+			o:        netip.MustParsePrefix("2001:db8::/32"),
+			expected: true,
+		},
+		{
+			name:     "IPv6: Larger contains smaller",
+			p:        netip.MustParsePrefix("2001:db8::/32"),
+			o:        netip.MustParsePrefix("2001:db8:1::/48"),
+			expected: true,
+		},
+		{
+			name:     "IPv6: Smaller doesn't contain larger",
+			p:        netip.MustParsePrefix("2001:db8:1::/48"),
+			o:        netip.MustParsePrefix("2001:db8::/32"),
+			expected: false,
+		},
+		{
+			name:     "IPv6: Non-overlapping",
+			p:        netip.MustParsePrefix("2001:db8::/32"),
+			o:        netip.MustParsePrefix("2001:db9::/32"),
+			expected: false,
+		},
 	}
 
-	fixtureIPv6Prefixes := func(n int64) []netip.Prefix {
-		prefixes := make([]netip.Prefix, 0, n)
-		for i := int64(0); i < n; i++ {
-			addr := netip.AddrFrom16([16]byte{
-				0, 0, 0, 0,
-				0, 0, 0, 0,
-				byte(i >> 56), byte(i >> 48), byte(i >> 40), byte(i >> 32),
-				byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i),
-			})
-			prefix, err := addr.Prefix(128)
-			assert.NoError(b, err)
-			prefixes = append(prefixes, prefix)
-		}
-		return prefixes
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := ContainsPrefix(tt.p, tt.o)
+			assert.Equal(t, tt.expected, result)
+		})
 	}
+}
 
-	runIPv4Tests := func(b *testing.B, n int64) {
-		b.Run(fmt.Sprintf("IPv4-%d", n), func(b *testing.B) {
-			b.StopTimer()
-			prefixes := fixtureIPv4Prefixes(n)
-			b.StartTimer()
+func TestMergePrefixes(t *testing.T) {
+	tests := []struct {
+		name     string
+		p1       netip.Prefix
+		p2       netip.Prefix
+		expected netip.Prefix
+		ok       bool
+	}{
+		{
+			name:     "IPv4: Overlapping prefixes",
+			p1:       netip.MustParsePrefix("192.168.0.0/24"),
+			p2:       netip.MustParsePrefix("192.168.0.0/25"),
+			expected: netip.Prefix{},
+			ok:       false,
+		},
+		{
+			name:     "IPv4: Adjacent prefixes",
+			p1:       netip.MustParsePrefix("192.168.0.0/25"),
+			p2:       netip.MustParsePrefix("192.168.0.128/25"),
+			expected: netip.MustParsePrefix("192.168.0.0/24"),
+			ok:       true,
+		},
+		{
+			name:     "IPv4: Non-mergeable prefixes",
+			p1:       netip.MustParsePrefix("192.168.0.0/24"),
+			p2:       netip.MustParsePrefix("192.168.2.0/24"),
+			expected: netip.Prefix{},
+			ok:       false,
+		},
+		{
+			name:     "IPv6: Overlapping prefixes",
+			p1:       netip.MustParsePrefix("2001:db8::/32"),
+			p2:       netip.MustParsePrefix("2001:db8::/48"),
+			expected: netip.Prefix{},
+			ok:       false,
+		},
+		{
+			name:     "IPv6: Adjacent prefixes",
+			p1:       netip.MustParsePrefix("2001:db8::/33"),
+			p2:       netip.MustParsePrefix("2001:db8:8000::/33"),
+			expected: netip.MustParsePrefix("2001:db8::/32"),
+			ok:       true,
+		},
+		{
+			name:     "IPv6: Non-mergeable prefixes",
+			p1:       netip.MustParsePrefix("2001:db8::/32"),
+			p2:       netip.MustParsePrefix("2001:db10::/32"),
+			expected: netip.Prefix{},
+			ok:       false,
+		},
+	}
 
-			for i := 0; i < b.N; i++ {
-				AggregatePrefixes(prefixes)
-			}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, ok := mergeAdjacentPrefixes(tt.p1, tt.p2)
+			assert.Equal(t, tt.ok, ok)
+			assert.Equal(t, tt.expected, result)
 		})
 	}
+}
 
-	runIPv6Tests := func(b *testing.B, n int64) {
-		b.Run(fmt.Sprintf("IPv6-%d", n), func(b *testing.B) {
-			b.StopTimer()
-			prefixes := fixtureIPv4Prefixes(n)
-			b.StartTimer()
+// BenchmarkPrefixFixtures generates a list of prefixes for aggregation benchmarks.
+// The second return value is the expected result of the aggregation.
+func benchmarkPrefixFixtures() ([]netip.Prefix, []netip.Prefix) {
+	var rv []netip.Prefix
+	for i := 0; i <= 255; i++ {
+		for j := 0; j <= 255; j++ {
+			rv = append(rv, netip.MustParsePrefix(fmt.Sprintf("192.168.%d.%d/32", i, j)))
+		}
+	}
 
-			for i := 0; i < b.N; i++ {
-				AggregatePrefixes(prefixes)
-			}
-		})
+	return rv, []netip.Prefix{
+		netip.MustParsePrefix("192.168.0.0/16"),
 	}
+}
 
-	runMixedTests := func(b *testing.B, n int64) {
-		b.Run(fmt.Sprintf("IPv4-IPv6-%d", 2*n), func(b *testing.B) {
-			b.StopTimer()
-			prefixes := append(fixtureIPv4Prefixes(n), fixtureIPv6Prefixes(n)...)
-			b.StartTimer()
+func BenchmarkAggregatePrefixes(b *testing.B) {
+	prefixes, expected := benchmarkPrefixFixtures()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		actual := AggregatePrefixes(prefixes)
+		assert.Len(b, actual, 1)
+		assert.Equal(b, expected, actual)
+	}
+}
 
-			for i := 0; i < b.N; i++ {
-				AggregatePrefixes(prefixes)
-			}
-		})
+func BenchmarkAggregatePrefixesWithPrefixTree(b *testing.B) {
+	prefixes, expected := benchmarkPrefixFixtures()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		actual := AggregatePrefixesWithPrefixTree(prefixes)
+		assert.Len(b, actual, 1)
+		assert.Equal(b, expected, actual)
 	}
+}
 
-	for _, n := range []int64{100, 1_000, 10_000} {
-		runIPv4Tests(b, n)
-		runIPv6Tests(b, n)
-		runMixedTests(b, n)
+func FuzzAggregatePrefixesIPv4(f *testing.F) {
+	f.Add(
+		netip.MustParseAddr("192.168.0.0").AsSlice(),
+		24,
+		netip.MustParseAddr("192.168.1.0").AsSlice(),
+		24,
+		netip.MustParseAddr("10.0.0.0").AsSlice(),
+		8,
+	)
+
+	parsePrefix := func(bytes []byte, bits int) (netip.Prefix, error) {
+		if bits < 0 || bits > 32 {
+			return netip.Prefix{}, fmt.Errorf("invalid bits")
+		}
+
+		addr, ok := netip.AddrFromSlice(bytes)
+		if !ok {
+			return netip.Prefix{}, fmt.Errorf("invalid address")
+		}
+
+		return addr.Prefix(bits)
 	}
+
+	listAddressesAsString := func(prefixes ...netip.Prefix) []string {
+		rv := make(map[string]struct{})
+		for _, p := range prefixes {
+			for addr := p.Addr(); p.Contains(addr); addr = addr.Next() {
+				rv[addr.String()] = struct{}{}
+			}
+		}
+		return fnutil.Keys(rv)
+	}
+
+	f.Fuzz(func(
+		t *testing.T,
+		p1Bytes []byte, p1Bits int,
+		p2Bytes []byte, p2Bits int,
+		p3Bytes []byte, p3Bits int,
+	) {
+
+		p1, err := parsePrefix(p1Bytes, p1Bits)
+		if err != nil {
+			return
+		}
+		p2, err := parsePrefix(p2Bytes, p2Bits)
+		if err != nil {
+			return
+		}
+		p3, err := parsePrefix(p3Bytes, p3Bits)
+		if err != nil {
+			return
+		}
+
+		input := []netip.Prefix{p1, p2, p3}
+		output := AggregatePrefixes(input)
+
+		prefixAsString := func(p netip.Prefix) string { return p.String() }
+		t.Logf("input: %s", fnutil.Map(prefixAsString, input))
+		t.Logf("output: %s", fnutil.Map(prefixAsString, output))
+
+		expectedAddresses := listAddressesAsString(input...)
+		actualAddresses := listAddressesAsString(output...)
+		assert.Equal(t, len(expectedAddresses), len(actualAddresses))
+
+		sort.Strings(expectedAddresses)
+		sort.Strings(actualAddresses)
+		assert.Equal(t, expectedAddresses, actualAddresses)
+	})
 }