github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/apis/apps/v1alpha1/cluster_webhook.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 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 v1alpha1 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 24 corev1 "k8s.io/api/core/v1" 25 apierrors "k8s.io/apimachinery/pkg/api/errors" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 "k8s.io/apimachinery/pkg/types" 29 "k8s.io/apimachinery/pkg/util/validation/field" 30 ctrl "sigs.k8s.io/controller-runtime" 31 logf "sigs.k8s.io/controller-runtime/pkg/log" 32 "sigs.k8s.io/controller-runtime/pkg/webhook" 33 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 34 ) 35 36 // log is for logging in this package. 37 var clusterlog = logf.Log.WithName("cluster-resource") 38 39 func (r *Cluster) SetupWebhookWithManager(mgr ctrl.Manager) error { 40 return ctrl.NewWebhookManagedBy(mgr). 41 For(r). 42 Complete() 43 } 44 45 // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. 46 // +kubebuilder:webhook:path=/validate-apps-kubeblocks-io-v1alpha1-cluster,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps.kubeblocks.io,resources=clusters,verbs=create;update,versions=v1alpha1,name=vcluster.kb.io,admissionReviewVersions=v1 47 48 var _ webhook.Validator = &Cluster{} 49 50 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 51 func (r *Cluster) ValidateCreate() (admission.Warnings, error) { 52 clusterlog.Info("validate create", "name", r.Name) 53 return nil, r.validate() 54 } 55 56 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 57 func (r *Cluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { 58 clusterlog.Info("validate update", "name", r.Name) 59 lastCluster := old.(*Cluster) 60 if lastCluster.Spec.ClusterDefRef != r.Spec.ClusterDefRef { 61 return nil, newInvalidError(ClusterKind, r.Name, "spec.clusterDefinitionRef", "clusterDefinitionRef is immutable, you can not update it. ") 62 } 63 if err := r.validate(); err != nil { 64 return nil, err 65 } 66 return nil, r.validateVolumeClaimTemplates(lastCluster) 67 } 68 69 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 70 func (r *Cluster) ValidateDelete() (admission.Warnings, error) { 71 clusterlog.Info("validate delete", "name", r.Name) 72 if r.Spec.TerminationPolicy == DoNotTerminate { 73 return nil, fmt.Errorf("the deletion for a cluster with DoNotTerminate termination policy is denied") 74 } 75 return nil, nil 76 } 77 78 // validateVolumeClaimTemplates volumeClaimTemplates is forbidden modification except for storage size. 79 func (r *Cluster) validateVolumeClaimTemplates(lastCluster *Cluster) error { 80 var allErrs field.ErrorList 81 for i, component := range r.Spec.ComponentSpecs { 82 lastComponent := getLastComponentByName(lastCluster, component.Name) 83 if lastComponent == nil { 84 continue 85 } 86 setVolumeClaimStorageSizeZero(component.VolumeClaimTemplates) 87 setVolumeClaimStorageSizeZero(lastComponent.VolumeClaimTemplates) 88 if !reflect.DeepEqual(component.VolumeClaimTemplates, lastComponent.VolumeClaimTemplates) { 89 path := fmt.Sprintf("spec.components[%d].volumeClaimTemplates", i) 90 allErrs = append(allErrs, field.Invalid(field.NewPath(path), 91 nil, "volumeClaimTemplates is forbidden modification except for storage size.")) 92 } 93 } 94 if len(allErrs) > 0 { 95 return apierrors.NewInvalid( 96 schema.GroupKind{Group: APIVersion, Kind: ClusterKind}, 97 r.Name, allErrs) 98 } 99 return nil 100 } 101 102 // getLastComponentByName the cluster maybe delete or add a component, so we get the component by name. 103 func getLastComponentByName(lastCluster *Cluster, componentName string) *ClusterComponentSpec { 104 for _, component := range lastCluster.Spec.ComponentSpecs { 105 if component.Name == componentName { 106 return &component 107 } 108 } 109 return nil 110 } 111 112 // setVolumeClaimStorageSizeZero set the volumeClaimTemplates storage size to zero. then we can diff last/current volumeClaimTemplates. 113 func setVolumeClaimStorageSizeZero(volumeClaimTemplates []ClusterComponentVolumeClaimTemplate) { 114 for i := range volumeClaimTemplates { 115 volumeClaimTemplates[i].Spec.Resources = corev1.ResourceRequirements{} 116 } 117 } 118 119 // Validate Cluster.spec is legal 120 func (r *Cluster) validate() error { 121 var ( 122 allErrs field.ErrorList 123 ctx = context.Background() 124 clusterDef = &ClusterDefinition{} 125 ) 126 if webhookMgr == nil { 127 return nil 128 } 129 130 r.validateClusterVersionRef(&allErrs) 131 132 err := webhookMgr.client.Get(ctx, types.NamespacedName{Name: r.Spec.ClusterDefRef}, clusterDef) 133 134 if err != nil { 135 allErrs = append(allErrs, field.Invalid(field.NewPath("spec.clusterDefinitionRef"), 136 r.Spec.ClusterDefRef, err.Error())) 137 } else { 138 r.validateComponents(&allErrs, clusterDef) 139 } 140 141 if len(allErrs) > 0 { 142 return apierrors.NewInvalid( 143 schema.GroupKind{Group: APIVersion, Kind: ClusterKind}, 144 r.Name, allErrs) 145 } 146 return nil 147 } 148 149 // ValidateClusterVersionRef validate spec.clusterVersionRef is legal 150 func (r *Cluster) validateClusterVersionRef(allErrs *field.ErrorList) { 151 clusterVersion := &ClusterVersion{} 152 err := webhookMgr.client.Get(context.Background(), types.NamespacedName{ 153 Namespace: r.Namespace, 154 Name: r.Spec.ClusterVersionRef, 155 }, clusterVersion) 156 if err != nil { 157 *allErrs = append(*allErrs, field.Invalid(field.NewPath("spec.clusterVersionRef"), 158 r.Spec.ClusterDefRef, err.Error())) 159 } 160 } 161 162 // ValidateComponents validate spec.components is legal 163 func (r *Cluster) validateComponents(allErrs *field.ErrorList, clusterDef *ClusterDefinition) { 164 var ( 165 // invalid component slice 166 invalidComponentDefs = make([]string, 0) 167 componentNameMap = make(map[string]struct{}) 168 componentDefMap = make(map[string]struct{}) 169 componentMap = make(map[string]ClusterComponentDefinition) 170 ) 171 172 for _, v := range clusterDef.Spec.ComponentDefs { 173 componentDefMap[v.Name] = struct{}{} 174 componentMap[v.Name] = v 175 } 176 177 for i, v := range r.Spec.ComponentSpecs { 178 if _, ok := componentDefMap[v.ComponentDefRef]; !ok { 179 invalidComponentDefs = append(invalidComponentDefs, v.ComponentDefRef) 180 } 181 182 componentNameMap[v.Name] = struct{}{} 183 r.validateComponentResources(allErrs, v.Resources, i) 184 } 185 186 r.validateComponentTLSSettings(allErrs) 187 188 if len(invalidComponentDefs) > 0 { 189 *allErrs = append(*allErrs, field.NotFound(field.NewPath("spec.components[*].type"), 190 getComponentDefNotFoundMsg(invalidComponentDefs, r.Spec.ClusterDefRef))) 191 } 192 } 193 194 // validateComponentResources validate component resources 195 func (r *Cluster) validateComponentResources(allErrs *field.ErrorList, resources corev1.ResourceRequirements, index int) { 196 if invalidValue, err := validateVerticalResourceList(resources.Requests); err != nil { 197 *allErrs = append(*allErrs, field.Invalid(field.NewPath(fmt.Sprintf("spec.components[%d].resources.requests", index)), invalidValue, err.Error())) 198 } 199 if invalidValue, err := validateVerticalResourceList(resources.Limits); err != nil { 200 *allErrs = append(*allErrs, field.Invalid(field.NewPath(fmt.Sprintf("spec.components[%d].resources.limits", index)), invalidValue, err.Error())) 201 } 202 if invalidValue, err := compareRequestsAndLimits(resources); err != nil { 203 *allErrs = append(*allErrs, field.Invalid(field.NewPath(fmt.Sprintf("spec.components[%d].resources.requests", index)), invalidValue, err.Error())) 204 } 205 } 206 207 func (r *Cluster) validateComponentTLSSettings(allErrs *field.ErrorList) { 208 for index, component := range r.Spec.ComponentSpecs { 209 if !component.TLS { 210 continue 211 } 212 if component.Issuer == nil { 213 *allErrs = append(*allErrs, field.Required(field.NewPath(fmt.Sprintf("spec.components[%d].issuer", index)), "Issuer must be set when Tls enabled")) 214 continue 215 } 216 if component.Issuer.Name == IssuerUserProvided && component.Issuer.SecretRef == nil { 217 *allErrs = append(*allErrs, field.Required(field.NewPath(fmt.Sprintf("spec.components[%d].issuer.secretRef", index)), "Secret must provide when issuer name is UserProvided")) 218 } 219 } 220 }