sigs.k8s.io/cluster-api@v1.7.1/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/ptr" 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 infraMachinePoolTemplateWorker := builder.InfrastructureMachinePoolTemplate(ns.Name, "inframachinepooltemplate-worker").Build() 69 70 // Control plane template. 71 controlPlaneTemplate := builder.ControlPlaneTemplate(ns.Name, "controlplanetemplate").Build() 72 73 // InfraClusterTemplate. 74 infraClusterTemplate := builder.InfrastructureClusterTemplate(ns.Name, "infraclustertemplate").Build() 75 76 // MachineDeploymentClasses that will be part of the ClusterClass. 77 machineDeploymentClass1 := builder.MachineDeploymentClass(workerClassName1). 78 WithBootstrapTemplate(bootstrapTemplate). 79 WithInfrastructureTemplate(infraMachineTemplateWorker). 80 Build() 81 machineDeploymentClass2 := builder.MachineDeploymentClass(workerClassName2). 82 WithBootstrapTemplate(bootstrapTemplate). 83 WithInfrastructureTemplate(infraMachineTemplateWorker). 84 Build() 85 86 // MachinePoolClasses that will be part of the ClusterClass. 87 machinePoolClass1 := builder.MachinePoolClass(workerClassName1). 88 WithBootstrapTemplate(bootstrapTemplate). 89 WithInfrastructureTemplate(infraMachinePoolTemplateWorker). 90 Build() 91 machinePoolClass2 := builder.MachinePoolClass(workerClassName2). 92 WithBootstrapTemplate(bootstrapTemplate). 93 WithInfrastructureTemplate(infraMachinePoolTemplateWorker). 94 Build() 95 96 // ClusterClass. 97 clusterClass := builder.ClusterClass(ns.Name, clusterClassName). 98 WithInfrastructureClusterTemplate(infraClusterTemplate). 99 WithControlPlaneTemplate(controlPlaneTemplate). 100 WithControlPlaneInfrastructureMachineTemplate(infraMachineTemplateControlPlane). 101 WithWorkerMachineDeploymentClasses(*machineDeploymentClass1, *machineDeploymentClass2). 102 WithWorkerMachinePoolClasses(*machinePoolClass1, *machinePoolClass2). 103 WithVariables( 104 clusterv1.ClusterClassVariable{ 105 Name: "hdd", 106 Required: true, 107 Schema: clusterv1.VariableSchema{ 108 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 109 Type: "string", 110 }, 111 }, 112 }, 113 clusterv1.ClusterClassVariable{ 114 Name: "cpu", 115 Schema: clusterv1.VariableSchema{ 116 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 117 Type: "integer", 118 }, 119 }, 120 Metadata: clusterv1.ClusterClassVariableMetadata{ 121 Labels: map[string]string{ 122 "some-label": "some-label-value", 123 }, 124 Annotations: map[string]string{ 125 "some-annotation": "some-annotation-value", 126 }, 127 }, 128 }). 129 Build() 130 131 // Create the set of initObjects from the objects above to add to the API server when the test environment starts. 132 initObjs := []client.Object{ 133 bootstrapTemplate, 134 infraMachineTemplateWorker, 135 infraMachinePoolTemplateWorker, 136 infraMachineTemplateControlPlane, 137 controlPlaneTemplate, 138 infraClusterTemplate, 139 clusterClass, 140 } 141 142 for _, obj := range initObjs { 143 g.Expect(env.CreateAndWait(ctx, obj)).To(Succeed()) 144 } 145 defer func() { 146 for _, obj := range initObjs { 147 g.Expect(env.Delete(ctx, obj)).To(Succeed()) 148 } 149 }() 150 151 g.Eventually(func(g Gomega) error { 152 actualClusterClass := &clusterv1.ClusterClass{} 153 g.Expect(env.Get(ctx, client.ObjectKey{Name: clusterClassName, Namespace: ns.Name}, actualClusterClass)).To(Succeed()) 154 155 g.Expect(assertInfrastructureClusterTemplate(ctx, actualClusterClass, ns)).Should(Succeed()) 156 157 g.Expect(assertControlPlaneTemplate(ctx, actualClusterClass, ns)).Should(Succeed()) 158 159 g.Expect(assertMachineDeploymentClasses(ctx, actualClusterClass, ns)).Should(Succeed()) 160 161 g.Expect(assertMachinePoolClasses(ctx, actualClusterClass, ns)).Should(Succeed()) 162 163 g.Expect(assertStatusVariables(actualClusterClass)).Should(Succeed()) 164 return nil 165 }, timeout).Should(Succeed()) 166 } 167 168 func assertStatusVariables(actualClusterClass *clusterv1.ClusterClass) error { 169 // Assert that each inline variable definition has been exposed in the ClusterClass status. 170 for _, specVar := range actualClusterClass.Spec.Variables { 171 var found bool 172 for _, statusVar := range actualClusterClass.Status.Variables { 173 if specVar.Name != statusVar.Name { 174 continue 175 } 176 found = true 177 if statusVar.DefinitionsConflict { 178 return errors.Errorf("ClusterClass status %s variable DefinitionsConflict does not match. Expected %v , got %v", specVar.Name, false, statusVar.DefinitionsConflict) 179 } 180 if len(statusVar.Definitions) != 1 { 181 return errors.Errorf("ClusterClass status has multiple definitions for variable %s. Expected a single definition", specVar.Name) 182 } 183 // For this test assume there is only one status variable definition, and that it should match the spec. 184 statusVarDefinition := statusVar.Definitions[0] 185 if statusVarDefinition.From != clusterv1.VariableDefinitionFromInline { 186 return errors.Errorf("ClusterClass status variable %s from field does not match. Expected %s. Got %s", statusVar.Name, clusterv1.VariableDefinitionFromInline, statusVarDefinition.From) 187 } 188 if specVar.Required != statusVarDefinition.Required { 189 return errors.Errorf("ClusterClass status variable %s required field does not match. Expecte %v. Got %v", specVar.Name, statusVarDefinition.Required, statusVarDefinition.Required) 190 } 191 if !reflect.DeepEqual(specVar.Schema, statusVarDefinition.Schema) { 192 return errors.Errorf("ClusterClass status variable %s schema does not match. Expected %v. Got %v", specVar.Name, specVar.Schema, statusVarDefinition.Schema) 193 } 194 if !reflect.DeepEqual(specVar.Metadata, statusVarDefinition.Metadata) { 195 return errors.Errorf("ClusterClass status variable %s metadata does not match. Expected %v. Got %v", specVar.Name, specVar.Metadata, statusVarDefinition.Metadata) 196 } 197 } 198 if !found { 199 return errors.Errorf("ClusterClass does not have status for variable %s", specVar.Name) 200 } 201 } 202 return nil 203 } 204 func assertInfrastructureClusterTemplate(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, ns *corev1.Namespace) error { 205 // Assert the infrastructure cluster template has the correct owner reference. 206 actualInfraClusterTemplate := builder.InfrastructureClusterTemplate("", "").Build() 207 actualInfraClusterTemplateKey := client.ObjectKey{ 208 Namespace: ns.Name, 209 Name: actualClusterClass.Spec.Infrastructure.Ref.Name, 210 } 211 if err := env.Get(ctx, actualInfraClusterTemplateKey, actualInfraClusterTemplate); err != nil { 212 return err 213 } 214 if err := assertHasOwnerReference(actualInfraClusterTemplate, *ownerReferenceTo(actualClusterClass, clusterv1.GroupVersion.WithKind("ClusterClass"))); err != nil { 215 return err 216 } 217 218 // Assert the ClusterClass has the expected APIVersion and Kind of to the infrastructure cluster template 219 return referenceExistsWithCorrectKindAndAPIVersion(actualClusterClass.Spec.Infrastructure.Ref, 220 builder.GenericInfrastructureClusterTemplateKind, 221 builder.InfrastructureGroupVersion) 222 } 223 224 func assertControlPlaneTemplate(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, ns *corev1.Namespace) error { 225 // Assert the control plane template has the correct owner reference. 226 actualControlPlaneTemplate := builder.ControlPlaneTemplate("", "").Build() 227 actualControlPlaneTemplateKey := client.ObjectKey{ 228 Namespace: ns.Name, 229 Name: actualClusterClass.Spec.ControlPlane.Ref.Name, 230 } 231 if err := env.Get(ctx, actualControlPlaneTemplateKey, actualControlPlaneTemplate); err != nil { 232 return err 233 } 234 if err := assertHasOwnerReference(actualControlPlaneTemplate, *ownerReferenceTo(actualClusterClass, clusterv1.GroupVersion.WithKind("ClusterClass"))); err != nil { 235 return err 236 } 237 238 // Assert the ClusterClass has the expected APIVersion and Kind to the control plane template 239 if err := referenceExistsWithCorrectKindAndAPIVersion(actualClusterClass.Spec.ControlPlane.Ref, 240 builder.GenericControlPlaneTemplateKind, 241 builder.ControlPlaneGroupVersion); err != nil { 242 return err 243 } 244 245 // If the control plane has machine infra assert that the infra machine template has the correct owner reference. 246 if actualClusterClass.Spec.ControlPlane.MachineInfrastructure != nil && actualClusterClass.Spec.ControlPlane.MachineInfrastructure.Ref != nil { 247 actualInfrastructureMachineTemplate := builder.InfrastructureMachineTemplate("", "").Build() 248 actualInfrastructureMachineTemplateKey := client.ObjectKey{ 249 Namespace: ns.Name, 250 Name: actualClusterClass.Spec.ControlPlane.MachineInfrastructure.Ref.Name, 251 } 252 if err := env.Get(ctx, actualInfrastructureMachineTemplateKey, actualInfrastructureMachineTemplate); err != nil { 253 return err 254 } 255 if err := assertHasOwnerReference(actualInfrastructureMachineTemplate, *ownerReferenceTo(actualClusterClass, clusterv1.GroupVersion.WithKind("ClusterClass"))); err != nil { 256 return err 257 } 258 259 // Assert the ClusterClass has the expected APIVersion and Kind to the infrastructure machine template 260 if err := referenceExistsWithCorrectKindAndAPIVersion(actualClusterClass.Spec.ControlPlane.MachineInfrastructure.Ref, 261 builder.GenericInfrastructureMachineTemplateKind, 262 builder.InfrastructureGroupVersion); err != nil { 263 return err 264 } 265 } 266 267 return nil 268 } 269 270 func assertMachineDeploymentClasses(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, ns *corev1.Namespace) error { 271 for _, mdClass := range actualClusterClass.Spec.Workers.MachineDeployments { 272 if err := assertMachineDeploymentClass(ctx, actualClusterClass, mdClass, ns); err != nil { 273 return err 274 } 275 } 276 return nil 277 } 278 279 func assertMachineDeploymentClass(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, mdClass clusterv1.MachineDeploymentClass, ns *corev1.Namespace) error { 280 // Assert the infrastructure machine template in the MachineDeploymentClass has an owner reference to the ClusterClass. 281 actualInfrastructureMachineTemplate := builder.InfrastructureMachineTemplate("", "").Build() 282 actualInfrastructureMachineTemplateKey := client.ObjectKey{ 283 Namespace: ns.Name, 284 Name: mdClass.Template.Infrastructure.Ref.Name, 285 } 286 if err := env.Get(ctx, actualInfrastructureMachineTemplateKey, actualInfrastructureMachineTemplate); err != nil { 287 return err 288 } 289 if err := assertHasOwnerReference(actualInfrastructureMachineTemplate, *ownerReferenceTo(actualClusterClass, clusterv1.GroupVersion.WithKind("ClusterClass"))); err != nil { 290 return err 291 } 292 293 // Assert the MachineDeploymentClass has the expected APIVersion and Kind to the infrastructure machine template 294 if err := referenceExistsWithCorrectKindAndAPIVersion(mdClass.Template.Infrastructure.Ref, 295 builder.GenericInfrastructureMachineTemplateKind, 296 builder.InfrastructureGroupVersion); err != nil { 297 return err 298 } 299 300 // Assert the bootstrap template in the MachineDeploymentClass has an owner reference to the ClusterClass. 301 actualBootstrapTemplate := builder.BootstrapTemplate("", "").Build() 302 actualBootstrapTemplateKey := client.ObjectKey{ 303 Namespace: ns.Name, 304 Name: mdClass.Template.Bootstrap.Ref.Name, 305 } 306 if err := env.Get(ctx, actualBootstrapTemplateKey, actualBootstrapTemplate); err != nil { 307 return err 308 } 309 if err := assertHasOwnerReference(actualBootstrapTemplate, *ownerReferenceTo(actualClusterClass, clusterv1.GroupVersion.WithKind("ClusterClass"))); err != nil { 310 return err 311 } 312 313 // Assert the MachineDeploymentClass has the expected APIVersion and Kind to the bootstrap template 314 return referenceExistsWithCorrectKindAndAPIVersion(mdClass.Template.Bootstrap.Ref, 315 builder.GenericBootstrapConfigTemplateKind, 316 builder.BootstrapGroupVersion) 317 } 318 319 func assertMachinePoolClasses(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, ns *corev1.Namespace) error { 320 for _, mpClass := range actualClusterClass.Spec.Workers.MachinePools { 321 if err := assertMachinePoolClass(ctx, actualClusterClass, mpClass, ns); err != nil { 322 return err 323 } 324 } 325 return nil 326 } 327 328 func assertMachinePoolClass(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, mpClass clusterv1.MachinePoolClass, ns *corev1.Namespace) error { 329 // Assert the infrastructure machinepool template in the MachinePoolClass has an owner reference to the ClusterClass. 330 actualInfrastructureMachinePoolTemplate := builder.InfrastructureMachinePoolTemplate("", "").Build() 331 actualInfrastructureMachinePoolTemplateKey := client.ObjectKey{ 332 Namespace: ns.Name, 333 Name: mpClass.Template.Infrastructure.Ref.Name, 334 } 335 if err := env.Get(ctx, actualInfrastructureMachinePoolTemplateKey, actualInfrastructureMachinePoolTemplate); err != nil { 336 return err 337 } 338 if err := assertHasOwnerReference(actualInfrastructureMachinePoolTemplate, *ownerReferenceTo(actualClusterClass, clusterv1.GroupVersion.WithKind("ClusterClass"))); err != nil { 339 return err 340 } 341 342 // Assert the MachinePoolClass has the expected APIVersion and Kind to the infrastructure machinepool template 343 if err := referenceExistsWithCorrectKindAndAPIVersion(mpClass.Template.Infrastructure.Ref, 344 builder.GenericInfrastructureMachinePoolTemplateKind, 345 builder.InfrastructureGroupVersion); err != nil { 346 return err 347 } 348 349 // Assert the bootstrap template in the MachinePoolClass has an owner reference to the ClusterClass. 350 actualBootstrapTemplate := builder.BootstrapTemplate("", "").Build() 351 actualBootstrapTemplateKey := client.ObjectKey{ 352 Namespace: ns.Name, 353 Name: mpClass.Template.Bootstrap.Ref.Name, 354 } 355 if err := env.Get(ctx, actualBootstrapTemplateKey, actualBootstrapTemplate); err != nil { 356 return err 357 } 358 if err := assertHasOwnerReference(actualBootstrapTemplate, *ownerReferenceTo(actualClusterClass, clusterv1.GroupVersion.WithKind("ClusterClass"))); err != nil { 359 return err 360 } 361 362 // Assert the MachinePoolClass has the expected APIVersion and Kind to the bootstrap template 363 return referenceExistsWithCorrectKindAndAPIVersion(mpClass.Template.Bootstrap.Ref, 364 builder.GenericBootstrapConfigTemplateKind, 365 builder.BootstrapGroupVersion) 366 } 367 368 func assertHasOwnerReference(obj client.Object, ownerRef metav1.OwnerReference) error { 369 found := false 370 for _, ref := range obj.GetOwnerReferences() { 371 if isOwnerReferenceEqual(ref, ownerRef) { 372 found = true 373 break 374 } 375 } 376 if !found { 377 return fmt.Errorf("object %s does not have OwnerReference %s", tlog.KObj{Obj: obj}, &ownerRef) 378 } 379 return nil 380 } 381 382 func isOwnerReferenceEqual(a, b metav1.OwnerReference) bool { 383 if a.APIVersion != b.APIVersion { 384 return false 385 } 386 if a.Kind != b.Kind { 387 return false 388 } 389 if a.Name != b.Name { 390 return false 391 } 392 if a.UID != b.UID { 393 return false 394 } 395 return true 396 } 397 398 func TestReconciler_reconcileVariables(t *testing.T) { 399 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, true)() 400 401 catalog := runtimecatalog.New() 402 _ = runtimehooksv1.AddToCatalog(catalog) 403 404 clusterClassWithInlineVariables := builder.ClusterClass(metav1.NamespaceDefault, "class1"). 405 WithVariables( 406 []clusterv1.ClusterClassVariable{ 407 { 408 Name: "cpu", 409 Schema: clusterv1.VariableSchema{ 410 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 411 Type: "integer", 412 }, 413 }, 414 Metadata: clusterv1.ClusterClassVariableMetadata{ 415 Labels: map[string]string{ 416 "some-label": "some-label-value", 417 }, 418 Annotations: map[string]string{ 419 "some-annotation": "some-annotation-value", 420 }, 421 }, 422 }, 423 { 424 Name: "memory", 425 Schema: clusterv1.VariableSchema{ 426 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 427 Type: "string", 428 }, 429 }, 430 }, 431 }..., 432 ) 433 tests := []struct { 434 name string 435 clusterClass *clusterv1.ClusterClass 436 want []clusterv1.ClusterClassStatusVariable 437 patchResponse *runtimehooksv1.DiscoverVariablesResponse 438 wantErr bool 439 }{ 440 { 441 name: "Reconcile inline variables to ClusterClass status", 442 clusterClass: clusterClassWithInlineVariables.DeepCopy().Build(), 443 want: []clusterv1.ClusterClassStatusVariable{ 444 { 445 Name: "cpu", 446 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 447 { 448 From: clusterv1.VariableDefinitionFromInline, 449 Schema: clusterv1.VariableSchema{ 450 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 451 Type: "integer", 452 }, 453 }, 454 Metadata: clusterv1.ClusterClassVariableMetadata{ 455 Labels: map[string]string{ 456 "some-label": "some-label-value", 457 }, 458 Annotations: map[string]string{ 459 "some-annotation": "some-annotation-value", 460 }, 461 }, 462 }, 463 }, 464 }, 465 { 466 Name: "memory", 467 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 468 { 469 From: clusterv1.VariableDefinitionFromInline, 470 Schema: clusterv1.VariableSchema{ 471 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 472 Type: "string", 473 }, 474 }, 475 }, 476 }, 477 }, 478 }, 479 }, 480 { 481 name: "Reconcile variables from inline and external variables to ClusterClass status", 482 clusterClass: clusterClassWithInlineVariables.DeepCopy().WithPatches( 483 []clusterv1.ClusterClassPatch{ 484 { 485 Name: "patch1", 486 External: &clusterv1.ExternalPatchDefinition{ 487 DiscoverVariablesExtension: ptr.To("variables-one"), 488 }}}). 489 Build(), 490 patchResponse: &runtimehooksv1.DiscoverVariablesResponse{ 491 CommonResponse: runtimehooksv1.CommonResponse{ 492 Status: runtimehooksv1.ResponseStatusSuccess, 493 }, 494 Variables: []clusterv1.ClusterClassVariable{ 495 { 496 Name: "cpu", 497 Schema: clusterv1.VariableSchema{ 498 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 499 Type: "string", 500 }, 501 }, 502 }, 503 { 504 Name: "memory", 505 Schema: clusterv1.VariableSchema{ 506 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 507 Type: "string", 508 }, 509 }, 510 }, 511 { 512 Name: "location", 513 Schema: clusterv1.VariableSchema{ 514 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 515 Type: "string", 516 }, 517 }, 518 Metadata: clusterv1.ClusterClassVariableMetadata{ 519 Labels: map[string]string{ 520 "some-label": "some-label-value", 521 }, 522 Annotations: map[string]string{ 523 "some-annotation": "some-annotation-value", 524 }, 525 }, 526 }, 527 }, 528 }, 529 want: []clusterv1.ClusterClassStatusVariable{ 530 { 531 Name: "cpu", 532 DefinitionsConflict: true, 533 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 534 { 535 From: clusterv1.VariableDefinitionFromInline, 536 Schema: clusterv1.VariableSchema{ 537 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 538 Type: "integer", 539 }, 540 }, 541 Metadata: clusterv1.ClusterClassVariableMetadata{ 542 Labels: map[string]string{ 543 "some-label": "some-label-value", 544 }, 545 Annotations: map[string]string{ 546 "some-annotation": "some-annotation-value", 547 }, 548 }, 549 }, 550 { 551 From: "patch1", 552 Schema: clusterv1.VariableSchema{ 553 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 554 Type: "string", 555 }, 556 }, 557 }, 558 }, 559 }, 560 { 561 Name: "location", 562 DefinitionsConflict: false, 563 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 564 { 565 From: "patch1", 566 Schema: clusterv1.VariableSchema{ 567 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 568 Type: "string", 569 }, 570 }, 571 Metadata: clusterv1.ClusterClassVariableMetadata{ 572 Labels: map[string]string{ 573 "some-label": "some-label-value", 574 }, 575 Annotations: map[string]string{ 576 "some-annotation": "some-annotation-value", 577 }, 578 }, 579 }, 580 }, 581 }, 582 { 583 Name: "memory", 584 DefinitionsConflict: false, 585 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 586 { 587 From: clusterv1.VariableDefinitionFromInline, 588 Schema: clusterv1.VariableSchema{ 589 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 590 Type: "string", 591 }, 592 }, 593 }, 594 { 595 From: "patch1", 596 Schema: clusterv1.VariableSchema{ 597 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 598 Type: "string", 599 }, 600 }, 601 }, 602 }, 603 }, 604 }, 605 }, 606 { 607 name: "Error if external patch defines a variable with same name multiple times", 608 wantErr: true, 609 clusterClass: clusterClassWithInlineVariables.DeepCopy().WithPatches( 610 []clusterv1.ClusterClassPatch{ 611 { 612 Name: "patch1", 613 External: &clusterv1.ExternalPatchDefinition{ 614 DiscoverVariablesExtension: ptr.To("variables-one"), 615 }}}). 616 Build(), 617 patchResponse: &runtimehooksv1.DiscoverVariablesResponse{ 618 CommonResponse: runtimehooksv1.CommonResponse{ 619 Status: runtimehooksv1.ResponseStatusSuccess, 620 }, 621 Variables: []clusterv1.ClusterClassVariable{ 622 { 623 Name: "cpu", 624 Schema: clusterv1.VariableSchema{ 625 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 626 Type: "string", 627 }, 628 }, 629 }, 630 { 631 Name: "cpu", 632 Schema: clusterv1.VariableSchema{ 633 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 634 Type: "integer", 635 }, 636 }, 637 }, 638 }, 639 }, 640 }, 641 } 642 for _, tt := range tests { 643 t.Run(tt.name, func(t *testing.T) { 644 g := NewWithT(t) 645 fakeRuntimeClient := fakeruntimeclient.NewRuntimeClientBuilder(). 646 WithCallExtensionResponses( 647 map[string]runtimehooksv1.ResponseObject{ 648 "variables-one": tt.patchResponse, 649 }). 650 WithCatalog(catalog). 651 Build() 652 653 r := &Reconciler{ 654 RuntimeClient: fakeRuntimeClient, 655 } 656 657 err := r.reconcileVariables(ctx, tt.clusterClass) 658 if tt.wantErr { 659 g.Expect(err).To(HaveOccurred()) 660 return 661 } 662 g.Expect(err).ToNot(HaveOccurred()) 663 g.Expect(tt.clusterClass.Status.Variables).To(BeComparableTo(tt.want), cmp.Diff(tt.clusterClass.Status.Variables, tt.want)) 664 }) 665 } 666 } 667 668 func TestReconciler_extensionConfigToClusterClass(t *testing.T) { 669 firstExtConfig := &runtimev1.ExtensionConfig{ 670 ObjectMeta: metav1.ObjectMeta{ 671 Name: "runtime1", 672 }, 673 TypeMeta: metav1.TypeMeta{ 674 Kind: "ExtensionConfig", 675 APIVersion: runtimev1.GroupVersion.String(), 676 }, 677 Spec: runtimev1.ExtensionConfigSpec{ 678 NamespaceSelector: &metav1.LabelSelector{}, 679 }, 680 } 681 secondExtConfig := &runtimev1.ExtensionConfig{ 682 ObjectMeta: metav1.ObjectMeta{ 683 Name: "runtime2", 684 }, 685 TypeMeta: metav1.TypeMeta{ 686 Kind: "ExtensionConfig", 687 APIVersion: runtimev1.GroupVersion.String(), 688 }, 689 Spec: runtimev1.ExtensionConfigSpec{ 690 NamespaceSelector: &metav1.LabelSelector{}, 691 }, 692 } 693 694 // These ClusterClasses will be reconciled as they both reference the passed ExtensionConfig `runtime1`. 695 onePatchClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc1"). 696 WithPatches([]clusterv1.ClusterClassPatch{ 697 {External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: ptr.To("discover-variables.runtime1")}}}). 698 Build() 699 twoPatchClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc2"). 700 WithPatches([]clusterv1.ClusterClassPatch{ 701 {External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: ptr.To("discover-variables.runtime1")}}, 702 {External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: ptr.To("discover-variables.runtime2")}}}). 703 Build() 704 705 // This ClusterClasses will not be reconciled as it does not reference the passed ExtensionConfig `runtime1`. 706 notReconciledClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc3"). 707 WithPatches([]clusterv1.ClusterClassPatch{ 708 {External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: ptr.To("discover-variables.other-runtime-class")}}}). 709 Build() 710 711 t.Run("test", func(t *testing.T) { 712 fakeClient := fake.NewClientBuilder().WithObjects(onePatchClusterClass, notReconciledClusterClass, twoPatchClusterClass).Build() 713 r := &Reconciler{ 714 Client: fakeClient, 715 } 716 717 // Expect both onePatchClusterClass and twoPatchClusterClass to trigger a reconcile as both reference ExtensionCopnfig `runtime1`. 718 firstExtConfigExpected := []reconcile.Request{ 719 {NamespacedName: types.NamespacedName{Namespace: onePatchClusterClass.Namespace, Name: onePatchClusterClass.Name}}, 720 {NamespacedName: types.NamespacedName{Namespace: twoPatchClusterClass.Namespace, Name: twoPatchClusterClass.Name}}, 721 } 722 if got := r.extensionConfigToClusterClass(context.Background(), firstExtConfig); !reflect.DeepEqual(got, firstExtConfigExpected) { 723 t.Errorf("extensionConfigToClusterClass() = %v, want %v", got, firstExtConfigExpected) 724 } 725 726 // Expect only twoPatchClusterClass to trigger a reconcile as it's the only class with a reference to ExtensionCopnfig `runtime2`. 727 secondExtConfigExpected := []reconcile.Request{ 728 {NamespacedName: types.NamespacedName{Namespace: twoPatchClusterClass.Namespace, Name: twoPatchClusterClass.Name}}, 729 } 730 if got := r.extensionConfigToClusterClass(context.Background(), secondExtConfig); !reflect.DeepEqual(got, secondExtConfigExpected) { 731 t.Errorf("extensionConfigToClusterClass() = %v, want %v", got, secondExtConfigExpected) 732 } 733 }) 734 }