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