sigs.k8s.io/cluster-api-provider-aws@v1.5.5/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_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 "net" 22 23 "github.com/apparentlymart/go-cidr/cidr" 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/apimachinery/pkg/util/version" 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 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 34 "sigs.k8s.io/cluster-api-provider-aws/pkg/eks" 35 ) 36 37 const ( 38 minAddonVersion = "v1.18.0" 39 maxClusterNameLength = 100 40 ) 41 42 // log is for logging in this package. 43 var mcpLog = logf.Log.WithName("awsmanagedcontrolplane-resource") 44 45 const ( 46 cidrSizeMax = 65536 47 cidrSizeMin = 16 48 vpcCniAddon = "vpc-cni" 49 kubeProxyAddon = "kube-proxy" 50 ) 51 52 // SetupWebhookWithManager will setup the webhooks for the AWSManagedControlPlane. 53 func (r *AWSManagedControlPlane) SetupWebhookWithManager(mgr ctrl.Manager) error { 54 return ctrl.NewWebhookManagedBy(mgr). 55 For(r). 56 Complete() 57 } 58 59 // +kubebuilder:webhook:verbs=create;update,path=/validate-controlplane-cluster-x-k8s-io-v1beta1-awsmanagedcontrolplane,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=controlplane.cluster.x-k8s.io,resources=awsmanagedcontrolplanes,versions=v1beta1,name=validation.awsmanagedcontrolplanes.controlplane.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1 60 // +kubebuilder:webhook:verbs=create;update,path=/mutate-controlplane-cluster-x-k8s-io-v1beta1-awsmanagedcontrolplane,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=controlplane.cluster.x-k8s.io,resources=awsmanagedcontrolplanes,versions=v1beta1,name=default.awsmanagedcontrolplanes.controlplane.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1 61 62 var _ webhook.Defaulter = &AWSManagedControlPlane{} 63 var _ webhook.Validator = &AWSManagedControlPlane{} 64 65 func parseEKSVersion(raw string) (*version.Version, error) { 66 v, err := version.ParseGeneric(raw) 67 if err != nil { 68 return nil, err 69 } 70 return version.MustParseGeneric(fmt.Sprintf("%d.%d", v.Major(), v.Minor())), nil 71 } 72 73 func normalizeVersion(raw string) (string, error) { 74 // Normalize version (i.e. remove patch, add "v" prefix) if necessary 75 eksV, err := parseEKSVersion(raw) 76 if err != nil { 77 return "", err 78 } 79 return fmt.Sprintf("v%d.%d", eksV.Major(), eksV.Minor()), nil 80 } 81 82 // ValidateCreate will do any extra validation when creating a AWSManagedControlPlane. 83 func (r *AWSManagedControlPlane) ValidateCreate() error { 84 mcpLog.Info("AWSManagedControlPlane validate create", "name", r.Name) 85 86 var allErrs field.ErrorList 87 88 if r.Spec.EKSClusterName == "" { 89 allErrs = append(allErrs, field.Required(field.NewPath("spec.eksClusterName"), "eksClusterName is required")) 90 } 91 92 allErrs = append(allErrs, r.validateEKSVersion(nil)...) 93 allErrs = append(allErrs, r.Spec.Bastion.Validate()...) 94 allErrs = append(allErrs, r.validateIAMAuthConfig()...) 95 allErrs = append(allErrs, r.validateSecondaryCIDR()...) 96 allErrs = append(allErrs, r.validateEKSAddons()...) 97 allErrs = append(allErrs, r.validateDisableVPCCNI()...) 98 allErrs = append(allErrs, r.validateKubeProxy()...) 99 allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) 100 101 if len(allErrs) == 0 { 102 return nil 103 } 104 105 return apierrors.NewInvalid( 106 r.GroupVersionKind().GroupKind(), 107 r.Name, 108 allErrs, 109 ) 110 } 111 112 // ValidateUpdate will do any extra validation when updating a AWSManagedControlPlane. 113 func (r *AWSManagedControlPlane) ValidateUpdate(old runtime.Object) error { 114 mcpLog.Info("AWSManagedControlPlane validate update", "name", r.Name) 115 oldAWSManagedControlplane, ok := old.(*AWSManagedControlPlane) 116 if !ok { 117 return apierrors.NewInvalid(GroupVersion.WithKind("AWSManagedControlPlane").GroupKind(), r.Name, field.ErrorList{ 118 field.InternalError(nil, errors.New("failed to convert old AWSManagedControlPlane to object")), 119 }) 120 } 121 122 var allErrs field.ErrorList 123 allErrs = append(allErrs, r.validateEKSClusterName()...) 124 allErrs = append(allErrs, r.validateEKSClusterNameSame(oldAWSManagedControlplane)...) 125 allErrs = append(allErrs, r.validateEKSVersion(oldAWSManagedControlplane)...) 126 allErrs = append(allErrs, r.Spec.Bastion.Validate()...) 127 allErrs = append(allErrs, r.validateIAMAuthConfig()...) 128 allErrs = append(allErrs, r.validateSecondaryCIDR()...) 129 allErrs = append(allErrs, r.validateEKSAddons()...) 130 allErrs = append(allErrs, r.validateDisableVPCCNI()...) 131 allErrs = append(allErrs, r.validateKubeProxy()...) 132 allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) 133 134 if r.Spec.Region != oldAWSManagedControlplane.Spec.Region { 135 allErrs = append(allErrs, 136 field.Invalid(field.NewPath("spec", "region"), r.Spec.Region, "field is immutable"), 137 ) 138 } 139 140 // If encryptionConfig is already set, do not allow removal of it. 141 if oldAWSManagedControlplane.Spec.EncryptionConfig != nil && r.Spec.EncryptionConfig == nil { 142 allErrs = append(allErrs, 143 field.Invalid(field.NewPath("spec", "encryptionConfig"), r.Spec.EncryptionConfig, "disabling EKS encryption is not allowed after it has been enabled"), 144 ) 145 } 146 147 // If encryptionConfig is already set, do not allow change in provider 148 if r.Spec.EncryptionConfig != nil && 149 r.Spec.EncryptionConfig.Provider != nil && 150 oldAWSManagedControlplane.Spec.EncryptionConfig != nil && 151 oldAWSManagedControlplane.Spec.EncryptionConfig.Provider != nil && 152 *r.Spec.EncryptionConfig.Provider != *oldAWSManagedControlplane.Spec.EncryptionConfig.Provider { 153 allErrs = append(allErrs, 154 field.Invalid(field.NewPath("spec", "encryptionConfig", "provider"), r.Spec.EncryptionConfig.Provider, "changing EKS encryption is not allowed after it has been enabled"), 155 ) 156 } 157 158 // If a identityRef is already set, do not allow removal of it. 159 if oldAWSManagedControlplane.Spec.IdentityRef != nil && r.Spec.IdentityRef == nil { 160 allErrs = append(allErrs, 161 field.Invalid(field.NewPath("spec", "identityRef"), 162 r.Spec.IdentityRef, "field cannot be set to nil"), 163 ) 164 } 165 166 if len(allErrs) == 0 { 167 return nil 168 } 169 170 return apierrors.NewInvalid( 171 r.GroupVersionKind().GroupKind(), 172 r.Name, 173 allErrs, 174 ) 175 } 176 177 // ValidateDelete allows you to add any extra validation when deleting. 178 func (r *AWSManagedControlPlane) ValidateDelete() error { 179 mcpLog.Info("AWSManagedControlPlane validate delete", "name", r.Name) 180 181 return nil 182 } 183 184 func (r *AWSManagedControlPlane) validateEKSClusterName() field.ErrorList { 185 var allErrs field.ErrorList 186 187 if r.Spec.EKSClusterName == "" { 188 allErrs = append(allErrs, field.Required(field.NewPath("spec.eksClusterName"), "eksClusterName is required")) 189 } 190 191 return allErrs 192 } 193 194 func (r *AWSManagedControlPlane) validateEKSClusterNameSame(old *AWSManagedControlPlane) field.ErrorList { 195 var allErrs field.ErrorList 196 if old.Spec.EKSClusterName != "" && r.Spec.EKSClusterName != old.Spec.EKSClusterName { 197 allErrs = append(allErrs, field.Invalid(field.NewPath("spec.eksClusterName"), r.Spec.EKSClusterName, "eksClusterName is different to current cluster name")) 198 } 199 200 return allErrs 201 } 202 203 func (r *AWSManagedControlPlane) validateEKSVersion(old *AWSManagedControlPlane) field.ErrorList { 204 path := field.NewPath("spec.version") 205 var allErrs field.ErrorList 206 207 if r.Spec.Version == nil { 208 return allErrs 209 } 210 211 v, err := parseEKSVersion(*r.Spec.Version) 212 if err != nil { 213 allErrs = append(allErrs, field.Invalid(path, *r.Spec.Version, err.Error())) 214 } 215 216 if old != nil { 217 oldV, err := parseEKSVersion(*old.Spec.Version) 218 if err == nil && (v.Major() < oldV.Major() || v.Minor() < oldV.Minor()) { 219 allErrs = append(allErrs, field.Invalid(path, *r.Spec.Version, "new version less than old version")) 220 } 221 } 222 223 return allErrs 224 } 225 226 func (r *AWSManagedControlPlane) validateEKSAddons() field.ErrorList { 227 var allErrs field.ErrorList 228 229 if r.Spec.Addons == nil || len(*r.Spec.Addons) == 0 { 230 return allErrs 231 } 232 233 path := field.NewPath("spec.version") 234 v, err := parseEKSVersion(*r.Spec.Version) 235 if err != nil { 236 allErrs = append(allErrs, field.Invalid(path, *r.Spec.Version, err.Error())) 237 } 238 239 minVersion, _ := version.ParseSemantic(minAddonVersion) 240 241 addonsPath := field.NewPath("spec.addons") 242 243 if v.LessThan(minVersion) { 244 message := fmt.Sprintf("addons requires Kubernetes %s or greater", minAddonVersion) 245 allErrs = append(allErrs, field.Invalid(addonsPath, *r.Spec.Version, message)) 246 } 247 248 return allErrs 249 } 250 251 func (r *AWSManagedControlPlane) validateIAMAuthConfig() field.ErrorList { 252 var allErrs field.ErrorList 253 254 parentPath := field.NewPath("spec.iamAuthenticatorConfig") 255 256 cfg := r.Spec.IAMAuthenticatorConfig 257 if cfg == nil { 258 return allErrs 259 } 260 261 for i, userMapping := range cfg.UserMappings { 262 usersPathName := fmt.Sprintf("mapUsers[%d]", i) 263 usersPath := parentPath.Child(usersPathName) 264 errs := userMapping.Validate() 265 for _, validErr := range errs { 266 allErrs = append(allErrs, field.Invalid(usersPath, userMapping, validErr.Error())) 267 } 268 } 269 270 for i, roleMapping := range cfg.RoleMappings { 271 rolePathName := fmt.Sprintf("mapRoles[%d]", i) 272 rolePath := parentPath.Child(rolePathName) 273 errs := roleMapping.Validate() 274 for _, validErr := range errs { 275 allErrs = append(allErrs, field.Invalid(rolePath, roleMapping, validErr.Error())) 276 } 277 } 278 279 return allErrs 280 } 281 282 func (r *AWSManagedControlPlane) validateSecondaryCIDR() field.ErrorList { 283 var allErrs field.ErrorList 284 if r.Spec.SecondaryCidrBlock != nil { 285 cidrField := field.NewPath("spec", "secondaryCidrBlock") 286 _, validRange1, _ := net.ParseCIDR("100.64.0.0/10") 287 _, validRange2, _ := net.ParseCIDR("198.19.0.0/16") 288 289 _, ipv4Net, err := net.ParseCIDR(*r.Spec.SecondaryCidrBlock) 290 if err != nil { 291 allErrs = append(allErrs, field.Invalid(cidrField, *r.Spec.SecondaryCidrBlock, "must be valid CIDR range")) 292 return allErrs 293 } 294 295 cidrSize := cidr.AddressCount(ipv4Net) 296 if cidrSize > cidrSizeMax || cidrSize < cidrSizeMin { 297 allErrs = append(allErrs, field.Invalid(cidrField, *r.Spec.SecondaryCidrBlock, "CIDR block sizes must be between a /16 netmask and /28 netmask")) 298 } 299 300 start, end := cidr.AddressRange(ipv4Net) 301 if (!validRange1.Contains(start) || !validRange1.Contains(end)) && (!validRange2.Contains(start) || !validRange2.Contains(end)) { 302 allErrs = append(allErrs, field.Invalid(cidrField, *r.Spec.SecondaryCidrBlock, "must be within the 100.64.0.0/10 or 198.19.0.0/16 range")) 303 } 304 } 305 306 if len(allErrs) == 0 { 307 return nil 308 } 309 return allErrs 310 } 311 312 func (r *AWSManagedControlPlane) validateKubeProxy() field.ErrorList { 313 var allErrs field.ErrorList 314 315 if r.Spec.KubeProxy.Disable { 316 disableField := field.NewPath("spec", "kubeProxy", "disable") 317 318 if r.Spec.Addons != nil { 319 for _, addon := range *r.Spec.Addons { 320 if addon.Name == kubeProxyAddon { 321 allErrs = append(allErrs, field.Invalid(disableField, r.Spec.KubeProxy.Disable, "cannot disable kube-proxy if the kube-proxy addon is specified")) 322 break 323 } 324 } 325 } 326 } 327 328 if len(allErrs) == 0 { 329 return nil 330 } 331 return allErrs 332 } 333 334 func (r *AWSManagedControlPlane) validateDisableVPCCNI() field.ErrorList { 335 var allErrs field.ErrorList 336 337 if r.Spec.DisableVPCCNI { 338 disableField := field.NewPath("spec", "disableVPCCNI") 339 340 if r.Spec.Addons != nil { 341 for _, addon := range *r.Spec.Addons { 342 if addon.Name == vpcCniAddon { 343 allErrs = append(allErrs, field.Invalid(disableField, r.Spec.DisableVPCCNI, "cannot disable vpc cni if the vpc-cni addon is specified")) 344 break 345 } 346 } 347 } 348 } 349 350 if len(allErrs) == 0 { 351 return nil 352 } 353 return allErrs 354 } 355 356 // Default will set default values for the AWSManagedControlPlane. 357 func (r *AWSManagedControlPlane) Default() { 358 mcpLog.Info("AWSManagedControlPlane setting defaults", "name", r.Name) 359 360 if r.Spec.EKSClusterName == "" { 361 mcpLog.Info("EKSClusterName is empty, generating name") 362 name, err := eks.GenerateEKSName(r.Name, r.Namespace, maxClusterNameLength) 363 if err != nil { 364 mcpLog.Error(err, "failed to create EKS cluster name") 365 return 366 } 367 368 mcpLog.Info("defaulting EKS cluster name", "cluster-name", name) 369 r.Spec.EKSClusterName = name 370 } 371 372 if r.Spec.IdentityRef == nil { 373 r.Spec.IdentityRef = &infrav1.AWSIdentityReference{ 374 Kind: infrav1.ControllerIdentityKind, 375 Name: infrav1.AWSClusterControllerIdentityName, 376 } 377 } 378 379 // Normalize version (i.e. remove patch, add "v" prefix) if necessary 380 if r.Spec.Version != nil { 381 normalizedV, err := normalizeVersion(*r.Spec.Version) 382 if err != nil { 383 mcpLog.Error(err, "couldn't parse version") 384 return 385 } 386 r.Spec.Version = &normalizedV 387 } 388 389 infrav1.SetDefaults_Bastion(&r.Spec.Bastion) 390 infrav1.SetDefaults_NetworkSpec(&r.Spec.NetworkSpec) 391 }