Repository: kubernetes-sigs/cloud-provider-azure
Pull request #1080 (2022-01-28 08:06:38)

[release-1.23] feat: support platformSubFaultDomain node label
This is an automated cherry-pick of #1061

/assign nilo19

```release-note
feat: support platformSubFaultDomain node label
```


Patch:
diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go
index 6edd9f1a2..e7960ce63 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 5193b47d6..f439ebfee 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)