sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/scope/machinepoolmachine_test.go (about) 1 /* 2 Copyright 2021 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 scope 18 19 import ( 20 "context" 21 "reflect" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25 . "github.com/onsi/gomega" 26 "github.com/pkg/errors" 27 "go.uber.org/mock/gomock" 28 corev1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/utils/ptr" 32 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" 33 "sigs.k8s.io/cluster-api-provider-azure/azure" 34 "sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure" 35 mock_scope "sigs.k8s.io/cluster-api-provider-azure/azure/scope/mocks" 36 "sigs.k8s.io/cluster-api-provider-azure/azure/services/scalesetvms" 37 infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1beta1" 38 gomock2 "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" 39 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 40 expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 41 "sigs.k8s.io/cluster-api/util/conditions" 42 "sigs.k8s.io/controller-runtime/pkg/client/fake" 43 ) 44 45 const ( 46 FakeProviderID = "/foo/bin/bazz" 47 ) 48 49 func TestNewMachinePoolMachineScope(t *testing.T) { 50 scheme := runtime.NewScheme() 51 _ = expv1.AddToScheme(scheme) 52 _ = infrav1exp.AddToScheme(scheme) 53 54 cases := []struct { 55 Name string 56 Input MachinePoolMachineScopeParams 57 Err string 58 }{ 59 { 60 Name: "successfully create machine scope", 61 Input: MachinePoolMachineScopeParams{ 62 Client: fake.NewClientBuilder().WithScheme(scheme).Build(), 63 ClusterScope: &ClusterScope{ 64 Cluster: &clusterv1.Cluster{ 65 ObjectMeta: metav1.ObjectMeta{ 66 Name: "clusterName", 67 }, 68 }, 69 }, 70 MachinePool: new(expv1.MachinePool), 71 AzureMachinePool: new(infrav1exp.AzureMachinePool), 72 Machine: new(clusterv1.Machine), 73 AzureMachinePoolMachine: new(infrav1exp.AzureMachinePoolMachine), 74 }, 75 }, 76 { 77 Name: "no client", 78 Input: MachinePoolMachineScopeParams{ 79 ClusterScope: new(ClusterScope), 80 MachinePool: new(expv1.MachinePool), 81 AzureMachinePool: new(infrav1exp.AzureMachinePool), 82 Machine: new(clusterv1.Machine), 83 AzureMachinePoolMachine: new(infrav1exp.AzureMachinePoolMachine), 84 }, 85 Err: "client is required when creating a MachinePoolScope", 86 }, 87 { 88 Name: "no ClusterScope", 89 Input: MachinePoolMachineScopeParams{ 90 Client: fake.NewClientBuilder().WithScheme(scheme).Build(), 91 MachinePool: new(expv1.MachinePool), 92 AzureMachinePool: new(infrav1exp.AzureMachinePool), 93 Machine: new(clusterv1.Machine), 94 AzureMachinePoolMachine: new(infrav1exp.AzureMachinePoolMachine), 95 }, 96 Err: "cluster scope is required when creating a MachinePoolScope", 97 }, 98 { 99 Name: "no MachinePool", 100 Input: MachinePoolMachineScopeParams{ 101 Client: fake.NewClientBuilder().WithScheme(scheme).Build(), 102 ClusterScope: new(ClusterScope), 103 AzureMachinePool: new(infrav1exp.AzureMachinePool), 104 Machine: new(clusterv1.Machine), 105 AzureMachinePoolMachine: new(infrav1exp.AzureMachinePoolMachine), 106 }, 107 Err: "machine pool is required when creating a MachinePoolScope", 108 }, 109 { 110 Name: "no AzureMachinePool", 111 Input: MachinePoolMachineScopeParams{ 112 Client: fake.NewClientBuilder().WithScheme(scheme).Build(), 113 ClusterScope: new(ClusterScope), 114 MachinePool: new(expv1.MachinePool), 115 Machine: new(clusterv1.Machine), 116 AzureMachinePoolMachine: new(infrav1exp.AzureMachinePoolMachine), 117 }, 118 Err: "azure machine pool is required when creating a MachinePoolScope", 119 }, 120 { 121 Name: "no AzureMachinePoolMachine", 122 Input: MachinePoolMachineScopeParams{ 123 Client: fake.NewClientBuilder().WithScheme(scheme).Build(), 124 ClusterScope: new(ClusterScope), 125 MachinePool: new(expv1.MachinePool), 126 Machine: new(clusterv1.Machine), 127 AzureMachinePool: new(infrav1exp.AzureMachinePool), 128 }, 129 Err: "azure machine pool machine is required when creating a MachinePoolScope", 130 }, 131 { 132 Name: "no MachinePool Machine", 133 Input: MachinePoolMachineScopeParams{ 134 Client: fake.NewClientBuilder().WithScheme(scheme).Build(), 135 ClusterScope: new(ClusterScope), 136 MachinePool: new(expv1.MachinePool), 137 AzureMachinePool: new(infrav1exp.AzureMachinePool), 138 AzureMachinePoolMachine: new(infrav1exp.AzureMachinePoolMachine), 139 }, 140 Err: "machine is required when creating a MachinePoolScope", 141 }, 142 } 143 144 for _, c := range cases { 145 t.Run(c.Name, func(t *testing.T) { 146 g := NewWithT(t) 147 s, err := NewMachinePoolMachineScope(c.Input) 148 if c.Err != "" { 149 g.Expect(err).To(MatchError(c.Err)) 150 } else { 151 g.Expect(err).NotTo(HaveOccurred()) 152 g.Expect(s).NotTo(BeNil()) 153 } 154 }) 155 } 156 } 157 158 func TestMachinePoolMachineScope_ScaleSetVMSpecs(t *testing.T) { 159 tests := []struct { 160 name string 161 machinePoolMachineScope MachinePoolMachineScope 162 want azure.ResourceSpecGetter 163 }{ 164 { 165 name: "return vmss vm spec for uniform vmss", 166 machinePoolMachineScope: MachinePoolMachineScope{ 167 MachinePool: &expv1.MachinePool{}, 168 AzureMachinePool: &infrav1exp.AzureMachinePool{ 169 ObjectMeta: metav1.ObjectMeta{ 170 Name: "machinepool-name", 171 }, 172 Spec: infrav1exp.AzureMachinePoolSpec{ 173 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 174 OSDisk: infrav1.OSDisk{ 175 OSType: "Linux", 176 }, 177 }, 178 OrchestrationMode: infrav1.UniformOrchestrationMode, 179 }, 180 }, 181 AzureMachinePoolMachine: &infrav1exp.AzureMachinePoolMachine{ 182 ObjectMeta: metav1.ObjectMeta{ 183 Name: "machinepoolmachine-name", 184 }, 185 Spec: infrav1exp.AzureMachinePoolMachineSpec{ 186 ProviderID: "azure:///subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/machinepool-name/virtualMachines/0", 187 InstanceID: "0", 188 }, 189 }, 190 ClusterScoper: &ClusterScope{ 191 AzureCluster: &infrav1.AzureCluster{ 192 Spec: infrav1.AzureClusterSpec{ 193 ResourceGroup: "my-rg", 194 }, 195 }, 196 }, 197 MachinePoolScope: &MachinePoolScope{ 198 AzureMachinePool: &infrav1exp.AzureMachinePool{ 199 ObjectMeta: metav1.ObjectMeta{ 200 Name: "machinepool-name", 201 }, 202 }, 203 }, 204 }, 205 want: &scalesetvms.ScaleSetVMSpec{ 206 Name: "machinepoolmachine-name", 207 InstanceID: "0", 208 ResourceGroup: "my-rg", 209 ScaleSetName: "machinepool-name", 210 ProviderID: "azure:///subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/machinepool-name/virtualMachines/0", 211 IsFlex: false, 212 ResourceID: "", 213 }, 214 }, 215 { 216 name: "return vmss vm spec for vmss flex", 217 machinePoolMachineScope: MachinePoolMachineScope{ 218 MachinePool: &expv1.MachinePool{}, 219 AzureMachinePool: &infrav1exp.AzureMachinePool{ 220 ObjectMeta: metav1.ObjectMeta{ 221 Name: "machinepool-name", 222 }, 223 Spec: infrav1exp.AzureMachinePoolSpec{ 224 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 225 OSDisk: infrav1.OSDisk{ 226 OSType: "Linux", 227 }, 228 }, 229 OrchestrationMode: infrav1.FlexibleOrchestrationMode, 230 }, 231 }, 232 AzureMachinePoolMachine: &infrav1exp.AzureMachinePoolMachine{ 233 ObjectMeta: metav1.ObjectMeta{ 234 Name: "machinepoolmachine-name", 235 }, 236 Spec: infrav1exp.AzureMachinePoolMachineSpec{ 237 ProviderID: "azure:///subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/machinepool-name/virtualMachines/0", 238 InstanceID: "0", 239 }, 240 }, 241 ClusterScoper: &ClusterScope{ 242 AzureCluster: &infrav1.AzureCluster{ 243 Spec: infrav1.AzureClusterSpec{ 244 ResourceGroup: "my-rg", 245 }, 246 }, 247 }, 248 MachinePoolScope: &MachinePoolScope{ 249 AzureMachinePool: &infrav1exp.AzureMachinePool{ 250 ObjectMeta: metav1.ObjectMeta{ 251 Name: "machinepool-name", 252 }, 253 }, 254 }, 255 }, 256 want: &scalesetvms.ScaleSetVMSpec{ 257 Name: "machinepoolmachine-name", 258 InstanceID: "0", 259 ResourceGroup: "my-rg", 260 ScaleSetName: "machinepool-name", 261 ProviderID: "azure:///subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/machinepool-name/virtualMachines/0", 262 IsFlex: true, 263 ResourceID: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/machinepool-name/virtualMachines/0", 264 }, 265 }, 266 } 267 for _, tt := range tests { 268 t.Run(tt.name, func(t *testing.T) { 269 if got := tt.machinePoolMachineScope.ScaleSetVMSpec(); !reflect.DeepEqual(got, tt.want) { 270 t.Errorf("Diff between expected result and actual result: %+v", cmp.Diff(tt.want, got)) 271 } 272 }) 273 } 274 } 275 func TestMachineScope_updateDeleteMachineAnnotation(t *testing.T) { 276 cases := []struct { 277 name string 278 machine clusterv1.Machine 279 ampm infrav1exp.AzureMachinePoolMachine 280 }{ 281 { 282 name: "add annotation to ampm", 283 machine: clusterv1.Machine{ 284 ObjectMeta: metav1.ObjectMeta{ 285 Annotations: map[string]string{ 286 clusterv1.DeleteMachineAnnotation: "true", 287 }, 288 }, 289 }, 290 ampm: infrav1exp.AzureMachinePoolMachine{}, 291 }, 292 { 293 name: "do not add annotation to ampm when machine annotations are nil", 294 machine: clusterv1.Machine{}, 295 ampm: infrav1exp.AzureMachinePoolMachine{}, 296 }, 297 { 298 name: "do not add annotation to ampm", 299 machine: clusterv1.Machine{ 300 ObjectMeta: metav1.ObjectMeta{ 301 Annotations: map[string]string{}, 302 }, 303 }, 304 ampm: infrav1exp.AzureMachinePoolMachine{}, 305 }, 306 } 307 308 for _, c := range cases { 309 c := c 310 t.Run(c.name, func(t *testing.T) { 311 g := NewWithT(t) 312 313 machineScope := &MachinePoolMachineScope{ 314 Machine: &c.machine, 315 AzureMachinePoolMachine: &c.ampm, 316 } 317 318 machineScope.updateDeleteMachineAnnotation() 319 _, machineHasAnnotation := machineScope.Machine.Annotations[clusterv1.DeleteMachineAnnotation] 320 _, ampmHasAnnotation := machineScope.AzureMachinePoolMachine.Annotations[clusterv1.DeleteMachineAnnotation] 321 g.Expect(machineHasAnnotation).To(Equal(ampmHasAnnotation)) 322 }) 323 } 324 } 325 326 func TestMachineScope_UpdateNodeStatus(t *testing.T) { 327 scheme := runtime.NewScheme() 328 _ = expv1.AddToScheme(scheme) 329 _ = infrav1exp.AddToScheme(scheme) 330 331 mockCtrl := gomock.NewController(t) 332 defer mockCtrl.Finish() 333 334 clusterScope := mock_azure.NewMockClusterScoper(mockCtrl) 335 clusterScope.EXPECT().BaseURI().AnyTimes() 336 clusterScope.EXPECT().Location().AnyTimes() 337 clusterScope.EXPECT().SubscriptionID().AnyTimes() 338 clusterScope.EXPECT().ClusterName().Return("cluster-foo").AnyTimes() 339 340 cases := []struct { 341 Name string 342 Setup func(mockNodeGetter *mock_scope.MocknodeGetter, ampm *infrav1exp.AzureMachinePoolMachine) (*azure.VMSSVM, *infrav1exp.AzureMachinePoolMachine) 343 Verify func(g *WithT, scope *MachinePoolMachineScope) 344 Err string 345 }{ 346 { 347 Name: "should set kubernetes version, ready, and node reference upon finding the node", 348 Setup: func(mockNodeGetter *mock_scope.MocknodeGetter, ampm *infrav1exp.AzureMachinePoolMachine) (*azure.VMSSVM, *infrav1exp.AzureMachinePoolMachine) { 349 mockNodeGetter.EXPECT().GetNodeByProviderID(gomock2.AContext(), FakeProviderID).Return(getReadyNode(), nil) 350 return nil, ampm 351 }, 352 Verify: func(g *WithT, scope *MachinePoolMachineScope) { 353 g.Expect(scope.AzureMachinePoolMachine.Status.Ready).To(BeTrue()) 354 g.Expect(scope.AzureMachinePoolMachine.Status.Version).To(Equal("1.2.3")) 355 g.Expect(scope.AzureMachinePoolMachine.Status.NodeRef).To(Equal(&corev1.ObjectReference{ 356 Name: "node1", 357 })) 358 assertCondition(t, scope.AzureMachinePoolMachine, conditions.TrueCondition(clusterv1.MachineNodeHealthyCondition)) 359 }, 360 }, 361 { 362 Name: "should not mark AMPM ready if node is not ready", 363 Setup: func(mockNodeGetter *mock_scope.MocknodeGetter, ampm *infrav1exp.AzureMachinePoolMachine) (*azure.VMSSVM, *infrav1exp.AzureMachinePoolMachine) { 364 mockNodeGetter.EXPECT().GetNodeByProviderID(gomock2.AContext(), FakeProviderID).Return(getNotReadyNode(), nil) 365 return nil, ampm 366 }, 367 Verify: func(g *WithT, scope *MachinePoolMachineScope) { 368 g.Expect(scope.AzureMachinePoolMachine.Status.Ready).To(BeFalse()) 369 g.Expect(scope.AzureMachinePoolMachine.Status.Version).To(Equal("1.2.3")) 370 g.Expect(scope.AzureMachinePoolMachine.Status.NodeRef).To(Equal(&corev1.ObjectReference{ 371 Name: "node1", 372 })) 373 assertCondition(t, scope.AzureMachinePoolMachine, conditions.FalseCondition(clusterv1.MachineNodeHealthyCondition, clusterv1.NodeConditionsFailedReason, clusterv1.ConditionSeverityWarning, "")) 374 }, 375 }, 376 { 377 Name: "fails fetching the node", 378 Setup: func(mockNodeGetter *mock_scope.MocknodeGetter, ampm *infrav1exp.AzureMachinePoolMachine) (*azure.VMSSVM, *infrav1exp.AzureMachinePoolMachine) { 379 mockNodeGetter.EXPECT().GetNodeByProviderID(gomock2.AContext(), FakeProviderID).Return(nil, errors.New("boom")) 380 return nil, ampm 381 }, 382 Err: "failed to get node by providerID: boom", 383 }, 384 { 385 Name: "node is not found by providerID without error", 386 Setup: func(mockNodeGetter *mock_scope.MocknodeGetter, ampm *infrav1exp.AzureMachinePoolMachine) (*azure.VMSSVM, *infrav1exp.AzureMachinePoolMachine) { 387 mockNodeGetter.EXPECT().GetNodeByProviderID(gomock2.AContext(), FakeProviderID).Return(nil, nil) 388 return nil, ampm 389 }, 390 Verify: func(g *WithT, scope *MachinePoolMachineScope) { 391 assertCondition(t, scope.AzureMachinePoolMachine, conditions.FalseCondition(clusterv1.MachineNodeHealthyCondition, clusterv1.NodeProvisioningReason, clusterv1.ConditionSeverityInfo, "")) 392 }, 393 }, 394 { 395 Name: "node is found by ObjectReference", 396 Setup: func(mockNodeGetter *mock_scope.MocknodeGetter, ampm *infrav1exp.AzureMachinePoolMachine) (*azure.VMSSVM, *infrav1exp.AzureMachinePoolMachine) { 397 nodeRef := corev1.ObjectReference{ 398 Name: "node1", 399 } 400 ampm.Status.NodeRef = &nodeRef 401 mockNodeGetter.EXPECT().GetNodeByObjectReference(gomock2.AContext(), nodeRef).Return(getReadyNode(), nil) 402 return nil, ampm 403 }, 404 Verify: func(g *WithT, scope *MachinePoolMachineScope) { 405 g.Expect(scope.AzureMachinePoolMachine.Status.Ready).To(BeTrue()) 406 g.Expect(scope.AzureMachinePoolMachine.Status.Version).To(Equal("1.2.3")) 407 g.Expect(scope.AzureMachinePoolMachine.Status.NodeRef).To(Equal(&corev1.ObjectReference{ 408 Name: "node1", 409 })) 410 assertCondition(t, scope.AzureMachinePoolMachine, conditions.TrueCondition(clusterv1.MachineNodeHealthyCondition)) 411 }, 412 }, 413 } 414 415 for _, c := range cases { 416 t.Run(c.Name, func(t *testing.T) { 417 var ( 418 controller = gomock.NewController(t) 419 mockClient = mock_scope.NewMocknodeGetter(controller) 420 g = NewWithT(t) 421 params = MachinePoolMachineScopeParams{ 422 Client: fake.NewClientBuilder().WithScheme(scheme).Build(), 423 ClusterScope: clusterScope, 424 MachinePool: &expv1.MachinePool{ 425 Spec: expv1.MachinePoolSpec{ 426 Template: clusterv1.MachineTemplateSpec{ 427 Spec: clusterv1.MachineSpec{ 428 Version: ptr.To("v1.19.11"), 429 }, 430 }, 431 }, 432 }, 433 AzureMachinePool: new(infrav1exp.AzureMachinePool), 434 Machine: new(clusterv1.Machine), 435 } 436 ) 437 438 defer controller.Finish() 439 440 instance, ampm := c.Setup(mockClient, &infrav1exp.AzureMachinePoolMachine{ 441 Spec: infrav1exp.AzureMachinePoolMachineSpec{ 442 ProviderID: FakeProviderID, 443 }, 444 }) 445 params.AzureMachinePoolMachine = ampm 446 s, err := NewMachinePoolMachineScope(params) 447 g.Expect(err).NotTo(HaveOccurred()) 448 g.Expect(s).NotTo(BeNil()) 449 s.instance = instance 450 s.workloadNodeGetter = mockClient 451 452 err = s.UpdateNodeStatus(context.TODO()) 453 if c.Err == "" { 454 g.Expect(err).To(Succeed()) 455 } else { 456 g.Expect(err).To(MatchError(c.Err)) 457 } 458 459 if c.Verify != nil { 460 c.Verify(g, s) 461 } 462 }) 463 } 464 } 465 466 func getReadyNode() *corev1.Node { 467 return &corev1.Node{ 468 ObjectMeta: metav1.ObjectMeta{ 469 Name: "node1", 470 }, 471 Status: corev1.NodeStatus{ 472 NodeInfo: corev1.NodeSystemInfo{ 473 KubeletVersion: "1.2.3", 474 }, 475 Conditions: []corev1.NodeCondition{ 476 { 477 Type: corev1.NodeReady, 478 Status: corev1.ConditionTrue, 479 }, 480 }, 481 }, 482 } 483 } 484 485 func getNotReadyNode() *corev1.Node { 486 return &corev1.Node{ 487 ObjectMeta: metav1.ObjectMeta{ 488 Name: "node1", 489 }, 490 Status: corev1.NodeStatus{ 491 NodeInfo: corev1.NodeSystemInfo{ 492 KubeletVersion: "1.2.3", 493 }, 494 Conditions: []corev1.NodeCondition{ 495 { 496 Type: corev1.NodeReady, 497 Status: corev1.ConditionFalse, 498 }, 499 }, 500 }, 501 } 502 } 503 504 // asserts whether a condition of type is set on the Getter object 505 // when the condition is true, asserting the reason/severity/message 506 // for the condition are avoided. 507 func assertCondition(t *testing.T, from conditions.Getter, condition *clusterv1.Condition) { 508 t.Helper() 509 510 g := NewWithT(t) 511 g.Expect(conditions.Has(from, condition.Type)).To(BeTrue()) 512 513 if condition.Status == corev1.ConditionTrue { 514 conditions.IsTrue(from, condition.Type) 515 } else { 516 conditionToBeAsserted := conditions.Get(from, condition.Type) 517 g.Expect(conditionToBeAsserted.Status).To(Equal(condition.Status)) 518 g.Expect(conditionToBeAsserted.Severity).To(Equal(condition.Severity)) 519 g.Expect(conditionToBeAsserted.Reason).To(Equal(condition.Reason)) 520 if condition.Message != "" { 521 g.Expect(conditionToBeAsserted.Message).To(Equal(condition.Message)) 522 } 523 } 524 }