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  }