sigs.k8s.io/cluster-api-provider-aws@v1.5.5/api/v1beta1/awscluster_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  
    22  	"github.com/google/go-cmp/cmp"
    23  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    24  	"k8s.io/apimachinery/pkg/runtime"
    25  	"k8s.io/apimachinery/pkg/util/validation/field"
    26  	ctrl "sigs.k8s.io/controller-runtime"
    27  	logf "sigs.k8s.io/controller-runtime/pkg/log"
    28  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    29  
    30  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    31  	"sigs.k8s.io/cluster-api/util/annotations"
    32  )
    33  
    34  // log is for logging in this package.
    35  var _ = logf.Log.WithName("awscluster-resource")
    36  
    37  func (r *AWSCluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
    38  	return ctrl.NewWebhookManagedBy(mgr).
    39  		For(r).
    40  		Complete()
    41  }
    42  
    43  // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-awscluster,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsclusters,versions=v1beta1,name=validation.awscluster.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
    44  // +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-awscluster,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsclusters,versions=v1beta1,name=default.awscluster.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
    45  
    46  var (
    47  	_ webhook.Validator = &AWSCluster{}
    48  	_ webhook.Defaulter = &AWSCluster{}
    49  )
    50  
    51  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
    52  func (r *AWSCluster) ValidateCreate() error {
    53  	var allErrs field.ErrorList
    54  
    55  	allErrs = append(allErrs, r.Spec.Bastion.Validate()...)
    56  	allErrs = append(allErrs, r.validateSSHKeyName()...)
    57  	allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
    58  	allErrs = append(allErrs, r.Spec.S3Bucket.Validate()...)
    59  
    60  	return aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
    61  }
    62  
    63  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
    64  func (r *AWSCluster) ValidateDelete() error {
    65  	return nil
    66  }
    67  
    68  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
    69  func (r *AWSCluster) ValidateUpdate(old runtime.Object) error {
    70  	var allErrs field.ErrorList
    71  
    72  	oldC, ok := old.(*AWSCluster)
    73  	if !ok {
    74  		return apierrors.NewBadRequest(fmt.Sprintf("expected an AWSCluster but got a %T", old))
    75  	}
    76  
    77  	if r.Spec.Region != oldC.Spec.Region {
    78  		allErrs = append(allErrs,
    79  			field.Invalid(field.NewPath("spec", "region"), r.Spec.Region, "field is immutable"),
    80  		)
    81  	}
    82  
    83  	newLoadBalancer := &AWSLoadBalancerSpec{}
    84  
    85  	if r.Spec.ControlPlaneLoadBalancer != nil {
    86  		newLoadBalancer = r.Spec.ControlPlaneLoadBalancer.DeepCopy()
    87  	}
    88  
    89  	if oldC.Spec.ControlPlaneLoadBalancer == nil {
    90  		// If old scheme was nil, the only value accepted here is the default value: internet-facing
    91  		if newLoadBalancer.Scheme != nil && newLoadBalancer.Scheme.String() != ClassicELBSchemeInternetFacing.String() {
    92  			allErrs = append(allErrs,
    93  				field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "scheme"),
    94  					r.Spec.ControlPlaneLoadBalancer.Scheme, "field is immutable, default value was set to internet-facing"),
    95  			)
    96  		}
    97  	} else {
    98  		// If old scheme was not nil, the new scheme should be the same.
    99  		existingLoadBalancer := oldC.Spec.ControlPlaneLoadBalancer.DeepCopy()
   100  		if !cmp.Equal(existingLoadBalancer.Scheme, newLoadBalancer.Scheme) {
   101  			// Only allow changes from Internet-facing scheme to internet-facing.
   102  			if !(existingLoadBalancer.Scheme.String() == ClassicELBSchemeIncorrectInternetFacing.String() &&
   103  				newLoadBalancer.Scheme.String() == ClassicELBSchemeInternetFacing.String()) {
   104  				allErrs = append(allErrs,
   105  					field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "scheme"),
   106  						r.Spec.ControlPlaneLoadBalancer.Scheme, "field is immutable"),
   107  				)
   108  			}
   109  		}
   110  		// The name must be defined when the AWSCluster is created. If it is not defined,
   111  		// then the controller generates a default name at runtime, but does not store it,
   112  		// so the name remains nil. In either case, the name cannot be changed.
   113  		if !cmp.Equal(existingLoadBalancer.Name, newLoadBalancer.Name) {
   114  			allErrs = append(allErrs,
   115  				field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "name"),
   116  					r.Spec.ControlPlaneLoadBalancer.Name, "field is immutable"),
   117  			)
   118  		}
   119  
   120  		// Block the update for HealthCheckProtocol :
   121  		// - if it was not set in old spec but added in new spec
   122  		// - if it was set in old spec but changed in new spec
   123  		if !cmp.Equal(newLoadBalancer.HealthCheckProtocol, existingLoadBalancer.HealthCheckProtocol) {
   124  			allErrs = append(allErrs,
   125  				field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "healthCheckProtocol"),
   126  					newLoadBalancer.HealthCheckProtocol, "field is immutable once set"),
   127  			)
   128  		}
   129  	}
   130  
   131  	if !cmp.Equal(oldC.Spec.ControlPlaneEndpoint, clusterv1.APIEndpoint{}) &&
   132  		!cmp.Equal(r.Spec.ControlPlaneEndpoint, oldC.Spec.ControlPlaneEndpoint) {
   133  		allErrs = append(allErrs,
   134  			field.Invalid(field.NewPath("spec", "controlPlaneEndpoint"), r.Spec.ControlPlaneEndpoint, "field is immutable"),
   135  		)
   136  	}
   137  
   138  	// Modifying VPC id is not allowed because it will cause a new VPC creation if set to nil.
   139  	if !cmp.Equal(oldC.Spec.NetworkSpec, NetworkSpec{}) &&
   140  		!cmp.Equal(oldC.Spec.NetworkSpec.VPC, VPCSpec{}) &&
   141  		oldC.Spec.NetworkSpec.VPC.ID != "" {
   142  		if cmp.Equal(r.Spec.NetworkSpec, NetworkSpec{}) ||
   143  			cmp.Equal(r.Spec.NetworkSpec.VPC, VPCSpec{}) ||
   144  			oldC.Spec.NetworkSpec.VPC.ID != r.Spec.NetworkSpec.VPC.ID {
   145  			allErrs = append(allErrs,
   146  				field.Invalid(field.NewPath("spec", "network", "vpc", "id"),
   147  					r.Spec.IdentityRef, "field cannot be modified once set"))
   148  		}
   149  	}
   150  
   151  	// If a identityRef is already set, do not allow removal of it.
   152  	if oldC.Spec.IdentityRef != nil && r.Spec.IdentityRef == nil {
   153  		allErrs = append(allErrs,
   154  			field.Invalid(field.NewPath("spec", "identityRef"),
   155  				r.Spec.IdentityRef, "field cannot be set to nil"),
   156  		)
   157  	}
   158  
   159  	if annotations.IsExternallyManaged(oldC) && !annotations.IsExternallyManaged(r) {
   160  		allErrs = append(allErrs,
   161  			field.Invalid(field.NewPath("metadata", "annotations"),
   162  				r.Annotations, "removal of externally managed annotation is not allowed"),
   163  		)
   164  	}
   165  
   166  	allErrs = append(allErrs, r.Spec.Bastion.Validate()...)
   167  	allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
   168  	allErrs = append(allErrs, r.Spec.S3Bucket.Validate()...)
   169  
   170  	return aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
   171  }
   172  
   173  // Default satisfies the defaulting webhook interface.
   174  func (r *AWSCluster) Default() {
   175  	SetObjectDefaults_AWSCluster(r)
   176  }
   177  
   178  func (r *AWSCluster) validateSSHKeyName() field.ErrorList {
   179  	return validateSSHKeyName(r.Spec.SSHKeyName)
   180  }