@@ -39,8 +39,10 @@ import (
39
39
40
40
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
41
41
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster/internal/dryrun"
42
+ "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
42
43
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
43
44
"sigs.k8s.io/cluster-api/feature"
45
+ clusterclasscontroller "sigs.k8s.io/cluster-api/internal/controllers/clusterclass"
44
46
clustertopologycontroller "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster"
45
47
"sigs.k8s.io/cluster-api/internal/webhooks"
46
48
"sigs.k8s.io/cluster-api/util/contract"
@@ -134,6 +136,7 @@ func (t *topologyClient) Plan(in *TopologyPlanInput) (*TopologyPlanOutput, error
134
136
if err := t .prepareInput (ctx , in , c ); err != nil {
135
137
return nil , errors .Wrap (err , "failed preparing input" )
136
138
}
139
+
137
140
// Run defaulting and validation on core CAPI objects - Cluster and ClusterClasses.
138
141
// This mimics the defaulting and validation webhooks that will run on the objects during a real execution.
139
142
// Running defaulting and validation on these objects helps to improve the UX of using the plan operation.
@@ -397,18 +400,28 @@ func (t *topologyClient) runDefaultAndValidationWebhooks(ctx context.Context, in
397
400
return errors .Wrap (err , "failed to run defaulting and validation on ClusterClasses" )
398
401
}
399
402
400
- // From the inputs gather all the objects that are not Clusters.
403
+ // From the inputs gather all the objects that are not Clusters or ClusterClasses .
401
404
// These objects will be used when initializing a dryrun client to use in the webhooks.
402
- // We want to keep ClusterClasses in the webhook client. This is because validation of
403
- // Cluster objects might need access to ClusterClass objects that are in the input.
404
405
filteredObjs = filterObjects (
405
406
in .Objs ,
406
407
clusterv1 .GroupVersion .WithKind ("Cluster" ),
408
+ clusterv1 .GroupVersion .WithKind ("ClusterClass" ),
407
409
)
410
+
408
411
objs = []client.Object {}
409
412
for _ , o := range filteredObjs {
410
413
objs = append (objs , o )
411
414
}
415
+ // Reconcile the ClusterClasses and add the reconciled version of them to the webhook client.
416
+ // This is required as validation of Cluster objects might need access to ClusterClass objects that are in the input.
417
+ // Cluster variable defaulting and validation relies on the ClusterClass `.status.variables` which is added
418
+ // during ClusterClass reconciliation.
419
+ reconciledClusterClasses , err := t .reconcileClusterClasses (ctx , in .Objs , apiReader )
420
+ if err != nil {
421
+ return errors .Wrapf (err , "failed to reconcile ClusterClasses for defaulting and validating" )
422
+ }
423
+ objs = append (objs , reconciledClusterClasses ... )
424
+
412
425
webhookClient = dryrun .NewClient (apiReader , objs )
413
426
414
427
// Run defaulting and validation on Clusters.
@@ -427,6 +440,108 @@ func (t *topologyClient) runDefaultAndValidationWebhooks(ctx context.Context, in
427
440
return nil
428
441
}
429
442
443
+ func (t * topologyClient ) reconcileClusterClasses (ctx context.Context , inputObjects []* unstructured.Unstructured , apiReader client.Reader ) ([]client.Object , error ) {
444
+ reconciliationObjects := []client.Object {}
445
+ // From the inputs gather all the objects that are not ClusterClasses.
446
+ // These objects will be used when initializing a dryrun client to use in the reconciler.
447
+ for _ , o := range filterObjects (inputObjects , clusterv1 .GroupVersion .WithKind ("ClusterClass" )) {
448
+ reconciliationObjects = append (reconciliationObjects , o )
449
+ }
450
+ // Add mock CRDs of all the provider objects in the input to the list used when initializing the client.
451
+ // Adding these CRDs makes sure that UpdateReferenceAPIContract calls in the reconciler can work.
452
+ for _ , o := range t .generateCRDs (inputObjects ) {
453
+ reconciliationObjects = append (reconciliationObjects , o )
454
+ }
455
+
456
+ // Create a list of all ClusterClasses, including those in the dry run input and those in the management Cluster
457
+ // API Server.
458
+ allClusterClasses := []client.Object {}
459
+ ccList := & clusterv1.ClusterClassList {}
460
+ // If an APIReader is available add the ClusterClasses from the management cluster
461
+ if apiReader != nil {
462
+ if err := apiReader .List (ctx , ccList ); err != nil {
463
+ return nil , errors .Wrap (err , "failed to find ClusterClasses to default and validate Clusters" )
464
+ }
465
+ for i := range ccList .Items {
466
+ allClusterClasses = append (allClusterClasses , & ccList .Items [i ])
467
+ }
468
+ }
469
+
470
+ // Add ClusterClasses from the input
471
+ inClusterClasses := getClusterClasses (inputObjects )
472
+ cc := clusterv1.ClusterClass {}
473
+ for _ , class := range inClusterClasses {
474
+ if err := scheme .Scheme .Convert (class , & cc , ctx ); err != nil {
475
+ return nil , errors .Wrapf (err , "failed to convert object %s/%s to ClusterClass" , class .GetNamespace (), class .GetName ())
476
+ }
477
+ allClusterClasses = append (allClusterClasses , & cc )
478
+ }
479
+
480
+ // Each ClusterClass should be reconciled in order to ensure variables are correctly added to `status.variables`.
481
+ // This is required as Clusters are validated based of variable definitions in the ClusterClass `.status.variables`.
482
+ reconciledClusterClasses := []client.Object {}
483
+ for _ , class := range allClusterClasses {
484
+ reconciledClusterClass , err := reconcileClusterClass (apiReader , class , reconciliationObjects )
485
+ if err != nil {
486
+ return nil , errors .Wrapf (err , "ClusterClass %s could not be reconciled for dry run" , class .GetName ())
487
+ }
488
+ reconciledClusterClasses = append (reconciledClusterClasses , reconciledClusterClass )
489
+ }
490
+
491
+ // Remove the ClusterClasses from the input objects and replace them with the reconciled version.
492
+ for i , obj := range inputObjects {
493
+ if obj .GroupVersionKind () == clusterv1 .GroupVersion .WithKind ("ClusterClass" ) {
494
+ // remove the clusterclass from the list of reconciled clusterclasses if it was not in the input.
495
+ inputObjects = append (inputObjects [:i ], inputObjects [i + 1 :]... )
496
+ }
497
+ }
498
+ for _ , class := range reconciledClusterClasses {
499
+ obj := & unstructured.Unstructured {}
500
+ if err := localScheme .Convert (class , obj , nil ); err != nil {
501
+ return nil , errors .Wrapf (err , "failed to convert %s to object" , obj .GetKind ())
502
+ }
503
+ inputObjects = append (inputObjects , obj )
504
+ }
505
+
506
+ // Return a list of successfully reconciled ClusterClasses.
507
+ return reconciledClusterClasses , nil
508
+ }
509
+
510
+ func reconcileClusterClass (apiReader client.Reader , class client.Object , reconciliationObjects []client.Object ) (* unstructured.Unstructured , error ) {
511
+ targetClusterClass := client.ObjectKey {Namespace : class .GetNamespace (), Name : class .GetName ()}
512
+ reconciliationObjects = append (reconciliationObjects , class )
513
+
514
+ // Create a reconcilerClient that has access to all of the necessary templates to complete a successful reconcile
515
+ // of the ClusterClass.
516
+ reconcilerClient := dryrun .NewClient (apiReader , reconciliationObjects )
517
+
518
+ clusterClassReconciler := & clusterclasscontroller.Reconciler {
519
+ Client : reconcilerClient ,
520
+ APIReader : reconcilerClient ,
521
+ UnstructuredCachingClient : reconcilerClient ,
522
+ }
523
+
524
+ if _ , err := clusterClassReconciler .Reconcile (ctx , reconcile.Request {NamespacedName : targetClusterClass }); err != nil {
525
+ return nil , errors .Wrap (err , "failed to dry run the ClusterClass controller" )
526
+ }
527
+
528
+ // Pull the reconciled ClusterClass using the reconcilerClient, and return the version with the updated status.
529
+ reconciledClusterClass := & clusterv1.ClusterClass {}
530
+ if err := reconcilerClient .Get (ctx , targetClusterClass , reconciledClusterClass ); err != nil {
531
+ return nil , fmt .Errorf ("could not retrieve ClusterClass" )
532
+ }
533
+
534
+ obj := & unstructured.Unstructured {}
535
+ // Converted the defaulted and validated object back into unstructured.
536
+ // Note: This step also makes sure that modified object is updated into the
537
+ // original unstructured object.
538
+ if err := localScheme .Convert (reconciledClusterClass , obj , nil ); err != nil {
539
+ return nil , errors .Wrapf (err , "failed to convert %s to object" , obj .GetKind ())
540
+ }
541
+
542
+ return obj , nil
543
+ }
544
+
430
545
func (t * topologyClient ) defaultAndValidateObjs (ctx context.Context , objs []* unstructured.Unstructured , o client.Object , defaulter crwebhook.CustomDefaulter , validator crwebhook.CustomValidator , apiReader client.Reader ) error {
431
546
for _ , obj := range objs {
432
547
// The defaulter and validator need a typed object. Convert the unstructured obj to a typed object.
0 commit comments