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  }