Repository: kubernetes-sigs/cloud-provider-azure
Pull request #7442 (2024-10-30 00:29:58)

Migrate ZoneClient to SDK track v2
<!--  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?

<!--
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
-->
/kind feature

#### What this PR does / why we need it:

Migrate ZoneClient to SDK track v2

#### 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:

#### 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: Hi @zarvd. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcloud-provider-azure).

<details>

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>

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

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cloud-provider-azure/pull/7442#" title="Author self-approved">zarvd</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign [jwtty](https://github.com/jwtty) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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).

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

- **[OWNERS](https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/OWNERS)**

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":["jwtty"]} -->
MartinForReal: /ok-to-test
k8s-ci-robot: @zarvd: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-unit | 32e6e90aa1f9ec2954a8f845b425d6883d5f6fef | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/7442/pull-cloud-provider-azure-unit/1851427280836890625) | true | `/test pull-cloud-provider-azure-unit`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=7442). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Azarvd). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cloud-provider-azure/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @zarvd: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-unit | 7a884ca785bd46b9353e2d4a1d1b8f85710dfaa6 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/7442/pull-cloud-provider-azure-unit/1851440845627068416) | true | `/test pull-cloud-provider-azure-unit`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=7442). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Azarvd). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cloud-provider-azure/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
coveralls: 
[![Coverage Status](https://coveralls.io/builds/70610498/badge)](https://coveralls.io/builds/70610498)

coverage: 76.628% (-0.1%) from 76.735%
when pulling **6de67a0b4c8bbbd71ca80c66dc6bd21d45d65c3f on zarvd:chore/sdkv2/zone**
into **93ffd31d3954ab18cf2fc53b3469b835c171bd4f on kubernetes-sigs:master**.

MartinForReal: /lgtm
k8s-ci-robot: New changes are detected. LGTM label has been removed.
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cloud-provider-azure/pull/7442#" title="Author self-approved">zarvd</a>*
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from [martinforreal](https://github.com/martinforreal). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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).

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

- **[OWNERS](https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/OWNERS)**

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":["martinforreal"]} -->
MartinForReal: /lgtm
k8s-ci-robot: New changes are detected. LGTM label has been removed.
k8s-ci-robot: @zarvd: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-e2e-ccm-vmss-capz | 8c72b555014f43f0cc53d533ccdf6b546490b985 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/7442/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1851729317764009984) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=7442). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Azarvd). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cloud-provider-azure/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
zarvd: /retest
MartinForReal: /label tide/merge-method-squash
MartinForReal: /lgtm
MartinForReal: /cherrypick release-1.31
k8s-infra-cherrypick-robot: @MartinForReal: once the present PR merges, I will cherry-pick it on top of `release-1.31` in a new PR and assign it to you.

<details>

In response to [this](https://github.com/kubernetes-sigs/cloud-provider-azure/pull/7442#issuecomment-2448899475):

>/cherrypick release-1.31


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>
MartinForReal: /cherrypick release-1.30
k8s-infra-cherrypick-robot: @MartinForReal: once the present PR merges, I will cherry-pick it on top of `release-1.30` in a new PR and assign it to you.

<details>

In response to [this](https://github.com/kubernetes-sigs/cloud-provider-azure/pull/7442#issuecomment-2448899645):

>/cherrypick release-1.30


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>
MartinForReal: /approve
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/7442#issuecomment-2449049927" title="Approved">MartinForReal</a>*, *<a href="https://github.com/kubernetes-sigs/cloud-provider-azure/pull/7442#" title="Author self-approved">zarvd</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/master/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/pkg/azureclients/zoneclient/azure_zoneclient.go b/pkg/azureclients/zoneclient/azure_zoneclient.go
deleted file mode 100644
index 9a226d0b0..000000000
--- a/pkg/azureclients/zoneclient/azure_zoneclient.go
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
-Copyright 2021 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 zoneclient
-
-import (
-	"context"
-	"net/http"
-	"strings"
-
-	"github.com/Azure/go-autorest/autorest"
-	"github.com/Azure/go-autorest/autorest/azure"
-
-	"k8s.io/klog/v2"
-
-	azclients "sigs.k8s.io/cloud-provider-azure/pkg/azureclients"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/armclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
-)
-
-var _ Interface = &Client{}
-
-const computeResourceProvider = "Microsoft.Compute"
-
-type resourceTypeMetadata struct {
-	ResourceType string         `json:"resourceType"`
-	ZoneMappings []zoneMappings `json:"zoneMappings"`
-}
-
-type zoneMappings struct {
-	Location string   `json:"location"`
-	Zones    []string `json:"zones"`
-}
-
-type providerListDataProperty struct {
-	ID            string                 `json:"id"`
-	ResourceTypes []resourceTypeMetadata `json:"resourceTypes"`
-}
-
-type providerListData struct {
-	ProviderListDataProperties []providerListDataProperty `json:"value"`
-}
-
-// Client implements zone client Interface.
-type Client struct {
-	armClient      armclient.Interface
-	subscriptionID string
-	cloudName      string
-}
-
-// New creates a new zone client with ratelimiting.
-func New(config *azclients.ClientConfig) *Client {
-	baseURI := config.ResourceManagerEndpoint
-	authorizer := config.Authorizer
-	apiVersion := APIVersion
-	if strings.EqualFold(config.CloudName, AzureStackCloudName) && !config.DisableAzureStackCloud {
-		apiVersion = AzureStackCloudAPIVersion
-	}
-
-	armClient := armclient.New(authorizer, *config, baseURI, apiVersion)
-	client := &Client{
-		armClient:      armClient,
-		subscriptionID: config.SubscriptionID,
-		cloudName:      config.CloudName,
-	}
-
-	return client
-}
-
-// GetZones gets the region-zone map for the subscription specified
-func (c *Client) GetZones(ctx context.Context, subscriptionID string) (map[string][]string, *retry.Error) {
-	result, rerr := c.getZones(ctx, subscriptionID)
-	if rerr != nil {
-
-		return result, rerr
-	}
-
-	return result, nil
-}
-
-// getZones gets the region-zone map for the subscription specified
-func (c *Client) getZones(ctx context.Context, subscriptionID string) (map[string][]string, *retry.Error) {
-	resourceID := armclient.GetProviderResourcesListID(subscriptionID)
-
-	response, rerr := c.armClient.GetResource(ctx, resourceID)
-	defer c.armClient.CloseResponse(ctx, response)
-	if rerr != nil {
-		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "zone.get.request", resourceID, rerr.Error())
-		return nil, rerr
-	}
-
-	result := providerListData{}
-	err := autorest.Respond(
-		response,
-		azure.WithErrorUnlessStatusCode(http.StatusOK),
-		autorest.ByUnmarshallingJSON(&result))
-	if err != nil {
-		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "zone.get.respond", resourceID, err)
-		return nil, retry.GetError(response, err)
-	}
-
-	regionZoneMap := make(map[string][]string)
-	expectedID := armclient.GetProviderResourceID(
-		subscriptionID,
-		computeResourceProvider,
-	)
-	if len(result.ProviderListDataProperties) != 0 {
-		for _, property := range result.ProviderListDataProperties {
-			if strings.EqualFold(property.ID, expectedID) {
-				for _, resourceType := range property.ResourceTypes {
-					if strings.EqualFold(resourceType.ResourceType, "virtualMachines") {
-						if len(resourceType.ZoneMappings) != 0 {
-							for _, zoneMapping := range resourceType.ZoneMappings {
-								location := strings.ToLower(strings.ReplaceAll(zoneMapping.Location, " ", ""))
-								regionZoneMap[location] = zoneMapping.Zones
-							}
-						}
-					}
-				}
-			}
-		}
-	}
-
-	return regionZoneMap, nil
-}
diff --git a/pkg/azureclients/zoneclient/doc.go b/pkg/azureclients/zoneclient/doc.go
deleted file mode 100644
index ee1020364..000000000
--- a/pkg/azureclients/zoneclient/doc.go
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2021 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 zoneclient implements the client for ARM.
-package zoneclient // import "sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient"
diff --git a/pkg/azureclients/zoneclient/interface.go b/pkg/azureclients/zoneclient/interface.go
deleted file mode 100644
index d9f542afa..000000000
--- a/pkg/azureclients/zoneclient/interface.go
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
-Copyright 2021 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 zoneclient
-
-import (
-	"context"
-
-	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
-)
-
-const (
-	// APIVersion is the API version for provider list api.
-	APIVersion = "2020-06-01"
-	// AzureStackCloudAPIVersion is the API version for Azure Stack
-	AzureStackCloudAPIVersion = "2019-07-01"
-	// AzureStackCloudName is the cloud name of Azure Stack
-	AzureStackCloudName = "AZURESTACKCLOUD"
-)
-
-// Interface is the client interface for ARM.
-// Don't forget to run "hack/update-mock-clients.sh" command to generate the mock client.
-type Interface interface {
-	GetZones(ctx context.Context, subscriptionID string) (map[string][]string, *retry.Error)
-}
diff --git a/pkg/azureclients/zoneclient/mockzoneclient/doc.go b/pkg/azureclients/zoneclient/mockzoneclient/doc.go
deleted file mode 100644
index d74c1ebbb..000000000
--- a/pkg/azureclients/zoneclient/mockzoneclient/doc.go
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2021 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 mockzonevmclient implements the mock client for zone
-package mockzoneclient // import "sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient/mockzoneclient"
diff --git a/pkg/azureclients/zoneclient/mockzoneclient/interface.go b/pkg/azureclients/zoneclient/mockzoneclient/interface.go
deleted file mode 100644
index 5a7cf2730..000000000
--- a/pkg/azureclients/zoneclient/mockzoneclient/interface.go
+++ /dev/null
@@ -1,73 +0,0 @@
-// /*
-// Copyright 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.
-// */
-//
-
-// Code generated by MockGen. DO NOT EDIT.
-// Source: pkg/azureclients/zoneclient/interface.go
-//
-// Generated by this command:
-//
-//	mockgen -copyright_file=/home/runner/work/cloud-provider-azure/cloud-provider-azure/hack/boilerplate/boilerplate.generatego.txt -source=pkg/azureclients/zoneclient/interface.go -package=mockzoneclient Interface
-//
-
-// Package mockzoneclient is a generated GoMock package.
-package mockzoneclient
-
-import (
-	context "context"
-	reflect "reflect"
-
-	gomock "go.uber.org/mock/gomock"
-	retry "sigs.k8s.io/cloud-provider-azure/pkg/retry"
-)
-
-// MockInterface is a mock of Interface interface.
-type MockInterface struct {
-	ctrl     *gomock.Controller
-	recorder *MockInterfaceMockRecorder
-}
-
-// MockInterfaceMockRecorder is the mock recorder for MockInterface.
-type MockInterfaceMockRecorder struct {
-	mock *MockInterface
-}
-
-// NewMockInterface creates a new mock instance.
-func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
-	mock := &MockInterface{ctrl: ctrl}
-	mock.recorder = &MockInterfaceMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
-	return m.recorder
-}
-
-// GetZones mocks base method.
-func (m *MockInterface) GetZones(ctx context.Context, subscriptionID string) (map[string][]string, *retry.Error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetZones", ctx, subscriptionID)
-	ret0, _ := ret[0].(map[string][]string)
-	ret1, _ := ret[1].(*retry.Error)
-	return ret0, ret1
-}
-
-// GetZones indicates an expected call of GetZones.
-func (mr *MockInterfaceMockRecorder) GetZones(ctx, subscriptionID any) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZones", reflect.TypeOf((*MockInterface)(nil).GetZones), ctx, subscriptionID)
-}
diff --git a/pkg/provider/azure.go b/pkg/provider/azure.go
index 2e8f8b1c1..ae8a2fdd4 100644
--- a/pkg/provider/azure.go
+++ b/pkg/provider/azure.go
@@ -69,7 +69,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmsizeclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmssclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmssvmclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient"
+	"sigs.k8s.io/cloud-provider-azure/pkg/provider/zone"
 
 	"sigs.k8s.io/yaml"
 
@@ -369,7 +369,6 @@ type Cloud struct {
 	VirtualMachineScaleSetVMsClient vmssvmclient.Interface
 	VirtualMachineSizesClient       vmsizeclient.Interface
 	AvailabilitySetsClient          vmasclient.Interface
-	ZoneClient                      zoneclient.Interface
 	privateendpointclient           privateendpointclient.Interface
 	privatednszonegroupclient       privatednszonegroupclient.Interface
 	PrivateLinkServiceClient        privatelinkserviceclient.Interface
@@ -416,10 +415,11 @@ type Cloud struct {
 	routeUpdater       batchProcessor
 	backendPoolUpdater batchProcessor
 
-	vmCache azcache.Resource
-	lbCache azcache.Resource
-	nsgRepo securitygroup.Repository
-	rtCache azcache.Resource
+	vmCache  azcache.Resource
+	lbCache  azcache.Resource
+	nsgRepo  securitygroup.Repository
+	zoneRepo zone.Repository
+	rtCache  azcache.Resource
 	// public ip cache
 	// key: [resourceGroupName]
 	// Value: sync.Map of [pipName]*PublicIPAddress
@@ -742,6 +742,11 @@ func (az *Cloud) InitializeCloudFromConfig(ctx context.Context, config *Config,
 		if err != nil {
 			return err
 		}
+
+		az.zoneRepo, err = zone.NewRepo(az.ComputeClientFactory.GetProviderClient())
+		if err != nil {
+			return err
+		}
 	}
 	err = az.initCaches()
 	if err != nil {
@@ -989,7 +994,6 @@ func (az *Cloud) configAzureClients(
 	fileClientConfig := azClientConfig.WithRateLimiter(nil)
 	blobClientConfig := azClientConfig.WithRateLimiter(nil)
 	vmasClientConfig := azClientConfig.WithRateLimiter(az.Config.AvailabilitySetRateLimit)
-	zoneClientConfig := azClientConfig.WithRateLimiter(nil)
 
 	// If uses network resources in different AAD Tenant, update Authorizer for VM/VMSS/VMAS client config
 	if multiTenantOAuthTokenProvider != nil {
@@ -1035,10 +1039,6 @@ func (az *Cloud) configAzureClients(
 	az.privateendpointclient = privateendpointclient.New(privateEndpointConfig)
 	az.privatednszonegroupclient = privatednszonegroupclient.New(privateDNSZoenGroupConfig)
 	az.PrivateLinkServiceClient = privatelinkserviceclient.New(privateLinkServiceConfig)
-
-	if az.ZoneClient == nil {
-		az.ZoneClient = zoneclient.New(zoneClientConfig)
-	}
 }
 
 func (az *Cloud) getAzureClientConfig(servicePrincipalToken *adal.ServicePrincipalToken) *azclients.ClientConfig {
diff --git a/pkg/provider/azure_zones.go b/pkg/provider/azure_zones.go
index 49d4e3365..8e05dd944 100644
--- a/pkg/provider/azure_zones.go
+++ b/pkg/provider/azure_zones.go
@@ -18,7 +18,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"os"
 	"strconv"
@@ -31,7 +30,6 @@ import (
 
 	azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
-	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
 )
 
 var _ cloudprovider.Zones = (*Cloud)(nil)
@@ -47,11 +45,11 @@ func (az *Cloud) refreshZones(ctx context.Context, refreshFunc func(ctx context.
 
 func (az *Cloud) syncRegionZonesMap(ctx context.Context) error {
 	klog.V(2).Infof("syncRegionZonesMap: starting to fetch all available zones for the subscription %s", az.SubscriptionID)
-	zones, rerr := az.ZoneClient.GetZones(ctx, az.SubscriptionID)
-	if rerr != nil {
-		klog.Warningf("syncRegionZonesMap: error when get zones: %s, will retry after %s", rerr.Error().Error(), consts.ZoneFetchingInterval.String())
-		return rerr.Error()
+	zones, err := az.zoneRepo.ListZones(ctx)
+	if err != nil {
+		return fmt.Errorf("list zones: %w", err)
 	}
+
 	if len(zones) == 0 {
 		klog.Warning("syncRegionZonesMap: empty zone list")
 	}
@@ -92,21 +90,21 @@ func (az *Cloud) getRegionZonesBackoff(ctx context.Context, region string) ([]st
 	klog.V(2).Infof("getRegionZonesMapWrapper: the region-zones map is not initialized successfully, retrying immediately")
 
 	var (
-		zones map[string][]string
-		rerr  *retry.Error
+		zones    map[string][]string
+		innerErr error
 	)
 	err := wait.ExponentialBackoffWithContext(ctx, az.RequestBackoff(), func(ctx context.Context) (done bool, err error) {
-		zones, rerr = az.ZoneClient.GetZones(ctx, az.SubscriptionID)
-		if rerr != nil {
-			klog.Errorf("getRegionZonesMapWrapper: failed to fetch zones information: %v", rerr.Error())
+		zones, innerErr = az.zoneRepo.ListZones(ctx)
+		if innerErr != nil {
+			klog.ErrorS(err, "Failed to list zones")
 			return false, nil
 		}
 
 		return true, nil
 	})
 
-	if errors.Is(err, wait.ErrWaitTimeout) {
-		return []string{}, rerr.Error()
+	if wait.Interrupted(err) {
+		return []string{}, innerErr
 	}
 
 	az.updateRegionZonesMap(zones)
diff --git a/pkg/provider/zone/mock_repo.go b/pkg/provider/zone/mock_repo.go
new file mode 100644
index 000000000..5a446d893
--- /dev/null
+++ b/pkg/provider/zone/mock_repo.go
@@ -0,0 +1,71 @@
+// /*
+// Copyright 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.
+// */
+//
+
+// Code generated by MockGen. DO NOT EDIT.
+// Source: repo.go
+//
+// Generated by this command:
+//
+//	mockgen -destination=./mock_repo.go -package=zone -copyright_file ../../../hack/boilerplate/boilerplate.generatego.txt -source=repo.go Repository
+//
+// Package zone is a generated GoMock package.
+package zone
+
+import (
+	context "context"
+	reflect "reflect"
+
+	gomock "go.uber.org/mock/gomock"
+)
+
+// MockRepository is a mock of Repository interface.
+type MockRepository struct {
+	ctrl     *gomock.Controller
+	recorder *MockRepositoryMockRecorder
+}
+
+// MockRepositoryMockRecorder is the mock recorder for MockRepository.
+type MockRepositoryMockRecorder struct {
+	mock *MockRepository
+}
+
+// NewMockRepository creates a new mock instance.
+func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
+	mock := &MockRepository{ctrl: ctrl}
+	mock.recorder = &MockRepositoryMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
+	return m.recorder
+}
+
+// ListZones mocks base method.
+func (m *MockRepository) ListZones(ctx context.Context) (map[string][]string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListZones", ctx)
+	ret0, _ := ret[0].(map[string][]string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListZones indicates an expected call of ListZones.
+func (mr *MockRepositoryMockRecorder) ListZones(ctx any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListZones", reflect.TypeOf((*MockRepository)(nil).ListZones), ctx)
+}
diff --git a/pkg/provider/zone/repo.go b/pkg/provider/zone/repo.go
new file mode 100644
index 000000000..ad3284b3a
--- /dev/null
+++ b/pkg/provider/zone/repo.go
@@ -0,0 +1,53 @@
+/*
+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 zone
+
+// Generate mocks for the repository interface
+//go:generate mockgen -destination=./mock_repo.go -package=zone -copyright_file ../../../hack/boilerplate/boilerplate.generatego.txt -source=repo.go Repository
+
+import (
+	"context"
+
+	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/providerclient"
+	fnutil "sigs.k8s.io/cloud-provider-azure/pkg/util/collectionutil"
+)
+
+type Repository interface {
+	ListZones(ctx context.Context) (map[string][]string, error)
+}
+
+type repo struct {
+	client providerclient.Interface
+}
+
+func NewRepo(client providerclient.Interface) (Repository, error) {
+	return &repo{client: client}, nil
+}
+
+func (r *repo) ListZones(ctx context.Context) (map[string][]string, error) {
+	zones, err := r.client.GetVirtualMachineSupportedZones(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	var rv = make(map[string][]string, len(zones))
+	for region, z := range zones {
+		rv[region] = fnutil.Map(func(s *string) string { return *s }, z)
+	}
+
+	return rv, nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index b00b511c5..4e727a123 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1560,6 +1560,7 @@ sigs.k8s.io/cloud-provider-azure/pkg/azclient/privatelinkserviceclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/privatezoneclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/privatezoneclient/mock_privatezoneclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/providerclient
+sigs.k8s.io/cloud-provider-azure/pkg/azclient/providerclient/mock_providerclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/publicipaddressclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/publicipprefixclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/registryclient
diff --git a/vendor/sigs.k8s.io/cloud-provider-azure/pkg/azclient/providerclient/mock_providerclient/interface.go b/vendor/sigs.k8s.io/cloud-provider-azure/pkg/azclient/providerclient/mock_providerclient/interface.go
new file mode 100644
index 000000000..0fe5952a4
--- /dev/null
+++ b/vendor/sigs.k8s.io/cloud-provider-azure/pkg/azclient/providerclient/mock_providerclient/interface.go
@@ -0,0 +1,102 @@
+// /*
+// Copyright 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.
+// */
+
+// Code generated by MockGen. DO NOT EDIT.
+// Source: providerclient/interface.go
+//
+// Generated by this command:
+//
+//	mockgen -package mock_providerclient -source providerclient/interface.go
+//
+
+// Package mock_providerclient is a generated GoMock package.
+package mock_providerclient
+
+import (
+	context "context"
+	reflect "reflect"
+
+	armresources "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
+	gomock "go.uber.org/mock/gomock"
+)
+
+// MockInterface is a mock of Interface interface.
+type MockInterface struct {
+	ctrl     *gomock.Controller
+	recorder *MockInterfaceMockRecorder
+}
+
+// MockInterfaceMockRecorder is the mock recorder for MockInterface.
+type MockInterfaceMockRecorder struct {
+	mock *MockInterface
+}
+
+// NewMockInterface creates a new mock instance.
+func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
+	mock := &MockInterface{ctrl: ctrl}
+	mock.recorder = &MockInterfaceMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
+	return m.recorder
+}
+
+// GetProvider mocks base method.
+func (m *MockInterface) GetProvider(ctx context.Context, resourceProviderNamespace string) (*armresources.Provider, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetProvider", ctx, resourceProviderNamespace)
+	ret0, _ := ret[0].(*armresources.Provider)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetProvider indicates an expected call of GetProvider.
+func (mr *MockInterfaceMockRecorder) GetProvider(ctx, resourceProviderNamespace any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvider", reflect.TypeOf((*MockInterface)(nil).GetProvider), ctx, resourceProviderNamespace)
+}
+
+// GetVirtualMachineSupportedZones mocks base method.
+func (m *MockInterface) GetVirtualMachineSupportedZones(ctx context.Context) (map[string][]*string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetVirtualMachineSupportedZones", ctx)
+	ret0, _ := ret[0].(map[string][]*string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetVirtualMachineSupportedZones indicates an expected call of GetVirtualMachineSupportedZones.
+func (mr *MockInterfaceMockRecorder) GetVirtualMachineSupportedZones(ctx any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVirtualMachineSupportedZones", reflect.TypeOf((*MockInterface)(nil).GetVirtualMachineSupportedZones), ctx)
+}
+
+// ListProviders mocks base method.
+func (m *MockInterface) ListProviders(ctx context.Context) ([]*armresources.Provider, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListProviders", ctx)
+	ret0, _ := ret[0].([]*armresources.Provider)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListProviders indicates an expected call of ListProviders.
+func (mr *MockInterfaceMockRecorder) ListProviders(ctx any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProviders", reflect.TypeOf((*MockInterface)(nil).ListProviders), ctx)
+}


Test patch:
diff --git a/pkg/azureclients/zoneclient/azure_zoneclient_test.go b/pkg/azureclients/zoneclient/azure_zoneclient_test.go
deleted file mode 100644
index 03b5d2fde..000000000
--- a/pkg/azureclients/zoneclient/azure_zoneclient_test.go
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
-Copyright 2021 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 zoneclient
-
-import (
-	"bytes"
-	"context"
-	"io"
-	"net/http"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"go.uber.org/mock/gomock"
-
-	azclients "sigs.k8s.io/cloud-provider-azure/pkg/azureclients"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/armclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/armclient/mockarmclient"
-)
-
-const (
-	responseString = `
-{
-	"value": [
-		{
-			"id": "/subscriptions/subscriptionID/providers/Microsoft.Network",
-			"resourceTypes": [
-				{
-					"resourceType": "foo",
-					"zoneMappings": [
-						{
-							"location": "East US 2",
-							"zones": [
-								"1",
-								"2",
-								"3"
-							]
-						}
-					]
-				},
-				{
-					"resourceType": "virtualMachines",
-					"zoneMappings": [
-						{
-							"location": "East US 2",
-							"zones": [
-								"1",
-								"2",
-								"3"
-							]
-						}
-					]
-				}
-			]
-		},
-		{
-			"id": "/subscriptions/subscriptionID/providers/Microsoft.Compute",
-			"resourceTypes": [
-				{
-					"resourceType": "foo",
-					"zoneMappings": [
-						{
-							"location": "East US 2",
-							"zones": [
-								"1",
-								"2",
-								"3"
-							]
-						}
-					]
-				},
-				{
-					"resourceType": "virtualMachines",
-					"zoneMappings": [
-						{
-							"location": "East US 2",
-							"zones": [
-								"1",
-								"2",
-								"3"
-							]
-						}
-					]
-				}
-			]
-		}
-	]
-}
-`
-	testProviderResourcesListID = "/subscriptions/subscriptionID/providers"
-)
-
-func TestNew(t *testing.T) {
-	config := &azclients.ClientConfig{
-		SubscriptionID:          "sub",
-		ResourceManagerEndpoint: "endpoint",
-		Location:                "eastus",
-	}
-
-	zoneClient := New(config)
-	assert.Equal(t, "sub", zoneClient.subscriptionID)
-}
-
-func TestGetZones(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	response := &http.Response{
-		StatusCode: http.StatusOK,
-		Body:       io.NopCloser(bytes.NewReader([]byte(responseString))),
-	}
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	armClient.EXPECT().GetResource(gomock.Any(), testProviderResourcesListID).Return(response, nil).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	zoneClient := getTestZoneClient(armClient)
-	expected := map[string][]string{"eastus2": {"1", "2", "3"}}
-	zones, rerr := zoneClient.GetZones(context.TODO(), zoneClient.subscriptionID)
-	assert.Nil(t, rerr)
-	assert.Equal(t, expected, zones)
-}
-
-func getTestZoneClient(armClient armclient.Interface) *Client {
-	return &Client{
-		armClient:      armClient,
-		subscriptionID: "subscriptionID",
-	}
-}
-
-func TestGetNotFound(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	response := &http.Response{
-		StatusCode: http.StatusNotFound,
-		Body:       io.NopCloser(bytes.NewReader([]byte("{}"))),
-	}
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	armClient.EXPECT().GetResource(gomock.Any(), testProviderResourcesListID).Return(response, nil).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	zoneClient := getTestZoneClient(armClient)
-	result, rerr := zoneClient.GetZones(context.TODO(), zoneClient.subscriptionID)
-	assert.Nil(t, result)
-	assert.NotNil(t, rerr)
-	assert.Equal(t, http.StatusNotFound, rerr.HTTPStatusCode)
-}
-
-func TestGetInternalError(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	response := &http.Response{
-		StatusCode: http.StatusInternalServerError,
-		Body:       io.NopCloser(bytes.NewReader([]byte("{}"))),
-	}
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	armClient.EXPECT().GetResource(gomock.Any(), testProviderResourcesListID).Return(response, nil).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	zoneClient := getTestZoneClient(armClient)
-	result, rerr := zoneClient.GetZones(context.TODO(), zoneClient.subscriptionID)
-	assert.Nil(t, result)
-	assert.NotNil(t, rerr)
-	assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode)
-}
diff --git a/pkg/provider/azure_loadbalancer_test.go b/pkg/provider/azure_loadbalancer_test.go
index 204cb9734..83eb8acb6 100644
--- a/pkg/provider/azure_loadbalancer_test.go
+++ b/pkg/provider/azure_loadbalancer_test.go
@@ -52,8 +52,8 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/publicipclient/mockpublicipclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/subnetclient/mocksubnetclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmssclient/mockvmssclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient/mockzoneclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
+	"sigs.k8s.io/cloud-provider-azure/pkg/provider/zone"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
 	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
@@ -6286,7 +6286,7 @@ func TestReconcileZonesForFrontendIPConfigs(t *testing.T) {
 		existingPIPV4             network.PublicIPAddress
 		existingPIPV6             network.PublicIPAddress
 		status                    *v1.LoadBalancerStatus
-		getZoneError              *retry.Error
+		getZoneError              error
 		regionZonesMap            map[string][]string
 		expectedZones             *[]string
 		expectedDirty             bool
@@ -6316,7 +6316,7 @@ func TestReconcileZonesForFrontendIPConfigs(t *testing.T) {
 		{
 			description:  "reconcileFrontendIPConfigs should report an error if failed to get zones",
 			service:      getInternalTestServiceDualStack("test", 80),
-			getZoneError: retry.NewError(false, errors.New("get zone failed")),
+			getZoneError: errors.New("get zone failed"),
 			expectedErr:  errors.New("get zone failed"),
 		},
 		{
@@ -6413,9 +6413,9 @@ func TestReconcileZonesForFrontendIPConfigs(t *testing.T) {
 			subnetClient.EXPECT().Get(gomock.Any(), "rg", "vnet", "subnet", gomock.Any()).Return(
 				network.Subnet{ID: ptr.To("subnet0"), SubnetPropertiesFormat: &network.SubnetPropertiesFormat{AddressPrefixes: &[]string{"1.2.3.4/31", "2001::1/127"}}}, nil).MaxTimes(1)
 
-			zoneClient := mockzoneclient.NewMockInterface(ctrl)
-			zoneClient.EXPECT().GetZones(gomock.Any(), gomock.Any()).Return(map[string][]string{}, tc.getZoneError).MaxTimes(2)
-			cloud.ZoneClient = zoneClient
+			zoneMock := zone.NewMockRepository(ctrl)
+			zoneMock.EXPECT().ListZones(gomock.Any()).Return(map[string][]string{}, tc.getZoneError).MaxTimes(2)
+			cloud.zoneRepo = zoneMock
 
 			service := tc.service
 			isDualStack := isServiceDualStack(&service)
diff --git a/pkg/provider/azure_test.go b/pkg/provider/azure_test.go
index 615693364..2eed65922 100644
--- a/pkg/provider/azure_test.go
+++ b/pkg/provider/azure_test.go
@@ -52,9 +52,9 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/publicipclient/mockpublicipclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/subnetclient/mocksubnetclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmclient/mockvmclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient/mockzoneclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	providerconfig "sigs.k8s.io/cloud-provider-azure/pkg/provider/config"
+	"sigs.k8s.io/cloud-provider-azure/pkg/provider/zone"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
 	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 	"sigs.k8s.io/cloud-provider-azure/pkg/util/taints"
@@ -2264,6 +2264,7 @@ func getCloudFromConfig(t *testing.T, config string) *Cloud {
 	c, err := ParseConfig(configReader)
 	assert.NoError(t, err)
 
+	zoneMock := zone.NewMockRepository(ctrl)
 	az := &Cloud{
 		nodeNames:                utilsets.NewString(),
 		nodeZones:                map[string]*utilsets.IgnoreCaseSet{},
@@ -2271,12 +2272,11 @@ func getCloudFromConfig(t *testing.T, config string) *Cloud {
 		unmanagedNodes:           utilsets.NewString(),
 		excludeLoadBalancerNodes: utilsets.NewString(),
 		routeCIDRs:               map[string]string{},
-		ZoneClient:               mockzoneclient.NewMockInterface(ctrl),
+		zoneRepo:                 zoneMock,
 		ComputeClientFactory:     mock_azclient.NewMockClientFactory(ctrl),
 		NetworkClientFactory:     mock_azclient.NewMockClientFactory(ctrl),
 	}
-	mockZoneClient := az.ZoneClient.(*mockzoneclient.MockInterface)
-	mockZoneClient.EXPECT().GetZones(gomock.Any(), gomock.Any()).Return(map[string][]string{"eastus": {"1", "2", "3"}}, nil)
+	zoneMock.EXPECT().ListZones(gomock.Any()).Return(map[string][]string{"eastus": {"1", "2", "3"}}, nil)
 
 	// Skip AAD client cert path validation since it will read the file from the path
 	aadCertPath := c.AADClientCertPath
@@ -2782,9 +2782,9 @@ func TestInitializeCloudFromConfig(t *testing.T) {
 	defer ctrl.Finish()
 	az := GetTestCloud(ctrl)
 
-	mockZoneClient := mockzoneclient.NewMockInterface(ctrl)
-	mockZoneClient.EXPECT().GetZones(gomock.Any(), gomock.Any()).Return(map[string][]string{"eastus": {"1", "2", "3"}}, nil).AnyTimes()
-	az.ZoneClient = mockZoneClient
+	zoneMock := zone.NewMockRepository(ctrl)
+	zoneMock.EXPECT().ListZones(gomock.Any()).Return(map[string][]string{"eastus": {"1", "2", "3"}}, nil).AnyTimes()
+	az.zoneRepo = zoneMock
 
 	err := az.InitializeCloudFromConfig(context.Background(), nil, false, true)
 	assert.Equal(t, fmt.Errorf("InitializeCloudFromConfig: cannot initialize from nil config"), err)
diff --git a/pkg/provider/azure_zones_test.go b/pkg/provider/azure_zones_test.go
index a1a5e476a..6d9eea2f9 100644
--- a/pkg/provider/azure_zones_test.go
+++ b/pkg/provider/azure_zones_test.go
@@ -33,8 +33,7 @@ import (
 	"k8s.io/utils/ptr"
 
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmclient/mockvmclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient/mockzoneclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	"sigs.k8s.io/cloud-provider-azure/pkg/provider/zone"
 	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
@@ -253,14 +252,12 @@ func TestSyncRegionZonesMap(t *testing.T) {
 	for _, testCase := range []struct {
 		description   string
 		zones         map[string][]string
-		getZonesErr   *retry.Error
+		getZonesErr   error
 		expectedZones map[string][]string
-		expectedErr   error
 	}{
 		{
 			description: "syncRegionZonesMap should report an error if failed to get zones",
-			getZonesErr: retry.NewError(false, fmt.Errorf("error")),
-			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: error"),
+			getZonesErr: fmt.Errorf("error"),
 		},
 		{
 			description:   "syncRegionZonesMap should not report an error if the given zones map is empty",
@@ -268,15 +265,15 @@ func TestSyncRegionZonesMap(t *testing.T) {
 		},
 	} {
 		t.Run(testCase.description, func(t *testing.T) {
+			m := zone.NewMockRepository(ctrl)
 			az := &Cloud{
-				ZoneClient: mockzoneclient.NewMockInterface(ctrl),
+				zoneRepo: m,
 			}
-			mockZoneClient := az.ZoneClient.(*mockzoneclient.MockInterface)
-			mockZoneClient.EXPECT().GetZones(gomock.Any(), gomock.Any()).Return(testCase.expectedZones, testCase.getZonesErr)
+			m.EXPECT().ListZones(gomock.Any()).Return(testCase.expectedZones, testCase.getZonesErr)
 
 			err := az.syncRegionZonesMap(context.TODO())
 			if err != nil {
-				assert.EqualError(t, testCase.expectedErr, err.Error())
+				assert.ErrorIs(t, err, testCase.getZonesErr)
 			}
 			assert.Equal(t, testCase.expectedZones, az.regionZonesMap)
 		})
@@ -290,14 +287,14 @@ func TestGetRegionZonesBackoff(t *testing.T) {
 	for _, testCase := range []struct {
 		description   string
 		zones         map[string][]string
-		getZonesErr   *retry.Error
+		getZonesErr   error
 		expectedZones map[string][]string
 		expectedErr   error
 	}{
 		{
 			description: "syncRegionZonesMap should report an error if failed to get zones",
-			getZonesErr: retry.NewError(false, fmt.Errorf("error")),
-			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: error"),
+			getZonesErr: fmt.Errorf("error"),
+			expectedErr: fmt.Errorf("error"),
 		},
 		{
 			description:   "syncRegionZonesMap should not report an error if the given zones map is empty",
@@ -305,11 +302,11 @@ func TestGetRegionZonesBackoff(t *testing.T) {
 		},
 	} {
 		t.Run(testCase.description, func(t *testing.T) {
+			m := zone.NewMockRepository(ctrl)
 			az := &Cloud{
-				ZoneClient: mockzoneclient.NewMockInterface(ctrl),
+				zoneRepo: m,
 			}
-			mockZoneClient := az.ZoneClient.(*mockzoneclient.MockInterface)
-			mockZoneClient.EXPECT().GetZones(gomock.Any(), gomock.Any()).Return(testCase.expectedZones, testCase.getZonesErr)
+			m.EXPECT().ListZones(gomock.Any()).Return(testCase.expectedZones, testCase.getZonesErr)
 
 			_, err := az.getRegionZonesBackoff(context.TODO(), "eastus2")
 			if err != nil {
@@ -324,10 +321,10 @@ func TestRefreshZones(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
+	m := zone.NewMockRepository(ctrl)
 	az := GetTestCloud(ctrl)
-	mockZoneClient := mockzoneclient.NewMockInterface(ctrl)
-	mockZoneClient.EXPECT().GetZones(gomock.Any(), gomock.Any()).Return(map[string][]string{}, nil).Times(0)
-	az.ZoneClient = mockZoneClient
+	az.zoneRepo = m
+	m.EXPECT().ListZones(gomock.Any()).Return(map[string][]string{}, nil).Times(0)
 
 	ctx, cancel := context.WithCancel(context.Background())
 	go func() {
diff --git a/pkg/provider/zone/repo_test.go b/pkg/provider/zone/repo_test.go
new file mode 100644
index 000000000..e9d269b99
--- /dev/null
+++ b/pkg/provider/zone/repo_test.go
@@ -0,0 +1,68 @@
+/*
+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 zone
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+	"github.com/stretchr/testify/assert"
+	"go.uber.org/mock/gomock"
+
+	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/providerclient/mock_providerclient"
+)
+
+func TestRepo_ListZones(t *testing.T) {
+	t.Run("return zones", func(t *testing.T) {
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+
+		client := mock_providerclient.NewMockInterface(ctrl)
+		repo, err := NewRepo(client)
+		assert.NoError(t, err)
+
+		expectedZones := map[string][]*string{
+			"k1": {to.Ptr("zone1"), to.Ptr("zone2")},
+			"k2": {to.Ptr("zone3"), to.Ptr("zone4")},
+		}
+		client.EXPECT().GetVirtualMachineSupportedZones(gomock.Any()).Return(expectedZones, nil).Times(1)
+
+		zones, err := repo.ListZones(context.Background())
+		assert.NoError(t, err)
+		assert.Equal(t, map[string][]string{
+			"k1": {"zone1", "zone2"},
+			"k2": {"zone3", "zone4"},
+		}, zones)
+	})
+
+	t.Run("return error", func(t *testing.T) {
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+
+		client := mock_providerclient.NewMockInterface(ctrl)
+		repo, err := NewRepo(client)
+		assert.NoError(t, err)
+
+		expectedErr := fmt.Errorf("error")
+		client.EXPECT().GetVirtualMachineSupportedZones(gomock.Any()).Return(nil, expectedErr).Times(1)
+
+		_, err = repo.ListZones(context.Background())
+		assert.ErrorIs(t, err, expectedErr)
+	})
+}