github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/apis/apps/v1alpha1/opsrequest_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  	"encoding/json"
    22  	"fmt"
    23  	"reflect"
    24  	"strings"
    25  
    26  	"github.com/pkg/errors"
    27  	"golang.org/x/exp/slices"
    28  	corev1 "k8s.io/api/core/v1"
    29  	storagev1 "k8s.io/api/storage/v1"
    30  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    31  	"k8s.io/apimachinery/pkg/api/resource"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/types"
    34  	ctrl "sigs.k8s.io/controller-runtime"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  	logf "sigs.k8s.io/controller-runtime/pkg/log"
    37  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    38  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    39  
    40  	"github.com/1aal/kubeblocks/pkg/constant"
    41  )
    42  
    43  // log is for logging in this package.
    44  var (
    45  	opsRequestLog           = logf.Log.WithName("opsrequest-resource")
    46  	opsRequestAnnotationKey = "kubeblocks.io/ops-request"
    47  	// OpsRequestBehaviourMapper records the opsRequest behaviour according to the OpsType.
    48  	OpsRequestBehaviourMapper = map[OpsType]OpsRequestBehaviour{}
    49  )
    50  
    51  func (r *OpsRequest) SetupWebhookWithManager(mgr ctrl.Manager) error {
    52  	return ctrl.NewWebhookManagedBy(mgr).
    53  		For(r).
    54  		Complete()
    55  }
    56  
    57  // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
    58  // +kubebuilder:webhook:path=/validate-apps-kubeblocks-io-v1alpha1-opsrequest,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps.kubeblocks.io,resources=opsrequests,verbs=create;update,versions=v1alpha1,name=vopsrequest.kb.io,admissionReviewVersions=v1
    59  
    60  var _ webhook.Validator = &OpsRequest{}
    61  
    62  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type
    63  func (r *OpsRequest) ValidateCreate() (admission.Warnings, error) {
    64  	opsRequestLog.Info("validate create", "name", r.Name)
    65  	return nil, r.validateEntry(true)
    66  }
    67  
    68  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
    69  func (r *OpsRequest) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
    70  	opsRequestLog.Info("validate update", "name", r.Name)
    71  	lastOpsRequest := old.(*OpsRequest).DeepCopy()
    72  	// if no spec updated, we should skip validation.
    73  	// if not, we can not delete the OpsRequest when cluster has been deleted.
    74  	// because when cluster not existed, r.validate will report an error.
    75  	if reflect.DeepEqual(lastOpsRequest.Spec, r.Spec) {
    76  		return nil, nil
    77  	}
    78  
    79  	if r.IsComplete() {
    80  		return nil, fmt.Errorf("update OpsRequest: %s is forbidden when status.Phase is %s", r.Name, r.Status.Phase)
    81  	}
    82  
    83  	// Keep the cancel consistent between the two opsRequest for comparing the diff.
    84  	lastOpsRequest.Spec.Cancel = r.Spec.Cancel
    85  	if !reflect.DeepEqual(lastOpsRequest.Spec, r.Spec) && r.Status.Phase != "" {
    86  		return nil, fmt.Errorf("update OpsRequest: %s is forbidden except for cancel when status.Phase is %s", r.Name, r.Status.Phase)
    87  	}
    88  	return nil, r.validateEntry(false)
    89  }
    90  
    91  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type
    92  func (r *OpsRequest) ValidateDelete() (admission.Warnings, error) {
    93  	opsRequestLog.Info("validate delete", "name", r.Name)
    94  	return nil, nil
    95  }
    96  
    97  // IsComplete checks if opsRequest has been completed.
    98  func (r *OpsRequest) IsComplete(phases ...OpsPhase) bool {
    99  	if len(phases) == 0 {
   100  		return slices.Contains([]OpsPhase{OpsCancelledPhase, OpsSucceedPhase, OpsFailedPhase}, r.Status.Phase)
   101  	}
   102  	return slices.Contains([]OpsPhase{OpsCancelledPhase, OpsSucceedPhase, OpsFailedPhase}, phases[0])
   103  }
   104  
   105  // validateClusterPhase validates whether the current cluster state supports the OpsRequest
   106  func (r *OpsRequest) validateClusterPhase(cluster *Cluster) error {
   107  	opsBehaviour := OpsRequestBehaviourMapper[r.Spec.Type]
   108  	// if the OpsType has no cluster phases, ignore it
   109  	if len(opsBehaviour.FromClusterPhases) == 0 {
   110  		return nil
   111  	}
   112  	// validate whether existing the same type OpsRequest
   113  	var (
   114  		opsRequestValue string
   115  		opsRecorder     []OpsRecorder
   116  		ok              bool
   117  	)
   118  	if opsRequestValue, ok = cluster.Annotations[opsRequestAnnotationKey]; ok {
   119  		// opsRequest annotation value in cluster to map
   120  		if err := json.Unmarshal([]byte(opsRequestValue), &opsRecorder); err != nil {
   121  			return err
   122  		}
   123  	}
   124  
   125  	opsNamesInQueue := make([]string, len(opsRecorder))
   126  	for i, v := range opsRecorder {
   127  		// judge whether the opsRequest meets the following conditions:
   128  		// 1. the opsRequest is Reentrant.
   129  		// 2. the opsRequest supports concurrent execution of the same kind.
   130  		// 3. reconfiguring is a special case, it can be executed concurrently with other opsRequests.
   131  		if v.Name != r.Name && v.Type != ReconfiguringType {
   132  			return fmt.Errorf("existing OpsRequest: %s is running in Cluster: %s, handle this OpsRequest first", v.Name, cluster.Name)
   133  		}
   134  		opsNamesInQueue[i] = v.Name
   135  	}
   136  	// check if the opsRequest can be executed in the current cluster phase unless this opsRequest is reentrant.
   137  	if !slices.Contains(opsBehaviour.FromClusterPhases, cluster.Status.Phase) &&
   138  		!slices.Contains(opsNamesInQueue, r.Name) {
   139  		// if TTLSecondsBeforeAbort is not set or 0, return error
   140  		if r.Spec.TTLSecondsBeforeAbort == nil || *r.Spec.TTLSecondsBeforeAbort == 0 {
   141  			return fmt.Errorf("OpsRequest.spec.type=%s is forbidden when Cluster.status.phase=%s", r.Spec.Type, cluster.Status.Phase)
   142  		}
   143  	}
   144  	return nil
   145  }
   146  
   147  // getCluster gets cluster with webhook client
   148  func (r *OpsRequest) getCluster(ctx context.Context, k8sClient client.Client) (*Cluster, error) {
   149  	if k8sClient == nil {
   150  		return nil, nil
   151  	}
   152  	cluster := &Cluster{}
   153  	// get cluster resource
   154  	if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: r.Namespace, Name: r.Spec.ClusterRef}, cluster); err != nil {
   155  		return nil, fmt.Errorf("get cluster: %s failed, err: %s", r.Spec.ClusterRef, err.Error())
   156  	}
   157  	return cluster, nil
   158  }
   159  
   160  func (r *OpsRequest) getConfigMap(cmName string) (*corev1.ConfigMap, error) {
   161  	cmObj := &corev1.ConfigMap{}
   162  	cmKey := client.ObjectKey{
   163  		Namespace: r.Namespace,
   164  		Name:      cmName,
   165  	}
   166  
   167  	ctx := context.Background()
   168  	if err := webhookMgr.client.Get(ctx, cmKey, cmObj); err != nil {
   169  		return nil, err
   170  	}
   171  	return cmObj, nil
   172  }
   173  
   174  // Validate validates OpsRequest
   175  func (r *OpsRequest) Validate(ctx context.Context,
   176  	k8sClient client.Client,
   177  	cluster *Cluster,
   178  	isCreate bool) error {
   179  	if isCreate {
   180  		if err := r.validateClusterPhase(cluster); err != nil {
   181  			return err
   182  		}
   183  	}
   184  	return r.validateOps(ctx, k8sClient, cluster)
   185  }
   186  
   187  // ValidateEntry OpsRequest webhook validate entry
   188  func (r *OpsRequest) validateEntry(isCreate bool) error {
   189  	if webhookMgr == nil || webhookMgr.client == nil {
   190  		return nil
   191  	}
   192  	ctx := context.Background()
   193  	k8sClient := webhookMgr.client
   194  	cluster, err := r.getCluster(ctx, k8sClient)
   195  	if err != nil {
   196  		return err
   197  	}
   198  	return r.Validate(ctx, k8sClient, cluster, isCreate)
   199  }
   200  
   201  // validateOps validates ops attributes
   202  func (r *OpsRequest) validateOps(ctx context.Context,
   203  	k8sClient client.Client,
   204  	cluster *Cluster) error {
   205  	if webhookMgr == nil {
   206  		return nil
   207  	}
   208  	// Check whether the corresponding attribute is legal according to the operation type
   209  	switch r.Spec.Type {
   210  	case UpgradeType:
   211  		return r.validateUpgrade(ctx, k8sClient)
   212  	case VerticalScalingType:
   213  		return r.validateVerticalScaling(cluster)
   214  	case HorizontalScalingType:
   215  		return r.validateHorizontalScaling(ctx, k8sClient, cluster)
   216  	case VolumeExpansionType:
   217  		return r.validateVolumeExpansion(ctx, k8sClient, cluster)
   218  	case RestartType:
   219  		return r.validateRestart(cluster)
   220  	case ReconfiguringType:
   221  		return r.validateReconfigure(cluster)
   222  	case SwitchoverType:
   223  		return r.validateSwitchover(ctx, k8sClient, cluster)
   224  	case DataScriptType:
   225  		return r.validateDataScript(ctx, k8sClient, cluster)
   226  	}
   227  	return nil
   228  }
   229  
   230  // validateUpgrade validates spec.restart
   231  func (r *OpsRequest) validateRestart(cluster *Cluster) error {
   232  	restartList := r.Spec.RestartList
   233  	if len(restartList) == 0 {
   234  		return notEmptyError("spec.restart")
   235  	}
   236  
   237  	compNames := make([]string, len(restartList))
   238  	for i, v := range restartList {
   239  		compNames[i] = v.ComponentName
   240  	}
   241  	return r.checkComponentExistence(cluster, compNames)
   242  }
   243  
   244  // validateUpgrade validates spec.clusterOps.upgrade
   245  func (r *OpsRequest) validateUpgrade(ctx context.Context,
   246  	k8sClient client.Client) error {
   247  	if r.Spec.Upgrade == nil {
   248  		return notEmptyError("spec.upgrade")
   249  	}
   250  
   251  	clusterVersion := &ClusterVersion{}
   252  	clusterVersionRef := r.Spec.Upgrade.ClusterVersionRef
   253  	if err := k8sClient.Get(ctx, types.NamespacedName{Name: clusterVersionRef}, clusterVersion); err != nil {
   254  		return fmt.Errorf("get clusterVersion: %s failed, err: %s", clusterVersionRef, err.Error())
   255  	}
   256  	return nil
   257  }
   258  
   259  // validateVerticalScaling validates api when spec.type is VerticalScaling
   260  func (r *OpsRequest) validateVerticalScaling(cluster *Cluster) error {
   261  	verticalScalingList := r.Spec.VerticalScalingList
   262  	if len(verticalScalingList) == 0 {
   263  		return notEmptyError("spec.verticalScaling")
   264  	}
   265  
   266  	// validate resources is legal and get component name slice
   267  	componentNames := make([]string, len(verticalScalingList))
   268  	for i, v := range verticalScalingList {
   269  		componentNames[i] = v.ComponentName
   270  
   271  		if invalidValue, err := validateVerticalResourceList(v.Requests); err != nil {
   272  			return invalidValueError(invalidValue, err.Error())
   273  		}
   274  		if invalidValue, err := validateVerticalResourceList(v.Limits); err != nil {
   275  			return invalidValueError(invalidValue, err.Error())
   276  		}
   277  		if invalidValue, err := compareRequestsAndLimits(v.ResourceRequirements); err != nil {
   278  			return invalidValueError(invalidValue, err.Error())
   279  		}
   280  	}
   281  	return r.checkComponentExistence(cluster, componentNames)
   282  }
   283  
   284  // validateVerticalScaling validate api is legal when spec.type is VerticalScaling
   285  func (r *OpsRequest) validateReconfigure(cluster *Cluster) error {
   286  	if webhookMgr == nil || webhookMgr.client == nil {
   287  		return nil
   288  	}
   289  	reconfigure := r.Spec.Reconfigure
   290  	if reconfigure == nil {
   291  		return notEmptyError("spec.reconfigure")
   292  	}
   293  	if cluster.Spec.GetComponentByName(reconfigure.ComponentName) == nil {
   294  		return fmt.Errorf("component %s not found", reconfigure.ComponentName)
   295  	}
   296  	for _, configuration := range reconfigure.Configurations {
   297  		cmObj, err := r.getConfigMap(fmt.Sprintf("%s-%s-%s", r.Spec.ClusterRef, reconfigure.ComponentName, configuration.Name))
   298  		if err != nil {
   299  			return err
   300  		}
   301  		for _, key := range configuration.Keys {
   302  			// check add file
   303  			if _, ok := cmObj.Data[key.Key]; !ok && key.FileContent == "" {
   304  				return errors.Errorf("key %s not found in configmap %s", key.Key, configuration.Name)
   305  			}
   306  			if key.FileContent == "" && len(key.Parameters) == 0 {
   307  				return errors.New("key.fileContent and key.parameters cannot be empty at the same time")
   308  			}
   309  		}
   310  	}
   311  	return nil
   312  }
   313  
   314  // compareRequestsAndLimits compares the resource requests and limits
   315  func compareRequestsAndLimits(resources corev1.ResourceRequirements) (string, error) {
   316  	requests := resources.Requests
   317  	limits := resources.Limits
   318  	if requests == nil || limits == nil {
   319  		return "", nil
   320  	}
   321  	for k, v := range requests {
   322  		if limitQuantity, ok := limits[k]; !ok {
   323  			continue
   324  		} else if compareQuantity(&v, &limitQuantity) {
   325  			return v.String(), errors.New(fmt.Sprintf(`must be less than or equal to %s limit`, k))
   326  		}
   327  	}
   328  	return "", nil
   329  }
   330  
   331  // compareQuantity compares requests quantity and limits quantity
   332  func compareQuantity(requestQuantity, limitQuantity *resource.Quantity) bool {
   333  	return requestQuantity != nil && limitQuantity != nil && requestQuantity.Cmp(*limitQuantity) > 0
   334  }
   335  
   336  // validateHorizontalScaling validates api when spec.type is HorizontalScaling
   337  func (r *OpsRequest) validateHorizontalScaling(ctx context.Context, cli client.Client, cluster *Cluster) error {
   338  	horizontalScalingList := r.Spec.HorizontalScalingList
   339  	if len(horizontalScalingList) == 0 {
   340  		return notEmptyError("spec.horizontalScaling")
   341  	}
   342  
   343  	componentNames := make([]string, len(horizontalScalingList))
   344  	for i, v := range horizontalScalingList {
   345  		componentNames[i] = v.ComponentName
   346  	}
   347  	return r.checkComponentExistence(cluster, componentNames)
   348  }
   349  
   350  // validateVolumeExpansion validates volumeExpansion api when spec.type is VolumeExpansion
   351  func (r *OpsRequest) validateVolumeExpansion(ctx context.Context, cli client.Client, cluster *Cluster) error {
   352  	volumeExpansionList := r.Spec.VolumeExpansionList
   353  	if len(volumeExpansionList) == 0 {
   354  		return notEmptyError("spec.volumeExpansion")
   355  	}
   356  
   357  	componentNames := make([]string, len(volumeExpansionList))
   358  	for i, v := range volumeExpansionList {
   359  		componentNames[i] = v.ComponentName
   360  	}
   361  	if err := r.checkComponentExistence(cluster, componentNames); err != nil {
   362  		return err
   363  	}
   364  	runningOpsList, err := GetRunningOpsByOpsType(ctx, cli, r.Spec.ClusterRef, r.Namespace, string(VolumeExpansionType))
   365  	if err != nil {
   366  		return err
   367  	}
   368  	if len(runningOpsList) > 0 && runningOpsList[0].Name != r.Name {
   369  		return fmt.Errorf("existing other VolumeExpansion OpsRequest: %s is running in Cluster: %s, handle this OpsRequest first", runningOpsList[0].Name, cluster.Name)
   370  	}
   371  	return r.checkVolumesAllowExpansion(ctx, cli, cluster)
   372  }
   373  
   374  // validateSwitchover validates switchover api when spec.type is Switchover.
   375  func (r *OpsRequest) validateSwitchover(ctx context.Context, cli client.Client, cluster *Cluster) error {
   376  	switchoverList := r.Spec.SwitchoverList
   377  	if len(switchoverList) == 0 {
   378  		return notEmptyError("spec.switchover")
   379  	}
   380  	componentNames := make([]string, len(switchoverList))
   381  	for i, v := range switchoverList {
   382  		componentNames[i] = v.ComponentName
   383  
   384  	}
   385  	if err := r.checkComponentExistence(cluster, componentNames); err != nil {
   386  		return err
   387  	}
   388  	runningOpsList, err := GetRunningOpsByOpsType(ctx, cli, r.Spec.ClusterRef, r.Namespace, string(SwitchoverType))
   389  	if err != nil {
   390  		return err
   391  	}
   392  	if len(runningOpsList) > 0 && runningOpsList[0].Name != r.Name {
   393  		return fmt.Errorf("existing other Switchover OpsRequest: %s is running in Cluster: %s, handle this OpsRequest first", runningOpsList[0].Name, cluster.Name)
   394  	}
   395  	return validateSwitchoverResourceList(ctx, cli, cluster, switchoverList)
   396  }
   397  
   398  // checkComponentExistence checks whether components to be operated exist in cluster spec.
   399  func (r *OpsRequest) checkComponentExistence(cluster *Cluster, compNames []string) error {
   400  	compSpecNameMap := make(map[string]bool)
   401  	for _, compSpec := range cluster.Spec.ComponentSpecs {
   402  		compSpecNameMap[compSpec.Name] = true
   403  	}
   404  
   405  	var notFoundCompNames []string
   406  	for _, compName := range compNames {
   407  		if _, ok := compSpecNameMap[compName]; !ok {
   408  			notFoundCompNames = append(notFoundCompNames, compName)
   409  		}
   410  	}
   411  
   412  	if len(notFoundCompNames) > 0 {
   413  		return fmt.Errorf("components: %v not found, you can view the components by command: "+
   414  			"kbcli cluster describe %s -n %s", notFoundCompNames, cluster.Name, r.Namespace)
   415  	}
   416  	return nil
   417  }
   418  
   419  func (r *OpsRequest) checkVolumesAllowExpansion(ctx context.Context, cli client.Client, cluster *Cluster) error {
   420  	type Entity struct {
   421  		existInSpec      bool
   422  		storageClassName *string
   423  		allowExpansion   bool
   424  		requestStorage   resource.Quantity
   425  	}
   426  
   427  	// component name -> vct name -> entity
   428  	vols := make(map[string]map[string]Entity)
   429  	for _, comp := range r.Spec.VolumeExpansionList {
   430  		for _, vct := range comp.VolumeClaimTemplates {
   431  			if _, ok := vols[comp.ComponentName]; !ok {
   432  				vols[comp.ComponentName] = make(map[string]Entity)
   433  			}
   434  			vols[comp.ComponentName][vct.Name] = Entity{false, nil, false, vct.Storage}
   435  		}
   436  	}
   437  	// traverse the spec to update volumes
   438  	for _, comp := range cluster.Spec.ComponentSpecs {
   439  		if _, ok := vols[comp.Name]; !ok {
   440  			continue // ignore not-exist component
   441  		}
   442  		for _, vct := range comp.VolumeClaimTemplates {
   443  			e, ok := vols[comp.Name][vct.Name]
   444  			if !ok {
   445  				continue
   446  			}
   447  			e.existInSpec = true
   448  			e.storageClassName = vct.Spec.StorageClassName
   449  			vols[comp.Name][vct.Name] = e
   450  		}
   451  	}
   452  
   453  	// check all used storage classes
   454  	var err error
   455  	for cname, compVols := range vols {
   456  		for vname := range compVols {
   457  			e := vols[cname][vname]
   458  			if !e.existInSpec {
   459  				continue
   460  			}
   461  			e.storageClassName, err = r.getSCNameByPvcAndCheckStorageSize(ctx, cli, cname, vname, e.requestStorage)
   462  			if err != nil {
   463  				return err
   464  			}
   465  			allowExpansion, err := r.checkStorageClassAllowExpansion(ctx, cli, e.storageClassName)
   466  			if err != nil {
   467  				continue // ignore the error and take it as not-supported
   468  			}
   469  			e.allowExpansion = allowExpansion
   470  			vols[cname][vname] = e
   471  		}
   472  	}
   473  
   474  	for cname, compVols := range vols {
   475  		var (
   476  			notFound     []string
   477  			notSupport   []string
   478  			notSupportSc []string
   479  		)
   480  		for vct, e := range compVols {
   481  			if !e.existInSpec {
   482  				notFound = append(notFound, vct)
   483  			}
   484  			if !e.allowExpansion {
   485  				notSupport = append(notSupport, vct)
   486  				if e.storageClassName != nil {
   487  					notSupportSc = append(notSupportSc, *e.storageClassName)
   488  				}
   489  			}
   490  		}
   491  		if len(notFound) > 0 {
   492  			return fmt.Errorf("volumeClaimTemplates: %v not found in component: %s, you can view infos by command: "+
   493  				"kbcli cluster describe %s -n %s", notFound, cname, cluster.Name, r.Namespace)
   494  		}
   495  		if len(notSupport) > 0 {
   496  			var notSupportScString string
   497  			if len(notSupportSc) > 0 {
   498  				notSupportScString = fmt.Sprintf("storageClass: %v of ", notSupportSc)
   499  			}
   500  			return fmt.Errorf(notSupportScString+"volumeClaimTemplate: %s not support volume expansion in component: %s, you can view infos by command: "+
   501  				"kubectl get sc", notSupport, cname)
   502  		}
   503  	}
   504  	return nil
   505  }
   506  
   507  // checkStorageClassAllowExpansion checks whether the specified storage class supports volume expansion.
   508  func (r *OpsRequest) checkStorageClassAllowExpansion(ctx context.Context,
   509  	cli client.Client,
   510  	storageClassName *string) (bool, error) {
   511  	if storageClassName == nil {
   512  		return false, nil
   513  	}
   514  	storageClass := &storagev1.StorageClass{}
   515  	// take not found error as unsupported
   516  	if err := cli.Get(ctx, types.NamespacedName{Name: *storageClassName}, storageClass); err != nil && !apierrors.IsNotFound(err) {
   517  		return false, err
   518  	}
   519  	if storageClass == nil || storageClass.AllowVolumeExpansion == nil {
   520  		return false, nil
   521  	}
   522  	return *storageClass.AllowVolumeExpansion, nil
   523  }
   524  
   525  // getSCNameByPvcAndCheckStorageSize gets the storageClassName by pvc and checks if the storage size is valid.
   526  func (r *OpsRequest) getSCNameByPvcAndCheckStorageSize(ctx context.Context,
   527  	cli client.Client,
   528  	compName,
   529  	vctName string,
   530  	requestStorage resource.Quantity) (*string, error) {
   531  	pvcList := &corev1.PersistentVolumeClaimList{}
   532  	if err := cli.List(ctx, pvcList, client.InNamespace(r.Namespace), client.MatchingLabels{
   533  		constant.AppInstanceLabelKey:    r.Spec.ClusterRef,
   534  		constant.KBAppComponentLabelKey: compName,
   535  	}); err != nil {
   536  		return nil, err
   537  	}
   538  	if len(pvcList.Items) == 0 {
   539  		return nil, nil
   540  	}
   541  	var pvc *corev1.PersistentVolumeClaim
   542  	for _, v := range pvcList.Items {
   543  		// VolumeClaimTemplateNameLabelKeyForLegacy is deprecated: only compatible with version 0.5, will be removed in 0.7?
   544  		if v.Labels[constant.VolumeClaimTemplateNameLabelKey] == vctName ||
   545  			v.Labels[constant.VolumeClaimTemplateNameLabelKeyForLegacy] == vctName {
   546  			pvc = &v
   547  			break
   548  		}
   549  	}
   550  	if pvc == nil {
   551  		return nil, nil
   552  	}
   553  	previousValue := *pvc.Status.Capacity.Storage()
   554  	if requestStorage.Cmp(previousValue) < 0 {
   555  		return nil, fmt.Errorf(`requested storage size of volumeClaimTemplate "%s" can not less than status.capacity.storage "%s" `,
   556  			vctName, previousValue.String())
   557  	}
   558  	return pvc.Spec.StorageClassName, nil
   559  }
   560  
   561  // validateDataScript validates the data script.
   562  func (r *OpsRequest) validateDataScript(ctx context.Context, cli client.Client, cluster *Cluster) error {
   563  	validateScript := func(spec *ScriptSpec) error {
   564  		rawScripts := spec.Script
   565  		scriptsFrom := spec.ScriptFrom
   566  		if len(rawScripts) == 0 && (scriptsFrom == nil) {
   567  			return fmt.Errorf("spec.scriptSpec.script and spec.scriptSpec.scriptFrom can not be empty at the same time")
   568  		}
   569  		if scriptsFrom != nil {
   570  			if scriptsFrom.ConfigMapRef == nil && scriptsFrom.SecretRef == nil {
   571  				return fmt.Errorf("spec.scriptSpec.scriptFrom.configMapRefs and spec.scriptSpec.scriptFrom.secretRefs can not be empty at the same time")
   572  			}
   573  			for _, configMapRef := range scriptsFrom.ConfigMapRef {
   574  				if err := cli.Get(ctx, types.NamespacedName{Name: configMapRef.Name, Namespace: r.Namespace}, &corev1.ConfigMap{}); err != nil {
   575  					return err
   576  				}
   577  			}
   578  			for _, secret := range scriptsFrom.SecretRef {
   579  				if err := cli.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: r.Namespace}, &corev1.Secret{}); err != nil {
   580  					return err
   581  				}
   582  			}
   583  		}
   584  		return nil
   585  	}
   586  
   587  	scriptSpec := r.Spec.ScriptSpec
   588  	if scriptSpec == nil {
   589  		return notEmptyError("spec.scriptSpec")
   590  	}
   591  
   592  	if err := r.checkComponentExistence(cluster, []string{scriptSpec.ComponentName}); err != nil {
   593  		return err
   594  	}
   595  
   596  	if err := validateScript(scriptSpec); err != nil {
   597  		return err
   598  	}
   599  
   600  	return nil
   601  }
   602  
   603  // validateVerticalResourceList checks if k8s resourceList is legal
   604  func validateVerticalResourceList(resourceList map[corev1.ResourceName]resource.Quantity) (string, error) {
   605  	for k := range resourceList {
   606  		if k != corev1.ResourceCPU && k != corev1.ResourceMemory && !strings.HasPrefix(k.String(), corev1.ResourceHugePagesPrefix) {
   607  			return string(k), fmt.Errorf("resource key is not cpu or memory or hugepages- ")
   608  		}
   609  	}
   610  
   611  	return "", nil
   612  }
   613  
   614  func notEmptyError(target string) error {
   615  	return fmt.Errorf(`"%s" can not be empty`, target)
   616  }
   617  
   618  func invalidValueError(target string, value string) error {
   619  	return fmt.Errorf(`invalid value for "%s": %s`, target, value)
   620  }
   621  
   622  // GetRunningOpsByOpsType gets the running opsRequests by type.
   623  func GetRunningOpsByOpsType(ctx context.Context, cli client.Client,
   624  	clusterName, namespace, opsType string) ([]OpsRequest, error) {
   625  	opsRequestList := &OpsRequestList{}
   626  	if err := cli.List(ctx, opsRequestList, client.MatchingLabels{
   627  		constant.AppInstanceLabelKey:    clusterName,
   628  		constant.OpsRequestTypeLabelKey: opsType,
   629  	}, client.InNamespace(namespace)); err != nil {
   630  		return nil, err
   631  	}
   632  	if len(opsRequestList.Items) == 0 {
   633  		return nil, nil
   634  	}
   635  	var runningOpsList []OpsRequest
   636  	for _, v := range opsRequestList.Items {
   637  		if v.Status.Phase == OpsRunningPhase {
   638  			runningOpsList = append(runningOpsList, v)
   639  			break
   640  		}
   641  	}
   642  	return runningOpsList, nil
   643  }
   644  
   645  // validateSwitchoverResourceList checks if switchover resourceList is legal.
   646  func validateSwitchoverResourceList(ctx context.Context, cli client.Client, cluster *Cluster, switchoverList []Switchover) error {
   647  	for _, switchover := range switchoverList {
   648  		if switchover.InstanceName == "" {
   649  			return notEmptyError("switchover.instanceName")
   650  		}
   651  
   652  		// check clusterComponentDefinition whether support switchover
   653  		compDefObj, err := GetComponentDefByCluster(ctx, cli, *cluster, cluster.Spec.GetComponentDefRefName(switchover.ComponentName))
   654  		if err != nil {
   655  			return err
   656  		}
   657  		if compDefObj == nil {
   658  			return fmt.Errorf("this cluster component %s is invalid", switchover.ComponentName)
   659  		}
   660  		if compDefObj.SwitchoverSpec == nil {
   661  			return fmt.Errorf("this cluster component %s does not support switchover", switchover.ComponentName)
   662  		}
   663  		switch switchover.InstanceName {
   664  		case constant.KBSwitchoverCandidateInstanceForAnyPod:
   665  			if compDefObj.SwitchoverSpec.WithoutCandidate == nil {
   666  				return fmt.Errorf("this cluster component %s does not support promote without specifying an instance. Please specify a specific instance for the promotion", switchover.ComponentName)
   667  			}
   668  		default:
   669  			if compDefObj.SwitchoverSpec.WithCandidate == nil {
   670  				return fmt.Errorf("this cluster component %s does not support specifying an instance for promote. If you want to perform a promote operation, please do not specify an instance", switchover.ComponentName)
   671  			}
   672  		}
   673  
   674  		// check switchover.InstanceName whether exist and role label is correct
   675  		if switchover.InstanceName == constant.KBSwitchoverCandidateInstanceForAnyPod {
   676  			return nil
   677  		}
   678  		pod := &corev1.Pod{}
   679  		if err := cli.Get(ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: switchover.InstanceName}, pod); err != nil {
   680  			return fmt.Errorf("get instanceName %s failed, err: %s, and check the validity of the instanceName using \"kbcli cluster list-instances\"", switchover.InstanceName, err.Error())
   681  		}
   682  		v, ok := pod.Labels[constant.RoleLabelKey]
   683  		if !ok || v == "" {
   684  			return fmt.Errorf("instanceName %s cannot be promoted because it had a invalid role label", switchover.InstanceName)
   685  		}
   686  		if v == constant.Primary || v == constant.Leader {
   687  			return fmt.Errorf("instanceName %s cannot be promoted because it is already the primary or leader instance", switchover.InstanceName)
   688  		}
   689  		if !strings.HasPrefix(pod.Name, fmt.Sprintf("%s-%s", cluster.Name, switchover.ComponentName)) {
   690  			return fmt.Errorf("instanceName %s does not belong to the current component, please check the validity of the instance using \"kbcli cluster list-instances\"", switchover.InstanceName)
   691  		}
   692  	}
   693  	return nil
   694  }