sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/topology.go (about)

     1  /*
     2  Copyright 2022 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 cluster
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"strings"
    24  
    25  	jsonpatch "github.com/evanphx/json-patch/v5"
    26  	"github.com/pkg/errors"
    27  	corev1 "k8s.io/api/core/v1"
    28  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/apimachinery/pkg/util/sets"
    35  	"k8s.io/component-base/featuregate"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    38  	crwebhook "sigs.k8s.io/controller-runtime/pkg/webhook"
    39  
    40  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    41  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster/internal/dryrun"
    42  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
    43  	logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
    44  	"sigs.k8s.io/cluster-api/feature"
    45  	clusterclasscontroller "sigs.k8s.io/cluster-api/internal/controllers/clusterclass"
    46  	clustertopologycontroller "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster"
    47  	"sigs.k8s.io/cluster-api/internal/webhooks"
    48  	"sigs.k8s.io/cluster-api/util/contract"
    49  )
    50  
    51  const (
    52  	maxClusterPerInput        = 1
    53  	maxClusterClassesPerInput = 1
    54  )
    55  
    56  // TopologyClient has methods to work with ClusterClass and ManagedTopologies.
    57  type TopologyClient interface {
    58  	Plan(ctx context.Context, in *TopologyPlanInput) (*TopologyPlanOutput, error)
    59  }
    60  
    61  // topologyClient implements TopologyClient.
    62  type topologyClient struct {
    63  	proxy           Proxy
    64  	inventoryClient InventoryClient
    65  }
    66  
    67  // ensure topologyClient implements TopologyClient.
    68  var _ TopologyClient = &topologyClient{}
    69  
    70  // newTopologyClient returns a TopologyClient.
    71  func newTopologyClient(proxy Proxy, inventoryClient InventoryClient) TopologyClient {
    72  	return &topologyClient{
    73  		proxy:           proxy,
    74  		inventoryClient: inventoryClient,
    75  	}
    76  }
    77  
    78  // TopologyPlanInput defines the input for the Plan function.
    79  type TopologyPlanInput struct {
    80  	Objs              []*unstructured.Unstructured
    81  	TargetClusterName string
    82  	TargetNamespace   string
    83  }
    84  
    85  // PatchSummary defined the patch observed on an object.
    86  type PatchSummary = dryrun.PatchSummary
    87  
    88  // ChangeSummary defines all the changes detected by the plan operation.
    89  type ChangeSummary = dryrun.ChangeSummary
    90  
    91  // TopologyPlanOutput defines the output of the Plan function.
    92  type TopologyPlanOutput struct {
    93  	// Clusters is the list clusters affected by the input.
    94  	Clusters []client.ObjectKey
    95  	// ClusterClasses is the list of clusters affected by the input.
    96  	ClusterClasses []client.ObjectKey
    97  	// ReconciledCluster is the cluster on which the topology reconciler loop is executed.
    98  	// If there is only one affected cluster then it becomes the ReconciledCluster. If not,
    99  	// the ReconciledCluster is chosen using additional information in the TopologyPlanInput.
   100  	// ReconciledCluster can be empty if no single target cluster is provided.
   101  	ReconciledCluster *client.ObjectKey
   102  	// ChangeSummary is the full list of changes (objects created, modified and deleted) observed
   103  	// on the ReconciledCluster. ChangeSummary is empty if ReconciledCluster is empty.
   104  	*ChangeSummary
   105  }
   106  
   107  // Plan performs a dry run execution of the topology reconciler using the given inputs.
   108  // It returns a summary of the changes observed during the execution.
   109  func (t *topologyClient) Plan(ctx context.Context, in *TopologyPlanInput) (*TopologyPlanOutput, error) {
   110  	log := logf.Log
   111  
   112  	// Make sure the inputs are valid.
   113  	if err := t.validateInput(in); err != nil {
   114  		return nil, errors.Wrap(err, "input failed validation")
   115  	}
   116  
   117  	// If there is a reachable apiserver with CAPI installed fetch a client for the server.
   118  	// This client will be used as a fall back client when looking for objects that are not
   119  	// in the input.
   120  	// Example: This client will be used to fetch the underlying ClusterClass when the input
   121  	// only has a Cluster object.
   122  	var c client.Client
   123  	if err := t.proxy.CheckClusterAvailable(ctx); err == nil {
   124  		if initialized, err := t.inventoryClient.CheckCAPIInstalled(ctx); err == nil && initialized {
   125  			c, err = t.proxy.NewClient(ctx)
   126  			if err != nil {
   127  				return nil, errors.Wrap(err, "failed to create a client to the cluster")
   128  			}
   129  			log.Info("Detected a cluster with Cluster API installed. Will use it to fetch missing objects.")
   130  		}
   131  	}
   132  
   133  	// Prepare the inputs for dry running the reconciler. This includes steps like setting missing namespaces on objects
   134  	// and adjusting cluster objects to reflect updated state.
   135  	if err := t.prepareInput(ctx, in, c); err != nil {
   136  		return nil, errors.Wrap(err, "failed preparing input")
   137  	}
   138  
   139  	// Run defaulting and validation on core CAPI objects - Cluster and ClusterClasses.
   140  	// This mimics the defaulting and validation webhooks that will run on the objects during a real execution.
   141  	// Running defaulting and validation on these objects helps to improve the UX of using the plan operation.
   142  	// This is especially important when working with Clusters and ClusterClasses that use variable and patches.
   143  	if err := t.runDefaultAndValidationWebhooks(ctx, in, c); err != nil {
   144  		return nil, errors.Wrap(err, "failed defaulting and validation on input objects")
   145  	}
   146  
   147  	objs := []client.Object{}
   148  	// Add all the objects from the input to the list used when initializing the dry run client.
   149  	for _, o := range in.Objs {
   150  		objs = append(objs, o)
   151  	}
   152  	// Add mock CRDs of all the provider objects in the input to the list used when initializing the dry run client.
   153  	// Adding these CRDs makes sure that UpdateReferenceAPIContract calls in the reconciler can work.
   154  	for _, o := range t.generateCRDs(in.Objs) {
   155  		objs = append(objs, o)
   156  	}
   157  
   158  	dryRunClient := dryrun.NewClient(c, objs)
   159  	// Calculate affected ClusterClasses.
   160  	affectedClusterClasses, err := t.affectedClusterClasses(ctx, in, dryRunClient)
   161  	if err != nil {
   162  		return nil, errors.Wrap(err, "failed calculating affected ClusterClasses")
   163  	}
   164  	// Calculate affected Clusters.
   165  	affectedClusters, err := t.affectedClusters(ctx, in, dryRunClient)
   166  	if err != nil {
   167  		return nil, errors.Wrap(err, "failed calculating affected Clusters")
   168  	}
   169  
   170  	res := &TopologyPlanOutput{
   171  		Clusters:       affectedClusters,
   172  		ClusterClasses: affectedClusterClasses,
   173  		ChangeSummary:  &dryrun.ChangeSummary{},
   174  	}
   175  
   176  	// Calculate the target cluster object.
   177  	// Full changeset is only generated for the target cluster.
   178  	var targetCluster *client.ObjectKey
   179  	if in.TargetClusterName != "" {
   180  		// Check if the target cluster is among the list of affected clusters and use that.
   181  		target := client.ObjectKey{
   182  			Namespace: in.TargetNamespace,
   183  			Name:      in.TargetClusterName,
   184  		}
   185  		if inList(affectedClusters, target) {
   186  			targetCluster = &target
   187  		} else {
   188  			return nil, fmt.Errorf("target cluster %q is not among the list of affected clusters", target.String())
   189  		}
   190  	} else if len(affectedClusters) == 1 {
   191  		// If no target cluster is specified and if there is only one affected cluster, use that as the target cluster.
   192  		targetCluster = &affectedClusters[0]
   193  	}
   194  
   195  	if targetCluster == nil {
   196  		// There is no target cluster, return here. We will
   197  		// not generate a full change summary.
   198  		return res, nil
   199  	}
   200  
   201  	res.ReconciledCluster = targetCluster
   202  	reconciler := &clustertopologycontroller.Reconciler{
   203  		Client:                    dryRunClient,
   204  		APIReader:                 dryRunClient,
   205  		UnstructuredCachingClient: dryRunClient,
   206  	}
   207  	reconciler.SetupForDryRun(&noOpRecorder{})
   208  	request := reconcile.Request{NamespacedName: *targetCluster}
   209  	// Run the topology reconciler.
   210  	if _, err := reconciler.Reconcile(ctx, request); err != nil {
   211  		return nil, errors.Wrap(err, "failed to dry run the topology controller")
   212  	}
   213  	// Calculate changes observed by dry run client.
   214  	changes, err := dryRunClient.Changes(ctx)
   215  	if err != nil {
   216  		return nil, errors.Wrap(err, "failed to get changes made by the topology controller")
   217  	}
   218  	res.ChangeSummary = changes
   219  
   220  	return res, nil
   221  }
   222  
   223  // validateInput checks that the topology plan input does not violate any of the below expectations:
   224  // - no more than 1 cluster in the input.
   225  // - no more than 1 clusterclass in the input.
   226  func (t *topologyClient) validateInput(in *TopologyPlanInput) error {
   227  	// Check if objects of the same Group.Kind are using the same version.
   228  	// Note: This is because the dryrun client does not guarantee any coversions.
   229  	if !hasUniqueVersionPerGroupKind(in.Objs) {
   230  		return fmt.Errorf("objects of the same Group.Kind should use the same apiVersion")
   231  	}
   232  
   233  	// Check all the objects in the input belong to the same namespace.
   234  	// Note: It is okay if all the objects in the input do not have any namespace.
   235  	// In such case, the list of unique namespaces will be [""].
   236  	namespaces := uniqueNamespaces(in.Objs)
   237  	if len(namespaces) != 1 {
   238  		return fmt.Errorf("all the objects in the input should belong to the same namespace")
   239  	}
   240  
   241  	ns := namespaces[0]
   242  	// If the objects have a non empty namespace make sure that it matches the TargetNamespace.
   243  	if ns != "" && in.TargetNamespace != "" && ns != in.TargetNamespace {
   244  		return fmt.Errorf("the namespace from the provided object(s) %q does not match the namespace %q", ns, in.TargetNamespace)
   245  	}
   246  	in.TargetNamespace = ns
   247  
   248  	clusterCount, clusterClassCount := len(getClusters(in.Objs)), len(getClusterClasses(in.Objs))
   249  	if clusterCount > maxClusterPerInput || clusterClassCount > maxClusterClassesPerInput {
   250  		return fmt.Errorf(
   251  			"input should have at most %d Cluster(s) and at most %d ClusterClass(es). Found %d Cluster(s) and %d ClusterClass(es)",
   252  			maxClusterPerInput,
   253  			maxClusterClassesPerInput,
   254  			clusterCount,
   255  			clusterClassCount,
   256  		)
   257  	}
   258  	return nil
   259  }
   260  
   261  // prepareInput does the following on the input objects:
   262  //   - Set the target namespace on the objects if not set (this operation is generally done by kubectl)
   263  //   - Prepare cluster objects so that the state of the cluster, if modified, correctly represents
   264  //     the expected changes.
   265  func (t *topologyClient) prepareInput(ctx context.Context, in *TopologyPlanInput, apiReader client.Reader) error {
   266  	if err := t.setMissingNamespaces(ctx, in.TargetNamespace, in.Objs); err != nil {
   267  		return errors.Wrap(err, "failed to set missing namespaces")
   268  	}
   269  
   270  	if err := t.prepareClusters(ctx, getClusters(in.Objs), apiReader); err != nil {
   271  		return errors.Wrap(err, "failed to prepare clusters")
   272  	}
   273  	return nil
   274  }
   275  
   276  // setMissingNamespaces sets the object to the current namespace on objects
   277  // that are missing the namespace field.
   278  func (t *topologyClient) setMissingNamespaces(ctx context.Context, currentNamespace string, objs []*unstructured.Unstructured) error {
   279  	if currentNamespace == "" {
   280  		// If TargetNamespace is not provided use "default" namespace.
   281  		currentNamespace = metav1.NamespaceDefault
   282  		// If a cluster is available use the current namespace as defined in its kubeconfig.
   283  		if err := t.proxy.CheckClusterAvailable(ctx); err == nil {
   284  			currentNamespace, err = t.proxy.CurrentNamespace()
   285  			if err != nil {
   286  				return errors.Wrap(err, "failed to get current namespace")
   287  			}
   288  		}
   289  	}
   290  	// Set namespace on objects that do not have namespace value.
   291  	// Skip Namespace objects, as they are non-namespaced.
   292  	for i := range objs {
   293  		isNamespace := objs[i].GroupVersionKind().Kind == namespaceKind
   294  		if objs[i].GetNamespace() == "" && !isNamespace {
   295  			objs[i].SetNamespace(currentNamespace)
   296  		}
   297  	}
   298  	return nil
   299  }
   300  
   301  // prepareClusters does the following operations on each Cluster in the input.
   302  //   - Check if the Cluster exists in the real apiserver.
   303  //   - If the Cluster exists in the real apiserver we merge the object from the
   304  //     server with the object from the input. This final object correctly represents the
   305  //     modified cluster object.
   306  //     Note: We are using a simple 2-way merge to calculate the final object in this function
   307  //     to keep the function simple. In reality kubectl does a lot more. This function does not behave exactly
   308  //     the same way as kubectl does.
   309  //
   310  // *Important note*: We do this above operation because the topology reconciler in a
   311  //
   312  //	real run takes as input a cluster object from the apiserver that has merged spec of
   313  //	the changes in the input and the one stored in the server. For example: the cluster
   314  //	object in the input will not have cluster.spec.infrastructureRef and cluster.spec.controlPlaneRef
   315  //	but the merged object will have these fields set.
   316  func (t *topologyClient) prepareClusters(ctx context.Context, clusters []*unstructured.Unstructured, apiReader client.Reader) error {
   317  	if apiReader == nil {
   318  		// If there is no backing server there is nothing more to do here.
   319  		// Return early.
   320  		return nil
   321  	}
   322  
   323  	// For a Cluster check if it already exists in the server. If it does, get the object from the server
   324  	// and merge it with the Cluster from the file to get effective 'modified' Cluster.
   325  	for _, cluster := range clusters {
   326  		storedCluster := &clusterv1.Cluster{}
   327  		if err := apiReader.Get(
   328  			ctx,
   329  			client.ObjectKey{Namespace: cluster.GetNamespace(), Name: cluster.GetName()},
   330  			storedCluster,
   331  		); err != nil {
   332  			if apierrors.IsNotFound(err) {
   333  				// The Cluster does not exist in the server. Nothing more to do here.
   334  				continue
   335  			}
   336  			return errors.Wrapf(err, "failed to get Cluster %s/%s", cluster.GetNamespace(), cluster.GetName())
   337  		}
   338  		originalJSON, err := json.Marshal(storedCluster)
   339  		if err != nil {
   340  			return errors.Wrapf(err, "failed to convert Cluster %s/%s to json", storedCluster.Namespace, storedCluster.Name)
   341  		}
   342  		modifiedJSON, err := json.Marshal(cluster)
   343  		if err != nil {
   344  			return errors.Wrapf(err, "failed to convert Cluster %s/%s", cluster.GetNamespace(), cluster.GetName())
   345  		}
   346  		// Apply the modified object to the original one, merging the values of both;
   347  		// in case of conflicts, values from the modified object are preserved.
   348  		originalWithModifiedJSON, err := jsonpatch.MergePatch(originalJSON, modifiedJSON)
   349  		if err != nil {
   350  			return errors.Wrap(err, "failed to apply modified json to original json")
   351  		}
   352  		if err := json.Unmarshal(originalWithModifiedJSON, &cluster.Object); err != nil {
   353  			return errors.Wrap(err, "failed to convert modified json to object")
   354  		}
   355  	}
   356  	return nil
   357  }
   358  
   359  // runDefaultAndValidationWebhooks runs the defaulting and validation webhooks on the
   360  // ClusterClass and Cluster objects in the input thus replicating the real kube-apiserver flow
   361  // when applied.
   362  // Nb. Perform ValidateUpdate only if the object is already in the cluster. In all other cases,
   363  // ValidateCreate is performed.
   364  // *Important Note*: We cannot perform defaulting and validation on provider objects as we do not have access to
   365  // that code.
   366  func (t *topologyClient) runDefaultAndValidationWebhooks(ctx context.Context, in *TopologyPlanInput, apiReader client.Reader) error {
   367  	// Enable the ClusterTopology feature gate so that the defaulter and validators do not complain.
   368  	// Note: We don't need to disable it later because the CLI is short lived.
   369  	if err := feature.Gates.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", feature.ClusterTopology, true)); err != nil {
   370  		return errors.Wrapf(err, "failed to enable %s feature gate", feature.ClusterTopology)
   371  	}
   372  
   373  	// From the inputs gather all the objects that are not Clusters or ClusterClasses.
   374  	// These objects will be used when initializing a dryrun client to use in the webhooks.
   375  	filteredObjs := filterObjects(
   376  		in.Objs,
   377  		clusterv1.GroupVersion.WithKind("ClusterClass"),
   378  		clusterv1.GroupVersion.WithKind("Cluster"),
   379  	)
   380  	objs := []client.Object{}
   381  	for _, o := range filteredObjs {
   382  		objs = append(objs, o)
   383  	}
   384  	// Creating a dryrun client will a fall back to the apiReader client (client to the underlying Kubernetes cluster)
   385  	// allows the defaulting and validation webhooks to complete actions to could potentially depend on other objects in the cluster.
   386  	// Example: Validation of cluster objects will use the client to read ClusterClasses.
   387  	webhookClient := dryrun.NewClient(apiReader, objs)
   388  
   389  	// Run defaulting and validation on ClusterClasses.
   390  	ccWebhook := &webhooks.ClusterClass{Client: webhookClient}
   391  	if err := t.defaultAndValidateObjs(
   392  		ctx,
   393  		getClusterClasses(in.Objs),
   394  		&clusterv1.ClusterClass{},
   395  		ccWebhook,
   396  		ccWebhook,
   397  		apiReader,
   398  	); err != nil {
   399  		return errors.Wrap(err, "failed to run defaulting and validation on ClusterClasses")
   400  	}
   401  
   402  	// From the inputs gather all the objects that are not Clusters or ClusterClasses.
   403  	// These objects will be used when initializing a dryrun client to use in the webhooks.
   404  	filteredObjs = filterObjects(
   405  		in.Objs,
   406  		clusterv1.GroupVersion.WithKind("Cluster"),
   407  		clusterv1.GroupVersion.WithKind("ClusterClass"),
   408  	)
   409  
   410  	objs = []client.Object{}
   411  	for _, o := range filteredObjs {
   412  		objs = append(objs, o)
   413  	}
   414  	// Reconcile the ClusterClasses and add the reconciled version of them to the webhook client.
   415  	// This is required as validation of Cluster objects might need access to ClusterClass objects that are in the input.
   416  	// Cluster variable defaulting and validation relies on the ClusterClass `.status.variables` which is added
   417  	// during ClusterClass reconciliation.
   418  	reconciledClusterClasses, err := t.reconcileClusterClasses(ctx, in.Objs, apiReader)
   419  	if err != nil {
   420  		return errors.Wrapf(err, "failed to reconcile ClusterClasses for defaulting and validating")
   421  	}
   422  	objs = append(objs, reconciledClusterClasses...)
   423  
   424  	webhookClient = dryrun.NewClient(apiReader, objs)
   425  
   426  	// Run defaulting and validation on Clusters.
   427  	clusterWebhook := &webhooks.Cluster{Client: webhookClient}
   428  	if err := t.defaultAndValidateObjs(
   429  		ctx,
   430  		getClusters(in.Objs),
   431  		&clusterv1.Cluster{},
   432  		clusterWebhook,
   433  		clusterWebhook,
   434  		apiReader,
   435  	); err != nil {
   436  		return errors.Wrap(err, "failed to run defaulting and validation on Clusters")
   437  	}
   438  
   439  	return nil
   440  }
   441  
   442  func (t *topologyClient) reconcileClusterClasses(ctx context.Context, inputObjects []*unstructured.Unstructured, apiReader client.Reader) ([]client.Object, error) {
   443  	reconciliationObjects := []client.Object{}
   444  	// From the inputs gather all the objects that are not ClusterClasses.
   445  	// These objects will be used when initializing a dryrun client to use in the reconciler.
   446  	for _, o := range filterObjects(inputObjects, clusterv1.GroupVersion.WithKind("ClusterClass")) {
   447  		reconciliationObjects = append(reconciliationObjects, o)
   448  	}
   449  	// Add mock CRDs of all the provider objects in the input to the list used when initializing the client.
   450  	// Adding these CRDs makes sure that UpdateReferenceAPIContract calls in the reconciler can work.
   451  	for _, o := range t.generateCRDs(inputObjects) {
   452  		reconciliationObjects = append(reconciliationObjects, o)
   453  	}
   454  
   455  	// Create a list of all ClusterClasses, including those in the dry run input and those in the management Cluster
   456  	// API Server.
   457  	allClusterClasses := []client.Object{}
   458  	ccList := &clusterv1.ClusterClassList{}
   459  	// If an APIReader is available add the ClusterClasses from the management cluster
   460  	if apiReader != nil {
   461  		if err := apiReader.List(ctx, ccList); err != nil {
   462  			return nil, errors.Wrap(err, "failed to find ClusterClasses to default and validate Clusters")
   463  		}
   464  		for i := range ccList.Items {
   465  			allClusterClasses = append(allClusterClasses, &ccList.Items[i])
   466  		}
   467  	}
   468  
   469  	// Add ClusterClasses from the input
   470  	inClusterClasses := getClusterClasses(inputObjects)
   471  	cc := clusterv1.ClusterClass{}
   472  	for _, class := range inClusterClasses {
   473  		if err := scheme.Scheme.Convert(class, &cc, ctx); err != nil {
   474  			return nil, errors.Wrapf(err, "failed to convert object %s/%s to ClusterClass", class.GetNamespace(), class.GetName())
   475  		}
   476  		allClusterClasses = append(allClusterClasses, &cc)
   477  	}
   478  
   479  	// Each ClusterClass should be reconciled in order to ensure variables are correctly added to `status.variables`.
   480  	// This is required as Clusters are validated based of variable definitions in the ClusterClass `.status.variables`.
   481  	reconciledClusterClasses := []client.Object{}
   482  	for _, class := range allClusterClasses {
   483  		reconciledClusterClass, err := reconcileClusterClass(ctx, apiReader, class, reconciliationObjects)
   484  		if err != nil {
   485  			return nil, errors.Wrapf(err, "ClusterClass %s could not be reconciled for dry run", class.GetName())
   486  		}
   487  		reconciledClusterClasses = append(reconciledClusterClasses, reconciledClusterClass)
   488  	}
   489  
   490  	// Remove the ClusterClasses from the input objects and replace them with the reconciled version.
   491  	for i, obj := range inputObjects {
   492  		if obj.GroupVersionKind() == clusterv1.GroupVersion.WithKind("ClusterClass") {
   493  			// remove the clusterclass from the list of reconciled clusterclasses if it was not in the input.
   494  			inputObjects = append(inputObjects[:i], inputObjects[i+1:]...)
   495  		}
   496  	}
   497  	for _, class := range reconciledClusterClasses {
   498  		obj := &unstructured.Unstructured{}
   499  		if err := localScheme.Convert(class, obj, nil); err != nil {
   500  			return nil, errors.Wrapf(err, "failed to convert %s to object", obj.GetKind())
   501  		}
   502  		inputObjects = append(inputObjects, obj)
   503  	}
   504  
   505  	// Return a list of successfully reconciled ClusterClasses.
   506  	return reconciledClusterClasses, nil
   507  }
   508  
   509  func reconcileClusterClass(ctx context.Context, apiReader client.Reader, class client.Object, reconciliationObjects []client.Object) (*unstructured.Unstructured, error) {
   510  	targetClusterClass := client.ObjectKey{Namespace: class.GetNamespace(), Name: class.GetName()}
   511  	reconciliationObjects = append(reconciliationObjects, class)
   512  
   513  	// Create a reconcilerClient that has access to all of the necessary templates to complete a successful reconcile
   514  	// of the ClusterClass.
   515  	reconcilerClient := dryrun.NewClient(apiReader, reconciliationObjects)
   516  
   517  	clusterClassReconciler := &clusterclasscontroller.Reconciler{
   518  		Client:                    reconcilerClient,
   519  		UnstructuredCachingClient: reconcilerClient,
   520  	}
   521  
   522  	if _, err := clusterClassReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: targetClusterClass}); err != nil {
   523  		return nil, errors.Wrap(err, "failed to dry run the ClusterClass controller")
   524  	}
   525  
   526  	// Pull the reconciled ClusterClass using the reconcilerClient, and return the version with the updated status.
   527  	reconciledClusterClass := &clusterv1.ClusterClass{}
   528  	if err := reconcilerClient.Get(ctx, targetClusterClass, reconciledClusterClass); err != nil {
   529  		return nil, fmt.Errorf("could not retrieve ClusterClass")
   530  	}
   531  
   532  	obj := &unstructured.Unstructured{}
   533  	// Converted the defaulted and validated object back into unstructured.
   534  	// Note: This step also makes sure that modified object is updated into the
   535  	// original unstructured object.
   536  	if err := localScheme.Convert(reconciledClusterClass, obj, nil); err != nil {
   537  		return nil, errors.Wrapf(err, "failed to convert %s to object", obj.GetKind())
   538  	}
   539  
   540  	return obj, nil
   541  }
   542  
   543  func (t *topologyClient) defaultAndValidateObjs(ctx context.Context, objs []*unstructured.Unstructured, o client.Object, defaulter crwebhook.CustomDefaulter, validator crwebhook.CustomValidator, apiReader client.Reader) error {
   544  	for _, obj := range objs {
   545  		// The defaulter and validator need a typed object. Convert the unstructured obj to a typed object.
   546  		object := o.DeepCopyObject().(client.Object) // object here is a typed object.
   547  		if err := localScheme.Convert(obj, object, nil); err != nil {
   548  			return errors.Wrapf(err, "failed to convert object to %s", obj.GetKind())
   549  		}
   550  
   551  		// Perform Defaulting
   552  		if err := defaulter.Default(ctx, object); err != nil {
   553  			return errors.Wrapf(err, "failed defaulting of %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName())
   554  		}
   555  
   556  		var oldObject client.Object
   557  		if apiReader != nil {
   558  			tmpObj := o.DeepCopyObject().(client.Object)
   559  			if err := apiReader.Get(ctx, client.ObjectKeyFromObject(obj), tmpObj); err != nil {
   560  				if !apierrors.IsNotFound(err) {
   561  					return errors.Wrapf(err, "failed to get object %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName())
   562  				}
   563  			} else {
   564  				oldObject = tmpObj
   565  			}
   566  		}
   567  		if oldObject != nil {
   568  			if _, err := validator.ValidateUpdate(ctx, oldObject, object); err != nil {
   569  				return errors.Wrapf(err, "failed validation of %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName())
   570  			}
   571  		} else {
   572  			if _, err := validator.ValidateCreate(ctx, object); err != nil {
   573  				return errors.Wrapf(err, "failed validation of %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName())
   574  			}
   575  		}
   576  
   577  		// Converted the defaulted and validated object back into unstructured.
   578  		// Note: This step also makes sure that modified object is updated into the
   579  		// original unstructured object.
   580  		if err := localScheme.Convert(object, obj, nil); err != nil {
   581  			return errors.Wrapf(err, "failed to convert %s to object", obj.GetKind())
   582  		}
   583  	}
   584  
   585  	return nil
   586  }
   587  
   588  func getClusterClasses(objs []*unstructured.Unstructured) []*unstructured.Unstructured {
   589  	res := make([]*unstructured.Unstructured, 0)
   590  	for _, obj := range objs {
   591  		if obj.GroupVersionKind() == clusterv1.GroupVersion.WithKind("ClusterClass") {
   592  			res = append(res, obj)
   593  		}
   594  	}
   595  	return res
   596  }
   597  
   598  func getClusters(objs []*unstructured.Unstructured) []*unstructured.Unstructured {
   599  	res := make([]*unstructured.Unstructured, 0)
   600  	for _, obj := range objs {
   601  		if obj.GroupVersionKind() == clusterv1.GroupVersion.WithKind("Cluster") {
   602  			res = append(res, obj)
   603  		}
   604  	}
   605  	return res
   606  }
   607  
   608  func getTemplates(objs []*unstructured.Unstructured) []*unstructured.Unstructured {
   609  	res := make([]*unstructured.Unstructured, 0)
   610  	for _, obj := range objs {
   611  		if strings.HasSuffix(obj.GetKind(), clusterv1.TemplateSuffix) {
   612  			res = append(res, obj)
   613  		}
   614  	}
   615  	return res
   616  }
   617  
   618  // generateCRDs creates mock CRD objects for all the provider specific objects in the input.
   619  // These CRD objects will be added to the dry run client for UpdateReferenceAPIContract
   620  // to work as expected.
   621  func (t *topologyClient) generateCRDs(objs []*unstructured.Unstructured) []*apiextensionsv1.CustomResourceDefinition {
   622  	crds := []*apiextensionsv1.CustomResourceDefinition{}
   623  	crdMap := map[string]bool{}
   624  	var gvk schema.GroupVersionKind
   625  
   626  	for _, obj := range objs {
   627  		gvk = obj.GroupVersionKind()
   628  		if strings.HasSuffix(gvk.Group, ".cluster.x-k8s.io") && !crdMap[gvk.String()] {
   629  			crd := &apiextensionsv1.CustomResourceDefinition{
   630  				TypeMeta: metav1.TypeMeta{
   631  					APIVersion: apiextensionsv1.SchemeGroupVersion.String(),
   632  					Kind:       "CustomResourceDefinition",
   633  				},
   634  				ObjectMeta: metav1.ObjectMeta{
   635  					Name: contract.CalculateCRDName(gvk.Group, gvk.Kind),
   636  					Labels: map[string]string{
   637  						// Here we assume that all the versions are compatible with the Cluster API contract version.
   638  						clusterv1.GroupVersion.String(): gvk.Version,
   639  					},
   640  				},
   641  			}
   642  			crds = append(crds, crd)
   643  			crdMap[gvk.String()] = true
   644  		}
   645  	}
   646  
   647  	return crds
   648  }
   649  
   650  func (t *topologyClient) affectedClusterClasses(ctx context.Context, in *TopologyPlanInput, c client.Reader) ([]client.ObjectKey, error) {
   651  	affectedClusterClasses := map[client.ObjectKey]bool{}
   652  	ccList := &clusterv1.ClusterClassList{}
   653  	if err := c.List(
   654  		ctx,
   655  		ccList,
   656  		client.InNamespace(in.TargetNamespace),
   657  	); err != nil {
   658  		return nil, errors.Wrapf(err, "failed to list ClusterClasses in namespace %s", in.TargetNamespace)
   659  	}
   660  
   661  	// Each of the ClusterClass that uses any of the Templates in the input is an affected ClusterClass.
   662  	for _, template := range getTemplates(in.Objs) {
   663  		for i := range ccList.Items {
   664  			if clusterClassUsesTemplate(&ccList.Items[i], objToRef(template)) {
   665  				affectedClusterClasses[client.ObjectKeyFromObject(&ccList.Items[i])] = true
   666  			}
   667  		}
   668  	}
   669  
   670  	// All the ClusterClasses in the input are considered affected ClusterClasses.
   671  	for _, cc := range getClusterClasses(in.Objs) {
   672  		affectedClusterClasses[client.ObjectKeyFromObject(cc)] = true
   673  	}
   674  
   675  	affectedClusterClassesList := []client.ObjectKey{}
   676  	for k := range affectedClusterClasses {
   677  		affectedClusterClassesList = append(affectedClusterClassesList, k)
   678  	}
   679  	return affectedClusterClassesList, nil
   680  }
   681  
   682  func (t *topologyClient) affectedClusters(ctx context.Context, in *TopologyPlanInput, c client.Reader) ([]client.ObjectKey, error) {
   683  	affectedClusters := map[client.ObjectKey]bool{}
   684  	affectedClusterClasses, err := t.affectedClusterClasses(ctx, in, c)
   685  	if err != nil {
   686  		return nil, errors.Wrap(err, "failed to get list of affected ClusterClasses")
   687  	}
   688  	clusterList := &clusterv1.ClusterList{}
   689  	if err := c.List(
   690  		ctx,
   691  		clusterList,
   692  		client.InNamespace(in.TargetNamespace),
   693  	); err != nil {
   694  		return nil, errors.Wrapf(err, "failed to list Clusters in namespace %s", in.TargetNamespace)
   695  	}
   696  
   697  	// Each of the Cluster that uses the ClusterClass in the input is an affected cluster.
   698  	for _, cc := range affectedClusterClasses {
   699  		for i := range clusterList.Items {
   700  			if clusterList.Items[i].Spec.Topology != nil && clusterList.Items[i].Spec.Topology.Class == cc.Name {
   701  				affectedClusters[client.ObjectKeyFromObject(&clusterList.Items[i])] = true
   702  			}
   703  		}
   704  	}
   705  
   706  	// All the Clusters in the input are considered affected Clusters.
   707  	for _, cluster := range getClusters(in.Objs) {
   708  		affectedClusters[client.ObjectKeyFromObject(cluster)] = true
   709  	}
   710  
   711  	affectedClustersList := []client.ObjectKey{}
   712  	for k := range affectedClusters {
   713  		affectedClustersList = append(affectedClustersList, k)
   714  	}
   715  	return affectedClustersList, nil
   716  }
   717  
   718  func inList(list []client.ObjectKey, target client.ObjectKey) bool {
   719  	for _, i := range list {
   720  		if i == target {
   721  			return true
   722  		}
   723  	}
   724  	return false
   725  }
   726  
   727  // filterObjects returns a new list of objects after dropping all the objects that match any of the given GVKs.
   728  func filterObjects(objs []*unstructured.Unstructured, gvks ...schema.GroupVersionKind) []*unstructured.Unstructured {
   729  	res := []*unstructured.Unstructured{}
   730  	for _, o := range objs {
   731  		skip := false
   732  		for _, gvk := range gvks {
   733  			if o.GroupVersionKind() == gvk {
   734  				skip = true
   735  				break
   736  			}
   737  		}
   738  		if !skip {
   739  			res = append(res, o)
   740  		}
   741  	}
   742  	return res
   743  }
   744  
   745  type noOpRecorder struct{}
   746  
   747  func (nr *noOpRecorder) Event(_ runtime.Object, _, _, _ string)                    {}
   748  func (nr *noOpRecorder) Eventf(_ runtime.Object, _, _, _ string, _ ...interface{}) {}
   749  func (nr *noOpRecorder) AnnotatedEventf(_ runtime.Object, _ map[string]string, _, _, _ string, _ ...interface{}) {
   750  }
   751  
   752  func objToRef(o *unstructured.Unstructured) *corev1.ObjectReference {
   753  	return &corev1.ObjectReference{
   754  		Kind:       o.GetKind(),
   755  		Namespace:  o.GetNamespace(),
   756  		Name:       o.GetName(),
   757  		APIVersion: o.GetAPIVersion(),
   758  	}
   759  }
   760  
   761  func equalRef(a, b *corev1.ObjectReference) bool {
   762  	if a.APIVersion != b.APIVersion {
   763  		return false
   764  	}
   765  	if a.Namespace != b.Namespace {
   766  		return false
   767  	}
   768  	if a.Name != b.Name {
   769  		return false
   770  	}
   771  	if a.Kind != b.Kind {
   772  		return false
   773  	}
   774  	return true
   775  }
   776  
   777  func clusterClassUsesTemplate(cc *clusterv1.ClusterClass, templateRef *corev1.ObjectReference) bool {
   778  	// Check infrastructure ref.
   779  	if equalRef(cc.Spec.Infrastructure.Ref, templateRef) {
   780  		return true
   781  	}
   782  	// Check control plane ref.
   783  	if equalRef(cc.Spec.ControlPlane.Ref, templateRef) {
   784  		return true
   785  	}
   786  	// If control plane uses machine, check it.
   787  	if cc.Spec.ControlPlane.MachineInfrastructure != nil && cc.Spec.ControlPlane.MachineInfrastructure.Ref != nil {
   788  		if equalRef(cc.Spec.ControlPlane.MachineInfrastructure.Ref, templateRef) {
   789  			return true
   790  		}
   791  	}
   792  
   793  	for _, mdClass := range cc.Spec.Workers.MachineDeployments {
   794  		// Check bootstrap template ref.
   795  		if equalRef(mdClass.Template.Bootstrap.Ref, templateRef) {
   796  			return true
   797  		}
   798  		// Check the infrastructure ref.
   799  		if equalRef(mdClass.Template.Infrastructure.Ref, templateRef) {
   800  			return true
   801  		}
   802  	}
   803  
   804  	for _, mpClass := range cc.Spec.Workers.MachinePools {
   805  		// Check the bootstrap ref
   806  		if equalRef(mpClass.Template.Bootstrap.Ref, templateRef) {
   807  			return true
   808  		}
   809  		// Check the infrastructure ref.
   810  		if equalRef(mpClass.Template.Infrastructure.Ref, templateRef) {
   811  			return true
   812  		}
   813  	}
   814  
   815  	return false
   816  }
   817  
   818  func uniqueNamespaces(objs []*unstructured.Unstructured) []string {
   819  	ns := sets.Set[string]{}
   820  	for _, obj := range objs {
   821  		// Namespace objects do not have metadata.namespace set, but we can add the
   822  		// name of the obj to the namespace list, as it is another unique namespace.
   823  		isNamespace := obj.GroupVersionKind().Kind == namespaceKind
   824  		if isNamespace {
   825  			ns.Insert(obj.GetName())
   826  			continue
   827  		}
   828  
   829  		// Note: treat empty namespace (namespace not set) as a unique namespace.
   830  		// If some have a namespace set and some do not. It is safer to consider them as
   831  		// objects from different namespaces.
   832  		ns.Insert(obj.GetNamespace())
   833  	}
   834  	return sets.List(ns)
   835  }
   836  
   837  func hasUniqueVersionPerGroupKind(objs []*unstructured.Unstructured) bool {
   838  	versionMap := map[string]string{}
   839  	for _, obj := range objs {
   840  		gvk := obj.GroupVersionKind()
   841  		gk := gvk.GroupKind().String()
   842  		if ver, ok := versionMap[gk]; ok && ver != gvk.Version {
   843  			return false
   844  		}
   845  		versionMap[gk] = gvk.Version
   846  	}
   847  	return true
   848  }