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  }