sigs.k8s.io/cluster-api@v1.7.1/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  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/pkg/errors"
    26  	v1 "k8s.io/api/admission/v1"
    27  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/labels"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	"k8s.io/apimachinery/pkg/util/validation/field"
    33  	"k8s.io/utils/ptr"
    34  	ctrl "sigs.k8s.io/controller-runtime"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    37  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    38  
    39  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    40  	"sigs.k8s.io/cluster-api/feature"
    41  	"sigs.k8s.io/cluster-api/util/labels/format"
    42  	"sigs.k8s.io/cluster-api/util/version"
    43  )
    44  
    45  func (webhook *MachineSet) SetupWebhookWithManager(mgr ctrl.Manager) error {
    46  	if webhook.decoder == nil {
    47  		webhook.decoder = admission.NewDecoder(mgr.GetScheme())
    48  	}
    49  
    50  	return ctrl.NewWebhookManagedBy(mgr).
    51  		For(&clusterv1.MachineSet{}).
    52  		WithDefaulter(webhook).
    53  		WithValidator(webhook).
    54  		Complete()
    55  }
    56  
    57  // +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
    58  // +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
    59  
    60  // MachineSet implements a validation and defaulting webhook for MachineSet.
    61  type MachineSet struct {
    62  	decoder *admission.Decoder
    63  }
    64  
    65  var _ webhook.CustomDefaulter = &MachineSet{}
    66  var _ webhook.CustomValidator = &MachineSet{}
    67  
    68  // Default sets default MachineSet field values.
    69  func (webhook *MachineSet) Default(ctx context.Context, obj runtime.Object) error {
    70  	m, ok := obj.(*clusterv1.MachineSet)
    71  	if !ok {
    72  		return apierrors.NewBadRequest(fmt.Sprintf("expected a MachineSet but got a %T", obj))
    73  	}
    74  
    75  	req, err := admission.RequestFromContext(ctx)
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	dryRun := false
    81  	if req.DryRun != nil {
    82  		dryRun = *req.DryRun
    83  	}
    84  
    85  	var oldMS *clusterv1.MachineSet
    86  	if req.Operation == v1.Update {
    87  		oldMS = &clusterv1.MachineSet{}
    88  		if err := webhook.decoder.DecodeRaw(req.OldObject, oldMS); err != nil {
    89  			return errors.Wrapf(err, "failed to decode oldObject to MachineSet")
    90  		}
    91  	}
    92  
    93  	if m.Labels == nil {
    94  		m.Labels = make(map[string]string)
    95  	}
    96  	m.Labels[clusterv1.ClusterNameLabel] = m.Spec.ClusterName
    97  
    98  	replicas, err := calculateMachineSetReplicas(ctx, oldMS, m, dryRun)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	m.Spec.Replicas = ptr.To[int32](replicas)
   103  
   104  	if m.Spec.DeletePolicy == "" {
   105  		randomPolicy := string(clusterv1.RandomMachineSetDeletePolicy)
   106  		m.Spec.DeletePolicy = randomPolicy
   107  	}
   108  
   109  	if m.Spec.Selector.MatchLabels == nil {
   110  		m.Spec.Selector.MatchLabels = make(map[string]string)
   111  	}
   112  
   113  	if m.Spec.Template.Labels == nil {
   114  		m.Spec.Template.Labels = make(map[string]string)
   115  	}
   116  
   117  	if len(m.Spec.Selector.MatchLabels) == 0 && len(m.Spec.Selector.MatchExpressions) == 0 {
   118  		// Note: MustFormatValue is used here as the value of this label will be a hash if the MachineSet name is longer than 63 characters.
   119  		m.Spec.Selector.MatchLabels[clusterv1.MachineSetNameLabel] = format.MustFormatValue(m.Name)
   120  		m.Spec.Template.Labels[clusterv1.MachineSetNameLabel] = format.MustFormatValue(m.Name)
   121  	}
   122  
   123  	if m.Spec.Template.Spec.Version != nil && !strings.HasPrefix(*m.Spec.Template.Spec.Version, "v") {
   124  		normalizedVersion := "v" + *m.Spec.Template.Spec.Version
   125  		m.Spec.Template.Spec.Version = &normalizedVersion
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
   132  func (webhook *MachineSet) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
   133  	m, ok := obj.(*clusterv1.MachineSet)
   134  	if !ok {
   135  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachineSet but got a %T", obj))
   136  	}
   137  
   138  	return nil, webhook.validate(nil, m)
   139  }
   140  
   141  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
   142  func (webhook *MachineSet) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   143  	oldMS, ok := oldObj.(*clusterv1.MachineSet)
   144  	if !ok {
   145  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachineSet but got a %T", oldObj))
   146  	}
   147  	newMS, ok := newObj.(*clusterv1.MachineSet)
   148  	if !ok {
   149  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachineSet but got a %T", newObj))
   150  	}
   151  
   152  	return nil, webhook.validate(oldMS, newMS)
   153  }
   154  
   155  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
   156  func (webhook *MachineSet) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
   157  	return nil, nil
   158  }
   159  
   160  func (webhook *MachineSet) validate(oldMS, newMS *clusterv1.MachineSet) error {
   161  	var allErrs field.ErrorList
   162  	specPath := field.NewPath("spec")
   163  	selector, err := metav1.LabelSelectorAsSelector(&newMS.Spec.Selector)
   164  	if err != nil {
   165  		allErrs = append(
   166  			allErrs,
   167  			field.Invalid(
   168  				specPath.Child("selector"),
   169  				newMS.Spec.Selector,
   170  				err.Error(),
   171  			),
   172  		)
   173  	} else if !selector.Matches(labels.Set(newMS.Spec.Template.Labels)) {
   174  		allErrs = append(
   175  			allErrs,
   176  			field.Invalid(
   177  				specPath.Child("template", "metadata", "labels"),
   178  				newMS.Spec.Template.ObjectMeta.Labels,
   179  				fmt.Sprintf("must match spec.selector %q", selector.String()),
   180  			),
   181  		)
   182  	}
   183  
   184  	if feature.Gates.Enabled(feature.MachineSetPreflightChecks) {
   185  		if err := validateSkippedMachineSetPreflightChecks(newMS); err != nil {
   186  			allErrs = append(allErrs, err)
   187  		}
   188  	}
   189  
   190  	if oldMS != nil && oldMS.Spec.ClusterName != newMS.Spec.ClusterName {
   191  		allErrs = append(
   192  			allErrs,
   193  			field.Forbidden(
   194  				specPath.Child("clusterName"),
   195  				"field is immutable",
   196  			),
   197  		)
   198  	}
   199  
   200  	if newMS.Spec.Template.Spec.Version != nil {
   201  		if !version.KubeSemver.MatchString(*newMS.Spec.Template.Spec.Version) {
   202  			allErrs = append(
   203  				allErrs,
   204  				field.Invalid(
   205  					specPath.Child("template", "spec", "version"),
   206  					*newMS.Spec.Template.Spec.Version,
   207  					"must be a valid semantic version",
   208  				),
   209  			)
   210  		}
   211  	}
   212  
   213  	// Validate the metadata of the template.
   214  	allErrs = append(allErrs, newMS.Spec.Template.ObjectMeta.Validate(specPath.Child("template", "metadata"))...)
   215  
   216  	if len(allErrs) == 0 {
   217  		return nil
   218  	}
   219  
   220  	return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("MachineSet").GroupKind(), newMS.Name, allErrs)
   221  }
   222  
   223  func validateSkippedMachineSetPreflightChecks(o client.Object) *field.Error {
   224  	if o == nil {
   225  		return nil
   226  	}
   227  	skip := o.GetAnnotations()[clusterv1.MachineSetSkipPreflightChecksAnnotation]
   228  	if skip == "" {
   229  		return nil
   230  	}
   231  
   232  	supported := sets.New[clusterv1.MachineSetPreflightCheck](
   233  		clusterv1.MachineSetPreflightCheckAll,
   234  		clusterv1.MachineSetPreflightCheckKubeadmVersionSkew,
   235  		clusterv1.MachineSetPreflightCheckKubernetesVersionSkew,
   236  		clusterv1.MachineSetPreflightCheckControlPlaneIsStable,
   237  	)
   238  
   239  	skippedList := strings.Split(skip, ",")
   240  	invalid := []clusterv1.MachineSetPreflightCheck{}
   241  	for i := range skippedList {
   242  		skipped := clusterv1.MachineSetPreflightCheck(strings.TrimSpace(skippedList[i]))
   243  		if !supported.Has(skipped) {
   244  			invalid = append(invalid, skipped)
   245  		}
   246  	}
   247  	if len(invalid) > 0 {
   248  		return field.Invalid(
   249  			field.NewPath("metadata", "annotations", clusterv1.MachineSetSkipPreflightChecksAnnotation),
   250  			invalid,
   251  			fmt.Sprintf("skipped preflight check(s) must be among: %v", sets.List(supported)),
   252  		)
   253  	}
   254  	return nil
   255  }
   256  
   257  // calculateMachineSetReplicas calculates the default value of the replicas field.
   258  // The value will be calculated based on the following logic:
   259  // * if replicas is already set on newMS, keep the current value
   260  // * if the autoscaler min size and max size annotations are set:
   261  //   - if it's a new MachineSet, use min size
   262  //   - if the replicas field of the old MachineSet is < min size, use min size
   263  //   - if the replicas field of the old MachineSet is > max size, use max size
   264  //   - if the replicas field of the old MachineSet is in the (min size, max size) range, keep the value from the oldMS
   265  //
   266  // * otherwise use 1
   267  //
   268  // The goal of this logic is to provide a smoother UX for clusters using the Kubernetes autoscaler.
   269  // Note: Autoscaler only takes over control of the replicas field if the replicas value is in the (min size, max size) range.
   270  //
   271  // We are supporting the following use cases:
   272  // * A new MS is created and replicas should be managed by the autoscaler
   273  //   - Either via the default annotation or via the min size and max size annotations the replicas field
   274  //     is defaulted to a value which is within the (min size, max size) range so the autoscaler can take control.
   275  //
   276  // * An existing MS which initially wasn't controlled by the autoscaler should be later controlled by the autoscaler
   277  //   - To adopt an existing MS users can use the default, min size and max size annotations to enable the autoscaler
   278  //     and to ensure the replicas field is within the (min size, max size) range. Without the annotations handing over
   279  //     control to the autoscaler by unsetting the replicas field would lead to the field being set to 1. This is very
   280  //     disruptive for existing Machines and if 1 is outside the (min size, max size) range the autoscaler won't take
   281  //     control.
   282  //
   283  // Notes:
   284  //   - While the min size and max size annotations of the autoscaler provide the best UX, other autoscalers can use the
   285  //     DefaultReplicasAnnotation if they have similar use cases.
   286  func calculateMachineSetReplicas(ctx context.Context, oldMS *clusterv1.MachineSet, newMS *clusterv1.MachineSet, dryRun bool) (int32, error) {
   287  	// If replicas is already set => Keep the current value.
   288  	if newMS.Spec.Replicas != nil {
   289  		return *newMS.Spec.Replicas, nil
   290  	}
   291  
   292  	log := ctrl.LoggerFrom(ctx)
   293  
   294  	// If both autoscaler annotations are set, use them to calculate the default value.
   295  	minSizeString, hasMinSizeAnnotation := newMS.Annotations[clusterv1.AutoscalerMinSizeAnnotation]
   296  	maxSizeString, hasMaxSizeAnnotation := newMS.Annotations[clusterv1.AutoscalerMaxSizeAnnotation]
   297  	if hasMinSizeAnnotation && hasMaxSizeAnnotation {
   298  		minSize, err := strconv.ParseInt(minSizeString, 10, 32)
   299  		if err != nil {
   300  			return 0, errors.Wrapf(err, "failed to caculate MachineSet replicas value: could not parse the value of the %q annotation", clusterv1.AutoscalerMinSizeAnnotation)
   301  		}
   302  		maxSize, err := strconv.ParseInt(maxSizeString, 10, 32)
   303  		if err != nil {
   304  			return 0, errors.Wrapf(err, "failed to caculate MachineSet replicas value: could not parse the value of the %q annotation", clusterv1.AutoscalerMaxSizeAnnotation)
   305  		}
   306  
   307  		// If it's a new MachineSet => Use the min size.
   308  		// Note: This will result in a scale up to get into the range where autoscaler takes over.
   309  		if oldMS == nil {
   310  			if !dryRun {
   311  				log.V(2).Info(fmt.Sprintf("Replica field has been defaulted to %d based on the %s annotation (MS is a new MS)", minSize, clusterv1.AutoscalerMinSizeAnnotation))
   312  			}
   313  			return int32(minSize), nil
   314  		}
   315  
   316  		// Otherwise we are handing over the control for the replicas field for an existing MachineSet
   317  		// to the autoscaler.
   318  
   319  		switch {
   320  		// If the old MachineSet doesn't have replicas set => Use the min size.
   321  		// Note: As defaulting always sets the replica field, this case should not be possible
   322  		// We only have this handling to be 100% safe against panics.
   323  		case oldMS.Spec.Replicas == nil:
   324  			if !dryRun {
   325  				log.V(2).Info(fmt.Sprintf("Replica field has been defaulted to %d based on the %s annotation (old MS didn't have replicas set)", minSize, clusterv1.AutoscalerMinSizeAnnotation))
   326  			}
   327  			return int32(minSize), nil
   328  		// If the old MachineSet replicas are lower than min size => Use the min size.
   329  		// Note: This will result in a scale up to get into the range where autoscaler takes over.
   330  		case *oldMS.Spec.Replicas < int32(minSize):
   331  			if !dryRun {
   332  				log.V(2).Info(fmt.Sprintf("Replica field has been defaulted to %d based on the %s annotation (old MS had replicas below min size)", minSize, clusterv1.AutoscalerMinSizeAnnotation))
   333  			}
   334  			return int32(minSize), nil
   335  		// If the old MachineSet replicas are higher than max size => Use the max size.
   336  		// Note: This will result in a scale down to get into the range where autoscaler takes over.
   337  		case *oldMS.Spec.Replicas > int32(maxSize):
   338  			if !dryRun {
   339  				log.V(2).Info(fmt.Sprintf("Replica field has been defaulted to %d based on the %s annotation (old MS had replicas above max size)", maxSize, clusterv1.AutoscalerMaxSizeAnnotation))
   340  			}
   341  			return int32(maxSize), nil
   342  		// If the old MachineSet replicas are between min and max size => Keep the current value.
   343  		default:
   344  			if !dryRun {
   345  				log.V(2).Info(fmt.Sprintf("Replica field has been defaulted to %d based on replicas of the old MachineSet (old MS had replicas within min size / max size range)", *oldMS.Spec.Replicas))
   346  			}
   347  			return *oldMS.Spec.Replicas, nil
   348  		}
   349  	}
   350  
   351  	// If neither the default nor the autoscaler annotations are set => Default to 1.
   352  	if !dryRun {
   353  		log.V(2).Info("Replica field has been defaulted to 1")
   354  	}
   355  	return 1, nil
   356  }