sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/clusterclass/clusterclass_controller_test.go (about) 1 /* 2 Copyright 2021 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 clusterclass 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 "testing" 24 "time" 25 26 "github.com/google/go-cmp/cmp" 27 . "github.com/onsi/gomega" 28 "github.com/pkg/errors" 29 corev1 "k8s.io/api/core/v1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/types" 32 utilfeature "k8s.io/component-base/featuregate/testing" 33 "k8s.io/utils/pointer" 34 "sigs.k8s.io/controller-runtime/pkg/client" 35 "sigs.k8s.io/controller-runtime/pkg/client/fake" 36 "sigs.k8s.io/controller-runtime/pkg/reconcile" 37 38 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 39 runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" 40 runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog" 41 runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" 42 "sigs.k8s.io/cluster-api/feature" 43 tlog "sigs.k8s.io/cluster-api/internal/log" 44 fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake" 45 "sigs.k8s.io/cluster-api/internal/test/builder" 46 ) 47 48 func TestClusterClassReconciler_reconcile(t *testing.T) { 49 g := NewWithT(t) 50 timeout := 30 * time.Second 51 52 ns, err := env.CreateNamespace(ctx, "test-topology-clusterclass-reconciler") 53 g.Expect(err).ToNot(HaveOccurred()) 54 55 clusterClassName := "class1" 56 workerClassName1 := "linux-worker-1" 57 workerClassName2 := "linux-worker-2" 58 59 // The below objects are created in order to feed the reconcile loop all the information it needs to create a 60 // full tree of ClusterClass objects (the objects should have owner references to the ClusterClass). 61 62 // Bootstrap templates for the workers. 63 bootstrapTemplate := builder.BootstrapTemplate(ns.Name, "bootstraptemplate").Build() 64 65 // InfraMachineTemplates for the workers and the control plane. 66 infraMachineTemplateControlPlane := builder.InfrastructureMachineTemplate(ns.Name, "inframachinetemplate-control-plane").Build() 67 infraMachineTemplateWorker := builder.InfrastructureMachineTemplate(ns.Name, "inframachinetemplate-worker").Build() 68 69 // Control plane template. 70 controlPlaneTemplate := builder.ControlPlaneTemplate(ns.Name, "controlplanetemplate").Build() 71 72 // InfraClusterTemplate. 73 infraClusterTemplate := builder.InfrastructureClusterTemplate(ns.Name, "infraclustertemplate").Build() 74 75 // MachineDeploymentClasses that will be part of the ClusterClass. 76 machineDeploymentClass1 := builder.MachineDeploymentClass(workerClassName1). 77 WithBootstrapTemplate(bootstrapTemplate). 78 WithInfrastructureTemplate(infraMachineTemplateWorker). 79 Build() 80 machineDeploymentClass2 := builder.MachineDeploymentClass(workerClassName2). 81 WithBootstrapTemplate(bootstrapTemplate). 82 WithInfrastructureTemplate(infraMachineTemplateWorker). 83 Build() 84 85 // ClusterClass. 86 clusterClass := builder.ClusterClass(ns.Name, clusterClassName). 87 WithInfrastructureClusterTemplate(infraClusterTemplate). 88 WithControlPlaneTemplate(controlPlaneTemplate). 89 WithControlPlaneInfrastructureMachineTemplate(infraMachineTemplateControlPlane). 90 WithWorkerMachineDeploymentClasses(*machineDeploymentClass1, *machineDeploymentClass2). 91 WithVariables( 92 clusterv1.ClusterClassVariable{ 93 Name: "hdd", 94 Required: true, 95 Schema: clusterv1.VariableSchema{ 96 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 97 Type: "string", 98 }, 99 }, 100 }, 101 clusterv1.ClusterClassVariable{ 102 Name: "cpu", 103 Schema: clusterv1.VariableSchema{ 104 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 105 Type: "integer", 106 }, 107 }, 108 }). 109 Build() 110 111 // Create the set of initObjects from the objects above to add to the API server when the test environment starts. 112 initObjs := []client.Object{ 113 bootstrapTemplate, 114 infraMachineTemplateWorker, 115 infraMachineTemplateControlPlane, 116 controlPlaneTemplate, 117 infraClusterTemplate, 118 clusterClass, 119 } 120 121 for _, obj := range initObjs { 122 g.Expect(env.Create(ctx, obj)).To(Succeed()) 123 } 124 defer func() { 125 for _, obj := range initObjs { 126 g.Expect(env.Delete(ctx, obj)).To(Succeed()) 127 } 128 }() 129 130 g.Eventually(func(g Gomega) error { 131 actualClusterClass := &clusterv1.ClusterClass{} 132 g.Expect(env.Get(ctx, client.ObjectKey{Name: clusterClassName, Namespace: ns.Name}, actualClusterClass)).To(Succeed()) 133 134 g.Expect(assertInfrastructureClusterTemplate(ctx, actualClusterClass, ns)).Should(Succeed()) 135 136 g.Expect(assertControlPlaneTemplate(ctx, actualClusterClass, ns)).Should(Succeed()) 137 138 g.Expect(assertMachineDeploymentClasses(ctx, actualClusterClass, ns)).Should(Succeed()) 139 140 g.Expect(assertStatusVariables(actualClusterClass)).Should(Succeed()) 141 return nil 142 }, timeout).Should(Succeed()) 143 } 144 145 func assertStatusVariables(actualClusterClass *clusterv1.ClusterClass) error { 146 // Assert that each inline variable definition has been exposed in the ClusterClass status. 147 for _, specVar := range actualClusterClass.Spec.Variables { 148 var found bool 149 for _, statusVar := range actualClusterClass.Status.Variables { 150 if specVar.Name != statusVar.Name { 151 continue 152 } 153 found = true 154 if statusVar.DefinitionsConflict { 155 return errors.Errorf("ClusterClass status %s variable DefinitionsConflict does not match. Expected %v , got %v", specVar.Name, false, statusVar.DefinitionsConflict) 156 } 157 if len(statusVar.Definitions) != 1 { 158 return errors.Errorf("ClusterClass status has multiple definitions for variable %s. Expected a single definition", specVar.Name) 159 } 160 // For this test assume there is only one status variable definition, and that it should match the spec. 161 statusVarDefinition := statusVar.Definitions[0] 162 if statusVarDefinition.From != clusterv1.VariableDefinitionFromInline { 163 return errors.Errorf("ClusterClass status variable %s from field does not match. Expected %s. Got %s", statusVar.Name, clusterv1.VariableDefinitionFromInline, statusVarDefinition.From) 164 } 165 if specVar.Required != statusVarDefinition.Required { 166 return errors.Errorf("ClusterClass status variable %s required field does not match. Expecte %v. Got %v", specVar.Name, statusVarDefinition.Required, statusVarDefinition.Required) 167 } 168 if !reflect.DeepEqual(specVar.Schema, statusVarDefinition.Schema) { 169 return errors.Errorf("ClusterClass status variable %s schema does not match. Expected %v. Got %v", specVar.Name, specVar.Schema, statusVarDefinition.Schema) 170 } 171 } 172 if !found { 173 return errors.Errorf("ClusterClass does not have status for variable %s", specVar.Name) 174 } 175 } 176 return nil 177 } 178 func assertInfrastructureClusterTemplate(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, ns *corev1.Namespace) error { 179 // Assert the infrastructure cluster template has the correct owner reference. 180 actualInfraClusterTemplate := builder.InfrastructureClusterTemplate("", "").Build() 181 actualInfraClusterTemplateKey := client.ObjectKey{ 182 Namespace: ns.Name, 183 Name: actualClusterClass.Spec.Infrastructure.Ref.Name, 184 } 185 if err := env.Get(ctx, actualInfraClusterTemplateKey, actualInfraClusterTemplate); err != nil { 186 return err 187 } 188 if err := assertHasOwnerReference(actualInfraClusterTemplate, *ownerReferenceTo(actualClusterClass)); err != nil { 189 return err 190 } 191 192 // Assert the ClusterClass has the expected APIVersion and Kind of to the infrastructure cluster template 193 return referenceExistsWithCorrectKindAndAPIVersion(actualClusterClass.Spec.Infrastructure.Ref, 194 builder.GenericInfrastructureClusterTemplateKind, 195 builder.InfrastructureGroupVersion) 196 } 197 198 func assertControlPlaneTemplate(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, ns *corev1.Namespace) error { 199 // Assert the control plane template has the correct owner reference. 200 actualControlPlaneTemplate := builder.ControlPlaneTemplate("", "").Build() 201 actualControlPlaneTemplateKey := client.ObjectKey{ 202 Namespace: ns.Name, 203 Name: actualClusterClass.Spec.ControlPlane.Ref.Name, 204 } 205 if err := env.Get(ctx, actualControlPlaneTemplateKey, actualControlPlaneTemplate); err != nil { 206 return err 207 } 208 if err := assertHasOwnerReference(actualControlPlaneTemplate, *ownerReferenceTo(actualClusterClass)); err != nil { 209 return err 210 } 211 212 // Assert the ClusterClass has the expected APIVersion and Kind to the control plane template 213 if err := referenceExistsWithCorrectKindAndAPIVersion(actualClusterClass.Spec.ControlPlane.Ref, 214 builder.GenericControlPlaneTemplateKind, 215 builder.ControlPlaneGroupVersion); err != nil { 216 return err 217 } 218 219 // If the control plane has machine infra assert that the infra machine template has the correct owner reference. 220 if actualClusterClass.Spec.ControlPlane.MachineInfrastructure != nil && actualClusterClass.Spec.ControlPlane.MachineInfrastructure.Ref != nil { 221 actualInfrastructureMachineTemplate := builder.InfrastructureMachineTemplate("", "").Build() 222 actualInfrastructureMachineTemplateKey := client.ObjectKey{ 223 Namespace: ns.Name, 224 Name: actualClusterClass.Spec.ControlPlane.MachineInfrastructure.Ref.Name, 225 } 226 if err := env.Get(ctx, actualInfrastructureMachineTemplateKey, actualInfrastructureMachineTemplate); err != nil { 227 return err 228 } 229 if err := assertHasOwnerReference(actualInfrastructureMachineTemplate, *ownerReferenceTo(actualClusterClass)); err != nil { 230 return err 231 } 232 233 // Assert the ClusterClass has the expected APIVersion and Kind to the infrastructure machine template 234 if err := referenceExistsWithCorrectKindAndAPIVersion(actualClusterClass.Spec.ControlPlane.MachineInfrastructure.Ref, 235 builder.GenericInfrastructureMachineTemplateKind, 236 builder.InfrastructureGroupVersion); err != nil { 237 return err 238 } 239 } 240 241 return nil 242 } 243 244 func assertMachineDeploymentClasses(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, ns *corev1.Namespace) error { 245 for _, mdClass := range actualClusterClass.Spec.Workers.MachineDeployments { 246 if err := assertMachineDeploymentClass(ctx, actualClusterClass, mdClass, ns); err != nil { 247 return err 248 } 249 } 250 return nil 251 } 252 253 func assertMachineDeploymentClass(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, mdClass clusterv1.MachineDeploymentClass, ns *corev1.Namespace) error { 254 // Assert the infrastructure machine template in the MachineDeploymentClass has an owner reference to the ClusterClass. 255 actualInfrastructureMachineTemplate := builder.InfrastructureMachineTemplate("", "").Build() 256 actualInfrastructureMachineTemplateKey := client.ObjectKey{ 257 Namespace: ns.Name, 258 Name: mdClass.Template.Infrastructure.Ref.Name, 259 } 260 if err := env.Get(ctx, actualInfrastructureMachineTemplateKey, actualInfrastructureMachineTemplate); err != nil { 261 return err 262 } 263 if err := assertHasOwnerReference(actualInfrastructureMachineTemplate, *ownerReferenceTo(actualClusterClass)); err != nil { 264 return err 265 } 266 267 // Assert the MachineDeploymentClass has the expected APIVersion and Kind to the infrastructure machine template 268 if err := referenceExistsWithCorrectKindAndAPIVersion(mdClass.Template.Infrastructure.Ref, 269 builder.GenericInfrastructureMachineTemplateKind, 270 builder.InfrastructureGroupVersion); err != nil { 271 return err 272 } 273 274 // Assert the bootstrap template in the MachineDeploymentClass has an owner reference to the ClusterClass. 275 actualBootstrapTemplate := builder.BootstrapTemplate("", "").Build() 276 actualBootstrapTemplateKey := client.ObjectKey{ 277 Namespace: ns.Name, 278 Name: mdClass.Template.Bootstrap.Ref.Name, 279 } 280 if err := env.Get(ctx, actualBootstrapTemplateKey, actualBootstrapTemplate); err != nil { 281 return err 282 } 283 if err := assertHasOwnerReference(actualBootstrapTemplate, *ownerReferenceTo(actualClusterClass)); err != nil { 284 return err 285 } 286 287 // Assert the MachineDeploymentClass has the expected APIVersion and Kind to the bootstrap template 288 return referenceExistsWithCorrectKindAndAPIVersion(mdClass.Template.Bootstrap.Ref, 289 builder.GenericBootstrapConfigTemplateKind, 290 builder.BootstrapGroupVersion) 291 } 292 293 func assertHasOwnerReference(obj client.Object, ownerRef metav1.OwnerReference) error { 294 found := false 295 for _, ref := range obj.GetOwnerReferences() { 296 if isOwnerReferenceEqual(ref, ownerRef) { 297 found = true 298 break 299 } 300 } 301 if !found { 302 return fmt.Errorf("object %s does not have OwnerReference %s", tlog.KObj{Obj: obj}, &ownerRef) 303 } 304 return nil 305 } 306 307 func isOwnerReferenceEqual(a, b metav1.OwnerReference) bool { 308 if a.APIVersion != b.APIVersion { 309 return false 310 } 311 if a.Kind != b.Kind { 312 return false 313 } 314 if a.Name != b.Name { 315 return false 316 } 317 if a.UID != b.UID { 318 return false 319 } 320 return true 321 } 322 323 func TestReconciler_reconcileVariables(t *testing.T) { 324 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, true)() 325 326 g := NewWithT(t) 327 catalog := runtimecatalog.New() 328 _ = runtimehooksv1.AddToCatalog(catalog) 329 330 clusterClassWithInlineVariables := builder.ClusterClass(metav1.NamespaceDefault, "class1"). 331 WithVariables( 332 []clusterv1.ClusterClassVariable{ 333 { 334 Name: "cpu", 335 Schema: clusterv1.VariableSchema{ 336 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 337 Type: "integer", 338 }, 339 }, 340 }, 341 { 342 Name: "memory", 343 Schema: clusterv1.VariableSchema{ 344 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 345 Type: "string", 346 }, 347 }, 348 }, 349 }..., 350 ) 351 tests := []struct { 352 name string 353 clusterClass *clusterv1.ClusterClass 354 want []clusterv1.ClusterClassStatusVariable 355 patchResponse *runtimehooksv1.DiscoverVariablesResponse 356 wantErr bool 357 }{ 358 { 359 name: "Reconcile inline variables to ClusterClass status", 360 clusterClass: clusterClassWithInlineVariables.DeepCopy().Build(), 361 want: []clusterv1.ClusterClassStatusVariable{ 362 { 363 Name: "cpu", 364 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 365 { 366 From: clusterv1.VariableDefinitionFromInline, 367 Schema: clusterv1.VariableSchema{ 368 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 369 Type: "integer", 370 }, 371 }, 372 }, 373 }, 374 }, 375 { 376 Name: "memory", 377 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 378 { 379 From: clusterv1.VariableDefinitionFromInline, 380 Schema: clusterv1.VariableSchema{ 381 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 382 Type: "string", 383 }, 384 }, 385 }, 386 }, 387 }, 388 }, 389 }, 390 { 391 name: "Reconcile variables from inline and external variables to ClusterClass status", 392 clusterClass: clusterClassWithInlineVariables.DeepCopy().WithPatches( 393 []clusterv1.ClusterClassPatch{ 394 { 395 Name: "patch1", 396 External: &clusterv1.ExternalPatchDefinition{ 397 DiscoverVariablesExtension: pointer.String("variables-one"), 398 }}}). 399 Build(), 400 patchResponse: &runtimehooksv1.DiscoverVariablesResponse{ 401 CommonResponse: runtimehooksv1.CommonResponse{ 402 Status: runtimehooksv1.ResponseStatusSuccess, 403 }, 404 Variables: []clusterv1.ClusterClassVariable{ 405 { 406 Name: "cpu", 407 Schema: clusterv1.VariableSchema{ 408 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 409 Type: "string", 410 }, 411 }, 412 }, 413 { 414 Name: "memory", 415 Schema: clusterv1.VariableSchema{ 416 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 417 Type: "string", 418 }, 419 }, 420 }, 421 { 422 Name: "location", 423 Schema: clusterv1.VariableSchema{ 424 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 425 Type: "string", 426 }, 427 }, 428 }, 429 }, 430 }, 431 want: []clusterv1.ClusterClassStatusVariable{ 432 { 433 Name: "cpu", 434 DefinitionsConflict: true, 435 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 436 { 437 From: clusterv1.VariableDefinitionFromInline, 438 Schema: clusterv1.VariableSchema{ 439 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 440 Type: "integer", 441 }, 442 }, 443 }, 444 { 445 From: "patch1", 446 Schema: clusterv1.VariableSchema{ 447 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 448 Type: "string", 449 }, 450 }, 451 }, 452 }, 453 }, 454 { 455 Name: "location", 456 DefinitionsConflict: false, 457 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 458 { 459 From: "patch1", 460 Schema: clusterv1.VariableSchema{ 461 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 462 Type: "string", 463 }, 464 }, 465 }, 466 }, 467 }, 468 { 469 Name: "memory", 470 DefinitionsConflict: false, 471 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 472 { 473 From: clusterv1.VariableDefinitionFromInline, 474 Schema: clusterv1.VariableSchema{ 475 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 476 Type: "string", 477 }, 478 }, 479 }, 480 { 481 From: "patch1", 482 Schema: clusterv1.VariableSchema{ 483 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 484 Type: "string", 485 }, 486 }, 487 }, 488 }, 489 }, 490 }, 491 }, 492 { 493 name: "Error if external patch defines a variable with same name multiple times", 494 wantErr: true, 495 clusterClass: clusterClassWithInlineVariables.DeepCopy().WithPatches( 496 []clusterv1.ClusterClassPatch{ 497 { 498 Name: "patch1", 499 External: &clusterv1.ExternalPatchDefinition{ 500 DiscoverVariablesExtension: pointer.String("variables-one"), 501 }}}). 502 Build(), 503 patchResponse: &runtimehooksv1.DiscoverVariablesResponse{ 504 CommonResponse: runtimehooksv1.CommonResponse{ 505 Status: runtimehooksv1.ResponseStatusSuccess, 506 }, 507 Variables: []clusterv1.ClusterClassVariable{ 508 { 509 Name: "cpu", 510 Schema: clusterv1.VariableSchema{ 511 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 512 Type: "string", 513 }, 514 }, 515 }, 516 { 517 Name: "cpu", 518 Schema: clusterv1.VariableSchema{ 519 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 520 Type: "integer", 521 }, 522 }, 523 }, 524 }, 525 }, 526 }, 527 } 528 for _, tt := range tests { 529 t.Run(tt.name, func(t *testing.T) { 530 fakeRuntimeClient := fakeruntimeclient.NewRuntimeClientBuilder(). 531 WithCallExtensionResponses( 532 map[string]runtimehooksv1.ResponseObject{ 533 "variables-one": tt.patchResponse, 534 }). 535 WithCatalog(catalog). 536 Build() 537 538 r := &Reconciler{ 539 RuntimeClient: fakeRuntimeClient, 540 } 541 542 err := r.reconcileVariables(ctx, tt.clusterClass) 543 if tt.wantErr { 544 g.Expect(err).To(HaveOccurred()) 545 return 546 } 547 g.Expect(err).ToNot(HaveOccurred()) 548 g.Expect(tt.clusterClass.Status.Variables).To(BeComparableTo(tt.want), cmp.Diff(tt.clusterClass.Status.Variables, tt.want)) 549 }) 550 } 551 } 552 553 func TestReconciler_extensionConfigToClusterClass(t *testing.T) { 554 firstExtConfig := &runtimev1.ExtensionConfig{ 555 ObjectMeta: metav1.ObjectMeta{ 556 Name: "runtime1", 557 }, 558 TypeMeta: metav1.TypeMeta{ 559 Kind: "ExtensionConfig", 560 APIVersion: runtimev1.GroupVersion.String(), 561 }, 562 Spec: runtimev1.ExtensionConfigSpec{ 563 NamespaceSelector: &metav1.LabelSelector{}, 564 }, 565 } 566 secondExtConfig := &runtimev1.ExtensionConfig{ 567 ObjectMeta: metav1.ObjectMeta{ 568 Name: "runtime2", 569 }, 570 TypeMeta: metav1.TypeMeta{ 571 Kind: "ExtensionConfig", 572 APIVersion: runtimev1.GroupVersion.String(), 573 }, 574 Spec: runtimev1.ExtensionConfigSpec{ 575 NamespaceSelector: &metav1.LabelSelector{}, 576 }, 577 } 578 579 // These ClusterClasses will be reconciled as they both reference the passed ExtensionConfig `runtime1`. 580 onePatchClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc1"). 581 WithPatches([]clusterv1.ClusterClassPatch{ 582 {External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.runtime1")}}}). 583 Build() 584 twoPatchClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc2"). 585 WithPatches([]clusterv1.ClusterClassPatch{ 586 {External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.runtime1")}}, 587 {External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.runtime2")}}}). 588 Build() 589 590 // This ClusterClasses will not be reconciled as it does not reference the passed ExtensionConfig `runtime1`. 591 notReconciledClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc3"). 592 WithPatches([]clusterv1.ClusterClassPatch{ 593 {External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.other-runtime-class")}}}). 594 Build() 595 596 t.Run("test", func(t *testing.T) { 597 fakeClient := fake.NewClientBuilder().WithObjects(onePatchClusterClass, notReconciledClusterClass, twoPatchClusterClass).Build() 598 r := &Reconciler{ 599 Client: fakeClient, 600 } 601 602 // Expect both onePatchClusterClass and twoPatchClusterClass to trigger a reconcile as both reference ExtensionCopnfig `runtime1`. 603 firstExtConfigExpected := []reconcile.Request{ 604 {NamespacedName: types.NamespacedName{Namespace: onePatchClusterClass.Namespace, Name: onePatchClusterClass.Name}}, 605 {NamespacedName: types.NamespacedName{Namespace: twoPatchClusterClass.Namespace, Name: twoPatchClusterClass.Name}}, 606 } 607 if got := r.extensionConfigToClusterClass(context.Background(), firstExtConfig); !reflect.DeepEqual(got, firstExtConfigExpected) { 608 t.Errorf("extensionConfigToClusterClass() = %v, want %v", got, firstExtConfigExpected) 609 } 610 611 // Expect only twoPatchClusterClass to trigger a reconcile as it's the only class with a reference to ExtensionCopnfig `runtime2`. 612 secondExtConfigExpected := []reconcile.Request{ 613 {NamespacedName: types.NamespacedName{Namespace: twoPatchClusterClass.Namespace, Name: twoPatchClusterClass.Name}}, 614 } 615 if got := r.extensionConfigToClusterClass(context.Background(), secondExtConfig); !reflect.DeepEqual(got, secondExtConfigExpected) { 616 t.Errorf("extensionConfigToClusterClass() = %v, want %v", got, secondExtConfigExpected) 617 } 618 }) 619 }