Repository: kubernetes-sigs/cloud-provider-azure
Pull request #1081 (2022-01-28 09:00:19)

[release-0.7] 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:

[release-0.7] feat: support platformSubFaultDomain node label

#### 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
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/pkg/consts/consts.go b/pkg/consts/consts.go
index 3620f77ed..2f103ec40 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 0c3d25ea6..460bed56e 100644
--- a/pkg/node/node.go
+++ b/pkg/node/node.go
@@ -77,3 +77,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 8faeb4431..355bd9f0d 100644
--- a/pkg/node/nodearm.go
+++ b/pkg/node/nodearm.go
@@ -96,3 +96,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 22a955011..fa1935211 100644
--- a/pkg/nodemanager/nodemanager.go
+++ b/pkg/nodemanager/nodemanager.go
@@ -44,6 +44,8 @@ import (
 	cloudproviderapi "k8s.io/cloud-provider/api"
 	cloudnodeutil "k8s.io/cloud-provider/node/helpers"
 	"k8s.io/klog"
+
+	"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.
@@ -497,6 +501,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
 }
 
@@ -603,6 +621,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 := cloudnodeutil.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 48d9cb7ea..1a61891a0 100644
--- a/pkg/provider/azure_zones.go
+++ b/pkg/provider/azure_zones.go
@@ -209,3 +209,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 100eb0835..d84bdacfb 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"
@@ -34,13 +37,15 @@ import (
 	cloudproviderapi "k8s.io/cloud-provider/api"
 	"k8s.io/klog"
 
+	"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"
 )
 
 func TestEnsureNodeExistsByProviderID(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	testCases := []struct {
 		testName           string
 		node               *v1.Node
@@ -103,10 +108,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}
@@ -119,6 +123,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{
 			{
@@ -153,14 +160,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",
@@ -173,7 +180,8 @@ func TestNodeInitialized(t *testing.T) {
 			Type:    v1.NodeExternalIP,
 			Address: "132.143.154.163",
 		},
-	}, nil)
+	}, nil).AnyTimes()
+	mockNP.EXPECT().GetPlatformSubFaultDomain().Return("1", nil)
 
 	eventBroadcaster := record.NewBroadcaster()
 	cloudNodeController := &CloudNodeController{
@@ -191,10 +199,14 @@ 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])
 }
 
 // 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{
 			{
@@ -218,29 +230,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 := &CloudNodeController{
@@ -260,6 +251,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{
 			{
@@ -295,14 +289,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",
@@ -315,7 +309,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{
@@ -346,6 +341,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 TestNodeAddresses(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
 	fnh := &testutil.FakeNodeHandler{
 		Existing: []*v1.Node{
 			{
@@ -384,16 +382,15 @@ func TestNodeAddresses(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{
+	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", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().NodeAddresses(gomock.Any(), types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeHostName,
 			Address: "node0.cloud.internal",
@@ -406,7 +403,8 @@ func TestNodeAddresses(t *testing.T) {
 			Type:    v1.NodeExternalIP,
 			Address: "132.143.154.163",
 		},
-	}, nil)
+	}, nil).Times(2)
+	mockNP.EXPECT().GetPlatformSubFaultDomain().Return("", nil)
 
 	eventBroadcaster := record.NewBroadcaster()
 	cloudNodeController := &CloudNodeController{
@@ -424,8 +422,7 @@ func TestNodeAddresses(t *testing.T) {
 	assert.Equal(t, "node0", fnh.UpdatedNodes[0].Name, "Node was not updated")
 	assert.Equal(t, 3, len(fnh.UpdatedNodes[0].Status.Addresses), "Node status not updated")
 
-	mockNP.ExpectedCalls = nil
-	mockNP.On("NodeAddresses", ctx, types.NodeName("node0")).Return([]v1.NodeAddress{
+	mockNP.EXPECT().NodeAddresses(gomock.Any(), types.NodeName("node0")).Return([]v1.NodeAddress{
 		{
 			Type:    v1.NodeHostName,
 			Address: "node0.cloud.internal",
@@ -443,6 +440,9 @@ func TestNodeAddresses(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{
 			{
@@ -493,14 +493,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",
@@ -509,7 +508,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 := &CloudNodeController{
@@ -773,10 +773,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{
@@ -809,27 +813,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{
@@ -849,6 +834,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{
 			{
@@ -889,14 +877,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",
@@ -909,7 +897,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{
@@ -931,6 +920,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{
 			{
@@ -972,14 +964,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",
@@ -992,7 +983,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 8040bb131..f6415b880 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"
@@ -187,6 +188,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)