@@ -139,6 +139,7 @@ func (webhook *Cluster) ValidateDelete(_ context.Context, _ runtime.Object) (adm
139139
140140func (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,21 @@ 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 )) {
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 )) {
275281 allErrs = append (
276282 allErrs , field .InternalError (
277283 fldPath .Child ("class" ),
278284 clusterClassPollErr ))
279- return allErrs
285+ return allWarnings , allErrs
280286 }
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.
281292 if clusterClassPollErr == nil {
282- // If there's no error validate the Cluster based on the ClusterClass.
283293 allErrs = append (allErrs , ValidateClusterForClusterClass (newCluster , clusterClass )... )
284294 }
285295 if oldCluster != nil { // On update
@@ -290,13 +300,13 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
290300 allErrs , field .InternalError (
291301 fldPath .Child ("class" ),
292302 clusterClassPollErr ))
293- return allErrs
303+ return allWarnings , allErrs
294304 }
295305
296306 // Topology or Class can not be added on update unless ClusterTopologyUnsafeUpdateClassNameAnnotation is set.
297307 if oldCluster .Spec .Topology == nil || oldCluster .Spec .Topology .Class == "" {
298308 if _ , ok := newCluster .Annotations [clusterv1 .ClusterTopologyUnsafeUpdateClassNameAnnotation ]; ok {
299- return allErrs
309+ return allWarnings , allErrs
300310 }
301311
302312 allErrs = append (
@@ -307,7 +317,7 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
307317 ),
308318 )
309319 // return early here if there is no class to compare.
310- return allErrs
320+ return allWarnings , allErrs
311321 }
312322
313323 // Version could only be increased.
@@ -368,18 +378,18 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu
368378 allErrs = append (
369379 allErrs , field .Forbidden (
370380 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 () )))
373383
374384 // Return early with errors if the ClusterClass can't be retrieved.
375- return allErrs
385+ return allWarnings , allErrs
376386 }
377387
378388 // Check if the new and old ClusterClasses are compatible with one another.
379389 allErrs = append (allErrs , check .ClusterClassesAreCompatible (oldClusterClass , clusterClass )... )
380390 }
381391 }
382- return allErrs
392+ return allWarnings , allErrs
383393}
384394
385395func validateMachineHealthChecks (cluster * clusterv1.Cluster , clusterClass * clusterv1.ClusterClass ) field.ErrorList {
@@ -551,11 +561,43 @@ func ValidateClusterForClusterClass(cluster *clusterv1.Cluster, clusterClass *cl
551561 return allErrs
552562}
553563
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+
554597// pollClusterClassForCluster will retry getting the ClusterClass referenced in the Cluster for two seconds.
555598func (webhook * Cluster ) pollClusterClassForCluster (ctx context.Context , cluster * clusterv1.Cluster ) (* clusterv1.ClusterClass , error ) {
556599 clusterClass := & clusterv1.ClusterClass {}
557600 var clusterClassPollErr error
558- // TODO: Add a webhook warning if the ClusterClass is not up to date or not found.
559601 _ = wait .PollUntilContextTimeout (ctx , 200 * time .Millisecond , 2 * time .Second , true , func (ctx context.Context ) (bool , error ) {
560602 if clusterClassPollErr = webhook .Client .Get (ctx , client.ObjectKey {Namespace : cluster .Namespace , Name : cluster .Spec .Topology .Class }, clusterClass ); clusterClassPollErr != nil {
561603 return false , nil //nolint:nilerr
@@ -567,7 +609,10 @@ func (webhook *Cluster) pollClusterClassForCluster(ctx context.Context, cluster
567609 clusterClassPollErr = nil
568610 return true , nil
569611 })
570- return clusterClass , clusterClassPollErr
612+ if clusterClassPollErr != nil {
613+ return nil , clusterClassPollErr
614+ }
615+ return clusterClass , nil
571616}
572617
573618// clusterClassIsReconciled returns errClusterClassNotReconciled if the ClusterClass has not successfully reconciled or if the
0 commit comments