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 }