sigs.k8s.io/cluster-api-provider-aws@v1.5.5/api/v1beta1/awsmachine_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 "github.com/google/go-cmp/cmp" 21 "github.com/pkg/errors" 22 apierrors "k8s.io/apimachinery/pkg/api/errors" 23 "k8s.io/apimachinery/pkg/runtime" 24 "k8s.io/apimachinery/pkg/util/validation/field" 25 ctrl "sigs.k8s.io/controller-runtime" 26 logf "sigs.k8s.io/controller-runtime/pkg/log" 27 "sigs.k8s.io/controller-runtime/pkg/webhook" 28 29 "sigs.k8s.io/cluster-api-provider-aws/feature" 30 ) 31 32 // log is for logging in this package. 33 var log = logf.Log.WithName("awsmachine-resource") 34 35 func (r *AWSMachine) SetupWebhookWithManager(mgr ctrl.Manager) error { 36 return ctrl.NewWebhookManagedBy(mgr). 37 For(r). 38 Complete() 39 } 40 41 // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-awsmachine,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsmachines,versions=v1beta1,name=validation.awsmachine.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1 42 // +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-awsmachine,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=awsmachines,versions=v1beta1,name=mawsmachine.kb.io,name=mutation.awsmachine.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1 43 44 var ( 45 _ webhook.Validator = &AWSMachine{} 46 _ webhook.Defaulter = &AWSMachine{} 47 ) 48 49 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type. 50 func (r *AWSMachine) ValidateCreate() error { 51 var allErrs field.ErrorList 52 53 allErrs = append(allErrs, r.validateCloudInitSecret()...) 54 allErrs = append(allErrs, r.validateIgnitionAndCloudInit()...) 55 allErrs = append(allErrs, r.validateRootVolume()...) 56 allErrs = append(allErrs, r.validateNonRootVolumes()...) 57 allErrs = append(allErrs, r.validateSSHKeyName()...) 58 allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...) 59 allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) 60 61 return aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs) 62 } 63 64 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. 65 func (r *AWSMachine) ValidateUpdate(old runtime.Object) error { 66 newAWSMachine, err := runtime.DefaultUnstructuredConverter.ToUnstructured(r) 67 if err != nil { 68 return apierrors.NewInvalid(GroupVersion.WithKind("AWSMachine").GroupKind(), r.Name, field.ErrorList{ 69 field.InternalError(nil, errors.Wrap(err, "failed to convert new AWSMachine to unstructured object")), 70 }) 71 } 72 oldAWSMachine, err := runtime.DefaultUnstructuredConverter.ToUnstructured(old) 73 if err != nil { 74 return apierrors.NewInvalid(GroupVersion.WithKind("AWSMachine").GroupKind(), r.Name, field.ErrorList{ 75 field.InternalError(nil, errors.Wrap(err, "failed to convert old AWSMachine to unstructured object")), 76 }) 77 } 78 79 var allErrs field.ErrorList 80 81 allErrs = append(allErrs, r.validateCloudInitSecret()...) 82 allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) 83 84 newAWSMachineSpec := newAWSMachine["spec"].(map[string]interface{}) 85 oldAWSMachineSpec := oldAWSMachine["spec"].(map[string]interface{}) 86 87 // allow changes to providerID 88 delete(oldAWSMachineSpec, "providerID") 89 delete(newAWSMachineSpec, "providerID") 90 91 // allow changes to instanceID 92 delete(oldAWSMachineSpec, "instanceID") 93 delete(newAWSMachineSpec, "instanceID") 94 95 // allow changes to additionalTags 96 delete(oldAWSMachineSpec, "additionalTags") 97 delete(newAWSMachineSpec, "additionalTags") 98 99 // allow changes to additionalSecurityGroups 100 delete(oldAWSMachineSpec, "additionalSecurityGroups") 101 delete(newAWSMachineSpec, "additionalSecurityGroups") 102 103 // allow changes to secretPrefix, secretCount, and secureSecretsBackend 104 if cloudInit, ok := oldAWSMachineSpec["cloudInit"].(map[string]interface{}); ok { 105 delete(cloudInit, "secretPrefix") 106 delete(cloudInit, "secretCount") 107 delete(cloudInit, "secureSecretsBackend") 108 } 109 110 if cloudInit, ok := newAWSMachineSpec["cloudInit"].(map[string]interface{}); ok { 111 delete(cloudInit, "secretPrefix") 112 delete(cloudInit, "secretCount") 113 delete(cloudInit, "secureSecretsBackend") 114 } 115 116 if !cmp.Equal(oldAWSMachineSpec, newAWSMachineSpec) { 117 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "cannot be modified")) 118 } 119 120 return aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs) 121 } 122 123 func (r *AWSMachine) validateCloudInitSecret() field.ErrorList { 124 var allErrs field.ErrorList 125 126 if r.Spec.CloudInit.InsecureSkipSecretsManager { 127 if r.Spec.CloudInit.SecretPrefix != "" { 128 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "cloudInit", "secretPrefix"), "cannot be set if spec.cloudInit.insecureSkipSecretsManager is true")) 129 } 130 if r.Spec.CloudInit.SecretCount != 0 { 131 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "cloudInit", "secretCount"), "cannot be set if spec.cloudInit.insecureSkipSecretsManager is true")) 132 } 133 if r.Spec.CloudInit.SecureSecretsBackend != "" { 134 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "cloudInit", "secureSecretsBackend"), "cannot be set if spec.cloudInit.insecureSkipSecretsManager is true")) 135 } 136 } 137 138 if (r.Spec.CloudInit.SecretPrefix != "") != (r.Spec.CloudInit.SecretCount != 0) { 139 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "cloudInit", "secretCount"), "must be set together with spec.CloudInit.SecretPrefix")) 140 } 141 142 return allErrs 143 } 144 145 func (r *AWSMachine) cloudInitConfigured() bool { 146 configured := false 147 148 configured = configured || r.Spec.CloudInit.SecretPrefix != "" 149 configured = configured || r.Spec.CloudInit.SecretCount != 0 150 configured = configured || r.Spec.CloudInit.SecureSecretsBackend != "" 151 configured = configured || r.Spec.CloudInit.InsecureSkipSecretsManager 152 153 return configured 154 } 155 156 func (r *AWSMachine) ignitionEnabled() bool { 157 return r.Spec.Ignition != nil 158 } 159 160 func (r *AWSMachine) validateIgnitionAndCloudInit() field.ErrorList { 161 var allErrs field.ErrorList 162 163 // Feature gate is not enabled but ignition is enabled then send a forbidden error. 164 if !feature.Gates.Enabled(feature.BootstrapFormatIgnition) && r.ignitionEnabled() { 165 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition"), 166 "can be set only if the BootstrapFormatIgnition feature gate is enabled")) 167 } 168 169 if r.ignitionEnabled() && r.cloudInitConfigured() { 170 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "cloudInit"), "cannot be set if spec.ignition is set")) 171 } 172 173 return allErrs 174 } 175 176 func (r *AWSMachine) validateRootVolume() field.ErrorList { 177 var allErrs field.ErrorList 178 179 if r.Spec.RootVolume == nil { 180 return allErrs 181 } 182 183 if VolumeTypesProvisioned.Has(string(r.Spec.RootVolume.Type)) && r.Spec.RootVolume.IOPS == 0 { 184 allErrs = append(allErrs, field.Required(field.NewPath("spec.rootVolume.iops"), "iops required if type is 'io1' or 'io2'")) 185 } 186 187 if r.Spec.RootVolume.Throughput != nil { 188 if r.Spec.RootVolume.Type != VolumeTypeGP3 { 189 allErrs = append(allErrs, field.Required(field.NewPath("spec.rootVolume.throughput"), "throughput is valid only for type 'gp3'")) 190 } 191 if *r.Spec.RootVolume.Throughput < 0 { 192 allErrs = append(allErrs, field.Required(field.NewPath("spec.rootVolume.throughput"), "throughput must be nonnegative")) 193 } 194 } 195 196 if r.Spec.RootVolume.DeviceName != "" { 197 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec.rootVolume.deviceName"), "root volume shouldn't have device name")) 198 } 199 200 return allErrs 201 } 202 203 func (r *AWSMachine) validateNonRootVolumes() field.ErrorList { 204 var allErrs field.ErrorList 205 206 for _, volume := range r.Spec.NonRootVolumes { 207 if VolumeTypesProvisioned.Has(string(r.Spec.RootVolume.Type)) && volume.IOPS == 0 { 208 allErrs = append(allErrs, field.Required(field.NewPath("spec.nonRootVolumes.iops"), "iops required if type is 'io1' or 'io2'")) 209 } 210 211 if volume.Throughput != nil { 212 if volume.Type != VolumeTypeGP3 { 213 allErrs = append(allErrs, field.Required(field.NewPath("spec.nonRootVolumes.throughput"), "throughput is valid only for type 'gp3'")) 214 } 215 if *volume.Throughput < 0 { 216 allErrs = append(allErrs, field.Required(field.NewPath("spec.nonRootVolumes.throughput"), "throughput must be nonnegative")) 217 } 218 } 219 220 if volume.DeviceName == "" { 221 allErrs = append(allErrs, field.Required(field.NewPath("spec.nonRootVolumes.deviceName"), "non root volume should have device name")) 222 } 223 } 224 225 return allErrs 226 } 227 228 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type. 229 func (r *AWSMachine) ValidateDelete() error { 230 return nil 231 } 232 233 // Default implements webhook.Defaulter such that an empty CloudInit will be defined with a default 234 // SecureSecretsBackend as SecretBackendSecretsManager iff InsecureSkipSecretsManager is unset. 235 func (r *AWSMachine) Default() { 236 if !r.Spec.CloudInit.InsecureSkipSecretsManager && r.Spec.CloudInit.SecureSecretsBackend == "" && !r.ignitionEnabled() { 237 r.Spec.CloudInit.SecureSecretsBackend = SecretBackendSecretsManager 238 } 239 240 if r.ignitionEnabled() && r.Spec.Ignition.Version == "" { 241 if r.Spec.Ignition == nil { 242 r.Spec.Ignition = &Ignition{} 243 } 244 245 r.Spec.Ignition.Version = DefaultIgnitionVersion 246 } 247 } 248 249 func (r *AWSMachine) validateAdditionalSecurityGroups() field.ErrorList { 250 var allErrs field.ErrorList 251 252 for _, additionalSecurityGroup := range r.Spec.AdditionalSecurityGroups { 253 if len(additionalSecurityGroup.Filters) > 0 && additionalSecurityGroup.ID != nil { 254 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec.additionalSecurityGroups"), "only one of ID or Filters may be specified, specifying both is forbidden")) 255 } 256 if additionalSecurityGroup.ARN != nil { 257 log.Info("ARN field is deprecated and is no operation function.") 258 } 259 } 260 return allErrs 261 } 262 263 func (r *AWSMachine) validateSSHKeyName() field.ErrorList { 264 return validateSSHKeyName(r.Spec.SSHKeyName) 265 }