github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/update.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package cluster
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"encoding/csv"
    26  	"encoding/json"
    27  	"fmt"
    28  	"strconv"
    29  	"strings"
    30  	"text/template"
    31  
    32  	"github.com/google/uuid"
    33  	"github.com/pkg/errors"
    34  	"github.com/robfig/cron/v3"
    35  	"github.com/spf13/cobra"
    36  	"github.com/spf13/pflag"
    37  	corev1 "k8s.io/api/core/v1"
    38  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    39  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    40  	"k8s.io/apimachinery/pkg/runtime"
    41  	"k8s.io/cli-runtime/pkg/genericiooptions"
    42  	"k8s.io/client-go/dynamic"
    43  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    44  	"k8s.io/kubectl/pkg/util/templates"
    45  
    46  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    47  	dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1"
    48  	"github.com/1aal/kubeblocks/pkg/cli/cluster"
    49  	"github.com/1aal/kubeblocks/pkg/cli/patch"
    50  	"github.com/1aal/kubeblocks/pkg/cli/types"
    51  	"github.com/1aal/kubeblocks/pkg/cli/util"
    52  	cfgcore "github.com/1aal/kubeblocks/pkg/configuration/core"
    53  	"github.com/1aal/kubeblocks/pkg/constant"
    54  	"github.com/1aal/kubeblocks/pkg/controller/configuration"
    55  	"github.com/1aal/kubeblocks/pkg/dataprotection/utils"
    56  	"github.com/1aal/kubeblocks/pkg/gotemplate"
    57  )
    58  
    59  var clusterUpdateExample = templates.Examples(`
    60  	# update cluster mycluster termination policy to Delete
    61  	kbcli cluster update mycluster --termination-policy=Delete
    62  
    63  	# enable cluster monitor
    64  	kbcli cluster update mycluster --monitor=true
    65  
    66      # enable all logs
    67  	kbcli cluster update mycluster --enable-all-logs=true
    68  
    69      # update cluster topology keys and affinity
    70  	kbcli cluster update mycluster --topology-keys=kubernetes.io/hostname --pod-anti-affinity=Required
    71  
    72  	# update cluster tolerations
    73  	kbcli cluster update mycluster --tolerations='"key=engineType,value=mongo,operator=Equal,effect=NoSchedule","key=diskType,value=ssd,operator=Equal,effect=NoSchedule"'
    74  
    75  	# edit cluster
    76  	kbcli cluster update mycluster --edit
    77  
    78  	# enable cluster monitor and edit
    79      # kbcli cluster update mycluster --monitor=true --edit
    80  
    81  	# enable cluster auto backup
    82  	kbcli cluster update mycluster --backup-enabled=true
    83  	
    84  	# update cluster backup retention period
    85  	kbcli cluster update mycluster --backup-retention-period=1d
    86  
    87  	# update cluster backup method
    88  	kbcli cluster update mycluster --backup-method=snapshot
    89  
    90  	# update cluster backup cron expression
    91  	kbcli cluster update mycluster --backup-cron-expression="0 0 * * *"
    92  
    93  	# update cluster backup starting deadline minutes
    94  	kbcli cluster update mycluster --backup-starting-deadline-minutes=10
    95  
    96  	# update cluster backup repo name
    97  	kbcli cluster update mycluster --backup-repo-name=repo1
    98  
    99  	# update cluster backup pitr enabled
   100  	kbcli cluster update mycluster --pitr-enabled=true
   101  `)
   102  
   103  type updateOptions struct {
   104  	namespace string
   105  	dynamic   dynamic.Interface
   106  	cluster   *appsv1alpha1.Cluster
   107  
   108  	UpdatableFlags
   109  	*patch.Options
   110  }
   111  
   112  func NewUpdateCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   113  	o := &updateOptions{Options: patch.NewOptions(f, streams, types.ClusterGVR())}
   114  	o.Options.OutputOperation = func(didPatch bool) string {
   115  		if didPatch {
   116  			return "updated"
   117  		}
   118  		return "updated (no change)"
   119  	}
   120  
   121  	cmd := &cobra.Command{
   122  		Use:               "update NAME",
   123  		Short:             "Update the cluster settings, such as enable or disable monitor or log.",
   124  		Example:           clusterUpdateExample,
   125  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR),
   126  		Run: func(cmd *cobra.Command, args []string) {
   127  			util.CheckErr(o.complete(cmd, args))
   128  			util.CheckErr(o.Run(cmd))
   129  		},
   130  	}
   131  	o.UpdatableFlags.addFlags(cmd)
   132  	o.Options.AddFlags(cmd)
   133  
   134  	return cmd
   135  }
   136  
   137  func (o *updateOptions) complete(cmd *cobra.Command, args []string) error {
   138  	var err error
   139  	if len(args) == 0 {
   140  		return makeMissingClusterNameErr()
   141  	}
   142  	if len(args) > 1 {
   143  		return fmt.Errorf("only support to update one cluster")
   144  	}
   145  	o.Names = args
   146  
   147  	// record the flags that been set by user
   148  	var flags []*pflag.Flag
   149  	cmd.Flags().Visit(func(flag *pflag.Flag) {
   150  		flags = append(flags, flag)
   151  	})
   152  
   153  	// nothing to do
   154  	if len(flags) == 0 {
   155  		return nil
   156  	}
   157  
   158  	if o.namespace, _, err = o.Factory.ToRawKubeConfigLoader().Namespace(); err != nil {
   159  		return err
   160  	}
   161  	if o.dynamic, err = o.Factory.DynamicClient(); err != nil {
   162  		return err
   163  	}
   164  	return o.buildPatch(flags)
   165  }
   166  
   167  func (o *updateOptions) buildPatch(flags []*pflag.Flag) error {
   168  	var err error
   169  	type buildFn func(obj map[string]interface{}, v pflag.Value, field string) error
   170  
   171  	buildFlagObj := func(obj map[string]interface{}, v pflag.Value, field string) error {
   172  		var val interface{}
   173  		switch v.Type() {
   174  		case "string":
   175  			val = v.String()
   176  		case "stringArray", "stringSlice":
   177  			val = v.(pflag.SliceValue).GetSlice()
   178  		case "stringToString":
   179  			valMap := make(map[string]interface{}, 0)
   180  			vStr := strings.Trim(v.String(), "[]")
   181  			if len(vStr) > 0 {
   182  				r := csv.NewReader(strings.NewReader(vStr))
   183  				ss, err := r.Read()
   184  				if err != nil {
   185  					return err
   186  				}
   187  				for _, pair := range ss {
   188  					kv := strings.SplitN(pair, "=", 2)
   189  					if len(kv) != 2 {
   190  						return fmt.Errorf("%s must be formatted as key=value", pair)
   191  					}
   192  					valMap[kv[0]] = kv[1]
   193  				}
   194  			}
   195  			val = valMap
   196  		}
   197  		return unstructured.SetNestedField(obj, val, field)
   198  	}
   199  
   200  	buildTolObj := func(obj map[string]interface{}, v pflag.Value, field string) error {
   201  		tolerations, err := util.BuildTolerations(o.TolerationsRaw)
   202  		if err != nil {
   203  			return err
   204  		}
   205  		return unstructured.SetNestedField(obj, tolerations, field)
   206  	}
   207  
   208  	buildComps := func(obj map[string]interface{}, v pflag.Value, field string) error {
   209  		return o.buildComponents(field, v.String())
   210  	}
   211  
   212  	buildBackup := func(obj map[string]interface{}, v pflag.Value, field string) error {
   213  		return o.buildBackup(field, v.String())
   214  	}
   215  
   216  	spec := map[string]interface{}{}
   217  	affinity := map[string]interface{}{}
   218  	type filedObj struct {
   219  		field string
   220  		obj   map[string]interface{}
   221  		fn    buildFn
   222  	}
   223  
   224  	flagFieldMapping := map[string]*filedObj{
   225  		"termination-policy": {field: "terminationPolicy", obj: spec, fn: buildFlagObj},
   226  		"pod-anti-affinity":  {field: "podAntiAffinity", obj: affinity, fn: buildFlagObj},
   227  		"topology-keys":      {field: "topologyKeys", obj: affinity, fn: buildFlagObj},
   228  		"node-labels":        {field: "nodeLabels", obj: affinity, fn: buildFlagObj},
   229  		"tenancy":            {field: "tenancy", obj: affinity, fn: buildFlagObj},
   230  
   231  		// tolerations
   232  		"tolerations": {field: "tolerations", obj: spec, fn: buildTolObj},
   233  
   234  		// monitor and logs
   235  		"monitoring-interval": {field: "monitor", obj: nil, fn: buildComps},
   236  		"enable-all-logs":     {field: "enable-all-logs", obj: nil, fn: buildComps},
   237  
   238  		// backup config
   239  		"backup-enabled":                   {field: "enabled", obj: nil, fn: buildBackup},
   240  		"backup-retention-period":          {field: "retentionPeriod", obj: nil, fn: buildBackup},
   241  		"backup-method":                    {field: "method", obj: nil, fn: buildBackup},
   242  		"backup-cron-expression":           {field: "cronExpression", obj: nil, fn: buildBackup},
   243  		"backup-starting-deadline-minutes": {field: "startingDeadlineMinutes", obj: nil, fn: buildBackup},
   244  		"backup-repo-name":                 {field: "repoName", obj: nil, fn: buildBackup},
   245  		"pitr-enabled":                     {field: "pitrEnabled", obj: nil, fn: buildBackup},
   246  	}
   247  
   248  	for _, flag := range flags {
   249  		if f, ok := flagFieldMapping[flag.Name]; ok {
   250  			if err = f.fn(f.obj, flag.Value, f.field); err != nil {
   251  				return err
   252  			}
   253  		}
   254  	}
   255  
   256  	if len(affinity) > 0 {
   257  		if err = unstructured.SetNestedField(spec, affinity, "affinity"); err != nil {
   258  			return err
   259  		}
   260  	}
   261  
   262  	if o.cluster != nil {
   263  		// if update the backup config, the backup method must have value
   264  		if o.cluster.Spec.Backup != nil {
   265  			backupPolicyListObj, err := o.dynamic.Resource(types.BackupPolicyGVR()).Namespace(o.namespace).List(context.Background(), metav1.ListOptions{
   266  				LabelSelector: fmt.Sprintf("%s=%s", constant.AppInstanceLabelKey, o.cluster.Name),
   267  			})
   268  			if err != nil {
   269  				return err
   270  			}
   271  			backupPolicyList := &dpv1alpha1.BackupPolicyList{}
   272  			if err := runtime.DefaultUnstructuredConverter.FromUnstructured(backupPolicyListObj.UnstructuredContent(), backupPolicyList); err != nil {
   273  				return err
   274  			}
   275  
   276  			defaultBackupMethod, backupMethodMap, err := utils.GetBackupMethodsFromBackupPolicy(backupPolicyList, "")
   277  			if err != nil {
   278  				return err
   279  			}
   280  			if o.cluster.Spec.Backup.Method == "" {
   281  				o.cluster.Spec.Backup.Method = defaultBackupMethod
   282  			}
   283  			if _, ok := backupMethodMap[o.cluster.Spec.Backup.Method]; !ok {
   284  				return fmt.Errorf("backup method %s is not supported, please view the supported backup methods by `kbcli cd describe %s`", o.cluster.Spec.Backup.Method, o.cluster.Spec.ClusterDefRef)
   285  			}
   286  		}
   287  
   288  		data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&o.cluster.Spec)
   289  		if err != nil {
   290  			return err
   291  		}
   292  
   293  		if err = unstructured.SetNestedField(spec, data["componentSpecs"], "componentSpecs"); err != nil {
   294  			return err
   295  		}
   296  
   297  		if err = unstructured.SetNestedField(spec, data["backup"], "backup"); err != nil {
   298  			return err
   299  		}
   300  	}
   301  
   302  	obj := unstructured.Unstructured{
   303  		Object: map[string]interface{}{
   304  			"spec": spec,
   305  		},
   306  	}
   307  	bytes, err := obj.MarshalJSON()
   308  	if err != nil {
   309  		return err
   310  	}
   311  	o.Patch = string(bytes)
   312  	return nil
   313  }
   314  
   315  func (o *updateOptions) buildComponents(field string, val string) error {
   316  	if o.cluster == nil {
   317  		c, err := cluster.GetClusterByName(o.dynamic, o.Names[0], o.namespace)
   318  		if err != nil {
   319  			return err
   320  		}
   321  		o.cluster = c
   322  	}
   323  
   324  	switch field {
   325  	case "monitor":
   326  		return o.updateMonitor(val)
   327  	case "enable-all-logs":
   328  		return o.updateEnabledLog(val)
   329  	default:
   330  		return nil
   331  	}
   332  }
   333  
   334  func (o *updateOptions) buildBackup(field string, val string) error {
   335  	if o.cluster == nil {
   336  		c, err := cluster.GetClusterByName(o.dynamic, o.Names[0], o.namespace)
   337  		if err != nil {
   338  			return err
   339  		}
   340  		o.cluster = c
   341  	}
   342  	if o.cluster.Spec.Backup == nil {
   343  		o.cluster.Spec.Backup = &appsv1alpha1.ClusterBackup{}
   344  	}
   345  
   346  	switch field {
   347  	case "enabled":
   348  		return o.updateBackupEnabled(val)
   349  	case "retentionPeriod":
   350  		return o.updateBackupRetentionPeriod(val)
   351  	case "method":
   352  		return o.updateBackupMethod(val)
   353  	case "cronExpression":
   354  		return o.updateBackupCronExpression(val)
   355  	case "startingDeadlineMinutes":
   356  		return o.updateBackupStartingDeadlineMinutes(val)
   357  	case "repoName":
   358  		return o.updateBackupRepoName(val)
   359  	case "pitrEnabled":
   360  		return o.updateBackupPitrEnabled(val)
   361  	default:
   362  		return nil
   363  	}
   364  }
   365  
   366  func (o *updateOptions) updateEnabledLog(val string) error {
   367  	boolVal, err := strconv.ParseBool(val)
   368  	if err != nil {
   369  		return err
   370  	}
   371  
   372  	// update --enabled-all-logs=false for all components
   373  	if !boolVal {
   374  		for index := range o.cluster.Spec.ComponentSpecs {
   375  			o.cluster.Spec.ComponentSpecs[index].EnabledLogs = nil
   376  		}
   377  		return nil
   378  	}
   379  
   380  	// update --enabled-all-logs=true for all components
   381  	cd, err := cluster.GetClusterDefByName(o.dynamic, o.cluster.Spec.ClusterDefRef)
   382  	if err != nil {
   383  		return err
   384  	}
   385  	// set --enabled-all-logs at cluster components
   386  	setEnableAllLogs(o.cluster, cd)
   387  	if err = o.reconfigureLogVariables(o.cluster, cd); err != nil {
   388  		return errors.Wrap(err, "failed to reconfigure log variables of target cluster")
   389  	}
   390  	return nil
   391  }
   392  
   393  const logsBlockName = "logsBlock"
   394  const logsTemplateName = "template-logs-block"
   395  const topTPLLogsObject = "component"
   396  const defaultSectionName = "default"
   397  
   398  // reconfigureLogVariables reconfigures the log variables of cluster
   399  func (o *updateOptions) reconfigureLogVariables(c *appsv1alpha1.Cluster, cd *appsv1alpha1.ClusterDefinition) error {
   400  	var (
   401  		err        error
   402  		configSpec *appsv1alpha1.ComponentConfigSpec
   403  		logValue   *gotemplate.TplValues
   404  	)
   405  
   406  	createReconfigureOps := func(compSpec appsv1alpha1.ClusterComponentSpec, configSpec *appsv1alpha1.ComponentConfigSpec, logValue *gotemplate.TplValues) error {
   407  		var (
   408  			buf             bytes.Buffer
   409  			keyName         string
   410  			configTemplate  *corev1.ConfigMap
   411  			formatter       *appsv1alpha1.FormatterConfig
   412  			logTPL          *template.Template
   413  			logVariables    map[string]string
   414  			unstructuredObj *unstructured.Unstructured
   415  		)
   416  
   417  		if configTemplate, formatter, err = findConfigTemplateInfo(o.dynamic, configSpec); err != nil {
   418  			return err
   419  		}
   420  		if keyName, logTPL, err = findLogsBlockTPL(configTemplate.Data); err != nil {
   421  			return err
   422  		}
   423  		if logTPL == nil {
   424  			return nil
   425  		}
   426  		if err = logTPL.Execute(&buf, logValue); err != nil {
   427  			return err
   428  		}
   429  		// TODO: very hack logic for ini config file
   430  		formatter.FormatterOptions = appsv1alpha1.FormatterOptions{IniConfig: &appsv1alpha1.IniConfig{SectionName: defaultSectionName}}
   431  		if logVariables, err = cfgcore.TransformConfigFileToKeyValueMap(keyName, formatter, buf.Bytes()); err != nil {
   432  			return err
   433  		}
   434  		// build OpsRequest and apply this OpsRequest
   435  		opsRequest := buildLogsReconfiguringOps(c.Name, c.Namespace, compSpec.Name, configSpec.Name, keyName, logVariables)
   436  		if unstructuredObj, err = util.ConvertObjToUnstructured(opsRequest); err != nil {
   437  			return err
   438  		}
   439  		return util.CreateResourceIfAbsent(o.dynamic, types.OpsGVR(), c.Namespace, unstructuredObj)
   440  	}
   441  
   442  	for _, compSpec := range c.Spec.ComponentSpecs {
   443  		if configSpec, err = findFirstConfigSpec(c.Spec.ComponentSpecs, cd.Spec.ComponentDefs, compSpec.Name); err != nil {
   444  			return err
   445  		}
   446  		if logValue, err = buildLogsTPLValues(&compSpec); err != nil {
   447  			return err
   448  		}
   449  		if err = createReconfigureOps(compSpec, configSpec, logValue); err != nil {
   450  			return err
   451  		}
   452  	}
   453  	return nil
   454  }
   455  
   456  func findFirstConfigSpec(
   457  	compSpecs []appsv1alpha1.ClusterComponentSpec,
   458  	cdCompSpecs []appsv1alpha1.ClusterComponentDefinition,
   459  	compName string) (*appsv1alpha1.ComponentConfigSpec, error) {
   460  	configSpecs, err := util.GetConfigTemplateListWithResource(compSpecs, cdCompSpecs, nil, compName, true)
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  	if len(configSpecs) == 0 {
   465  		return nil, errors.Errorf("no config templates for component %s", compName)
   466  	}
   467  	return &configSpecs[0], nil
   468  }
   469  
   470  func findConfigTemplateInfo(dynamic dynamic.Interface, configSpec *appsv1alpha1.ComponentConfigSpec) (*corev1.ConfigMap, *appsv1alpha1.FormatterConfig, error) {
   471  	if configSpec == nil {
   472  		return nil, nil, errors.New("configTemplateSpec is nil")
   473  	}
   474  	configTemplate, err := cluster.GetConfigMapByName(dynamic, configSpec.Namespace, configSpec.TemplateRef)
   475  	if err != nil {
   476  		return nil, nil, err
   477  	}
   478  	configConstraint, err := cluster.GetConfigConstraintByName(dynamic, configSpec.ConfigConstraintRef)
   479  	if err != nil {
   480  		return nil, nil, err
   481  	}
   482  	return configTemplate, configConstraint.Spec.FormatterConfig, nil
   483  }
   484  
   485  func newConfigTemplateEngine() *template.Template {
   486  	customizedFuncMap := configuration.BuiltInCustomFunctions(nil, nil, nil)
   487  	engine := gotemplate.NewTplEngine(nil, customizedFuncMap, logsTemplateName, nil, context.TODO())
   488  	return engine.GetTplEngine()
   489  }
   490  
   491  func findLogsBlockTPL(confData map[string]string) (string, *template.Template, error) {
   492  	engine := newConfigTemplateEngine()
   493  	for key, value := range confData {
   494  		if !strings.Contains(value, logsBlockName) {
   495  			continue
   496  		}
   497  		tpl, err := engine.Parse(value)
   498  		if err != nil {
   499  			return key, nil, err
   500  		}
   501  		logTPL := tpl.Lookup(logsBlockName)
   502  		// find target logs template
   503  		if logTPL != nil {
   504  			return key, logTPL, nil
   505  		}
   506  		return "", nil, errors.New("no logs config template found")
   507  	}
   508  	return "", nil, nil
   509  }
   510  
   511  func buildLogsTPLValues(compSpec *appsv1alpha1.ClusterComponentSpec) (*gotemplate.TplValues, error) {
   512  	compMap := map[string]interface{}{}
   513  	bytesData, err := json.Marshal(compSpec)
   514  	if err != nil {
   515  		return nil, err
   516  	}
   517  	err = json.Unmarshal(bytesData, &compMap)
   518  	if err != nil {
   519  		return nil, err
   520  	}
   521  	value := gotemplate.TplValues{
   522  		topTPLLogsObject: compMap,
   523  	}
   524  	return &value, nil
   525  }
   526  
   527  func buildLogsReconfiguringOps(clusterName, namespace, compName, configName, keyName string, variables map[string]string) *appsv1alpha1.OpsRequest {
   528  	opsName := fmt.Sprintf("%s-%s", "logs-reconfigure", uuid.NewString())
   529  	opsRequest := util.NewOpsRequestForReconfiguring(opsName, namespace, clusterName)
   530  	parameterPairs := make([]appsv1alpha1.ParameterPair, 0, len(variables))
   531  	for key, value := range variables {
   532  		v := value
   533  		parameterPairs = append(parameterPairs, appsv1alpha1.ParameterPair{
   534  			Key:   key,
   535  			Value: &v,
   536  		})
   537  	}
   538  	var keys []appsv1alpha1.ParameterConfig
   539  	keys = append(keys, appsv1alpha1.ParameterConfig{
   540  		Key:        keyName,
   541  		Parameters: parameterPairs,
   542  	})
   543  	var configurations []appsv1alpha1.ConfigurationItem
   544  	configurations = append(configurations, appsv1alpha1.ConfigurationItem{
   545  		Keys: keys,
   546  		Name: configName,
   547  	})
   548  	reconfigure := opsRequest.Spec.Reconfigure
   549  	reconfigure.ComponentName = compName
   550  	reconfigure.Configurations = append(reconfigure.Configurations, configurations...)
   551  	return opsRequest
   552  }
   553  
   554  func (o *updateOptions) updateMonitor(val string) error {
   555  	intVal, err := strconv.ParseInt(val, 10, 32)
   556  	if err != nil {
   557  		return err
   558  	}
   559  
   560  	for i := range o.cluster.Spec.ComponentSpecs {
   561  		o.cluster.Spec.ComponentSpecs[i].Monitor = intVal != 0
   562  	}
   563  	return nil
   564  }
   565  
   566  func (o *updateOptions) updateBackupEnabled(val string) error {
   567  	boolVal, err := strconv.ParseBool(val)
   568  	if err != nil {
   569  		return err
   570  	}
   571  	o.cluster.Spec.Backup.Enabled = &boolVal
   572  	return nil
   573  }
   574  
   575  func (o *updateOptions) updateBackupRetentionPeriod(val string) error {
   576  	// if val is empty, do nothing
   577  	if len(val) == 0 {
   578  		return nil
   579  	}
   580  
   581  	// judge whether val end with the 'd'|'h' character
   582  	lastChar := val[len(val)-1]
   583  	if lastChar != 'd' && lastChar != 'h' {
   584  		return fmt.Errorf("invalid retention period: %s, only support d|h", val)
   585  	}
   586  
   587  	o.cluster.Spec.Backup.RetentionPeriod = dpv1alpha1.RetentionPeriod(val)
   588  	return nil
   589  }
   590  
   591  func (o *updateOptions) updateBackupMethod(val string) error {
   592  	// TODO(ldm): validate backup method are defined in the backup policy.
   593  	o.cluster.Spec.Backup.Method = val
   594  	return nil
   595  }
   596  
   597  func (o *updateOptions) updateBackupCronExpression(val string) error {
   598  	// judge whether val is a valid cron expression
   599  	if _, err := cron.ParseStandard(val); err != nil {
   600  		return fmt.Errorf("invalid cron expression: %s, please see https://en.wikipedia.org/wiki/Cron", val)
   601  	}
   602  
   603  	o.cluster.Spec.Backup.CronExpression = val
   604  	return nil
   605  }
   606  
   607  func (o *updateOptions) updateBackupStartingDeadlineMinutes(val string) error {
   608  	intVal, err := strconv.ParseInt(val, 10, 64)
   609  	if err != nil {
   610  		return err
   611  	}
   612  	o.cluster.Spec.Backup.StartingDeadlineMinutes = &intVal
   613  	return nil
   614  }
   615  
   616  func (o *updateOptions) updateBackupRepoName(val string) error {
   617  	o.cluster.Spec.Backup.RepoName = val
   618  	return nil
   619  }
   620  
   621  func (o *updateOptions) updateBackupPitrEnabled(val string) error {
   622  	boolVal, err := strconv.ParseBool(val)
   623  	if err != nil {
   624  		return err
   625  	}
   626  	o.cluster.Spec.Backup.PITREnabled = &boolVal
   627  	return nil
   628  }