sigs.k8s.io/cluster-api-provider-azure@v1.17.0/api/v1beta1/azuremachinetemplate_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 "testing" 22 23 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" 24 . "github.com/onsi/gomega" 25 admissionv1 "k8s.io/api/admission/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/utils/ptr" 28 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 29 ) 30 31 func TestAzureMachineTemplate_ValidateCreate(t *testing.T) { 32 tests := []struct { 33 name string 34 machineTemplate *AzureMachineTemplate 35 wantErr bool 36 }{ 37 { 38 name: "azuremachinetemplate with marketplane image - full", 39 machineTemplate: createAzureMachineTemplateFromMachine( 40 createMachineWithMarketPlaceImage("PUB1234", "OFFER1234", "SKU1234", "1.0.0"), 41 ), 42 wantErr: false, 43 }, 44 { 45 name: "azuremachinetemplate with marketplace image - missing publisher", 46 machineTemplate: createAzureMachineTemplateFromMachine( 47 createMachineWithMarketPlaceImage("", "OFFER1234", "SKU1234", "1.0.0"), 48 ), 49 wantErr: true, 50 }, 51 { 52 name: "azuremachinetemplate with shared gallery image - full", 53 machineTemplate: createAzureMachineTemplateFromMachine( 54 createMachineWithSharedImage("SUB123", "RG123", "NAME123", "GALLERY1", "1.0.0"), 55 ), 56 wantErr: false, 57 }, 58 { 59 name: "azuremachinetemplate with marketplace image - missing subscription", 60 machineTemplate: createAzureMachineTemplateFromMachine( 61 createMachineWithSharedImage("", "RG123", "NAME123", "GALLERY2", "1.0.0"), 62 ), 63 wantErr: true, 64 }, 65 { 66 name: "azuremachinetemplate with image by - with id", 67 machineTemplate: createAzureMachineTemplateFromMachine( 68 createMachineWithImageByID("ID123"), 69 ), 70 wantErr: false, 71 }, 72 { 73 name: "azuremachinetemplate with image by - without id", 74 machineTemplate: createAzureMachineTemplateFromMachine( 75 createMachineWithImageByID(""), 76 ), 77 wantErr: true, 78 }, 79 { 80 name: "azuremachinetemplate with valid SSHPublicKey", 81 machineTemplate: createAzureMachineTemplateFromMachine( 82 createMachineWithSSHPublicKey(validSSHPublicKey), 83 ), 84 wantErr: false, 85 }, 86 { 87 name: "azuremachinetemplate without SSHPublicKey", 88 machineTemplate: createAzureMachineTemplateFromMachine( 89 createMachineWithSSHPublicKey(""), 90 ), 91 wantErr: true, 92 }, 93 { 94 name: "azuremachinetemplate with invalid SSHPublicKey", 95 machineTemplate: createAzureMachineTemplateFromMachine( 96 createMachineWithSSHPublicKey("invalid ssh key"), 97 ), 98 wantErr: true, 99 }, 100 { 101 name: "azuremachinetemplate with list of user-assigned identities", 102 machineTemplate: createAzureMachineTemplateFromMachine( 103 createMachineWithUserAssignedIdentities([]UserAssignedIdentity{ 104 {ProviderID: "azure:///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group/providers/Microsoft.Compute/virtualMachines/default-09091-control-plane-f1b2c"}, 105 {ProviderID: "azure:///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group/providers/Microsoft.Compute/virtualMachines/default-09091-control-plane-9a8b7"}, 106 }), 107 ), 108 wantErr: false, 109 }, 110 { 111 name: "azuremachinetemplate with empty list of user-assigned identities", 112 machineTemplate: createAzureMachineTemplateFromMachine( 113 createMachineWithUserAssignedIdentities([]UserAssignedIdentity{}), 114 ), 115 wantErr: true, 116 }, 117 { 118 name: "azuremachinetemplate with valid osDisk cache type", 119 machineTemplate: createAzureMachineTemplateFromMachine( 120 createMachineWithOsDiskCacheType(string(armcompute.PossibleCachingTypesValues()[1])), 121 ), 122 wantErr: false, 123 }, 124 { 125 name: "azuremachinetemplate with invalid osDisk cache type", 126 machineTemplate: createAzureMachineTemplateFromMachine( 127 createMachineWithOsDiskCacheType("invalid_cache_type"), 128 ), 129 wantErr: true, 130 }, 131 { 132 name: "azuremachinetemplate with SystemAssignedIdentityRoleName", 133 machineTemplate: createAzureMachineTemplateFromMachine(createMachineWithSystemAssignedIdentityRoleName()), 134 wantErr: true, 135 }, 136 { 137 name: "azuremachinetemplate without SystemAssignedIdentityRoleName", 138 machineTemplate: createAzureMachineTemplateFromMachine(createMachineWithoutSystemAssignedIdentityRoleName()), 139 wantErr: false, 140 }, 141 { 142 name: "azuremachinetemplate with RoleAssignmentName", 143 machineTemplate: createAzureMachineTemplateFromMachine(createMachineWithRoleAssignmentName()), 144 wantErr: true, 145 }, 146 { 147 name: "azuremachinetemplate with DisableExtensionOperations true and without VMExtensions", 148 machineTemplate: createAzureMachineTemplateFromMachine(createMachineWithDisableExtenionOperations()), 149 wantErr: false, 150 }, 151 { 152 name: "azuremachinetempalte with DisableExtensionOperations true and with VMExtension", 153 machineTemplate: createAzureMachineTemplateFromMachine(createMachineWithDisableExtenionOperationsAndHasExtension()), 154 wantErr: true, 155 }, 156 { 157 name: "azuremachinetemplate without RoleAssignmentName", 158 machineTemplate: createAzureMachineTemplateFromMachine(createMachineWithoutRoleAssignmentName()), 159 wantErr: false, 160 }, 161 { 162 name: "azuremachinetemplate with network interfaces > 0 and subnet name", 163 machineTemplate: createAzureMachineTemplateFromMachine( 164 createMachineWithNetworkConfig( 165 "test-subnet", 166 nil, 167 []NetworkInterface{ 168 {SubnetName: "subnet1", PrivateIPConfigs: 1}, 169 {SubnetName: "subnet2", PrivateIPConfigs: 1}, 170 }, 171 ), 172 ), 173 wantErr: true, 174 }, 175 { 176 name: "azuremachinetemplate with network interfaces > 0 and no subnet name", 177 machineTemplate: createAzureMachineTemplateFromMachine( 178 createMachineWithNetworkConfig( 179 "", 180 nil, 181 []NetworkInterface{ 182 {SubnetName: "subnet1", PrivateIPConfigs: 1}, 183 {SubnetName: "subnet2", PrivateIPConfigs: 1}, 184 }, 185 ), 186 ), 187 wantErr: false, 188 }, 189 { 190 name: "azuremachinetemplate with network interfaces > 0 and AcceleratedNetworking not nil", 191 machineTemplate: createAzureMachineTemplateFromMachine( 192 createMachineWithNetworkConfig( 193 "", 194 ptr.To(true), 195 []NetworkInterface{ 196 {SubnetName: "subnet1", PrivateIPConfigs: 1}, 197 {SubnetName: "subnet2", PrivateIPConfigs: 1}, 198 }, 199 ), 200 ), 201 wantErr: true, 202 }, 203 { 204 name: "azuremachinetemplate with network interfaces > 0 and AcceleratedNetworking nil", 205 machineTemplate: createAzureMachineTemplateFromMachine( 206 createMachineWithNetworkConfig( 207 "", 208 nil, 209 []NetworkInterface{ 210 {SubnetName: "subnet1", PrivateIPConfigs: 1}, 211 {SubnetName: "subnet2", PrivateIPConfigs: 1}, 212 }, 213 ), 214 ), 215 wantErr: false, 216 }, 217 { 218 name: "azuremachinetemplate with network interfaces and PrivateIPConfigs < 1", 219 machineTemplate: createAzureMachineTemplateFromMachine( 220 createMachineWithNetworkConfig( 221 "", 222 nil, 223 []NetworkInterface{ 224 {SubnetName: "subnet1", PrivateIPConfigs: 0}, 225 {SubnetName: "subnet2", PrivateIPConfigs: -1}, 226 }, 227 ), 228 ), 229 wantErr: true, 230 }, 231 { 232 name: "azuremachinetemplate with network interfaces and PrivateIPConfigs >= 1", 233 machineTemplate: createAzureMachineTemplateFromMachine( 234 createMachineWithNetworkConfig( 235 "", 236 nil, 237 []NetworkInterface{ 238 {SubnetName: "subnet1", PrivateIPConfigs: 1}, 239 {SubnetName: "subnet2", PrivateIPConfigs: 2}, 240 }, 241 ), 242 ), 243 wantErr: false, 244 }, 245 } 246 247 for _, test := range tests { 248 test := test 249 t.Run(test.name, func(t *testing.T) { 250 t.Parallel() 251 g := NewWithT(t) 252 ctx := context.Background() 253 _, err := test.machineTemplate.ValidateCreate(ctx, test.machineTemplate) 254 if test.wantErr { 255 g.Expect(err).To(HaveOccurred()) 256 } else { 257 g.Expect(err).NotTo(HaveOccurred()) 258 } 259 }) 260 } 261 } 262 263 func TestAzureMachineTemplate_ValidateUpdate(t *testing.T) { 264 failureDomain := "domaintest" 265 266 tests := []struct { 267 name string 268 oldTemplate *AzureMachineTemplate 269 template *AzureMachineTemplate 270 wantErr bool 271 }{ 272 { 273 name: "AzureMachineTemplate with immutable spec", 274 oldTemplate: &AzureMachineTemplate{ 275 Spec: AzureMachineTemplateSpec{ 276 Template: AzureMachineTemplateResource{ 277 Spec: AzureMachineSpec{ 278 VMSize: "size", 279 FailureDomain: &failureDomain, 280 OSDisk: OSDisk{ 281 OSType: "type", 282 DiskSizeGB: ptr.To[int32](11), 283 }, 284 DataDisks: []DataDisk{}, 285 SSHPublicKey: "", 286 }, 287 }, 288 }, 289 }, 290 template: &AzureMachineTemplate{ 291 Spec: AzureMachineTemplateSpec{ 292 Template: AzureMachineTemplateResource{ 293 Spec: AzureMachineSpec{ 294 VMSize: "size1", 295 FailureDomain: &failureDomain, 296 OSDisk: OSDisk{ 297 OSType: "type", 298 DiskSizeGB: ptr.To[int32](11), 299 }, 300 DataDisks: []DataDisk{}, 301 SSHPublicKey: "fake ssh key", 302 }, 303 }, 304 }, 305 }, 306 wantErr: true, 307 }, 308 { 309 name: "AzureMachineTemplate with mutable metadata", 310 oldTemplate: &AzureMachineTemplate{ 311 Spec: AzureMachineTemplateSpec{ 312 Template: AzureMachineTemplateResource{ 313 Spec: AzureMachineSpec{ 314 VMSize: "size", 315 FailureDomain: &failureDomain, 316 OSDisk: OSDisk{ 317 OSType: "type", 318 DiskSizeGB: ptr.To[int32](11), 319 }, 320 DataDisks: []DataDisk{}, 321 SSHPublicKey: "fake ssh key", 322 }, 323 }, 324 }, 325 ObjectMeta: metav1.ObjectMeta{ 326 Name: "OldTemplate", 327 }, 328 }, 329 template: &AzureMachineTemplate{ 330 Spec: AzureMachineTemplateSpec{ 331 Template: AzureMachineTemplateResource{ 332 Spec: AzureMachineSpec{ 333 VMSize: "size", 334 FailureDomain: &failureDomain, 335 OSDisk: OSDisk{ 336 OSType: "type", 337 DiskSizeGB: ptr.To[int32](11), 338 }, 339 DataDisks: []DataDisk{}, 340 SSHPublicKey: "fake ssh key", 341 }, 342 }, 343 }, 344 ObjectMeta: metav1.ObjectMeta{ 345 Name: "NewTemplate", 346 }, 347 }, 348 wantErr: false, 349 }, 350 { 351 name: "AzureMachineTemplate with default mismatch", 352 oldTemplate: &AzureMachineTemplate{ 353 Spec: AzureMachineTemplateSpec{ 354 Template: AzureMachineTemplateResource{ 355 Spec: AzureMachineSpec{ 356 VMSize: "size", 357 FailureDomain: &failureDomain, 358 OSDisk: OSDisk{ 359 OSType: "type", 360 DiskSizeGB: ptr.To[int32](11), 361 CachingType: "", 362 }, 363 DataDisks: []DataDisk{}, 364 SSHPublicKey: "", 365 }, 366 }, 367 }, 368 ObjectMeta: metav1.ObjectMeta{ 369 Name: "OldTemplate", 370 }, 371 }, 372 template: &AzureMachineTemplate{ 373 Spec: AzureMachineTemplateSpec{ 374 Template: AzureMachineTemplateResource{ 375 Spec: AzureMachineSpec{ 376 VMSize: "size", 377 FailureDomain: &failureDomain, 378 OSDisk: OSDisk{ 379 OSType: "type", 380 DiskSizeGB: ptr.To[int32](11), 381 CachingType: "None", 382 }, 383 DataDisks: []DataDisk{}, 384 SSHPublicKey: "fake ssh key", 385 NetworkInterfaces: []NetworkInterface{{ 386 PrivateIPConfigs: 1, 387 }}, 388 }, 389 }, 390 }, 391 ObjectMeta: metav1.ObjectMeta{ 392 Name: "NewTemplate", 393 }, 394 }, 395 wantErr: false, 396 }, 397 { 398 name: "AzureMachineTemplate ssh key removed", 399 oldTemplate: &AzureMachineTemplate{ 400 Spec: AzureMachineTemplateSpec{ 401 Template: AzureMachineTemplateResource{ 402 Spec: AzureMachineSpec{ 403 VMSize: "size", 404 FailureDomain: &failureDomain, 405 OSDisk: OSDisk{ 406 OSType: "type", 407 DiskSizeGB: ptr.To[int32](11), 408 CachingType: "None", 409 }, 410 DataDisks: []DataDisk{}, 411 SSHPublicKey: "some key", 412 }, 413 }, 414 }, 415 ObjectMeta: metav1.ObjectMeta{ 416 Name: "OldTemplate", 417 }, 418 }, 419 template: &AzureMachineTemplate{ 420 Spec: AzureMachineTemplateSpec{ 421 Template: AzureMachineTemplateResource{ 422 Spec: AzureMachineSpec{ 423 VMSize: "size", 424 FailureDomain: &failureDomain, 425 OSDisk: OSDisk{ 426 OSType: "type", 427 DiskSizeGB: ptr.To[int32](11), 428 CachingType: "None", 429 }, 430 DataDisks: []DataDisk{}, 431 SSHPublicKey: "", 432 }, 433 }, 434 }, 435 ObjectMeta: metav1.ObjectMeta{ 436 Name: "NewTemplate", 437 }, 438 }, 439 wantErr: true, 440 }, 441 { 442 name: "AzureMachineTemplate with legacy subnetName updated to new networkInterfaces", 443 oldTemplate: &AzureMachineTemplate{ 444 Spec: AzureMachineTemplateSpec{ 445 Template: AzureMachineTemplateResource{ 446 Spec: AzureMachineSpec{ 447 VMSize: "size", 448 FailureDomain: &failureDomain, 449 OSDisk: OSDisk{ 450 OSType: "type", 451 DiskSizeGB: ptr.To[int32](11), 452 CachingType: "None", 453 }, 454 DataDisks: []DataDisk{}, 455 SSHPublicKey: "fake ssh key", 456 SubnetName: "subnet1", 457 AcceleratedNetworking: ptr.To(true), 458 }, 459 }, 460 }, 461 }, 462 template: &AzureMachineTemplate{ 463 Spec: AzureMachineTemplateSpec{ 464 Template: AzureMachineTemplateResource{ 465 Spec: AzureMachineSpec{ 466 VMSize: "size", 467 FailureDomain: &failureDomain, 468 OSDisk: OSDisk{ 469 OSType: "type", 470 DiskSizeGB: ptr.To[int32](11), 471 CachingType: "None", 472 }, 473 DataDisks: []DataDisk{}, 474 SSHPublicKey: "fake ssh key", 475 SubnetName: "", 476 AcceleratedNetworking: nil, 477 NetworkInterfaces: []NetworkInterface{ 478 { 479 SubnetName: "subnet1", 480 AcceleratedNetworking: ptr.To(true), 481 PrivateIPConfigs: 1, 482 }, 483 }, 484 }, 485 }, 486 }, 487 }, 488 wantErr: false, 489 }, 490 { 491 name: "AzureMachineTemplate with legacy AcceleratedNetworking updated to new networkInterfaces", 492 oldTemplate: &AzureMachineTemplate{ 493 Spec: AzureMachineTemplateSpec{ 494 Template: AzureMachineTemplateResource{ 495 Spec: AzureMachineSpec{ 496 VMSize: "size", 497 FailureDomain: &failureDomain, 498 OSDisk: OSDisk{ 499 OSType: "type", 500 DiskSizeGB: ptr.To[int32](11), 501 CachingType: "None", 502 }, 503 DataDisks: []DataDisk{}, 504 SSHPublicKey: "fake ssh key", 505 SubnetName: "", 506 AcceleratedNetworking: ptr.To(true), 507 NetworkInterfaces: []NetworkInterface{}, 508 }, 509 }, 510 }, 511 }, 512 template: &AzureMachineTemplate{ 513 Spec: AzureMachineTemplateSpec{ 514 Template: AzureMachineTemplateResource{ 515 Spec: AzureMachineSpec{ 516 VMSize: "size", 517 FailureDomain: &failureDomain, 518 OSDisk: OSDisk{ 519 OSType: "type", 520 DiskSizeGB: ptr.To[int32](11), 521 CachingType: "None", 522 }, 523 DataDisks: []DataDisk{}, 524 SSHPublicKey: "fake ssh key", 525 SubnetName: "", 526 AcceleratedNetworking: nil, 527 NetworkInterfaces: []NetworkInterface{ 528 { 529 SubnetName: "", 530 AcceleratedNetworking: ptr.To(true), 531 PrivateIPConfigs: 1, 532 }, 533 }, 534 }, 535 }, 536 }, 537 }, 538 wantErr: false, 539 }, 540 { 541 name: "AzureMachineTemplate with modified networkInterfaces is immutable", 542 oldTemplate: &AzureMachineTemplate{ 543 Spec: AzureMachineTemplateSpec{ 544 Template: AzureMachineTemplateResource{ 545 Spec: AzureMachineSpec{ 546 VMSize: "size", 547 FailureDomain: &failureDomain, 548 OSDisk: OSDisk{ 549 OSType: "type", 550 DiskSizeGB: ptr.To[int32](11), 551 CachingType: "None", 552 }, 553 DataDisks: []DataDisk{}, 554 SSHPublicKey: "fake ssh key", 555 NetworkInterfaces: []NetworkInterface{ 556 { 557 SubnetName: "subnet1", 558 AcceleratedNetworking: ptr.To(true), 559 PrivateIPConfigs: 1, 560 }, 561 }, 562 }, 563 }, 564 }, 565 }, 566 template: &AzureMachineTemplate{ 567 Spec: AzureMachineTemplateSpec{ 568 Template: AzureMachineTemplateResource{ 569 Spec: AzureMachineSpec{ 570 VMSize: "size", 571 FailureDomain: &failureDomain, 572 OSDisk: OSDisk{ 573 OSType: "type", 574 DiskSizeGB: ptr.To[int32](11), 575 CachingType: "None", 576 }, 577 DataDisks: []DataDisk{}, 578 SSHPublicKey: "fake ssh key", 579 NetworkInterfaces: []NetworkInterface{ 580 { 581 SubnetName: "subnet2", 582 AcceleratedNetworking: ptr.To(true), 583 PrivateIPConfigs: 1, 584 }, 585 }, 586 }, 587 }, 588 }, 589 }, 590 wantErr: true, 591 }, 592 } 593 594 // dry-run=true 595 for _, amt := range tests { 596 amt := amt 597 t.Run(amt.name, func(t *testing.T) { 598 g := NewWithT(t) 599 ctx := admission.NewContextWithRequest(context.Background(), admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{DryRun: ptr.To(true)}}) 600 _, err := amt.template.ValidateUpdate(ctx, amt.oldTemplate, amt.template) 601 if amt.wantErr { 602 g.Expect(err).To(HaveOccurred()) 603 } else { 604 g.Expect(err).NotTo(HaveOccurred()) 605 } 606 }) 607 } 608 // dry-run=false 609 for _, amt := range tests { 610 amt := amt 611 t.Run(amt.name, func(t *testing.T) { 612 t.Parallel() 613 g := NewWithT(t) 614 ctx := admission.NewContextWithRequest(context.Background(), admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{DryRun: ptr.To(false)}}) 615 _, err := amt.template.ValidateUpdate(ctx, amt.oldTemplate, amt.template) 616 if amt.wantErr { 617 g.Expect(err).To(HaveOccurred()) 618 } else { 619 g.Expect(err).NotTo(HaveOccurred()) 620 } 621 }) 622 } 623 } 624 625 func createAzureMachineTemplateFromMachine(machine *AzureMachine) *AzureMachineTemplate { 626 return &AzureMachineTemplate{ 627 Spec: AzureMachineTemplateSpec{ 628 Template: AzureMachineTemplateResource{ 629 Spec: machine.Spec, 630 }, 631 }, 632 } 633 }