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  }