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  }