Skip to content

Commit c5dfe81

Browse files
committed
Add variable validation to ClusterClass controller, block Cluster
reconcile if variables not reconciled Signed-off-by: Stefan Büringer [email protected]
1 parent 8ce866c commit c5dfe81

File tree

9 files changed

+376
-41
lines changed

9 files changed

+376
-41
lines changed

cmd/clusterctl/client/cluster/topology.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,18 @@ func (t *topologyClient) Plan(ctx context.Context, in *TopologyPlanInput) (*Topo
140140
// This mimics the defaulting and validation webhooks that will run on the objects during a real execution.
141141
// Running defaulting and validation on these objects helps to improve the UX of using the plan operation.
142142
// 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 {
143+
reconciledClusterClasses, err := t.runDefaultAndValidationWebhooks(ctx, in, c)
144+
if err != nil {
144145
return nil, errors.Wrap(err, "failed defaulting and validation on input objects")
145146
}
146147

147148
objs := []client.Object{}
148149
// Add all the objects from the input to the list used when initializing the dry run client.
149-
for _, o := range in.Objs {
150+
for _, o := range filterObjects(in.Objs, clusterv1.GroupVersion.WithKind("ClusterClass")) {
150151
objs = append(objs, o)
151152
}
153+
// Note: We have to add the reconciled ClusterClasses, because the Cluster reconciler depends on that.
154+
objs = append(objs, reconciledClusterClasses...)
152155
// Add mock CRDs of all the provider objects in the input to the list used when initializing the dry run client.
153156
// Adding these CRDs makes sure that UpdateReferenceAPIContract calls in the reconciler can work.
154157
for _, o := range t.generateCRDs(in.Objs) {
@@ -362,11 +365,11 @@ func (t *topologyClient) prepareClusters(ctx context.Context, clusters []*unstru
362365
// ValidateCreate is performed.
363366
// *Important Note*: We cannot perform defaulting and validation on provider objects as we do not have access to
364367
// that code.
365-
func (t *topologyClient) runDefaultAndValidationWebhooks(ctx context.Context, in *TopologyPlanInput, apiReader client.Reader) error {
368+
func (t *topologyClient) runDefaultAndValidationWebhooks(ctx context.Context, in *TopologyPlanInput, apiReader client.Reader) ([]client.Object, error) {
366369
// Enable the ClusterTopology feature gate so that the defaulter and validators do not complain.
367370
// Note: We don't need to disable it later because the CLI is short lived.
368371
if err := feature.Gates.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", feature.ClusterTopology, true)); err != nil {
369-
return errors.Wrapf(err, "failed to enable %s feature gate", feature.ClusterTopology)
372+
return nil, errors.Wrapf(err, "failed to enable %s feature gate", feature.ClusterTopology)
370373
}
371374

372375
// From the inputs gather all the objects that are not Clusters or ClusterClasses.
@@ -395,7 +398,7 @@ func (t *topologyClient) runDefaultAndValidationWebhooks(ctx context.Context, in
395398
ccWebhook,
396399
apiReader,
397400
); err != nil {
398-
return errors.Wrap(err, "failed to run defaulting and validation on ClusterClasses")
401+
return nil, errors.Wrap(err, "failed to run defaulting and validation on ClusterClasses")
399402
}
400403

401404
// From the inputs gather all the objects that are not Clusters or ClusterClasses.
@@ -416,7 +419,7 @@ func (t *topologyClient) runDefaultAndValidationWebhooks(ctx context.Context, in
416419
// during ClusterClass reconciliation.
417420
reconciledClusterClasses, err := t.reconcileClusterClasses(ctx, in.Objs, apiReader)
418421
if err != nil {
419-
return errors.Wrapf(err, "failed to reconcile ClusterClasses for defaulting and validating")
422+
return nil, errors.Wrapf(err, "failed to reconcile ClusterClasses for defaulting and validating")
420423
}
421424
objs = append(objs, reconciledClusterClasses...)
422425

@@ -432,10 +435,10 @@ func (t *topologyClient) runDefaultAndValidationWebhooks(ctx context.Context, in
432435
clusterWebhook,
433436
apiReader,
434437
); err != nil {
435-
return errors.Wrap(err, "failed to run defaulting and validation on Clusters")
438+
return nil, errors.Wrap(err, "failed to run defaulting and validation on Clusters")
436439
}
437440

438-
return nil
441+
return reconciledClusterClasses, nil
439442
}
440443

441444
func (t *topologyClient) reconcileClusterClasses(ctx context.Context, inputObjects []*unstructured.Unstructured, apiReader client.Reader) ([]client.Object, error) {

internal/controllers/clusterclass/clusterclass_controller.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"k8s.io/apimachinery/pkg/runtime/schema"
3232
kerrors "k8s.io/apimachinery/pkg/util/errors"
3333
"k8s.io/apimachinery/pkg/util/sets"
34+
"k8s.io/apimachinery/pkg/util/validation/field"
3435
ctrl "sigs.k8s.io/controller-runtime"
3536
"sigs.k8s.io/controller-runtime/pkg/client"
3637
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -45,6 +46,7 @@ import (
4546
"sigs.k8s.io/cluster-api/feature"
4647
tlog "sigs.k8s.io/cluster-api/internal/log"
4748
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
49+
"sigs.k8s.io/cluster-api/internal/topology/variables"
4850
"sigs.k8s.io/cluster-api/util/annotations"
4951
"sigs.k8s.io/cluster-api/util/conditions"
5052
"sigs.k8s.io/cluster-api/util/conversion"
@@ -245,15 +247,13 @@ func (r *Reconciler) reconcileVariables(ctx context.Context, clusterClass *clust
245247
continue
246248
}
247249
if resp.Variables != nil {
248-
uniqueNamesForPatch := sets.Set[string]{}
249-
for _, variable := range resp.Variables {
250-
// Ensure a patch doesn't define multiple variables with the same name.
251-
if uniqueNamesForPatch.Has(variable.Name) {
252-
errs = append(errs, errors.Errorf("variable %q is defined multiple times in variable discovery response from patch %q", variable.Name, patch.Name))
253-
continue
254-
}
255-
uniqueNamesForPatch.Insert(variable.Name)
250+
validationErrors := variables.ValidateClusterClassVariables(ctx, nil, resp.Variables, field.NewPath(patch.Name, "variables")).ToAggregate()
251+
if validationErrors != nil {
252+
errs = append(errs, validationErrors)
253+
continue
254+
}
256255

256+
for _, variable := range resp.Variables {
257257
// If a variable of the same name already exists in allVariableDefinitions add the new definition to the existing list.
258258
if _, ok := allVariableDefinitions[variable.Name]; ok {
259259
allVariableDefinitions[variable.Name] = addDefinitionToExistingStatusVariable(variable, patch.Name, allVariableDefinitions[variable.Name])

0 commit comments

Comments
 (0)