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 }