github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/operations.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  	"context"
    24  	"fmt"
    25  	"strings"
    26  
    27  	jsonpatch "github.com/evanphx/json-patch"
    28  	"github.com/spf13/cobra"
    29  	"golang.org/x/exp/slices"
    30  	corev1 "k8s.io/api/core/v1"
    31  	"k8s.io/apimachinery/pkg/api/resource"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	"k8s.io/apimachinery/pkg/runtime/schema"
    35  	apitypes "k8s.io/apimachinery/pkg/types"
    36  	"k8s.io/apimachinery/pkg/util/json"
    37  	"k8s.io/cli-runtime/pkg/genericiooptions"
    38  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    39  	"k8s.io/kubectl/pkg/util/templates"
    40  	"sigs.k8s.io/controller-runtime/pkg/client"
    41  
    42  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    43  	"github.com/1aal/kubeblocks/pkg/cli/cluster"
    44  	classutil "github.com/1aal/kubeblocks/pkg/cli/cmd/class"
    45  	"github.com/1aal/kubeblocks/pkg/cli/create"
    46  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    47  	"github.com/1aal/kubeblocks/pkg/cli/types"
    48  	"github.com/1aal/kubeblocks/pkg/cli/util"
    49  	"github.com/1aal/kubeblocks/pkg/cli/util/flags"
    50  	"github.com/1aal/kubeblocks/pkg/cli/util/prompt"
    51  	"github.com/1aal/kubeblocks/pkg/constant"
    52  )
    53  
    54  type OperationsOptions struct {
    55  	create.CreateOptions  `json:"-"`
    56  	HasComponentNamesFlag bool `json:"-"`
    57  	// autoApprove when set true, skip the double check.
    58  	autoApprove            bool     `json:"-"`
    59  	ComponentNames         []string `json:"componentNames,omitempty"`
    60  	OpsRequestName         string   `json:"opsRequestName"`
    61  	TTLSecondsAfterSucceed int      `json:"ttlSecondsAfterSucceed"`
    62  
    63  	// OpsType operation type
    64  	OpsType appsv1alpha1.OpsType `json:"type"`
    65  
    66  	// OpsTypeLower lower OpsType
    67  	OpsTypeLower string `json:"typeLower"`
    68  
    69  	// Upgrade options
    70  	ClusterVersionRef string `json:"clusterVersionRef"`
    71  
    72  	// VerticalScaling options
    73  	CPU         string                   `json:"cpu"`
    74  	Memory      string                   `json:"memory"`
    75  	Class       string                   `json:"class"`
    76  	ClassDefRef appsv1alpha1.ClassDefRef `json:"classDefRef,omitempty"`
    77  
    78  	// HorizontalScaling options
    79  	Replicas int `json:"replicas"`
    80  
    81  	// Reconfiguring options
    82  	KeyValues       map[string]*string `json:"keyValues"`
    83  	CfgTemplateName string             `json:"cfgTemplateName"`
    84  	CfgFile         string             `json:"cfgFile"`
    85  	ForceRestart    bool               `json:"forceRestart"`
    86  	FileContent     string             `json:"fileContent"`
    87  	HasPatch        bool               `json:"hasPatch"`
    88  
    89  	// VolumeExpansion options.
    90  	// VCTNames VolumeClaimTemplate names
    91  	VCTNames []string `json:"vctNames,omitempty"`
    92  	Storage  string   `json:"storage"`
    93  
    94  	// Expose options
    95  	ExposeType    string                                 `json:"-"`
    96  	ExposeEnabled string                                 `json:"-"`
    97  	Services      []appsv1alpha1.ClusterComponentService `json:"services,omitempty"`
    98  
    99  	// Switchover options
   100  	Component string `json:"component"`
   101  	Instance  string `json:"instance"`
   102  }
   103  
   104  func newBaseOperationsOptions(f cmdutil.Factory, streams genericiooptions.IOStreams,
   105  	opsType appsv1alpha1.OpsType, hasComponentNamesFlag bool) *OperationsOptions {
   106  	customOutPut := func(opt *create.CreateOptions) {
   107  		output := fmt.Sprintf("OpsRequest %s created successfully, you can view the progress:", opt.Name)
   108  		printer.PrintLine(output)
   109  		nextLine := fmt.Sprintf("\tkbcli cluster describe-ops %s -n %s", opt.Name, opt.Namespace)
   110  		printer.PrintLine(nextLine)
   111  	}
   112  
   113  	o := &OperationsOptions{
   114  		// nil cannot be set to a map struct in CueLang, so init the map of KeyValues.
   115  		KeyValues:             map[string]*string{},
   116  		HasPatch:              true,
   117  		OpsType:               opsType,
   118  		HasComponentNamesFlag: hasComponentNamesFlag,
   119  		autoApprove:           false,
   120  		CreateOptions: create.CreateOptions{
   121  			Factory:         f,
   122  			IOStreams:       streams,
   123  			CueTemplateName: "cluster_operations_template.cue",
   124  			GVR:             types.OpsGVR(),
   125  			CustomOutPut:    customOutPut,
   126  		},
   127  	}
   128  
   129  	o.OpsTypeLower = strings.ToLower(string(o.OpsType))
   130  	o.CreateOptions.Options = o
   131  	return o
   132  }
   133  
   134  // addCommonFlags adds common flags for operations command
   135  func (o *OperationsOptions) addCommonFlags(cmd *cobra.Command, f cmdutil.Factory) {
   136  	// add print flags
   137  	printer.AddOutputFlagForCreate(cmd, &o.Format, false)
   138  
   139  	cmd.Flags().StringVar(&o.OpsRequestName, "name", "", "OpsRequest name. if not specified, it will be randomly generated ")
   140  	cmd.Flags().IntVar(&o.TTLSecondsAfterSucceed, "ttlSecondsAfterSucceed", 0, "Time to live after the OpsRequest succeed")
   141  	cmd.Flags().StringVar(&o.DryRun, "dry-run", "none", `Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent.`)
   142  	cmd.Flags().Lookup("dry-run").NoOptDefVal = "unchanged"
   143  	if o.HasComponentNamesFlag {
   144  		flags.AddComponentsFlag(f, cmd, &o.ComponentNames, "Component names to this operations")
   145  	}
   146  }
   147  
   148  // CompleteRestartOps restarts all components of the cluster
   149  // we should set all component names to ComponentNames flag.
   150  func (o *OperationsOptions) CompleteRestartOps() error {
   151  	if o.Name == "" {
   152  		return makeMissingClusterNameErr()
   153  	}
   154  	if len(o.ComponentNames) != 0 {
   155  		return nil
   156  	}
   157  	clusterObj, err := cluster.GetClusterByName(o.Dynamic, o.Name, o.Namespace)
   158  	if err != nil {
   159  		return err
   160  	}
   161  	componentSpecs := clusterObj.Spec.ComponentSpecs
   162  	o.ComponentNames = make([]string, len(componentSpecs))
   163  	for i := range componentSpecs {
   164  		o.ComponentNames[i] = componentSpecs[i].Name
   165  	}
   166  	return nil
   167  }
   168  
   169  // CompleteComponentsFlag when components flag is null and the cluster only has one component, auto complete it.
   170  func (o *OperationsOptions) CompleteComponentsFlag() error {
   171  	if o.Name == "" {
   172  		return makeMissingClusterNameErr()
   173  	}
   174  	if len(o.ComponentNames) != 0 {
   175  		return nil
   176  	}
   177  	clusterObj, err := cluster.GetClusterByName(o.Dynamic, o.Name, o.Namespace)
   178  	if err != nil {
   179  		return err
   180  	}
   181  	if len(clusterObj.Spec.ComponentSpecs) == 1 {
   182  		o.ComponentNames = []string{clusterObj.Spec.ComponentSpecs[0].Name}
   183  	}
   184  	return nil
   185  }
   186  
   187  func (o *OperationsOptions) validateUpgrade() error {
   188  	if len(o.ClusterVersionRef) == 0 {
   189  		return fmt.Errorf("missing cluster-version")
   190  	}
   191  	return nil
   192  }
   193  
   194  func (o *OperationsOptions) validateVolumeExpansion() error {
   195  	if len(o.VCTNames) == 0 {
   196  		return fmt.Errorf("missing volume-claim-templates")
   197  	}
   198  	if len(o.Storage) == 0 {
   199  		return fmt.Errorf("missing storage")
   200  	}
   201  
   202  	for _, cName := range o.ComponentNames {
   203  		for _, vctName := range o.VCTNames {
   204  			labels := fmt.Sprintf("%s=%s,%s=%s,%s=%s",
   205  				constant.AppInstanceLabelKey, o.Name,
   206  				constant.KBAppComponentLabelKey, cName,
   207  				constant.VolumeClaimTemplateNameLabelKey, vctName,
   208  			)
   209  			pvcs, err := o.Client.CoreV1().PersistentVolumeClaims(o.Namespace).List(context.Background(),
   210  				metav1.ListOptions{LabelSelector: labels, Limit: 1})
   211  			if err != nil {
   212  				return err
   213  			}
   214  			if len(pvcs.Items) == 0 {
   215  				continue
   216  			}
   217  			pvc := pvcs.Items[0]
   218  			specStorage := pvc.Spec.Resources.Requests.Storage()
   219  			statusStorage := pvc.Status.Capacity.Storage()
   220  			targetStorage, err := resource.ParseQuantity(o.Storage)
   221  			if err != nil {
   222  				return fmt.Errorf("cannot parse '%v', %v", o.Storage, err)
   223  			}
   224  			// determine whether the opsRequest is a recovery action for volume expansion failure
   225  			if specStorage.Cmp(targetStorage) > 0 &&
   226  				statusStorage.Cmp(targetStorage) <= 0 {
   227  				o.autoApprove = false
   228  				fmt.Fprintln(o.Out, printer.BoldYellow("Warning: this opsRequest is a recovery action for volume expansion failure and will re-create the PersistentVolumeClaims when RECOVER_VOLUME_EXPANSION_FAILURE=false"))
   229  				break
   230  			}
   231  		}
   232  	}
   233  	return nil
   234  }
   235  
   236  func (o *OperationsOptions) validateVScale(cluster *appsv1alpha1.Cluster) error {
   237  	if o.Class != "" && (o.CPU != "" || o.Memory != "") {
   238  		return fmt.Errorf("class and cpu/memory cannot be both specified")
   239  	}
   240  	if o.Class == "" && o.CPU == "" && o.Memory == "" {
   241  		return fmt.Errorf("class or cpu/memory must be specified")
   242  	}
   243  
   244  	clsMgr, err := classutil.GetManager(o.Dynamic, cluster.Spec.ClusterDefRef)
   245  	if err != nil {
   246  		return err
   247  	}
   248  
   249  	fillClassParams := func(comp *appsv1alpha1.ClusterComponentSpec) error {
   250  		if o.Class != "" {
   251  			clsDefRef := appsv1alpha1.ClassDefRef{}
   252  			parts := strings.SplitN(o.Class, ":", 2)
   253  			if len(parts) == 1 {
   254  				clsDefRef.Class = parts[0]
   255  			} else {
   256  				clsDefRef.Name = parts[0]
   257  				clsDefRef.Class = parts[1]
   258  			}
   259  			comp.ClassDefRef = &clsDefRef
   260  			comp.Resources = corev1.ResourceRequirements{}
   261  		} else {
   262  			comp.ClassDefRef = &appsv1alpha1.ClassDefRef{}
   263  			requests := make(corev1.ResourceList)
   264  			if o.CPU != "" {
   265  				cpu, err := resource.ParseQuantity(o.CPU)
   266  				if err != nil {
   267  					return fmt.Errorf("cannot parse '%v', %v", o.CPU, err)
   268  				}
   269  				requests[corev1.ResourceCPU] = cpu
   270  			}
   271  			if o.Memory != "" {
   272  				memory, err := resource.ParseQuantity(o.Memory)
   273  				if err != nil {
   274  					return fmt.Errorf("cannot parse '%v', %v", o.Memory, err)
   275  				}
   276  				requests[corev1.ResourceMemory] = memory
   277  			}
   278  			requests.DeepCopyInto(&comp.Resources.Requests)
   279  			requests.DeepCopyInto(&comp.Resources.Limits)
   280  		}
   281  		return nil
   282  	}
   283  
   284  	for _, name := range o.ComponentNames {
   285  		for _, comp := range cluster.Spec.ComponentSpecs {
   286  			if comp.Name != name {
   287  				continue
   288  			}
   289  			if err = fillClassParams(&comp); err != nil {
   290  				return err
   291  			}
   292  			if err = clsMgr.ValidateResources(cluster.Spec.ClusterDefRef, &comp); err != nil {
   293  				return err
   294  			}
   295  		}
   296  	}
   297  
   298  	return nil
   299  }
   300  
   301  // Validate command flags or args is legal
   302  func (o *OperationsOptions) Validate() error {
   303  	if o.Name == "" {
   304  		return makeMissingClusterNameErr()
   305  	}
   306  
   307  	// check if cluster exist
   308  	obj, err := o.Dynamic.Resource(types.ClusterGVR()).Namespace(o.Namespace).Get(context.TODO(), o.Name, metav1.GetOptions{})
   309  	if err != nil {
   310  		return err
   311  	}
   312  	var cluster appsv1alpha1.Cluster
   313  	if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &cluster); err != nil {
   314  		return err
   315  	}
   316  
   317  	// common validate for componentOps
   318  	if o.HasComponentNamesFlag && len(o.ComponentNames) == 0 {
   319  		return fmt.Errorf(`missing components, please specify the "--components" flag for multi-components cluster`)
   320  	}
   321  
   322  	switch o.OpsType {
   323  	case appsv1alpha1.VolumeExpansionType:
   324  		if err = o.validateVolumeExpansion(); err != nil {
   325  			return err
   326  		}
   327  	case appsv1alpha1.UpgradeType:
   328  		if err = o.validateUpgrade(); err != nil {
   329  			return err
   330  		}
   331  	case appsv1alpha1.VerticalScalingType:
   332  		if err = o.validateVScale(&cluster); err != nil {
   333  			return err
   334  		}
   335  	case appsv1alpha1.ExposeType:
   336  		if err = o.validateExpose(); err != nil {
   337  			return err
   338  		}
   339  	case appsv1alpha1.SwitchoverType:
   340  		if err = o.validatePromote(&cluster); err != nil {
   341  			return err
   342  		}
   343  	}
   344  	if !o.autoApprove && o.DryRun == "none" {
   345  		return prompt.Confirm([]string{o.Name}, o.In, "", "")
   346  	}
   347  	return nil
   348  }
   349  
   350  func (o *OperationsOptions) validatePromote(cluster *appsv1alpha1.Cluster) error {
   351  	var (
   352  		clusterDefObj = appsv1alpha1.ClusterDefinition{}
   353  		podObj        = &corev1.Pod{}
   354  		componentName string
   355  	)
   356  
   357  	if len(cluster.Spec.ComponentSpecs) == 0 {
   358  		return fmt.Errorf("cluster.Spec.ComponentSpecs cannot be empty")
   359  	}
   360  
   361  	if o.Component != "" {
   362  		componentName = o.Component
   363  	} else {
   364  		if len(cluster.Spec.ComponentSpecs) > 1 {
   365  			return fmt.Errorf("there are multiple components in cluster, please use --component to specify the component for promote")
   366  		}
   367  		componentName = cluster.Spec.ComponentSpecs[0].Name
   368  	}
   369  
   370  	if o.Instance != "" {
   371  		// checks the validity of the instance whether it belongs to the current component and ensure it is not the primary or leader instance currently.
   372  		podKey := client.ObjectKey{
   373  			Namespace: cluster.Namespace,
   374  			Name:      o.Instance,
   375  		}
   376  		if err := util.GetResourceObjectFromGVR(types.PodGVR(), podKey, o.Dynamic, podObj); err != nil || podObj == nil {
   377  			return fmt.Errorf("instance %s not found, please check the validity of the instance using \"kbcli cluster list-instances\"", o.Instance)
   378  		}
   379  		v, ok := podObj.Labels[constant.RoleLabelKey]
   380  		if !ok || v == "" {
   381  			return fmt.Errorf("instance %s cannot be promoted because it had a invalid role label", o.Instance)
   382  		}
   383  		if v == constant.Primary || v == constant.Leader {
   384  			return fmt.Errorf("instance %s cannot be promoted because it is already the primary or leader instance", o.Instance)
   385  		}
   386  		if !strings.HasPrefix(podObj.Name, fmt.Sprintf("%s-%s", cluster.Name, componentName)) {
   387  			return fmt.Errorf("instance %s does not belong to the current component, please check the validity of the instance using \"kbcli cluster list-instances\"", o.Instance)
   388  		}
   389  	}
   390  
   391  	// check clusterDefinition switchoverSpec exist
   392  	clusterDefKey := client.ObjectKey{
   393  		Namespace: "",
   394  		Name:      cluster.Spec.ClusterDefRef,
   395  	}
   396  	if err := util.GetResourceObjectFromGVR(types.ClusterDefGVR(), clusterDefKey, o.Dynamic, &clusterDefObj); err != nil {
   397  		return err
   398  	}
   399  	var compDefObj *appsv1alpha1.ClusterComponentDefinition
   400  	for _, compDef := range clusterDefObj.Spec.ComponentDefs {
   401  		if compDef.Name == cluster.Spec.GetComponentDefRefName(componentName) {
   402  			compDefObj = &compDef
   403  			break
   404  		}
   405  	}
   406  	if compDefObj == nil {
   407  		return fmt.Errorf("cluster component %s is invalid", componentName)
   408  	}
   409  	if compDefObj.SwitchoverSpec == nil {
   410  		return fmt.Errorf("cluster component %s does not support switchover", componentName)
   411  	}
   412  	switch o.Instance {
   413  	case "":
   414  		if compDefObj.SwitchoverSpec.WithoutCandidate == nil {
   415  			return fmt.Errorf("cluster component %s does not support promote without specifying an instance. Please specify a specific instance for the promotion", componentName)
   416  		}
   417  	default:
   418  		if compDefObj.SwitchoverSpec.WithCandidate == nil {
   419  			return fmt.Errorf("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", componentName)
   420  		}
   421  	}
   422  	return nil
   423  }
   424  
   425  func (o *OperationsOptions) validateExpose() error {
   426  	switch util.ExposeType(o.ExposeType) {
   427  	case "", util.ExposeToVPC, util.ExposeToInternet:
   428  	default:
   429  		return fmt.Errorf("invalid expose type %q", o.ExposeType)
   430  	}
   431  
   432  	switch strings.ToLower(o.ExposeEnabled) {
   433  	case util.EnableValue, util.DisableValue:
   434  	default:
   435  		return fmt.Errorf("invalid value for enable flag: %s", o.ExposeEnabled)
   436  	}
   437  	return nil
   438  }
   439  
   440  func (o *OperationsOptions) fillExpose() error {
   441  	version, err := util.GetK8sVersion(o.Client.Discovery())
   442  	if err != nil {
   443  		return err
   444  	}
   445  	provider, err := util.GetK8sProvider(version, o.Client)
   446  	if err != nil {
   447  		return err
   448  	}
   449  	if provider == util.UnknownProvider {
   450  		return fmt.Errorf("unknown k8s provider")
   451  	}
   452  
   453  	// default expose to internet
   454  	exposeType := util.ExposeType(o.ExposeType)
   455  	if exposeType == "" {
   456  		exposeType = util.ExposeToInternet
   457  	}
   458  
   459  	annotations, err := util.GetExposeAnnotations(provider, exposeType)
   460  	if err != nil {
   461  		return err
   462  	}
   463  
   464  	gvr := schema.GroupVersionResource{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion, Resource: types.ResourceClusters}
   465  	unstructuredObj, err := o.Dynamic.Resource(gvr).Namespace(o.Namespace).Get(context.TODO(), o.Name, metav1.GetOptions{})
   466  	if err != nil {
   467  		return err
   468  	}
   469  	cluster := appsv1alpha1.Cluster{}
   470  	if err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.UnstructuredContent(), &cluster); err != nil {
   471  		return err
   472  	}
   473  
   474  	compMap := make(map[string]appsv1alpha1.ClusterComponentSpec)
   475  	for _, compSpec := range cluster.Spec.ComponentSpecs {
   476  		compMap[compSpec.Name] = compSpec
   477  	}
   478  
   479  	var (
   480  		// currently, we use the expose type as service name
   481  		svcName = string(exposeType)
   482  		enabled = strings.ToLower(o.ExposeEnabled) == util.EnableValue
   483  	)
   484  	for _, name := range o.ComponentNames {
   485  		comp, ok := compMap[name]
   486  		if !ok {
   487  			return fmt.Errorf("component %s not found", name)
   488  		}
   489  
   490  		for _, svc := range comp.Services {
   491  			if svc.Name != svcName {
   492  				o.Services = append(o.Services, svc)
   493  			}
   494  		}
   495  
   496  		if enabled {
   497  			o.Services = append(o.Services, appsv1alpha1.ClusterComponentService{
   498  				Name:        svcName,
   499  				ServiceType: corev1.ServiceTypeLoadBalancer,
   500  				Annotations: annotations,
   501  			})
   502  		}
   503  	}
   504  	return nil
   505  }
   506  
   507  var restartExample = templates.Examples(`
   508  		# restart all components
   509  		kbcli cluster restart mycluster
   510  
   511  		# specified component to restart, separate with commas for multiple components
   512  		kbcli cluster restart mycluster --components=mysql
   513  `)
   514  
   515  // NewRestartCmd creates a restart command
   516  func NewRestartCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   517  	o := newBaseOperationsOptions(f, streams, appsv1alpha1.RestartType, true)
   518  	cmd := &cobra.Command{
   519  		Use:               "restart NAME",
   520  		Short:             "Restart the specified components in the cluster.",
   521  		Example:           restartExample,
   522  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   523  		Run: func(cmd *cobra.Command, args []string) {
   524  			o.Args = args
   525  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   526  			cmdutil.CheckErr(o.Complete())
   527  			cmdutil.CheckErr(o.CompleteRestartOps())
   528  			cmdutil.CheckErr(o.Validate())
   529  			cmdutil.CheckErr(o.Run())
   530  		},
   531  	}
   532  	o.addCommonFlags(cmd, f)
   533  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before restarting the cluster")
   534  	return cmd
   535  }
   536  
   537  var upgradeExample = templates.Examples(`
   538  		# upgrade the cluster to the target version 
   539  		kbcli cluster upgrade mycluster --cluster-version=ac-mysql-8.0.30
   540  `)
   541  
   542  // NewUpgradeCmd creates an upgrade command
   543  func NewUpgradeCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   544  	o := newBaseOperationsOptions(f, streams, appsv1alpha1.UpgradeType, false)
   545  	cmd := &cobra.Command{
   546  		Use:               "upgrade NAME",
   547  		Short:             "Upgrade the cluster version.",
   548  		Example:           upgradeExample,
   549  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   550  		Run: func(cmd *cobra.Command, args []string) {
   551  			o.Args = args
   552  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   553  			cmdutil.CheckErr(o.Complete())
   554  			cmdutil.CheckErr(o.Validate())
   555  			cmdutil.CheckErr(o.Run())
   556  		},
   557  	}
   558  	o.addCommonFlags(cmd, f)
   559  	cmd.Flags().StringVar(&o.ClusterVersionRef, "cluster-version", "", "Reference cluster version (required)")
   560  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before upgrading the cluster")
   561  	_ = cmd.MarkFlagRequired("cluster-version")
   562  	return cmd
   563  }
   564  
   565  var verticalScalingExample = templates.Examples(`
   566  		# scale the computing resources of specified components, separate with commas for multiple components
   567  		kbcli cluster vscale mycluster --components=mysql --cpu=500m --memory=500Mi 
   568  
   569  		# scale the computing resources of specified components by class, run command 'kbcli class list --cluster-definition cluster-definition-name' to get available classes
   570  		kbcli cluster vscale mycluster --components=mysql --class=general-2c4g
   571  `)
   572  
   573  // NewVerticalScalingCmd creates a vertical scaling command
   574  func NewVerticalScalingCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   575  	o := newBaseOperationsOptions(f, streams, appsv1alpha1.VerticalScalingType, true)
   576  	cmd := &cobra.Command{
   577  		Use:               "vscale NAME",
   578  		Short:             "Vertically scale the specified components in the cluster.",
   579  		Example:           verticalScalingExample,
   580  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   581  		Run: func(cmd *cobra.Command, args []string) {
   582  			o.Args = args
   583  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   584  			cmdutil.CheckErr(o.Complete())
   585  			cmdutil.CheckErr(o.CompleteComponentsFlag())
   586  			cmdutil.CheckErr(o.Validate())
   587  			cmdutil.CheckErr(o.Run())
   588  		},
   589  	}
   590  	o.addCommonFlags(cmd, f)
   591  	cmd.Flags().StringVar(&o.CPU, "cpu", "", "Request and limit size of component cpu")
   592  	cmd.Flags().StringVar(&o.Memory, "memory", "", "Request and limit size of component memory")
   593  	cmd.Flags().StringVar(&o.Class, "class", "", "Component class")
   594  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before vertically scaling the cluster")
   595  	_ = cmd.MarkFlagRequired("components")
   596  	return cmd
   597  }
   598  
   599  var horizontalScalingExample = templates.Examples(`
   600  		# expand storage resources of specified components, separate with commas for multiple components
   601  		kbcli cluster hscale mycluster --components=mysql --replicas=3
   602  `)
   603  
   604  // NewHorizontalScalingCmd creates a horizontal scaling command
   605  func NewHorizontalScalingCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   606  	o := newBaseOperationsOptions(f, streams, appsv1alpha1.HorizontalScalingType, true)
   607  	cmd := &cobra.Command{
   608  		Use:               "hscale NAME",
   609  		Short:             "Horizontally scale the specified components in the cluster.",
   610  		Example:           horizontalScalingExample,
   611  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   612  		Run: func(cmd *cobra.Command, args []string) {
   613  			o.Args = args
   614  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   615  			cmdutil.CheckErr(o.Complete())
   616  			cmdutil.CheckErr(o.CompleteComponentsFlag())
   617  			cmdutil.CheckErr(o.Validate())
   618  			cmdutil.CheckErr(o.Run())
   619  		},
   620  	}
   621  
   622  	o.addCommonFlags(cmd, f)
   623  	cmd.Flags().IntVar(&o.Replicas, "replicas", o.Replicas, "Replicas with the specified components")
   624  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before horizontally scaling the cluster")
   625  	_ = cmd.MarkFlagRequired("replicas")
   626  	_ = cmd.MarkFlagRequired("components")
   627  	return cmd
   628  }
   629  
   630  var volumeExpansionExample = templates.Examples(`
   631  		# restart specifies the component, separate with commas for multiple components
   632  		kbcli cluster volume-expand mycluster --components=mysql --volume-claim-templates=data --storage=10Gi
   633  `)
   634  
   635  // NewVolumeExpansionCmd creates a volume expanding command
   636  func NewVolumeExpansionCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   637  	o := newBaseOperationsOptions(f, streams, appsv1alpha1.VolumeExpansionType, true)
   638  	cmd := &cobra.Command{
   639  		Use:               "volume-expand NAME",
   640  		Short:             "Expand volume with the specified components and volumeClaimTemplates in the cluster.",
   641  		Example:           volumeExpansionExample,
   642  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   643  		Run: func(cmd *cobra.Command, args []string) {
   644  			o.Args = args
   645  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   646  			cmdutil.CheckErr(o.Complete())
   647  			cmdutil.CheckErr(o.CompleteComponentsFlag())
   648  			cmdutil.CheckErr(o.Validate())
   649  			cmdutil.CheckErr(o.Run())
   650  		},
   651  	}
   652  	o.addCommonFlags(cmd, f)
   653  	cmd.Flags().StringSliceVarP(&o.VCTNames, "volume-claim-templates", "t", nil, "VolumeClaimTemplate names in components (required)")
   654  	cmd.Flags().StringVar(&o.Storage, "storage", "", "Volume storage size (required)")
   655  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before expanding the cluster volume")
   656  	_ = cmd.MarkFlagRequired("volume-claim-templates")
   657  	_ = cmd.MarkFlagRequired("storage")
   658  	_ = cmd.MarkFlagRequired("components")
   659  	return cmd
   660  }
   661  
   662  var (
   663  	exposeExamples = templates.Examples(`
   664  		# Expose a cluster to vpc
   665  		kbcli cluster expose mycluster --type vpc --enable=true
   666  
   667  		# Expose a cluster to public internet
   668  		kbcli cluster expose mycluster --type internet --enable=true
   669  		
   670  		# Stop exposing a cluster
   671  		kbcli cluster expose mycluster --type vpc --enable=false
   672  	`)
   673  )
   674  
   675  // NewExposeCmd creates an expose command
   676  func NewExposeCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   677  	o := newBaseOperationsOptions(f, streams, appsv1alpha1.ExposeType, true)
   678  	cmd := &cobra.Command{
   679  		Use:               "expose NAME --enable=[true|false] --type=[vpc|internet]",
   680  		Short:             "Expose a cluster with a new endpoint, the new endpoint can be found by executing 'kbcli cluster describe NAME'.",
   681  		Example:           exposeExamples,
   682  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   683  		Run: func(cmd *cobra.Command, args []string) {
   684  			o.Args = args
   685  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   686  			cmdutil.CheckErr(o.Complete())
   687  			cmdutil.CheckErr(o.CompleteComponentsFlag())
   688  			cmdutil.CheckErr(o.fillExpose())
   689  			cmdutil.CheckErr(o.Validate())
   690  			cmdutil.CheckErr(o.Run())
   691  		},
   692  	}
   693  	o.addCommonFlags(cmd, f)
   694  	cmd.Flags().StringVar(&o.ExposeType, "type", "", "Expose type, currently supported types are 'vpc', 'internet'")
   695  	cmd.Flags().StringVar(&o.ExposeEnabled, "enable", "", "Enable or disable the expose, values can be true or false")
   696  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before exposing the cluster")
   697  
   698  	util.CheckErr(cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   699  		return []string{string(util.ExposeToVPC), string(util.ExposeToInternet)}, cobra.ShellCompDirectiveNoFileComp
   700  	}))
   701  	util.CheckErr(cmd.RegisterFlagCompletionFunc("enable", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   702  		return []string{"true", "false"}, cobra.ShellCompDirectiveNoFileComp
   703  	}))
   704  
   705  	_ = cmd.MarkFlagRequired("enable")
   706  	return cmd
   707  }
   708  
   709  var stopExample = templates.Examples(`
   710  		# stop the cluster and release all the pods of the cluster
   711  		kbcli cluster stop mycluster
   712  `)
   713  
   714  // NewStopCmd creates a stop command
   715  func NewStopCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   716  	o := newBaseOperationsOptions(f, streams, appsv1alpha1.StopType, false)
   717  	cmd := &cobra.Command{
   718  		Use:               "stop NAME",
   719  		Short:             "Stop the cluster and release all the pods of the cluster.",
   720  		Example:           stopExample,
   721  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   722  		Run: func(cmd *cobra.Command, args []string) {
   723  			o.Args = args
   724  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   725  			cmdutil.CheckErr(o.Complete())
   726  			cmdutil.CheckErr(o.Validate())
   727  			cmdutil.CheckErr(o.Run())
   728  		},
   729  	}
   730  	o.addCommonFlags(cmd, f)
   731  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before stopping the cluster")
   732  	return cmd
   733  }
   734  
   735  var startExample = templates.Examples(`
   736  		# start the cluster when cluster is stopped
   737  		kbcli cluster start mycluster
   738  `)
   739  
   740  // NewStartCmd creates a start command
   741  func NewStartCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   742  	o := newBaseOperationsOptions(f, streams, appsv1alpha1.StartType, false)
   743  	o.autoApprove = true
   744  	cmd := &cobra.Command{
   745  		Use:               "start NAME",
   746  		Short:             "Start the cluster if cluster is stopped.",
   747  		Example:           startExample,
   748  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   749  		Run: func(cmd *cobra.Command, args []string) {
   750  			o.Args = args
   751  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   752  			cmdutil.CheckErr(o.Complete())
   753  			cmdutil.CheckErr(o.Validate())
   754  			cmdutil.CheckErr(o.Run())
   755  		},
   756  	}
   757  	o.addCommonFlags(cmd, f)
   758  	return cmd
   759  }
   760  
   761  var cancelExample = templates.Examples(`
   762  		# cancel the opsRequest which is not completed.
   763  		kbcli cluster cancel-ops <opsRequestName>
   764  `)
   765  
   766  func cancelOps(o *OperationsOptions) error {
   767  	opsRequest := &appsv1alpha1.OpsRequest{}
   768  	if err := cluster.GetK8SClientObject(o.Dynamic, opsRequest, o.GVR, o.Namespace, o.Name); err != nil {
   769  		return err
   770  	}
   771  	notSupportedPhases := []appsv1alpha1.OpsPhase{appsv1alpha1.OpsFailedPhase, appsv1alpha1.OpsSucceedPhase, appsv1alpha1.OpsCancelledPhase}
   772  	if slices.Contains(notSupportedPhases, opsRequest.Status.Phase) {
   773  		return fmt.Errorf("can not cancel the opsRequest when phase is %s", opsRequest.Status.Phase)
   774  	}
   775  	if opsRequest.Status.Phase == appsv1alpha1.OpsCancellingPhase {
   776  		return fmt.Errorf(`opsRequest "%s" is cancelling`, opsRequest.Name)
   777  	}
   778  	supportedType := []appsv1alpha1.OpsType{appsv1alpha1.HorizontalScalingType, appsv1alpha1.VerticalScalingType}
   779  	if !slices.Contains(supportedType, opsRequest.Spec.Type) {
   780  		return fmt.Errorf("opsRequest type: %s not support cancel action", opsRequest.Spec.Type)
   781  	}
   782  	if !o.autoApprove {
   783  		if err := prompt.Confirm([]string{o.Name}, o.In, "", ""); err != nil {
   784  			return err
   785  		}
   786  	}
   787  	oldOps := opsRequest.DeepCopy()
   788  	opsRequest.Spec.Cancel = true
   789  	oldData, err := json.Marshal(oldOps)
   790  	if err != nil {
   791  		return err
   792  	}
   793  	newData, err := json.Marshal(opsRequest)
   794  	if err != nil {
   795  		return err
   796  	}
   797  	patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData)
   798  	if err != nil {
   799  		return err
   800  	}
   801  	if _, err = o.Dynamic.Resource(types.OpsGVR()).Namespace(opsRequest.Namespace).Patch(context.TODO(),
   802  		opsRequest.Name, apitypes.MergePatchType, patchBytes, metav1.PatchOptions{}); err != nil {
   803  		return err
   804  	}
   805  	fmt.Fprintf(o.Out, "start to cancel opsRequest \"%s\", you can view the progress:\n\tkbcli cluster list-ops --name %s\n", o.Name, o.Name)
   806  	return nil
   807  }
   808  
   809  func NewCancelCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   810  	o := newBaseOperationsOptions(f, streams, "", false)
   811  	cmd := &cobra.Command{
   812  		Use:               "cancel-ops NAME",
   813  		Short:             "Cancel the pending/creating/running OpsRequest which type is vscale or hscale.",
   814  		Example:           cancelExample,
   815  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.OpsGVR()),
   816  		Run: func(cmd *cobra.Command, args []string) {
   817  			o.Args = args
   818  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   819  			cmdutil.CheckErr(o.Complete())
   820  			cmdutil.CheckErr(cancelOps(o))
   821  		},
   822  	}
   823  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before cancel the opsRequest")
   824  	return cmd
   825  }
   826  
   827  var promoteExample = templates.Examples(`
   828  		# Promote the instance mycluster-mysql-1 as the new primary or leader.
   829  		kbcli cluster promote mycluster --instance mycluster-mysql-1
   830  
   831  		# Promote a non-primary or non-leader instance as the new primary or leader, the new primary or leader is determined by the system.
   832  		kbcli cluster promote mycluster
   833  
   834  		# If the cluster has multiple components, you need to specify a component, otherwise an error will be reported.
   835  	    kbcli cluster promote mycluster --component=mysql --instance mycluster-mysql-1
   836  `)
   837  
   838  // NewPromoteCmd creates a promote command
   839  func NewPromoteCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   840  	o := newBaseOperationsOptions(f, streams, appsv1alpha1.SwitchoverType, false)
   841  	cmd := &cobra.Command{
   842  		Use:               "promote NAME [--component=<comp-name>] [--instance <instance-name>]",
   843  		Short:             "Promote a non-primary or non-leader instance as the new primary or leader of the cluster",
   844  		Example:           promoteExample,
   845  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   846  		Run: func(cmd *cobra.Command, args []string) {
   847  			o.Args = args
   848  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   849  			cmdutil.CheckErr(o.Complete())
   850  			cmdutil.CheckErr(o.CompleteComponentsFlag())
   851  			cmdutil.CheckErr(o.Validate())
   852  			cmdutil.CheckErr(o.Run())
   853  		},
   854  	}
   855  	cmd.Flags().StringVar(&o.Component, "component", "", "Specify the component name of the cluster, if the cluster has multiple components, you need to specify a component")
   856  	cmd.Flags().StringVar(&o.Instance, "instance", "", "Specify the instance name as the new primary or leader of the cluster, you can get the instance name by running \"kbcli cluster list-instances\"")
   857  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before promote the instance")
   858  	o.addCommonFlags(cmd, f)
   859  	return cmd
   860  }