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