sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/scope/machinepool_test.go (about) 1 /* 2 Copyright 2020 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 "fmt" 22 "reflect" 23 "testing" 24 "time" 25 26 "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 27 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2" 28 azureautorest "github.com/Azure/go-autorest/autorest/azure" 29 "github.com/Azure/go-autorest/autorest/azure/auth" 30 . "github.com/onsi/gomega" 31 "go.uber.org/mock/gomock" 32 corev1 "k8s.io/api/core/v1" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/util/intstr" 36 "k8s.io/utils/ptr" 37 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" 38 "sigs.k8s.io/cluster-api-provider-azure/azure" 39 "sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure" 40 "sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus" 41 "sigs.k8s.io/cluster-api-provider-azure/azure/services/roleassignments" 42 "sigs.k8s.io/cluster-api-provider-azure/azure/services/scalesets" 43 infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1beta1" 44 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 45 expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 46 "sigs.k8s.io/controller-runtime/pkg/client" 47 "sigs.k8s.io/controller-runtime/pkg/client/fake" 48 ) 49 50 func TestMachinePoolScope_Name(t *testing.T) { 51 tests := []struct { 52 name string 53 machinePoolScope MachinePoolScope 54 want string 55 testLength bool 56 }{ 57 { 58 name: "linux can be any length", 59 machinePoolScope: MachinePoolScope{ 60 MachinePool: nil, 61 AzureMachinePool: &infrav1exp.AzureMachinePool{ 62 ObjectMeta: metav1.ObjectMeta{ 63 Name: "some-really-really-long-name", 64 }, 65 }, 66 ClusterScoper: nil, 67 }, 68 want: "some-really-really-long-name", 69 }, 70 { 71 name: "windows longer than 9 should be shortened", 72 machinePoolScope: MachinePoolScope{ 73 MachinePool: nil, 74 AzureMachinePool: &infrav1exp.AzureMachinePool{ 75 ObjectMeta: metav1.ObjectMeta{ 76 Name: "machine-90123456", 77 }, 78 Spec: infrav1exp.AzureMachinePoolSpec{ 79 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 80 OSDisk: infrav1.OSDisk{ 81 OSType: "Windows", 82 }, 83 }, 84 }, 85 }, 86 ClusterScoper: nil, 87 }, 88 want: "win-23456", 89 }, 90 } 91 92 for _, tt := range tests { 93 t.Run(tt.name, func(t *testing.T) { 94 got := tt.machinePoolScope.Name() 95 if got != tt.want { 96 t.Errorf("MachinePoolScope.Name() = %v, want %v", got, tt.want) 97 } 98 99 if tt.testLength && len(got) > 9 { 100 t.Errorf("Length of MachinePoolScope.Name() = %v, want less than %v", len(got), 9) 101 } 102 }) 103 } 104 } 105 106 func TestMachinePoolScope_ProviderID(t *testing.T) { 107 tests := []struct { 108 name string 109 machinePoolScope MachinePoolScope 110 want string 111 }{ 112 { 113 name: "valid providerID", 114 machinePoolScope: MachinePoolScope{ 115 AzureMachinePool: &infrav1exp.AzureMachinePool{ 116 Spec: infrav1exp.AzureMachinePoolSpec{ 117 ProviderID: "azure:///subscriptions/1234/resourcegroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/cloud-provider-user-identity", 118 }, 119 }, 120 }, 121 want: "cloud-provider-user-identity", 122 }, 123 { 124 name: "valid providerID: VMSS Flex instance", 125 machinePoolScope: MachinePoolScope{ 126 AzureMachinePool: &infrav1exp.AzureMachinePool{ 127 Spec: infrav1exp.AzureMachinePoolSpec{ 128 ProviderID: "azure:///subscriptions/1234/resourceGroups/my-cluster/providers/Microsoft.Compute/virtualMachines/machine-0", 129 }, 130 }, 131 }, 132 want: "machine-0", 133 }, 134 { 135 name: "valid providerID: VMSS Uniform instance", 136 machinePoolScope: MachinePoolScope{ 137 AzureMachinePool: &infrav1exp.AzureMachinePool{ 138 Spec: infrav1exp.AzureMachinePoolSpec{ 139 ProviderID: "azure:///subscriptions/1234/resourceGroups/my-cluster/providers/Microsoft.Compute/virtualMachineScaleSets/my-cluster-mp-0/virtualMachines/0", 140 }, 141 }, 142 }, 143 want: "0", 144 }, 145 { 146 name: "invalid providerID: no cloud provider", 147 machinePoolScope: MachinePoolScope{ 148 AzureMachinePool: &infrav1exp.AzureMachinePool{ 149 Spec: infrav1exp.AzureMachinePoolSpec{ 150 ProviderID: "subscriptions/123/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm", 151 }, 152 }, 153 }, 154 want: "", 155 }, 156 { 157 name: "invalid providerID: incomplete URL", 158 machinePoolScope: MachinePoolScope{ 159 AzureMachinePool: &infrav1exp.AzureMachinePool{ 160 Spec: infrav1exp.AzureMachinePoolSpec{ 161 ProviderID: "azure:///", 162 }, 163 }, 164 }, 165 want: "", 166 }, 167 } 168 169 for _, tt := range tests { 170 t.Run(tt.name, func(t *testing.T) { 171 got := tt.machinePoolScope.ProviderID() 172 if got != tt.want { 173 t.Errorf("MachinePoolScope.ProviderID() = %v, want %v", got, tt.want) 174 } 175 }) 176 } 177 } 178 179 func TestMachinePoolScope_NetworkInterfaces(t *testing.T) { 180 tests := []struct { 181 name string 182 machinePoolScope MachinePoolScope 183 want int 184 }{ 185 { 186 name: "zero network interfaces", 187 machinePoolScope: MachinePoolScope{ 188 MachinePool: nil, 189 AzureMachinePool: &infrav1exp.AzureMachinePool{ 190 ObjectMeta: metav1.ObjectMeta{ 191 Name: "default-nics", 192 }, 193 Spec: infrav1exp.AzureMachinePoolSpec{ 194 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 195 AcceleratedNetworking: ptr.To(true), 196 SubnetName: "node-subnet", 197 }, 198 }, 199 }, 200 ClusterScoper: nil, 201 }, 202 want: 0, 203 }, 204 { 205 name: "one network interface", 206 machinePoolScope: MachinePoolScope{ 207 MachinePool: nil, 208 AzureMachinePool: &infrav1exp.AzureMachinePool{ 209 ObjectMeta: metav1.ObjectMeta{ 210 Name: "single-nic", 211 }, 212 Spec: infrav1exp.AzureMachinePoolSpec{ 213 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 214 NetworkInterfaces: []infrav1.NetworkInterface{ 215 { 216 SubnetName: "node-subnet", 217 }, 218 }, 219 }, 220 }, 221 }, 222 ClusterScoper: nil, 223 }, 224 want: 1, 225 }, 226 { 227 name: "two network interfaces", 228 machinePoolScope: MachinePoolScope{ 229 MachinePool: nil, 230 AzureMachinePool: &infrav1exp.AzureMachinePool{ 231 ObjectMeta: metav1.ObjectMeta{ 232 Name: "dual-nics", 233 }, 234 Spec: infrav1exp.AzureMachinePoolSpec{ 235 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 236 NetworkInterfaces: []infrav1.NetworkInterface{ 237 { 238 SubnetName: "control-plane-subnet", 239 }, 240 { 241 SubnetName: "node-subnet", 242 }, 243 }, 244 }, 245 }, 246 }, 247 ClusterScoper: nil, 248 }, 249 want: 2, 250 }, 251 } 252 253 for _, tt := range tests { 254 t.Run(tt.name, func(t *testing.T) { 255 got := len(tt.machinePoolScope.AzureMachinePool.Spec.Template.NetworkInterfaces) 256 if got != tt.want { 257 t.Errorf("MachinePoolScope.Name() = %v, want %v", got, tt.want) 258 } 259 }) 260 } 261 } 262 263 func TestMachinePoolScope_MaxSurge(t *testing.T) { 264 cases := []struct { 265 Name string 266 Setup func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) 267 Verify func(g *WithT, surge int, err error) 268 }{ 269 { 270 Name: "default surge should be 1 if no deployment strategy is set", 271 Verify: func(g *WithT, surge int, err error) { 272 g.Expect(surge).To(Equal(1)) 273 g.Expect(err).NotTo(HaveOccurred()) 274 }, 275 }, 276 { 277 Name: "default surge should be 1 regardless of replica count with no surger", 278 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) { 279 mp.Spec.Replicas = ptr.To[int32](3) 280 }, 281 Verify: func(g *WithT, surge int, err error) { 282 g.Expect(surge).To(Equal(1)) 283 g.Expect(err).NotTo(HaveOccurred()) 284 }, 285 }, 286 { 287 Name: "default surge should be 2 as specified by the surger", 288 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) { 289 mp.Spec.Replicas = ptr.To[int32](3) 290 two := intstr.FromInt(2) 291 amp.Spec.Strategy = infrav1exp.AzureMachinePoolDeploymentStrategy{ 292 Type: infrav1exp.RollingUpdateAzureMachinePoolDeploymentStrategyType, 293 RollingUpdate: &infrav1exp.MachineRollingUpdateDeployment{ 294 MaxSurge: &two, 295 }, 296 } 297 }, 298 Verify: func(g *WithT, surge int, err error) { 299 g.Expect(surge).To(Equal(2)) 300 g.Expect(err).NotTo(HaveOccurred()) 301 }, 302 }, 303 { 304 Name: "default surge should be 2 (50%) of the desired replicas", 305 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) { 306 mp.Spec.Replicas = ptr.To[int32](4) 307 fiftyPercent := intstr.FromString("50%") 308 amp.Spec.Strategy = infrav1exp.AzureMachinePoolDeploymentStrategy{ 309 Type: infrav1exp.RollingUpdateAzureMachinePoolDeploymentStrategyType, 310 RollingUpdate: &infrav1exp.MachineRollingUpdateDeployment{ 311 MaxSurge: &fiftyPercent, 312 }, 313 } 314 }, 315 Verify: func(g *WithT, surge int, err error) { 316 g.Expect(surge).To(Equal(2)) 317 g.Expect(err).NotTo(HaveOccurred()) 318 }, 319 }, 320 } 321 322 for _, c := range cases { 323 t.Run(c.Name, func(t *testing.T) { 324 var ( 325 g = NewWithT(t) 326 mockCtrl = gomock.NewController(t) 327 amp = &infrav1exp.AzureMachinePool{ 328 ObjectMeta: metav1.ObjectMeta{ 329 Name: "amp1", 330 Namespace: "default", 331 OwnerReferences: []metav1.OwnerReference{ 332 { 333 Name: "mp1", 334 Kind: "MachinePool", 335 APIVersion: expv1.GroupVersion.String(), 336 }, 337 }, 338 }, 339 } 340 mp = &expv1.MachinePool{ 341 ObjectMeta: metav1.ObjectMeta{ 342 Name: "mp1", 343 Namespace: "default", 344 }, 345 } 346 ) 347 defer mockCtrl.Finish() 348 349 if c.Setup != nil { 350 c.Setup(mp, amp) 351 } 352 353 s := &MachinePoolScope{ 354 MachinePool: mp, 355 AzureMachinePool: amp, 356 } 357 surge, err := s.MaxSurge() 358 c.Verify(g, surge, err) 359 }) 360 } 361 } 362 363 func TestMachinePoolScope_SaveVMImageToStatus(t *testing.T) { 364 var ( 365 g = NewWithT(t) 366 mockCtrl = gomock.NewController(t) 367 amp = &infrav1exp.AzureMachinePool{ 368 ObjectMeta: metav1.ObjectMeta{ 369 Name: "amp1", 370 Namespace: "default", 371 OwnerReferences: []metav1.OwnerReference{ 372 { 373 Name: "mp1", 374 Kind: "MachinePool", 375 APIVersion: expv1.GroupVersion.String(), 376 }, 377 }, 378 }, 379 } 380 s = &MachinePoolScope{ 381 AzureMachinePool: amp, 382 } 383 image = &infrav1.Image{ 384 Marketplace: &infrav1.AzureMarketplaceImage{ 385 ImagePlan: infrav1.ImagePlan{ 386 Publisher: "cncf-upstream", 387 Offer: "capi", 388 SKU: "k8s-1dot19dot11-ubuntu-1804", 389 }, 390 Version: "latest", 391 ThirdPartyImage: false, 392 }, 393 } 394 ) 395 defer mockCtrl.Finish() 396 397 s.SaveVMImageToStatus(image) 398 g.Expect(s.AzureMachinePool.Status.Image).To(Equal(image)) 399 } 400 401 func TestMachinePoolScope_GetVMImage(t *testing.T) { 402 mockCtrl := gomock.NewController(t) 403 defer mockCtrl.Finish() 404 405 clusterMock := mock_azure.NewMockClusterScoper(mockCtrl) 406 clusterMock.EXPECT().Location().AnyTimes() 407 clusterMock.EXPECT().SubscriptionID().AnyTimes() 408 clusterMock.EXPECT().CloudEnvironment().AnyTimes() 409 clusterMock.EXPECT().Token().Return(&azidentity.DefaultAzureCredential{}).AnyTimes() 410 cases := []struct { 411 Name string 412 Setup func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) 413 Verify func(g *WithT, amp *infrav1exp.AzureMachinePool, vmImage *infrav1.Image, err error) 414 }{ 415 { 416 Name: "should set and default the image if no image is specified for the AzureMachinePool", 417 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) { 418 mp.Spec.Template.Spec.Version = ptr.To("v1.19.11") 419 }, 420 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, vmImage *infrav1.Image, err error) { 421 g.Expect(err).NotTo(HaveOccurred()) 422 image := &infrav1.Image{ 423 Marketplace: &infrav1.AzureMarketplaceImage{ 424 ImagePlan: infrav1.ImagePlan{ 425 Publisher: "cncf-upstream", 426 Offer: "capi", 427 SKU: "k8s-1dot19dot11-ubuntu-1804", 428 }, 429 Version: "latest", 430 ThirdPartyImage: false, 431 }, 432 } 433 g.Expect(vmImage).To(Equal(image)) 434 g.Expect(amp.Spec.Template.Image).To(BeNil()) 435 }, 436 }, 437 { 438 Name: "should not default or set the image on the AzureMachinePool if it already exists", 439 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) { 440 mp.Spec.Template.Spec.Version = ptr.To("v1.19.11") 441 amp.Spec.Template.Image = &infrav1.Image{ 442 Marketplace: &infrav1.AzureMarketplaceImage{ 443 ImagePlan: infrav1.ImagePlan{ 444 Publisher: "cncf-upstream", 445 Offer: "capi", 446 SKU: "k8s-1dot19dot19-ubuntu-1804", 447 }, 448 Version: "latest", 449 ThirdPartyImage: false, 450 }, 451 } 452 }, 453 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, vmImage *infrav1.Image, err error) { 454 g.Expect(err).NotTo(HaveOccurred()) 455 image := &infrav1.Image{ 456 Marketplace: &infrav1.AzureMarketplaceImage{ 457 ImagePlan: infrav1.ImagePlan{ 458 Publisher: "cncf-upstream", 459 Offer: "capi", 460 SKU: "k8s-1dot19dot19-ubuntu-1804", 461 }, 462 Version: "latest", 463 ThirdPartyImage: false, 464 }, 465 } 466 g.Expect(vmImage).To(Equal(image)) 467 g.Expect(amp.Spec.Template.Image).To(Equal(image)) 468 }, 469 }, 470 } 471 472 for _, c := range cases { 473 t.Run(c.Name, func(t *testing.T) { 474 var ( 475 g = NewWithT(t) 476 mockCtrl = gomock.NewController(t) 477 amp = &infrav1exp.AzureMachinePool{ 478 ObjectMeta: metav1.ObjectMeta{ 479 Name: "amp1", 480 Namespace: "default", 481 OwnerReferences: []metav1.OwnerReference{ 482 { 483 Name: "mp1", 484 Kind: "MachinePool", 485 APIVersion: expv1.GroupVersion.String(), 486 }, 487 }, 488 }, 489 } 490 mp = &expv1.MachinePool{ 491 ObjectMeta: metav1.ObjectMeta{ 492 Name: "mp1", 493 Namespace: "default", 494 }, 495 } 496 ) 497 defer mockCtrl.Finish() 498 499 if c.Setup != nil { 500 c.Setup(mp, amp) 501 } 502 503 s := &MachinePoolScope{ 504 MachinePool: mp, 505 AzureMachinePool: amp, 506 ClusterScoper: clusterMock, 507 } 508 image, err := s.GetVMImage(context.TODO()) 509 c.Verify(g, amp, image, err) 510 }) 511 } 512 } 513 514 func TestMachinePoolScope_NeedsRequeue(t *testing.T) { 515 cases := []struct { 516 Name string 517 Setup func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) 518 Verify func(g *WithT, requeue bool) 519 }{ 520 { 521 Name: "should requeue if the machine is not in succeeded state", 522 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) { 523 creating := infrav1.Creating 524 mp.Spec.Replicas = ptr.To[int32](0) 525 amp.Status.ProvisioningState = &creating 526 }, 527 Verify: func(g *WithT, requeue bool) { 528 g.Expect(requeue).To(BeTrue()) 529 }, 530 }, 531 { 532 Name: "should not requeue if the machine is in succeeded state", 533 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) { 534 succeeded := infrav1.Succeeded 535 mp.Spec.Replicas = ptr.To[int32](0) 536 amp.Status.ProvisioningState = &succeeded 537 }, 538 Verify: func(g *WithT, requeue bool) { 539 g.Expect(requeue).To(BeFalse()) 540 }, 541 }, 542 { 543 Name: "should requeue if the machine is in succeeded state but desired replica count does not match", 544 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) { 545 succeeded := infrav1.Succeeded 546 mp.Spec.Replicas = ptr.To[int32](1) 547 amp.Status.ProvisioningState = &succeeded 548 }, 549 Verify: func(g *WithT, requeue bool) { 550 g.Expect(requeue).To(BeTrue()) 551 }, 552 }, 553 { 554 Name: "should not requeue if the machine is in succeeded state but desired replica count does match", 555 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) { 556 succeeded := infrav1.Succeeded 557 mp.Spec.Replicas = ptr.To[int32](1) 558 amp.Status.ProvisioningState = &succeeded 559 vmss.Instances = []azure.VMSSVM{ 560 { 561 Name: "instance1", 562 }, 563 } 564 }, 565 Verify: func(g *WithT, requeue bool) { 566 g.Expect(requeue).To(BeFalse()) 567 }, 568 }, 569 { 570 Name: "should requeue if an instance VM image does not match the VM image of the VMSS", 571 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) { 572 succeeded := infrav1.Succeeded 573 mp.Spec.Replicas = ptr.To[int32](1) 574 amp.Status.ProvisioningState = &succeeded 575 vmss.Instances = []azure.VMSSVM{ 576 { 577 Name: "instance1", 578 Image: infrav1.Image{ 579 Marketplace: &infrav1.AzureMarketplaceImage{ 580 Version: "foo1", 581 }, 582 }, 583 }, 584 } 585 }, 586 Verify: func(g *WithT, requeue bool) { 587 g.Expect(requeue).To(BeTrue()) 588 }, 589 }, 590 } 591 592 for _, c := range cases { 593 t.Run(c.Name, func(t *testing.T) { 594 var ( 595 g = NewWithT(t) 596 mockCtrl = gomock.NewController(t) 597 amp = &infrav1exp.AzureMachinePool{ 598 ObjectMeta: metav1.ObjectMeta{ 599 Name: "amp1", 600 Namespace: "default", 601 OwnerReferences: []metav1.OwnerReference{ 602 { 603 Name: "mp1", 604 Kind: "MachinePool", 605 APIVersion: expv1.GroupVersion.String(), 606 }, 607 }, 608 }, 609 } 610 mp = &expv1.MachinePool{ 611 ObjectMeta: metav1.ObjectMeta{ 612 Name: "mp1", 613 Namespace: "default", 614 }, 615 } 616 vmssState = &azure.VMSS{} 617 ) 618 defer mockCtrl.Finish() 619 620 if c.Setup != nil { 621 c.Setup(mp, amp, vmssState) 622 } 623 624 s := &MachinePoolScope{ 625 vmssState: vmssState, 626 MachinePool: mp, 627 AzureMachinePool: amp, 628 } 629 c.Verify(g, s.NeedsRequeue()) 630 }) 631 } 632 } 633 634 func TestMachinePoolScope_updateReplicasAndProviderIDs(t *testing.T) { 635 scheme := runtime.NewScheme() 636 _ = clusterv1.AddToScheme(scheme) 637 _ = infrav1exp.AddToScheme(scheme) 638 _ = expv1.AddToScheme(scheme) 639 640 cases := []struct { 641 Name string 642 Setup func(cb *fake.ClientBuilder) 643 Verify func(g *WithT, amp *infrav1exp.AzureMachinePool, err error) 644 }{ 645 { 646 Name: "if there are three ready machines with matching labels, then should count them", 647 Setup: func(cb *fake.ClientBuilder) { 648 for _, machine := range getReadyAzureMachinePoolMachines(3) { 649 obj := machine 650 cb.WithObjects(&obj) 651 } 652 }, 653 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, err error) { 654 g.Expect(err).NotTo(HaveOccurred()) 655 g.Expect(amp.Status.Replicas).To(BeEquivalentTo(3)) 656 g.Expect(amp.Spec.ProviderIDList).To(ConsistOf("azure://foo/ampm0", "azure://foo/ampm1", "azure://foo/ampm2")) 657 }, 658 }, 659 { 660 Name: "should only count machines with matching machine pool label", 661 Setup: func(cb *fake.ClientBuilder) { 662 machines := getReadyAzureMachinePoolMachines(3) 663 machines[0].Labels[infrav1exp.MachinePoolNameLabel] = "not_correct" 664 for _, machine := range machines { 665 obj := machine 666 cb.WithObjects(&obj) 667 } 668 }, 669 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, err error) { 670 g.Expect(err).NotTo(HaveOccurred()) 671 g.Expect(amp.Status.Replicas).To(BeEquivalentTo(2)) 672 }, 673 }, 674 { 675 Name: "should only count machines with matching cluster name label", 676 Setup: func(cb *fake.ClientBuilder) { 677 machines := getReadyAzureMachinePoolMachines(3) 678 machines[0].Labels[clusterv1.ClusterNameLabel] = "not_correct" 679 for _, machine := range machines { 680 obj := machine 681 cb.WithObjects(&obj) 682 } 683 }, 684 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, err error) { 685 g.Expect(err).NotTo(HaveOccurred()) 686 g.Expect(amp.Status.Replicas).To(BeEquivalentTo(2)) 687 }, 688 }, 689 } 690 691 for _, c := range cases { 692 t.Run(c.Name, func(t *testing.T) { 693 var ( 694 g = NewWithT(t) 695 mockCtrl = gomock.NewController(t) 696 cb = fake.NewClientBuilder().WithScheme(scheme) 697 cluster = &clusterv1.Cluster{ 698 ObjectMeta: metav1.ObjectMeta{ 699 Name: "cluster1", 700 Namespace: "default", 701 }, 702 Spec: clusterv1.ClusterSpec{ 703 InfrastructureRef: &corev1.ObjectReference{ 704 Name: "azCluster1", 705 }, 706 }, 707 Status: clusterv1.ClusterStatus{ 708 InfrastructureReady: true, 709 }, 710 } 711 mp = &expv1.MachinePool{ 712 ObjectMeta: metav1.ObjectMeta{ 713 Name: "mp1", 714 Namespace: "default", 715 }, 716 } 717 amp = &infrav1exp.AzureMachinePool{ 718 ObjectMeta: metav1.ObjectMeta{ 719 Name: "amp1", 720 Namespace: "default", 721 OwnerReferences: []metav1.OwnerReference{ 722 { 723 Name: "mp1", 724 Kind: "MachinePool", 725 APIVersion: expv1.GroupVersion.String(), 726 }, 727 }, 728 }, 729 } 730 ) 731 defer mockCtrl.Finish() 732 733 c.Setup(cb.WithObjects(amp, cluster)) 734 s := &MachinePoolScope{ 735 client: cb.Build(), 736 ClusterScoper: &ClusterScope{ 737 Cluster: cluster, 738 }, 739 AzureMachinePool: amp, 740 MachinePool: mp, 741 } 742 err := s.updateReplicasAndProviderIDs(context.TODO()) 743 c.Verify(g, s.AzureMachinePool, err) 744 }) 745 } 746 } 747 748 func TestMachinePoolScope_RoleAssignmentSpecs(t *testing.T) { 749 tests := []struct { 750 name string 751 machinePoolScope MachinePoolScope 752 want []azure.ResourceSpecGetter 753 }{ 754 { 755 name: "returns empty if VM identity is not system assigned", 756 machinePoolScope: MachinePoolScope{ 757 AzureMachinePool: &infrav1exp.AzureMachinePool{ 758 ObjectMeta: metav1.ObjectMeta{ 759 Name: "machine-name", 760 }, 761 }, 762 }, 763 want: []azure.ResourceSpecGetter{}, 764 }, 765 { 766 name: "returns role assignment spec if VM identity is system assigned", 767 machinePoolScope: MachinePoolScope{ 768 MachinePool: &expv1.MachinePool{}, 769 AzureMachinePool: &infrav1exp.AzureMachinePool{ 770 ObjectMeta: metav1.ObjectMeta{ 771 Name: "machine-name", 772 }, 773 Spec: infrav1exp.AzureMachinePoolSpec{ 774 Identity: infrav1.VMIdentitySystemAssigned, 775 SystemAssignedIdentityRole: &infrav1.SystemAssignedIdentityRole{ 776 Name: "role-assignment-name", 777 }, 778 }, 779 }, 780 ClusterScoper: &ClusterScope{ 781 AzureClients: AzureClients{ 782 EnvironmentSettings: auth.EnvironmentSettings{ 783 Values: map[string]string{ 784 auth.SubscriptionID: "123", 785 }, 786 }, 787 }, 788 AzureCluster: &infrav1.AzureCluster{ 789 Spec: infrav1.AzureClusterSpec{ 790 ResourceGroup: "my-rg", 791 AzureClusterClassSpec: infrav1.AzureClusterClassSpec{ 792 Location: "westus", 793 }, 794 }, 795 }, 796 }, 797 }, 798 want: []azure.ResourceSpecGetter{ 799 &roleassignments.RoleAssignmentSpec{ 800 ResourceType: azure.VirtualMachineScaleSet, 801 MachineName: "machine-name", 802 Name: "role-assignment-name", 803 ResourceGroup: "my-rg", 804 PrincipalID: ptr.To("fakePrincipalID"), 805 PrincipalType: armauthorization.PrincipalTypeServicePrincipal, 806 }, 807 }, 808 }, 809 { 810 name: "returns role assignment spec if scope and role definition ID are set", 811 machinePoolScope: MachinePoolScope{ 812 MachinePool: &expv1.MachinePool{}, 813 AzureMachinePool: &infrav1exp.AzureMachinePool{ 814 ObjectMeta: metav1.ObjectMeta{ 815 Name: "machine-name", 816 }, 817 Spec: infrav1exp.AzureMachinePoolSpec{ 818 Identity: infrav1.VMIdentitySystemAssigned, 819 SystemAssignedIdentityRole: &infrav1.SystemAssignedIdentityRole{ 820 Name: "role-assignment-name", 821 Scope: "scope", 822 DefinitionID: "role-definition-id", 823 }, 824 }, 825 }, 826 ClusterScoper: &ClusterScope{ 827 AzureClients: AzureClients{ 828 EnvironmentSettings: auth.EnvironmentSettings{ 829 Values: map[string]string{ 830 auth.SubscriptionID: "123", 831 }, 832 }, 833 }, 834 AzureCluster: &infrav1.AzureCluster{ 835 Spec: infrav1.AzureClusterSpec{ 836 ResourceGroup: "my-rg", 837 AzureClusterClassSpec: infrav1.AzureClusterClassSpec{ 838 Location: "westus", 839 }, 840 }, 841 }, 842 }, 843 }, 844 want: []azure.ResourceSpecGetter{ 845 &roleassignments.RoleAssignmentSpec{ 846 ResourceType: azure.VirtualMachineScaleSet, 847 MachineName: "machine-name", 848 Name: "role-assignment-name", 849 ResourceGroup: "my-rg", 850 Scope: "scope", 851 RoleDefinitionID: "role-definition-id", 852 PrincipalID: ptr.To("fakePrincipalID"), 853 PrincipalType: armauthorization.PrincipalTypeServicePrincipal, 854 }, 855 }, 856 }, 857 } 858 for _, tt := range tests { 859 t.Run(tt.name, func(t *testing.T) { 860 if got := tt.machinePoolScope.RoleAssignmentSpecs(ptr.To("fakePrincipalID")); !reflect.DeepEqual(got, tt.want) { 861 t.Errorf("RoleAssignmentSpecs() = %v, want %v", got, tt.want) 862 } 863 }) 864 } 865 } 866 867 func TestMachinePoolScope_VMSSExtensionSpecs(t *testing.T) { 868 tests := []struct { 869 name string 870 machinePoolScope MachinePoolScope 871 want []azure.ResourceSpecGetter 872 }{ 873 { 874 name: "If OS type is Linux and cloud is AzurePublicCloud, it returns ExtensionSpec", 875 machinePoolScope: MachinePoolScope{ 876 MachinePool: &expv1.MachinePool{}, 877 AzureMachinePool: &infrav1exp.AzureMachinePool{ 878 ObjectMeta: metav1.ObjectMeta{ 879 Name: "machinepool-name", 880 }, 881 Spec: infrav1exp.AzureMachinePoolSpec{ 882 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 883 OSDisk: infrav1.OSDisk{ 884 OSType: "Linux", 885 }, 886 }, 887 }, 888 }, 889 ClusterScoper: &ClusterScope{ 890 AzureClients: AzureClients{ 891 EnvironmentSettings: auth.EnvironmentSettings{ 892 Environment: azureautorest.Environment{ 893 Name: azureautorest.PublicCloud.Name, 894 }, 895 }, 896 }, 897 AzureCluster: &infrav1.AzureCluster{ 898 Spec: infrav1.AzureClusterSpec{ 899 ResourceGroup: "my-rg", 900 }, 901 }, 902 }, 903 cache: &MachinePoolCache{ 904 VMSKU: resourceskus.SKU{}, 905 }, 906 }, 907 want: []azure.ResourceSpecGetter{ 908 &scalesets.VMSSExtensionSpec{ 909 ExtensionSpec: azure.ExtensionSpec{ 910 Name: "CAPZ.Linux.Bootstrapping", 911 VMName: "machinepool-name", 912 Publisher: "Microsoft.Azure.ContainerUpstream", 913 Version: "1.0", 914 ProtectedSettings: map[string]string{ 915 "commandToExecute": azure.LinuxBootstrapExtensionCommand, 916 }, 917 }, 918 ResourceGroup: "my-rg", 919 }, 920 }, 921 }, 922 { 923 name: "If OS type is Linux and cloud is not AzurePublicCloud, it returns empty", 924 machinePoolScope: MachinePoolScope{ 925 MachinePool: &expv1.MachinePool{}, 926 AzureMachinePool: &infrav1exp.AzureMachinePool{ 927 ObjectMeta: metav1.ObjectMeta{ 928 Name: "machinepool-name", 929 }, 930 Spec: infrav1exp.AzureMachinePoolSpec{ 931 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 932 OSDisk: infrav1.OSDisk{ 933 OSType: "Linux", 934 }, 935 }, 936 }, 937 }, 938 ClusterScoper: &ClusterScope{ 939 AzureClients: AzureClients{ 940 EnvironmentSettings: auth.EnvironmentSettings{ 941 Environment: azureautorest.Environment{ 942 Name: azureautorest.USGovernmentCloud.Name, 943 }, 944 }, 945 }, 946 AzureCluster: &infrav1.AzureCluster{ 947 Spec: infrav1.AzureClusterSpec{ 948 ResourceGroup: "my-rg", 949 }, 950 }, 951 }, 952 cache: &MachinePoolCache{ 953 VMSKU: resourceskus.SKU{}, 954 }, 955 }, 956 want: []azure.ResourceSpecGetter{}, 957 }, 958 { 959 name: "If OS type is Windows and cloud is AzurePublicCloud, it returns ExtensionSpec", 960 machinePoolScope: MachinePoolScope{ 961 MachinePool: &expv1.MachinePool{}, 962 AzureMachinePool: &infrav1exp.AzureMachinePool{ 963 ObjectMeta: metav1.ObjectMeta{ 964 // Note: machine pool names longer than 9 characters get truncated. See MachinePoolScope::Name() for more details. 965 Name: "winpool", 966 }, 967 Spec: infrav1exp.AzureMachinePoolSpec{ 968 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 969 OSDisk: infrav1.OSDisk{ 970 OSType: "Windows", 971 }, 972 }, 973 }, 974 }, 975 ClusterScoper: &ClusterScope{ 976 AzureClients: AzureClients{ 977 EnvironmentSettings: auth.EnvironmentSettings{ 978 Environment: azureautorest.Environment{ 979 Name: azureautorest.PublicCloud.Name, 980 }, 981 }, 982 }, 983 AzureCluster: &infrav1.AzureCluster{ 984 Spec: infrav1.AzureClusterSpec{ 985 ResourceGroup: "my-rg", 986 }, 987 }, 988 }, 989 cache: &MachinePoolCache{ 990 VMSKU: resourceskus.SKU{}, 991 }, 992 }, 993 want: []azure.ResourceSpecGetter{ 994 &scalesets.VMSSExtensionSpec{ 995 ExtensionSpec: azure.ExtensionSpec{ 996 Name: "CAPZ.Windows.Bootstrapping", 997 // Note: machine pool names longer than 9 characters get truncated. See MachinePoolScope::Name() for more details. 998 VMName: "winpool", 999 Publisher: "Microsoft.Azure.ContainerUpstream", 1000 Version: "1.0", 1001 ProtectedSettings: map[string]string{ 1002 "commandToExecute": azure.WindowsBootstrapExtensionCommand, 1003 }, 1004 }, 1005 ResourceGroup: "my-rg", 1006 }, 1007 }, 1008 }, 1009 { 1010 name: "If OS type is Windows and cloud is not AzurePublicCloud, it returns empty", 1011 machinePoolScope: MachinePoolScope{ 1012 MachinePool: &expv1.MachinePool{}, 1013 AzureMachinePool: &infrav1exp.AzureMachinePool{ 1014 ObjectMeta: metav1.ObjectMeta{ 1015 Name: "machinepool-name", 1016 }, 1017 Spec: infrav1exp.AzureMachinePoolSpec{ 1018 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 1019 OSDisk: infrav1.OSDisk{ 1020 OSType: "Windows", 1021 }, 1022 }, 1023 }, 1024 }, 1025 ClusterScoper: &ClusterScope{ 1026 AzureClients: AzureClients{ 1027 EnvironmentSettings: auth.EnvironmentSettings{ 1028 Environment: azureautorest.Environment{ 1029 Name: azureautorest.USGovernmentCloud.Name, 1030 }, 1031 }, 1032 }, 1033 AzureCluster: &infrav1.AzureCluster{ 1034 Spec: infrav1.AzureClusterSpec{ 1035 ResourceGroup: "my-rg", 1036 }, 1037 }, 1038 }, 1039 cache: &MachinePoolCache{ 1040 VMSKU: resourceskus.SKU{}, 1041 }, 1042 }, 1043 want: []azure.ResourceSpecGetter{}, 1044 }, 1045 { 1046 name: "If OS type is not Linux or Windows and cloud is AzurePublicCloud, it returns empty", 1047 machinePoolScope: MachinePoolScope{ 1048 MachinePool: &expv1.MachinePool{}, 1049 AzureMachinePool: &infrav1exp.AzureMachinePool{ 1050 ObjectMeta: metav1.ObjectMeta{ 1051 Name: "machinepool-name", 1052 }, 1053 Spec: infrav1exp.AzureMachinePoolSpec{ 1054 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 1055 OSDisk: infrav1.OSDisk{ 1056 OSType: "Other", 1057 }, 1058 }, 1059 }, 1060 }, 1061 ClusterScoper: &ClusterScope{ 1062 AzureClients: AzureClients{ 1063 EnvironmentSettings: auth.EnvironmentSettings{ 1064 Environment: azureautorest.Environment{ 1065 Name: azureautorest.PublicCloud.Name, 1066 }, 1067 }, 1068 }, 1069 AzureCluster: &infrav1.AzureCluster{ 1070 Spec: infrav1.AzureClusterSpec{ 1071 ResourceGroup: "my-rg", 1072 }, 1073 }, 1074 }, 1075 cache: &MachinePoolCache{ 1076 VMSKU: resourceskus.SKU{}, 1077 }, 1078 }, 1079 want: []azure.ResourceSpecGetter{}, 1080 }, 1081 { 1082 name: "If OS type is not Windows or Linux and cloud is not AzurePublicCloud, it returns empty", 1083 machinePoolScope: MachinePoolScope{ 1084 MachinePool: &expv1.MachinePool{}, 1085 AzureMachinePool: &infrav1exp.AzureMachinePool{ 1086 ObjectMeta: metav1.ObjectMeta{ 1087 Name: "machinepool-name", 1088 }, 1089 Spec: infrav1exp.AzureMachinePoolSpec{ 1090 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 1091 OSDisk: infrav1.OSDisk{ 1092 OSType: "Other", 1093 }, 1094 }, 1095 }, 1096 }, 1097 ClusterScoper: &ClusterScope{ 1098 AzureClients: AzureClients{ 1099 EnvironmentSettings: auth.EnvironmentSettings{ 1100 Environment: azureautorest.Environment{ 1101 Name: azureautorest.USGovernmentCloud.Name, 1102 }, 1103 }, 1104 }, 1105 AzureCluster: &infrav1.AzureCluster{ 1106 Spec: infrav1.AzureClusterSpec{ 1107 ResourceGroup: "my-rg", 1108 }, 1109 }, 1110 }, 1111 cache: &MachinePoolCache{ 1112 VMSKU: resourceskus.SKU{}, 1113 }, 1114 }, 1115 want: []azure.ResourceSpecGetter{}, 1116 }, 1117 { 1118 name: "If a custom VM extension is specified, it returns the custom VM extension", 1119 machinePoolScope: MachinePoolScope{ 1120 MachinePool: &expv1.MachinePool{}, 1121 AzureMachinePool: &infrav1exp.AzureMachinePool{ 1122 ObjectMeta: metav1.ObjectMeta{ 1123 Name: "machinepool-name", 1124 }, 1125 Spec: infrav1exp.AzureMachinePoolSpec{ 1126 Template: infrav1exp.AzureMachinePoolMachineTemplate{ 1127 OSDisk: infrav1.OSDisk{ 1128 OSType: "Linux", 1129 }, 1130 VMExtensions: []infrav1.VMExtension{ 1131 { 1132 Name: "custom-vm-extension", 1133 Publisher: "Microsoft.Azure.Extensions", 1134 Version: "2.0", 1135 Settings: map[string]string{ 1136 "timestamp": "1234567890", 1137 }, 1138 ProtectedSettings: map[string]string{ 1139 "commandToExecute": "echo hello world", 1140 }, 1141 }, 1142 }, 1143 }, 1144 }, 1145 }, 1146 ClusterScoper: &ClusterScope{ 1147 AzureClients: AzureClients{ 1148 EnvironmentSettings: auth.EnvironmentSettings{ 1149 Environment: azureautorest.Environment{ 1150 Name: azureautorest.PublicCloud.Name, 1151 }, 1152 }, 1153 }, 1154 AzureCluster: &infrav1.AzureCluster{ 1155 Spec: infrav1.AzureClusterSpec{ 1156 ResourceGroup: "my-rg", 1157 AzureClusterClassSpec: infrav1.AzureClusterClassSpec{ 1158 Location: "westus", 1159 }, 1160 }, 1161 }, 1162 }, 1163 cache: &MachinePoolCache{ 1164 VMSKU: resourceskus.SKU{}, 1165 }, 1166 }, 1167 want: []azure.ResourceSpecGetter{ 1168 &scalesets.VMSSExtensionSpec{ 1169 ExtensionSpec: azure.ExtensionSpec{ 1170 Name: "custom-vm-extension", 1171 VMName: "machinepool-name", 1172 Publisher: "Microsoft.Azure.Extensions", 1173 Version: "2.0", 1174 Settings: map[string]string{ 1175 "timestamp": "1234567890", 1176 }, 1177 ProtectedSettings: map[string]string{ 1178 "commandToExecute": "echo hello world", 1179 }, 1180 }, 1181 ResourceGroup: "my-rg", 1182 }, 1183 &scalesets.VMSSExtensionSpec{ 1184 ExtensionSpec: azure.ExtensionSpec{ 1185 Name: "CAPZ.Linux.Bootstrapping", 1186 VMName: "machinepool-name", 1187 Publisher: "Microsoft.Azure.ContainerUpstream", 1188 Version: "1.0", 1189 ProtectedSettings: map[string]string{ 1190 "commandToExecute": azure.LinuxBootstrapExtensionCommand, 1191 }, 1192 }, 1193 ResourceGroup: "my-rg", 1194 }, 1195 }, 1196 }, 1197 } 1198 for _, tt := range tests { 1199 t.Run(tt.name, func(t *testing.T) { 1200 if got := tt.machinePoolScope.VMSSExtensionSpecs(); !reflect.DeepEqual(got, tt.want) { 1201 t.Errorf("VMSSExtensionSpecs() = \n%s, want \n%s", specArrayToString(got), specArrayToString(tt.want)) 1202 } 1203 }) 1204 } 1205 } 1206 1207 func getReadyAzureMachinePoolMachines(count int32) []infrav1exp.AzureMachinePoolMachine { 1208 machines := make([]infrav1exp.AzureMachinePoolMachine, count) 1209 succeeded := infrav1.Succeeded 1210 for i := 0; i < int(count); i++ { 1211 machines[i] = infrav1exp.AzureMachinePoolMachine{ 1212 ObjectMeta: metav1.ObjectMeta{ 1213 Name: fmt.Sprintf("ampm%d", i), 1214 Namespace: "default", 1215 OwnerReferences: []metav1.OwnerReference{ 1216 { 1217 Name: "amp", 1218 Kind: infrav1.AzureMachinePoolKind, 1219 APIVersion: infrav1exp.GroupVersion.String(), 1220 }, 1221 }, 1222 Labels: map[string]string{ 1223 clusterv1.ClusterNameLabel: "cluster1", 1224 infrav1exp.MachinePoolNameLabel: "amp1", 1225 clusterv1.MachinePoolNameLabel: "mp1", 1226 "cluster1": string(infrav1.ResourceLifecycleOwned), 1227 }, 1228 }, 1229 Spec: infrav1exp.AzureMachinePoolMachineSpec{ 1230 ProviderID: fmt.Sprintf("azure://foo/ampm%d", i), 1231 }, 1232 Status: infrav1exp.AzureMachinePoolMachineStatus{ 1233 Ready: true, 1234 ProvisioningState: &succeeded, 1235 }, 1236 } 1237 } 1238 1239 return machines 1240 } 1241 1242 func getAzureMachinePoolMachine(index int) infrav1exp.AzureMachinePoolMachine { 1243 return infrav1exp.AzureMachinePoolMachine{ 1244 ObjectMeta: metav1.ObjectMeta{ 1245 Name: fmt.Sprintf("ampm%d", index), 1246 Namespace: "default", 1247 OwnerReferences: []metav1.OwnerReference{ 1248 { 1249 Name: "amp", 1250 Kind: "AzureMachinePool", 1251 APIVersion: infrav1exp.GroupVersion.String(), 1252 }, 1253 }, 1254 Labels: map[string]string{ 1255 clusterv1.ClusterNameLabel: "cluster1", 1256 infrav1exp.MachinePoolNameLabel: "amp1", 1257 clusterv1.MachinePoolNameLabel: "mp1", 1258 "cluster1": string(infrav1.ResourceLifecycleOwned), 1259 }, 1260 }, 1261 Spec: infrav1exp.AzureMachinePoolMachineSpec{ 1262 ProviderID: fmt.Sprintf("azure:///subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/%d", index), 1263 }, 1264 Status: infrav1exp.AzureMachinePoolMachineStatus{ 1265 Ready: true, 1266 ProvisioningState: ptr.To(infrav1.Succeeded), 1267 }, 1268 } 1269 } 1270 1271 func getAzureMachinePoolMachineWithOwnerMachine(index int) (clusterv1.Machine, infrav1exp.AzureMachinePoolMachine) { 1272 ampm := getAzureMachinePoolMachine(index) 1273 machine := clusterv1.Machine{ 1274 ObjectMeta: metav1.ObjectMeta{ 1275 Name: fmt.Sprintf("mpm%d", index), 1276 Namespace: "default", 1277 OwnerReferences: []metav1.OwnerReference{ 1278 { 1279 Name: "mp", 1280 Kind: "MachinePool", 1281 APIVersion: expv1.GroupVersion.String(), 1282 }, 1283 }, 1284 Labels: map[string]string{ 1285 clusterv1.ClusterNameLabel: "cluster1", 1286 clusterv1.MachinePoolNameLabel: "mp1", 1287 }, 1288 }, 1289 Spec: clusterv1.MachineSpec{ 1290 ProviderID: &m.Spec.ProviderID, 1291 InfrastructureRef: corev1.ObjectReference{ 1292 Kind: "AzureMachinePoolMachine", 1293 Name: ampm.Name, 1294 Namespace: ampm.Namespace, 1295 }, 1296 }, 1297 } 1298 1299 ampm.OwnerReferences = append(ampm.OwnerReferences, metav1.OwnerReference{ 1300 Name: machine.Name, 1301 Kind: "Machine", 1302 APIVersion: clusterv1.GroupVersion.String(), 1303 }) 1304 1305 return machine, ampm 1306 } 1307 1308 func TestMachinePoolScope_SetInfrastructureMachineKind(t *testing.T) { 1309 testcases := []struct { 1310 name string 1311 azureMachinePool infrav1exp.AzureMachinePool 1312 updated bool 1313 }{ 1314 { 1315 name: "should set infrastructure machine kind", 1316 azureMachinePool: infrav1exp.AzureMachinePool{ 1317 Status: infrav1exp.AzureMachinePoolStatus{}, 1318 }, 1319 updated: true, 1320 }, 1321 { 1322 name: "already set infrastructure machine kind", 1323 azureMachinePool: infrav1exp.AzureMachinePool{ 1324 Status: infrav1exp.AzureMachinePoolStatus{ 1325 InfrastructureMachineKind: infrav1exp.AzureMachinePoolMachineKind, 1326 }, 1327 }, 1328 updated: false, 1329 }, 1330 } 1331 1332 for _, tt := range testcases { 1333 tt := tt 1334 t.Run(tt.name, func(t *testing.T) { 1335 g := NewWithT(t) 1336 1337 machinePoolScope := &MachinePoolScope{ 1338 AzureMachinePool: &tt.azureMachinePool, 1339 } 1340 1341 got := machinePoolScope.SetInfrastructureMachineKind() 1342 g.Expect(machinePoolScope.AzureMachinePool.Status.InfrastructureMachineKind).To(Equal(infrav1exp.AzureMachinePoolMachineKind)) 1343 g.Expect(got).To(Equal(tt.updated)) 1344 }) 1345 } 1346 } 1347 1348 func TestMachinePoolScope_applyAzureMachinePoolMachines(t *testing.T) { 1349 ctx, cancel := context.WithCancel(context.Background()) 1350 defer cancel() 1351 scheme := runtime.NewScheme() 1352 _ = clusterv1.AddToScheme(scheme) 1353 _ = infrav1exp.AddToScheme(scheme) 1354 1355 tests := []struct { 1356 Name string 1357 Setup func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) 1358 Verify func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) 1359 }{ 1360 { 1361 Name: "if MachinePool is externally managed and overProvisionCount > 0, do not try to reduce replicas", 1362 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) { 1363 mp.Annotations = map[string]string{clusterv1.ReplicasManagedByAnnotation: "cluster-autoscaler"} 1364 mp.Spec.Replicas = ptr.To[int32](1) 1365 1366 mpm1, ampm1 := getAzureMachinePoolMachineWithOwnerMachine(1) 1367 mpm2, ampm2 := getAzureMachinePoolMachineWithOwnerMachine(2) 1368 objects := []client.Object{} 1369 objects = append(objects, &mpm1, &m1, &mpm2, &m2) 1370 cb.WithObjects(objects...) 1371 vmssState.Instances = []azure.VMSSVM{ 1372 { 1373 ID: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/1", 1374 Name: "ampm1", 1375 }, 1376 { 1377 ID: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/2", 1378 Name: "ampm2", 1379 }, 1380 } 1381 }, 1382 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) { 1383 g.Expect(err).NotTo(HaveOccurred()) 1384 list := clusterv1.MachineList{} 1385 g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred()) 1386 g.Expect(list.Items).Should(HaveLen(2)) 1387 }, 1388 }, 1389 { 1390 Name: "if MachinePool is not externally managed and overProvisionCount > 0, reduce replicas", 1391 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) { 1392 mp.Spec.Replicas = ptr.To[int32](1) 1393 1394 mpm1, ampm1 := getAzureMachinePoolMachineWithOwnerMachine(1) 1395 mpm2, ampm2 := getAzureMachinePoolMachineWithOwnerMachine(2) 1396 objects := []client.Object{} 1397 objects = append(objects, &mpm1, &m1, &mpm2, &m2) 1398 cb.WithObjects(objects...) 1399 1400 vmssState.Instances = []azure.VMSSVM{ 1401 { 1402 ID: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/1", 1403 Name: "ampm1", 1404 }, 1405 { 1406 ID: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/2", 1407 Name: "ampm2", 1408 }, 1409 } 1410 }, 1411 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) { 1412 g.Expect(err).NotTo(HaveOccurred()) 1413 list := clusterv1.MachineList{} 1414 g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred()) 1415 g.Expect(list.Items).Should(HaveLen(1)) 1416 }, 1417 }, 1418 { 1419 Name: "if MachinePool is not externally managed, and Machines have delete machine annotation, and overProvisionCount > 0, delete machines with deleteMachine annotation first", 1420 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) { 1421 mp.Spec.Replicas = ptr.To[int32](2) 1422 1423 mpm1, ampm1 := getAzureMachinePoolMachineWithOwnerMachine(1) 1424 1425 mpm2, ampm2 := getAzureMachinePoolMachineWithOwnerMachine(2) 1426 mpm2.Annotations = map[string]string{ 1427 clusterv1.DeleteMachineAnnotation: time.Now().String(), 1428 } 1429 1430 mpm3, ampm3 := getAzureMachinePoolMachineWithOwnerMachine(3) 1431 objects := []client.Object{&mpm1, &m1, &mpm2, &m2, &mpm3, &m3} 1432 cb.WithObjects(objects...) 1433 1434 vmssState.Instances = []azure.VMSSVM{ 1435 { 1436 ID: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/1", 1437 Name: "ampm1", 1438 }, 1439 { 1440 ID: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/2", 1441 Name: "ampm2", 1442 }, 1443 { 1444 ID: "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/3", 1445 Name: "ampm3", 1446 }, 1447 } 1448 }, 1449 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) { 1450 g.Expect(err).NotTo(HaveOccurred()) 1451 list := clusterv1.MachineList{} 1452 g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred()) 1453 g.Expect(list.Items).Should(HaveLen(2)) 1454 g.Expect(list.Items[0].Name).Should(Equal("mpm1")) 1455 g.Expect(list.Items[1].Name).Should(Equal("mpm3")) 1456 }, 1457 }, 1458 { 1459 Name: "if existing MachinePool is not present, reduce replicas", 1460 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) { 1461 mp.Spec.Replicas = ptr.To[int32](1) 1462 1463 vmssState.Instances = []azure.VMSSVM{ 1464 { 1465 ID: "/subscriptions/123/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm", 1466 Name: "vm", 1467 }, 1468 } 1469 }, 1470 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) { 1471 g.Expect(err).NotTo(HaveOccurred()) 1472 list := infrav1exp.AzureMachinePoolMachineList{} 1473 g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred()) 1474 g.Expect(list.Items).Should(HaveLen(1)) 1475 }, 1476 }, 1477 { 1478 Name: "if existing MachinePool is not present and Instances ID is in wrong format, reduce replicas", 1479 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) { 1480 mp.Spec.Replicas = ptr.To[int32](1) 1481 1482 vmssState.Instances = []azure.VMSSVM{ 1483 { 1484 ID: "foo/ampm0", 1485 Name: "ampm0", 1486 }, 1487 } 1488 }, 1489 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) { 1490 g.Expect(err).To(HaveOccurred()) 1491 }, 1492 }, 1493 { 1494 Name: "if existing MachinePool is present but in deleting state, do not recreate AzureMachinePoolMachines", 1495 Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) { 1496 mp.Spec.Replicas = ptr.To[int32](1) 1497 1498 vmssState.Instances = []azure.VMSSVM{ 1499 { 1500 ID: "/subscriptions/123/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm", 1501 Name: "vm", 1502 State: infrav1.Deleting, 1503 }, 1504 } 1505 }, 1506 Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) { 1507 g.Expect(err).NotTo(HaveOccurred()) 1508 list := infrav1exp.AzureMachinePoolMachineList{} 1509 g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred()) 1510 g.Expect(list.Items).Should(BeEmpty()) 1511 }, 1512 }, 1513 } 1514 for _, tt := range tests { 1515 t.Run(tt.Name, func(t *testing.T) { 1516 var ( 1517 g = NewWithT(t) 1518 mockCtrl = gomock.NewController(t) 1519 cb = fake.NewClientBuilder().WithScheme(scheme) 1520 cluster = &clusterv1.Cluster{ 1521 ObjectMeta: metav1.ObjectMeta{ 1522 Name: "cluster1", 1523 Namespace: "default", 1524 }, 1525 Spec: clusterv1.ClusterSpec{ 1526 InfrastructureRef: &corev1.ObjectReference{ 1527 Name: "azCluster1", 1528 }, 1529 }, 1530 Status: clusterv1.ClusterStatus{ 1531 InfrastructureReady: true, 1532 }, 1533 } 1534 mp = &expv1.MachinePool{ 1535 ObjectMeta: metav1.ObjectMeta{ 1536 Name: "mp1", 1537 Namespace: "default", 1538 OwnerReferences: []metav1.OwnerReference{ 1539 { 1540 Name: "cluster1", 1541 Kind: "Cluster", 1542 APIVersion: clusterv1.GroupVersion.String(), 1543 }, 1544 }, 1545 }, 1546 } 1547 amp = &infrav1exp.AzureMachinePool{ 1548 ObjectMeta: metav1.ObjectMeta{ 1549 Name: "amp1", 1550 Namespace: "default", 1551 OwnerReferences: []metav1.OwnerReference{ 1552 { 1553 Name: "mp1", 1554 Kind: "MachinePool", 1555 APIVersion: expv1.GroupVersion.String(), 1556 }, 1557 }, 1558 }, 1559 } 1560 vmssState = &azure.VMSS{} 1561 ) 1562 defer mockCtrl.Finish() 1563 1564 tt.Setup(mp, amp, vmssState, cb.WithObjects(amp, cluster)) 1565 s := &MachinePoolScope{ 1566 client: cb.Build(), 1567 ClusterScoper: &ClusterScope{ 1568 Cluster: cluster, 1569 }, 1570 MachinePool: mp, 1571 AzureMachinePool: amp, 1572 vmssState: vmssState, 1573 } 1574 err := s.applyAzureMachinePoolMachines(ctx) 1575 tt.Verify(g, s.AzureMachinePool, s.client, err) 1576 }) 1577 } 1578 }