@@ -139,6 +139,7 @@ func (webhook *Cluster) ValidateDelete(_ context.Context, _ runtime.Object) (adm
139
139
140
140
func (webhook * Cluster ) validate (ctx context.Context , oldCluster , newCluster * clusterv1.Cluster ) (admission.Warnings , error ) {
141
141
var allErrs field.ErrorList
142
+ var allWarnings admission.Warnings
142
143
// The Cluster name is used as a label value. This check ensures that names which are not valid label values are rejected.
143
144
if errs := validation .IsValidLabelValue (newCluster .Name ); len (errs ) != 0 {
144
145
for _ , err := range errs {
@@ -191,7 +192,9 @@ func (webhook *Cluster) validate(ctx context.Context, oldCluster, newCluster *cl
191
192
192
193
// Validate the managed topology, if defined.
193
194
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 ... )
195
198
}
196
199
197
200
// On update.
@@ -206,16 +209,18 @@ func (webhook *Cluster) validate(ctx context.Context, oldCluster, newCluster *cl
206
209
}
207
210
208
211
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 )
210
213
}
211
- return nil , nil
214
+ return allWarnings , nil
212
215
}
213
216
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
+
215
220
// NOTE: ClusterClass and managed topologies are behind ClusterTopology feature gate flag; the web hook
216
221
// must prevent the usage of Cluster.Topology in case the feature flag is disabled.
217
222
if ! feature .Gates .Enabled (feature .ClusterTopology ) {
218
- return field.ErrorList {
223
+ return allWarnings , field.ErrorList {
219
224
field .Forbidden (
220
225
fldPath ,
221
226
"can be set only if the ClusterTopology feature flag is enabled" ,
@@ -234,6 +239,8 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
234
239
"class cannot be empty" ,
235
240
),
236
241
)
242
+ // Return early if there is no defined class to validate.
243
+ return allWarnings , allErrs
237
244
}
238
245
239
246
// version should be valid.
@@ -268,18 +275,21 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
268
275
}
269
276
270
277
// 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 )) {
278
+ clusterClass , warnings , clusterClassPollErr := webhook .validateClusterClassExistsAndIsReconciled (ctx , newCluster )
279
+ // If the error is anything other than "NotFound" or "NotReconciled" return all errors.
280
+ if clusterClassPollErr != nil && ! (apierrors .IsNotFound (clusterClassPollErr ) || errors .Is (clusterClassPollErr , errClusterClassNotReconciled )) {
275
281
allErrs = append (
276
282
allErrs , field .InternalError (
277
283
fldPath .Child ("class" ),
278
284
clusterClassPollErr ))
279
- return allErrs
285
+ return allWarnings , allErrs
280
286
}
287
+
288
+ // Add the warnings if no error was returned.
289
+ allWarnings = append (allWarnings , warnings ... )
290
+
291
+ // If there's no error validate the Cluster based on the ClusterClass.
281
292
if clusterClassPollErr == nil {
282
- // If there's no error validate the Cluster based on the ClusterClass.
283
293
allErrs = append (allErrs , ValidateClusterForClusterClass (newCluster , clusterClass )... )
284
294
}
285
295
if oldCluster != nil { // On update
@@ -290,13 +300,13 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
290
300
allErrs , field .InternalError (
291
301
fldPath .Child ("class" ),
292
302
clusterClassPollErr ))
293
- return allErrs
303
+ return allWarnings , allErrs
294
304
}
295
305
296
306
// Topology or Class can not be added on update unless ClusterTopologyUnsafeUpdateClassNameAnnotation is set.
297
307
if oldCluster .Spec .Topology == nil || oldCluster .Spec .Topology .Class == "" {
298
308
if _ , ok := newCluster .Annotations [clusterv1 .ClusterTopologyUnsafeUpdateClassNameAnnotation ]; ok {
299
- return allErrs
309
+ return allWarnings , allErrs
300
310
}
301
311
302
312
allErrs = append (
@@ -307,7 +317,7 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
307
317
),
308
318
)
309
319
// return early here if there is no class to compare.
310
- return allErrs
320
+ return allWarnings , allErrs
311
321
}
312
322
313
323
// Version could only be increased.
@@ -368,18 +378,18 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
368
378
allErrs = append (
369
379
allErrs , field .Forbidden (
370
380
fldPath .Child ("class" ),
371
- fmt .Sprintf ("valid ClusterClass with name %q could not be found , change from class %[1]q to class %q cannot be validated" ,
372
- oldCluster .Spec .Topology .Class , newCluster .Spec .Topology .Class )))
381
+ fmt .Sprintf ("valid ClusterClass with name %q could not be retrieved , change from class %[1]q to class %q cannot be validated. Error: %s " ,
382
+ oldCluster .Spec .Topology .Class , newCluster .Spec .Topology .Class , err . Error () )))
373
383
374
384
// Return early with errors if the ClusterClass can't be retrieved.
375
- return allErrs
385
+ return allWarnings , allErrs
376
386
}
377
387
378
388
// Check if the new and old ClusterClasses are compatible with one another.
379
389
allErrs = append (allErrs , check .ClusterClassesAreCompatible (oldClusterClass , clusterClass )... )
380
390
}
381
391
}
382
- return allErrs
392
+ return allWarnings , allErrs
383
393
}
384
394
385
395
func validateMachineHealthChecks (cluster * clusterv1.Cluster , clusterClass * clusterv1.ClusterClass ) field.ErrorList {
@@ -551,11 +561,43 @@ func ValidateClusterForClusterClass(cluster *clusterv1.Cluster, clusterClass *cl
551
561
return allErrs
552
562
}
553
563
564
+ // 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.
565
+ // In any other case it will return an error.
566
+ func (webhook * Cluster ) validateClusterClassExistsAndIsReconciled (ctx context.Context , newCluster * clusterv1.Cluster ) (* clusterv1.ClusterClass , admission.Warnings , error ) {
567
+ var allWarnings admission.Warnings
568
+ clusterClass , clusterClassPollErr := webhook .pollClusterClassForCluster (ctx , newCluster )
569
+ if clusterClassPollErr != nil {
570
+ // Add a warning if the Class does not exist or if it has not been successfully reconciled.
571
+ switch {
572
+ case apierrors .IsNotFound (clusterClassPollErr ):
573
+ allWarnings = append (allWarnings ,
574
+ fmt .Sprintf (
575
+ "Cluster refers to ClusterClass %s in the topology but it does not exist. " +
576
+ "Cluster topology has not been fully validated. " +
577
+ "The ClusterClass must be created to reconcile the Cluster" , newCluster .Spec .Topology .Class ),
578
+ )
579
+ case errors .Is (clusterClassPollErr , errClusterClassNotReconciled ):
580
+ allWarnings = append (allWarnings ,
581
+ fmt .Sprintf (
582
+ "Cluster refers to ClusterClass %s but this object which hasn't yet been reconciled. " +
583
+ "Cluster topology has not been fully validated. " , newCluster .Spec .Topology .Class ),
584
+ )
585
+ // If there's any other error return a generic warning with the error message.
586
+ default :
587
+ allWarnings = append (allWarnings ,
588
+ fmt .Sprintf (
589
+ "Cluster refers to ClusterClass %s in the topology but it could not be retrieved. " +
590
+ "Cluster topology has not been fully validated: %s" , newCluster .Spec .Topology .Class , clusterClassPollErr .Error ()),
591
+ )
592
+ }
593
+ }
594
+ return clusterClass , allWarnings , clusterClassPollErr
595
+ }
596
+
554
597
// pollClusterClassForCluster will retry getting the ClusterClass referenced in the Cluster for two seconds.
555
598
func (webhook * Cluster ) pollClusterClassForCluster (ctx context.Context , cluster * clusterv1.Cluster ) (* clusterv1.ClusterClass , error ) {
556
599
clusterClass := & clusterv1.ClusterClass {}
557
600
var clusterClassPollErr error
558
- // TODO: Add a webhook warning if the ClusterClass is not up to date or not found.
559
601
_ = wait .PollUntilContextTimeout (ctx , 200 * time .Millisecond , 2 * time .Second , true , func (ctx context.Context ) (bool , error ) {
560
602
if clusterClassPollErr = webhook .Client .Get (ctx , client.ObjectKey {Namespace : cluster .Namespace , Name : cluster .Spec .Topology .Class }, clusterClass ); clusterClassPollErr != nil {
561
603
return false , nil //nolint:nilerr
@@ -567,7 +609,10 @@ func (webhook *Cluster) pollClusterClassForCluster(ctx context.Context, cluster
567
609
clusterClassPollErr = nil
568
610
return true , nil
569
611
})
570
- return clusterClass , clusterClassPollErr
612
+ if clusterClassPollErr != nil {
613
+ return nil , clusterClassPollErr
614
+ }
615
+ return clusterClass , nil
571
616
}
572
617
573
618
// clusterClassIsReconciled returns errClusterClassNotReconciled if the ClusterClass has not successfully reconciled or if the
0 commit comments