sigs.k8s.io/cluster-api@v1.6.3/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(); err == nil {
   124  		if initialized, err := t.inventoryClient.CheckCAPIInstalled(ctx); err == nil && initialized {
   125  			c, err = t.proxy.NewClient()
   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(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(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(); 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  		APIReader:                 reconcilerClient,
   520  		UnstructuredCachingClient: reconcilerClient,
   521  	}
   522  
   523  	if _, err := clusterClassReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: targetClusterClass}); err != nil {
   524  		return nil, errors.Wrap(err, "failed to dry run the ClusterClass controller")
   525  	}
   526  
   527  	// Pull the reconciled ClusterClass using the reconcilerClient, and return the version with the updated status.
   528  	reconciledClusterClass := &clusterv1.ClusterClass{}
   529  	if err := reconcilerClient.Get(ctx, targetClusterClass, reconciledClusterClass); err != nil {
   530  		return nil, fmt.Errorf("could not retrieve ClusterClass")
   531  	}
   532  
   533  	obj := &unstructured.Unstructured{}
   534  	// Converted the defaulted and validated object back into unstructured.
   535  	// Note: This step also makes sure that modified object is updated into the
   536  	// original unstructured object.
   537  	if err := localScheme.Convert(reconciledClusterClass, obj, nil); err != nil {
   538  		return nil, errors.Wrapf(err, "failed to convert %s to object", obj.GetKind())
   539  	}
   540  
   541  	return obj, nil
   542  }
   543  
   544  func (t *topologyClient) defaultAndValidateObjs(ctx context.Context, objs []*unstructured.Unstructured, o client.Object, defaulter crwebhook.CustomDefaulter, validator crwebhook.CustomValidator, apiReader client.Reader) error {
   545  	for _, obj := range objs {
   546  		// The defaulter and validator need a typed object. Convert the unstructured obj to a typed object.
   547  		object := o.DeepCopyObject().(client.Object) // object here is a typed object.
   548  		if err := localScheme.Convert(obj, object, nil); err != nil {
   549  			return errors.Wrapf(err, "failed to convert object to %s", obj.GetKind())
   550  		}
   551  
   552  		// Perform Defaulting
   553  		if err := defaulter.Default(ctx, object); err != nil {
   554  			return errors.Wrapf(err, "failed defaulting of %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName())
   555  		}
   556  
   557  		var oldObject client.Object
   558  		if apiReader != nil {
   559  			tmpObj := o.DeepCopyObject().(client.Object)
   560  			if err := apiReader.Get(ctx, client.ObjectKeyFromObject(obj), tmpObj); err != nil {
   561  				if !apierrors.IsNotFound(err) {
   562  					return errors.Wrapf(err, "failed to get object %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName())
   563  				}
   564  			} else {
   565  				oldObject = tmpObj
   566  			}
   567  		}
   568  		if oldObject != nil {
   569  			if _, err := validator.ValidateUpdate(ctx, oldObject, object); err != nil {
   570  				return errors.Wrapf(err, "failed validation of %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName())
   571  			}
   572  		} else {
   573  			if _, err := validator.ValidateCreate(ctx, object); err != nil {
   574  				return errors.Wrapf(err, "failed validation of %s %s/%s", obj.GroupVersionKind().String(), obj.GetNamespace(), obj.GetName())
   575  			}
   576  		}
   577  
   578  		// Converted the defaulted and validated object back into unstructured.
   579  		// Note: This step also makes sure that modified object is updated into the
   580  		// original unstructured object.
   581  		if err := localScheme.Convert(object, obj, nil); err != nil {
   582  			return errors.Wrapf(err, "failed to convert %s to object", obj.GetKind())
   583  		}
   584  	}
   585  
   586  	return nil
   587  }
   588  
   589  func getClusterClasses(objs []*unstructured.Unstructured) []*unstructured.Unstructured {
   590  	res := make([]*unstructured.Unstructured, 0)
   591  	for _, obj := range objs {
   592  		if obj.GroupVersionKind() == clusterv1.GroupVersion.WithKind("ClusterClass") {
   593  			res = append(res, obj)
   594  		}
   595  	}
   596  	return res
   597  }
   598  
   599  func getClusters(objs []*unstructured.Unstructured) []*unstructured.Unstructured {
   600  	res := make([]*unstructured.Unstructured, 0)
   601  	for _, obj := range objs {
   602  		if obj.GroupVersionKind() == clusterv1.GroupVersion.WithKind("Cluster") {
   603  			res = append(res, obj)
   604  		}
   605  	}
   606  	return res
   607  }
   608  
   609  func getTemplates(objs []*unstructured.Unstructured) []*unstructured.Unstructured {
   610  	res := make([]*unstructured.Unstructured, 0)
   611  	for _, obj := range objs {
   612  		if strings.HasSuffix(obj.GetKind(), clusterv1.TemplateSuffix) {
   613  			res = append(res, obj)
   614  		}
   615  	}
   616  	return res
   617  }
   618  
   619  // generateCRDs creates mock CRD objects for all the provider specific objects in the input.
   620  // These CRD objects will be added to the dry run client for UpdateReferenceAPIContract
   621  // to work as expected.
   622  func (t *topologyClient) generateCRDs(objs []*unstructured.Unstructured) []*apiextensionsv1.CustomResourceDefinition {
   623  	crds := []*apiextensionsv1.CustomResourceDefinition{}
   624  	crdMap := map[string]bool{}
   625  	var gvk schema.GroupVersionKind
   626  
   627  	for _, obj := range objs {
   628  		gvk = obj.GroupVersionKind()
   629  		if strings.HasSuffix(gvk.Group, ".cluster.x-k8s.io") && !crdMap[gvk.String()] {
   630  			crd := &apiextensionsv1.CustomResourceDefinition{
   631  				TypeMeta: metav1.TypeMeta{
   632  					APIVersion: apiextensionsv1.SchemeGroupVersion.String(),
   633  					Kind:       "CustomResourceDefinition",
   634  				},
   635  				ObjectMeta: metav1.ObjectMeta{
   636  					Name: contract.CalculateCRDName(gvk.Group, gvk.Kind),
   637  					Labels: map[string]string{
   638  						// Here we assume that all the versions are compatible with the Cluster API contract version.
   639  						clusterv1.GroupVersion.String(): gvk.Version,
   640  					},
   641  				},
   642  			}
   643  			crds = append(crds, crd)
   644  			crdMap[gvk.String()] = true
   645  		}
   646  	}
   647  
   648  	return crds
   649  }
   650  
   651  func (t *topologyClient) affectedClusterClasses(ctx context.Context, in *TopologyPlanInput, c client.Reader) ([]client.ObjectKey, error) {
   652  	affectedClusterClasses := map[client.ObjectKey]bool{}
   653  	ccList := &clusterv1.ClusterClassList{}
   654  	if err := c.List(
   655  		ctx,
   656  		ccList,
   657  		client.InNamespace(in.TargetNamespace),
   658  	); err != nil {
   659  		return nil, errors.Wrapf(err, "failed to list ClusterClasses in namespace %s", in.TargetNamespace)
   660  	}
   661  
   662  	// Each of the ClusterClass that uses any of the Templates in the input is an affected ClusterClass.
   663  	for _, template := range getTemplates(in.Objs) {
   664  		for i := range ccList.Items {
   665  			if clusterClassUsesTemplate(&ccList.Items[i], objToRef(template)) {
   666  				affectedClusterClasses[client.ObjectKeyFromObject(&ccList.Items[i])] = true
   667  			}
   668  		}
   669  	}
   670  
   671  	// All the ClusterClasses in the input are considered affected ClusterClasses.
   672  	for _, cc := range getClusterClasses(in.Objs) {
   673  		affectedClusterClasses[client.ObjectKeyFromObject(cc)] = true
   674  	}
   675  
   676  	affectedClusterClassesList := []client.ObjectKey{}
   677  	for k := range affectedClusterClasses {
   678  		affectedClusterClassesList = append(affectedClusterClassesList, k)
   679  	}
   680  	return affectedClusterClassesList, nil
   681  }
   682  
   683  func (t *topologyClient) affectedClusters(ctx context.Context, in *TopologyPlanInput, c client.Reader) ([]client.ObjectKey, error) {
   684  	affectedClusters := map[client.ObjectKey]bool{}
   685  	affectedClusterClasses, err := t.affectedClusterClasses(ctx, in, c)
   686  	if err != nil {
   687  		return nil, errors.Wrap(err, "failed to get list of affected ClusterClasses")
   688  	}
   689  	clusterList := &clusterv1.ClusterList{}
   690  	if err := c.List(
   691  		ctx,
   692  		clusterList,
   693  		client.InNamespace(in.TargetNamespace),
   694  	); err != nil {
   695  		return nil, errors.Wrapf(err, "failed to list Clusters in namespace %s", in.TargetNamespace)
   696  	}
   697  
   698  	// Each of the Cluster that uses the ClusterClass in the input is an affected cluster.
   699  	for _, cc := range affectedClusterClasses {
   700  		for i := range clusterList.Items {
   701  			if clusterList.Items[i].Spec.Topology != nil && clusterList.Items[i].Spec.Topology.Class == cc.Name {
   702  				affectedClusters[client.ObjectKeyFromObject(&clusterList.Items[i])] = true
   703  			}
   704  		}
   705  	}
   706  
   707  	// All the Clusters in the input are considered affected Clusters.
   708  	for _, cluster := range getClusters(in.Objs) {
   709  		affectedClusters[client.ObjectKeyFromObject(cluster)] = true
   710  	}
   711  
   712  	affectedClustersList := []client.ObjectKey{}
   713  	for k := range affectedClusters {
   714  		affectedClustersList = append(affectedClustersList, k)
   715  	}
   716  	return affectedClustersList, nil
   717  }
   718  
   719  func inList(list []client.ObjectKey, target client.ObjectKey) bool {
   720  	for _, i := range list {
   721  		if i == target {
   722  			return true
   723  		}
   724  	}
   725  	return false
   726  }
   727  
   728  // filterObjects returns a new list of objects after dropping all the objects that match any of the given GVKs.
   729  func filterObjects(objs []*unstructured.Unstructured, gvks ...schema.GroupVersionKind) []*unstructured.Unstructured {
   730  	res := []*unstructured.Unstructured{}
   731  	for _, o := range objs {
   732  		skip := false
   733  		for _, gvk := range gvks {
   734  			if o.GroupVersionKind() == gvk {
   735  				skip = true
   736  				break
   737  			}
   738  		}
   739  		if !skip {
   740  			res = append(res, o)
   741  		}
   742  	}
   743  	return res
   744  }
   745  
   746  type noOpRecorder struct{}
   747  
   748  func (nr *noOpRecorder) Event(_ runtime.Object, _, _, _ string)                    {}
   749  func (nr *noOpRecorder) Eventf(_ runtime.Object, _, _, _ string, _ ...interface{}) {}
   750  func (nr *noOpRecorder) AnnotatedEventf(_ runtime.Object, _ map[string]string, _, _, _ string, _ ...interface{}) {
   751  }
   752  
   753  func objToRef(o *unstructured.Unstructured) *corev1.ObjectReference {
   754  	return &corev1.ObjectReference{
   755  		Kind:       o.GetKind(),
   756  		Namespace:  o.GetNamespace(),
   757  		Name:       o.GetName(),
   758  		APIVersion: o.GetAPIVersion(),
   759  	}
   760  }
   761  
   762  func equalRef(a, b *corev1.ObjectReference) bool {
   763  	if a.APIVersion != b.APIVersion {
   764  		return false
   765  	}
   766  	if a.Namespace != b.Namespace {
   767  		return false
   768  	}
   769  	if a.Name != b.Name {
   770  		return false
   771  	}
   772  	if a.Kind != b.Kind {
   773  		return false
   774  	}
   775  	return true
   776  }
   777  
   778  func clusterClassUsesTemplate(cc *clusterv1.ClusterClass, templateRef *corev1.ObjectReference) bool {
   779  	// Check infrastructure ref.
   780  	if equalRef(cc.Spec.Infrastructure.Ref, templateRef) {
   781  		return true
   782  	}
   783  	// Check control plane ref.
   784  	if equalRef(cc.Spec.ControlPlane.Ref, templateRef) {
   785  		return true
   786  	}
   787  	// If control plane uses machine, check it.
   788  	if cc.Spec.ControlPlane.MachineInfrastructure != nil && cc.Spec.ControlPlane.MachineInfrastructure.Ref != nil {
   789  		if equalRef(cc.Spec.ControlPlane.MachineInfrastructure.Ref, templateRef) {
   790  			return true
   791  		}
   792  	}
   793  
   794  	for _, mdClass := range cc.Spec.Workers.MachineDeployments {
   795  		// Check bootstrap template ref.
   796  		if equalRef(mdClass.Template.Bootstrap.Ref, templateRef) {
   797  			return true
   798  		}
   799  		// Check the infrastructure ref.
   800  		if equalRef(mdClass.Template.Infrastructure.Ref, templateRef) {
   801  			return true
   802  		}
   803  	}
   804  
   805  	for _, mpClass := range cc.Spec.Workers.MachinePools {
   806  		// Check the bootstrap ref
   807  		if equalRef(mpClass.Template.Bootstrap.Ref, templateRef) {
   808  			return true
   809  		}
   810  		// Check the infrastructure ref.
   811  		if equalRef(mpClass.Template.Infrastructure.Ref, templateRef) {
   812  			return true
   813  		}
   814  	}
   815  
   816  	return false
   817  }
   818  
   819  func uniqueNamespaces(objs []*unstructured.Unstructured) []string {
   820  	ns := sets.Set[string]{}
   821  	for _, obj := range objs {
   822  		// Namespace objects do not have metadata.namespace set, but we can add the
   823  		// name of the obj to the namespace list, as it is another unique namespace.
   824  		isNamespace := obj.GroupVersionKind().Kind == namespaceKind
   825  		if isNamespace {
   826  			ns.Insert(obj.GetName())
   827  			continue
   828  		}
   829  
   830  		// Note: treat empty namespace (namespace not set) as a unique namespace.
   831  		// If some have a namespace set and some do not. It is safer to consider them as
   832  		// objects from different namespaces.
   833  		ns.Insert(obj.GetNamespace())
   834  	}
   835  	return sets.List(ns)
   836  }
   837  
   838  func hasUniqueVersionPerGroupKind(objs []*unstructured.Unstructured) bool {
   839  	versionMap := map[string]string{}
   840  	for _, obj := range objs {
   841  		gvk := obj.GroupVersionKind()
   842  		gk := gvk.GroupKind().String()
   843  		if ver, ok := versionMap[gk]; ok && ver != gvk.Version {
   844  			return false
   845  		}
   846  		versionMap[gk] = gvk.Version
   847  	}
   848  	return true
   849  }