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 }