sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/scope/machinepool_test.go (about)

     1  /*
     2  Copyright 2020 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 scope
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"reflect"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
    27  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
    28  	azureautorest "github.com/Azure/go-autorest/autorest/azure"
    29  	"github.com/Azure/go-autorest/autorest/azure/auth"
    30  	. "github.com/onsi/gomega"
    31  	"go.uber.org/mock/gomock"
    32  	corev1 "k8s.io/api/core/v1"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/util/intstr"
    36  	"k8s.io/utils/ptr"
    37  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    38  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    39  	"sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure"
    40  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus"
    41  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/roleassignments"
    42  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/scalesets"
    43  	infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1beta1"
    44  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    45  	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
    46  	"sigs.k8s.io/controller-runtime/pkg/client"
    47  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    48  )
    49  
    50  func TestMachinePoolScope_Name(t *testing.T) {
    51  	tests := []struct {
    52  		name             string
    53  		machinePoolScope MachinePoolScope
    54  		want             string
    55  		testLength       bool
    56  	}{
    57  		{
    58  			name: "linux can be any length",
    59  			machinePoolScope: MachinePoolScope{
    60  				MachinePool: nil,
    61  				AzureMachinePool: &infrav1exp.AzureMachinePool{
    62  					ObjectMeta: metav1.ObjectMeta{
    63  						Name: "some-really-really-long-name",
    64  					},
    65  				},
    66  				ClusterScoper: nil,
    67  			},
    68  			want: "some-really-really-long-name",
    69  		},
    70  		{
    71  			name: "windows longer than 9 should be shortened",
    72  			machinePoolScope: MachinePoolScope{
    73  				MachinePool: nil,
    74  				AzureMachinePool: &infrav1exp.AzureMachinePool{
    75  					ObjectMeta: metav1.ObjectMeta{
    76  						Name: "machine-90123456",
    77  					},
    78  					Spec: infrav1exp.AzureMachinePoolSpec{
    79  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
    80  							OSDisk: infrav1.OSDisk{
    81  								OSType: "Windows",
    82  							},
    83  						},
    84  					},
    85  				},
    86  				ClusterScoper: nil,
    87  			},
    88  			want: "win-23456",
    89  		},
    90  	}
    91  
    92  	for _, tt := range tests {
    93  		t.Run(tt.name, func(t *testing.T) {
    94  			got := tt.machinePoolScope.Name()
    95  			if got != tt.want {
    96  				t.Errorf("MachinePoolScope.Name() = %v, want %v", got, tt.want)
    97  			}
    98  
    99  			if tt.testLength && len(got) > 9 {
   100  				t.Errorf("Length of MachinePoolScope.Name() = %v, want less than %v", len(got), 9)
   101  			}
   102  		})
   103  	}
   104  }
   105  
   106  func TestMachinePoolScope_ProviderID(t *testing.T) {
   107  	tests := []struct {
   108  		name             string
   109  		machinePoolScope MachinePoolScope
   110  		want             string
   111  	}{
   112  		{
   113  			name: "valid providerID",
   114  			machinePoolScope: MachinePoolScope{
   115  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   116  					Spec: infrav1exp.AzureMachinePoolSpec{
   117  						ProviderID: "azure:///subscriptions/1234/resourcegroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/cloud-provider-user-identity",
   118  					},
   119  				},
   120  			},
   121  			want: "cloud-provider-user-identity",
   122  		},
   123  		{
   124  			name: "valid providerID: VMSS Flex instance",
   125  			machinePoolScope: MachinePoolScope{
   126  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   127  					Spec: infrav1exp.AzureMachinePoolSpec{
   128  						ProviderID: "azure:///subscriptions/1234/resourceGroups/my-cluster/providers/Microsoft.Compute/virtualMachines/machine-0",
   129  					},
   130  				},
   131  			},
   132  			want: "machine-0",
   133  		},
   134  		{
   135  			name: "valid providerID: VMSS Uniform instance",
   136  			machinePoolScope: MachinePoolScope{
   137  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   138  					Spec: infrav1exp.AzureMachinePoolSpec{
   139  						ProviderID: "azure:///subscriptions/1234/resourceGroups/my-cluster/providers/Microsoft.Compute/virtualMachineScaleSets/my-cluster-mp-0/virtualMachines/0",
   140  					},
   141  				},
   142  			},
   143  			want: "0",
   144  		},
   145  		{
   146  			name: "invalid providerID: no cloud provider",
   147  			machinePoolScope: MachinePoolScope{
   148  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   149  					Spec: infrav1exp.AzureMachinePoolSpec{
   150  						ProviderID: "subscriptions/123/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm",
   151  					},
   152  				},
   153  			},
   154  			want: "",
   155  		},
   156  		{
   157  			name: "invalid providerID: incomplete URL",
   158  			machinePoolScope: MachinePoolScope{
   159  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   160  					Spec: infrav1exp.AzureMachinePoolSpec{
   161  						ProviderID: "azure:///",
   162  					},
   163  				},
   164  			},
   165  			want: "",
   166  		},
   167  	}
   168  
   169  	for _, tt := range tests {
   170  		t.Run(tt.name, func(t *testing.T) {
   171  			got := tt.machinePoolScope.ProviderID()
   172  			if got != tt.want {
   173  				t.Errorf("MachinePoolScope.ProviderID() = %v, want %v", got, tt.want)
   174  			}
   175  		})
   176  	}
   177  }
   178  
   179  func TestMachinePoolScope_NetworkInterfaces(t *testing.T) {
   180  	tests := []struct {
   181  		name             string
   182  		machinePoolScope MachinePoolScope
   183  		want             int
   184  	}{
   185  		{
   186  			name: "zero network interfaces",
   187  			machinePoolScope: MachinePoolScope{
   188  				MachinePool: nil,
   189  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   190  					ObjectMeta: metav1.ObjectMeta{
   191  						Name: "default-nics",
   192  					},
   193  					Spec: infrav1exp.AzureMachinePoolSpec{
   194  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
   195  							AcceleratedNetworking: ptr.To(true),
   196  							SubnetName:            "node-subnet",
   197  						},
   198  					},
   199  				},
   200  				ClusterScoper: nil,
   201  			},
   202  			want: 0,
   203  		},
   204  		{
   205  			name: "one network interface",
   206  			machinePoolScope: MachinePoolScope{
   207  				MachinePool: nil,
   208  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   209  					ObjectMeta: metav1.ObjectMeta{
   210  						Name: "single-nic",
   211  					},
   212  					Spec: infrav1exp.AzureMachinePoolSpec{
   213  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
   214  							NetworkInterfaces: []infrav1.NetworkInterface{
   215  								{
   216  									SubnetName: "node-subnet",
   217  								},
   218  							},
   219  						},
   220  					},
   221  				},
   222  				ClusterScoper: nil,
   223  			},
   224  			want: 1,
   225  		},
   226  		{
   227  			name: "two network interfaces",
   228  			machinePoolScope: MachinePoolScope{
   229  				MachinePool: nil,
   230  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   231  					ObjectMeta: metav1.ObjectMeta{
   232  						Name: "dual-nics",
   233  					},
   234  					Spec: infrav1exp.AzureMachinePoolSpec{
   235  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
   236  							NetworkInterfaces: []infrav1.NetworkInterface{
   237  								{
   238  									SubnetName: "control-plane-subnet",
   239  								},
   240  								{
   241  									SubnetName: "node-subnet",
   242  								},
   243  							},
   244  						},
   245  					},
   246  				},
   247  				ClusterScoper: nil,
   248  			},
   249  			want: 2,
   250  		},
   251  	}
   252  
   253  	for _, tt := range tests {
   254  		t.Run(tt.name, func(t *testing.T) {
   255  			got := len(tt.machinePoolScope.AzureMachinePool.Spec.Template.NetworkInterfaces)
   256  			if got != tt.want {
   257  				t.Errorf("MachinePoolScope.Name() = %v, want %v", got, tt.want)
   258  			}
   259  		})
   260  	}
   261  }
   262  
   263  func TestMachinePoolScope_MaxSurge(t *testing.T) {
   264  	cases := []struct {
   265  		Name   string
   266  		Setup  func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool)
   267  		Verify func(g *WithT, surge int, err error)
   268  	}{
   269  		{
   270  			Name: "default surge should be 1 if no deployment strategy is set",
   271  			Verify: func(g *WithT, surge int, err error) {
   272  				g.Expect(surge).To(Equal(1))
   273  				g.Expect(err).NotTo(HaveOccurred())
   274  			},
   275  		},
   276  		{
   277  			Name: "default surge should be 1 regardless of replica count with no surger",
   278  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) {
   279  				mp.Spec.Replicas = ptr.To[int32](3)
   280  			},
   281  			Verify: func(g *WithT, surge int, err error) {
   282  				g.Expect(surge).To(Equal(1))
   283  				g.Expect(err).NotTo(HaveOccurred())
   284  			},
   285  		},
   286  		{
   287  			Name: "default surge should be 2 as specified by the surger",
   288  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) {
   289  				mp.Spec.Replicas = ptr.To[int32](3)
   290  				two := intstr.FromInt(2)
   291  				amp.Spec.Strategy = infrav1exp.AzureMachinePoolDeploymentStrategy{
   292  					Type: infrav1exp.RollingUpdateAzureMachinePoolDeploymentStrategyType,
   293  					RollingUpdate: &infrav1exp.MachineRollingUpdateDeployment{
   294  						MaxSurge: &two,
   295  					},
   296  				}
   297  			},
   298  			Verify: func(g *WithT, surge int, err error) {
   299  				g.Expect(surge).To(Equal(2))
   300  				g.Expect(err).NotTo(HaveOccurred())
   301  			},
   302  		},
   303  		{
   304  			Name: "default surge should be 2 (50%) of the desired replicas",
   305  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) {
   306  				mp.Spec.Replicas = ptr.To[int32](4)
   307  				fiftyPercent := intstr.FromString("50%")
   308  				amp.Spec.Strategy = infrav1exp.AzureMachinePoolDeploymentStrategy{
   309  					Type: infrav1exp.RollingUpdateAzureMachinePoolDeploymentStrategyType,
   310  					RollingUpdate: &infrav1exp.MachineRollingUpdateDeployment{
   311  						MaxSurge: &fiftyPercent,
   312  					},
   313  				}
   314  			},
   315  			Verify: func(g *WithT, surge int, err error) {
   316  				g.Expect(surge).To(Equal(2))
   317  				g.Expect(err).NotTo(HaveOccurred())
   318  			},
   319  		},
   320  	}
   321  
   322  	for _, c := range cases {
   323  		t.Run(c.Name, func(t *testing.T) {
   324  			var (
   325  				g        = NewWithT(t)
   326  				mockCtrl = gomock.NewController(t)
   327  				amp      = &infrav1exp.AzureMachinePool{
   328  					ObjectMeta: metav1.ObjectMeta{
   329  						Name:      "amp1",
   330  						Namespace: "default",
   331  						OwnerReferences: []metav1.OwnerReference{
   332  							{
   333  								Name:       "mp1",
   334  								Kind:       "MachinePool",
   335  								APIVersion: expv1.GroupVersion.String(),
   336  							},
   337  						},
   338  					},
   339  				}
   340  				mp = &expv1.MachinePool{
   341  					ObjectMeta: metav1.ObjectMeta{
   342  						Name:      "mp1",
   343  						Namespace: "default",
   344  					},
   345  				}
   346  			)
   347  			defer mockCtrl.Finish()
   348  
   349  			if c.Setup != nil {
   350  				c.Setup(mp, amp)
   351  			}
   352  
   353  			s := &MachinePoolScope{
   354  				MachinePool:      mp,
   355  				AzureMachinePool: amp,
   356  			}
   357  			surge, err := s.MaxSurge()
   358  			c.Verify(g, surge, err)
   359  		})
   360  	}
   361  }
   362  
   363  func TestMachinePoolScope_SaveVMImageToStatus(t *testing.T) {
   364  	var (
   365  		g        = NewWithT(t)
   366  		mockCtrl = gomock.NewController(t)
   367  		amp      = &infrav1exp.AzureMachinePool{
   368  			ObjectMeta: metav1.ObjectMeta{
   369  				Name:      "amp1",
   370  				Namespace: "default",
   371  				OwnerReferences: []metav1.OwnerReference{
   372  					{
   373  						Name:       "mp1",
   374  						Kind:       "MachinePool",
   375  						APIVersion: expv1.GroupVersion.String(),
   376  					},
   377  				},
   378  			},
   379  		}
   380  		s = &MachinePoolScope{
   381  			AzureMachinePool: amp,
   382  		}
   383  		image = &infrav1.Image{
   384  			Marketplace: &infrav1.AzureMarketplaceImage{
   385  				ImagePlan: infrav1.ImagePlan{
   386  					Publisher: "cncf-upstream",
   387  					Offer:     "capi",
   388  					SKU:       "k8s-1dot19dot11-ubuntu-1804",
   389  				},
   390  				Version:         "latest",
   391  				ThirdPartyImage: false,
   392  			},
   393  		}
   394  	)
   395  	defer mockCtrl.Finish()
   396  
   397  	s.SaveVMImageToStatus(image)
   398  	g.Expect(s.AzureMachinePool.Status.Image).To(Equal(image))
   399  }
   400  
   401  func TestMachinePoolScope_GetVMImage(t *testing.T) {
   402  	mockCtrl := gomock.NewController(t)
   403  	defer mockCtrl.Finish()
   404  
   405  	clusterMock := mock_azure.NewMockClusterScoper(mockCtrl)
   406  	clusterMock.EXPECT().Location().AnyTimes()
   407  	clusterMock.EXPECT().SubscriptionID().AnyTimes()
   408  	clusterMock.EXPECT().CloudEnvironment().AnyTimes()
   409  	clusterMock.EXPECT().Token().Return(&azidentity.DefaultAzureCredential{}).AnyTimes()
   410  	cases := []struct {
   411  		Name   string
   412  		Setup  func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool)
   413  		Verify func(g *WithT, amp *infrav1exp.AzureMachinePool, vmImage *infrav1.Image, err error)
   414  	}{
   415  		{
   416  			Name: "should set and default the image if no image is specified for the AzureMachinePool",
   417  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) {
   418  				mp.Spec.Template.Spec.Version = ptr.To("v1.19.11")
   419  			},
   420  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, vmImage *infrav1.Image, err error) {
   421  				g.Expect(err).NotTo(HaveOccurred())
   422  				image := &infrav1.Image{
   423  					Marketplace: &infrav1.AzureMarketplaceImage{
   424  						ImagePlan: infrav1.ImagePlan{
   425  							Publisher: "cncf-upstream",
   426  							Offer:     "capi",
   427  							SKU:       "k8s-1dot19dot11-ubuntu-1804",
   428  						},
   429  						Version:         "latest",
   430  						ThirdPartyImage: false,
   431  					},
   432  				}
   433  				g.Expect(vmImage).To(Equal(image))
   434  				g.Expect(amp.Spec.Template.Image).To(BeNil())
   435  			},
   436  		},
   437  		{
   438  			Name: "should not default or set the image on the AzureMachinePool if it already exists",
   439  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) {
   440  				mp.Spec.Template.Spec.Version = ptr.To("v1.19.11")
   441  				amp.Spec.Template.Image = &infrav1.Image{
   442  					Marketplace: &infrav1.AzureMarketplaceImage{
   443  						ImagePlan: infrav1.ImagePlan{
   444  							Publisher: "cncf-upstream",
   445  							Offer:     "capi",
   446  							SKU:       "k8s-1dot19dot19-ubuntu-1804",
   447  						},
   448  						Version:         "latest",
   449  						ThirdPartyImage: false,
   450  					},
   451  				}
   452  			},
   453  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, vmImage *infrav1.Image, err error) {
   454  				g.Expect(err).NotTo(HaveOccurred())
   455  				image := &infrav1.Image{
   456  					Marketplace: &infrav1.AzureMarketplaceImage{
   457  						ImagePlan: infrav1.ImagePlan{
   458  							Publisher: "cncf-upstream",
   459  							Offer:     "capi",
   460  							SKU:       "k8s-1dot19dot19-ubuntu-1804",
   461  						},
   462  						Version:         "latest",
   463  						ThirdPartyImage: false,
   464  					},
   465  				}
   466  				g.Expect(vmImage).To(Equal(image))
   467  				g.Expect(amp.Spec.Template.Image).To(Equal(image))
   468  			},
   469  		},
   470  	}
   471  
   472  	for _, c := range cases {
   473  		t.Run(c.Name, func(t *testing.T) {
   474  			var (
   475  				g        = NewWithT(t)
   476  				mockCtrl = gomock.NewController(t)
   477  				amp      = &infrav1exp.AzureMachinePool{
   478  					ObjectMeta: metav1.ObjectMeta{
   479  						Name:      "amp1",
   480  						Namespace: "default",
   481  						OwnerReferences: []metav1.OwnerReference{
   482  							{
   483  								Name:       "mp1",
   484  								Kind:       "MachinePool",
   485  								APIVersion: expv1.GroupVersion.String(),
   486  							},
   487  						},
   488  					},
   489  				}
   490  				mp = &expv1.MachinePool{
   491  					ObjectMeta: metav1.ObjectMeta{
   492  						Name:      "mp1",
   493  						Namespace: "default",
   494  					},
   495  				}
   496  			)
   497  			defer mockCtrl.Finish()
   498  
   499  			if c.Setup != nil {
   500  				c.Setup(mp, amp)
   501  			}
   502  
   503  			s := &MachinePoolScope{
   504  				MachinePool:      mp,
   505  				AzureMachinePool: amp,
   506  				ClusterScoper:    clusterMock,
   507  			}
   508  			image, err := s.GetVMImage(context.TODO())
   509  			c.Verify(g, amp, image, err)
   510  		})
   511  	}
   512  }
   513  
   514  func TestMachinePoolScope_NeedsRequeue(t *testing.T) {
   515  	cases := []struct {
   516  		Name   string
   517  		Setup  func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS)
   518  		Verify func(g *WithT, requeue bool)
   519  	}{
   520  		{
   521  			Name: "should requeue if the machine is not in succeeded state",
   522  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) {
   523  				creating := infrav1.Creating
   524  				mp.Spec.Replicas = ptr.To[int32](0)
   525  				amp.Status.ProvisioningState = &creating
   526  			},
   527  			Verify: func(g *WithT, requeue bool) {
   528  				g.Expect(requeue).To(BeTrue())
   529  			},
   530  		},
   531  		{
   532  			Name: "should not requeue if the machine is in succeeded state",
   533  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) {
   534  				succeeded := infrav1.Succeeded
   535  				mp.Spec.Replicas = ptr.To[int32](0)
   536  				amp.Status.ProvisioningState = &succeeded
   537  			},
   538  			Verify: func(g *WithT, requeue bool) {
   539  				g.Expect(requeue).To(BeFalse())
   540  			},
   541  		},
   542  		{
   543  			Name: "should requeue if the machine is in succeeded state but desired replica count does not match",
   544  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) {
   545  				succeeded := infrav1.Succeeded
   546  				mp.Spec.Replicas = ptr.To[int32](1)
   547  				amp.Status.ProvisioningState = &succeeded
   548  			},
   549  			Verify: func(g *WithT, requeue bool) {
   550  				g.Expect(requeue).To(BeTrue())
   551  			},
   552  		},
   553  		{
   554  			Name: "should not requeue if the machine is in succeeded state but desired replica count does match",
   555  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) {
   556  				succeeded := infrav1.Succeeded
   557  				mp.Spec.Replicas = ptr.To[int32](1)
   558  				amp.Status.ProvisioningState = &succeeded
   559  				vmss.Instances = []azure.VMSSVM{
   560  					{
   561  						Name: "instance1",
   562  					},
   563  				}
   564  			},
   565  			Verify: func(g *WithT, requeue bool) {
   566  				g.Expect(requeue).To(BeFalse())
   567  			},
   568  		},
   569  		{
   570  			Name: "should requeue if an instance VM image does not match the VM image of the VMSS",
   571  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmss *azure.VMSS) {
   572  				succeeded := infrav1.Succeeded
   573  				mp.Spec.Replicas = ptr.To[int32](1)
   574  				amp.Status.ProvisioningState = &succeeded
   575  				vmss.Instances = []azure.VMSSVM{
   576  					{
   577  						Name: "instance1",
   578  						Image: infrav1.Image{
   579  							Marketplace: &infrav1.AzureMarketplaceImage{
   580  								Version: "foo1",
   581  							},
   582  						},
   583  					},
   584  				}
   585  			},
   586  			Verify: func(g *WithT, requeue bool) {
   587  				g.Expect(requeue).To(BeTrue())
   588  			},
   589  		},
   590  	}
   591  
   592  	for _, c := range cases {
   593  		t.Run(c.Name, func(t *testing.T) {
   594  			var (
   595  				g        = NewWithT(t)
   596  				mockCtrl = gomock.NewController(t)
   597  				amp      = &infrav1exp.AzureMachinePool{
   598  					ObjectMeta: metav1.ObjectMeta{
   599  						Name:      "amp1",
   600  						Namespace: "default",
   601  						OwnerReferences: []metav1.OwnerReference{
   602  							{
   603  								Name:       "mp1",
   604  								Kind:       "MachinePool",
   605  								APIVersion: expv1.GroupVersion.String(),
   606  							},
   607  						},
   608  					},
   609  				}
   610  				mp = &expv1.MachinePool{
   611  					ObjectMeta: metav1.ObjectMeta{
   612  						Name:      "mp1",
   613  						Namespace: "default",
   614  					},
   615  				}
   616  				vmssState = &azure.VMSS{}
   617  			)
   618  			defer mockCtrl.Finish()
   619  
   620  			if c.Setup != nil {
   621  				c.Setup(mp, amp, vmssState)
   622  			}
   623  
   624  			s := &MachinePoolScope{
   625  				vmssState:        vmssState,
   626  				MachinePool:      mp,
   627  				AzureMachinePool: amp,
   628  			}
   629  			c.Verify(g, s.NeedsRequeue())
   630  		})
   631  	}
   632  }
   633  
   634  func TestMachinePoolScope_updateReplicasAndProviderIDs(t *testing.T) {
   635  	scheme := runtime.NewScheme()
   636  	_ = clusterv1.AddToScheme(scheme)
   637  	_ = infrav1exp.AddToScheme(scheme)
   638  	_ = expv1.AddToScheme(scheme)
   639  
   640  	cases := []struct {
   641  		Name   string
   642  		Setup  func(cb *fake.ClientBuilder)
   643  		Verify func(g *WithT, amp *infrav1exp.AzureMachinePool, err error)
   644  	}{
   645  		{
   646  			Name: "if there are three ready machines with matching labels, then should count them",
   647  			Setup: func(cb *fake.ClientBuilder) {
   648  				for _, machine := range getReadyAzureMachinePoolMachines(3) {
   649  					obj := machine
   650  					cb.WithObjects(&obj)
   651  				}
   652  			},
   653  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, err error) {
   654  				g.Expect(err).NotTo(HaveOccurred())
   655  				g.Expect(amp.Status.Replicas).To(BeEquivalentTo(3))
   656  				g.Expect(amp.Spec.ProviderIDList).To(ConsistOf("azure://foo/ampm0", "azure://foo/ampm1", "azure://foo/ampm2"))
   657  			},
   658  		},
   659  		{
   660  			Name: "should only count machines with matching machine pool label",
   661  			Setup: func(cb *fake.ClientBuilder) {
   662  				machines := getReadyAzureMachinePoolMachines(3)
   663  				machines[0].Labels[infrav1exp.MachinePoolNameLabel] = "not_correct"
   664  				for _, machine := range machines {
   665  					obj := machine
   666  					cb.WithObjects(&obj)
   667  				}
   668  			},
   669  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, err error) {
   670  				g.Expect(err).NotTo(HaveOccurred())
   671  				g.Expect(amp.Status.Replicas).To(BeEquivalentTo(2))
   672  			},
   673  		},
   674  		{
   675  			Name: "should only count machines with matching cluster name label",
   676  			Setup: func(cb *fake.ClientBuilder) {
   677  				machines := getReadyAzureMachinePoolMachines(3)
   678  				machines[0].Labels[clusterv1.ClusterNameLabel] = "not_correct"
   679  				for _, machine := range machines {
   680  					obj := machine
   681  					cb.WithObjects(&obj)
   682  				}
   683  			},
   684  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, err error) {
   685  				g.Expect(err).NotTo(HaveOccurred())
   686  				g.Expect(amp.Status.Replicas).To(BeEquivalentTo(2))
   687  			},
   688  		},
   689  	}
   690  
   691  	for _, c := range cases {
   692  		t.Run(c.Name, func(t *testing.T) {
   693  			var (
   694  				g        = NewWithT(t)
   695  				mockCtrl = gomock.NewController(t)
   696  				cb       = fake.NewClientBuilder().WithScheme(scheme)
   697  				cluster  = &clusterv1.Cluster{
   698  					ObjectMeta: metav1.ObjectMeta{
   699  						Name:      "cluster1",
   700  						Namespace: "default",
   701  					},
   702  					Spec: clusterv1.ClusterSpec{
   703  						InfrastructureRef: &corev1.ObjectReference{
   704  							Name: "azCluster1",
   705  						},
   706  					},
   707  					Status: clusterv1.ClusterStatus{
   708  						InfrastructureReady: true,
   709  					},
   710  				}
   711  				mp = &expv1.MachinePool{
   712  					ObjectMeta: metav1.ObjectMeta{
   713  						Name:      "mp1",
   714  						Namespace: "default",
   715  					},
   716  				}
   717  				amp = &infrav1exp.AzureMachinePool{
   718  					ObjectMeta: metav1.ObjectMeta{
   719  						Name:      "amp1",
   720  						Namespace: "default",
   721  						OwnerReferences: []metav1.OwnerReference{
   722  							{
   723  								Name:       "mp1",
   724  								Kind:       "MachinePool",
   725  								APIVersion: expv1.GroupVersion.String(),
   726  							},
   727  						},
   728  					},
   729  				}
   730  			)
   731  			defer mockCtrl.Finish()
   732  
   733  			c.Setup(cb.WithObjects(amp, cluster))
   734  			s := &MachinePoolScope{
   735  				client: cb.Build(),
   736  				ClusterScoper: &ClusterScope{
   737  					Cluster: cluster,
   738  				},
   739  				AzureMachinePool: amp,
   740  				MachinePool:      mp,
   741  			}
   742  			err := s.updateReplicasAndProviderIDs(context.TODO())
   743  			c.Verify(g, s.AzureMachinePool, err)
   744  		})
   745  	}
   746  }
   747  
   748  func TestMachinePoolScope_RoleAssignmentSpecs(t *testing.T) {
   749  	tests := []struct {
   750  		name             string
   751  		machinePoolScope MachinePoolScope
   752  		want             []azure.ResourceSpecGetter
   753  	}{
   754  		{
   755  			name: "returns empty if VM identity is not system assigned",
   756  			machinePoolScope: MachinePoolScope{
   757  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   758  					ObjectMeta: metav1.ObjectMeta{
   759  						Name: "machine-name",
   760  					},
   761  				},
   762  			},
   763  			want: []azure.ResourceSpecGetter{},
   764  		},
   765  		{
   766  			name: "returns role assignment spec if VM identity is system assigned",
   767  			machinePoolScope: MachinePoolScope{
   768  				MachinePool: &expv1.MachinePool{},
   769  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   770  					ObjectMeta: metav1.ObjectMeta{
   771  						Name: "machine-name",
   772  					},
   773  					Spec: infrav1exp.AzureMachinePoolSpec{
   774  						Identity: infrav1.VMIdentitySystemAssigned,
   775  						SystemAssignedIdentityRole: &infrav1.SystemAssignedIdentityRole{
   776  							Name: "role-assignment-name",
   777  						},
   778  					},
   779  				},
   780  				ClusterScoper: &ClusterScope{
   781  					AzureClients: AzureClients{
   782  						EnvironmentSettings: auth.EnvironmentSettings{
   783  							Values: map[string]string{
   784  								auth.SubscriptionID: "123",
   785  							},
   786  						},
   787  					},
   788  					AzureCluster: &infrav1.AzureCluster{
   789  						Spec: infrav1.AzureClusterSpec{
   790  							ResourceGroup: "my-rg",
   791  							AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
   792  								Location: "westus",
   793  							},
   794  						},
   795  					},
   796  				},
   797  			},
   798  			want: []azure.ResourceSpecGetter{
   799  				&roleassignments.RoleAssignmentSpec{
   800  					ResourceType:  azure.VirtualMachineScaleSet,
   801  					MachineName:   "machine-name",
   802  					Name:          "role-assignment-name",
   803  					ResourceGroup: "my-rg",
   804  					PrincipalID:   ptr.To("fakePrincipalID"),
   805  					PrincipalType: armauthorization.PrincipalTypeServicePrincipal,
   806  				},
   807  			},
   808  		},
   809  		{
   810  			name: "returns role assignment spec if scope and role definition ID are set",
   811  			machinePoolScope: MachinePoolScope{
   812  				MachinePool: &expv1.MachinePool{},
   813  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   814  					ObjectMeta: metav1.ObjectMeta{
   815  						Name: "machine-name",
   816  					},
   817  					Spec: infrav1exp.AzureMachinePoolSpec{
   818  						Identity: infrav1.VMIdentitySystemAssigned,
   819  						SystemAssignedIdentityRole: &infrav1.SystemAssignedIdentityRole{
   820  							Name:         "role-assignment-name",
   821  							Scope:        "scope",
   822  							DefinitionID: "role-definition-id",
   823  						},
   824  					},
   825  				},
   826  				ClusterScoper: &ClusterScope{
   827  					AzureClients: AzureClients{
   828  						EnvironmentSettings: auth.EnvironmentSettings{
   829  							Values: map[string]string{
   830  								auth.SubscriptionID: "123",
   831  							},
   832  						},
   833  					},
   834  					AzureCluster: &infrav1.AzureCluster{
   835  						Spec: infrav1.AzureClusterSpec{
   836  							ResourceGroup: "my-rg",
   837  							AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
   838  								Location: "westus",
   839  							},
   840  						},
   841  					},
   842  				},
   843  			},
   844  			want: []azure.ResourceSpecGetter{
   845  				&roleassignments.RoleAssignmentSpec{
   846  					ResourceType:     azure.VirtualMachineScaleSet,
   847  					MachineName:      "machine-name",
   848  					Name:             "role-assignment-name",
   849  					ResourceGroup:    "my-rg",
   850  					Scope:            "scope",
   851  					RoleDefinitionID: "role-definition-id",
   852  					PrincipalID:      ptr.To("fakePrincipalID"),
   853  					PrincipalType:    armauthorization.PrincipalTypeServicePrincipal,
   854  				},
   855  			},
   856  		},
   857  	}
   858  	for _, tt := range tests {
   859  		t.Run(tt.name, func(t *testing.T) {
   860  			if got := tt.machinePoolScope.RoleAssignmentSpecs(ptr.To("fakePrincipalID")); !reflect.DeepEqual(got, tt.want) {
   861  				t.Errorf("RoleAssignmentSpecs() = %v, want %v", got, tt.want)
   862  			}
   863  		})
   864  	}
   865  }
   866  
   867  func TestMachinePoolScope_VMSSExtensionSpecs(t *testing.T) {
   868  	tests := []struct {
   869  		name             string
   870  		machinePoolScope MachinePoolScope
   871  		want             []azure.ResourceSpecGetter
   872  	}{
   873  		{
   874  			name: "If OS type is Linux and cloud is AzurePublicCloud, it returns ExtensionSpec",
   875  			machinePoolScope: MachinePoolScope{
   876  				MachinePool: &expv1.MachinePool{},
   877  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   878  					ObjectMeta: metav1.ObjectMeta{
   879  						Name: "machinepool-name",
   880  					},
   881  					Spec: infrav1exp.AzureMachinePoolSpec{
   882  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
   883  							OSDisk: infrav1.OSDisk{
   884  								OSType: "Linux",
   885  							},
   886  						},
   887  					},
   888  				},
   889  				ClusterScoper: &ClusterScope{
   890  					AzureClients: AzureClients{
   891  						EnvironmentSettings: auth.EnvironmentSettings{
   892  							Environment: azureautorest.Environment{
   893  								Name: azureautorest.PublicCloud.Name,
   894  							},
   895  						},
   896  					},
   897  					AzureCluster: &infrav1.AzureCluster{
   898  						Spec: infrav1.AzureClusterSpec{
   899  							ResourceGroup: "my-rg",
   900  						},
   901  					},
   902  				},
   903  				cache: &MachinePoolCache{
   904  					VMSKU: resourceskus.SKU{},
   905  				},
   906  			},
   907  			want: []azure.ResourceSpecGetter{
   908  				&scalesets.VMSSExtensionSpec{
   909  					ExtensionSpec: azure.ExtensionSpec{
   910  						Name:      "CAPZ.Linux.Bootstrapping",
   911  						VMName:    "machinepool-name",
   912  						Publisher: "Microsoft.Azure.ContainerUpstream",
   913  						Version:   "1.0",
   914  						ProtectedSettings: map[string]string{
   915  							"commandToExecute": azure.LinuxBootstrapExtensionCommand,
   916  						},
   917  					},
   918  					ResourceGroup: "my-rg",
   919  				},
   920  			},
   921  		},
   922  		{
   923  			name: "If OS type is Linux and cloud is not AzurePublicCloud, it returns empty",
   924  			machinePoolScope: MachinePoolScope{
   925  				MachinePool: &expv1.MachinePool{},
   926  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   927  					ObjectMeta: metav1.ObjectMeta{
   928  						Name: "machinepool-name",
   929  					},
   930  					Spec: infrav1exp.AzureMachinePoolSpec{
   931  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
   932  							OSDisk: infrav1.OSDisk{
   933  								OSType: "Linux",
   934  							},
   935  						},
   936  					},
   937  				},
   938  				ClusterScoper: &ClusterScope{
   939  					AzureClients: AzureClients{
   940  						EnvironmentSettings: auth.EnvironmentSettings{
   941  							Environment: azureautorest.Environment{
   942  								Name: azureautorest.USGovernmentCloud.Name,
   943  							},
   944  						},
   945  					},
   946  					AzureCluster: &infrav1.AzureCluster{
   947  						Spec: infrav1.AzureClusterSpec{
   948  							ResourceGroup: "my-rg",
   949  						},
   950  					},
   951  				},
   952  				cache: &MachinePoolCache{
   953  					VMSKU: resourceskus.SKU{},
   954  				},
   955  			},
   956  			want: []azure.ResourceSpecGetter{},
   957  		},
   958  		{
   959  			name: "If OS type is Windows and cloud is AzurePublicCloud, it returns ExtensionSpec",
   960  			machinePoolScope: MachinePoolScope{
   961  				MachinePool: &expv1.MachinePool{},
   962  				AzureMachinePool: &infrav1exp.AzureMachinePool{
   963  					ObjectMeta: metav1.ObjectMeta{
   964  						// Note: machine pool names longer than 9 characters get truncated. See MachinePoolScope::Name() for more details.
   965  						Name: "winpool",
   966  					},
   967  					Spec: infrav1exp.AzureMachinePoolSpec{
   968  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
   969  							OSDisk: infrav1.OSDisk{
   970  								OSType: "Windows",
   971  							},
   972  						},
   973  					},
   974  				},
   975  				ClusterScoper: &ClusterScope{
   976  					AzureClients: AzureClients{
   977  						EnvironmentSettings: auth.EnvironmentSettings{
   978  							Environment: azureautorest.Environment{
   979  								Name: azureautorest.PublicCloud.Name,
   980  							},
   981  						},
   982  					},
   983  					AzureCluster: &infrav1.AzureCluster{
   984  						Spec: infrav1.AzureClusterSpec{
   985  							ResourceGroup: "my-rg",
   986  						},
   987  					},
   988  				},
   989  				cache: &MachinePoolCache{
   990  					VMSKU: resourceskus.SKU{},
   991  				},
   992  			},
   993  			want: []azure.ResourceSpecGetter{
   994  				&scalesets.VMSSExtensionSpec{
   995  					ExtensionSpec: azure.ExtensionSpec{
   996  						Name: "CAPZ.Windows.Bootstrapping",
   997  						// Note: machine pool names longer than 9 characters get truncated. See MachinePoolScope::Name() for more details.
   998  						VMName:    "winpool",
   999  						Publisher: "Microsoft.Azure.ContainerUpstream",
  1000  						Version:   "1.0",
  1001  						ProtectedSettings: map[string]string{
  1002  							"commandToExecute": azure.WindowsBootstrapExtensionCommand,
  1003  						},
  1004  					},
  1005  					ResourceGroup: "my-rg",
  1006  				},
  1007  			},
  1008  		},
  1009  		{
  1010  			name: "If OS type is Windows and cloud is not AzurePublicCloud, it returns empty",
  1011  			machinePoolScope: MachinePoolScope{
  1012  				MachinePool: &expv1.MachinePool{},
  1013  				AzureMachinePool: &infrav1exp.AzureMachinePool{
  1014  					ObjectMeta: metav1.ObjectMeta{
  1015  						Name: "machinepool-name",
  1016  					},
  1017  					Spec: infrav1exp.AzureMachinePoolSpec{
  1018  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
  1019  							OSDisk: infrav1.OSDisk{
  1020  								OSType: "Windows",
  1021  							},
  1022  						},
  1023  					},
  1024  				},
  1025  				ClusterScoper: &ClusterScope{
  1026  					AzureClients: AzureClients{
  1027  						EnvironmentSettings: auth.EnvironmentSettings{
  1028  							Environment: azureautorest.Environment{
  1029  								Name: azureautorest.USGovernmentCloud.Name,
  1030  							},
  1031  						},
  1032  					},
  1033  					AzureCluster: &infrav1.AzureCluster{
  1034  						Spec: infrav1.AzureClusterSpec{
  1035  							ResourceGroup: "my-rg",
  1036  						},
  1037  					},
  1038  				},
  1039  				cache: &MachinePoolCache{
  1040  					VMSKU: resourceskus.SKU{},
  1041  				},
  1042  			},
  1043  			want: []azure.ResourceSpecGetter{},
  1044  		},
  1045  		{
  1046  			name: "If OS type is not Linux or Windows and cloud is AzurePublicCloud, it returns empty",
  1047  			machinePoolScope: MachinePoolScope{
  1048  				MachinePool: &expv1.MachinePool{},
  1049  				AzureMachinePool: &infrav1exp.AzureMachinePool{
  1050  					ObjectMeta: metav1.ObjectMeta{
  1051  						Name: "machinepool-name",
  1052  					},
  1053  					Spec: infrav1exp.AzureMachinePoolSpec{
  1054  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
  1055  							OSDisk: infrav1.OSDisk{
  1056  								OSType: "Other",
  1057  							},
  1058  						},
  1059  					},
  1060  				},
  1061  				ClusterScoper: &ClusterScope{
  1062  					AzureClients: AzureClients{
  1063  						EnvironmentSettings: auth.EnvironmentSettings{
  1064  							Environment: azureautorest.Environment{
  1065  								Name: azureautorest.PublicCloud.Name,
  1066  							},
  1067  						},
  1068  					},
  1069  					AzureCluster: &infrav1.AzureCluster{
  1070  						Spec: infrav1.AzureClusterSpec{
  1071  							ResourceGroup: "my-rg",
  1072  						},
  1073  					},
  1074  				},
  1075  				cache: &MachinePoolCache{
  1076  					VMSKU: resourceskus.SKU{},
  1077  				},
  1078  			},
  1079  			want: []azure.ResourceSpecGetter{},
  1080  		},
  1081  		{
  1082  			name: "If OS type is not Windows or Linux and cloud is not AzurePublicCloud, it returns empty",
  1083  			machinePoolScope: MachinePoolScope{
  1084  				MachinePool: &expv1.MachinePool{},
  1085  				AzureMachinePool: &infrav1exp.AzureMachinePool{
  1086  					ObjectMeta: metav1.ObjectMeta{
  1087  						Name: "machinepool-name",
  1088  					},
  1089  					Spec: infrav1exp.AzureMachinePoolSpec{
  1090  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
  1091  							OSDisk: infrav1.OSDisk{
  1092  								OSType: "Other",
  1093  							},
  1094  						},
  1095  					},
  1096  				},
  1097  				ClusterScoper: &ClusterScope{
  1098  					AzureClients: AzureClients{
  1099  						EnvironmentSettings: auth.EnvironmentSettings{
  1100  							Environment: azureautorest.Environment{
  1101  								Name: azureautorest.USGovernmentCloud.Name,
  1102  							},
  1103  						},
  1104  					},
  1105  					AzureCluster: &infrav1.AzureCluster{
  1106  						Spec: infrav1.AzureClusterSpec{
  1107  							ResourceGroup: "my-rg",
  1108  						},
  1109  					},
  1110  				},
  1111  				cache: &MachinePoolCache{
  1112  					VMSKU: resourceskus.SKU{},
  1113  				},
  1114  			},
  1115  			want: []azure.ResourceSpecGetter{},
  1116  		},
  1117  		{
  1118  			name: "If a custom VM extension is specified, it returns the custom VM extension",
  1119  			machinePoolScope: MachinePoolScope{
  1120  				MachinePool: &expv1.MachinePool{},
  1121  				AzureMachinePool: &infrav1exp.AzureMachinePool{
  1122  					ObjectMeta: metav1.ObjectMeta{
  1123  						Name: "machinepool-name",
  1124  					},
  1125  					Spec: infrav1exp.AzureMachinePoolSpec{
  1126  						Template: infrav1exp.AzureMachinePoolMachineTemplate{
  1127  							OSDisk: infrav1.OSDisk{
  1128  								OSType: "Linux",
  1129  							},
  1130  							VMExtensions: []infrav1.VMExtension{
  1131  								{
  1132  									Name:      "custom-vm-extension",
  1133  									Publisher: "Microsoft.Azure.Extensions",
  1134  									Version:   "2.0",
  1135  									Settings: map[string]string{
  1136  										"timestamp": "1234567890",
  1137  									},
  1138  									ProtectedSettings: map[string]string{
  1139  										"commandToExecute": "echo hello world",
  1140  									},
  1141  								},
  1142  							},
  1143  						},
  1144  					},
  1145  				},
  1146  				ClusterScoper: &ClusterScope{
  1147  					AzureClients: AzureClients{
  1148  						EnvironmentSettings: auth.EnvironmentSettings{
  1149  							Environment: azureautorest.Environment{
  1150  								Name: azureautorest.PublicCloud.Name,
  1151  							},
  1152  						},
  1153  					},
  1154  					AzureCluster: &infrav1.AzureCluster{
  1155  						Spec: infrav1.AzureClusterSpec{
  1156  							ResourceGroup: "my-rg",
  1157  							AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
  1158  								Location: "westus",
  1159  							},
  1160  						},
  1161  					},
  1162  				},
  1163  				cache: &MachinePoolCache{
  1164  					VMSKU: resourceskus.SKU{},
  1165  				},
  1166  			},
  1167  			want: []azure.ResourceSpecGetter{
  1168  				&scalesets.VMSSExtensionSpec{
  1169  					ExtensionSpec: azure.ExtensionSpec{
  1170  						Name:      "custom-vm-extension",
  1171  						VMName:    "machinepool-name",
  1172  						Publisher: "Microsoft.Azure.Extensions",
  1173  						Version:   "2.0",
  1174  						Settings: map[string]string{
  1175  							"timestamp": "1234567890",
  1176  						},
  1177  						ProtectedSettings: map[string]string{
  1178  							"commandToExecute": "echo hello world",
  1179  						},
  1180  					},
  1181  					ResourceGroup: "my-rg",
  1182  				},
  1183  				&scalesets.VMSSExtensionSpec{
  1184  					ExtensionSpec: azure.ExtensionSpec{
  1185  						Name:      "CAPZ.Linux.Bootstrapping",
  1186  						VMName:    "machinepool-name",
  1187  						Publisher: "Microsoft.Azure.ContainerUpstream",
  1188  						Version:   "1.0",
  1189  						ProtectedSettings: map[string]string{
  1190  							"commandToExecute": azure.LinuxBootstrapExtensionCommand,
  1191  						},
  1192  					},
  1193  					ResourceGroup: "my-rg",
  1194  				},
  1195  			},
  1196  		},
  1197  	}
  1198  	for _, tt := range tests {
  1199  		t.Run(tt.name, func(t *testing.T) {
  1200  			if got := tt.machinePoolScope.VMSSExtensionSpecs(); !reflect.DeepEqual(got, tt.want) {
  1201  				t.Errorf("VMSSExtensionSpecs() = \n%s, want \n%s", specArrayToString(got), specArrayToString(tt.want))
  1202  			}
  1203  		})
  1204  	}
  1205  }
  1206  
  1207  func getReadyAzureMachinePoolMachines(count int32) []infrav1exp.AzureMachinePoolMachine {
  1208  	machines := make([]infrav1exp.AzureMachinePoolMachine, count)
  1209  	succeeded := infrav1.Succeeded
  1210  	for i := 0; i < int(count); i++ {
  1211  		machines[i] = infrav1exp.AzureMachinePoolMachine{
  1212  			ObjectMeta: metav1.ObjectMeta{
  1213  				Name:      fmt.Sprintf("ampm%d", i),
  1214  				Namespace: "default",
  1215  				OwnerReferences: []metav1.OwnerReference{
  1216  					{
  1217  						Name:       "amp",
  1218  						Kind:       infrav1.AzureMachinePoolKind,
  1219  						APIVersion: infrav1exp.GroupVersion.String(),
  1220  					},
  1221  				},
  1222  				Labels: map[string]string{
  1223  					clusterv1.ClusterNameLabel:      "cluster1",
  1224  					infrav1exp.MachinePoolNameLabel: "amp1",
  1225  					clusterv1.MachinePoolNameLabel:  "mp1",
  1226  					"cluster1":                      string(infrav1.ResourceLifecycleOwned),
  1227  				},
  1228  			},
  1229  			Spec: infrav1exp.AzureMachinePoolMachineSpec{
  1230  				ProviderID: fmt.Sprintf("azure://foo/ampm%d", i),
  1231  			},
  1232  			Status: infrav1exp.AzureMachinePoolMachineStatus{
  1233  				Ready:             true,
  1234  				ProvisioningState: &succeeded,
  1235  			},
  1236  		}
  1237  	}
  1238  
  1239  	return machines
  1240  }
  1241  
  1242  func getAzureMachinePoolMachine(index int) infrav1exp.AzureMachinePoolMachine {
  1243  	return infrav1exp.AzureMachinePoolMachine{
  1244  		ObjectMeta: metav1.ObjectMeta{
  1245  			Name:      fmt.Sprintf("ampm%d", index),
  1246  			Namespace: "default",
  1247  			OwnerReferences: []metav1.OwnerReference{
  1248  				{
  1249  					Name:       "amp",
  1250  					Kind:       "AzureMachinePool",
  1251  					APIVersion: infrav1exp.GroupVersion.String(),
  1252  				},
  1253  			},
  1254  			Labels: map[string]string{
  1255  				clusterv1.ClusterNameLabel:      "cluster1",
  1256  				infrav1exp.MachinePoolNameLabel: "amp1",
  1257  				clusterv1.MachinePoolNameLabel:  "mp1",
  1258  				"cluster1":                      string(infrav1.ResourceLifecycleOwned),
  1259  			},
  1260  		},
  1261  		Spec: infrav1exp.AzureMachinePoolMachineSpec{
  1262  			ProviderID: fmt.Sprintf("azure:///subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/%d", index),
  1263  		},
  1264  		Status: infrav1exp.AzureMachinePoolMachineStatus{
  1265  			Ready:             true,
  1266  			ProvisioningState: ptr.To(infrav1.Succeeded),
  1267  		},
  1268  	}
  1269  }
  1270  
  1271  func getAzureMachinePoolMachineWithOwnerMachine(index int) (clusterv1.Machine, infrav1exp.AzureMachinePoolMachine) {
  1272  	ampm := getAzureMachinePoolMachine(index)
  1273  	machine := clusterv1.Machine{
  1274  		ObjectMeta: metav1.ObjectMeta{
  1275  			Name:      fmt.Sprintf("mpm%d", index),
  1276  			Namespace: "default",
  1277  			OwnerReferences: []metav1.OwnerReference{
  1278  				{
  1279  					Name:       "mp",
  1280  					Kind:       "MachinePool",
  1281  					APIVersion: expv1.GroupVersion.String(),
  1282  				},
  1283  			},
  1284  			Labels: map[string]string{
  1285  				clusterv1.ClusterNameLabel:     "cluster1",
  1286  				clusterv1.MachinePoolNameLabel: "mp1",
  1287  			},
  1288  		},
  1289  		Spec: clusterv1.MachineSpec{
  1290  			ProviderID: &ampm.Spec.ProviderID,
  1291  			InfrastructureRef: corev1.ObjectReference{
  1292  				Kind:      "AzureMachinePoolMachine",
  1293  				Name:      ampm.Name,
  1294  				Namespace: ampm.Namespace,
  1295  			},
  1296  		},
  1297  	}
  1298  
  1299  	ampm.OwnerReferences = append(ampm.OwnerReferences, metav1.OwnerReference{
  1300  		Name:       machine.Name,
  1301  		Kind:       "Machine",
  1302  		APIVersion: clusterv1.GroupVersion.String(),
  1303  	})
  1304  
  1305  	return machine, ampm
  1306  }
  1307  
  1308  func TestMachinePoolScope_SetInfrastructureMachineKind(t *testing.T) {
  1309  	testcases := []struct {
  1310  		name             string
  1311  		azureMachinePool infrav1exp.AzureMachinePool
  1312  		updated          bool
  1313  	}{
  1314  		{
  1315  			name: "should set infrastructure machine kind",
  1316  			azureMachinePool: infrav1exp.AzureMachinePool{
  1317  				Status: infrav1exp.AzureMachinePoolStatus{},
  1318  			},
  1319  			updated: true,
  1320  		},
  1321  		{
  1322  			name: "already set infrastructure machine kind",
  1323  			azureMachinePool: infrav1exp.AzureMachinePool{
  1324  				Status: infrav1exp.AzureMachinePoolStatus{
  1325  					InfrastructureMachineKind: infrav1exp.AzureMachinePoolMachineKind,
  1326  				},
  1327  			},
  1328  			updated: false,
  1329  		},
  1330  	}
  1331  
  1332  	for _, tt := range testcases {
  1333  		tt := tt
  1334  		t.Run(tt.name, func(t *testing.T) {
  1335  			g := NewWithT(t)
  1336  
  1337  			machinePoolScope := &MachinePoolScope{
  1338  				AzureMachinePool: &tt.azureMachinePool,
  1339  			}
  1340  
  1341  			got := machinePoolScope.SetInfrastructureMachineKind()
  1342  			g.Expect(machinePoolScope.AzureMachinePool.Status.InfrastructureMachineKind).To(Equal(infrav1exp.AzureMachinePoolMachineKind))
  1343  			g.Expect(got).To(Equal(tt.updated))
  1344  		})
  1345  	}
  1346  }
  1347  
  1348  func TestMachinePoolScope_applyAzureMachinePoolMachines(t *testing.T) {
  1349  	ctx, cancel := context.WithCancel(context.Background())
  1350  	defer cancel()
  1351  	scheme := runtime.NewScheme()
  1352  	_ = clusterv1.AddToScheme(scheme)
  1353  	_ = infrav1exp.AddToScheme(scheme)
  1354  
  1355  	tests := []struct {
  1356  		Name   string
  1357  		Setup  func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder)
  1358  		Verify func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error)
  1359  	}{
  1360  		{
  1361  			Name: "if MachinePool is externally managed and overProvisionCount > 0, do not try to reduce replicas",
  1362  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) {
  1363  				mp.Annotations = map[string]string{clusterv1.ReplicasManagedByAnnotation: "cluster-autoscaler"}
  1364  				mp.Spec.Replicas = ptr.To[int32](1)
  1365  
  1366  				mpm1, ampm1 := getAzureMachinePoolMachineWithOwnerMachine(1)
  1367  				mpm2, ampm2 := getAzureMachinePoolMachineWithOwnerMachine(2)
  1368  				objects := []client.Object{}
  1369  				objects = append(objects, &mpm1, &ampm1, &mpm2, &ampm2)
  1370  				cb.WithObjects(objects...)
  1371  				vmssState.Instances = []azure.VMSSVM{
  1372  					{
  1373  						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/1",
  1374  						Name: "ampm1",
  1375  					},
  1376  					{
  1377  						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/2",
  1378  						Name: "ampm2",
  1379  					},
  1380  				}
  1381  			},
  1382  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) {
  1383  				g.Expect(err).NotTo(HaveOccurred())
  1384  				list := clusterv1.MachineList{}
  1385  				g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred())
  1386  				g.Expect(list.Items).Should(HaveLen(2))
  1387  			},
  1388  		},
  1389  		{
  1390  			Name: "if MachinePool is not externally managed and overProvisionCount > 0, reduce replicas",
  1391  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) {
  1392  				mp.Spec.Replicas = ptr.To[int32](1)
  1393  
  1394  				mpm1, ampm1 := getAzureMachinePoolMachineWithOwnerMachine(1)
  1395  				mpm2, ampm2 := getAzureMachinePoolMachineWithOwnerMachine(2)
  1396  				objects := []client.Object{}
  1397  				objects = append(objects, &mpm1, &ampm1, &mpm2, &ampm2)
  1398  				cb.WithObjects(objects...)
  1399  
  1400  				vmssState.Instances = []azure.VMSSVM{
  1401  					{
  1402  						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/1",
  1403  						Name: "ampm1",
  1404  					},
  1405  					{
  1406  						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/2",
  1407  						Name: "ampm2",
  1408  					},
  1409  				}
  1410  			},
  1411  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) {
  1412  				g.Expect(err).NotTo(HaveOccurred())
  1413  				list := clusterv1.MachineList{}
  1414  				g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred())
  1415  				g.Expect(list.Items).Should(HaveLen(1))
  1416  			},
  1417  		},
  1418  		{
  1419  			Name: "if MachinePool is not externally managed, and Machines have delete machine annotation, and overProvisionCount > 0, delete machines with deleteMachine annotation first",
  1420  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) {
  1421  				mp.Spec.Replicas = ptr.To[int32](2)
  1422  
  1423  				mpm1, ampm1 := getAzureMachinePoolMachineWithOwnerMachine(1)
  1424  
  1425  				mpm2, ampm2 := getAzureMachinePoolMachineWithOwnerMachine(2)
  1426  				mpm2.Annotations = map[string]string{
  1427  					clusterv1.DeleteMachineAnnotation: time.Now().String(),
  1428  				}
  1429  
  1430  				mpm3, ampm3 := getAzureMachinePoolMachineWithOwnerMachine(3)
  1431  				objects := []client.Object{&mpm1, &ampm1, &mpm2, &ampm2, &mpm3, &ampm3}
  1432  				cb.WithObjects(objects...)
  1433  
  1434  				vmssState.Instances = []azure.VMSSVM{
  1435  					{
  1436  						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/1",
  1437  						Name: "ampm1",
  1438  					},
  1439  					{
  1440  						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/2",
  1441  						Name: "ampm2",
  1442  					},
  1443  					{
  1444  						ID:   "/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines/3",
  1445  						Name: "ampm3",
  1446  					},
  1447  				}
  1448  			},
  1449  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) {
  1450  				g.Expect(err).NotTo(HaveOccurred())
  1451  				list := clusterv1.MachineList{}
  1452  				g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred())
  1453  				g.Expect(list.Items).Should(HaveLen(2))
  1454  				g.Expect(list.Items[0].Name).Should(Equal("mpm1"))
  1455  				g.Expect(list.Items[1].Name).Should(Equal("mpm3"))
  1456  			},
  1457  		},
  1458  		{
  1459  			Name: "if existing MachinePool is not present, reduce replicas",
  1460  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) {
  1461  				mp.Spec.Replicas = ptr.To[int32](1)
  1462  
  1463  				vmssState.Instances = []azure.VMSSVM{
  1464  					{
  1465  						ID:   "/subscriptions/123/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm",
  1466  						Name: "vm",
  1467  					},
  1468  				}
  1469  			},
  1470  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) {
  1471  				g.Expect(err).NotTo(HaveOccurred())
  1472  				list := infrav1exp.AzureMachinePoolMachineList{}
  1473  				g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred())
  1474  				g.Expect(list.Items).Should(HaveLen(1))
  1475  			},
  1476  		},
  1477  		{
  1478  			Name: "if existing MachinePool is not present and Instances ID is in wrong format, reduce replicas",
  1479  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) {
  1480  				mp.Spec.Replicas = ptr.To[int32](1)
  1481  
  1482  				vmssState.Instances = []azure.VMSSVM{
  1483  					{
  1484  						ID:   "foo/ampm0",
  1485  						Name: "ampm0",
  1486  					},
  1487  				}
  1488  			},
  1489  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) {
  1490  				g.Expect(err).To(HaveOccurred())
  1491  			},
  1492  		},
  1493  		{
  1494  			Name: "if existing MachinePool is present but in deleting state, do not recreate AzureMachinePoolMachines",
  1495  			Setup: func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool, vmssState *azure.VMSS, cb *fake.ClientBuilder) {
  1496  				mp.Spec.Replicas = ptr.To[int32](1)
  1497  
  1498  				vmssState.Instances = []azure.VMSSVM{
  1499  					{
  1500  						ID:    "/subscriptions/123/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm",
  1501  						Name:  "vm",
  1502  						State: infrav1.Deleting,
  1503  					},
  1504  				}
  1505  			},
  1506  			Verify: func(g *WithT, amp *infrav1exp.AzureMachinePool, c client.Client, err error) {
  1507  				g.Expect(err).NotTo(HaveOccurred())
  1508  				list := infrav1exp.AzureMachinePoolMachineList{}
  1509  				g.Expect(c.List(ctx, &list)).NotTo(HaveOccurred())
  1510  				g.Expect(list.Items).Should(BeEmpty())
  1511  			},
  1512  		},
  1513  	}
  1514  	for _, tt := range tests {
  1515  		t.Run(tt.Name, func(t *testing.T) {
  1516  			var (
  1517  				g        = NewWithT(t)
  1518  				mockCtrl = gomock.NewController(t)
  1519  				cb       = fake.NewClientBuilder().WithScheme(scheme)
  1520  				cluster  = &clusterv1.Cluster{
  1521  					ObjectMeta: metav1.ObjectMeta{
  1522  						Name:      "cluster1",
  1523  						Namespace: "default",
  1524  					},
  1525  					Spec: clusterv1.ClusterSpec{
  1526  						InfrastructureRef: &corev1.ObjectReference{
  1527  							Name: "azCluster1",
  1528  						},
  1529  					},
  1530  					Status: clusterv1.ClusterStatus{
  1531  						InfrastructureReady: true,
  1532  					},
  1533  				}
  1534  				mp = &expv1.MachinePool{
  1535  					ObjectMeta: metav1.ObjectMeta{
  1536  						Name:      "mp1",
  1537  						Namespace: "default",
  1538  						OwnerReferences: []metav1.OwnerReference{
  1539  							{
  1540  								Name:       "cluster1",
  1541  								Kind:       "Cluster",
  1542  								APIVersion: clusterv1.GroupVersion.String(),
  1543  							},
  1544  						},
  1545  					},
  1546  				}
  1547  				amp = &infrav1exp.AzureMachinePool{
  1548  					ObjectMeta: metav1.ObjectMeta{
  1549  						Name:      "amp1",
  1550  						Namespace: "default",
  1551  						OwnerReferences: []metav1.OwnerReference{
  1552  							{
  1553  								Name:       "mp1",
  1554  								Kind:       "MachinePool",
  1555  								APIVersion: expv1.GroupVersion.String(),
  1556  							},
  1557  						},
  1558  					},
  1559  				}
  1560  				vmssState = &azure.VMSS{}
  1561  			)
  1562  			defer mockCtrl.Finish()
  1563  
  1564  			tt.Setup(mp, amp, vmssState, cb.WithObjects(amp, cluster))
  1565  			s := &MachinePoolScope{
  1566  				client: cb.Build(),
  1567  				ClusterScoper: &ClusterScope{
  1568  					Cluster: cluster,
  1569  				},
  1570  				MachinePool:      mp,
  1571  				AzureMachinePool: amp,
  1572  				vmssState:        vmssState,
  1573  			}
  1574  			err := s.applyAzureMachinePoolMachines(ctx)
  1575  			tt.Verify(g, s.AzureMachinePool, s.client, err)
  1576  		})
  1577  	}
  1578  }