sigs.k8s.io/cluster-api@v1.6.3/internal/webhooks/clusterclass.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package webhooks 18 19 import ( 20 "context" 21 "fmt" 22 "strings" 23 24 "github.com/pkg/errors" 25 corev1 "k8s.io/api/core/v1" 26 apierrors "k8s.io/apimachinery/pkg/api/errors" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apimachinery/pkg/util/sets" 30 "k8s.io/apimachinery/pkg/util/validation" 31 "k8s.io/apimachinery/pkg/util/validation/field" 32 ctrl "sigs.k8s.io/controller-runtime" 33 "sigs.k8s.io/controller-runtime/pkg/client" 34 "sigs.k8s.io/controller-runtime/pkg/webhook" 35 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 36 37 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 38 "sigs.k8s.io/cluster-api/api/v1beta1/index" 39 "sigs.k8s.io/cluster-api/feature" 40 "sigs.k8s.io/cluster-api/internal/topology/check" 41 "sigs.k8s.io/cluster-api/internal/topology/names" 42 "sigs.k8s.io/cluster-api/internal/topology/variables" 43 ) 44 45 func (webhook *ClusterClass) SetupWebhookWithManager(mgr ctrl.Manager) error { 46 return ctrl.NewWebhookManagedBy(mgr). 47 For(&clusterv1.ClusterClass{}). 48 WithDefaulter(webhook). 49 WithValidator(webhook). 50 Complete() 51 } 52 53 // +kubebuilder:webhook:verbs=create;update;delete,path=/validate-cluster-x-k8s-io-v1beta1-clusterclass,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=clusterclasses,versions=v1beta1,name=validation.clusterclass.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 54 // +kubebuilder:webhook:verbs=create;update,path=/mutate-cluster-x-k8s-io-v1beta1-clusterclass,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=clusterclasses,versions=v1beta1,name=default.clusterclass.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 55 56 // ClusterClass implements a validation and defaulting webhook for ClusterClass. 57 type ClusterClass struct { 58 Client client.Reader 59 } 60 61 var _ webhook.CustomDefaulter = &ClusterClass{} 62 var _ webhook.CustomValidator = &ClusterClass{} 63 64 // Default implements defaulting for ClusterClass create and update. 65 func (webhook *ClusterClass) Default(_ context.Context, obj runtime.Object) error { 66 in, ok := obj.(*clusterv1.ClusterClass) 67 if !ok { 68 return apierrors.NewBadRequest(fmt.Sprintf("expected a ClusterClass but got a %T", obj)) 69 } 70 // Default all namespaces in the references to the object namespace. 71 defaultNamespace(in.Spec.Infrastructure.Ref, in.Namespace) 72 defaultNamespace(in.Spec.ControlPlane.Ref, in.Namespace) 73 74 if in.Spec.ControlPlane.MachineInfrastructure != nil { 75 defaultNamespace(in.Spec.ControlPlane.MachineInfrastructure.Ref, in.Namespace) 76 } 77 78 for i := range in.Spec.Workers.MachineDeployments { 79 defaultNamespace(in.Spec.Workers.MachineDeployments[i].Template.Bootstrap.Ref, in.Namespace) 80 defaultNamespace(in.Spec.Workers.MachineDeployments[i].Template.Infrastructure.Ref, in.Namespace) 81 } 82 83 for i := range in.Spec.Workers.MachinePools { 84 defaultNamespace(in.Spec.Workers.MachinePools[i].Template.Bootstrap.Ref, in.Namespace) 85 defaultNamespace(in.Spec.Workers.MachinePools[i].Template.Infrastructure.Ref, in.Namespace) 86 } 87 88 return nil 89 } 90 91 func defaultNamespace(ref *corev1.ObjectReference, namespace string) { 92 if ref != nil && ref.Namespace == "" { 93 ref.Namespace = namespace 94 } 95 } 96 97 // ValidateCreate implements validation for ClusterClass create. 98 func (webhook *ClusterClass) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 99 in, ok := obj.(*clusterv1.ClusterClass) 100 if !ok { 101 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a ClusterClass but got a %T", obj)) 102 } 103 return nil, webhook.validate(ctx, nil, in) 104 } 105 106 // ValidateUpdate implements validation for ClusterClass update. 107 func (webhook *ClusterClass) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { 108 newClusterClass, ok := newObj.(*clusterv1.ClusterClass) 109 if !ok { 110 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a ClusterClass but got a %T", newObj)) 111 } 112 oldClusterClass, ok := oldObj.(*clusterv1.ClusterClass) 113 if !ok { 114 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a ClusterClass but got a %T", oldObj)) 115 } 116 return nil, webhook.validate(ctx, oldClusterClass, newClusterClass) 117 } 118 119 // ValidateDelete implements validation for ClusterClass delete. 120 func (webhook *ClusterClass) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 121 clusterClass, ok := obj.(*clusterv1.ClusterClass) 122 if !ok { 123 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a ClusterClass but got a %T", obj)) 124 } 125 126 clusters, err := webhook.getClustersUsingClusterClass(ctx, clusterClass) 127 if err != nil { 128 return nil, apierrors.NewInternalError(errors.Wrapf(err, "could not retrieve Clusters using ClusterClass")) 129 } 130 131 if len(clusters) > 0 { 132 // TODO(killianmuldoon): Improve error here to include the names of some clusters using the clusterClass. 133 return nil, apierrors.NewForbidden(clusterv1.GroupVersion.WithResource("ClusterClass").GroupResource(), clusterClass.Name, 134 fmt.Errorf("ClusterClass cannot be deleted because it is used by %d Cluster(s)", len(clusters))) 135 } 136 return nil, nil 137 } 138 139 func (webhook *ClusterClass) validate(ctx context.Context, oldClusterClass, newClusterClass *clusterv1.ClusterClass) error { 140 // NOTE: ClusterClass and managed topologies are behind ClusterTopology feature gate flag; the web hook 141 // must prevent creating new objects when the feature flag is disabled. 142 if !feature.Gates.Enabled(feature.ClusterTopology) { 143 return field.Forbidden( 144 field.NewPath("spec"), 145 "can be set only if the ClusterTopology feature flag is enabled", 146 ) 147 } 148 var allErrs field.ErrorList 149 150 // Ensure all references are valid. 151 allErrs = append(allErrs, check.ClusterClassReferencesAreValid(newClusterClass)...) 152 153 // Ensure all MachineDeployment classes are unique. 154 allErrs = append(allErrs, check.MachineDeploymentClassesAreUnique(newClusterClass)...) 155 156 // Ensure all MachinePool classes are unique. 157 allErrs = append(allErrs, check.MachinePoolClassesAreUnique(newClusterClass)...) 158 159 // Ensure MachineHealthChecks are valid. 160 allErrs = append(allErrs, validateMachineHealthCheckClasses(newClusterClass)...) 161 162 // Ensure NamingStrategies are valid. 163 allErrs = append(allErrs, validateNamingStrategies(newClusterClass)...) 164 165 // Validate variables. 166 allErrs = append(allErrs, 167 variables.ValidateClusterClassVariables(ctx, newClusterClass.Spec.Variables, field.NewPath("spec", "variables"))..., 168 ) 169 170 // Validate patches. 171 allErrs = append(allErrs, validatePatches(newClusterClass)...) 172 173 // Validate metadata 174 allErrs = append(allErrs, validateClusterClassMetadata(newClusterClass)...) 175 176 // If this is an update run additional validation. 177 if oldClusterClass != nil { 178 // Ensure spec changes are compatible. 179 allErrs = append(allErrs, check.ClusterClassesAreCompatible(oldClusterClass, newClusterClass)...) 180 181 // Retrieve all clusters using the ClusterClass. 182 clusters, err := webhook.getClustersUsingClusterClass(ctx, oldClusterClass) 183 if err != nil { 184 allErrs = append(allErrs, field.InternalError(field.NewPath(""), 185 errors.Wrapf(err, "Clusters using ClusterClass %v can not be retrieved", oldClusterClass.Name))) 186 return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("ClusterClass").GroupKind(), newClusterClass.Name, allErrs) 187 } 188 189 // Ensure no MachineDeploymentClass currently in use has been removed from the ClusterClass. 190 allErrs = append(allErrs, 191 webhook.validateRemovedMachineDeploymentClassesAreNotUsed(clusters, oldClusterClass, newClusterClass)...) 192 193 // Ensure no MachinePoolClass currently in use has been removed from the ClusterClass. 194 allErrs = append(allErrs, 195 webhook.validateRemovedMachinePoolClassesAreNotUsed(clusters, oldClusterClass, newClusterClass)...) 196 197 // Ensure no MachineHealthCheck currently in use has been removed from the ClusterClass. 198 allErrs = append(allErrs, 199 validateUpdatesToMachineHealthCheckClasses(clusters, oldClusterClass, newClusterClass)...) 200 } 201 202 if len(allErrs) > 0 { 203 return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("ClusterClass").GroupKind(), newClusterClass.Name, allErrs) 204 } 205 return nil 206 } 207 208 // validateUpdatesToMachineHealthCheckClasses checks if the updates made to MachineHealthChecks are valid. 209 // It makes sure that if a MachineHealthCheck definition is dropped from the ClusterClass then none of the 210 // clusters using the ClusterClass rely on it to create a MachineHealthCheck. 211 // A cluster relies on an MachineHealthCheck in the ClusterClass if in cluster topology MachineHealthCheck 212 // is explicitly enabled and it does not provide a MachineHealthCheckOverride. 213 func validateUpdatesToMachineHealthCheckClasses(clusters []clusterv1.Cluster, oldClusterClass, newClusterClass *clusterv1.ClusterClass) field.ErrorList { 214 var allErrs field.ErrorList 215 216 // Check if the MachineHealthCheck for the control plane is dropped. 217 if oldClusterClass.Spec.ControlPlane.MachineHealthCheck != nil && newClusterClass.Spec.ControlPlane.MachineHealthCheck == nil { 218 // Make sure that none of the clusters are using this MachineHealthCheck. 219 clustersUsingMHC := []string{} 220 for _, cluster := range clusters { 221 if cluster.Spec.Topology.ControlPlane.MachineHealthCheck != nil && 222 cluster.Spec.Topology.ControlPlane.MachineHealthCheck.Enable != nil && 223 *cluster.Spec.Topology.ControlPlane.MachineHealthCheck.Enable && 224 cluster.Spec.Topology.ControlPlane.MachineHealthCheck.MachineHealthCheckClass.IsZero() { 225 clustersUsingMHC = append(clustersUsingMHC, cluster.Name) 226 } 227 } 228 if len(clustersUsingMHC) != 0 { 229 allErrs = append(allErrs, field.Forbidden( 230 field.NewPath("spec", "controlPlane", "machineHealthCheck"), 231 fmt.Sprintf("MachineHealthCheck cannot be deleted because it is used by Cluster(s) %q", strings.Join(clustersUsingMHC, ",")), 232 )) 233 } 234 } 235 236 // For each MachineDeploymentClass check if the MachineHealthCheck definition is dropped. 237 for i, newMdClass := range newClusterClass.Spec.Workers.MachineDeployments { 238 oldMdClass := machineDeploymentClassOfName(oldClusterClass, newMdClass.Class) 239 if oldMdClass == nil { 240 // This is a new MachineDeploymentClass. Nothing to do here. 241 continue 242 } 243 // If the MachineHealthCheck is dropped then check that no cluster is using it. 244 if oldMdClass.MachineHealthCheck != nil && newMdClass.MachineHealthCheck == nil { 245 clustersUsingMHC := []string{} 246 for _, cluster := range clusters { 247 if cluster.Spec.Topology.Workers == nil { 248 continue 249 } 250 for _, mdTopology := range cluster.Spec.Topology.Workers.MachineDeployments { 251 if mdTopology.Class == newMdClass.Class { 252 if mdTopology.MachineHealthCheck != nil && 253 mdTopology.MachineHealthCheck.Enable != nil && 254 *mdTopology.MachineHealthCheck.Enable && 255 mdTopology.MachineHealthCheck.MachineHealthCheckClass.IsZero() { 256 clustersUsingMHC = append(clustersUsingMHC, cluster.Name) 257 break 258 } 259 } 260 } 261 } 262 if len(clustersUsingMHC) != 0 { 263 allErrs = append(allErrs, field.Forbidden( 264 field.NewPath("spec", "workers", "machineDeployments").Index(i).Child("machineHealthCheck"), 265 fmt.Sprintf("MachineHealthCheck cannot be deleted because it is used by Cluster(s) %q", strings.Join(clustersUsingMHC, ",")), 266 )) 267 } 268 } 269 } 270 271 return allErrs 272 } 273 274 func (webhook *ClusterClass) validateRemovedMachineDeploymentClassesAreNotUsed(clusters []clusterv1.Cluster, oldClusterClass, newClusterClass *clusterv1.ClusterClass) field.ErrorList { 275 var allErrs field.ErrorList 276 277 removedClasses := webhook.removedMachineDeploymentClasses(oldClusterClass, newClusterClass) 278 // If no classes have been removed return early as no further checks are needed. 279 if len(removedClasses) == 0 { 280 return nil 281 } 282 // Error if any Cluster using the ClusterClass uses a MachineDeploymentClass that has been removed. 283 for _, c := range clusters { 284 for _, machineDeploymentTopology := range c.Spec.Topology.Workers.MachineDeployments { 285 if removedClasses.Has(machineDeploymentTopology.Class) { 286 // TODO(killianmuldoon): Improve error printing here so large scale changes don't flood the error log e.g. deduplication, only example usages given. 287 // TODO: consider if we get the index of the MachineDeploymentClass being deleted 288 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "workers", "machineDeployments"), 289 fmt.Sprintf("MachineDeploymentClass %q cannot be deleted because it is used by Cluster %q", 290 machineDeploymentTopology.Class, c.Name), 291 )) 292 } 293 } 294 } 295 return allErrs 296 } 297 298 func (webhook *ClusterClass) validateRemovedMachinePoolClassesAreNotUsed(clusters []clusterv1.Cluster, oldClusterClass, newClusterClass *clusterv1.ClusterClass) field.ErrorList { 299 var allErrs field.ErrorList 300 301 removedClasses := webhook.removedMachinePoolClasses(oldClusterClass, newClusterClass) 302 // If no classes have been removed return early as no further checks are needed. 303 if len(removedClasses) == 0 { 304 return nil 305 } 306 // Error if any Cluster using the ClusterClass uses a MachinePoolClass that has been removed. 307 for _, c := range clusters { 308 for _, machinePoolTopology := range c.Spec.Topology.Workers.MachinePools { 309 if removedClasses.Has(machinePoolTopology.Class) { 310 // TODO(killianmuldoon): Improve error printing here so large scale changes don't flood the error log e.g. deduplication, only example usages given. 311 // TODO: consider if we get the index of the MachinePoolClass being deleted 312 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "workers", "machinePools"), 313 fmt.Sprintf("MachinePoolClass %q cannot be deleted because it is used by Cluster %q", 314 machinePoolTopology.Class, c.Name), 315 )) 316 } 317 } 318 } 319 return allErrs 320 } 321 322 func (webhook *ClusterClass) removedMachineDeploymentClasses(oldClusterClass, newClusterClass *clusterv1.ClusterClass) sets.Set[string] { 323 removedClasses := sets.Set[string]{} 324 325 mdClasses := webhook.classNamesFromMDWorkerClass(newClusterClass.Spec.Workers) 326 for _, oldClass := range oldClusterClass.Spec.Workers.MachineDeployments { 327 if !mdClasses.Has(oldClass.Class) { 328 removedClasses.Insert(oldClass.Class) 329 } 330 } 331 return removedClasses 332 } 333 334 func (webhook *ClusterClass) removedMachinePoolClasses(oldClusterClass, newClusterClass *clusterv1.ClusterClass) sets.Set[string] { 335 removedClasses := sets.Set[string]{} 336 337 mpClasses := webhook.classNamesFromMPWorkerClass(newClusterClass.Spec.Workers) 338 for _, oldClass := range oldClusterClass.Spec.Workers.MachinePools { 339 if !mpClasses.Has(oldClass.Class) { 340 removedClasses.Insert(oldClass.Class) 341 } 342 } 343 return removedClasses 344 } 345 346 // classNamesFromMDWorkerClass returns the set of MachineDeployment class names. 347 func (webhook *ClusterClass) classNamesFromMDWorkerClass(w clusterv1.WorkersClass) sets.Set[string] { 348 classes := sets.Set[string]{} 349 for _, class := range w.MachineDeployments { 350 classes.Insert(class.Class) 351 } 352 return classes 353 } 354 355 // classNamesFromMPWorkerClass returns the set of MachinePool class names. 356 func (webhook *ClusterClass) classNamesFromMPWorkerClass(w clusterv1.WorkersClass) sets.Set[string] { 357 classes := sets.Set[string]{} 358 for _, class := range w.MachinePools { 359 classes.Insert(class.Class) 360 } 361 return classes 362 } 363 364 func (webhook *ClusterClass) getClustersUsingClusterClass(ctx context.Context, clusterClass *clusterv1.ClusterClass) ([]clusterv1.Cluster, error) { 365 clusters := &clusterv1.ClusterList{} 366 err := webhook.Client.List(ctx, clusters, 367 client.MatchingFields{index.ClusterClassNameField: clusterClass.Name}, 368 client.InNamespace(clusterClass.Namespace), 369 ) 370 if err != nil { 371 return nil, err 372 } 373 return clusters.Items, nil 374 } 375 376 func getClusterClassVariablesMapWithReverseIndex(clusterClassVariables []clusterv1.ClusterClassVariable) (map[string]*clusterv1.ClusterClassVariable, map[string]int) { 377 variablesMap := map[string]*clusterv1.ClusterClassVariable{} 378 variablesIndexMap := map[string]int{} 379 380 for i := range clusterClassVariables { 381 variablesMap[clusterClassVariables[i].Name] = &clusterClassVariables[i] 382 variablesIndexMap[clusterClassVariables[i].Name] = i 383 } 384 return variablesMap, variablesIndexMap 385 } 386 387 func validateMachineHealthCheckClasses(clusterClass *clusterv1.ClusterClass) field.ErrorList { 388 var allErrs field.ErrorList 389 390 // Validate ControlPlane MachineHealthCheck if defined. 391 if clusterClass.Spec.ControlPlane.MachineHealthCheck != nil { 392 fldPath := field.NewPath("spec", "controlPlane", "machineHealthCheck") 393 394 allErrs = append(allErrs, validateMachineHealthCheckClass(fldPath, clusterClass.Namespace, 395 clusterClass.Spec.ControlPlane.MachineHealthCheck)...) 396 397 // Ensure ControlPlane does not define a MachineHealthCheck if it does not define MachineInfrastructure. 398 if clusterClass.Spec.ControlPlane.MachineInfrastructure == nil { 399 allErrs = append(allErrs, field.Forbidden( 400 fldPath.Child("machineInfrastructure"), 401 "can be set only if spec.controlPlane.machineInfrastructure is set", 402 )) 403 } 404 } 405 406 // Ensure MachineDeployment MachineHealthChecks define UnhealthyConditions. 407 for i, md := range clusterClass.Spec.Workers.MachineDeployments { 408 if md.MachineHealthCheck == nil { 409 continue 410 } 411 fldPath := field.NewPath("spec", "workers", "machineDeployments", "machineHealthCheck").Index(i) 412 413 allErrs = append(allErrs, validateMachineHealthCheckClass(fldPath, clusterClass.Namespace, md.MachineHealthCheck)...) 414 } 415 return allErrs 416 } 417 418 func validateNamingStrategies(clusterClass *clusterv1.ClusterClass) field.ErrorList { 419 var allErrs field.ErrorList 420 421 if clusterClass.Spec.ControlPlane.NamingStrategy != nil && clusterClass.Spec.ControlPlane.NamingStrategy.Template != nil { 422 name, err := names.ControlPlaneNameGenerator(*clusterClass.Spec.ControlPlane.NamingStrategy.Template, "cluster").GenerateName() 423 templateFldPath := field.NewPath("spec", "controlPlane", "namingStrategy", "template") 424 if err != nil { 425 allErrs = append(allErrs, 426 field.Invalid( 427 templateFldPath, 428 *clusterClass.Spec.ControlPlane.NamingStrategy.Template, 429 fmt.Sprintf("invalid ControlPlane name template: %v", err), 430 )) 431 } else { 432 for _, err := range validation.IsDNS1123Subdomain(name) { 433 allErrs = append(allErrs, field.Invalid(templateFldPath, *clusterClass.Spec.ControlPlane.NamingStrategy.Template, err)) 434 } 435 } 436 } 437 438 for i, md := range clusterClass.Spec.Workers.MachineDeployments { 439 if md.NamingStrategy == nil || md.NamingStrategy.Template == nil { 440 continue 441 } 442 name, err := names.MachineDeploymentNameGenerator(*md.NamingStrategy.Template, "cluster", "mdtopology").GenerateName() 443 templateFldPath := field.NewPath("spec", "workers", "machineDeployments").Index(i).Child("namingStrategy", "template") 444 if err != nil { 445 allErrs = append(allErrs, 446 field.Invalid( 447 templateFldPath, 448 *md.NamingStrategy.Template, 449 fmt.Sprintf("invalid MachineDeployment name template: %v", err), 450 )) 451 } else { 452 for _, err := range validation.IsDNS1123Subdomain(name) { 453 allErrs = append(allErrs, field.Invalid(templateFldPath, *md.NamingStrategy.Template, err)) 454 } 455 } 456 } 457 458 for i, mp := range clusterClass.Spec.Workers.MachinePools { 459 if mp.NamingStrategy == nil || mp.NamingStrategy.Template == nil { 460 continue 461 } 462 name, err := names.MachinePoolNameGenerator(*mp.NamingStrategy.Template, "cluster", "mptopology").GenerateName() 463 templateFldPath := field.NewPath("spec", "workers", "machinePools").Index(i).Child("namingStrategy", "template") 464 if err != nil { 465 allErrs = append(allErrs, 466 field.Invalid( 467 templateFldPath, 468 *mp.NamingStrategy.Template, 469 fmt.Sprintf("invalid MachinePool name template: %v", err), 470 )) 471 } else { 472 for _, err := range validation.IsDNS1123Subdomain(name) { 473 allErrs = append(allErrs, field.Invalid(templateFldPath, *mp.NamingStrategy.Template, err)) 474 } 475 } 476 } 477 478 return allErrs 479 } 480 481 // validateMachineHealthCheckClass validates the MachineHealthCheckSpec fields defined in a MachineHealthCheckClass. 482 func validateMachineHealthCheckClass(fldPath *field.Path, namepace string, m *clusterv1.MachineHealthCheckClass) field.ErrorList { 483 mhc := clusterv1.MachineHealthCheck{ 484 ObjectMeta: metav1.ObjectMeta{ 485 Namespace: namepace, 486 }, 487 Spec: clusterv1.MachineHealthCheckSpec{ 488 NodeStartupTimeout: m.NodeStartupTimeout, 489 MaxUnhealthy: m.MaxUnhealthy, 490 UnhealthyConditions: m.UnhealthyConditions, 491 UnhealthyRange: m.UnhealthyRange, 492 RemediationTemplate: m.RemediationTemplate, 493 }} 494 495 return (&MachineHealthCheck{}).validateCommonFields(&mhc, fldPath) 496 } 497 498 func validateClusterClassMetadata(clusterClass *clusterv1.ClusterClass) field.ErrorList { 499 var allErrs field.ErrorList 500 allErrs = append(allErrs, clusterClass.Spec.ControlPlane.Metadata.Validate(field.NewPath("spec", "controlPlane", "metadata"))...) 501 for idx, m := range clusterClass.Spec.Workers.MachineDeployments { 502 allErrs = append(allErrs, m.Template.Metadata.Validate(field.NewPath("spec", "workers", "machineDeployments").Index(idx).Child("template", "metadata"))...) 503 } 504 for idx, m := range clusterClass.Spec.Workers.MachinePools { 505 allErrs = append(allErrs, m.Template.Metadata.Validate(field.NewPath("spec", "workers", "machinePools").Index(idx).Child("template", "metadata"))...) 506 } 507 return allErrs 508 }