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 }