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  }