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