sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/machine.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 "time" 24 25 apierrors "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 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 "sigs.k8s.io/cluster-api/util/labels" 35 "sigs.k8s.io/cluster-api/util/version" 36 ) 37 38 const defaultNodeDeletionTimeout = 10 * time.Second 39 40 func (webhook *Machine) SetupWebhookWithManager(mgr ctrl.Manager) error { 41 return ctrl.NewWebhookManagedBy(mgr). 42 For(&clusterv1.Machine{}). 43 WithDefaulter(webhook). 44 WithValidator(webhook). 45 Complete() 46 } 47 48 // +kubebuilder:webhook:verbs=create;update,path=/validate-cluster-x-k8s-io-v1beta1-machine,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machines,versions=v1beta1,name=validation.machine.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 49 // +kubebuilder:webhook:verbs=create;update,path=/mutate-cluster-x-k8s-io-v1beta1-machine,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machines,versions=v1beta1,name=default.machine.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 50 51 // Machine implements a validation and defaulting webhook for Machine. 52 type Machine struct{} 53 54 var _ webhook.CustomValidator = &Machine{} 55 var _ webhook.CustomDefaulter = &Machine{} 56 57 // Default implements webhook.Defaulter so a webhook will be registered for the type. 58 func (webhook *Machine) Default(_ context.Context, obj runtime.Object) error { 59 m, ok := obj.(*clusterv1.Machine) 60 if !ok { 61 return apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj)) 62 } 63 64 if m.Labels == nil { 65 m.Labels = make(map[string]string) 66 } 67 m.Labels[clusterv1.ClusterNameLabel] = m.Spec.ClusterName 68 69 if m.Spec.Bootstrap.ConfigRef != nil && m.Spec.Bootstrap.ConfigRef.Namespace == "" { 70 m.Spec.Bootstrap.ConfigRef.Namespace = m.Namespace 71 } 72 73 if m.Spec.InfrastructureRef.Namespace == "" { 74 m.Spec.InfrastructureRef.Namespace = m.Namespace 75 } 76 77 if m.Spec.Version != nil && !strings.HasPrefix(*m.Spec.Version, "v") { 78 normalizedVersion := "v" + *m.Spec.Version 79 m.Spec.Version = &normalizedVersion 80 } 81 82 if m.Spec.NodeDeletionTimeout == nil { 83 m.Spec.NodeDeletionTimeout = &metav1.Duration{Duration: defaultNodeDeletionTimeout} 84 } 85 86 return nil 87 } 88 89 // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. 90 func (webhook *Machine) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { 91 m, ok := obj.(*clusterv1.Machine) 92 if !ok { 93 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj)) 94 } 95 96 return nil, webhook.validate(nil, m) 97 } 98 99 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. 100 func (webhook *Machine) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { 101 oldM, ok := oldObj.(*clusterv1.Machine) 102 if !ok { 103 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", oldObj)) 104 } 105 106 newM, ok := newObj.(*clusterv1.Machine) 107 if !ok { 108 return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", newObj)) 109 } 110 111 return nil, webhook.validate(oldM, newM) 112 } 113 114 // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type. 115 func (webhook *Machine) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { 116 return nil, nil 117 } 118 119 func (webhook *Machine) validate(oldM, newM *clusterv1.Machine) error { 120 var allErrs field.ErrorList 121 specPath := field.NewPath("spec") 122 if newM.Spec.Bootstrap.ConfigRef == nil && newM.Spec.Bootstrap.DataSecretName == nil { 123 // MachinePool Machines don't have a bootstrap configRef, so don't require it. The bootstrap config is instead owned by the MachinePool. 124 if !labels.IsMachinePoolOwned(newM) { 125 allErrs = append( 126 allErrs, 127 field.Required( 128 specPath.Child("bootstrap", "data"), 129 "expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated", 130 ), 131 ) 132 } 133 } 134 135 if newM.Spec.Bootstrap.ConfigRef != nil && newM.Spec.Bootstrap.ConfigRef.Namespace != newM.Namespace { 136 allErrs = append( 137 allErrs, 138 field.Invalid( 139 specPath.Child("bootstrap", "configRef", "namespace"), 140 newM.Spec.Bootstrap.ConfigRef.Namespace, 141 "must match metadata.namespace", 142 ), 143 ) 144 } 145 146 if newM.Spec.InfrastructureRef.Namespace != newM.Namespace { 147 allErrs = append( 148 allErrs, 149 field.Invalid( 150 specPath.Child("infrastructureRef", "namespace"), 151 newM.Spec.InfrastructureRef.Namespace, 152 "must match metadata.namespace", 153 ), 154 ) 155 } 156 157 if oldM != nil && oldM.Spec.ClusterName != newM.Spec.ClusterName { 158 allErrs = append( 159 allErrs, 160 field.Forbidden(specPath.Child("clusterName"), "field is immutable"), 161 ) 162 } 163 164 if newM.Spec.Version != nil { 165 if !version.KubeSemver.MatchString(*newM.Spec.Version) { 166 allErrs = append(allErrs, field.Invalid(specPath.Child("version"), *newM.Spec.Version, "must be a valid semantic version")) 167 } 168 } 169 170 if len(allErrs) == 0 { 171 return nil 172 } 173 return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("Machine").GroupKind(), newM.Name, allErrs) 174 }