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

     1  /*
     2  Copyright 2019 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 controllers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/go-logr/logr"
    27  	"github.com/google/go-cmp/cmp"
    28  	. "github.com/onsi/gomega"
    29  	"go.uber.org/mock/gomock"
    30  	"golang.org/x/exp/maps"
    31  	corev1 "k8s.io/api/core/v1"
    32  	"k8s.io/apimachinery/pkg/api/resource"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/types"
    36  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    37  	"k8s.io/utils/ptr"
    38  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    39  	"sigs.k8s.io/cluster-api-provider-azure/azure/scope"
    40  	"sigs.k8s.io/cluster-api-provider-azure/internal/test/mock_log"
    41  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    42  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    43  	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
    44  	"sigs.k8s.io/controller-runtime/pkg/client"
    45  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    46  	"sigs.k8s.io/controller-runtime/pkg/event"
    47  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    48  )
    49  
    50  var (
    51  	cpName      = "my-managed-cp"
    52  	clusterName = "my-cluster"
    53  )
    54  
    55  func TestAzureClusterToAzureMachinesMapper(t *testing.T) {
    56  	g := NewWithT(t)
    57  	scheme := setupScheme(g)
    58  	clusterName := "my-cluster"
    59  	initObjects := []runtime.Object{
    60  		newCluster(clusterName),
    61  		// Create two Machines with an infrastructure ref and one without.
    62  		newMachineWithInfrastructureRef(clusterName, "my-machine-0"),
    63  		newMachineWithInfrastructureRef(clusterName, "my-machine-1"),
    64  		newMachine(clusterName, "my-machine-2"),
    65  	}
    66  	client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
    67  
    68  	mockCtrl := gomock.NewController(t)
    69  	defer mockCtrl.Finish()
    70  
    71  	sink := mock_log.NewMockLogSink(mockCtrl)
    72  	sink.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
    73  	sink.EXPECT().WithValues("AzureCluster", "my-cluster", "Namespace", "default")
    74  	mapper, err := AzureClusterToAzureMachinesMapper(context.Background(), client, &infrav1.AzureMachine{}, scheme, logr.New(sink))
    75  	g.Expect(err).NotTo(HaveOccurred())
    76  
    77  	requests := mapper(context.TODO(), &infrav1.AzureCluster{
    78  		ObjectMeta: metav1.ObjectMeta{
    79  			Name:      clusterName,
    80  			Namespace: "default",
    81  			OwnerReferences: []metav1.OwnerReference{
    82  				{
    83  					Name:       clusterName,
    84  					Kind:       "Cluster",
    85  					APIVersion: clusterv1.GroupVersion.String(),
    86  				},
    87  			},
    88  		},
    89  	})
    90  	g.Expect(requests).To(HaveLen(2))
    91  }
    92  
    93  func TestGetCloudProviderConfig(t *testing.T) {
    94  	g := NewWithT(t)
    95  	scheme := runtime.NewScheme()
    96  	_ = clusterv1.AddToScheme(scheme)
    97  	_ = infrav1.AddToScheme(scheme)
    98  	_ = corev1.AddToScheme(scheme)
    99  
   100  	cluster := newCluster("foo")
   101  	azureCluster := newAzureCluster("bar")
   102  	azureCluster.Default()
   103  	azureClusterCustomVnet := newAzureClusterWithCustomVnet("bar")
   104  	azureClusterCustomVnet.Default()
   105  
   106  	cases := map[string]struct {
   107  		cluster                    *clusterv1.Cluster
   108  		azureCluster               *infrav1.AzureCluster
   109  		identityType               infrav1.VMIdentity
   110  		identityID                 string
   111  		expectedControlPlaneConfig string
   112  		expectedWorkerNodeConfig   string
   113  	}{
   114  		"serviceprincipal": {
   115  			cluster:                    cluster,
   116  			azureCluster:               azureCluster,
   117  			identityType:               infrav1.VMIdentityNone,
   118  			expectedControlPlaneConfig: spControlPlaneCloudConfig,
   119  			expectedWorkerNodeConfig:   spWorkerNodeCloudConfig,
   120  		},
   121  		"system-assigned-identity": {
   122  			cluster:                    cluster,
   123  			azureCluster:               azureCluster,
   124  			identityType:               infrav1.VMIdentitySystemAssigned,
   125  			expectedControlPlaneConfig: systemAssignedControlPlaneCloudConfig,
   126  			expectedWorkerNodeConfig:   systemAssignedWorkerNodeCloudConfig,
   127  		},
   128  		"user-assigned-identity": {
   129  			cluster:                    cluster,
   130  			azureCluster:               azureCluster,
   131  			identityType:               infrav1.VMIdentityUserAssigned,
   132  			identityID:                 "foobar",
   133  			expectedControlPlaneConfig: userAssignedControlPlaneCloudConfig,
   134  			expectedWorkerNodeConfig:   userAssignedWorkerNodeCloudConfig,
   135  		},
   136  		"serviceprincipal with custom vnet": {
   137  			cluster:                    cluster,
   138  			azureCluster:               azureClusterCustomVnet,
   139  			identityType:               infrav1.VMIdentityNone,
   140  			expectedControlPlaneConfig: spCustomVnetControlPlaneCloudConfig,
   141  			expectedWorkerNodeConfig:   spCustomVnetWorkerNodeCloudConfig,
   142  		},
   143  		"with rate limits": {
   144  			cluster:                    cluster,
   145  			azureCluster:               withRateLimits(*azureCluster),
   146  			identityType:               infrav1.VMIdentityNone,
   147  			expectedControlPlaneConfig: rateLimitsControlPlaneCloudConfig,
   148  			expectedWorkerNodeConfig:   rateLimitsWorkerNodeCloudConfig,
   149  		},
   150  		"with back-off config": {
   151  			cluster:                    cluster,
   152  			azureCluster:               withbackOffConfig(*azureCluster),
   153  			identityType:               infrav1.VMIdentityNone,
   154  			expectedControlPlaneConfig: backOffCloudConfig,
   155  			expectedWorkerNodeConfig:   backOffCloudConfig,
   156  		},
   157  		"with machinepools": {
   158  			cluster:                    cluster,
   159  			azureCluster:               azureCluster,
   160  			identityType:               infrav1.VMIdentityNone,
   161  			expectedControlPlaneConfig: vmssCloudConfig,
   162  			expectedWorkerNodeConfig:   vmssCloudConfig,
   163  		},
   164  	}
   165  
   166  	os.Setenv("AZURE_CLIENT_ID", "fooClient")
   167  	os.Setenv("AZURE_CLIENT_SECRET", "fooSecret")
   168  	os.Setenv("AZURE_TENANT_ID", "fooTenant")
   169  
   170  	for name, tc := range cases {
   171  		t.Run(name, func(t *testing.T) {
   172  			fakeIdentity := &infrav1.AzureClusterIdentity{
   173  				ObjectMeta: metav1.ObjectMeta{
   174  					Name:      "fake-identity",
   175  					Namespace: "default",
   176  				},
   177  				Spec: infrav1.AzureClusterIdentitySpec{
   178  					Type:         infrav1.ServicePrincipal,
   179  					ClientID:     "fooClient",
   180  					TenantID:     "fooTenant",
   181  					ClientSecret: corev1.SecretReference{Name: "fooSecret", Namespace: "default"},
   182  				},
   183  			}
   184  			fakeSecret := getASOSecret(tc.cluster, func(s *corev1.Secret) {
   185  				s.ObjectMeta.Name = "fooSecret"
   186  				s.Data = map[string][]byte{
   187  					"AZURE_SUBSCRIPTION_ID": []byte("fooSubscription"),
   188  					"AZURE_TENANT_ID":       []byte("fooTenant"),
   189  					"AZURE_CLIENT_ID":       []byte("fooClient"),
   190  					"AZURE_CLIENT_SECRET":   []byte("fooSecret"),
   191  					"clientSecret":          []byte("fooSecret"),
   192  				}
   193  			})
   194  
   195  			initObjects := []runtime.Object{tc.cluster, tc.azureCluster, fakeIdentity, fakeSecret}
   196  			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
   197  			resultSecret := &corev1.Secret{}
   198  			key := client.ObjectKey{Name: fakeSecret.Name, Namespace: fakeSecret.Namespace}
   199  			g.Expect(fakeClient.Get(context.Background(), key, resultSecret)).To(Succeed())
   200  
   201  			clusterScope, err := scope.NewClusterScope(context.Background(), scope.ClusterScopeParams{
   202  				Cluster:      tc.cluster,
   203  				AzureCluster: tc.azureCluster,
   204  				Client:       fakeClient,
   205  			})
   206  			g.Expect(err).NotTo(HaveOccurred())
   207  
   208  			cloudConfig, err := GetCloudProviderSecret(clusterScope, "default", "foo", metav1.OwnerReference{}, tc.identityType, tc.identityID)
   209  			g.Expect(err).NotTo(HaveOccurred())
   210  			g.Expect(cloudConfig.Data).NotTo(BeNil())
   211  
   212  			if diff := cmp.Diff(tc.expectedControlPlaneConfig, string(cloudConfig.Data["control-plane-azure.json"])); diff != "" {
   213  				t.Errorf(diff)
   214  			}
   215  			if diff := cmp.Diff(tc.expectedWorkerNodeConfig, string(cloudConfig.Data["worker-node-azure.json"])); diff != "" {
   216  				t.Errorf(diff)
   217  			}
   218  			if diff := cmp.Diff(tc.expectedControlPlaneConfig, string(cloudConfig.Data["azure.json"])); diff != "" {
   219  				t.Errorf(diff)
   220  			}
   221  		})
   222  	}
   223  }
   224  
   225  func TestReconcileAzureSecret(t *testing.T) {
   226  	g := NewWithT(t)
   227  
   228  	cases := map[string]struct {
   229  		kind             string
   230  		apiVersion       string
   231  		ownerName        string
   232  		existingSecret   *corev1.Secret
   233  		expectedNoChange bool
   234  	}{
   235  		"azuremachine should reconcile secret successfully": {
   236  			kind:       "AzureMachine",
   237  			apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   238  			ownerName:  "azureMachineName",
   239  		},
   240  		"azuremachinepool should reconcile secret successfully": {
   241  			kind:       infrav1.AzureMachinePoolKind,
   242  			apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   243  			ownerName:  "azureMachinePoolName",
   244  		},
   245  		"azuremachinetemplate should reconcile secret successfully": {
   246  			kind:       "AzureMachineTemplate",
   247  			apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   248  			ownerName:  "azureMachineTemplateName",
   249  		},
   250  		"should not replace the content of the pre-existing unowned secret": {
   251  			kind:       "AzureMachine",
   252  			apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   253  			ownerName:  "azureMachineName",
   254  			existingSecret: &corev1.Secret{
   255  				ObjectMeta: metav1.ObjectMeta{
   256  					Name:      "azureMachineName-azure-json",
   257  					Namespace: "default",
   258  					Labels:    map[string]string{"testCluster": "foo"},
   259  				},
   260  				Data: map[string][]byte{
   261  					"azure.json": []byte("foobar"),
   262  				},
   263  			},
   264  			expectedNoChange: true,
   265  		},
   266  		"should not replace the content of the pre-existing unowned secret without the label": {
   267  			kind:       "AzureMachine",
   268  			apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   269  			ownerName:  "azureMachineName",
   270  			existingSecret: &corev1.Secret{
   271  				ObjectMeta: metav1.ObjectMeta{
   272  					Name:      "azureMachineName-azure-json",
   273  					Namespace: "default",
   274  				},
   275  				Data: map[string][]byte{
   276  					"azure.json": []byte("foobar"),
   277  				},
   278  			},
   279  			expectedNoChange: true,
   280  		},
   281  		"should replace the content of the pre-existing owned secret": {
   282  			kind:       "AzureMachine",
   283  			apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   284  			ownerName:  "azureMachineName",
   285  			existingSecret: &corev1.Secret{
   286  				ObjectMeta: metav1.ObjectMeta{
   287  					Name:      "azureMachineName-azure-json",
   288  					Namespace: "default",
   289  					Labels:    map[string]string{"testCluster": string(infrav1.ResourceLifecycleOwned)},
   290  				},
   291  				Data: map[string][]byte{
   292  					"azure.json": []byte("foobar"),
   293  				},
   294  			},
   295  		},
   296  	}
   297  
   298  	cluster := newCluster("foo")
   299  	azureCluster := newAzureCluster("bar")
   300  
   301  	azureCluster.Default()
   302  	cluster.Name = "testCluster"
   303  
   304  	fakeIdentity := &infrav1.AzureClusterIdentity{
   305  		ObjectMeta: metav1.ObjectMeta{
   306  			Name:      "fake-identity",
   307  			Namespace: "default",
   308  		},
   309  		Spec: infrav1.AzureClusterIdentitySpec{
   310  			Type:     infrav1.ServicePrincipal,
   311  			TenantID: "fake-tenantid",
   312  		},
   313  	}
   314  	fakeSecret := &corev1.Secret{Data: map[string][]byte{"clientSecret": []byte("fooSecret")}}
   315  	initObjects := []runtime.Object{fakeIdentity, fakeSecret}
   316  
   317  	scheme := setupScheme(g)
   318  	kubeclient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
   319  
   320  	clusterScope, err := scope.NewClusterScope(context.Background(), scope.ClusterScopeParams{
   321  		Cluster:      cluster,
   322  		AzureCluster: azureCluster,
   323  		Client:       kubeclient,
   324  	})
   325  	g.Expect(err).NotTo(HaveOccurred())
   326  
   327  	for name, tc := range cases {
   328  		t.Run(name, func(t *testing.T) {
   329  			if tc.existingSecret != nil {
   330  				_ = kubeclient.Delete(context.Background(), tc.existingSecret)
   331  				_ = kubeclient.Create(context.Background(), tc.existingSecret)
   332  				defer func() {
   333  					_ = kubeclient.Delete(context.Background(), tc.existingSecret)
   334  				}()
   335  			}
   336  
   337  			owner := metav1.OwnerReference{
   338  				APIVersion: tc.apiVersion,
   339  				Kind:       tc.kind,
   340  				Name:       tc.ownerName,
   341  			}
   342  			cloudConfig, err := GetCloudProviderSecret(clusterScope, "default", tc.ownerName, owner, infrav1.VMIdentitySystemAssigned, "")
   343  			g.Expect(err).NotTo(HaveOccurred())
   344  			g.Expect(cloudConfig.Data).NotTo(BeNil())
   345  
   346  			if err := reconcileAzureSecret(context.Background(), kubeclient, owner, cloudConfig, cluster.Name); err != nil {
   347  				t.Error(err)
   348  			}
   349  
   350  			key := types.NamespacedName{
   351  				Namespace: "default",
   352  				Name:      fmt.Sprintf("%s-azure-json", tc.ownerName),
   353  			}
   354  			found := &corev1.Secret{}
   355  			if err := kubeclient.Get(context.Background(), key, found); err != nil {
   356  				t.Error(err)
   357  			}
   358  
   359  			if tc.expectedNoChange {
   360  				g.Expect(cloudConfig.Data).NotTo(Equal(found.Data))
   361  			} else {
   362  				g.Expect(cloudConfig.Data).To(Equal(found.Data))
   363  				g.Expect(found.OwnerReferences).To(Equal(cloudConfig.OwnerReferences))
   364  			}
   365  		})
   366  	}
   367  }
   368  
   369  func setupScheme(g *WithT) *runtime.Scheme {
   370  	scheme := runtime.NewScheme()
   371  	g.Expect(clientgoscheme.AddToScheme(scheme)).To(Succeed())
   372  	g.Expect(infrav1.AddToScheme(scheme)).To(Succeed())
   373  	g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed())
   374  	return scheme
   375  }
   376  
   377  func newMachine(clusterName, machineName string) *clusterv1.Machine {
   378  	return &clusterv1.Machine{
   379  		ObjectMeta: metav1.ObjectMeta{
   380  			Labels: map[string]string{
   381  				clusterv1.ClusterNameLabel: clusterName,
   382  			},
   383  			Name:      machineName,
   384  			Namespace: "default",
   385  		},
   386  	}
   387  }
   388  
   389  func newMachineWithInfrastructureRef(clusterName, machineName string) *clusterv1.Machine {
   390  	m := newMachine(clusterName, machineName)
   391  	m.Spec.InfrastructureRef = corev1.ObjectReference{
   392  		Kind:       "AzureMachine",
   393  		Namespace:  "default",
   394  		Name:       "azure" + machineName,
   395  		APIVersion: infrav1.GroupVersion.String(),
   396  	}
   397  	return m
   398  }
   399  
   400  func newCluster(name string) *clusterv1.Cluster {
   401  	return &clusterv1.Cluster{
   402  		ObjectMeta: metav1.ObjectMeta{
   403  			Name:      name,
   404  			Namespace: "default",
   405  		},
   406  	}
   407  }
   408  
   409  func newAzureCluster(location string) *infrav1.AzureCluster {
   410  	return &infrav1.AzureCluster{
   411  		ObjectMeta: metav1.ObjectMeta{
   412  			Name:      "foo",
   413  			Namespace: "default",
   414  		},
   415  		Spec: infrav1.AzureClusterSpec{
   416  			AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
   417  				Location:       location,
   418  				SubscriptionID: "baz",
   419  				IdentityRef: &corev1.ObjectReference{
   420  					Kind:      "AzureClusterIdentity",
   421  					Name:      "fake-identity",
   422  					Namespace: "default",
   423  				},
   424  			},
   425  			NetworkSpec: infrav1.NetworkSpec{
   426  				Vnet: infrav1.VnetSpec{},
   427  			},
   428  			ResourceGroup: "bar",
   429  		},
   430  	}
   431  }
   432  
   433  func withRateLimits(ac infrav1.AzureCluster) *infrav1.AzureCluster {
   434  	cloudProviderRateLimitQPS := resource.MustParse("1.2")
   435  	rateLimits := []infrav1.RateLimitSpec{
   436  		{
   437  			Name: "defaultRateLimit",
   438  			Config: infrav1.RateLimitConfig{
   439  				CloudProviderRateLimit:    true,
   440  				CloudProviderRateLimitQPS: &cloudProviderRateLimitQPS,
   441  			},
   442  		},
   443  		{
   444  			Name: "loadBalancerRateLimit",
   445  			Config: infrav1.RateLimitConfig{
   446  				CloudProviderRateLimitBucket: 10,
   447  			},
   448  		},
   449  	}
   450  	ac.Spec.CloudProviderConfigOverrides = &infrav1.CloudProviderConfigOverrides{RateLimits: rateLimits}
   451  	return &ac
   452  }
   453  
   454  func withbackOffConfig(ac infrav1.AzureCluster) *infrav1.AzureCluster {
   455  	cloudProviderBackOffExponent := resource.MustParse("1.2")
   456  	backOff := infrav1.BackOffConfig{
   457  		CloudProviderBackoff:         true,
   458  		CloudProviderBackoffRetries:  1,
   459  		CloudProviderBackoffExponent: &cloudProviderBackOffExponent,
   460  		CloudProviderBackoffDuration: 60,
   461  		CloudProviderBackoffJitter:   &cloudProviderBackOffExponent,
   462  	}
   463  	ac.Spec.CloudProviderConfigOverrides = &infrav1.CloudProviderConfigOverrides{BackOffs: backOff}
   464  	return &ac
   465  }
   466  
   467  func newAzureClusterWithCustomVnet(location string) *infrav1.AzureCluster {
   468  	return &infrav1.AzureCluster{
   469  		ObjectMeta: metav1.ObjectMeta{
   470  			Name:      "foo",
   471  			Namespace: "default",
   472  		},
   473  		Spec: infrav1.AzureClusterSpec{
   474  			AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
   475  				Location:       location,
   476  				SubscriptionID: "baz",
   477  				IdentityRef: &corev1.ObjectReference{
   478  					Kind:      "AzureClusterIdentity",
   479  					Name:      "fake-identity",
   480  					Namespace: "default",
   481  				},
   482  			},
   483  			NetworkSpec: infrav1.NetworkSpec{
   484  				Vnet: infrav1.VnetSpec{
   485  					Name:          "custom-vnet",
   486  					ResourceGroup: "custom-vnet-resource-group",
   487  				},
   488  				Subnets: infrav1.Subnets{
   489  					infrav1.SubnetSpec{
   490  						SubnetClassSpec: infrav1.SubnetClassSpec{
   491  							Name: "foo-controlplane-subnet",
   492  							Role: infrav1.SubnetControlPlane,
   493  						},
   494  					},
   495  					infrav1.SubnetSpec{
   496  						SubnetClassSpec: infrav1.SubnetClassSpec{
   497  							Name: "foo-node-subnet",
   498  							Role: infrav1.SubnetNode,
   499  						},
   500  					},
   501  				},
   502  			},
   503  			ResourceGroup: "bar",
   504  		},
   505  	}
   506  }
   507  
   508  const (
   509  	spControlPlaneCloudConfig = `{
   510      "cloud": "AzurePublicCloud",
   511      "tenantId": "fooTenant",
   512      "subscriptionId": "baz",
   513      "aadClientId": "fooClient",
   514      "aadClientSecret": "fooSecret",
   515      "resourceGroup": "bar",
   516      "securityGroupName": "foo-node-nsg",
   517      "securityGroupResourceGroup": "bar",
   518      "location": "bar",
   519      "vmType": "vmss",
   520      "vnetName": "foo-vnet",
   521      "vnetResourceGroup": "bar",
   522      "subnetName": "foo-node-subnet",
   523      "routeTableName": "foo-node-routetable",
   524      "loadBalancerSku": "Standard",
   525      "loadBalancerName": "",
   526      "maximumLoadBalancerRuleCount": 250,
   527      "useManagedIdentityExtension": false,
   528      "useInstanceMetadata": true,
   529      "enableVmssFlexNodes": true
   530  }`
   531  	//nolint:gosec // Ignore "G101: Potential hardcoded credentials" check.
   532  	spWorkerNodeCloudConfig = `{
   533      "cloud": "AzurePublicCloud",
   534      "tenantId": "fooTenant",
   535      "subscriptionId": "baz",
   536      "aadClientId": "fooClient",
   537      "aadClientSecret": "fooSecret",
   538      "resourceGroup": "bar",
   539      "securityGroupName": "foo-node-nsg",
   540      "securityGroupResourceGroup": "bar",
   541      "location": "bar",
   542      "vmType": "vmss",
   543      "vnetName": "foo-vnet",
   544      "vnetResourceGroup": "bar",
   545      "subnetName": "foo-node-subnet",
   546      "routeTableName": "foo-node-routetable",
   547      "loadBalancerSku": "Standard",
   548      "loadBalancerName": "",
   549      "maximumLoadBalancerRuleCount": 250,
   550      "useManagedIdentityExtension": false,
   551      "useInstanceMetadata": true,
   552      "enableVmssFlexNodes": true
   553  }`
   554  
   555  	systemAssignedControlPlaneCloudConfig = `{
   556      "cloud": "AzurePublicCloud",
   557      "tenantId": "fooTenant",
   558      "subscriptionId": "baz",
   559      "resourceGroup": "bar",
   560      "securityGroupName": "foo-node-nsg",
   561      "securityGroupResourceGroup": "bar",
   562      "location": "bar",
   563      "vmType": "vmss",
   564      "vnetName": "foo-vnet",
   565      "vnetResourceGroup": "bar",
   566      "subnetName": "foo-node-subnet",
   567      "routeTableName": "foo-node-routetable",
   568      "loadBalancerSku": "Standard",
   569      "loadBalancerName": "",
   570      "maximumLoadBalancerRuleCount": 250,
   571      "useManagedIdentityExtension": true,
   572      "useInstanceMetadata": true,
   573      "enableVmssFlexNodes": true
   574  }`
   575  	systemAssignedWorkerNodeCloudConfig = `{
   576      "cloud": "AzurePublicCloud",
   577      "tenantId": "fooTenant",
   578      "subscriptionId": "baz",
   579      "resourceGroup": "bar",
   580      "securityGroupName": "foo-node-nsg",
   581      "securityGroupResourceGroup": "bar",
   582      "location": "bar",
   583      "vmType": "vmss",
   584      "vnetName": "foo-vnet",
   585      "vnetResourceGroup": "bar",
   586      "subnetName": "foo-node-subnet",
   587      "routeTableName": "foo-node-routetable",
   588      "loadBalancerSku": "Standard",
   589      "loadBalancerName": "",
   590      "maximumLoadBalancerRuleCount": 250,
   591      "useManagedIdentityExtension": true,
   592      "useInstanceMetadata": true,
   593      "enableVmssFlexNodes": true
   594  }`
   595  
   596  	userAssignedControlPlaneCloudConfig = `{
   597      "cloud": "AzurePublicCloud",
   598      "tenantId": "fooTenant",
   599      "subscriptionId": "baz",
   600      "resourceGroup": "bar",
   601      "securityGroupName": "foo-node-nsg",
   602      "securityGroupResourceGroup": "bar",
   603      "location": "bar",
   604      "vmType": "vmss",
   605      "vnetName": "foo-vnet",
   606      "vnetResourceGroup": "bar",
   607      "subnetName": "foo-node-subnet",
   608      "routeTableName": "foo-node-routetable",
   609      "loadBalancerSku": "Standard",
   610      "loadBalancerName": "",
   611      "maximumLoadBalancerRuleCount": 250,
   612      "useManagedIdentityExtension": true,
   613      "useInstanceMetadata": true,
   614      "enableVmssFlexNodes": true,
   615      "userAssignedIdentityID": "foobar"
   616  }`
   617  	userAssignedWorkerNodeCloudConfig = `{
   618      "cloud": "AzurePublicCloud",
   619      "tenantId": "fooTenant",
   620      "subscriptionId": "baz",
   621      "resourceGroup": "bar",
   622      "securityGroupName": "foo-node-nsg",
   623      "securityGroupResourceGroup": "bar",
   624      "location": "bar",
   625      "vmType": "vmss",
   626      "vnetName": "foo-vnet",
   627      "vnetResourceGroup": "bar",
   628      "subnetName": "foo-node-subnet",
   629      "routeTableName": "foo-node-routetable",
   630      "loadBalancerSku": "Standard",
   631      "loadBalancerName": "",
   632      "maximumLoadBalancerRuleCount": 250,
   633      "useManagedIdentityExtension": true,
   634      "useInstanceMetadata": true,
   635      "enableVmssFlexNodes": true,
   636      "userAssignedIdentityID": "foobar"
   637  }`
   638  	spCustomVnetControlPlaneCloudConfig = `{
   639      "cloud": "AzurePublicCloud",
   640      "tenantId": "fooTenant",
   641      "subscriptionId": "baz",
   642      "aadClientId": "fooClient",
   643      "aadClientSecret": "fooSecret",
   644      "resourceGroup": "bar",
   645      "securityGroupName": "foo-node-nsg",
   646      "securityGroupResourceGroup": "custom-vnet-resource-group",
   647      "location": "bar",
   648      "vmType": "vmss",
   649      "vnetName": "custom-vnet",
   650      "vnetResourceGroup": "custom-vnet-resource-group",
   651      "subnetName": "foo-node-subnet",
   652      "routeTableName": "foo-node-routetable",
   653      "loadBalancerSku": "Standard",
   654      "loadBalancerName": "",
   655      "maximumLoadBalancerRuleCount": 250,
   656      "useManagedIdentityExtension": false,
   657      "useInstanceMetadata": true,
   658      "enableVmssFlexNodes": true
   659  }`
   660  	spCustomVnetWorkerNodeCloudConfig = `{
   661      "cloud": "AzurePublicCloud",
   662      "tenantId": "fooTenant",
   663      "subscriptionId": "baz",
   664      "aadClientId": "fooClient",
   665      "aadClientSecret": "fooSecret",
   666      "resourceGroup": "bar",
   667      "securityGroupName": "foo-node-nsg",
   668      "securityGroupResourceGroup": "custom-vnet-resource-group",
   669      "location": "bar",
   670      "vmType": "vmss",
   671      "vnetName": "custom-vnet",
   672      "vnetResourceGroup": "custom-vnet-resource-group",
   673      "subnetName": "foo-node-subnet",
   674      "routeTableName": "foo-node-routetable",
   675      "loadBalancerSku": "Standard",
   676      "loadBalancerName": "",
   677      "maximumLoadBalancerRuleCount": 250,
   678      "useManagedIdentityExtension": false,
   679      "useInstanceMetadata": true,
   680      "enableVmssFlexNodes": true
   681  }`
   682  	rateLimitsControlPlaneCloudConfig = `{
   683      "cloud": "AzurePublicCloud",
   684      "tenantId": "fooTenant",
   685      "subscriptionId": "baz",
   686      "aadClientId": "fooClient",
   687      "aadClientSecret": "fooSecret",
   688      "resourceGroup": "bar",
   689      "securityGroupName": "foo-node-nsg",
   690      "securityGroupResourceGroup": "bar",
   691      "location": "bar",
   692      "vmType": "vmss",
   693      "vnetName": "foo-vnet",
   694      "vnetResourceGroup": "bar",
   695      "subnetName": "foo-node-subnet",
   696      "routeTableName": "foo-node-routetable",
   697      "loadBalancerSku": "Standard",
   698      "loadBalancerName": "",
   699      "maximumLoadBalancerRuleCount": 250,
   700      "useManagedIdentityExtension": false,
   701      "useInstanceMetadata": true,
   702      "enableVmssFlexNodes": true,
   703      "cloudProviderRateLimit": true,
   704      "cloudProviderRateLimitQPS": 1.2,
   705      "loadBalancerRateLimit": {
   706          "cloudProviderRateLimitBucket": 10
   707      }
   708  }`
   709  	rateLimitsWorkerNodeCloudConfig = `{
   710      "cloud": "AzurePublicCloud",
   711      "tenantId": "fooTenant",
   712      "subscriptionId": "baz",
   713      "aadClientId": "fooClient",
   714      "aadClientSecret": "fooSecret",
   715      "resourceGroup": "bar",
   716      "securityGroupName": "foo-node-nsg",
   717      "securityGroupResourceGroup": "bar",
   718      "location": "bar",
   719      "vmType": "vmss",
   720      "vnetName": "foo-vnet",
   721      "vnetResourceGroup": "bar",
   722      "subnetName": "foo-node-subnet",
   723      "routeTableName": "foo-node-routetable",
   724      "loadBalancerSku": "Standard",
   725      "loadBalancerName": "",
   726      "maximumLoadBalancerRuleCount": 250,
   727      "useManagedIdentityExtension": false,
   728      "useInstanceMetadata": true,
   729      "enableVmssFlexNodes": true,
   730      "cloudProviderRateLimit": true,
   731      "cloudProviderRateLimitQPS": 1.2,
   732      "loadBalancerRateLimit": {
   733          "cloudProviderRateLimitBucket": 10
   734      }
   735  }`
   736  	backOffCloudConfig = `{
   737      "cloud": "AzurePublicCloud",
   738      "tenantId": "fooTenant",
   739      "subscriptionId": "baz",
   740      "aadClientId": "fooClient",
   741      "aadClientSecret": "fooSecret",
   742      "resourceGroup": "bar",
   743      "securityGroupName": "foo-node-nsg",
   744      "securityGroupResourceGroup": "bar",
   745      "location": "bar",
   746      "vmType": "vmss",
   747      "vnetName": "foo-vnet",
   748      "vnetResourceGroup": "bar",
   749      "subnetName": "foo-node-subnet",
   750      "routeTableName": "foo-node-routetable",
   751      "loadBalancerSku": "Standard",
   752      "loadBalancerName": "",
   753      "maximumLoadBalancerRuleCount": 250,
   754      "useManagedIdentityExtension": false,
   755      "useInstanceMetadata": true,
   756      "enableVmssFlexNodes": true,
   757      "cloudProviderBackoff": true,
   758      "cloudProviderBackoffRetries": 1,
   759      "cloudProviderBackoffExponent": 1.2000000000000002,
   760      "cloudProviderBackoffDuration": 60,
   761      "cloudProviderBackoffJitter": 1.2000000000000002
   762  }`
   763  	vmssCloudConfig = `{
   764      "cloud": "AzurePublicCloud",
   765      "tenantId": "fooTenant",
   766      "subscriptionId": "baz",
   767      "aadClientId": "fooClient",
   768      "aadClientSecret": "fooSecret",
   769      "resourceGroup": "bar",
   770      "securityGroupName": "foo-node-nsg",
   771      "securityGroupResourceGroup": "bar",
   772      "location": "bar",
   773      "vmType": "vmss",
   774      "vnetName": "foo-vnet",
   775      "vnetResourceGroup": "bar",
   776      "subnetName": "foo-node-subnet",
   777      "routeTableName": "foo-node-routetable",
   778      "loadBalancerSku": "Standard",
   779      "loadBalancerName": "",
   780      "maximumLoadBalancerRuleCount": 250,
   781      "useManagedIdentityExtension": false,
   782      "useInstanceMetadata": true,
   783      "enableVmssFlexNodes": true
   784  }`
   785  )
   786  
   787  func Test_clusterIdentityFinalizer(t *testing.T) {
   788  	type args struct {
   789  		prefix           string
   790  		clusterNamespace string
   791  		clusterName      string
   792  	}
   793  	tests := []struct {
   794  		name string
   795  		args args
   796  		want string
   797  	}{
   798  		{
   799  			name: "cluster identity finalizer should be deterministic",
   800  			args: args{
   801  				prefix:           infrav1.ClusterFinalizer,
   802  				clusterNamespace: "foo",
   803  				clusterName:      "bar",
   804  			},
   805  			want: "azurecluster.infrastructure.cluster.x-k8s.io/48998dbcd8fb929369c78981cbfb6f26145ea0412e6e05a1423941a6",
   806  		},
   807  		{
   808  			name: "long cluster name and namespace",
   809  			args: args{
   810  				prefix:           infrav1.ClusterFinalizer,
   811  				clusterNamespace: "this-is-a-very-very-very-very-very-very-very-very-very-long-namespace-name",
   812  				clusterName:      "this-is-a-very-very-very-very-very-very-very-very-very-long-cluster-name",
   813  			},
   814  			want: "azurecluster.infrastructure.cluster.x-k8s.io/557d064144d2b495db694dedc53c9a1e9bd8575bdf06b5b151972614",
   815  		},
   816  	}
   817  	for _, tt := range tests {
   818  		t.Run(tt.name, func(t *testing.T) {
   819  			got := clusterIdentityFinalizer(tt.args.prefix, tt.args.clusterNamespace, tt.args.clusterName)
   820  			if got != tt.want {
   821  				t.Errorf("clusterIdentityFinalizer() = %v, want %v", got, tt.want)
   822  			}
   823  			key := strings.Split(got, "/")[1]
   824  			if len(key) > 63 {
   825  				t.Errorf("clusterIdentityFinalizer() name %v length = %v should be less than 63 characters", key, len(key))
   826  			}
   827  		})
   828  	}
   829  }
   830  
   831  func Test_deprecatedClusterIdentityFinalizer(t *testing.T) {
   832  	type args struct {
   833  		prefix           string
   834  		clusterNamespace string
   835  		clusterName      string
   836  	}
   837  	tests := []struct {
   838  		name string
   839  		args args
   840  		want string
   841  	}{
   842  		{
   843  			name: "cluster identity finalizer should be deterministic",
   844  			args: args{
   845  				prefix:           infrav1.ClusterFinalizer,
   846  				clusterNamespace: "foo",
   847  				clusterName:      "bar",
   848  			},
   849  			want: "azurecluster.infrastructure.cluster.x-k8s.io/foo-bar",
   850  		},
   851  		{
   852  			name: "long cluster name and namespace",
   853  			args: args{
   854  				prefix:           infrav1.ClusterFinalizer,
   855  				clusterNamespace: "this-is-a-very-very-very-very-very-very-very-very-very-long-namespace-name",
   856  				clusterName:      "this-is-a-very-very-very-very-very-very-very-very-very-long-cluster-name",
   857  			},
   858  			want: "azurecluster.infrastructure.cluster.x-k8s.io/this-is-a-very-very-very-very-very-very-very-very-very-long-namespace-name-this-is-a-very-very-very-very-very-very-very-very-very-long-cluster-name",
   859  		},
   860  	}
   861  	for _, tt := range tests {
   862  		t.Run(tt.name, func(t *testing.T) {
   863  			if got := deprecatedClusterIdentityFinalizer(tt.args.prefix, tt.args.clusterNamespace, tt.args.clusterName); got != tt.want {
   864  				t.Errorf("deprecatedClusterIdentityFinalizer() = %v, want %v", got, tt.want)
   865  			}
   866  		})
   867  	}
   868  }
   869  
   870  func TestAzureManagedClusterToAzureManagedMachinePoolsMapper(t *testing.T) {
   871  	g := NewWithT(t)
   872  	scheme, err := newScheme()
   873  	g.Expect(err).NotTo(HaveOccurred())
   874  	initObjects := []runtime.Object{
   875  		newCluster(clusterName),
   876  		// Create two Machines with an infrastructure ref and one without.
   877  		newManagedMachinePoolInfraReference(clusterName, "my-mmp-0"),
   878  		newManagedMachinePoolInfraReference(clusterName, "my-mmp-1"),
   879  		newManagedMachinePoolInfraReference(clusterName, "my-mmp-2"),
   880  		newMachinePool(clusterName, "my-machine-2"),
   881  	}
   882  	fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
   883  
   884  	sink := mock_log.NewMockLogSink(gomock.NewController(t))
   885  	sink.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
   886  	sink.EXPECT().Enabled(4).Return(true)
   887  	sink.EXPECT().WithValues("AzureManagedCluster", "my-cluster", "Namespace", "default").Return(sink)
   888  	sink.EXPECT().Info(4, "gk does not match", "gk", gomock.Any(), "infraGK", gomock.Any())
   889  	mapper, err := AzureManagedClusterToAzureManagedMachinePoolsMapper(context.Background(), fakeClient, scheme, logr.New(sink))
   890  	g.Expect(err).NotTo(HaveOccurred())
   891  
   892  	requests := mapper(context.TODO(), &infrav1.AzureManagedCluster{
   893  		ObjectMeta: metav1.ObjectMeta{
   894  			Name:      clusterName,
   895  			Namespace: "default",
   896  			OwnerReferences: []metav1.OwnerReference{
   897  				{
   898  					Name:       clusterName,
   899  					Kind:       "Cluster",
   900  					APIVersion: clusterv1.GroupVersion.String(),
   901  				},
   902  			},
   903  		},
   904  	})
   905  	g.Expect(requests).To(ConsistOf([]reconcile.Request{
   906  		{
   907  			NamespacedName: types.NamespacedName{
   908  				Name:      "azuremy-mmp-0",
   909  				Namespace: "default",
   910  			},
   911  		},
   912  		{
   913  			NamespacedName: types.NamespacedName{
   914  				Name:      "azuremy-mmp-1",
   915  				Namespace: "default",
   916  			},
   917  		},
   918  		{
   919  			NamespacedName: types.NamespacedName{
   920  				Name:      "azuremy-mmp-2",
   921  				Namespace: "default",
   922  			},
   923  		},
   924  	}))
   925  }
   926  
   927  func TestAzureManagedControlPlaneToAzureManagedMachinePoolsMapper(t *testing.T) {
   928  	g := NewWithT(t)
   929  	scheme, err := newScheme()
   930  	g.Expect(err).NotTo(HaveOccurred())
   931  	cluster := newCluster("my-cluster")
   932  	cluster.Spec.ControlPlaneRef = &corev1.ObjectReference{
   933  		APIVersion: infrav1.GroupVersion.String(),
   934  		Kind:       infrav1.AzureManagedControlPlaneKind,
   935  		Name:       cpName,
   936  		Namespace:  cluster.Namespace,
   937  	}
   938  	initObjects := []runtime.Object{
   939  		cluster,
   940  		newAzureManagedControlPlane(cpName),
   941  		// Create two Machines with an infrastructure ref and one without.
   942  		newManagedMachinePoolInfraReference(clusterName, "my-mmp-0"),
   943  		newManagedMachinePoolInfraReference(clusterName, "my-mmp-1"),
   944  		newManagedMachinePoolInfraReference(clusterName, "my-mmp-2"),
   945  		newMachinePool(clusterName, "my-machine-2"),
   946  	}
   947  	fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
   948  
   949  	sink := mock_log.NewMockLogSink(gomock.NewController(t))
   950  	sink.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
   951  	sink.EXPECT().Enabled(4).Return(true)
   952  	sink.EXPECT().WithValues("AzureManagedControlPlane", cpName, "Namespace", cluster.Namespace).Return(sink)
   953  	sink.EXPECT().Info(4, "gk does not match", "gk", gomock.Any(), "infraGK", gomock.Any())
   954  	mapper, err := AzureManagedControlPlaneToAzureManagedMachinePoolsMapper(context.Background(), fakeClient, scheme, logr.New(sink))
   955  	g.Expect(err).NotTo(HaveOccurred())
   956  
   957  	requests := mapper(context.TODO(), &infrav1.AzureManagedControlPlane{
   958  		ObjectMeta: metav1.ObjectMeta{
   959  			Name:      cpName,
   960  			Namespace: cluster.Namespace,
   961  			OwnerReferences: []metav1.OwnerReference{
   962  				{
   963  					Name:       cluster.Name,
   964  					Kind:       "Cluster",
   965  					APIVersion: clusterv1.GroupVersion.String(),
   966  				},
   967  			},
   968  		},
   969  	})
   970  	g.Expect(requests).To(ConsistOf([]reconcile.Request{
   971  		{
   972  			NamespacedName: types.NamespacedName{
   973  				Name:      "azuremy-mmp-0",
   974  				Namespace: "default",
   975  			},
   976  		},
   977  		{
   978  			NamespacedName: types.NamespacedName{
   979  				Name:      "azuremy-mmp-1",
   980  				Namespace: "default",
   981  			},
   982  		},
   983  		{
   984  			NamespacedName: types.NamespacedName{
   985  				Name:      "azuremy-mmp-2",
   986  				Namespace: "default",
   987  			},
   988  		},
   989  	}))
   990  }
   991  
   992  func TestMachinePoolToAzureManagedControlPlaneMapFuncSuccess(t *testing.T) {
   993  	g := NewWithT(t)
   994  	scheme, err := newScheme()
   995  	g.Expect(err).NotTo(HaveOccurred())
   996  	cluster := newCluster(clusterName)
   997  	controlPlane := newAzureManagedControlPlane(cpName)
   998  	cluster.Spec.ControlPlaneRef = &corev1.ObjectReference{
   999  		APIVersion: infrav1.GroupVersion.String(),
  1000  		Kind:       infrav1.AzureManagedControlPlaneKind,
  1001  		Name:       cpName,
  1002  		Namespace:  cluster.Namespace,
  1003  	}
  1004  
  1005  	managedMachinePool0 := newManagedMachinePoolInfraReference(clusterName, "my-mmp-0")
  1006  	azureManagedMachinePool0 := newAzureManagedMachinePool(clusterName, "azuremy-mmp-0", "System")
  1007  	managedMachinePool0.Spec.ClusterName = clusterName
  1008  
  1009  	managedMachinePool1 := newManagedMachinePoolInfraReference(clusterName, "my-mmp-1")
  1010  	azureManagedMachinePool1 := newAzureManagedMachinePool(clusterName, "azuremy-mmp-1", "User")
  1011  	managedMachinePool1.Spec.ClusterName = clusterName
  1012  
  1013  	initObjects := []runtime.Object{
  1014  		cluster,
  1015  		controlPlane,
  1016  		managedMachinePool0,
  1017  		azureManagedMachinePool0,
  1018  		// Create two Machines with an infrastructure ref and one without.
  1019  		managedMachinePool1,
  1020  		azureManagedMachinePool1,
  1021  	}
  1022  	fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
  1023  
  1024  	sink := mock_log.NewMockLogSink(gomock.NewController(t))
  1025  	sink.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
  1026  	mapper := MachinePoolToAzureManagedControlPlaneMapFunc(context.Background(), fakeClient, infrav1.GroupVersion.WithKind(infrav1.AzureManagedControlPlaneKind), logr.New(sink))
  1027  
  1028  	// system pool should trigger
  1029  	requests := mapper(context.TODO(), newManagedMachinePoolInfraReference(clusterName, "my-mmp-0"))
  1030  	g.Expect(requests).To(ConsistOf([]reconcile.Request{
  1031  		{
  1032  			NamespacedName: types.NamespacedName{
  1033  				Name:      "my-managed-cp",
  1034  				Namespace: "default",
  1035  			},
  1036  		},
  1037  	}))
  1038  
  1039  	// any other pool should not trigger
  1040  	requests = mapper(context.TODO(), newManagedMachinePoolInfraReference(clusterName, "my-mmp-1"))
  1041  	g.Expect(requests).To(BeNil())
  1042  }
  1043  
  1044  func TestMachinePoolToAzureManagedControlPlaneMapFuncFailure(t *testing.T) {
  1045  	g := NewWithT(t)
  1046  	scheme, err := newScheme()
  1047  	g.Expect(err).NotTo(HaveOccurred())
  1048  	cluster := newCluster(clusterName)
  1049  	cluster.Spec.ControlPlaneRef = &corev1.ObjectReference{
  1050  		APIVersion: infrav1.GroupVersion.String(),
  1051  		Kind:       infrav1.AzureManagedControlPlaneKind,
  1052  		Name:       cpName,
  1053  		Namespace:  cluster.Namespace,
  1054  	}
  1055  	managedMachinePool := newManagedMachinePoolInfraReference(clusterName, "my-mmp-0")
  1056  	managedMachinePool.Spec.ClusterName = clusterName
  1057  	initObjects := []runtime.Object{
  1058  		cluster,
  1059  		managedMachinePool,
  1060  		// Create two Machines with an infrastructure ref and one without.
  1061  		newManagedMachinePoolInfraReference(clusterName, "my-mmp-1"),
  1062  	}
  1063  	fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
  1064  
  1065  	sink := mock_log.NewMockLogSink(gomock.NewController(t))
  1066  	sink.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
  1067  	sink.EXPECT().Error(gomock.Any(), "failed to fetch default pool reference")
  1068  	sink.EXPECT().Error(gomock.Any(), "failed to fetch default pool reference") // twice because we are testing two calls
  1069  
  1070  	mapper := MachinePoolToAzureManagedControlPlaneMapFunc(context.Background(), fakeClient, infrav1.GroupVersion.WithKind(infrav1.AzureManagedControlPlaneKind), logr.New(sink))
  1071  
  1072  	// default pool should trigger if owned cluster could not be fetched
  1073  	requests := mapper(context.TODO(), newManagedMachinePoolInfraReference(clusterName, "my-mmp-0"))
  1074  	g.Expect(requests).To(ConsistOf([]reconcile.Request{
  1075  		{
  1076  			NamespacedName: types.NamespacedName{
  1077  				Name:      "my-managed-cp",
  1078  				Namespace: "default",
  1079  			},
  1080  		},
  1081  	}))
  1082  
  1083  	// any other pool should also trigger if owned cluster could not be fetched
  1084  	requests = mapper(context.TODO(), newManagedMachinePoolInfraReference(clusterName, "my-mmp-1"))
  1085  	g.Expect(requests).To(ConsistOf([]reconcile.Request{
  1086  		{
  1087  			NamespacedName: types.NamespacedName{
  1088  				Name:      "my-managed-cp",
  1089  				Namespace: "default",
  1090  			},
  1091  		},
  1092  	}))
  1093  }
  1094  
  1095  func TestAzureManagedClusterToAzureManagedControlPlaneMapper(t *testing.T) {
  1096  	g := NewWithT(t)
  1097  	scheme, err := newScheme()
  1098  	g.Expect(err).NotTo(HaveOccurred())
  1099  	cluster := newCluster("my-cluster")
  1100  	cluster.Spec.ControlPlaneRef = &corev1.ObjectReference{
  1101  		APIVersion: infrav1.GroupVersion.String(),
  1102  		Kind:       infrav1.AzureManagedControlPlaneKind,
  1103  		Name:       cpName,
  1104  		Namespace:  cluster.Namespace,
  1105  	}
  1106  
  1107  	initObjects := []runtime.Object{
  1108  		cluster,
  1109  		newAzureManagedControlPlane(cpName),
  1110  	}
  1111  	fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
  1112  
  1113  	sink := mock_log.NewMockLogSink(gomock.NewController(t))
  1114  	sink.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
  1115  	sink.EXPECT().WithValues("AzureManagedCluster", "az-"+cluster.Name, "Namespace", "default")
  1116  
  1117  	mapper, err := AzureManagedClusterToAzureManagedControlPlaneMapper(context.Background(), fakeClient, logr.New(sink))
  1118  	g.Expect(err).NotTo(HaveOccurred())
  1119  	requests := mapper(context.TODO(), &infrav1.AzureManagedCluster{
  1120  		ObjectMeta: metav1.ObjectMeta{
  1121  			Name:      "az-" + cluster.Name,
  1122  			Namespace: "default",
  1123  			OwnerReferences: []metav1.OwnerReference{
  1124  				{
  1125  					Name:       cluster.Name,
  1126  					Kind:       "Cluster",
  1127  					APIVersion: clusterv1.GroupVersion.String(),
  1128  				},
  1129  			},
  1130  		},
  1131  	})
  1132  	g.Expect(requests).To(HaveLen(1))
  1133  	g.Expect(requests).To(Equal([]reconcile.Request{
  1134  		{
  1135  			NamespacedName: types.NamespacedName{
  1136  				Name:      cpName,
  1137  				Namespace: cluster.Namespace,
  1138  			},
  1139  		},
  1140  	}))
  1141  }
  1142  
  1143  func TestAzureManagedControlPlaneToAzureManagedClusterMapper(t *testing.T) {
  1144  	g := NewWithT(t)
  1145  	scheme, err := newScheme()
  1146  	g.Expect(err).NotTo(HaveOccurred())
  1147  	cluster := newCluster("my-cluster")
  1148  	azManagedCluster := &infrav1.AzureManagedCluster{
  1149  		ObjectMeta: metav1.ObjectMeta{
  1150  			Name:      "az-" + cluster.Name,
  1151  			Namespace: "default",
  1152  			OwnerReferences: []metav1.OwnerReference{
  1153  				{
  1154  					Name:       cluster.Name,
  1155  					Kind:       "Cluster",
  1156  					APIVersion: clusterv1.GroupVersion.String(),
  1157  				},
  1158  			},
  1159  		},
  1160  	}
  1161  
  1162  	cluster.Spec.ControlPlaneRef = &corev1.ObjectReference{
  1163  		APIVersion: infrav1.GroupVersion.String(),
  1164  		Kind:       infrav1.AzureManagedControlPlaneKind,
  1165  		Name:       cpName,
  1166  		Namespace:  cluster.Namespace,
  1167  	}
  1168  	cluster.Spec.InfrastructureRef = &corev1.ObjectReference{
  1169  		APIVersion: infrav1.GroupVersion.String(),
  1170  		Kind:       infrav1.AzureManagedClusterKind,
  1171  		Name:       azManagedCluster.Name,
  1172  		Namespace:  azManagedCluster.Namespace,
  1173  	}
  1174  
  1175  	initObjects := []runtime.Object{
  1176  		cluster,
  1177  		newAzureManagedControlPlane(cpName),
  1178  		azManagedCluster,
  1179  	}
  1180  	fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
  1181  
  1182  	sink := mock_log.NewMockLogSink(gomock.NewController(t))
  1183  	sink.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
  1184  	sink.EXPECT().WithValues("AzureManagedControlPlane", cpName, "Namespace", cluster.Namespace)
  1185  
  1186  	mapper, err := AzureManagedControlPlaneToAzureManagedClusterMapper(context.Background(), fakeClient, logr.New(sink))
  1187  	g.Expect(err).NotTo(HaveOccurred())
  1188  	requests := mapper(context.TODO(), &infrav1.AzureManagedControlPlane{
  1189  		ObjectMeta: metav1.ObjectMeta{
  1190  			Name:      cpName,
  1191  			Namespace: cluster.Namespace,
  1192  			OwnerReferences: []metav1.OwnerReference{
  1193  				{
  1194  					Name:       cluster.Name,
  1195  					Kind:       "Cluster",
  1196  					APIVersion: clusterv1.GroupVersion.String(),
  1197  				},
  1198  			},
  1199  		},
  1200  	})
  1201  	g.Expect(requests).To(HaveLen(1))
  1202  	g.Expect(requests).To(Equal([]reconcile.Request{
  1203  		{
  1204  			NamespacedName: types.NamespacedName{
  1205  				Name:      azManagedCluster.Name,
  1206  				Namespace: azManagedCluster.Namespace,
  1207  			},
  1208  		},
  1209  	}))
  1210  }
  1211  
  1212  func newAzureManagedControlPlane(cpName string) *infrav1.AzureManagedControlPlane {
  1213  	return &infrav1.AzureManagedControlPlane{
  1214  		ObjectMeta: metav1.ObjectMeta{
  1215  			Name:      cpName,
  1216  			Namespace: "default",
  1217  		},
  1218  	}
  1219  }
  1220  
  1221  func newManagedMachinePoolInfraReference(clusterName, poolName string) *expv1.MachinePool {
  1222  	m := newMachinePool(clusterName, poolName)
  1223  	m.Spec.ClusterName = clusterName
  1224  	m.Spec.Template.Spec.InfrastructureRef = corev1.ObjectReference{
  1225  		Kind:       "AzureManagedMachinePool",
  1226  		Namespace:  m.Namespace,
  1227  		Name:       "azure" + poolName,
  1228  		APIVersion: infrav1.GroupVersion.String(),
  1229  	}
  1230  	return m
  1231  }
  1232  
  1233  func newAzureManagedMachinePool(clusterName, poolName, mode string) *infrav1.AzureManagedMachinePool {
  1234  	var cpuManagerPolicyStatic = infrav1.CPUManagerPolicyStatic
  1235  	var topologyManagerPolicy = infrav1.TopologyManagerPolicyBestEffort
  1236  	var transparentHugePageDefragMAdvise = infrav1.TransparentHugePageOptionMadvise
  1237  	var transparentHugePageEnabledAlways = infrav1.TransparentHugePageOptionAlways
  1238  	return &infrav1.AzureManagedMachinePool{
  1239  		ObjectMeta: metav1.ObjectMeta{
  1240  			Labels: map[string]string{
  1241  				clusterv1.ClusterNameLabel: clusterName,
  1242  			},
  1243  			Name:      poolName,
  1244  			Namespace: "default",
  1245  		},
  1246  		Spec: infrav1.AzureManagedMachinePoolSpec{
  1247  			AzureManagedMachinePoolClassSpec: infrav1.AzureManagedMachinePoolClassSpec{
  1248  				Mode:         mode,
  1249  				SKU:          "Standard_B2s",
  1250  				OSDiskSizeGB: ptr.To(512),
  1251  				KubeletConfig: &infrav1.KubeletConfig{
  1252  					CPUManagerPolicy:      &cpuManagerPolicyStatic,
  1253  					TopologyManagerPolicy: &topologyManagerPolicy,
  1254  				},
  1255  				LinuxOSConfig: &infrav1.LinuxOSConfig{
  1256  					TransparentHugePageDefrag:  &transparentHugePageDefragMAdvise,
  1257  					TransparentHugePageEnabled: &transparentHugePageEnabledAlways,
  1258  				},
  1259  			},
  1260  		},
  1261  	}
  1262  }
  1263  
  1264  func newMachinePool(clusterName, poolName string) *expv1.MachinePool {
  1265  	return &expv1.MachinePool{
  1266  		ObjectMeta: metav1.ObjectMeta{
  1267  			Labels: map[string]string{
  1268  				clusterv1.ClusterNameLabel: clusterName,
  1269  			},
  1270  			Name:      poolName,
  1271  			Namespace: "default",
  1272  		},
  1273  		Spec: expv1.MachinePoolSpec{
  1274  			Replicas: ptr.To[int32](2),
  1275  		},
  1276  	}
  1277  }
  1278  
  1279  func newManagedMachinePoolWithInfrastructureRef(clusterName, poolName string) *expv1.MachinePool {
  1280  	m := newMachinePool(clusterName, poolName)
  1281  	m.Spec.Template.Spec.InfrastructureRef = corev1.ObjectReference{
  1282  		Kind:       "AzureManagedMachinePool",
  1283  		Namespace:  m.Namespace,
  1284  		Name:       "azure" + poolName,
  1285  		APIVersion: infrav1.GroupVersion.String(),
  1286  	}
  1287  	return m
  1288  }
  1289  
  1290  func Test_ManagedMachinePoolToInfrastructureMapFunc(t *testing.T) {
  1291  	cases := []struct {
  1292  		Name             string
  1293  		Setup            func(logMock *mock_log.MockLogSink)
  1294  		MapObjectFactory func(*GomegaWithT) client.Object
  1295  		Expect           func(*GomegaWithT, []reconcile.Request)
  1296  	}{
  1297  		{
  1298  			Name: "MachinePoolToAzureManagedMachinePool",
  1299  			MapObjectFactory: func(g *GomegaWithT) client.Object {
  1300  				return newManagedMachinePoolWithInfrastructureRef("azureManagedCluster", "ManagedMachinePool")
  1301  			},
  1302  			Setup: func(logMock *mock_log.MockLogSink) {
  1303  				logMock.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
  1304  			},
  1305  			Expect: func(g *GomegaWithT, reqs []reconcile.Request) {
  1306  				g.Expect(reqs).To(HaveLen(1))
  1307  				g.Expect(reqs[0]).To(Equal(reconcile.Request{
  1308  					NamespacedName: types.NamespacedName{
  1309  						Name:      "azureManagedMachinePool",
  1310  						Namespace: "default",
  1311  					},
  1312  				}))
  1313  			},
  1314  		},
  1315  		{
  1316  			Name: "MachinePoolWithoutMatchingInfraRef",
  1317  			MapObjectFactory: func(g *GomegaWithT) client.Object {
  1318  				return newMachinePool("azureManagedCluster", "machinePool")
  1319  			},
  1320  			Setup: func(logMock *mock_log.MockLogSink) {
  1321  				ampGK := infrav1.GroupVersion.WithKind("AzureManagedMachinePool").GroupKind()
  1322  				logMock.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
  1323  				logMock.EXPECT().Enabled(4).Return(true)
  1324  				logMock.EXPECT().Info(4, "gk does not match", "gk", ampGK, "infraGK", gomock.Any())
  1325  			},
  1326  			Expect: func(g *GomegaWithT, reqs []reconcile.Request) {
  1327  				g.Expect(reqs).To(BeEmpty())
  1328  			},
  1329  		},
  1330  		{
  1331  			Name: "NotAMachinePool",
  1332  			MapObjectFactory: func(g *GomegaWithT) client.Object {
  1333  				return newCluster("azureManagedCluster")
  1334  			},
  1335  			Setup: func(logMock *mock_log.MockLogSink) {
  1336  				logMock.EXPECT().Init(logr.RuntimeInfo{CallDepth: 1})
  1337  				logMock.EXPECT().Enabled(4).Return(true)
  1338  				logMock.EXPECT().Info(4, "attempt to map incorrect type", "type", "*v1beta1.Cluster")
  1339  			},
  1340  			Expect: func(g *GomegaWithT, reqs []reconcile.Request) {
  1341  				g.Expect(reqs).To(BeEmpty())
  1342  			},
  1343  		},
  1344  	}
  1345  
  1346  	for _, c := range cases {
  1347  		c := c
  1348  		t.Run(c.Name, func(t *testing.T) {
  1349  			g := NewWithT(t)
  1350  
  1351  			mockCtrl := gomock.NewController(t)
  1352  			defer mockCtrl.Finish()
  1353  
  1354  			sink := mock_log.NewMockLogSink(mockCtrl)
  1355  			if c.Setup != nil {
  1356  				c.Setup(sink)
  1357  			}
  1358  			f := MachinePoolToInfrastructureMapFunc(infrav1.GroupVersion.WithKind("AzureManagedMachinePool"), logr.New(sink))
  1359  			reqs := f(context.TODO(), c.MapObjectFactory(g))
  1360  			c.Expect(g, reqs)
  1361  		})
  1362  	}
  1363  }
  1364  
  1365  func TestClusterPauseChangeAndInfrastructureReady(t *testing.T) {
  1366  	tests := []struct {
  1367  		name   string
  1368  		event  any // an event.(Create|Update)Event
  1369  		expect bool
  1370  	}{
  1371  		{
  1372  			name: "create cluster infra not ready, not paused",
  1373  			event: event.CreateEvent{
  1374  				Object: &clusterv1.Cluster{
  1375  					Spec: clusterv1.ClusterSpec{
  1376  						Paused: false,
  1377  					},
  1378  					Status: clusterv1.ClusterStatus{
  1379  						InfrastructureReady: false,
  1380  					},
  1381  				},
  1382  			},
  1383  			expect: false,
  1384  		},
  1385  		{
  1386  			name: "create cluster infra ready, not paused",
  1387  			event: event.CreateEvent{
  1388  				Object: &clusterv1.Cluster{
  1389  					Spec: clusterv1.ClusterSpec{
  1390  						Paused: false,
  1391  					},
  1392  					Status: clusterv1.ClusterStatus{
  1393  						InfrastructureReady: true,
  1394  					},
  1395  				},
  1396  			},
  1397  			expect: true,
  1398  		},
  1399  		{
  1400  			name: "create cluster infra not ready, paused",
  1401  			event: event.CreateEvent{
  1402  				Object: &clusterv1.Cluster{
  1403  					Spec: clusterv1.ClusterSpec{
  1404  						Paused: true,
  1405  					},
  1406  					Status: clusterv1.ClusterStatus{
  1407  						InfrastructureReady: false,
  1408  					},
  1409  				},
  1410  			},
  1411  			expect: false,
  1412  		},
  1413  		{
  1414  			name: "create cluster infra ready, paused",
  1415  			event: event.CreateEvent{
  1416  				Object: &clusterv1.Cluster{
  1417  					Spec: clusterv1.ClusterSpec{
  1418  						Paused: true,
  1419  					},
  1420  					Status: clusterv1.ClusterStatus{
  1421  						InfrastructureReady: true,
  1422  					},
  1423  				},
  1424  			},
  1425  			expect: true,
  1426  		},
  1427  		{
  1428  			name: "update cluster infra ready true->true",
  1429  			event: event.UpdateEvent{
  1430  				ObjectOld: &clusterv1.Cluster{
  1431  					Status: clusterv1.ClusterStatus{
  1432  						InfrastructureReady: true,
  1433  					},
  1434  				},
  1435  				ObjectNew: &clusterv1.Cluster{
  1436  					Status: clusterv1.ClusterStatus{
  1437  						InfrastructureReady: true,
  1438  					},
  1439  				},
  1440  			},
  1441  			expect: false,
  1442  		},
  1443  		{
  1444  			name: "update cluster infra ready false->true",
  1445  			event: event.UpdateEvent{
  1446  				ObjectOld: &clusterv1.Cluster{
  1447  					Status: clusterv1.ClusterStatus{
  1448  						InfrastructureReady: false,
  1449  					},
  1450  				},
  1451  				ObjectNew: &clusterv1.Cluster{
  1452  					Status: clusterv1.ClusterStatus{
  1453  						InfrastructureReady: true,
  1454  					},
  1455  				},
  1456  			},
  1457  			expect: true,
  1458  		},
  1459  		{
  1460  			name: "update cluster infra ready true->false",
  1461  			event: event.UpdateEvent{
  1462  				ObjectOld: &clusterv1.Cluster{
  1463  					Status: clusterv1.ClusterStatus{
  1464  						InfrastructureReady: true,
  1465  					},
  1466  				},
  1467  				ObjectNew: &clusterv1.Cluster{
  1468  					Status: clusterv1.ClusterStatus{
  1469  						InfrastructureReady: false,
  1470  					},
  1471  				},
  1472  			},
  1473  			expect: false,
  1474  		},
  1475  		{
  1476  			name: "update cluster infra ready false->false",
  1477  			event: event.UpdateEvent{
  1478  				ObjectOld: &clusterv1.Cluster{
  1479  					Status: clusterv1.ClusterStatus{
  1480  						InfrastructureReady: false,
  1481  					},
  1482  				},
  1483  				ObjectNew: &clusterv1.Cluster{
  1484  					Status: clusterv1.ClusterStatus{
  1485  						InfrastructureReady: false,
  1486  					},
  1487  				},
  1488  			},
  1489  			expect: false,
  1490  		},
  1491  		{
  1492  			name: "update cluster paused false->false",
  1493  			event: event.UpdateEvent{
  1494  				ObjectOld: &clusterv1.Cluster{
  1495  					Spec: clusterv1.ClusterSpec{
  1496  						Paused: false,
  1497  					},
  1498  				},
  1499  				ObjectNew: &clusterv1.Cluster{
  1500  					Spec: clusterv1.ClusterSpec{
  1501  						Paused: false,
  1502  					},
  1503  				},
  1504  			},
  1505  			expect: false,
  1506  		},
  1507  		{
  1508  			name: "update cluster paused false->true",
  1509  			event: event.UpdateEvent{
  1510  				ObjectOld: &clusterv1.Cluster{
  1511  					Spec: clusterv1.ClusterSpec{
  1512  						Paused: false,
  1513  					},
  1514  				},
  1515  				ObjectNew: &clusterv1.Cluster{
  1516  					Spec: clusterv1.ClusterSpec{
  1517  						Paused: true,
  1518  					},
  1519  				},
  1520  			},
  1521  			expect: true,
  1522  		},
  1523  		{
  1524  			name: "update cluster paused true->false",
  1525  			event: event.UpdateEvent{
  1526  				ObjectOld: &clusterv1.Cluster{
  1527  					Spec: clusterv1.ClusterSpec{
  1528  						Paused: true,
  1529  					},
  1530  				},
  1531  				ObjectNew: &clusterv1.Cluster{
  1532  					Spec: clusterv1.ClusterSpec{
  1533  						Paused: false,
  1534  					},
  1535  				},
  1536  			},
  1537  			expect: true,
  1538  		},
  1539  		{
  1540  			name: "update cluster paused true->true",
  1541  			event: event.UpdateEvent{
  1542  				ObjectOld: &clusterv1.Cluster{
  1543  					Spec: clusterv1.ClusterSpec{
  1544  						Paused: true,
  1545  					},
  1546  				},
  1547  				ObjectNew: &clusterv1.Cluster{
  1548  					Spec: clusterv1.ClusterSpec{
  1549  						Paused: true,
  1550  					},
  1551  				},
  1552  			},
  1553  			expect: false,
  1554  		},
  1555  	}
  1556  
  1557  	for _, test := range tests {
  1558  		test := test
  1559  		t.Run(test.name, func(t *testing.T) {
  1560  			t.Parallel()
  1561  			p := ClusterPauseChangeAndInfrastructureReady(logr.New(nil))
  1562  			var actual bool
  1563  			switch e := test.event.(type) {
  1564  			case event.CreateEvent:
  1565  				actual = p.Create(e)
  1566  			case event.UpdateEvent:
  1567  				actual = p.Update(e)
  1568  			default:
  1569  				panic("unimplemented event type")
  1570  			}
  1571  			NewGomegaWithT(t).Expect(actual).To(Equal(test.expect))
  1572  		})
  1573  	}
  1574  }
  1575  
  1576  func TestAddBlockMoveAnnotation(t *testing.T) {
  1577  	tests := []struct {
  1578  		name                string
  1579  		annotations         map[string]string
  1580  		expectedAnnotations map[string]string
  1581  		expected            bool
  1582  	}{
  1583  		{
  1584  			name:                "annotation does not exist",
  1585  			annotations:         nil,
  1586  			expectedAnnotations: map[string]string{clusterctlv1.BlockMoveAnnotation: "true"},
  1587  			expected:            true,
  1588  		},
  1589  		{
  1590  			name:                "annotation already exists",
  1591  			annotations:         map[string]string{clusterctlv1.BlockMoveAnnotation: "this value might be different but it doesn't matter"},
  1592  			expectedAnnotations: map[string]string{clusterctlv1.BlockMoveAnnotation: "this value might be different but it doesn't matter"},
  1593  			expected:            false,
  1594  		},
  1595  	}
  1596  
  1597  	for _, test := range tests {
  1598  		t.Run(test.name, func(t *testing.T) {
  1599  			obj := &metav1.ObjectMeta{
  1600  				Annotations: test.annotations,
  1601  			}
  1602  			actual := AddBlockMoveAnnotation(obj)
  1603  			if test.expected != actual {
  1604  				t.Errorf("expected %v, got %v", test.expected, actual)
  1605  			}
  1606  			if !maps.Equal(test.expectedAnnotations, obj.GetAnnotations()) {
  1607  				t.Errorf("expected %v, got %v", test.expectedAnnotations, obj.GetAnnotations())
  1608  			}
  1609  		})
  1610  	}
  1611  }
  1612  
  1613  func TestRemoveBlockMoveAnnotation(t *testing.T) {
  1614  	tests := []struct {
  1615  		name        string
  1616  		annotations map[string]string
  1617  		expected    map[string]string
  1618  	}{
  1619  		{
  1620  			name:        "nil",
  1621  			annotations: nil,
  1622  			expected:    nil,
  1623  		},
  1624  		{
  1625  			name:        "annotation not present",
  1626  			annotations: map[string]string{"another": "annotation"},
  1627  			expected:    map[string]string{"another": "annotation"},
  1628  		},
  1629  		{
  1630  			name: "annotation present",
  1631  			annotations: map[string]string{
  1632  				clusterctlv1.BlockMoveAnnotation: "any value",
  1633  				"another":                        "annotation",
  1634  			},
  1635  			expected: map[string]string{
  1636  				"another": "annotation",
  1637  			},
  1638  		},
  1639  	}
  1640  
  1641  	for _, test := range tests {
  1642  		t.Run(test.name, func(t *testing.T) {
  1643  			obj := &metav1.ObjectMeta{
  1644  				Annotations: maps.Clone(test.annotations),
  1645  			}
  1646  			RemoveBlockMoveAnnotation(obj)
  1647  			actual := obj.GetAnnotations()
  1648  			if !maps.Equal(test.expected, actual) {
  1649  				t.Errorf("expected %v, got %v", test.expected, actual)
  1650  			}
  1651  		})
  1652  	}
  1653  }