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 }