sigs.k8s.io/cluster-api@v1.7.1/exp/internal/webhooks/machinepool.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 "k8s.io/apimachinery/pkg/runtime" 26 "k8s.io/apimachinery/pkg/util/validation/field" 27 "k8s.io/utils/ptr" 28 ctrl "sigs.k8s.io/controller-runtime" 29 "sigs.k8s.io/controller-runtime/pkg/webhook" 30 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 31 32 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 33 expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 34 "sigs.k8s.io/cluster-api/feature" 35 "sigs.k8s.io/cluster-api/util/version" 36 ) 37 38 func (webhook *MachinePool) SetupWebhookWithManager(mgr ctrl.Manager) error { 39 return ctrl.NewWebhookManagedBy(mgr). 40 For(&expv1.MachinePool{}). 41 WithDefaulter(webhook). 42 WithValidator(webhook). 43 Complete() 44 } 45 46 // +kubebuilder:webhook:verbs=create;update,path=/validate-cluster-x-k8s-io-v1beta1-machinepool,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machinepools,versions=v1beta1,name=validation.machinepool.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 47 // +kubebuilder:webhook:verbs=create;update,path=/mutate-cluster-x-k8s-io-v1beta1-machinepool,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machinepools,versions=v1beta1,name=default.machinepool.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 48 49 // MachinePool implements a validation and defaulting webhook for MachinePool. 50 type MachinePool struct{} 51 52 var _ webhook.CustomValidator = &MachinePool{} 53 var _ webhook.CustomDefaulter = &MachinePool{} 54 55 // Default implements webhook.Defaulter so a webhook will be registered for the type. 56 func (webhook *MachinePool) Default(_ context.Context, obj runtime.Object) error { 57 m, ok := obj.(*expv1.MachinePool) 58 if !ok { 59 return apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", obj)) 60 } 61 62 if m.Labels == nil { 63 m.Labels = make(map[string]string) 64 } 65 m.Labels[clusterv1.ClusterNameLabel] = m.Spec.ClusterName 66 67 if m.Spec.Replicas == nil { 68 m.Spec.Replicas = ptr.To[int32](1) 69 } 70 71 if m.Spec.MinReadySeconds == nil { 72 m.Spec.MinReadySeconds = ptr.To[int32](0) 73 } 74 75 if m.Spec.Template.Spec.Bootstrap.ConfigRef != nil && m.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace == "" { 76 m.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace = m.Namespace 77 } 78 79 if m.Spec.Template.Spec.InfrastructureRef.Namespace == "" { 80 m.Spec.Template.Spec.InfrastructureRef.Namespace = m.Namespace 81 } 82 83 // tolerate version strings without a "v" prefix: prepend it if it's not there. 84 if m.Spec.Template.Spec.Version != nil && !strings.HasPrefix(*m.Spec.Template.Spec.Version, "v") { 85 normalizedVersion := "v" + *m.Spec.Template.Spec.Version 86 m.Spec.Template.Spec.Version = &normalizedVersion 87 } 88 return nil 89 } 90 91 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type. 92 func (webhook *MachinePool) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { 93 mp, ok := obj.(*expv1.MachinePool) 94 if !ok { 95 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", obj)) 96 } 97 98 return nil, webhook.validate(nil, mp) 99 } 100 101 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. 102 func (webhook *MachinePool) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { 103 oldMP, ok := oldObj.(*expv1.MachinePool) 104 if !ok { 105 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", oldObj)) 106 } 107 newMP, ok := newObj.(*expv1.MachinePool) 108 if !ok { 109 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", newObj)) 110 } 111 return nil, webhook.validate(oldMP, newMP) 112 } 113 114 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type. 115 func (webhook *MachinePool) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { 116 mp, ok := obj.(*expv1.MachinePool) 117 if !ok { 118 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", obj)) 119 } 120 121 return nil, webhook.validate(nil, mp) 122 } 123 124 func (webhook *MachinePool) validate(oldObj, newObj *expv1.MachinePool) error { 125 // NOTE: MachinePool is behind MachinePool feature gate flag; the web hook 126 // must prevent creating newObj objects when the feature flag is disabled. 127 specPath := field.NewPath("spec") 128 if !feature.Gates.Enabled(feature.MachinePool) { 129 return field.Forbidden( 130 specPath, 131 "can be set only if the MachinePool feature flag is enabled", 132 ) 133 } 134 var allErrs field.ErrorList 135 if newObj.Spec.Template.Spec.Bootstrap.ConfigRef == nil && newObj.Spec.Template.Spec.Bootstrap.DataSecretName == nil { 136 allErrs = append( 137 allErrs, 138 field.Required( 139 specPath.Child("template", "spec", "bootstrap", "data"), 140 "expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated", 141 ), 142 ) 143 } 144 145 if newObj.Spec.Template.Spec.Bootstrap.ConfigRef != nil && newObj.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace != newObj.Namespace { 146 allErrs = append( 147 allErrs, 148 field.Invalid( 149 specPath.Child("template", "spec", "bootstrap", "configRef", "namespace"), 150 newObj.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace, 151 "must match metadata.namespace", 152 ), 153 ) 154 } 155 156 if newObj.Spec.Template.Spec.InfrastructureRef.Namespace != newObj.Namespace { 157 allErrs = append( 158 allErrs, 159 field.Invalid( 160 specPath.Child("infrastructureRef", "namespace"), 161 newObj.Spec.Template.Spec.InfrastructureRef.Namespace, 162 "must match metadata.namespace", 163 ), 164 ) 165 } 166 167 if oldObj != nil && oldObj.Spec.ClusterName != newObj.Spec.ClusterName { 168 allErrs = append( 169 allErrs, 170 field.Forbidden( 171 specPath.Child("clusterName"), 172 "field is immutable"), 173 ) 174 } 175 176 if newObj.Spec.Template.Spec.Version != nil { 177 if !version.KubeSemver.MatchString(*newObj.Spec.Template.Spec.Version) { 178 allErrs = append(allErrs, field.Invalid(specPath.Child("template", "spec", "version"), *newObj.Spec.Template.Spec.Version, "must be a valid semantic version")) 179 } 180 } 181 182 // Validate the metadata of the MachinePool template. 183 allErrs = append(allErrs, newObj.Spec.Template.ObjectMeta.Validate(specPath.Child("template", "metadata"))...) 184 185 if len(allErrs) == 0 { 186 return nil 187 } 188 return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("MachinePool").GroupKind(), newObj.Name, allErrs) 189 }