sigs.k8s.io/cluster-api-provider-azure@v1.17.0/exp/api/v1beta1/azuremachinepool_webhook_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 v1beta1 18 19 import ( 20 "context" 21 "crypto/rand" 22 "crypto/rsa" 23 "encoding/base64" 24 "fmt" 25 "testing" 26 27 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" 28 guuid "github.com/google/uuid" 29 . "github.com/onsi/gomega" 30 "github.com/pkg/errors" 31 "golang.org/x/crypto/ssh" 32 corev1 "k8s.io/api/core/v1" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/util/intstr" 35 "k8s.io/apimachinery/pkg/util/uuid" 36 utilfeature "k8s.io/component-base/featuregate/testing" 37 "k8s.io/utils/ptr" 38 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" 39 "sigs.k8s.io/cluster-api-provider-azure/feature" 40 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 41 expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 42 capifeature "sigs.k8s.io/cluster-api/feature" 43 "sigs.k8s.io/controller-runtime/pkg/client" 44 ) 45 46 var ( 47 validSSHPublicKey = generateSSHPublicKey(true) 48 zero = intstr.FromInt(0) 49 one = intstr.FromInt(1) 50 ) 51 52 type mockClient struct { 53 client.Client 54 Version string 55 ReturnError bool 56 } 57 58 func (m mockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 59 obj.(*expv1.MachinePool).Spec.Template.Spec.Version = &m.Version 60 return nil 61 } 62 63 func (m mockClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 64 if m.ReturnError { 65 return errors.New("MachinePool.cluster.x-k8s.io \"mock-machinepool-mp-0\" not found") 66 } 67 mp := &expv1.MachinePool{} 68 mp.Spec.Template.Spec.Version = &m.Version 69 list.(*expv1.MachinePoolList).Items = []expv1.MachinePool{*mp} 70 71 return nil 72 } 73 74 func TestAzureMachinePool_ValidateCreate(t *testing.T) { 75 tests := []struct { 76 name string 77 amp *AzureMachinePool 78 version string 79 ownerNotFound bool 80 wantErr bool 81 }{ 82 { 83 name: "valid", 84 amp: getKnownValidAzureMachinePool(), 85 wantErr: false, 86 }, 87 { 88 name: "azuremachinepool with marketplace image - full", 89 amp: createMachinePoolWithMarketPlaceImage("PUB1234", "OFFER1234", "SKU1234", "1.0.0", ptr.To(10)), 90 wantErr: false, 91 }, 92 { 93 name: "azuremachinepool with marketplace image - missing publisher", 94 amp: createMachinePoolWithMarketPlaceImage("", "OFFER1234", "SKU1234", "1.0.0", ptr.To(10)), 95 wantErr: true, 96 }, 97 { 98 name: "azuremachinepool with shared gallery image - full", 99 amp: createMachinePoolWithSharedImage("SUB123", "RG123", "NAME123", "GALLERY1", "1.0.0", ptr.To(10)), 100 wantErr: false, 101 }, 102 { 103 name: "azuremachinepool with marketplace image - missing subscription", 104 amp: createMachinePoolWithSharedImage("", "RG123", "NAME123", "GALLERY1", "1.0.0", ptr.To(10)), 105 wantErr: true, 106 }, 107 { 108 name: "azuremachinepool with image by - with id", 109 amp: createMachinePoolWithImageByID("ID123", ptr.To(10)), 110 wantErr: false, 111 }, 112 { 113 name: "azuremachinepool with image by - without id", 114 amp: createMachinePoolWithImageByID("", ptr.To(10)), 115 wantErr: true, 116 }, 117 { 118 name: "azuremachinepool with valid SSHPublicKey", 119 amp: createMachinePoolWithSSHPublicKey(validSSHPublicKey), 120 wantErr: false, 121 }, 122 { 123 name: "azuremachinepool with invalid SSHPublicKey", 124 amp: createMachinePoolWithSSHPublicKey("invalid ssh key"), 125 wantErr: true, 126 }, 127 { 128 name: "azuremachinepool with wrong terminate notification", 129 amp: createMachinePoolWithSharedImage("SUB123", "RG123", "NAME123", "GALLERY1", "1.0.0", ptr.To(35)), 130 wantErr: true, 131 }, 132 { 133 name: "azuremachinepool with system assigned identity", 134 amp: createMachinePoolWithSystemAssignedIdentity(string(uuid.NewUUID())), 135 wantErr: false, 136 }, 137 { 138 name: "azuremachinepool with system assigned identity, but invalid role", 139 amp: createMachinePoolWithSystemAssignedIdentity("not_a_uuid"), 140 wantErr: true, 141 }, 142 { 143 name: "azuremachinepool with user assigned identity", 144 amp: createMachinePoolWithUserAssignedIdentity([]string{ 145 "azure:///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group/providers/Microsoft.Compute/virtualMachines/default-20202-control-plane-7w265", 146 "azure:///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group/providers/Microsoft.Compute/virtualMachines/default-20202-control-plane-a6b7d", 147 }), 148 wantErr: false, 149 }, 150 { 151 name: "azuremachinepool with user assigned identity, but without any provider ids", 152 amp: createMachinePoolWithUserAssignedIdentity([]string{}), 153 wantErr: true, 154 }, 155 { 156 name: "azuremachinepool with managed diagnostics profile", 157 amp: createMachinePoolWithDiagnostics(infrav1.ManagedDiagnosticsStorage, nil), 158 wantErr: false, 159 }, 160 { 161 name: "azuremachinepool with disabled diagnostics profile", 162 amp: createMachinePoolWithDiagnostics(infrav1.ManagedDiagnosticsStorage, nil), 163 wantErr: false, 164 }, 165 { 166 name: "azuremachinepool with user managed diagnostics profile and defined user managed storage account", 167 amp: createMachinePoolWithDiagnostics(infrav1.UserManagedDiagnosticsStorage, &infrav1.UserManagedBootDiagnostics{StorageAccountURI: "https://fakeurl"}), 168 wantErr: false, 169 }, 170 { 171 name: "azuremachinepool with empty diagnostics profile", 172 amp: createMachinePoolWithDiagnostics("", nil), 173 wantErr: false, 174 }, 175 { 176 name: "azuremachinepool with user managed diagnostics profile, but empty user managed storage account", 177 amp: createMachinePoolWithDiagnostics(infrav1.UserManagedDiagnosticsStorage, nil), 178 wantErr: true, 179 }, 180 { 181 name: "azuremachinepool with invalid MaxSurge and MaxUnavailable rolling upgrade configuration", 182 amp: createMachinePoolWithStrategy(AzureMachinePoolDeploymentStrategy{ 183 Type: RollingUpdateAzureMachinePoolDeploymentStrategyType, 184 RollingUpdate: &MachineRollingUpdateDeployment{ 185 MaxSurge: &zero, 186 MaxUnavailable: &zero, 187 }, 188 }), 189 wantErr: true, 190 }, 191 { 192 name: "azuremachinepool with valid MaxSurge and MaxUnavailable rolling upgrade configuration", 193 amp: createMachinePoolWithStrategy(AzureMachinePoolDeploymentStrategy{ 194 Type: RollingUpdateAzureMachinePoolDeploymentStrategyType, 195 RollingUpdate: &MachineRollingUpdateDeployment{ 196 MaxSurge: &zero, 197 MaxUnavailable: &one, 198 }, 199 }), 200 wantErr: false, 201 }, 202 { 203 name: "azuremachinepool with valid legacy network configuration", 204 amp: createMachinePoolWithNetworkConfig("testSubnet", []infrav1.NetworkInterface{}), 205 wantErr: false, 206 }, 207 { 208 name: "azuremachinepool with invalid legacy network configuration", 209 amp: createMachinePoolWithNetworkConfig("testSubnet", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}), 210 wantErr: true, 211 }, 212 { 213 name: "azuremachinepool with valid networkinterface configuration", 214 amp: createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}), 215 wantErr: false, 216 }, 217 { 218 name: "azuremachinepool with Flexible orchestration mode", 219 amp: createMachinePoolWithOrchestrationMode(armcompute.OrchestrationModeFlexible), 220 version: "v1.26.0", 221 wantErr: false, 222 }, 223 { 224 name: "azuremachinepool with Flexible orchestration mode and invalid Kubernetes version", 225 amp: createMachinePoolWithOrchestrationMode(armcompute.OrchestrationModeFlexible), 226 version: "v1.25.6", 227 wantErr: true, 228 }, 229 { 230 name: "azuremachinepool with Flexible orchestration mode and invalid Kubernetes version, no owner", 231 amp: createMachinePoolWithOrchestrationMode(armcompute.OrchestrationModeFlexible), 232 version: "v1.25.6", 233 ownerNotFound: true, 234 wantErr: true, 235 }, 236 { 237 name: "azuremachinepool with invalid DiffDiskSettings", 238 amp: createMachinePoolWithDiffDiskSettings(infrav1.DiffDiskSettings{ 239 Placement: ptr.To(infrav1.DiffDiskPlacementResourceDisk), 240 }), 241 wantErr: true, 242 }, 243 { 244 name: "azuremachinepool with valid DiffDiskSettings", 245 amp: createMachinePoolWithDiffDiskSettings(infrav1.DiffDiskSettings{ 246 Option: string(armcompute.DiffDiskOptionsLocal), 247 Placement: ptr.To(infrav1.DiffDiskPlacementResourceDisk), 248 }), 249 wantErr: true, 250 }, 251 } 252 253 for _, tc := range tests { 254 client := mockClient{Version: tc.version, ReturnError: tc.ownerNotFound} 255 t.Run(tc.name, func(t *testing.T) { 256 g := NewWithT(t) 257 ampw := &azureMachinePoolWebhook{ 258 Client: client, 259 } 260 _, err := ampw.ValidateCreate(context.Background(), tc.amp) 261 if tc.wantErr { 262 g.Expect(err).To(HaveOccurred()) 263 } else { 264 g.Expect(err).NotTo(HaveOccurred()) 265 } 266 }) 267 } 268 } 269 270 type mockDefaultClient struct { 271 client.Client 272 Name string 273 ClusterName string 274 SubscriptionID string 275 Version string 276 ReturnError bool 277 } 278 279 func (m mockDefaultClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 280 switch obj := obj.(type) { 281 case *infrav1.AzureCluster: 282 obj.Spec.SubscriptionID = m.SubscriptionID 283 case *clusterv1.Cluster: 284 obj.Spec.InfrastructureRef = &corev1.ObjectReference{ 285 Kind: infrav1.AzureClusterKind, 286 Name: "test-cluster", 287 } 288 default: 289 return errors.New("invalid object type") 290 } 291 return nil 292 } 293 294 func (m mockDefaultClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 295 list.(*expv1.MachinePoolList).Items = []expv1.MachinePool{ 296 { 297 Spec: expv1.MachinePoolSpec{ 298 Template: clusterv1.MachineTemplateSpec{ 299 Spec: clusterv1.MachineSpec{ 300 InfrastructureRef: corev1.ObjectReference{ 301 Name: m.Name, 302 }, 303 }, 304 }, 305 ClusterName: m.ClusterName, 306 }, 307 }, 308 } 309 310 return nil 311 } 312 313 func TestAzureMachinePool_ValidateUpdate(t *testing.T) { 314 var ( 315 zero = intstr.FromInt(0) 316 one = intstr.FromInt(1) 317 ) 318 319 tests := []struct { 320 name string 321 oldAMP *AzureMachinePool 322 amp *AzureMachinePool 323 wantErr bool 324 }{ 325 { 326 name: "azuremachinepool with valid SSHPublicKey", 327 oldAMP: createMachinePoolWithSSHPublicKey(""), 328 amp: createMachinePoolWithSSHPublicKey(validSSHPublicKey), 329 wantErr: false, 330 }, 331 { 332 name: "azuremachinepool with invalid SSHPublicKey", 333 oldAMP: createMachinePoolWithSSHPublicKey(""), 334 amp: createMachinePoolWithSSHPublicKey("invalid ssh key"), 335 wantErr: true, 336 }, 337 { 338 name: "azuremachinepool with system-assigned identity, and role unchanged", 339 oldAMP: createMachinePoolWithSystemAssignedIdentity("30a757d8-fcf0-4c8b-acf0-9253a7e093ea"), 340 amp: createMachinePoolWithSystemAssignedIdentity("30a757d8-fcf0-4c8b-acf0-9253a7e093ea"), 341 wantErr: false, 342 }, 343 { 344 name: "azuremachinepool with system-assigned identity, and role changed", 345 oldAMP: createMachinePoolWithSystemAssignedIdentity(string(uuid.NewUUID())), 346 amp: createMachinePoolWithSystemAssignedIdentity(string(uuid.NewUUID())), 347 wantErr: true, 348 }, 349 { 350 name: "azuremachinepool with invalid MaxSurge and MaxUnavailable rolling upgrade configuration", 351 oldAMP: createMachinePoolWithStrategy(AzureMachinePoolDeploymentStrategy{}), 352 amp: createMachinePoolWithStrategy(AzureMachinePoolDeploymentStrategy{ 353 Type: RollingUpdateAzureMachinePoolDeploymentStrategyType, 354 RollingUpdate: &MachineRollingUpdateDeployment{ 355 MaxSurge: &zero, 356 MaxUnavailable: &zero, 357 }, 358 }), 359 wantErr: true, 360 }, 361 { 362 name: "azuremachinepool with valid MaxSurge and MaxUnavailable rolling upgrade configuration", 363 oldAMP: createMachinePoolWithStrategy(AzureMachinePoolDeploymentStrategy{}), 364 amp: createMachinePoolWithStrategy(AzureMachinePoolDeploymentStrategy{ 365 Type: RollingUpdateAzureMachinePoolDeploymentStrategyType, 366 RollingUpdate: &MachineRollingUpdateDeployment{ 367 MaxSurge: &zero, 368 MaxUnavailable: &one, 369 }, 370 }), 371 wantErr: false, 372 }, 373 { 374 name: "azuremachinepool with valid network interface config", 375 oldAMP: createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}), 376 amp: createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet2"}}), 377 wantErr: false, 378 }, 379 { 380 name: "azuremachinepool with valid network interface config", 381 oldAMP: createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}), 382 amp: createMachinePoolWithNetworkConfig("subnet", []infrav1.NetworkInterface{{SubnetName: "testSubnet2"}}), 383 wantErr: true, 384 }, 385 { 386 name: "azuremachinepool with valid network interface config", 387 oldAMP: createMachinePoolWithNetworkConfig("subnet", []infrav1.NetworkInterface{}), 388 amp: createMachinePoolWithNetworkConfig("subnet", []infrav1.NetworkInterface{{SubnetName: "testSubnet2"}}), 389 wantErr: true, 390 }, 391 } 392 for _, tc := range tests { 393 t.Run(tc.name, func(t *testing.T) { 394 g := NewWithT(t) 395 ampw := &azureMachinePoolWebhook{} 396 _, err := ampw.ValidateUpdate(context.Background(), tc.oldAMP, tc.amp) 397 if tc.wantErr { 398 g.Expect(err).To(HaveOccurred()) 399 } else { 400 g.Expect(err).NotTo(HaveOccurred()) 401 } 402 }) 403 } 404 } 405 406 func TestAzureMachinePool_Default(t *testing.T) { 407 g := NewWithT(t) 408 409 type test struct { 410 amp *AzureMachinePool 411 } 412 413 existingPublicKey := validSSHPublicKey 414 publicKeyExistTest := test{amp: createMachinePoolWithSSHPublicKey(existingPublicKey)} 415 publicKeyNotExistTest := test{amp: createMachinePoolWithSSHPublicKey("")} 416 417 existingRoleAssignmentName := "42862306-e485-4319-9bf0-35dbc6f6fe9c" 418 419 fakeSubscriptionID := guuid.New().String() 420 fakeClusterName := "testcluster" 421 fakeMachinePoolName := "testmachinepool" 422 mockClient := mockDefaultClient{Name: fakeMachinePoolName, ClusterName: fakeClusterName, SubscriptionID: fakeSubscriptionID} 423 424 roleAssignmentExistTest := test{amp: &AzureMachinePool{ 425 Spec: AzureMachinePoolSpec{ 426 Identity: "SystemAssigned", 427 SystemAssignedIdentityRole: &infrav1.SystemAssignedIdentityRole{ 428 Name: existingRoleAssignmentName, 429 Scope: "scope", 430 DefinitionID: "definitionID", 431 }, 432 }, 433 ObjectMeta: metav1.ObjectMeta{ 434 Name: fakeMachinePoolName, 435 }, 436 }} 437 438 emptyTest := test{amp: &AzureMachinePool{ 439 Spec: AzureMachinePoolSpec{ 440 Identity: "SystemAssigned", 441 SystemAssignedIdentityRole: &infrav1.SystemAssignedIdentityRole{}, 442 }, 443 ObjectMeta: metav1.ObjectMeta{ 444 Name: fakeMachinePoolName, 445 }, 446 }} 447 448 systemAssignedIdentityRoleExistTest := test{amp: &AzureMachinePool{ 449 Spec: AzureMachinePoolSpec{ 450 Identity: "SystemAssigned", 451 SystemAssignedIdentityRole: &infrav1.SystemAssignedIdentityRole{ 452 DefinitionID: "testroledefinitionid", 453 Scope: "testscope", 454 }, 455 }, 456 ObjectMeta: metav1.ObjectMeta{ 457 Name: fakeMachinePoolName, 458 }, 459 }} 460 461 ampw := &azureMachinePoolWebhook{ 462 Client: mockClient, 463 } 464 465 err := ampw.Default(context.Background(), roleAssignmentExistTest.amp) 466 g.Expect(err).NotTo(HaveOccurred()) 467 g.Expect(roleAssignmentExistTest.amp.Spec.SystemAssignedIdentityRole.Name).To(Equal(existingRoleAssignmentName)) 468 469 err = ampw.Default(context.Background(), publicKeyExistTest.amp) 470 g.Expect(err).NotTo(HaveOccurred()) 471 g.Expect(publicKeyExistTest.amp.Spec.Template.SSHPublicKey).To(Equal(existingPublicKey)) 472 473 err = ampw.Default(context.Background(), publicKeyNotExistTest.amp) 474 g.Expect(err).NotTo(HaveOccurred()) 475 g.Expect(publicKeyNotExistTest.amp.Spec.Template.SSHPublicKey).NotTo(BeEmpty()) 476 477 err = ampw.Default(context.Background(), systemAssignedIdentityRoleExistTest.amp) 478 g.Expect(err).NotTo(HaveOccurred()) 479 g.Expect(systemAssignedIdentityRoleExistTest.amp.Spec.SystemAssignedIdentityRole.DefinitionID).To(Equal("testroledefinitionid")) 480 g.Expect(systemAssignedIdentityRoleExistTest.amp.Spec.SystemAssignedIdentityRole.Scope).To(Equal("testscope")) 481 482 err = ampw.Default(context.Background(), emptyTest.amp) 483 g.Expect(err).NotTo(HaveOccurred()) 484 g.Expect(emptyTest.amp.Spec.SystemAssignedIdentityRole.Name).To(Not(BeEmpty())) 485 _, err = guuid.Parse(emptyTest.amp.Spec.SystemAssignedIdentityRole.Name) 486 g.Expect(err).To(Not(HaveOccurred())) 487 g.Expect(emptyTest.amp.Spec.SystemAssignedIdentityRole).To(Not(BeNil())) 488 g.Expect(emptyTest.amp.Spec.SystemAssignedIdentityRole.Scope).To(Equal(fmt.Sprintf("/subscriptions/%s/", fakeSubscriptionID))) 489 g.Expect(emptyTest.amp.Spec.SystemAssignedIdentityRole.DefinitionID).To(Equal(fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/%s", fakeSubscriptionID, infrav1.ContributorRoleID))) 490 } 491 492 func createMachinePoolWithMarketPlaceImage(publisher, offer, sku, version string, terminateNotificationTimeout *int) *AzureMachinePool { 493 image := infrav1.Image{ 494 Marketplace: &infrav1.AzureMarketplaceImage{ 495 ImagePlan: infrav1.ImagePlan{ 496 Publisher: publisher, 497 Offer: offer, 498 SKU: sku, 499 }, 500 Version: version, 501 }, 502 } 503 504 return &AzureMachinePool{ 505 Spec: AzureMachinePoolSpec{ 506 Template: AzureMachinePoolMachineTemplate{ 507 Image: &image, 508 SSHPublicKey: validSSHPublicKey, 509 TerminateNotificationTimeout: terminateNotificationTimeout, 510 OSDisk: infrav1.OSDisk{ 511 CachingType: "None", 512 OSType: "Linux", 513 }, 514 }, 515 }, 516 } 517 } 518 519 func createMachinePoolWithSharedImage(subscriptionID, resourceGroup, name, gallery, version string, terminateNotificationTimeout *int) *AzureMachinePool { 520 image := infrav1.Image{ 521 SharedGallery: &infrav1.AzureSharedGalleryImage{ 522 SubscriptionID: subscriptionID, 523 ResourceGroup: resourceGroup, 524 Name: name, 525 Gallery: gallery, 526 Version: version, 527 }, 528 } 529 530 return &AzureMachinePool{ 531 Spec: AzureMachinePoolSpec{ 532 Template: AzureMachinePoolMachineTemplate{ 533 Image: &image, 534 SSHPublicKey: validSSHPublicKey, 535 TerminateNotificationTimeout: terminateNotificationTimeout, 536 OSDisk: infrav1.OSDisk{ 537 CachingType: "None", 538 OSType: "Linux", 539 }, 540 }, 541 }, 542 } 543 } 544 545 func createMachinePoolWithNetworkConfig(subnetName string, interfaces []infrav1.NetworkInterface) *AzureMachinePool { 546 return &AzureMachinePool{ 547 Spec: AzureMachinePoolSpec{ 548 Template: AzureMachinePoolMachineTemplate{ 549 SubnetName: subnetName, 550 NetworkInterfaces: interfaces, 551 OSDisk: infrav1.OSDisk{ 552 CachingType: "None", 553 OSType: "Linux", 554 }, 555 }, 556 }, 557 } 558 } 559 560 func createMachinePoolWithImageByID(imageID string, terminateNotificationTimeout *int) *AzureMachinePool { 561 image := infrav1.Image{ 562 ID: &imageID, 563 } 564 565 return &AzureMachinePool{ 566 Spec: AzureMachinePoolSpec{ 567 Template: AzureMachinePoolMachineTemplate{ 568 Image: &image, 569 SSHPublicKey: validSSHPublicKey, 570 TerminateNotificationTimeout: terminateNotificationTimeout, 571 OSDisk: infrav1.OSDisk{ 572 CachingType: "None", 573 OSType: "Linux", 574 }, 575 }, 576 }, 577 } 578 } 579 580 func createMachinePoolWithSystemAssignedIdentity(role string) *AzureMachinePool { 581 return &AzureMachinePool{ 582 Spec: AzureMachinePoolSpec{ 583 Identity: infrav1.VMIdentitySystemAssigned, 584 SystemAssignedIdentityRole: &infrav1.SystemAssignedIdentityRole{ 585 Name: role, 586 Scope: "scope", 587 DefinitionID: "definitionID", 588 }, 589 Template: AzureMachinePoolMachineTemplate{ 590 OSDisk: infrav1.OSDisk{ 591 CachingType: "None", 592 OSType: "Linux", 593 }, 594 }, 595 }, 596 } 597 } 598 599 func createMachinePoolWithDiagnostics(diagnosticsType infrav1.BootDiagnosticsStorageAccountType, userManaged *infrav1.UserManagedBootDiagnostics) *AzureMachinePool { 600 var diagnostics *infrav1.Diagnostics 601 602 if diagnosticsType != "" { 603 diagnostics = &infrav1.Diagnostics{ 604 Boot: &infrav1.BootDiagnostics{ 605 StorageAccountType: diagnosticsType, 606 }, 607 } 608 } 609 610 if userManaged != nil { 611 diagnostics.Boot.UserManaged = userManaged 612 } 613 614 return &AzureMachinePool{ 615 Spec: AzureMachinePoolSpec{ 616 Template: AzureMachinePoolMachineTemplate{ 617 Diagnostics: diagnostics, 618 OSDisk: infrav1.OSDisk{ 619 CachingType: "None", 620 OSType: "Linux", 621 }, 622 }, 623 }, 624 } 625 } 626 627 func createMachinePoolWithUserAssignedIdentity(providerIds []string) *AzureMachinePool { 628 userAssignedIdentities := make([]infrav1.UserAssignedIdentity, len(providerIds)) 629 630 for _, providerID := range providerIds { 631 userAssignedIdentities = append(userAssignedIdentities, infrav1.UserAssignedIdentity{ 632 ProviderID: providerID, 633 }) 634 } 635 636 return &AzureMachinePool{ 637 Spec: AzureMachinePoolSpec{ 638 Identity: infrav1.VMIdentityUserAssigned, 639 UserAssignedIdentities: userAssignedIdentities, 640 Template: AzureMachinePoolMachineTemplate{ 641 OSDisk: infrav1.OSDisk{ 642 CachingType: "None", 643 OSType: "Linux", 644 }, 645 }, 646 }, 647 } 648 } 649 650 func generateSSHPublicKey(b64Enconded bool) string { 651 privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) 652 publicRsaKey, _ := ssh.NewPublicKey(&privateKey.PublicKey) 653 if b64Enconded { 654 return base64.StdEncoding.EncodeToString(ssh.MarshalAuthorizedKey(publicRsaKey)) 655 } 656 return string(ssh.MarshalAuthorizedKey(publicRsaKey)) 657 } 658 659 func createMachinePoolWithStrategy(strategy AzureMachinePoolDeploymentStrategy) *AzureMachinePool { 660 return &AzureMachinePool{ 661 Spec: AzureMachinePoolSpec{ 662 Strategy: strategy, 663 Template: AzureMachinePoolMachineTemplate{ 664 OSDisk: infrav1.OSDisk{ 665 CachingType: "None", 666 OSType: "Linux", 667 }, 668 }, 669 }, 670 } 671 } 672 673 func createMachinePoolWithOrchestrationMode(mode armcompute.OrchestrationMode) *AzureMachinePool { 674 return &AzureMachinePool{ 675 Spec: AzureMachinePoolSpec{ 676 OrchestrationMode: infrav1.OrchestrationModeType(mode), 677 Template: AzureMachinePoolMachineTemplate{ 678 OSDisk: infrav1.OSDisk{ 679 CachingType: "None", 680 OSType: "Linux", 681 }, 682 }, 683 }, 684 } 685 } 686 687 func createMachinePoolWithDiffDiskSettings(settings infrav1.DiffDiskSettings) *AzureMachinePool { 688 return &AzureMachinePool{ 689 Spec: AzureMachinePoolSpec{ 690 Template: AzureMachinePoolMachineTemplate{ 691 OSDisk: infrav1.OSDisk{ 692 DiffDiskSettings: &settings, 693 }, 694 }, 695 }, 696 } 697 } 698 699 func TestAzureMachinePool_ValidateCreateFailure(t *testing.T) { 700 g := NewWithT(t) 701 702 tests := []struct { 703 name string 704 amp *AzureMachinePool 705 featureGateEnabled *bool 706 expectError bool 707 }{ 708 { 709 name: "feature gate explicitly disabled", 710 amp: getKnownValidAzureMachinePool(), 711 featureGateEnabled: ptr.To(false), 712 expectError: true, 713 }, 714 { 715 name: "feature gate implicitly enabled", 716 amp: getKnownValidAzureMachinePool(), 717 featureGateEnabled: nil, 718 expectError: false, 719 }, 720 } 721 for _, tc := range tests { 722 t.Run(tc.name, func(t *testing.T) { 723 if tc.featureGateEnabled != nil { 724 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, capifeature.MachinePool, *tc.featureGateEnabled)() 725 } 726 ampw := &azureMachinePoolWebhook{} 727 _, err := ampw.ValidateCreate(context.Background(), tc.amp) 728 if tc.expectError { 729 g.Expect(err).To(HaveOccurred()) 730 } else { 731 g.Expect(err).NotTo(HaveOccurred()) 732 } 733 }) 734 } 735 } 736 737 func getKnownValidAzureMachinePool() *AzureMachinePool { 738 image := infrav1.Image{ 739 Marketplace: &infrav1.AzureMarketplaceImage{ 740 ImagePlan: infrav1.ImagePlan{ 741 Publisher: "PUB1234", 742 Offer: "OFFER1234", 743 SKU: "SKU1234", 744 }, 745 Version: "1.0.0", 746 }, 747 } 748 return &AzureMachinePool{ 749 Spec: AzureMachinePoolSpec{ 750 Template: AzureMachinePoolMachineTemplate{ 751 Image: &image, 752 SSHPublicKey: validSSHPublicKey, 753 TerminateNotificationTimeout: ptr.To(10), 754 OSDisk: infrav1.OSDisk{ 755 CachingType: "None", 756 OSType: "Linux", 757 }, 758 }, 759 Identity: infrav1.VMIdentitySystemAssigned, 760 SystemAssignedIdentityRole: &infrav1.SystemAssignedIdentityRole{ 761 Name: string(uuid.NewUUID()), 762 Scope: "scope", 763 DefinitionID: "definitionID", 764 }, 765 Strategy: AzureMachinePoolDeploymentStrategy{ 766 Type: RollingUpdateAzureMachinePoolDeploymentStrategyType, 767 RollingUpdate: &MachineRollingUpdateDeployment{ 768 MaxSurge: &zero, 769 MaxUnavailable: &one, 770 }, 771 }, 772 }, 773 } 774 }