Repository: kubernetes-sigs/cloud-provider-azure
Pull request #4077 (2023-06-09 12:31:14)

track2 sdk: add config parser for track2 client
<!--  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 feature

<!--
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:
track2 sdk: add config parser for track2 client
#### 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

```



Patch:
diff --git a/Dockerfile b/Dockerfile
index 4c3501dd6..470ff8312 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,7 +14,7 @@
 
 # syntax=docker/dockerfile:1
 
-FROM --platform=linux/amd64 golang:1.20.5-buster@sha256:b0f97bfb6270353ab877014a44966138b3b31cdc0cea972697008e9f448f7176 AS builder
+FROM --platform=linux/amd64 golang:1.20.4-buster@sha256:4cf6dc46fc03a7aecde79ccc135d875129145b065cb1d29747ac7c8d86979266 AS builder
 
 ARG ENABLE_GIT_COMMAND=true
 ARG ARCH=amd64
diff --git a/cloud-node-manager.Dockerfile b/cloud-node-manager.Dockerfile
index aa007a7a9..90bcd4001 100644
--- a/cloud-node-manager.Dockerfile
+++ b/cloud-node-manager.Dockerfile
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-FROM --platform=linux/amd64 golang:1.20.5-buster AS builder
+FROM --platform=linux/amd64 golang:1.20.4-buster AS builder
 
 ARG ENABLE_GIT_COMMAND=true
 ARG ARCH=amd64
diff --git a/go.mod b/go.mod
index 2fd9e7c83..d84c33962 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,6 @@ require (
 	github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
 	github.com/Azure/go-autorest/autorest v0.11.29
 	github.com/Azure/go-autorest/autorest/adal v0.9.23
-	github.com/Azure/go-autorest/autorest/date v0.3.0
 	github.com/Azure/go-autorest/autorest/mocks v0.4.2
 	github.com/Azure/go-autorest/tracing v0.6.0
 	github.com/evanphx/json-patch v5.6.0+incompatible
@@ -46,6 +45,7 @@ require (
 	github.com/Azure/azure-storage-queue-go v0.0.0-20191125232315-636801874cdd // indirect
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
+	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
 	github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
 	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
diff --git a/pkg/azclient/arm_conf.go b/pkg/azclient/arm_conf.go
new file mode 100644
index 000000000..eb91b595e
--- /dev/null
+++ b/pkg/azclient/arm_conf.go
@@ -0,0 +1,50 @@
+/*
+Copyright 2023 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 azclient
+
+import (
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/utils"
+)
+
+type ARMClientConfig struct {
+	// The cloud environment identifier. Takes values from https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azcore@v1.6.0/cloud
+	Cloud string `json:"cloud,omitempty" yaml:"cloud,omitempty"`
+	// The user agent for Azure customer usage attribution
+	UserAgent string `json:"userAgent,omitempty" yaml:"userAgent,omitempty"`
+	// ResourceManagerEndpoint is the cloud's resource manager endpoint. If set, cloud provider queries this endpoint
+	// in order to generate an autorest.Environment instance instead of using one of the pre-defined Environments.
+	ResourceManagerEndpoint string `json:"resourceManagerEndpoint,omitempty" yaml:"resourceManagerEndpoint,omitempty"`
+}
+
+func NewClientOptionFromARMClientConfig(config *ARMClientConfig) (*policy.ClientOptions, error) {
+	//Get default settings
+	options := utils.GetDefaultOption()
+	var err error
+	if config != nil {
+		//update user agent header
+		options.ClientOptions.Telemetry.ApplicationID = config.UserAgent
+		//set cloud
+		var cloudConfig *cloud.Configuration
+		cloudConfig, err = GetAzureCloudConfig(config)
+		options.ClientOptions.Cloud = *cloudConfig
+	} else {
+		options.ClientOptions.Cloud = cloud.AzurePublic
+	}
+	return options, err
+}
diff --git a/pkg/azclient/auth.go b/pkg/azclient/auth.go
index e0790721b..2729b7700 100644
--- a/pkg/azclient/auth.go
+++ b/pkg/azclient/auth.go
@@ -26,7 +26,6 @@ import (
 
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
 	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
 	"golang.org/x/crypto/pkcs12"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/utils"
@@ -50,7 +49,7 @@ type AzureAuthConfig struct {
 	// More details of the user assigned identity can be found at: https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview
 	// For the user assigned identity specified here to be used, the UseManagedIdentityExtension has to be set to true.
 	UserAssignedIdentityID string `json:"userAssignedIdentityID,omitempty" yaml:"userAssignedIdentityID,omitempty"`
-	// The AAD Tenant ID for the Subscription that the network resources are deployed in
+	// The AAD Tenant ID for the Subscription that the network resources are deployed in.
 	NetworkResourceTenantID string `json:"networkResourceTenantID,omitempty" yaml:"networkResourceTenantID,omitempty"`
 	// The AAD federated token file
 	AADFederatedTokenFile string `json:"aadFederatedTokenFile,omitempty" yaml:"aadFederatedTokenFile,omitempty"`
@@ -76,24 +75,12 @@ const (
 
 func GetDefaultAuthClientOption(armConfig *ARMClientConfig) (*policy.ClientOptions, error) {
 	//Get default settings
-	options := utils.GetDefaultOption()
-	// armloadbalancer doesn't support ligin.microsoft.com
-	options.Transport = &http.Client{Transport: utils.DefaultTransport}
-
-	if armConfig != nil {
-		//update user agent header
-		options.ClientOptions.Telemetry.ApplicationID = armConfig.UserAgent
-		//todo: add backoff retry policy
-
-		//set cloud
-		var err error
-		options.ClientOptions.Cloud, err = AzureCloudConfigFromName(armConfig.Cloud, armConfig.ResourceManagerEndpoint)
-		if err != nil {
-			return nil, err
-		}
-	} else {
-		options.ClientOptions.Cloud = cloud.AzurePublic
+	options, err := NewClientOptionFromARMClientConfig(armConfig)
+	if err != nil {
+		return nil, err
 	}
+	// armloadbalancer doesn't support login.microsoft.com
+	options.Transport = &http.Client{Transport: utils.DefaultTransport}
 	return options, nil
 }
 
diff --git a/pkg/azclient/cloud.go b/pkg/azclient/cloud.go
new file mode 100644
index 000000000..2540da2a7
--- /dev/null
+++ b/pkg/azclient/cloud.go
@@ -0,0 +1,181 @@
+/*
+Copyright 2023 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 azclient
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"strings"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+)
+
+var EnvironmentMapping = map[string]*cloud.Configuration{
+	"AZURECHINACLOUD":        &cloud.AzureChina,
+	"AZURECLOUD":             &cloud.AzurePublic,
+	"AZUREPUBLICCLOUD":       &cloud.AzurePublic,
+	"AZUREUSGOVERNMENT":      &cloud.AzureGovernment,
+	"AZUREUSGOVERNMENTCLOUD": &cloud.AzureGovernment, //TODO: deprecate
+}
+
+const (
+	// EnvironmentFilepathName captures the name of the environment variable containing the path to the file
+	// to be used while populating the Azure Environment.
+	EnvironmentFilepathName = "AZURE_ENVIRONMENT_FILEPATH"
+)
+
+func AzureCloudConfigFromName(cloudName string) *cloud.Configuration {
+	if cloudName == "" {
+		return &cloud.AzurePublic
+	}
+	cloudName = strings.ToUpper(cloudName)
+	if cloudConfig, ok := EnvironmentMapping[cloudName]; ok {
+		return cloudConfig
+	}
+	return nil
+}
+
+// AzureCloudConfigFromUrl returns cloud config from url
+// track2 sdk will add this one in the near future https://github.com/Azure/azure-sdk-for-go/issues/20959
+func AzureCloudConfigFromUrl(endpoint string) (*cloud.Configuration, error) {
+	managementEndpoint := fmt.Sprintf("%s%s", strings.TrimSuffix(endpoint, "/"), "/metadata/endpoints?api-version=2019-05-01")
+	res, err := http.Get(managementEndpoint)
+	if err != nil {
+		return nil, err
+	}
+	body, err := io.ReadAll(res.Body)
+	if err != nil {
+		return nil, err
+	}
+	metadata := []struct {
+		Authentication struct {
+			Audiences     []string
+			LoginEndpoint string
+		}
+		Name, ResourceManager string
+	}{}
+	err = json.Unmarshal(body, &metadata)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(metadata) > 0 {
+		return &cloud.Configuration{
+			ActiveDirectoryAuthorityHost: metadata[0].Authentication.LoginEndpoint,
+			Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
+				cloud.ResourceManager: {
+					Endpoint: metadata[0].ResourceManager,
+					Audience: metadata[0].Authentication.Audiences[0],
+				},
+			},
+		}, nil
+	}
+	return nil, nil
+}
+
+func AzureCloudConfigOverrideFromEnv(config *cloud.Configuration) (*cloud.Configuration, error) {
+	if config == nil {
+		config = &cloud.AzurePublic
+	}
+	envFilePath, ok := os.LookupEnv(EnvironmentFilepathName)
+	if !ok {
+		return config, nil
+	}
+	content, err := os.ReadFile(envFilePath)
+	if err != nil {
+		return nil, err
+	}
+	var envConfig Environment
+	if err = json.Unmarshal(content, &envConfig); err != nil {
+		return nil, err
+	}
+	if len(envConfig.ActiveDirectoryEndpoint) > 0 {
+		config.ActiveDirectoryAuthorityHost = envConfig.ActiveDirectoryEndpoint
+	}
+	if len(envConfig.ResourceManagerEndpoint) > 0 && len(envConfig.TokenAudience) > 0 {
+		config.Services[cloud.ResourceManager] = cloud.ServiceConfiguration{
+			Endpoint: envConfig.ResourceManagerEndpoint,
+			Audience: envConfig.TokenAudience,
+		}
+	}
+	return config, nil
+}
+
+func GetAzureCloudConfig(armConfig *ARMClientConfig) (*cloud.Configuration, error) {
+	var config *cloud.Configuration
+	var err error
+	if armConfig == nil {
+		config = &cloud.AzurePublic
+	} else {
+		config = AzureCloudConfigFromName(armConfig.Cloud)
+		if armConfig.ResourceManagerEndpoint != "" {
+			config, err = AzureCloudConfigFromUrl(armConfig.ResourceManagerEndpoint)
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+	config, err = AzureCloudConfigOverrideFromEnv(config)
+	return config, nil
+}
+
+// Environment represents a set of endpoints for each of Azure's Clouds.
+type Environment struct {
+	Name                         string             `json:"name"`
+	ManagementPortalURL          string             `json:"managementPortalURL"`
+	PublishSettingsURL           string             `json:"publishSettingsURL"`
+	ServiceManagementEndpoint    string             `json:"serviceManagementEndpoint"`
+	ResourceManagerEndpoint      string             `json:"resourceManagerEndpoint"`
+	ActiveDirectoryEndpoint      string             `json:"activeDirectoryEndpoint"`
+	GalleryEndpoint              string             `json:"galleryEndpoint"`
+	KeyVaultEndpoint             string             `json:"keyVaultEndpoint"`
+	ManagedHSMEndpoint           string             `json:"managedHSMEndpoint"`
+	GraphEndpoint                string             `json:"graphEndpoint"`
+	ServiceBusEndpoint           string             `json:"serviceBusEndpoint"`
+	BatchManagementEndpoint      string             `json:"batchManagementEndpoint"`
+	MicrosoftGraphEndpoint       string             `json:"microsoftGraphEndpoint"`
+	StorageEndpointSuffix        string             `json:"storageEndpointSuffix"`
+	CosmosDBDNSSuffix            string             `json:"cosmosDBDNSSuffix"`
+	MariaDBDNSSuffix             string             `json:"mariaDBDNSSuffix"`
+	MySQLDatabaseDNSSuffix       string             `json:"mySqlDatabaseDNSSuffix"`
+	PostgresqlDatabaseDNSSuffix  string             `json:"postgresqlDatabaseDNSSuffix"`
+	SQLDatabaseDNSSuffix         string             `json:"sqlDatabaseDNSSuffix"`
+	TrafficManagerDNSSuffix      string             `json:"trafficManagerDNSSuffix"`
+	KeyVaultDNSSuffix            string             `json:"keyVaultDNSSuffix"`
+	ManagedHSMDNSSuffix          string             `json:"managedHSMDNSSuffix"`
+	ServiceBusEndpointSuffix     string             `json:"serviceBusEndpointSuffix"`
+	ServiceManagementVMDNSSuffix string             `json:"serviceManagementVMDNSSuffix"`
+	ResourceManagerVMDNSSuffix   string             `json:"resourceManagerVMDNSSuffix"`
+	ContainerRegistryDNSSuffix   string             `json:"containerRegistryDNSSuffix"`
+	TokenAudience                string             `json:"tokenAudience"`
+	APIManagementHostNameSuffix  string             `json:"apiManagementHostNameSuffix"`
+	SynapseEndpointSuffix        string             `json:"synapseEndpointSuffix"`
+	DatalakeSuffix               string             `json:"datalakeSuffix"`
+	ResourceIdentifiers          ResourceIdentifier `json:"resourceIdentifiers"`
+}
+
+// ResourceIdentifier contains a set of Azure resource IDs.
+type ResourceIdentifier struct {
+	Graph               string `json:"graph"`
+	KeyVault            string `json:"keyVault"`
+	Datalake            string `json:"datalake"`
+	Batch               string `json:"batch"`
+	OperationalInsights string `json:"operationalInsights"`
+}
diff --git a/pkg/azclient/config.go b/pkg/azclient/config.go
deleted file mode 100644
index 86519cae1..000000000
--- a/pkg/azclient/config.go
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
-Copyright 2023 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 azclient
-
-import (
-	"errors"
-	"strings"
-
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/policy/ratelimit"
-)
-
-type ClientFactoryConfig struct {
-	ratelimit.CloudProviderRateLimitConfig
-	ARMClientConfig
-
-	// Enable exponential backoff to manage resource request retries
-	CloudProviderBackoff bool `json:"cloudProviderBackoff,omitempty" yaml:"cloudProviderBackoff,omitempty"`
-}
-
-type ARMClientConfig struct {
-	// The cloud environment identifier. Takes values from https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azcore@v1.6.0/cloud
-	Cloud string `json:"cloud,omitempty" yaml:"cloud,omitempty"`
-	// The user agent for Azure customer usage attribution
-	UserAgent string `json:"userAgent,omitempty" yaml:"userAgent,omitempty"`
-	// ResourceManagerEndpoint is the cloud's resource manager endpoint. If set, cloud provider queries this endpoint
-	// in order to generate an autorest.Environment instance instead of using one of the pre-defined Environments.
-	ResourceManagerEndpoint string `json:"resourceManagerEndpoint,omitempty" yaml:"resourceManagerEndpoint,omitempty"`
-}
-
-var EnvironmentMapping = map[string]cloud.Configuration{
-	"AZURECHINACLOUD":        cloud.AzureChina,
-	"AZURECLOUD":             cloud.AzurePublic,
-	"AZUREPUBLICCLOUD":       cloud.AzurePublic,
-	"AZUREUSGOVERNMENT":      cloud.AzureGovernment,
-	"AZUREUSGOVERNMENTCLOUD": cloud.AzureGovernment, //TODO: deprecate
-}
-
-var ConfigNotFoundError = errors.New("resource manager service config is not found")
-
-func AzureCloudConfigFromName(cloudName string, endpoint string) (cloud.Configuration, error) {
-	cloudName = strings.ToUpper(cloudName)
-	var config *cloud.Configuration
-	if cloudName == "" {
-		cloudName = "AZUREPUBLICCLOUD"
-	}
-	if cloudConfig, ok := EnvironmentMapping[cloudName]; ok {
-		config = &cloudConfig
-	}
-	if endpoint != "" {
-		if config == nil {
-			//if cloudName is customized profile, ActiveDirectoryAuthorityHost is not set.
-			//todo: load ActiveDirectoryAuthorityHost from azure.json
-			return cloud.Configuration{}, ConfigNotFoundError
-		}
-		var serviceConfig cloud.ServiceConfiguration
-		var ok bool
-		if serviceConfig, ok = config.Services[cloud.ResourceManager]; !ok {
-			//todo: load cloud.ResourceManager.Audience from azure.json
-			return cloud.Configuration{}, ConfigNotFoundError
-		}
-		serviceConfig.Endpoint = endpoint
-		config.Services[cloud.ResourceManager] = serviceConfig
-	}
-	//todo: load endpoint config from file.
-	if config == nil {
-		return cloud.Configuration{}, ConfigNotFoundError
-	}
-	return *config, nil
-}
diff --git a/pkg/azclient/default.go b/pkg/azclient/default.go
deleted file mode 100644
index f592cbcb5..000000000
--- a/pkg/azclient/default.go
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
-Copyright 2023 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 azclient
-
-import (
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
-
-	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/utils"
-)
-
-func (factory *ClientFactory) GetDefaultResourceClientOption() (*policy.ClientOptions, error) {
-
-	//Get default settings
-	options := utils.GetDefaultOption()
-
-	//update user agent header
-	options.ClientOptions.Telemetry.ApplicationID = factory.ClientFactoryConfig.UserAgent
-
-	// add retry policy
-	if !factory.ClientFactoryConfig.CloudProviderBackoff {
-		options.ClientOptions.Retry.MaxRetries = 1
-	}
-
-	//set cloud
-	var err error
-	options.ClientOptions.Cloud, err = AzureCloudConfigFromName(factory.ClientFactoryConfig.Cloud, factory.ClientFactoryConfig.ResourceManagerEndpoint)
-	return options, err
-}
diff --git a/pkg/azclient/factory.go b/pkg/azclient/factory.go
index a06277079..5762b3533 100644
--- a/pkg/azclient/factory.go
+++ b/pkg/azclient/factory.go
@@ -40,7 +40,7 @@ func NewClientFactory(config *ClientFactoryConfig, authProvider *AuthProvider) *
 func (factory *ClientFactory) GetDiskClient(subscription string) (diskclient.Interface, error) {
 	subID := strings.ToLower(subscription)
 
-	options, err := factory.GetDefaultResourceClientOption()
+	options, err := GetDefaultResourceClientOption(factory.ClientFactoryConfig)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/azclient/factory_conf.go b/pkg/azclient/factory_conf.go
new file mode 100644
index 000000000..998759d6e
--- /dev/null
+++ b/pkg/azclient/factory_conf.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2023 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 azclient
+
+import (
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
+	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/policy/ratelimit"
+)
+
+type ClientFactoryConfig struct {
+	ratelimit.CloudProviderRateLimitConfig
+	*ARMClientConfig
+
+	// Enable exponential backoff to manage resource request retries
+	CloudProviderBackoff bool `json:"cloudProviderBackoff,omitempty" yaml:"cloudProviderBackoff,omitempty"`
+}
+
+func GetDefaultResourceClientOption(config *ClientFactoryConfig) (*policy.ClientOptions, error) {
+	var armConfig *ARMClientConfig
+	if config != nil {
+		armConfig = config.ARMClientConfig
+	}
+	//Get default settings
+	options, err := NewClientOptionFromARMClientConfig(armConfig)
+	if err != nil {
+		return nil, err
+	}
+	if config != nil {
+		// add retry policy
+		if !config.CloudProviderBackoff {
+			options.ClientOptions.Retry.MaxRetries = 1
+		}
+	}
+	return options, err
+}
diff --git a/pkg/azclient/utils/poller.go b/pkg/azclient/utils/poller.go
index 3d7c615f2..c93b43e21 100644
--- a/pkg/azclient/utils/poller.go
+++ b/pkg/azclient/utils/poller.go
@@ -19,6 +19,7 @@ package utils
 import (
 	"context"
 	"errors"
+	"time"
 
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
 )
@@ -44,7 +45,9 @@ func (handler *PollerWrapper[ResponseType]) WaitforPollerResp(ctx context.Contex
 	if handler.poller == nil {
 		return nil, errors.New("poller is nil")
 	}
-	resp, err := handler.poller.PollUntilDone(ctx, nil)
+	resp, err := handler.poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{
+		Frequency: time.Second * 1,
+	})
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/provider/azure_storageaccount.go b/pkg/provider/azure_storageaccount.go
index 6b4070391..d4afbf251 100644
--- a/pkg/provider/azure_storageaccount.go
+++ b/pkg/provider/azure_storageaccount.go
@@ -22,7 +22,6 @@ import (
 	"fmt"
 	"math/big"
 	"strings"
-	"time"
 
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
 	"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
@@ -75,7 +74,6 @@ type AccountOptions struct {
 	SubnetName                              string
 	AccessTier                              string
 	MatchTags                               bool
-	GetLatestAccountKey                     bool
 	EnableBlobVersioning                    *bool
 	SoftDeleteBlobs                         int32
 	SoftDeleteContainers                    int32
@@ -127,8 +125,7 @@ func (az *Cloud) getStorageAccounts(ctx context.Context, accountOptions *Account
 }
 
 // GetStorageAccesskey gets the storage account access key
-// getLatestAccountKey: get the latest account key per CreationTime if true, otherwise get the first account key
-func (az *Cloud) GetStorageAccesskey(ctx context.Context, subsID, account, resourceGroup string, getLatestAccountKey bool) (string, error) {
+func (az *Cloud) GetStorageAccesskey(ctx context.Context, subsID, account, resourceGroup string) (string, error) {
 	if az.StorageAccountClient == nil {
 		return "", fmt.Errorf("StorageAccountClient is nil")
 	}
@@ -141,40 +138,16 @@ func (az *Cloud) GetStorageAccesskey(ctx context.Context, subsID, account, resou
 		return "", fmt.Errorf("empty keys")
 	}
 
-	var key string
-	var creationTime time.Time
-
 	for _, k := range *result.Keys {
 		if k.Value != nil && *k.Value != "" {
 			v := *k.Value
 			if ind := strings.LastIndex(v, " "); ind >= 0 {
 				v = v[(ind + 1):]
 			}
-			if !getLatestAccountKey {
-				// get first key
-				return v, nil
-			}
-			// get account key with latest CreationTime
-			if key == "" {
-				key = v
-				if k.CreationTime != nil {
-					creationTime = k.CreationTime.ToTime()
-				}
-				klog.V(2).Infof("got storage account key with creation time: %v", creationTime)
-			} else {
-				if k.CreationTime != nil && creationTime.Before(k.CreationTime.ToTime()) {
-					key = v
-					creationTime = k.CreationTime.ToTime()
-					klog.V(2).Infof("got storage account key with latest creation time: %v", creationTime)
-				}
-			}
+			return v, nil
 		}
 	}
-
-	if key == "" {
-		return "", fmt.Errorf("no valid keys")
-	}
-	return key, nil
+	return "", fmt.Errorf("no valid keys")
 }
 
 // EnsureStorageAccount search storage account, create one storage account(with genAccountNamePrefix) if not found, return accountName, accountKey
@@ -266,7 +239,7 @@ func (az *Cloud) EnsureStorageAccount(ctx context.Context, accountOptions *Accou
 		createNewAccount = false
 		if accountOptions.CreateAccount {
 			// check whether account exists
-			if _, err := az.GetStorageAccesskey(ctx, subsID, accountName, resourceGroup, accountOptions.GetLatestAccountKey); err != nil {
+			if _, err := az.GetStorageAccesskey(ctx, subsID, accountName, resourceGroup); err != nil {
 				klog.V(2).Infof("get storage key for storage account %s returned with %v", accountName, err)
 				createNewAccount = true
 			}
@@ -496,7 +469,7 @@ func (az *Cloud) EnsureStorageAccount(ctx context.Context, accountOptions *Accou
 	}
 
 	// find the access key with this account
-	accountKey, err := az.GetStorageAccesskey(ctx, subsID, accountName, resourceGroup, accountOptions.GetLatestAccountKey)
+	accountKey, err := az.GetStorageAccesskey(ctx, subsID, accountName, resourceGroup)
 	if err != nil {
 		return "", "", fmt.Errorf("could not get storage key for storage account %s: %w", accountName, err)
 	}


Test patch:
diff --git a/e2e.Dockerfile b/e2e.Dockerfile
index 8f6df1890..8694f990e 100644
--- a/e2e.Dockerfile
+++ b/e2e.Dockerfile
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-FROM golang:1.20.5-buster
+FROM golang:1.20.4-buster
 
 WORKDIR /go/src/sigs.k8s.io/cloud-provider-azure
 
diff --git a/pkg/azclient/cloud_test.go b/pkg/azclient/cloud_test.go
new file mode 100644
index 000000000..61282b72a
--- /dev/null
+++ b/pkg/azclient/cloud_test.go
@@ -0,0 +1,165 @@
+/*
+Copyright 2023 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 azclient_test
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"os"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"sigs.k8s.io/cloud-provider-azure/pkg/azclient"
+)
+
+var _ = Describe("Cloud", func() {
+	Context("AzureCloudFromUrl", func() {
+		When("the url is valid", func() {
+			It("should return the cloud", func() {
+				server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					w.WriteHeader(http.StatusOK)
+					w.Write([]byte(`
+[
+    {
+        "portal":"https://portal.azure.com",
+        "authentication":{
+            "loginEndpoint":"https://login.microsoftonline.com/",
+            "audiences":[
+                "https://management.core.windows.net/",
+                "https://management.azure.com/"
+            ],
+            "tenant":"common",
+            "identityProvider":"AAD"
+        }
+,
+        "media":"https://rest.media.azure.net",
+        "graphAudience":"https://graph.windows.net/",
+        "graph":"https://graph.windows.net/",
+        "name":"AzureCloud",
+        "suffixes":{
+            "azureDataLakeStoreFileSystem":"azuredatalakestore.net",
+            "acrLoginServer":"azurecr.io",
+            "sqlServerHostname":"database.windows.net",
+            "azureDataLakeAnalyticsCatalogAndJob":"azuredatalakeanalytics.net",
+            "keyVaultDns":"vault.azure.net",
+            "storage":"core.windows.net",
+            "azureFrontDoorEndpointSuffix":"azurefd.net"
+        }
+,
+        "batch":"https://batch.core.windows.net/",
+        "resourceManager":"https://management.azure.com/",
+        "vmImageAliasDoc":"https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/arm-compute/quickstart-templates/aliases.json",
+        "activeDirectoryDataLake":"https://datalake.azure.net/",
+        "sqlManagement":"https://management.core.windows.net:8443/",
+        "gallery":"https://gallery.azure.com/"
+    }
+
+]
+					`))
+				}))
+				defer server.Close()
+
+				cloudConfig, err := azclient.AzureCloudConfigFromUrl(server.URL)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(cloudConfig).ToNot(BeNil())
+				Expect(cloudConfig.ActiveDirectoryAuthorityHost).To(Equal("https://login.microsoftonline.com/"))
+				Expect(cloudConfig.Services).NotTo(BeEmpty())
+				Expect(cloudConfig.Services[cloud.ResourceManager].Audience).To(Equal("https://management.core.windows.net/"))
+			})
+		})
+	})
+	Context("AzureCloudFromName", func() {
+		When("cloud name is empty", func() {
+			It("should return the default cloud", func() {
+				cloudConfig := azclient.AzureCloudConfigFromName("")
+				Expect(cloudConfig).ToNot(BeNil())
+				Expect(cloudConfig.ActiveDirectoryAuthorityHost).To(Equal("https://login.microsoftonline.com/"))
+				Expect(cloudConfig.Services).NotTo(BeEmpty())
+				Expect(cloudConfig.Services[cloud.ResourceManager].Audience).To(Equal("https://management.core.windows.net/"))
+			})
+		})
+		When("cloud name is wrong", func() {
+			It("should return the default cloud", func() {
+				cloudConfig := azclient.AzureCloudConfigFromName("wrong")
+				Expect(cloudConfig).To(BeNil())
+			})
+		})
+		When("cloud name is AzureChinaCloud", func() {
+			It("should return the AzureChinaCloud", func() {
+				cloudConfig := azclient.AzureCloudConfigFromName("AzureChinaCloud")
+				Expect(cloudConfig).ToNot(BeNil())
+				Expect(cloudConfig.ActiveDirectoryAuthorityHost).To(Equal("https://login.chinacloudapi.cn/"))
+				Expect(cloudConfig.Services).NotTo(BeEmpty())
+				Expect(cloudConfig.Services[cloud.ResourceManager].Audience).To(Equal("https://management.core.chinacloudapi.cn"))
+			})
+		})
+	})
+
+	Context("AzureCloudFromEnvironment", func() {
+		When("the environment is empty", func() {
+			It("should return the default cloud", func() {
+				cloudConfig, err := azclient.AzureCloudConfigOverrideFromEnv(nil)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(cloudConfig).ToNot(BeNil())
+				Expect(cloudConfig.ActiveDirectoryAuthorityHost).To(Equal("https://login.microsoftonline.com/"))
+				Expect(cloudConfig.Services).NotTo(BeEmpty())
+				Expect(cloudConfig.Services[cloud.ResourceManager].Audience).To(Equal("https://management.core.windows.net/"))
+			})
+		})
+
+		When("the environment is set,file is not found", func() {
+			It("should return error", func() {
+				os.Setenv(azclient.EnvironmentFilepathName, "notfound")
+				cloudConfig, err := azclient.AzureCloudConfigOverrideFromEnv(nil)
+				Expect(err).To(HaveOccurred())
+				Expect(cloudConfig).To(BeNil())
+			})
+		})
+		When("the environment is set,file is empty", func() {
+			It("should return error", func() {
+				os.Setenv(azclient.EnvironmentFilepathName, "notfound")
+				cloudConfig, err := azclient.AzureCloudConfigOverrideFromEnv(nil)
+				Expect(err).To(HaveOccurred())
+				Expect(cloudConfig).To(BeNil())
+			})
+		})
+		When("the environment is set,file is correct", func() {
+			It("should return error", func() {
+				configFile, err := os.CreateTemp("", "azure.json")
+				Expect(err).ToNot(HaveOccurred())
+				defer os.Remove(configFile.Name())
+
+				err = os.WriteFile(configFile.Name(), []byte(`
+				{
+                   "resourceManagerEndpoint":"https://management.chinacloudapi.cn",
+				   "activeDirectoryEndpoint":"https://login.chinacloudapi.cn",
+				   "tokenAudience":"https://management.core.chinacloudapi.cn/"
+				}`), 0644)
+				Expect(err).ToNot(HaveOccurred())
+				os.Setenv(azclient.EnvironmentFilepathName, configFile.Name())
+				cloudConfig, err := azclient.AzureCloudConfigOverrideFromEnv(&cloud.AzureGovernment)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(cloudConfig).ToNot(BeNil())
+				Expect(cloudConfig.ActiveDirectoryAuthorityHost).To(Equal("https://login.chinacloudapi.cn"))
+				Expect(cloudConfig.Services).NotTo(BeEmpty())
+				Expect(cloudConfig.Services[cloud.ResourceManager].Audience).To(Equal("https://management.core.chinacloudapi.cn/"))
+			})
+		})
+	})
+})
diff --git a/pkg/provider/azure_storageaccount_test.go b/pkg/provider/azure_storageaccount_test.go
index 01653586d..d40b84477 100644
--- a/pkg/provider/azure_storageaccount_test.go
+++ b/pkg/provider/azure_storageaccount_test.go
@@ -21,12 +21,10 @@ import (
 	"fmt"
 	"strings"
 	"testing"
-	"time"
 
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
 	"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
 	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2021-09-01/storage"
-	"github.com/Azure/go-autorest/autorest/date"
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"k8s.io/utils/pointer"
@@ -53,27 +51,20 @@ func TestGetStorageAccessKeys(t *testing.T) {
 
 	cloud := &Cloud{}
 	value := "foo bar"
-	oldTime := date.Time{}
-	oldTime.Time = time.Now().Add(-time.Hour)
-	newValue := "newkey"
-	newTime := date.Time{}
-	newTime.Time = time.Now()
 
 	tests := []struct {
-		results             storage.AccountListKeysResult
-		getLatestAccountKey bool
-		expectedKey         string
-		expectErr           bool
-		err                 error
+		results     storage.AccountListKeysResult
+		expectedKey string
+		expectErr   bool
+		err         error
 	}{
-		{storage.AccountListKeysResult{}, false, "", true, nil},
+		{storage.AccountListKeysResult{}, "", true, nil},
 		{
 			storage.AccountListKeysResult{
 				Keys: &[]storage.AccountKey{
 					{Value: &value},
 				},
 			},
-			false,
 			"bar",
 			false,
 			nil,
@@ -85,43 +76,18 @@ func TestGetStorageAccessKeys(t *testing.T) {
 					{Value: &value},
 				},
 			},
-			false,
-			"bar",
-			false,
-			nil,
-		},
-		{
-			storage.AccountListKeysResult{
-				Keys: &[]storage.AccountKey{
-					{Value: &value, CreationTime: &oldTime},
-					{Value: &newValue, CreationTime: &newTime},
-				},
-			},
-			true,
-			"newkey",
-			false,
-			nil,
-		},
-		{
-			storage.AccountListKeysResult{
-				Keys: &[]storage.AccountKey{
-					{Value: &value, CreationTime: &oldTime},
-					{Value: &newValue, CreationTime: &newTime},
-				},
-			},
-			false,
 			"bar",
 			false,
 			nil,
 		},
-		{storage.AccountListKeysResult{}, false, "", true, fmt.Errorf("test error")},
+		{storage.AccountListKeysResult{}, "", true, fmt.Errorf("test error")},
 	}
 
 	for _, test := range tests {
 		mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl)
 		cloud.StorageAccountClient = mockStorageAccountsClient
 		mockStorageAccountsClient.EXPECT().ListKeys(gomock.Any(), "", "rg", gomock.Any()).Return(test.results, nil).AnyTimes()
-		key, err := cloud.GetStorageAccesskey(ctx, "", "acct", "rg", test.getLatestAccountKey)
+		key, err := cloud.GetStorageAccesskey(ctx, "", "acct", "rg")
 		if test.expectErr && err == nil {
 			t.Errorf("Unexpected non-error")
 			continue