sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/machinehealthcheck.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  	"time"
    23  
    24  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/apimachinery/pkg/util/intstr"
    28  	"k8s.io/apimachinery/pkg/util/validation/field"
    29  	ctrl "sigs.k8s.io/controller-runtime"
    30  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    31  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    32  
    33  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    34  )
    35  
    36  var (
    37  	// Minimum time allowed for a node to start up.
    38  	minNodeStartupTimeout = metav1.Duration{Duration: 30 * time.Second}
    39  	// We allow users to disable the nodeStartupTimeout by setting the duration to 0.
    40  	disabledNodeStartupTimeout = clusterv1.ZeroDuration
    41  )
    42  
    43  // SetMinNodeStartupTimeout allows users to optionally set a custom timeout
    44  // for the validation webhook.
    45  //
    46  // This function is mostly used within envtest (integration tests), and should
    47  // never be used in a production environment.
    48  func SetMinNodeStartupTimeout(d metav1.Duration) {
    49  	minNodeStartupTimeout = d
    50  }
    51  
    52  func (webhook *MachineHealthCheck) SetupWebhookWithManager(mgr ctrl.Manager) error {
    53  	return ctrl.NewWebhookManagedBy(mgr).
    54  		For(&clusterv1.MachineHealthCheck{}).
    55  		WithDefaulter(webhook).
    56  		WithValidator(webhook).
    57  		Complete()
    58  }
    59  
    60  // +kubebuilder:webhook:verbs=create;update,path=/validate-cluster-x-k8s-io-v1beta1-machinehealthcheck,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machinehealthchecks,versions=v1beta1,name=validation.machinehealthcheck.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    61  // +kubebuilder:webhook:verbs=create;update,path=/mutate-cluster-x-k8s-io-v1beta1-machinehealthcheck,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machinehealthchecks,versions=v1beta1,name=default.machinehealthcheck.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    62  
    63  // MachineHealthCheck implements a validation and defaulting webhook for MachineHealthCheck.
    64  type MachineHealthCheck struct{}
    65  
    66  var _ webhook.CustomDefaulter = &MachineHealthCheck{}
    67  var _ webhook.CustomValidator = &MachineHealthCheck{}
    68  
    69  // Default implements webhook.CustomDefaulter so a webhook will be registered for the type.
    70  func (webhook *MachineHealthCheck) Default(_ context.Context, obj runtime.Object) error {
    71  	m, ok := obj.(*clusterv1.MachineHealthCheck)
    72  	if !ok {
    73  		return apierrors.NewBadRequest(fmt.Sprintf("expected a MachineHealthCheck but got a %T", obj))
    74  	}
    75  
    76  	if m.Labels == nil {
    77  		m.Labels = make(map[string]string)
    78  	}
    79  	m.Labels[clusterv1.ClusterNameLabel] = m.Spec.ClusterName
    80  
    81  	if m.Spec.MaxUnhealthy == nil {
    82  		defaultMaxUnhealthy := intstr.FromString("100%")
    83  		m.Spec.MaxUnhealthy = &defaultMaxUnhealthy
    84  	}
    85  
    86  	if m.Spec.NodeStartupTimeout == nil {
    87  		m.Spec.NodeStartupTimeout = &clusterv1.DefaultNodeStartupTimeout
    88  	}
    89  
    90  	if m.Spec.RemediationTemplate != nil && m.Spec.RemediationTemplate.Namespace == "" {
    91  		m.Spec.RemediationTemplate.Namespace = m.Namespace
    92  	}
    93  
    94  	return nil
    95  }
    96  
    97  // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type.
    98  func (webhook *MachineHealthCheck) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
    99  	m, ok := obj.(*clusterv1.MachineHealthCheck)
   100  	if !ok {
   101  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachineHealthCheck but got a %T", obj))
   102  	}
   103  
   104  	return nil, webhook.validate(nil, m)
   105  }
   106  
   107  // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type.
   108  func (webhook *MachineHealthCheck) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   109  	oldM, ok := oldObj.(*clusterv1.MachineHealthCheck)
   110  	if !ok {
   111  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachineHealthCheck but got a %T", oldObj))
   112  	}
   113  	newM, ok := newObj.(*clusterv1.MachineHealthCheck)
   114  	if !ok {
   115  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachineHealthCheck but got a %T", newObj))
   116  	}
   117  
   118  	return nil, webhook.validate(oldM, newM)
   119  }
   120  
   121  // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type.
   122  func (webhook *MachineHealthCheck) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
   123  	return nil, nil
   124  }
   125  
   126  func (webhook *MachineHealthCheck) validate(oldMHC, newMHC *clusterv1.MachineHealthCheck) error {
   127  	var allErrs field.ErrorList
   128  	specPath := field.NewPath("spec")
   129  
   130  	// Validate selector parses as Selector
   131  	selector, err := metav1.LabelSelectorAsSelector(&newMHC.Spec.Selector)
   132  	if err != nil {
   133  		allErrs = append(
   134  			allErrs,
   135  			field.Invalid(specPath.Child("selector"), newMHC.Spec.Selector, err.Error()),
   136  		)
   137  	}
   138  
   139  	// Validate that the selector isn't empty.
   140  	if selector != nil && selector.Empty() {
   141  		allErrs = append(
   142  			allErrs,
   143  			field.Required(specPath.Child("selector"), "selector must not be empty"),
   144  		)
   145  	}
   146  
   147  	if clusterName, ok := newMHC.Spec.Selector.MatchLabels[clusterv1.ClusterNameLabel]; ok && clusterName != newMHC.Spec.ClusterName {
   148  		allErrs = append(
   149  			allErrs,
   150  			field.Invalid(specPath.Child("selector"), newMHC.Spec.Selector, "cannot specify a cluster selector other than the one specified by ClusterName"))
   151  	}
   152  
   153  	if oldMHC != nil && oldMHC.Spec.ClusterName != newMHC.Spec.ClusterName {
   154  		allErrs = append(
   155  			allErrs,
   156  			field.Forbidden(specPath.Child("clusterName"), "field is immutable"),
   157  		)
   158  	}
   159  
   160  	allErrs = append(allErrs, webhook.validateCommonFields(newMHC, specPath)...)
   161  
   162  	if len(allErrs) == 0 {
   163  		return nil
   164  	}
   165  	return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("MachineHealthCheck").GroupKind(), newMHC.Name, allErrs)
   166  }
   167  
   168  // ValidateCommonFields validates UnhealthyConditions NodeStartupTimeout, MaxUnhealthy, and RemediationTemplate of the MHC.
   169  // These are the fields in common with other types which define MachineHealthChecks such as MachineHealthCheckClass and MachineHealthCheckTopology.
   170  func (webhook *MachineHealthCheck) validateCommonFields(m *clusterv1.MachineHealthCheck, fldPath *field.Path) field.ErrorList {
   171  	var allErrs field.ErrorList
   172  
   173  	if m.Spec.NodeStartupTimeout != nil &&
   174  		m.Spec.NodeStartupTimeout.Seconds() != disabledNodeStartupTimeout.Seconds() &&
   175  		m.Spec.NodeStartupTimeout.Seconds() < minNodeStartupTimeout.Seconds() {
   176  		allErrs = append(
   177  			allErrs,
   178  			field.Invalid(fldPath.Child("nodeStartupTimeout"), m.Spec.NodeStartupTimeout.String(), "must be at least 30s"),
   179  		)
   180  	}
   181  	if m.Spec.MaxUnhealthy != nil {
   182  		if _, err := intstr.GetScaledValueFromIntOrPercent(m.Spec.MaxUnhealthy, 0, false); err != nil {
   183  			allErrs = append(
   184  				allErrs,
   185  				field.Invalid(fldPath.Child("maxUnhealthy"), m.Spec.MaxUnhealthy, fmt.Sprintf("must be either an int or a percentage: %v", err.Error())),
   186  			)
   187  		}
   188  	}
   189  	if m.Spec.RemediationTemplate != nil && m.Spec.RemediationTemplate.Namespace != m.Namespace {
   190  		allErrs = append(
   191  			allErrs,
   192  			field.Invalid(
   193  				fldPath.Child("remediationTemplate", "namespace"),
   194  				m.Spec.RemediationTemplate.Namespace,
   195  				"must match metadata.namespace",
   196  			),
   197  		)
   198  	}
   199  
   200  	if len(m.Spec.UnhealthyConditions) == 0 {
   201  		allErrs = append(allErrs, field.Forbidden(
   202  			fldPath.Child("unhealthyConditions"),
   203  			"must have at least one entry",
   204  		))
   205  	}
   206  
   207  	return allErrs
   208  }