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 }