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  }