sigs.k8s.io/cluster-api-provider-aws@v1.5.5/exp/api/v1beta1/awsmanagedmachinepool_webhook.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 v1beta1 18 19 import ( 20 "fmt" 21 "reflect" 22 23 "github.com/google/go-cmp/cmp" 24 "github.com/pkg/errors" 25 apierrors "k8s.io/apimachinery/pkg/api/errors" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/apimachinery/pkg/util/validation/field" 28 "k8s.io/utils/pointer" 29 ctrl "sigs.k8s.io/controller-runtime" 30 logf "sigs.k8s.io/controller-runtime/pkg/log" 31 "sigs.k8s.io/controller-runtime/pkg/webhook" 32 33 "sigs.k8s.io/cluster-api-provider-aws/pkg/eks" 34 ) 35 36 const ( 37 maxNodegroupNameLength = 64 38 ) 39 40 // log is for logging in this package. 41 var mmpLog = logf.Log.WithName("awsmanagedmachinepool-resource") 42 43 // SetupWebhookWithManager will setup the webhooks for the AWSManagedMachinePool. 44 func (r *AWSManagedMachinePool) SetupWebhookWithManager(mgr ctrl.Manager) error { 45 return ctrl.NewWebhookManagedBy(mgr). 46 For(r). 47 Complete() 48 } 49 50 // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-awsmanagedmachinepool,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsmanagedmachinepools,versions=v1beta1,name=validation.awsmanagedmachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1 51 // +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-awsmanagedmachinepool,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsmanagedmachinepools,versions=v1beta1,name=default.awsmanagedmachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1 52 53 var _ webhook.Defaulter = &AWSManagedMachinePool{} 54 var _ webhook.Validator = &AWSManagedMachinePool{} 55 56 func (r *AWSManagedMachinePool) validateScaling() field.ErrorList { 57 var allErrs field.ErrorList 58 if r.Spec.Scaling != nil { // nolint:nestif 59 minField := field.NewPath("spec", "scaling", "minSize") 60 maxField := field.NewPath("spec", "scaling", "maxSize") 61 min := r.Spec.Scaling.MinSize 62 max := r.Spec.Scaling.MaxSize 63 if min != nil { 64 if *min < 0 { 65 allErrs = append(allErrs, field.Invalid(minField, *min, "must be greater or equal zero")) 66 } 67 if max != nil && *max < *min { 68 allErrs = append(allErrs, field.Invalid(maxField, *max, fmt.Sprintf("must be greater than field %s", minField.String()))) 69 } 70 } 71 if max != nil && *max < 0 { 72 allErrs = append(allErrs, field.Invalid(maxField, *max, "must be greater than zero")) 73 } 74 } 75 if len(allErrs) == 0 { 76 return nil 77 } 78 return allErrs 79 } 80 81 func (r *AWSManagedMachinePool) validateNodegroupUpdateConfig() field.ErrorList { 82 var allErrs field.ErrorList 83 84 if r.Spec.UpdateConfig != nil { 85 nodegroupUpdateConfigField := field.NewPath("spec", "updateConfig") 86 87 if r.Spec.UpdateConfig.MaxUnavailable == nil && r.Spec.UpdateConfig.MaxUnavailablePercentage == nil { 88 allErrs = append(allErrs, field.Invalid(nodegroupUpdateConfigField, r.Spec.UpdateConfig, "must specify one of maxUnavailable or maxUnavailablePercentage when using nodegroup updateconfig")) 89 } 90 91 if r.Spec.UpdateConfig.MaxUnavailable != nil && r.Spec.UpdateConfig.MaxUnavailablePercentage != nil { 92 allErrs = append(allErrs, field.Invalid(nodegroupUpdateConfigField, r.Spec.UpdateConfig, "cannot specify both maxUnavailable and maxUnavailablePercentage")) 93 } 94 } 95 96 if len(allErrs) == 0 { 97 return nil 98 } 99 return allErrs 100 } 101 102 func (r *AWSManagedMachinePool) validateRemoteAccess() field.ErrorList { 103 var allErrs field.ErrorList 104 if r.Spec.RemoteAccess == nil { 105 return allErrs 106 } 107 remoteAccessPath := field.NewPath("spec", "remoteAccess") 108 sourceSecurityGroups := r.Spec.RemoteAccess.SourceSecurityGroups 109 110 if public := r.Spec.RemoteAccess.Public; public && len(sourceSecurityGroups) > 0 { 111 allErrs = append( 112 allErrs, 113 field.Invalid(remoteAccessPath.Child("sourceSecurityGroups"), sourceSecurityGroups, "must be empty if public is set"), 114 ) 115 } 116 117 return allErrs 118 } 119 120 // ValidateCreate will do any extra validation when creating a AWSManagedMachinePool. 121 func (r *AWSManagedMachinePool) ValidateCreate() error { 122 mmpLog.Info("AWSManagedMachinePool validate create", "name", r.Name) 123 124 var allErrs field.ErrorList 125 126 if r.Spec.EKSNodegroupName == "" { 127 allErrs = append(allErrs, field.Required(field.NewPath("spec.eksNodegroupName"), "eksNodegroupName is required")) 128 } 129 if errs := r.validateScaling(); errs != nil || len(errs) == 0 { 130 allErrs = append(allErrs, errs...) 131 } 132 if errs := r.validateRemoteAccess(); len(errs) > 0 { 133 allErrs = append(allErrs, errs...) 134 } 135 if errs := r.validateNodegroupUpdateConfig(); len(errs) > 0 { 136 allErrs = append(allErrs, errs...) 137 } 138 139 allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) 140 141 if len(allErrs) == 0 { 142 return nil 143 } 144 145 return apierrors.NewInvalid( 146 r.GroupVersionKind().GroupKind(), 147 r.Name, 148 allErrs, 149 ) 150 } 151 152 // ValidateUpdate will do any extra validation when updating a AWSManagedMachinePool. 153 func (r *AWSManagedMachinePool) ValidateUpdate(old runtime.Object) error { 154 mmpLog.Info("AWSManagedMachinePool validate update", "name", r.Name) 155 oldPool, ok := old.(*AWSManagedMachinePool) 156 if !ok { 157 return apierrors.NewInvalid(GroupVersion.WithKind("AWSManagedMachinePool").GroupKind(), r.Name, field.ErrorList{ 158 field.InternalError(nil, errors.New("failed to convert old AWSManagedMachinePool to object")), 159 }) 160 } 161 162 var allErrs field.ErrorList 163 allErrs = append(allErrs, r.validateImmutable(oldPool)...) 164 allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) 165 166 if errs := r.validateScaling(); errs != nil || len(errs) == 0 { 167 allErrs = append(allErrs, errs...) 168 } 169 if errs := r.validateNodegroupUpdateConfig(); len(errs) > 0 { 170 allErrs = append(allErrs, errs...) 171 } 172 173 if len(allErrs) == 0 { 174 return nil 175 } 176 177 return apierrors.NewInvalid( 178 r.GroupVersionKind().GroupKind(), 179 r.Name, 180 allErrs, 181 ) 182 } 183 184 // ValidateDelete allows you to add any extra validation when deleting. 185 func (r *AWSManagedMachinePool) ValidateDelete() error { 186 mmpLog.Info("AWSManagedMachinePool validate delete", "name", r.Name) 187 188 return nil 189 } 190 191 func (r *AWSManagedMachinePool) validateImmutable(old *AWSManagedMachinePool) field.ErrorList { 192 var allErrs field.ErrorList 193 194 appendErrorIfMutated := func(old, update interface{}, name string) { 195 if !cmp.Equal(old, update) { 196 allErrs = append( 197 allErrs, 198 field.Invalid(field.NewPath("spec", name), update, "field is immutable"), 199 ) 200 } 201 } 202 appendErrorIfSetAndMutated := func(old, update interface{}, name string) { 203 if !reflect.ValueOf(old).IsZero() && !cmp.Equal(old, update) { 204 allErrs = append( 205 allErrs, 206 field.Invalid(field.NewPath("spec", name), update, "field is immutable"), 207 ) 208 } 209 } 210 211 if old.Spec.EKSNodegroupName != "" { 212 appendErrorIfMutated(old.Spec.EKSNodegroupName, r.Spec.EKSNodegroupName, "eksNodegroupName") 213 } 214 appendErrorIfMutated(old.Spec.SubnetIDs, r.Spec.SubnetIDs, "subnetIDs") 215 appendErrorIfSetAndMutated(old.Spec.RoleName, r.Spec.RoleName, "roleName") 216 appendErrorIfMutated(old.Spec.DiskSize, r.Spec.DiskSize, "diskSize") 217 appendErrorIfMutated(old.Spec.AMIType, r.Spec.AMIType, "amiType") 218 appendErrorIfMutated(old.Spec.RemoteAccess, r.Spec.RemoteAccess, "remoteAccess") 219 appendErrorIfSetAndMutated(old.Spec.CapacityType, r.Spec.CapacityType, "capacityType") 220 221 return allErrs 222 } 223 224 // Default will set default values for the AWSManagedMachinePool. 225 func (r *AWSManagedMachinePool) Default() { 226 mmpLog.Info("AWSManagedMachinePool setting defaults", "name", r.Name) 227 228 if r.Spec.EKSNodegroupName == "" { 229 mmpLog.Info("EKSNodegroupName is empty, generating name") 230 name, err := eks.GenerateEKSName(r.Name, r.Namespace, maxNodegroupNameLength) 231 if err != nil { 232 mmpLog.Error(err, "failed to create EKS nodegroup name") 233 return 234 } 235 236 mmpLog.Info("Generated EKSNodegroupName", "nodegroup-name", name) 237 r.Spec.EKSNodegroupName = name 238 } 239 240 if r.Spec.UpdateConfig == nil { 241 r.Spec.UpdateConfig = &UpdateConfig{ 242 MaxUnavailable: pointer.Int(1), 243 } 244 } 245 }