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  }