sigs.k8s.io/cluster-api@v1.6.3/internal/webhooks/machineset.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  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/labels"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	"k8s.io/apimachinery/pkg/util/validation/field"
    30  	ctrl "sigs.k8s.io/controller-runtime"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    33  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    34  
    35  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    36  	"sigs.k8s.io/cluster-api/feature"
    37  	"sigs.k8s.io/cluster-api/util/labels/format"
    38  	"sigs.k8s.io/cluster-api/util/version"
    39  )
    40  
    41  func (webhook *MachineSet) SetupWebhookWithManager(mgr ctrl.Manager) error {
    42  	return ctrl.NewWebhookManagedBy(mgr).
    43  		For(&clusterv1.MachineSet{}).
    44  		WithDefaulter(webhook).
    45  		WithValidator(webhook).
    46  		Complete()
    47  }
    48  
    49  // +kubebuilder:webhook:verbs=create;update,path=/validate-cluster-x-k8s-io-v1beta1-machineset,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machinesets,versions=v1beta1,name=validation.machineset.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    50  // +kubebuilder:webhook:verbs=create;update,path=/mutate-cluster-x-k8s-io-v1beta1-machineset,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machinesets,versions=v1beta1,name=default.machineset.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    51  
    52  // MachineSet implements a validation and defaulting webhook for MachineSet.
    53  type MachineSet struct{}
    54  
    55  var _ webhook.CustomDefaulter = &MachineSet{}
    56  var _ webhook.CustomValidator = &MachineSet{}
    57  
    58  // Default sets default MachineSet field values.
    59  func (webhook *MachineSet) Default(_ context.Context, obj runtime.Object) error {
    60  	m, ok := obj.(*clusterv1.MachineSet)
    61  	if !ok {
    62  		return apierrors.NewBadRequest(fmt.Sprintf("expected a MachineSet but got a %T", obj))
    63  	}
    64  
    65  	if m.Labels == nil {
    66  		m.Labels = make(map[string]string)
    67  	}
    68  	m.Labels[clusterv1.ClusterNameLabel] = m.Spec.ClusterName
    69  
    70  	if m.Spec.DeletePolicy == "" {
    71  		randomPolicy := string(clusterv1.RandomMachineSetDeletePolicy)
    72  		m.Spec.DeletePolicy = randomPolicy
    73  	}
    74  
    75  	if m.Spec.Selector.MatchLabels == nil {
    76  		m.Spec.Selector.MatchLabels = make(map[string]string)
    77  	}
    78  
    79  	if m.Spec.Template.Labels == nil {
    80  		m.Spec.Template.Labels = make(map[string]string)
    81  	}
    82  
    83  	if len(m.Spec.Selector.MatchLabels) == 0 && len(m.Spec.Selector.MatchExpressions) == 0 {
    84  		// Note: MustFormatValue is used here as the value of this label will be a hash if the MachineSet name is longer than 63 characters.
    85  		m.Spec.Selector.MatchLabels[clusterv1.MachineSetNameLabel] = format.MustFormatValue(m.Name)
    86  		m.Spec.Template.Labels[clusterv1.MachineSetNameLabel] = format.MustFormatValue(m.Name)
    87  	}
    88  
    89  	if m.Spec.Template.Spec.Version != nil && !strings.HasPrefix(*m.Spec.Template.Spec.Version, "v") {
    90  		normalizedVersion := "v" + *m.Spec.Template.Spec.Version
    91  		m.Spec.Template.Spec.Version = &normalizedVersion
    92  	}
    93  
    94  	return nil
    95  }
    96  
    97  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
    98  func (webhook *MachineSet) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
    99  	m, ok := obj.(*clusterv1.MachineSet)
   100  	if !ok {
   101  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachineSet but got a %T", obj))
   102  	}
   103  
   104  	return nil, webhook.validate(nil, m)
   105  }
   106  
   107  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
   108  func (webhook *MachineSet) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   109  	oldMS, ok := oldObj.(*clusterv1.MachineSet)
   110  	if !ok {
   111  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachineSet but got a %T", oldObj))
   112  	}
   113  	newMS, ok := newObj.(*clusterv1.MachineSet)
   114  	if !ok {
   115  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachineSet but got a %T", newObj))
   116  	}
   117  
   118  	return nil, webhook.validate(oldMS, newMS)
   119  }
   120  
   121  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
   122  func (webhook *MachineSet) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
   123  	return nil, nil
   124  }
   125  
   126  func (webhook *MachineSet) validate(oldMS, newMS *clusterv1.MachineSet) error {
   127  	var allErrs field.ErrorList
   128  	specPath := field.NewPath("spec")
   129  	selector, err := metav1.LabelSelectorAsSelector(&newMS.Spec.Selector)
   130  	if err != nil {
   131  		allErrs = append(
   132  			allErrs,
   133  			field.Invalid(
   134  				specPath.Child("selector"),
   135  				newMS.Spec.Selector,
   136  				err.Error(),
   137  			),
   138  		)
   139  	} else if !selector.Matches(labels.Set(newMS.Spec.Template.Labels)) {
   140  		allErrs = append(
   141  			allErrs,
   142  			field.Invalid(
   143  				specPath.Child("template", "metadata", "labels"),
   144  				newMS.Spec.Template.ObjectMeta.Labels,
   145  				fmt.Sprintf("must match spec.selector %q", selector.String()),
   146  			),
   147  		)
   148  	}
   149  
   150  	if feature.Gates.Enabled(feature.MachineSetPreflightChecks) {
   151  		if err := validateSkippedMachineSetPreflightChecks(newMS); err != nil {
   152  			allErrs = append(allErrs, err)
   153  		}
   154  	}
   155  
   156  	if oldMS != nil && oldMS.Spec.ClusterName != newMS.Spec.ClusterName {
   157  		allErrs = append(
   158  			allErrs,
   159  			field.Forbidden(
   160  				specPath.Child("clusterName"),
   161  				"field is immutable",
   162  			),
   163  		)
   164  	}
   165  
   166  	if newMS.Spec.Template.Spec.Version != nil {
   167  		if !version.KubeSemver.MatchString(*newMS.Spec.Template.Spec.Version) {
   168  			allErrs = append(
   169  				allErrs,
   170  				field.Invalid(
   171  					specPath.Child("template", "spec", "version"),
   172  					*newMS.Spec.Template.Spec.Version,
   173  					"must be a valid semantic version",
   174  				),
   175  			)
   176  		}
   177  	}
   178  
   179  	// Validate the metadata of the template.
   180  	allErrs = append(allErrs, newMS.Spec.Template.ObjectMeta.Validate(specPath.Child("template", "metadata"))...)
   181  
   182  	if len(allErrs) == 0 {
   183  		return nil
   184  	}
   185  
   186  	return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("MachineSet").GroupKind(), newMS.Name, allErrs)
   187  }
   188  
   189  func validateSkippedMachineSetPreflightChecks(o client.Object) *field.Error {
   190  	if o == nil {
   191  		return nil
   192  	}
   193  	skip := o.GetAnnotations()[clusterv1.MachineSetSkipPreflightChecksAnnotation]
   194  	if skip == "" {
   195  		return nil
   196  	}
   197  
   198  	supported := sets.New[clusterv1.MachineSetPreflightCheck](
   199  		clusterv1.MachineSetPreflightCheckAll,
   200  		clusterv1.MachineSetPreflightCheckKubeadmVersionSkew,
   201  		clusterv1.MachineSetPreflightCheckKubernetesVersionSkew,
   202  		clusterv1.MachineSetPreflightCheckControlPlaneIsStable,
   203  	)
   204  
   205  	skippedList := strings.Split(skip, ",")
   206  	invalid := []clusterv1.MachineSetPreflightCheck{}
   207  	for i := range skippedList {
   208  		skipped := clusterv1.MachineSetPreflightCheck(strings.TrimSpace(skippedList[i]))
   209  		if !supported.Has(skipped) {
   210  			invalid = append(invalid, skipped)
   211  		}
   212  	}
   213  	if len(invalid) > 0 {
   214  		return field.Invalid(
   215  			field.NewPath("metadata", "annotations", clusterv1.MachineSetSkipPreflightChecksAnnotation),
   216  			invalid,
   217  			fmt.Sprintf("skipped preflight check(s) must be among: %v", sets.List(supported)),
   218  		)
   219  	}
   220  	return nil
   221  }