Skip to content

Commit 94e4093

Browse files
Add webhook warning for missing ClusterClass
Signed-off-by: killianmuldoon <[email protected]>
1 parent 730f63d commit 94e4093

File tree

2 files changed

+125
-44
lines changed

2 files changed

+125
-44
lines changed

internal/webhooks/cluster.go

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ func (webhook *Cluster) ValidateDelete(_ context.Context, _ runtime.Object) (adm
139139

140140
func (webhook *Cluster) validate(ctx context.Context, oldCluster, newCluster *clusterv1.Cluster) (admission.Warnings, error) {
141141
var allErrs field.ErrorList
142+
var allWarnings admission.Warnings
142143
// The Cluster name is used as a label value. This check ensures that names which are not valid label values are rejected.
143144
if errs := validation.IsValidLabelValue(newCluster.Name); len(errs) != 0 {
144145
for _, err := range errs {
@@ -191,7 +192,9 @@ func (webhook *Cluster) validate(ctx context.Context, oldCluster, newCluster *cl
191192

192193
// Validate the managed topology, if defined.
193194
if newCluster.Spec.Topology != nil {
194-
allErrs = append(allErrs, webhook.validateTopology(ctx, oldCluster, newCluster, topologyPath)...)
195+
topologyWarnings, topologyErrs := webhook.validateTopology(ctx, oldCluster, newCluster, topologyPath)
196+
allWarnings = append(allWarnings, topologyWarnings...)
197+
allErrs = append(allErrs, topologyErrs...)
195198
}
196199

197200
// On update.
@@ -206,16 +209,18 @@ func (webhook *Cluster) validate(ctx context.Context, oldCluster, newCluster *cl
206209
}
207210

208211
if len(allErrs) > 0 {
209-
return nil, apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("Cluster").GroupKind(), newCluster.Name, allErrs)
212+
return allWarnings, apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("Cluster").GroupKind(), newCluster.Name, allErrs)
210213
}
211-
return nil, nil
214+
return allWarnings, nil
212215
}
213216

214-
func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newCluster *clusterv1.Cluster, fldPath *field.Path) field.ErrorList {
217+
func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newCluster *clusterv1.Cluster, fldPath *field.Path) (admission.Warnings, field.ErrorList) {
218+
var allWarnings admission.Warnings
219+
215220
// NOTE: ClusterClass and managed topologies are behind ClusterTopology feature gate flag; the web hook
216221
// must prevent the usage of Cluster.Topology in case the feature flag is disabled.
217222
if !feature.Gates.Enabled(feature.ClusterTopology) {
218-
return field.ErrorList{
223+
return allWarnings, field.ErrorList{
219224
field.Forbidden(
220225
fldPath,
221226
"can be set only if the ClusterTopology feature flag is enabled",
@@ -234,6 +239,8 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
234239
"class cannot be empty",
235240
),
236241
)
242+
// Return early if there is no defined class to validate.
243+
return allWarnings, allErrs
237244
}
238245

239246
// version should be valid.
@@ -268,18 +275,11 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
268275
}
269276

270277
// Get the ClusterClass referenced in the Cluster.
271-
clusterClass, clusterClassPollErr := webhook.pollClusterClassForCluster(ctx, newCluster)
272-
if clusterClassPollErr != nil &&
273-
// If the error is anything other than "NotFound" or "NotReconciled" return all errors at this point.
274-
!(apierrors.IsNotFound(clusterClassPollErr) || errors.Is(clusterClassPollErr, errClusterClassNotReconciled)) {
275-
allErrs = append(
276-
allErrs, field.InternalError(
277-
fldPath.Child("class"),
278-
clusterClassPollErr))
279-
return allErrs
280-
}
278+
clusterClass, warnings, clusterClassPollErr := webhook.validateClusterClassExistsAndIsReconciled(ctx, newCluster)
279+
allWarnings = append(allWarnings, warnings...)
280+
281+
// If there's no error validate the Cluster based on the ClusterClass.
281282
if clusterClassPollErr == nil {
282-
// If there's no error validate the Cluster based on the ClusterClass.
283283
allErrs = append(allErrs, ValidateClusterForClusterClass(newCluster, clusterClass)...)
284284
}
285285
if oldCluster != nil { // On update
@@ -290,13 +290,13 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
290290
allErrs, field.InternalError(
291291
fldPath.Child("class"),
292292
clusterClassPollErr))
293-
return allErrs
293+
return allWarnings, allErrs
294294
}
295295

296296
// Topology or Class can not be added on update unless ClusterTopologyUnsafeUpdateClassNameAnnotation is set.
297297
if oldCluster.Spec.Topology == nil || oldCluster.Spec.Topology.Class == "" {
298298
if _, ok := newCluster.Annotations[clusterv1.ClusterTopologyUnsafeUpdateClassNameAnnotation]; ok {
299-
return allErrs
299+
return allWarnings, allErrs
300300
}
301301

302302
allErrs = append(
@@ -307,7 +307,7 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
307307
),
308308
)
309309
// return early here if there is no class to compare.
310-
return allErrs
310+
return allWarnings, allErrs
311311
}
312312

313313
// Version could only be increased.
@@ -372,14 +372,14 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
372372
oldCluster.Spec.Topology.Class, newCluster.Spec.Topology.Class)))
373373

374374
// Return early with errors if the ClusterClass can't be retrieved.
375-
return allErrs
375+
return allWarnings, allErrs
376376
}
377377

378378
// Check if the new and old ClusterClasses are compatible with one another.
379379
allErrs = append(allErrs, check.ClusterClassesAreCompatible(oldClusterClass, clusterClass)...)
380380
}
381381
}
382-
return allErrs
382+
return allWarnings, allErrs
383383
}
384384

385385
func validateMachineHealthChecks(cluster *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass) field.ErrorList {
@@ -551,11 +551,44 @@ func ValidateClusterForClusterClass(cluster *clusterv1.Cluster, clusterClass *cl
551551
return allErrs
552552
}
553553

554+
// validateClusterClassExistsAndIsReconciled will try to get the ClusterClass referenced in the Cluster. If it does not exist or is not reconciled it will add a warning.
555+
// In any other case it will return an error.
556+
func (webhook *Cluster) validateClusterClassExistsAndIsReconciled(ctx context.Context, newCluster *clusterv1.Cluster) (*clusterv1.ClusterClass, admission.Warnings, error) {
557+
var allWarnings admission.Warnings
558+
clusterClass, clusterClassPollErr := webhook.pollClusterClassForCluster(ctx, newCluster)
559+
if clusterClassPollErr != nil {
560+
// Add a warning if the Class does not exist or if it has not been successfully reconciled.
561+
switch {
562+
case apierrors.IsNotFound(clusterClassPollErr):
563+
allWarnings = append(allWarnings,
564+
fmt.Sprintf(
565+
"Cluster refers to ClusterClass %s in the topology but it does not exist. "+
566+
"Cluster topology has not been fully validated. "+
567+
"The ClusterClass must be created to reconcile the Cluster", newCluster.Spec.Topology.Class),
568+
)
569+
case errors.Is(clusterClassPollErr, errClusterClassNotReconciled):
570+
allWarnings = append(allWarnings,
571+
fmt.Sprintf(
572+
"Cluster refers to ClusterClass %s but this object is not yet fully reconciled. "+
573+
"Cluster topology has not been fully validated. ", newCluster.Spec.Topology.Class),
574+
)
575+
// If there's any other error return a generic warning with the error message.
576+
default:
577+
allWarnings = append(allWarnings,
578+
fmt.Sprintf(
579+
"Cluster refers to ClusterClass %s in the topology but it could not be retrieved. "+
580+
"Cluster topology has not been fully validated. "+
581+
": %s", newCluster.Spec.Topology.Class, clusterClassPollErr.Error()),
582+
)
583+
}
584+
}
585+
return clusterClass, allWarnings, clusterClassPollErr
586+
}
587+
554588
// pollClusterClassForCluster will retry getting the ClusterClass referenced in the Cluster for two seconds.
555589
func (webhook *Cluster) pollClusterClassForCluster(ctx context.Context, cluster *clusterv1.Cluster) (*clusterv1.ClusterClass, error) {
556590
clusterClass := &clusterv1.ClusterClass{}
557591
var clusterClassPollErr error
558-
// TODO: Add a webhook warning if the ClusterClass is not up to date or not found.
559592
_ = wait.PollUntilContextTimeout(ctx, 200*time.Millisecond, 2*time.Second, true, func(ctx context.Context) (bool, error) {
560593
if clusterClassPollErr = webhook.Client.Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Spec.Topology.Class}, clusterClass); clusterClassPollErr != nil {
561594
return false, nil //nolint:nilerr

0 commit comments

Comments
 (0)