github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/apis/apps/v1alpha1/clusterdefinition_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 "fmt" 21 "strings" 22 23 "github.com/pkg/errors" 24 apierrors "k8s.io/apimachinery/pkg/api/errors" 25 "k8s.io/apimachinery/pkg/runtime" 26 "k8s.io/apimachinery/pkg/runtime/schema" 27 "k8s.io/apimachinery/pkg/util/validation/field" 28 ctrl "sigs.k8s.io/controller-runtime" 29 logf "sigs.k8s.io/controller-runtime/pkg/log" 30 "sigs.k8s.io/controller-runtime/pkg/webhook" 31 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 32 ) 33 34 // log is for logging in this package. 35 var ( 36 clusterdefinitionlog = logf.Log.WithName("clusterdefinition-resource") 37 ) 38 39 // DefaultRoleProbeTimeoutAfterPodsReady the default role probe timeout for application when all pods of component are ready. 40 // default values are 60 seconds. 41 const DefaultRoleProbeTimeoutAfterPodsReady int32 = 60 42 43 func (r *ClusterDefinition) SetupWebhookWithManager(mgr ctrl.Manager) error { 44 return ctrl.NewWebhookManagedBy(mgr). 45 For(r). 46 Complete() 47 } 48 49 // +kubebuilder:webhook:path=/mutate-apps-kubeblocks-io-v1alpha1-clusterdefinition,mutating=true,failurePolicy=fail,sideEffects=None,groups=apps.kubeblocks.io,resources=clusterdefinitions,verbs=create;update,versions=v1alpha1,name=mclusterdefinition.kb.io,admissionReviewVersions=v1 50 51 var _ webhook.Defaulter = &ClusterDefinition{} 52 53 // Default implements webhook.Defaulter so a webhook will be registered for the type 54 func (r *ClusterDefinition) Default() { 55 clusterdefinitionlog.Info("default", "name", r.Name) 56 for i := range r.Spec.ComponentDefs { 57 probes := r.Spec.ComponentDefs[i].Probes 58 if probes == nil { 59 continue 60 } 61 if probes.RoleProbe != nil { 62 // set default values 63 if probes.RoleProbeTimeoutAfterPodsReady == 0 { 64 probes.RoleProbeTimeoutAfterPodsReady = DefaultRoleProbeTimeoutAfterPodsReady 65 } 66 } else { 67 // if component does not support RoleProbe, reset RoleProbeTimeoutAtPodsReady to zero 68 if probes.RoleProbeTimeoutAfterPodsReady != 0 { 69 probes.RoleProbeTimeoutAfterPodsReady = 0 70 } 71 } 72 // set to CloneVolume if deprecated value used 73 if r.Spec.ComponentDefs[i].HorizontalScalePolicy != nil && 74 r.Spec.ComponentDefs[i].HorizontalScalePolicy.Type == HScaleDataClonePolicyFromSnapshot { 75 r.Spec.ComponentDefs[i].HorizontalScalePolicy.Type = HScaleDataClonePolicyCloneVolume 76 } 77 } 78 } 79 80 // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. 81 // +kubebuilder:webhook:path=/validate-apps-kubeblocks-io-v1alpha1-clusterdefinition,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps.kubeblocks.io,resources=clusterdefinitions,verbs=create;update,versions=v1alpha1,name=vclusterdefinition.kb.io,admissionReviewVersions=v1 82 83 var _ webhook.Validator = &ClusterDefinition{} 84 85 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 86 func (r *ClusterDefinition) ValidateCreate() (admission.Warnings, error) { 87 clusterdefinitionlog.Info("validate create", "name", r.Name) 88 return nil, r.validate() 89 } 90 91 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 92 func (r *ClusterDefinition) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { 93 clusterdefinitionlog.Info("validate update", "name", r.Name) 94 return nil, r.validate() 95 } 96 97 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 98 func (r *ClusterDefinition) ValidateDelete() (admission.Warnings, error) { 99 clusterdefinitionlog.Info("validate delete", "name", r.Name) 100 return nil, nil 101 } 102 103 // Validate ClusterDefinition.spec is legal 104 func (r *ClusterDefinition) validate() error { 105 var ( 106 allErrs field.ErrorList 107 ) 108 // clusterDefinition components to map 109 componentMap := make(map[string]struct{}) 110 for _, v := range r.Spec.ComponentDefs { 111 componentMap[v.Name] = struct{}{} 112 } 113 114 r.validateComponents(&allErrs) 115 r.validateLogFilePatternPrefix(&allErrs) 116 117 if len(allErrs) > 0 { 118 return apierrors.NewInvalid( 119 schema.GroupKind{Group: APIVersion, Kind: ClusterDefinitionKind}, 120 r.Name, allErrs) 121 } 122 return nil 123 } 124 125 // validateLogsPatternPrefix validate spec.components[*].logConfigs[*].filePathPattern 126 func (r *ClusterDefinition) validateLogFilePatternPrefix(allErrs *field.ErrorList) { 127 for idx1, component := range r.Spec.ComponentDefs { 128 if len(component.LogConfigs) == 0 { 129 continue 130 } 131 volumeMounts := component.PodSpec.Containers[0].VolumeMounts 132 for idx2, logConfig := range component.LogConfigs { 133 flag := false 134 for _, v := range volumeMounts { 135 if strings.HasPrefix(logConfig.FilePathPattern, v.MountPath) { 136 flag = true 137 break 138 } 139 } 140 if !flag { 141 *allErrs = append(*allErrs, field.Required(field.NewPath(fmt.Sprintf("spec.components[%d].logConfigs[%d].filePathPattern", idx1, idx2)), 142 fmt.Sprintf("filePathPattern %s should have a prefix string which in container VolumeMounts", logConfig.FilePathPattern))) 143 } 144 } 145 } 146 } 147 148 // ValidateComponents validate spec.components is legal. 149 func (r *ClusterDefinition) validateComponents(allErrs *field.ErrorList) { 150 151 validateSystemAccount := func(component *ClusterComponentDefinition) { 152 sysAccountSpec := component.SystemAccounts 153 if sysAccountSpec != nil { 154 sysAccountSpec.validate(allErrs) 155 } 156 } 157 158 validateConsensus := func(component *ClusterComponentDefinition) { 159 consensusSpec := component.ConsensusSpec 160 // roleObserveQuery and Leader are required 161 if consensusSpec.Leader.Name == "" { 162 *allErrs = append(*allErrs, 163 field.Required(field.NewPath("spec.components[*].consensusSpec.leader.name"), 164 "leader name can't be blank when workloadType is Consensus")) 165 } 166 167 // Leader.Replicas should not be present or should set to 1 168 if *consensusSpec.Leader.Replicas != 0 && *consensusSpec.Leader.Replicas != 1 { 169 *allErrs = append(*allErrs, 170 field.Invalid(field.NewPath("spec.components[*].consensusSpec.leader.replicas"), 171 consensusSpec.Leader.Replicas, 172 "leader replicas can only be 1")) 173 } 174 175 // Leader.replicas + Follower.replicas should be odd 176 candidates := int32(1) 177 for _, member := range consensusSpec.Followers { 178 if member.Replicas != nil { 179 candidates += *member.Replicas 180 } 181 } 182 if candidates%2 == 0 { 183 *allErrs = append(*allErrs, 184 field.Invalid(field.NewPath("spec.components[*].consensusSpec.candidates(leader.replicas+followers[*].replicas)"), 185 candidates, 186 "candidates(leader+followers) should be odd")) 187 } 188 // if component.replicas is 1, then only Leader should be present. just omit if present 189 190 // if Followers.Replicas present, Leader.Replicas(that is 1) + Followers.Replicas + Learner.Replicas should equal to component.defaultReplicas 191 } 192 193 for _, component := range r.Spec.ComponentDefs { 194 for _, compRef := range component.ComponentDefRef { 195 compRef.validate(allErrs, r) 196 } 197 198 if err := r.validateConfigSpec(component); err != nil { 199 *allErrs = append(*allErrs, field.Duplicate(field.NewPath("spec.components[*].configSpec.configTemplateRefs"), err)) 200 continue 201 } 202 203 // validate system account defined in spec.components[].systemAccounts 204 validateSystemAccount(&component) 205 206 switch component.WorkloadType { 207 case Consensus: 208 // if consensus 209 consensusSpec := component.ConsensusSpec 210 if consensusSpec == nil { 211 *allErrs = append(*allErrs, 212 field.Required(field.NewPath("spec.components[*].consensusSpec"), 213 "consensusSpec is required when workloadType=Consensus")) 214 continue 215 } 216 validateConsensus(&component) 217 case Replication: 218 default: 219 continue 220 } 221 } 222 } 223 224 // validate validates spec.components[].systemAccounts 225 func (r *SystemAccountSpec) validate(allErrs *field.ErrorList) { 226 accountName := make(map[AccountName]bool) 227 for _, sysAccount := range r.Accounts { 228 // validate provision policy 229 provisionPolicy := sysAccount.ProvisionPolicy 230 if provisionPolicy.Type == CreateByStmt && sysAccount.ProvisionPolicy.Statements == nil { 231 *allErrs = append(*allErrs, 232 field.Invalid(field.NewPath("spec.components[*].systemAccounts.accounts.provisionPolicy.statements"), 233 sysAccount.Name, "statements should not be empty when provisionPolicy = CreateByStmt.")) 234 continue 235 } 236 237 if sysAccount.ProvisionPolicy.Statements != nil { 238 updateStmt := sysAccount.ProvisionPolicy.Statements.UpdateStatement 239 deletionStmt := sysAccount.ProvisionPolicy.Statements.DeletionStatement 240 if len(updateStmt) == 0 && len(deletionStmt) == 0 { 241 *allErrs = append(*allErrs, 242 field.Invalid(field.NewPath("spec.components[*].systemAccounts.accounts.provisionPolicy.statements"), 243 sysAccount.Name, "either statements.update or statements.deletion should be specified.")) 244 continue 245 } 246 } 247 248 if provisionPolicy.Type == ReferToExisting && sysAccount.ProvisionPolicy.SecretRef == nil { 249 *allErrs = append(*allErrs, 250 field.Invalid(field.NewPath("spec.components[*].systemAccounts.accounts.provisionPolicy.secretRef"), 251 sysAccount.Name, "SecretRef should not be empty when provisionPolicy = ReferToExisting. ")) 252 continue 253 } 254 // account names should be unique 255 if _, exists := accountName[sysAccount.Name]; exists { 256 *allErrs = append(*allErrs, 257 field.Invalid(field.NewPath("spec.components[*].systemAccounts.accounts"), 258 sysAccount.Name, "duplicated system account names are not allowed.")) 259 continue 260 } else { 261 accountName[sysAccount.Name] = true 262 } 263 } 264 265 passwdConfig := r.PasswordConfig 266 if passwdConfig.Length < passwdConfig.NumDigits+passwdConfig.NumSymbols { 267 *allErrs = append(*allErrs, 268 field.Invalid(field.NewPath("spec.components[*].systemAccounts.passwordConfig"), 269 passwdConfig, "numDigits plus numSymbols exceeds password length. ")) 270 } 271 } 272 273 func (r *ClusterDefinition) validateConfigSpec(component ClusterComponentDefinition) error { 274 if len(component.ConfigSpecs) <= 1 && len(component.ScriptSpecs) <= 1 { 275 return nil 276 } 277 return validateConfigTemplateList(component.ConfigSpecs) 278 } 279 280 func validateConfigTemplateList(ctpls []ComponentConfigSpec) error { 281 var ( 282 volumeSet = map[string]struct{}{} 283 cmSet = map[string]struct{}{} 284 tplSet = map[string]struct{}{} 285 ) 286 287 for _, tpl := range ctpls { 288 if len(tpl.VolumeName) == 0 { 289 return errors.Errorf("ConfigTemplate.VolumeName not empty.") 290 } 291 if _, ok := tplSet[tpl.Name]; ok { 292 return errors.Errorf("configTemplate[%s] already existed.", tpl.Name) 293 } 294 if _, ok := volumeSet[tpl.VolumeName]; ok { 295 return errors.Errorf("volume[%s] already existed.", tpl.VolumeName) 296 } 297 if _, ok := cmSet[tpl.TemplateRef]; ok { 298 return errors.Errorf("configmap[%s] already existed.", tpl.TemplateRef) 299 } 300 tplSet[tpl.Name] = struct{}{} 301 cmSet[tpl.TemplateRef] = struct{}{} 302 volumeSet[tpl.VolumeName] = struct{}{} 303 } 304 return nil 305 } 306 307 func (r ComponentDefRef) validate(allErrs *field.ErrorList, clusterDef *ClusterDefinition) { 308 if len(r.ComponentDefName) == 0 { 309 *allErrs = append(*allErrs, field.Invalid(field.NewPath("componentDefName"), r.ComponentDefName, "componentDefName cannot be empty")) 310 } 311 312 for _, env := range r.ComponentRefEnvs { 313 if len(env.Value) > 0 && env.ValueFrom != nil { 314 *allErrs = append(*allErrs, field.Invalid(field.NewPath("componentRefEnv[*].value"), env.Value, "value and valueFrom cannot be set at the same time")) 315 } 316 if len(env.Value) == 0 && env.ValueFrom == nil { 317 *allErrs = append(*allErrs, field.Invalid(field.NewPath("componentRefEnv[*].value"), env.Value, "value and valueFrom cannot be empty at the same time")) 318 } 319 if env.ValueFrom == nil { 320 continue 321 } 322 valueFrom := env.ValueFrom 323 switch valueFrom.Type { 324 case FromFieldRef: 325 if len(valueFrom.FieldPath) == 0 { 326 *allErrs = append(*allErrs, field.Invalid(field.NewPath("componentRefEnv[*].valueFrom"), valueFrom.FieldPath, "fieldRef cannot be empty")) 327 } 328 case FromHeadlessServiceRef: 329 if len(valueFrom.FieldPath) > 0 { 330 *allErrs = append(*allErrs, field.Invalid(field.NewPath("componentRefEnv[*].valueFrom"), valueFrom, "headlessServiceRef cannot set fieldPath")) 331 } 332 } 333 // get the componentDef by name 334 compDefName := r.ComponentDefName 335 compDef := clusterDef.GetComponentDefByName(compDefName) 336 if compDef == nil { 337 *allErrs = append(*allErrs, field.Invalid(field.NewPath("componentRefEnv[*].componentDefName"), valueFrom, "componentDefName is invalid")) 338 } else if env.ValueFrom.Type == FromHeadlessServiceRef && compDef.WorkloadType == Stateless { 339 *allErrs = append(*allErrs, field.Invalid(field.NewPath("componentRefEnv[*].valueFrom"), valueFrom, "headlessServiceRef is only valid for statefulset")) 340 } 341 } 342 }