sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/services/virtualmachines/virtualmachines_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package virtualmachines
    18  
    19  import (
    20  	"context"
    21  	"io"
    22  	"net/http"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
    27  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
    28  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4"
    29  	. "github.com/onsi/gomega"
    30  	"github.com/pkg/errors"
    31  	"go.uber.org/mock/gomock"
    32  	corev1 "k8s.io/api/core/v1"
    33  	"k8s.io/utils/ptr"
    34  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    35  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/async/mock_async"
    36  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/identities/mock_identities"
    37  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/networkinterfaces"
    38  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/publicips"
    39  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/virtualmachines/mock_virtualmachines"
    40  	gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock"
    41  	"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
    42  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    43  )
    44  
    45  var (
    46  	fakeVMSpec = VMSpec{
    47  		Name:              "test-vm",
    48  		ResourceGroup:     "test-group",
    49  		Location:          "test-location",
    50  		ClusterName:       "test-cluster",
    51  		Role:              infrav1.ControlPlane,
    52  		NICIDs:            []string{"nic-id-1", "nic-id-2"},
    53  		SSHKeyData:        "fake ssh key",
    54  		Size:              "Standard_Fake_Size",
    55  		AvailabilitySetID: "availability-set",
    56  		Identity:          infrav1.VMIdentitySystemAssigned,
    57  		AdditionalTags:    map[string]string{"foo": "bar"},
    58  		Image:             &infrav1.Image{ID: ptr.To("fake-image-id")},
    59  		BootstrapData:     "fake data",
    60  	}
    61  	fakeExistingVM = armcompute.VirtualMachine{
    62  		ID:   ptr.To("subscriptions/123/resourceGroups/my_resource_group/providers/Microsoft.Compute/virtualMachines/my-vm"),
    63  		Name: ptr.To("test-vm-name"),
    64  		Properties: &armcompute.VirtualMachineProperties{
    65  			ProvisioningState: ptr.To("Succeeded"),
    66  			NetworkProfile: &armcompute.NetworkProfile{
    67  				NetworkInterfaces: []*armcompute.NetworkInterfaceReference{
    68  					{
    69  						ID: ptr.To("/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/nic-1"),
    70  					},
    71  				},
    72  			},
    73  		},
    74  	}
    75  	fakeNetworkInterfaceGetterSpec = networkinterfaces.NICSpec{
    76  		Name:          "nic-1",
    77  		ResourceGroup: "test-group",
    78  	}
    79  	fakeNetworkInterface = armnetwork.Interface{
    80  		Properties: &armnetwork.InterfacePropertiesFormat{
    81  			IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
    82  				{
    83  					Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
    84  						PrivateIPAddress: ptr.To("10.0.0.5"),
    85  						PublicIPAddress: &armnetwork.PublicIPAddress{
    86  							ID: ptr.To("/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Network/publicIPAddresses/pip-1"),
    87  						},
    88  					},
    89  				},
    90  			},
    91  		},
    92  	}
    93  	fakePublicIPSpec = publicips.PublicIPSpec{
    94  		Name:          "pip-1",
    95  		ResourceGroup: "test-group",
    96  	}
    97  	fakePublicIPs = armnetwork.PublicIPAddress{
    98  		Properties: &armnetwork.PublicIPAddressPropertiesFormat{
    99  			IPAddress: ptr.To("10.0.0.6"),
   100  		},
   101  	}
   102  	fakeNodeAddresses = []corev1.NodeAddress{
   103  		{
   104  			Type:    corev1.NodeInternalDNS,
   105  			Address: "test-vm-name",
   106  		},
   107  		{
   108  			Type:    corev1.NodeInternalIP,
   109  			Address: "10.0.0.5",
   110  		},
   111  		{
   112  			Type:    corev1.NodeExternalIP,
   113  			Address: "10.0.0.6",
   114  		},
   115  	}
   116  	fakeUserAssignedIdentity = infrav1.UserAssignedIdentity{
   117  		ProviderID: "azure:///subscriptions/123/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/fake-provider-id",
   118  	}
   119  	fakeUserAssignedIdentity2 = infrav1.UserAssignedIdentity{
   120  		ProviderID: "azure:///subscriptions/123/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/fake-provider-id-2",
   121  	}
   122  )
   123  
   124  func internalError() *azcore.ResponseError {
   125  	return &azcore.ResponseError{
   126  		RawResponse: &http.Response{
   127  			Body:       io.NopCloser(strings.NewReader("#: Internal Server Error: StatusCode=500")),
   128  			StatusCode: http.StatusInternalServerError,
   129  		},
   130  	}
   131  }
   132  
   133  func TestReconcileVM(t *testing.T) {
   134  	testcases := []struct {
   135  		name          string
   136  		expectedError string
   137  		expect        func(s *mock_virtualmachines.MockVMScopeMockRecorder, mnic *mock_async.MockGetterMockRecorder, mpip *mock_async.MockGetterMockRecorder, r *mock_async.MockReconcilerMockRecorder)
   138  	}{
   139  		{
   140  			name:          "noop if no vm spec is found",
   141  			expectedError: "",
   142  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, mnic *mock_async.MockGetterMockRecorder, mpip *mock_async.MockGetterMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   143  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   144  				s.VMSpec().Return(nil)
   145  			},
   146  		},
   147  		{
   148  			name:          "create vm succeeds",
   149  			expectedError: "",
   150  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, mnic *mock_async.MockGetterMockRecorder, mpip *mock_async.MockGetterMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   151  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   152  				s.VMSpec().Return(&fakeVMSpec)
   153  				r.CreateOrUpdateResource(gomockinternal.AContext(), &fakeVMSpec, serviceName).Return(fakeExistingVM, nil)
   154  				s.UpdatePutStatus(infrav1.VMRunningCondition, serviceName, nil)
   155  				s.UpdatePutStatus(infrav1.DisksReadyCondition, serviceName, nil)
   156  				s.SetProviderID("azure://subscriptions/123/resourceGroups/my_resource_group/providers/Microsoft.Compute/virtualMachines/my-vm")
   157  				s.SetAnnotation("cluster-api-provider-azure", "true")
   158  				mnic.Get(gomockinternal.AContext(), &fakeNetworkInterfaceGetterSpec).Return(fakeNetworkInterface, nil)
   159  				mpip.Get(gomockinternal.AContext(), &fakePublicIPSpec).Return(fakePublicIPs, nil)
   160  				s.SetAddresses(fakeNodeAddresses)
   161  				s.SetVMState(infrav1.Succeeded)
   162  			},
   163  		},
   164  		{
   165  			name:          "creating vm fails",
   166  			expectedError: "#: Internal Server Error: StatusCode=500",
   167  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, mnic *mock_async.MockGetterMockRecorder, mpip *mock_async.MockGetterMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   168  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   169  				s.VMSpec().Return(&fakeVMSpec)
   170  				r.CreateOrUpdateResource(gomockinternal.AContext(), &fakeVMSpec, serviceName).Return(nil, internalError())
   171  				s.UpdatePutStatus(infrav1.VMRunningCondition, serviceName, internalError())
   172  				s.UpdatePutStatus(infrav1.DisksReadyCondition, serviceName, internalError())
   173  			},
   174  		},
   175  		{
   176  			name:          "create vm succeeds but failed to get network interfaces",
   177  			expectedError: "failed to fetch VM addresses:.*#: Internal Server Error: StatusCode=500",
   178  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, mnic *mock_async.MockGetterMockRecorder, mpip *mock_async.MockGetterMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   179  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   180  				s.VMSpec().Return(&fakeVMSpec)
   181  				r.CreateOrUpdateResource(gomockinternal.AContext(), &fakeVMSpec, serviceName).Return(fakeExistingVM, nil)
   182  				s.UpdatePutStatus(infrav1.VMRunningCondition, serviceName, nil)
   183  				s.UpdatePutStatus(infrav1.DisksReadyCondition, serviceName, nil)
   184  				s.SetProviderID("azure://subscriptions/123/resourceGroups/my_resource_group/providers/Microsoft.Compute/virtualMachines/my-vm")
   185  				s.SetAnnotation("cluster-api-provider-azure", "true")
   186  				mnic.Get(gomockinternal.AContext(), &fakeNetworkInterfaceGetterSpec).Return(armnetwork.Interface{}, internalError())
   187  			},
   188  		},
   189  		{
   190  			name:          "create vm succeeds but failed to get public IPs",
   191  			expectedError: "failed to fetch VM addresses:.*#: Internal Server Error: StatusCode=500",
   192  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, mnic *mock_async.MockGetterMockRecorder, mpip *mock_async.MockGetterMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   193  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   194  				s.VMSpec().Return(&fakeVMSpec)
   195  				r.CreateOrUpdateResource(gomockinternal.AContext(), &fakeVMSpec, serviceName).Return(fakeExistingVM, nil)
   196  				s.UpdatePutStatus(infrav1.VMRunningCondition, serviceName, nil)
   197  				s.UpdatePutStatus(infrav1.DisksReadyCondition, serviceName, nil)
   198  				s.SetProviderID("azure://subscriptions/123/resourceGroups/my_resource_group/providers/Microsoft.Compute/virtualMachines/my-vm")
   199  				s.SetAnnotation("cluster-api-provider-azure", "true")
   200  				mnic.Get(gomockinternal.AContext(), &fakeNetworkInterfaceGetterSpec).Return(fakeNetworkInterface, nil)
   201  				mpip.Get(gomockinternal.AContext(), &fakePublicIPSpec).Return(armnetwork.PublicIPAddress{}, internalError())
   202  			},
   203  		},
   204  	}
   205  
   206  	for _, tc := range testcases {
   207  		tc := tc
   208  		t.Run(tc.name, func(t *testing.T) {
   209  			g := NewWithT(t)
   210  			t.Parallel()
   211  			mockCtrl := gomock.NewController(t)
   212  			defer mockCtrl.Finish()
   213  
   214  			scopeMock := mock_virtualmachines.NewMockVMScope(mockCtrl)
   215  			interfaceMock := mock_async.NewMockGetter(mockCtrl)
   216  			publicIPMock := mock_async.NewMockGetter(mockCtrl)
   217  			asyncMock := mock_async.NewMockReconciler(mockCtrl)
   218  
   219  			tc.expect(scopeMock.EXPECT(), interfaceMock.EXPECT(), publicIPMock.EXPECT(), asyncMock.EXPECT())
   220  
   221  			s := &Service{
   222  				Scope:            scopeMock,
   223  				interfacesGetter: interfaceMock,
   224  				publicIPsGetter:  publicIPMock,
   225  				Reconciler:       asyncMock,
   226  			}
   227  
   228  			err := s.Reconcile(context.TODO())
   229  			if tc.expectedError != "" {
   230  				g.Expect(err).To(HaveOccurred())
   231  				g.Expect(strings.ReplaceAll(err.Error(), "\n", "")).To(MatchRegexp(tc.expectedError))
   232  			} else {
   233  				g.Expect(err).NotTo(HaveOccurred())
   234  			}
   235  		})
   236  	}
   237  }
   238  
   239  func TestDeleteVM(t *testing.T) {
   240  	testcases := []struct {
   241  		name          string
   242  		expectedError string
   243  		expect        func(s *mock_virtualmachines.MockVMScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder)
   244  	}{
   245  		{
   246  			name:          "noop if no vm spec is found",
   247  			expectedError: "",
   248  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   249  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   250  				s.VMSpec().Return(nil)
   251  			},
   252  		},
   253  		{
   254  			name:          "vm doesn't exist",
   255  			expectedError: "",
   256  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   257  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   258  				s.VMSpec().AnyTimes().Return(&fakeVMSpec)
   259  				r.DeleteResource(gomockinternal.AContext(), &fakeVMSpec, serviceName).Return(nil)
   260  				s.SetVMState(infrav1.Deleted)
   261  				s.UpdateDeleteStatus(infrav1.VMRunningCondition, serviceName, nil)
   262  			},
   263  		},
   264  		{
   265  			name:          "error occurs when deleting vm",
   266  			expectedError: "#: Internal Server Error: StatusCode=500",
   267  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   268  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   269  				s.VMSpec().AnyTimes().Return(&fakeVMSpec)
   270  				r.DeleteResource(gomockinternal.AContext(), &fakeVMSpec, serviceName).Return(internalError())
   271  				s.SetVMState(infrav1.Deleting)
   272  				s.UpdateDeleteStatus(infrav1.VMRunningCondition, serviceName, internalError())
   273  			},
   274  		},
   275  		{
   276  			name:          "delete the vm successfully",
   277  			expectedError: "",
   278  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   279  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   280  				s.VMSpec().AnyTimes().Return(&fakeVMSpec)
   281  				r.DeleteResource(gomockinternal.AContext(), &fakeVMSpec, serviceName).Return(nil)
   282  				s.SetVMState(infrav1.Deleted)
   283  				s.UpdateDeleteStatus(infrav1.VMRunningCondition, serviceName, nil)
   284  			},
   285  		},
   286  	}
   287  
   288  	for _, tc := range testcases {
   289  		tc := tc
   290  		t.Run(tc.name, func(t *testing.T) {
   291  			g := NewWithT(t)
   292  			t.Parallel()
   293  			mockCtrl := gomock.NewController(t)
   294  			defer mockCtrl.Finish()
   295  			scopeMock := mock_virtualmachines.NewMockVMScope(mockCtrl)
   296  			asyncMock := mock_async.NewMockReconciler(mockCtrl)
   297  
   298  			tc.expect(scopeMock.EXPECT(), asyncMock.EXPECT())
   299  
   300  			s := &Service{
   301  				Scope:      scopeMock,
   302  				Reconciler: asyncMock,
   303  			}
   304  
   305  			err := s.Delete(context.TODO())
   306  			if tc.expectedError != "" {
   307  				g.Expect(err).To(HaveOccurred())
   308  				g.Expect(err.Error()).To(ContainSubstring(tc.expectedError))
   309  			} else {
   310  				g.Expect(err).NotTo(HaveOccurred())
   311  			}
   312  		})
   313  	}
   314  }
   315  
   316  func TestCheckUserAssignedIdentities(t *testing.T) {
   317  	testcases := []struct {
   318  		name             string
   319  		specIdentities   []infrav1.UserAssignedIdentity
   320  		actualIdentities []infrav1.UserAssignedIdentity
   321  		expect           func(s *mock_virtualmachines.MockVMScopeMockRecorder, i *mock_identities.MockClientMockRecorder)
   322  		expectedError    string
   323  	}{
   324  		{
   325  			name:             "no user assigned identities",
   326  			specIdentities:   []infrav1.UserAssignedIdentity{},
   327  			actualIdentities: []infrav1.UserAssignedIdentity{},
   328  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, i *mock_identities.MockClientMockRecorder) {
   329  				i.GetClientID(gomockinternal.AContext(), fakeUserAssignedIdentity.ProviderID).AnyTimes().Return(fakeUserAssignedIdentity.ProviderID, nil)
   330  			},
   331  			expectedError: "",
   332  		},
   333  		{
   334  			name:             "matching user assigned identities",
   335  			specIdentities:   []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity},
   336  			actualIdentities: []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity},
   337  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, i *mock_identities.MockClientMockRecorder) {
   338  				s.SubscriptionID().Return("123")
   339  				i.GetClientID(gomockinternal.AContext(), fakeUserAssignedIdentity.ProviderID).AnyTimes().Return(fakeUserAssignedIdentity.ProviderID, nil)
   340  			},
   341  			expectedError: "",
   342  		},
   343  		{
   344  			name:             "less user assigned identities than expected",
   345  			specIdentities:   []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity, fakeUserAssignedIdentity2},
   346  			actualIdentities: []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity},
   347  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, i *mock_identities.MockClientMockRecorder) {
   348  				s.SubscriptionID().AnyTimes().Return("123")
   349  				i.GetClientID(gomockinternal.AContext(), fakeUserAssignedIdentity.ProviderID).AnyTimes().Return(fakeUserAssignedIdentity.ProviderID, nil)
   350  				i.GetClientID(gomockinternal.AContext(), fakeUserAssignedIdentity2.ProviderID).AnyTimes().Return(fakeUserAssignedIdentity2.ProviderID, nil)
   351  				s.SetConditionFalse(infrav1.VMIdentitiesReadyCondition, infrav1.UserAssignedIdentityMissingReason, clusterv1.ConditionSeverityWarning, vmMissingUAI+fakeUserAssignedIdentity2.ProviderID).Times(1)
   352  			},
   353  			expectedError: "",
   354  		},
   355  		{
   356  			name:             "more user assigned identities than expected",
   357  			specIdentities:   []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity},
   358  			actualIdentities: []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity, fakeUserAssignedIdentity2},
   359  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, i *mock_identities.MockClientMockRecorder) {
   360  				s.SubscriptionID().Return("123")
   361  				i.GetClientID(gomockinternal.AContext(), fakeUserAssignedIdentity.ProviderID).AnyTimes().Return(fakeUserAssignedIdentity.ProviderID, nil)
   362  			},
   363  			expectedError: "",
   364  		},
   365  		{
   366  			name:             "mismatched user assigned identities by content",
   367  			specIdentities:   []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity},
   368  			actualIdentities: []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity2},
   369  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, i *mock_identities.MockClientMockRecorder) {
   370  				s.SubscriptionID().Return("123")
   371  				i.GetClientID(gomockinternal.AContext(), fakeUserAssignedIdentity.ProviderID).AnyTimes().Return(fakeUserAssignedIdentity.ProviderID, nil)
   372  				s.SetConditionFalse(infrav1.VMIdentitiesReadyCondition, infrav1.UserAssignedIdentityMissingReason, clusterv1.ConditionSeverityWarning, vmMissingUAI+fakeUserAssignedIdentity.ProviderID).Times(1)
   373  			},
   374  			expectedError: "",
   375  		},
   376  		{
   377  			name:             "duplicate user assigned identity in spec",
   378  			specIdentities:   []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity, fakeUserAssignedIdentity},
   379  			actualIdentities: []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity},
   380  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, i *mock_identities.MockClientMockRecorder) {
   381  				s.SubscriptionID().AnyTimes().Return("123")
   382  				i.GetClientID(gomockinternal.AContext(), fakeUserAssignedIdentity.ProviderID).AnyTimes().Return(fakeUserAssignedIdentity.ProviderID, nil)
   383  			},
   384  			expectedError: "",
   385  		},
   386  		{
   387  			name:             "invalid client id",
   388  			specIdentities:   []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity},
   389  			actualIdentities: []infrav1.UserAssignedIdentity{fakeUserAssignedIdentity},
   390  			expect: func(s *mock_virtualmachines.MockVMScopeMockRecorder, i *mock_identities.MockClientMockRecorder) {
   391  				s.SubscriptionID().Return("123")
   392  				i.GetClientID(gomockinternal.AContext(), fakeUserAssignedIdentity.ProviderID).AnyTimes().Return("", errors.New("failed to get client id"))
   393  			},
   394  			expectedError: "failed to get client id",
   395  		},
   396  	}
   397  	for _, tc := range testcases {
   398  		tc := tc
   399  		t.Run(tc.name, func(t *testing.T) {
   400  			g := NewWithT(t)
   401  			t.Parallel()
   402  			mockCtrl := gomock.NewController(t)
   403  			defer mockCtrl.Finish()
   404  			scopeMock := mock_virtualmachines.NewMockVMScope(mockCtrl)
   405  			asyncMock := mock_async.NewMockReconciler(mockCtrl)
   406  			identitiesMock := mock_identities.NewMockClient(mockCtrl)
   407  
   408  			tc.expect(scopeMock.EXPECT(), identitiesMock.EXPECT())
   409  			s := &Service{
   410  				Scope:            scopeMock,
   411  				Reconciler:       asyncMock,
   412  				identitiesGetter: identitiesMock,
   413  			}
   414  
   415  			err := s.checkUserAssignedIdentities(context.TODO(), tc.specIdentities, tc.actualIdentities)
   416  			if tc.expectedError != "" {
   417  				g.Expect(err).To(HaveOccurred())
   418  				g.Expect(err.Error()).To(ContainSubstring(tc.expectedError))
   419  			} else {
   420  				g.Expect(err).NotTo(HaveOccurred())
   421  			}
   422  		})
   423  	}
   424  }