Repository: kubernetes-sigs/cloud-provider-azure (go)

Issue:
feat: support platformSubFaultDomain as node labels
<!-- Feature requests are unlikely to make progress as an issue.

Instead, please suggest enhancements by engaging with SIGs on slack and mailing lists.

A proposal that works through the design along with the implications of the change can be opened as a KEP:
https://git.k8s.io/enhancements/keps#kubernetes-enhancement-proposals-keps
-->

#### What would you like to be added:

IMDS API version 2021-10-21 would add support platformSubFaultDomain and cloud provider should fetch it and expose as a node label `topology.kubernetes.azure.com/sub-fault-domain`.

IMDS API format:

```
curl "http://169.254.169.254/metadata/instance/compute?api-version=2021-10-01&format=json " -H metadata:true
```

Sample response:

```json
{
    "azEnvironment": "AzurePublicCloud",
    "platformFaultDomain": "5",
    "platformSubFaultDomain": "3",
    "platformUpdateDomain": "4",
    /// ...
}    

```

#### Why is this needed:


Pull request description:
feat: support platformSubFaultDomain node label
<!--  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:

#### 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 #809

#### Special notes for your reviewer:

This has not been tested because both aks-engine and capz are not supported to create a VMSS with a sub FD.

#### 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
feat: support platformSubFaultDomain node label
```

#### 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/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 591ba066b..b5a1acf50 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -88,60 +88,11 @@ jobs:
           path: bin/azure-cloud-node-manager-${{ matrix.os }}-${{ matrix.arch }}.exe
           if-no-files-found: error
 
-  build-acr-credential-provider:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        include:
-          - os: linux
-            arch: amd64
-          - os: linux
-            arch: arm
-          - os: linux
-            arch: arm64
-          - os: windows
-            arch: amd64
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v2
-      - name: Get Go version
-        run: echo "GO_VERSION=$(grep -Eo '^go ([0-9]+\.[0-9]+)$' go.mod | cut -d ' ' -f 2)" >> $GITHUB_ENV
-      - name: Setup Golang
-        uses: actions/setup-go@v2
-        with:
-          go-version: ${{ env.GO_VERSION }}
-      - name: Build binary for linux
-        if: matrix.os == 'linux'
-        run: |
-          rm -rf ./bin
-          ARCH=${{ matrix.arch }} make bin/azure-acr-credential-provider
-          mv bin/azure-acr-credential-provider bin/azure-acr-credential-provider-${{ matrix.os }}-${{ matrix.arch }}
-      - name: Build binary for windows
-        if: matrix.os == 'windows'
-        run: |
-          rm -rf ./bin
-          ARCH=${{ matrix.arch }} make bin/azure-acr-credential-provider.exe
-          mv bin/azure-acr-credential-provider.exe bin/azure-acr-credential-provider-${{ matrix.os }}-${{ matrix.arch }}.exe
-      - name: Upload artifact for linux
-        if: matrix.os == 'linux'
-        uses: actions/upload-artifact@v2
-        with:
-          name: acr-credential-provider-${{ matrix.os }}-${{ matrix.arch }}
-          path: bin/azure-acr-credential-provider-${{ matrix.os }}-${{ matrix.arch }}
-      - name: Upload artifact for windows
-        if: matrix.os == 'windows'
-        uses: actions/upload-artifact@v2
-        with:
-          name: acr-credential-provider-${{ matrix.os }}-${{ matrix.arch }}
-          path: bin/azure-acr-credential-provider-${{ matrix.os }}-${{ matrix.arch }}.exe
-          if-no-files-found: error
-
   publish:
     runs-on: ubuntu-latest
     needs:
       - build-cloud-controller-manager
       - build-cloud-node-manager
-      - build-acr-credential-provider
     permissions:
       contents: write
     steps:
@@ -196,5 +147,4 @@ jobs:
           body_path: release-note.md
           files: |
             ./artifacts/cloud-node-manager-*-*/*
-            ./artifacts/cloud-controller-manager-*-*/*
-            ./artifacts/acr-credential-provider-*-*/*
\ No newline at end of file
+            ./artifacts/cloud-controller-manager-*-*/*
\ No newline at end of file
diff --git a/Makefile b/Makefile
index edf514d53..9674523a3 100644
--- a/Makefile
+++ b/Makefile
@@ -108,12 +108,6 @@ $(BIN_DIR)/azure-cloud-node-manager.exe: $(PKG_CONFIG) $(wildcard cmd/cloud-node
 $(BIN_DIR)/azure-cloud-controller-manager: $(PKG_CONFIG) $(wildcard cmd/cloud-controller-manager/*) $(wildcard cmd/cloud-controller-manager/**/*) $(wildcard pkg/**/*) ## Build binary for controller-manager.
 	CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} go build -a -o $(BIN_DIR)/azure-cloud-controller-manager $(shell cat $(PKG_CONFIG)) ./cmd/cloud-controller-manager
 
-$(BIN_DIR)/azure-acr-credential-provider: $(PKG_CONFIG) $(wildcard cmd/acr-credential-provider/*) $(wildcard cmd/acr-credential-provider/**/*) $(wildcard pkg/**/*) ## Build binary for acr-credential-provider.
-	CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} go build -a -o $(BIN_DIR)/azure-acr-credential-provider $(shell cat $(PKG_CONFIG)) ./cmd/acr-credential-provider
-
-$(BIN_DIR)/azure-acr-credential-provider.exe: $(PKG_CONFIG) $(wildcard cmd/acr-credential-provider/*) $(wildcard cmd/acr-credential-provider/**/*) $(wildcard pkg/**/*) ## Build binary for acr-credential-provider.
-	CGO_ENABLED=0 GOOS=windows GOARCH=${ARCH} go build -a -o $(BIN_DIR)/azure-acr-credential-provider.exe $(shell cat $(PKG_CONFIG)) ./cmd/acr-credential-provider
-
 ## --------------------------------------
 ##@ Images
 ## --------------------------------------
@@ -155,8 +149,19 @@ build-node-image-linux: buildx-setup docker-pull-prerequisites ## Build node-man
 		--file cloud-node-manager.Dockerfile \
 		--tag $(NODE_MANAGER_LINUX_FULL_IMAGE_PREFIX)-$(ARCH) .
 
+# TODO(mainred): When using ACR, `az acr login` impacts the authentication of `docker buildx build --push` when the
+# ACR, capz in our case, has anonymous pull enabled.
+# Use `docker login` as a suggested workaround and remove this target when the issue is resolved.
+# Issue link: https://github.com/Azure/acr/issues/582
+# Failed building link: https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/974/pull-cloud-provider-azure-e2e-ccm-capz/1480459040440979456
+.PHONY: docker-login-acr
+docker-login-acr:
+ifneq (,$(findstring capzci.azurecr.io,$(IMAGE_REGISTRY)))
+	docker login -u $(AZURE_CLIENT_ID) -p $(AZURE_CLIENT_SECRET) capzci.azurecr.io
+endif
+
 .PHONY: build-node-image-windows
-build-node-image-windows: buildx-setup $(BIN_DIR)/azure-cloud-node-manager.exe ## Build node-manager image for Windows.
+build-node-image-windows: buildx-setup $(BIN_DIR)/azure-cloud-node-manager.exe docker-login-acr ## Build node-manager image for Windows.
 	docker buildx build --pull \
 		--output=type=$(OUTPUT_TYPE) \
 		--platform windows/$(ARCH) \
diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go
index bcb415587..e409486fe 100644
--- a/pkg/consts/consts.go
+++ b/pkg/consts/consts.go
@@ -40,9 +40,10 @@ const (
 
 	// LabelFailureDomainBetaZone refer to https://github.com/kubernetes/api/blob/8519c5ea46199d57724725d5b969c5e8e0533692/core/v1/well_known_labels.go#L22-L23
 	LabelFailureDomainBetaZone = "failure-domain.beta.kubernetes.io/zone"
-
 	// LabelFailureDomainBetaRegion failure-domain region label
 	LabelFailureDomainBetaRegion = "failure-domain.beta.kubernetes.io/region"
+	// LabelPlatformSubFaultDomain is the label key of platformSubFaultDomain
+	LabelPlatformSubFaultDomain = "topology.kubernetes.azure.com/sub-fault-domain"
 
 	// ADFSIdentitySystem is the override value for tenantID on Azure Stack clouds.
 	ADFSIdentitySystem = "adfs"
diff --git a/pkg/node/node.go b/pkg/node/node.go
index a36127a1e..6fd475e55 100644
--- a/pkg/node/node.go
+++ b/pkg/node/node.go
@@ -78,3 +78,8 @@ func (np *IMDSNodeProvider) InstanceType(ctx context.Context, name types.NodeNam
 func (np *IMDSNodeProvider) GetZone(ctx context.Context, name types.NodeName) (cloudprovider.Zone, error) {
 	return np.azure.GetZone(ctx)
 }
+
+// GetPlatformSubFaultDomain returns the PlatformSubFaultDomain from IMDS if set.
+func (np *IMDSNodeProvider) GetPlatformSubFaultDomain() (string, error) {
+	return np.azure.GetPlatformSubFaultDomain()
+}
diff --git a/pkg/node/nodearm.go b/pkg/node/nodearm.go
index 110a5a462..bc05d9d02 100644
--- a/pkg/node/nodearm.go
+++ b/pkg/node/nodearm.go
@@ -97,3 +97,8 @@ func (np *ARMNodeProvider) GetZone(ctx context.Context, name types.NodeName) (cl
 
 	return np.azure.GetZone(ctx)
 }
+
+// GetPlatformSubFaultDomain returns the PlatformSubFaultDomain from IMDS if set.
+func (np *ARMNodeProvider) GetPlatformSubFaultDomain() (string, error) {
+	return "", nil
+}
diff --git a/pkg/nodemanager/mock/mock_node_provider.go b/pkg/nodemanager/mock/mock_node_provider.go
index 9986bb9b5..4927fb407 100644
--- a/pkg/nodemanager/mock/mock_node_provider.go
+++ b/pkg/nodemanager/mock/mock_node_provider.go
@@ -17,104 +17,109 @@ limitations under the License.
 package mock
 
 import (
-	"context"
+	context "context"
+	reflect "reflect"
 
+	gomock "github.com/golang/mock/gomock"
+	v1 "k8s.io/api/core/v1"
+	types "k8s.io/apimachinery/pkg/types"
 	cloudprovider "k8s.io/cloud-provider"
+)
 
-	mock "github.com/stretchr/testify/mock"
+// NodeProvider is a mock of NodeProvider interface.
+type NodeProvider struct {
+	ctrl     *gomock.Controller
+	recorder *NodeProviderMockRecorder
+}
 
-	types "k8s.io/apimachinery/pkg/types"
+// NodeProviderMockRecorder is the mock recorder for NodeProvider.
+type NodeProviderMockRecorder struct {
+	mock *NodeProvider
+}
 
-	v1 "k8s.io/api/core/v1"
-)
+// NewMockNodeProvider creates a new mock instance.
+func NewMockNodeProvider(ctrl *gomock.Controller) *NodeProvider {
+	mock := &NodeProvider{ctrl: ctrl}
+	mock.recorder = &NodeProviderMockRecorder{mock}
+	return mock
+}
 
-// NodeProvider is an autogenerated mock type for the NodeProvider type
-type NodeProvider struct {
-	mock.Mock
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *NodeProvider) EXPECT() *NodeProviderMockRecorder {
+	return m.recorder
+}
+
+// GetPlatformSubFaultDomain mocks base method.
+func (m *NodeProvider) GetPlatformSubFaultDomain() (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPlatformSubFaultDomain")
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetPlatformSubFaultDomain indicates an expected call of GetPlatformSubFaultDomain.
+func (mr *NodeProviderMockRecorder) GetPlatformSubFaultDomain() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlatformSubFaultDomain", reflect.TypeOf((*NodeProvider)(nil).GetPlatformSubFaultDomain))
+}
+
+// GetZone mocks base method.
+func (m *NodeProvider) GetZone(arg0 context.Context, arg1 types.NodeName) (cloudprovider.Zone, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetZone", arg0, arg1)
+	ret0, _ := ret[0].(cloudprovider.Zone)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetZone indicates an expected call of GetZone.
+func (mr *NodeProviderMockRecorder) GetZone(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZone", reflect.TypeOf((*NodeProvider)(nil).GetZone), arg0, arg1)
+}
+
+// InstanceID mocks base method.
+func (m *NodeProvider) InstanceID(arg0 context.Context, arg1 types.NodeName) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InstanceID", arg0, arg1)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// InstanceID indicates an expected call of InstanceID.
+func (mr *NodeProviderMockRecorder) InstanceID(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceID", reflect.TypeOf((*NodeProvider)(nil).InstanceID), arg0, arg1)
 }
 
-// GetZone provides a mock function with given fields: ctx
-func (_m *NodeProvider) GetZone(ctx context.Context, name types.NodeName) (cloudprovider.Zone, error) {
-	ret := _m.Called(ctx)
-
-	var r0 cloudprovider.Zone
-	if rf, ok := ret.Get(0).(func(context.Context) cloudprovider.Zone); ok {
-		r0 = rf(ctx)
-	} else {
-		r0 = ret.Get(0).(cloudprovider.Zone)
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context) error); ok {
-		r1 = rf(ctx)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
+// InstanceType mocks base method.
+func (m *NodeProvider) InstanceType(arg0 context.Context, arg1 types.NodeName) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InstanceType", arg0, arg1)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
-// InstanceID provides a mock function with given fields: ctx, name
-func (_m *NodeProvider) InstanceID(ctx context.Context, name types.NodeName) (string, error) {
-	ret := _m.Called(ctx, name)
-
-	var r0 string
-	if rf, ok := ret.Get(0).(func(context.Context, types.NodeName) string); ok {
-		r0 = rf(ctx, name)
-	} else {
-		r0 = ret.Get(0).(string)
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, types.NodeName) error); ok {
-		r1 = rf(ctx, name)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
+// InstanceType indicates an expected call of InstanceType.
+func (mr *NodeProviderMockRecorder) InstanceType(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceType", reflect.TypeOf((*NodeProvider)(nil).InstanceType), arg0, arg1)
 }
 
-// InstanceType provides a mock function with given fields: ctx, name
-func (_m *NodeProvider) InstanceType(ctx context.Context, name types.NodeName) (string, error) {
-	ret := _m.Called(ctx, name)
-
-	var r0 string
-	if rf, ok := ret.Get(0).(func(context.Context, types.NodeName) string); ok {
-		r0 = rf(ctx, name)
-	} else {
-		r0 = ret.Get(0).(string)
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, types.NodeName) error); ok {
-		r1 = rf(ctx, name)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
+// NodeAddresses mocks base method.
+func (m *NodeProvider) NodeAddresses(arg0 context.Context, arg1 types.NodeName) ([]v1.NodeAddress, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NodeAddresses", arg0, arg1)
+	ret0, _ := ret[0].([]v1.NodeAddress)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
-// NodeAddresses provides a mock function with given fields: ctx, name
-func (_m *NodeProvider) NodeAddresses(ctx context.Context, name types.NodeName) ([]v1.NodeAddress, error) {
-	ret := _m.Called(ctx, name)
-
-	var r0 []v1.NodeAddress
-	if rf, ok := ret.Get(0).(func(context.Context, types.NodeName) []v1.NodeAddress); ok {
-		r0 = rf(ctx, name)
-	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]v1.NodeAddress)
-		}
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, types.NodeName) error); ok {
-		r1 = rf(ctx, name)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
+// NodeAddresses indicates an expected call of NodeAddresses.
+func (mr *NodeProviderMockRecorder) NodeAddresses(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeAddresses", reflect.TypeOf((*NodeProvider)(nil).NodeAddresses), arg0, arg1)
 }
diff --git a/pkg/nodemanager/nodemanager.go b/pkg/nodemanager/nodemanager.go
index 67ca91fa1..d61137a6e 100644
--- a/pkg/nodemanager/nodemanager.go
+++ b/pkg/nodemanager/nodemanager.go
@@ -44,6 +44,8 @@ import (
 	cloudnodeutil "k8s.io/cloud-provider/node/helpers"
 	nodeutil "k8s.io/component-helpers/node/util"
 	"k8s.io/klog/v2"
+
+	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 )
 
 // NodeProvider defines the interfaces for node provider.
@@ -56,6 +58,8 @@ type NodeProvider interface {
 	InstanceType(ctx context.Context, name types.NodeName) (string, error)
 	// GetZone returns the Zone containing the current failure zone and locality region that the program is running in
 	GetZone(ctx context.Context, name types.NodeName) (cloudprovider.Zone, error)
+	// GetPlatformSubFaultDomain returns the PlatformSubFaultDomain from IMDS if set.
+	GetPlatformSubFaultDomain() (string, error)
 }
 
 // labelReconcileInfo lists Node labels to reconcile, and how to reconcile them.
@@ -492,6 +496,20 @@ func (cnc *CloudNodeController) getNodeModifiersFromCloudProvider(ctx context.Co
 		})
 	}
 
+	platformSubFaultDomain, err := cnc.getPlatformSubFaultDomain()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get platformSubFaultDomain: %w", err)
+	}
+	if platformSubFaultDomain != "" {
+		klog.V(2).Infof("Adding node label from cloud provider: %s=%s", consts.LabelPlatformSubFaultDomain, platformSubFaultDomain)
+		nodeModifiers = append(nodeModifiers, func(n *v1.Node) {
+			if n.Labels == nil {
+				n.Labels = map[string]string{}
+			}
+			n.Labels[consts.LabelPlatformSubFaultDomain] = platformSubFaultDomain
+		})
+	}
+
 	return nodeModifiers, nil
 }
 
@@ -598,6 +616,14 @@ func (cnc *CloudNodeController) getZoneByName(ctx context.Context, node *v1.Node
 	return zone, nil
 }
 
+func (cnc *CloudNodeController) getPlatformSubFaultDomain() (string, error) {
+	subFD, err := cnc.nodeProvider.GetPlatformSubFaultDomain()
+	if err != nil {
+		return "", fmt.Errorf("cnc.getPlatformSubfaultDomain: %w", err)
+	}
+	return subFD, nil
+}
+
 func (cnc *CloudNodeController) updateNetworkingCondition(node *v1.Node, networkReady bool) error {
 	_, condition := nodeutil.GetNodeCondition(&(node.Status), v1.NodeNetworkUnavailable)
 	if networkReady && condition != nil && condition.Status == v1.ConditionFalse {
diff --git a/pkg/provider/azure_instance_metadata.go b/pkg/provider/azure_instance_metadata.go
index 629b8ec10..708804a19 100644
--- a/pkg/provider/azure_instance_metadata.go
+++ b/pkg/provider/azure_instance_metadata.go
@@ -59,18 +59,19 @@ type Subnet struct {
 
 // ComputeMetadata represents compute information
 type ComputeMetadata struct {
-	Environment    string `json:"azEnvironment,omitempty"`
-	SKU            string `json:"sku,omitempty"`
-	Name           string `json:"name,omitempty"`
-	Zone           string `json:"zone,omitempty"`
-	VMSize         string `json:"vmSize,omitempty"`
-	OSType         string `json:"osType,omitempty"`
-	Location       string `json:"location,omitempty"`
-	FaultDomain    string `json:"platformFaultDomain,omitempty"`
-	UpdateDomain   string `json:"platformUpdateDomain,omitempty"`
-	ResourceGroup  string `json:"resourceGroupName,omitempty"`
-	VMScaleSetName string `json:"vmScaleSetName,omitempty"`
-	SubscriptionID string `json:"subscriptionId,omitempty"`
+	Environment            string `json:"azEnvironment,omitempty"`
+	SKU                    string `json:"sku,omitempty"`
+	Name                   string `json:"name,omitempty"`
+	Zone                   string `json:"zone,omitempty"`
+	VMSize                 string `json:"vmSize,omitempty"`
+	OSType                 string `json:"osType,omitempty"`
+	Location               string `json:"location,omitempty"`
+	FaultDomain            string `json:"platformFaultDomain,omitempty"`
+	PlatformSubFaultDomain string `json:"platformSubFaultDomain,omitempty"`
+	UpdateDomain           string `json:"platformUpdateDomain,omitempty"`
+	ResourceGroup          string `json:"resourceGroupName,omitempty"`
+	VMScaleSetName         string `json:"vmScaleSetName,omitempty"`
+	SubscriptionID         string `json:"subscriptionId,omitempty"`
 }
 
 // InstanceMetadata represents instance information.
diff --git a/pkg/provider/azure_zones.go b/pkg/provider/azure_zones.go
index 52e178c01..68f8f8203 100644
--- a/pkg/provider/azure_zones.go
+++ b/pkg/provider/azure_zones.go
@@ -217,3 +217,20 @@ func (az *Cloud) GetZoneByNodeName(ctx context.Context, nodeName types.NodeName)
 
 	return az.VMSet.GetZoneByNodeName(string(nodeName))
 }
+
+// GetPlatformSubFaultDomain returns the PlatformSubFaultDomain from IMDS if set.
+func (az *Cloud) GetPlatformSubFaultDomain() (string, error) {
+	if az.UseInstanceMetadata {
+		metadata, err := az.Metadata.GetMetadata(azcache.CacheReadTypeUnsafe)
+		if err != nil {
+			klog.Errorf("GetPlatformSubFaultDomain: failed to GetMetadata: %s", err.Error())
+			return "", err
+		}
+		if metadata.Compute == nil {
+			_ = az.Metadata.imsCache.Delete(consts.MetadataCacheKey)
+			return "", errors.New("failure of getting compute information from instance metadata")
+		}
+		return metadata.Compute.PlatformSubFaultDomain, nil
+	}
+	return "", nil
+}


Test patch:
diff --git a/pkg/nodemanager/nodemanager_test.go b/pkg/nodemanager/nodemanager_test.go
index d4e382c86..1441c4112 100644
--- a/pkg/nodemanager/nodemanager_test.go
+++ b/pkg/nodemanager/nodemanager_test.go
@@ -23,6 +23,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
@@ -35,14 +38,15 @@ import (
 	cloudproviderapi "k8s.io/cloud-provider/api"
 	"k8s.io/klog/v2"
 
+	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	mocknodeprovider "sigs.k8s.io/cloud-provider-azure/pkg/nodemanager/mock"
 	"sigs.k8s.io/cloud-provider-azure/pkg/util/controller/testutil"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
 )
 
 func TestEnsureNodeExistsByProviderID(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	testCases := []struct {
 		testName           string
 		node               *v1.Node
@@ -105,10 +109,9 @@ func TestEnsureNodeExistsByProviderID(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.testName, func(t *testing.T) {
 			ctx := context.TODO()
-			mockNP := &mocknodeprovider.NodeProvider{}
+			mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
 			if tc.node.Spec.ProviderID == "" {
-				mockNP.On("InstanceID", ctx, types.NodeName(tc.node.Name)).
-					Return(tc.providerID, tc.nodeNameErr)
+				mockNP.EXPECT().InstanceID(ctx, types.NodeName(tc.node.Name)).Return(tc.providerID, tc.nodeNameErr)
 			}
 
 			cnc := &CloudNodeController{nodeProvider: mockNP}
@@ -121,6 +124,9 @@ func TestEnsureNodeExistsByProviderID(t *testing.T) {
 
 // This test checks that a node with the external cloud provider taint is cloudprovider initialized
 func TestNodeInitialized(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	fnh := &testutil.FakeNodeHandler{
 		Existing: []*v1.Node{
 			{
@@ -155,14 +161,14 @@ func TestNodeInitialized(t *testing.T) {
 
 	ctx := context.TODO()
 	factory := informers.NewSharedInformerFactory(fnh, 0)
-	mockNP := &mocknodeprovider.NodeProvider{}
-	mockNP.On("InstanceID", ctx, types.NodeName("node0")).Return("node0", nil)
-	mockNP.On("InstanceType", ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", ctx).Return(cloudprovider.Zone{
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
+	mockNP.EXPECT().InstanceID(ctx, types.NodeName("node0")).Return("node0", nil)
+	mockNP.EXPECT().InstanceType(ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
+	mockNP.EXPECT().GetZone(ctx, gomock.Any()).Return(cloudprovider.Zone{
 		Region:        "eastus",
 		FailureDomain: "1",
 	}, nil)
-	mockNP.On("NodeAddresses", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().NodeAddresses(ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeHostName,
 			Address: "node0.cloud.internal",
@@ -175,7 +181,8 @@ func TestNodeInitialized(t *testing.T) {
 			Type:    v1.NodeExternalIP,
 			Address: "132.143.154.163",
 		},
-	}, nil)
+	}, nil).AnyTimes()
+	mockNP.EXPECT().GetPlatformSubFaultDomain().Return("1", nil)
 
 	cloudNodeController := NewCloudNodeController(
 		"node0",
@@ -190,9 +197,13 @@ func TestNodeInitialized(t *testing.T) {
 	assert.Equal(t, 1, len(fnh.UpdatedNodes), "Node was not updated")
 	assert.Equal(t, "node0", fnh.UpdatedNodes[0].Name, "Node was not updated")
 	assert.Equal(t, 0, len(fnh.UpdatedNodes[0].Spec.Taints), "Node Taint was not removed after cloud init")
+	assert.Equal(t, "1", fnh.UpdatedNodes[0].Labels[consts.LabelPlatformSubFaultDomain])
 }
 
 func TestUpdateCloudNode(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	fnh := &testutil.FakeNodeHandler{
 		Existing: []*v1.Node{
 			{
@@ -227,14 +238,14 @@ func TestUpdateCloudNode(t *testing.T) {
 
 	ctx := context.TODO()
 	factory := informers.NewSharedInformerFactory(fnh, 0)
-	mockNP := &mocknodeprovider.NodeProvider{}
-	mockNP.On("InstanceID", ctx, types.NodeName("node0")).Return("node0", nil)
-	mockNP.On("InstanceType", ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", ctx).Return(cloudprovider.Zone{
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
+	mockNP.EXPECT().InstanceID(ctx, types.NodeName("node0")).Return("node0", nil)
+	mockNP.EXPECT().InstanceType(ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
+	mockNP.EXPECT().GetZone(ctx, gomock.Any()).Return(cloudprovider.Zone{
 		Region:        "eastus",
 		FailureDomain: "1",
 	}, nil)
-	mockNP.On("NodeAddresses", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().NodeAddresses(ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeHostName,
 			Address: "node0.cloud.internal",
@@ -247,7 +258,8 @@ func TestUpdateCloudNode(t *testing.T) {
 			Type:    v1.NodeExternalIP,
 			Address: "132.143.154.163",
 		},
-	}, nil)
+	}, nil).AnyTimes()
+	mockNP.EXPECT().GetPlatformSubFaultDomain().Return("1", nil)
 
 	eventBroadcaster := record.NewBroadcaster()
 	cloudNodeController := NewCloudNodeController(
@@ -266,10 +278,14 @@ func TestUpdateCloudNode(t *testing.T) {
 	assert.Equal(t, 0, len(fnh.UpdatedNodes[0].Spec.Taints), "Node Taint was not removed after cloud init")
 	assert.Equal(t, 2, len(fnh.UpdatedNodes[0].Status.Conditions), "Node Contions was not updated")
 	assert.Equal(t, "NetworkUnavailable", string(fnh.UpdatedNodes[0].Status.Conditions[0].Type), "Node Condition NetworkUnavailable was not updated")
+	assert.Equal(t, "1", fnh.UpdatedNodes[0].Labels[consts.LabelPlatformSubFaultDomain])
 }
 
 // This test checks that a node without the external cloud provider taint are NOT cloudprovider initialized
 func TestNodeIgnored(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	fnh := &testutil.FakeNodeHandler{
 		Existing: []*v1.Node{
 			{
@@ -293,29 +309,8 @@ func TestNodeIgnored(t *testing.T) {
 		DeleteWaitChan: make(chan struct{}),
 	}
 
-	ctx := context.TODO()
 	factory := informers.NewSharedInformerFactory(fnh, 0)
-	mockNP := &mocknodeprovider.NodeProvider{}
-	mockNP.On("InstanceID", ctx, types.NodeName("node0")).Return("node0", nil)
-	mockNP.On("InstanceType", ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", ctx).Return(cloudprovider.Zone{
-		Region:        "eastus",
-		FailureDomain: "1",
-	}, nil)
-	mockNP.On("NodeAddresses", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
-		{
-			Type:    v1.NodeHostName,
-			Address: "node0.cloud.internal",
-		},
-		{
-			Type:    v1.NodeInternalIP,
-			Address: "10.0.0.1",
-		},
-		{
-			Type:    v1.NodeExternalIP,
-			Address: "132.143.154.163",
-		},
-	}, nil)
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
 
 	eventBroadcaster := record.NewBroadcaster()
 	cloudNodeController := NewCloudNodeController(
@@ -335,6 +330,9 @@ func TestNodeIgnored(t *testing.T) {
 // This test checks that a node with the external cloud provider taint is cloudprovider initialized and
 // and that zone labels are added correctly
 func TestZoneInitialized(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	fnh := &testutil.FakeNodeHandler{
 		Existing: []*v1.Node{
 			{
@@ -370,14 +368,14 @@ func TestZoneInitialized(t *testing.T) {
 
 	ctx := context.TODO()
 	factory := informers.NewSharedInformerFactory(fnh, 0)
-	mockNP := &mocknodeprovider.NodeProvider{}
-	mockNP.On("InstanceID", ctx, types.NodeName("node0")).Return("node0", nil)
-	mockNP.On("InstanceType", ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", ctx).Return(cloudprovider.Zone{
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
+	mockNP.EXPECT().InstanceID(ctx, types.NodeName("node0")).Return("node0", nil)
+	mockNP.EXPECT().InstanceType(ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
+	mockNP.EXPECT().GetZone(ctx, gomock.Any()).Return(cloudprovider.Zone{
 		Region:        "eastus",
 		FailureDomain: "eastus-1",
 	}, nil)
-	mockNP.On("NodeAddresses", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().NodeAddresses(ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeHostName,
 			Address: "node0.cloud.internal",
@@ -390,7 +388,8 @@ func TestZoneInitialized(t *testing.T) {
 			Type:    v1.NodeExternalIP,
 			Address: "132.143.154.163",
 		},
-	}, nil)
+	}, nil).AnyTimes()
+	mockNP.EXPECT().GetPlatformSubFaultDomain().Return("", nil)
 
 	eventBroadcaster := record.NewBroadcaster()
 	cloudNodeController := &CloudNodeController{
@@ -421,6 +420,9 @@ func TestZoneInitialized(t *testing.T) {
 // This test checks that a node with the external cloud provider taint is cloudprovider initialized and
 // and nodeAddresses are updated from the cloudprovider
 func TestAddCloudNode(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 	fnh := &testutil.FakeNodeHandler{
@@ -461,14 +463,14 @@ func TestAddCloudNode(t *testing.T) {
 		DeleteWaitChan: make(chan struct{}),
 	}
 
-	mockNP := &mocknodeprovider.NodeProvider{}
-	mockNP.On("InstanceID", mock.Anything, types.NodeName("node0")).Return("node0", nil)
-	mockNP.On("InstanceType", mock.Anything, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", mock.Anything).Return(cloudprovider.Zone{
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
+	mockNP.EXPECT().InstanceID(gomock.Any(), types.NodeName("node0")).Return("node0", nil)
+	mockNP.EXPECT().InstanceType(gomock.Any(), types.NodeName("node0")).Return("Standard_D2_v3", nil)
+	mockNP.EXPECT().GetZone(gomock.Any(), gomock.Any()).Return(cloudprovider.Zone{
 		Region:        "eastus",
 		FailureDomain: "eastus-1",
 	}, nil)
-	mockNP.On("NodeAddresses", mock.Anything, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().NodeAddresses(gomock.Any(), types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeHostName,
 			Address: "node0.cloud.internal",
@@ -481,7 +483,8 @@ func TestAddCloudNode(t *testing.T) {
 			Type:    v1.NodeExternalIP,
 			Address: "132.143.154.163",
 		},
-	}, nil)
+	}, nil).AnyTimes()
+	mockNP.EXPECT().GetPlatformSubFaultDomain().Return("", nil)
 
 	factory := informers.NewSharedInformerFactory(fnh, 0)
 	nodeInformer := factory.Core().V1().Nodes()
@@ -503,6 +506,9 @@ func TestAddCloudNode(t *testing.T) {
 }
 
 func TestUpdateNodeAddresses(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 	fnh := &testutil.FakeNodeHandler{
@@ -538,7 +544,7 @@ func TestUpdateNodeAddresses(t *testing.T) {
 		DeleteWaitChan: make(chan struct{}),
 	}
 
-	mockNP := &mocknodeprovider.NodeProvider{}
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
 	factory := informers.NewSharedInformerFactory(fnh, 0)
 	nodeInformer := factory.Core().V1().Nodes()
 
@@ -552,13 +558,8 @@ func TestUpdateNodeAddresses(t *testing.T) {
 	factory.Start(ctx.Done())
 	cache.WaitForCacheSync(ctx.Done(), nodeInformer.Informer().HasSynced)
 
-	mockNP.On("InstanceID", mock.Anything, types.NodeName("node0")).Return("node0", nil)
-	mockNP.On("InstanceType", mock.Anything, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", mock.Anything).Return(cloudprovider.Zone{
-		Region:        "eastus",
-		FailureDomain: "eastus-1",
-	}, nil)
-	mockNP.On("NodeAddresses", mock.Anything, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().InstanceID(gomock.Any(), types.NodeName("node0")).Return("node0", nil)
+	mockNP.EXPECT().NodeAddresses(ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeHostName,
 			Address: "node0.cloud.internal",
@@ -576,6 +577,9 @@ func TestUpdateNodeAddresses(t *testing.T) {
 // This test checks that a node with the external cloud provider taint is cloudprovider initialized and
 // and the provided node ip is validated with the cloudprovider and nodeAddresses are updated from the cloudprovider
 func TestNodeProvidedIPAddresses(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	fnh := &testutil.FakeNodeHandler{
 		Existing: []*v1.Node{
 			{
@@ -626,14 +630,13 @@ func TestNodeProvidedIPAddresses(t *testing.T) {
 
 	ctx := context.TODO()
 	factory := informers.NewSharedInformerFactory(fnh, 0)
-	mockNP := &mocknodeprovider.NodeProvider{}
-	mockNP.On("InstanceID", ctx, types.NodeName("node0")).Return("node0", nil)
-	mockNP.On("InstanceType", ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", ctx).Return(cloudprovider.Zone{
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
+	mockNP.EXPECT().InstanceType(ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
+	mockNP.EXPECT().GetZone(ctx, gomock.Any()).Return(cloudprovider.Zone{
 		Region:        "eastus",
 		FailureDomain: "eastus-1",
 	}, nil)
-	mockNP.On("NodeAddresses", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().NodeAddresses(ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeInternalIP,
 			Address: "10.0.0.1",
@@ -642,7 +645,8 @@ func TestNodeProvidedIPAddresses(t *testing.T) {
 			Type:    v1.NodeExternalIP,
 			Address: "132.143.154.163",
 		},
-	}, nil)
+	}, nil).AnyTimes()
+	mockNP.EXPECT().GetPlatformSubFaultDomain().Return("", nil)
 
 	eventBroadcaster := record.NewBroadcaster()
 	cloudNodeController := NewCloudNodeController(
@@ -911,10 +915,14 @@ func TestNodeAddressesChangeDetected(t *testing.T) {
 		"Node address changes are not detected correctly")
 }
 
-// This test checks that a node with the external cloud provider taint is cloudprovider initialized and
+// This test checks that a node with the external cloud provider taint is cloudprovider initialized
 // and node addresses will not be updated when node isn't present according to the cloudprovider
 func TestNodeAddressesNotUpdate(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	fnh := &testutil.FakeNodeHandler{
+		Clientset: fake.NewSimpleClientset(),
 		Existing: []*v1.Node{
 			{
 				ObjectMeta: metav1.ObjectMeta{
@@ -947,27 +955,8 @@ func TestNodeAddressesNotUpdate(t *testing.T) {
 
 	ctx := context.TODO()
 	factory := informers.NewSharedInformerFactory(fnh, 0)
-	mockNP := &mocknodeprovider.NodeProvider{}
-	mockNP.On("InstanceID", ctx, types.NodeName("node0")).Return("", cloudprovider.InstanceNotFound)
-	mockNP.On("InstanceType", ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", ctx).Return(cloudprovider.Zone{
-		Region:        "eastus",
-		FailureDomain: "eastus-1",
-	}, nil)
-	mockNP.On("NodeAddresses", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
-		{
-			Type:    v1.NodeHostName,
-			Address: "node0",
-		},
-		{
-			Type:    v1.NodeInternalIP,
-			Address: "10.0.0.1",
-		},
-		{
-			Type:    v1.NodeExternalIP,
-			Address: "132.143.154.163",
-		},
-	}, nil)
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
+	mockNP.EXPECT().InstanceID(ctx, types.NodeName("node0")).Return("", nil)
 
 	eventBroadcaster := record.NewBroadcaster()
 	cloudNodeController := &CloudNodeController{
@@ -990,6 +979,9 @@ func TestNodeAddressesNotUpdate(t *testing.T) {
 
 // This test checks that a node is set with the correct providerID
 func TestNodeProviderID(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	fnh := &testutil.FakeNodeHandler{
 		Existing: []*v1.Node{
 			{
@@ -1030,14 +1022,14 @@ func TestNodeProviderID(t *testing.T) {
 
 	ctx := context.TODO()
 	factory := informers.NewSharedInformerFactory(fnh, 0)
-	mockNP := &mocknodeprovider.NodeProvider{}
-	mockNP.On("InstanceID", ctx, types.NodeName("node0")).Return("test:///12345", nil)
-	mockNP.On("InstanceType", ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", ctx).Return(cloudprovider.Zone{
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
+	mockNP.EXPECT().InstanceID(ctx, types.NodeName("node0")).Return("test:///12345", nil)
+	mockNP.EXPECT().InstanceType(ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
+	mockNP.EXPECT().GetZone(ctx, gomock.Any()).Return(cloudprovider.Zone{
 		Region:        "eastus",
 		FailureDomain: "eastus-1",
 	}, nil)
-	mockNP.On("NodeAddresses", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().NodeAddresses(ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeHostName,
 			Address: "node0",
@@ -1050,7 +1042,8 @@ func TestNodeProviderID(t *testing.T) {
 			Type:    v1.NodeExternalIP,
 			Address: "132.143.154.163",
 		},
-	}, nil)
+	}, nil).AnyTimes()
+	mockNP.EXPECT().GetPlatformSubFaultDomain().Return("", nil).AnyTimes()
 
 	eventBroadcaster := record.NewBroadcaster()
 	cloudNodeController := &CloudNodeController{
@@ -1072,6 +1065,9 @@ func TestNodeProviderID(t *testing.T) {
 
 // This test checks that a node's provider ID will not be overwritten
 func TestNodeProviderIDAlreadySet(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	fnh := &testutil.FakeNodeHandler{
 		Existing: []*v1.Node{
 			{
@@ -1113,14 +1109,13 @@ func TestNodeProviderIDAlreadySet(t *testing.T) {
 
 	ctx := context.TODO()
 	factory := informers.NewSharedInformerFactory(fnh, 0)
-	mockNP := &mocknodeprovider.NodeProvider{}
-	mockNP.On("InstanceID", ctx, types.NodeName("node0")).Return("test:///12345", nil)
-	mockNP.On("InstanceType", ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
-	mockNP.On("GetZone", ctx).Return(cloudprovider.Zone{
+	mockNP := mocknodeprovider.NewMockNodeProvider(ctrl)
+	mockNP.EXPECT().InstanceType(ctx, types.NodeName("node0")).Return("Standard_D2_v3", nil)
+	mockNP.EXPECT().GetZone(ctx, gomock.Any()).Return(cloudprovider.Zone{
 		Region:        "eastus",
 		FailureDomain: "eastus-1",
 	}, nil)
-	mockNP.On("NodeAddresses", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().NodeAddresses(ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeHostName,
 			Address: "node0",
@@ -1133,7 +1128,8 @@ func TestNodeProviderIDAlreadySet(t *testing.T) {
 			Type:    v1.NodeExternalIP,
 			Address: "132.143.154.163",
 		},
-	}, nil)
+	}, nil).AnyTimes()
+	mockNP.EXPECT().GetPlatformSubFaultDomain().Return("", nil).AnyTimes()
 
 	eventBroadcaster := record.NewBroadcaster()
 	cloudNodeController := &CloudNodeController{
diff --git a/pkg/provider/azure_zones_test.go b/pkg/provider/azure_zones_test.go
index 4c5294c5c..3f25b3ef5 100644
--- a/pkg/provider/azure_zones_test.go
+++ b/pkg/provider/azure_zones_test.go
@@ -18,6 +18,7 @@ package provider
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net"
 	"net/http"
@@ -190,6 +191,62 @@ func TestGetZone(t *testing.T) {
 	}
 }
 
+func TestGetPlatformSubFaultDomain(t *testing.T) {
+	for _, testCase := range []struct {
+		description string
+		nilCompute  bool
+		expectedErr error
+	}{
+		{
+			description: "GetPlatformSubFaultDomain should parse the correct platformSubFaultDomain",
+		},
+		{
+			description: "GetPlatformSubFaultDomain should report an error if the compute is nil",
+			nilCompute:  true,
+			expectedErr: errors.New("failure of getting compute information from instance metadata"),
+		},
+	} {
+		t.Run(testCase.description, func(t *testing.T) {
+			cloud := &Cloud{
+				Config: Config{
+					Location:            "eastus",
+					UseInstanceMetadata: true,
+				},
+			}
+			listener, err := net.Listen("tcp", "127.0.0.1:0")
+			if err != nil {
+				t.Errorf("Test [%s] unexpected error: %v", testCase.description, err)
+			}
+
+			respString := `{"compute":{"zone":"1", "platformFaultDomain":"1", "location":"westus", "platformSubFaultDomain": "2"}}`
+			if testCase.nilCompute {
+				respString = "{}"
+			}
+			mux := http.NewServeMux()
+			mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				fmt.Fprint(w, respString)
+			}))
+			go func() {
+				_ = http.Serve(listener, mux)
+			}()
+			defer listener.Close()
+
+			cloud.Metadata, err = NewInstanceMetadataService("http://" + listener.Addr().String() + "/")
+			if err != nil {
+				t.Errorf("Test [%s] unexpected error: %v", testCase.description, err)
+			}
+
+			fd, err := cloud.GetPlatformSubFaultDomain()
+			if testCase.expectedErr != nil {
+				assert.Equal(t, testCase.expectedErr, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, "2", fd)
+			}
+		})
+	}
+}
+
 func TestMakeZone(t *testing.T) {
 	az := &Cloud{}
 	zone := az.makeZone("EASTUS", 2)