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  }