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