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 }