github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/config_ops.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  	"fmt"
    24  	"os"
    25  	"strings"
    26  
    27  	"github.com/spf13/cobra"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	"k8s.io/cli-runtime/pkg/genericiooptions"
    30  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    31  	"k8s.io/kubectl/pkg/util/templates"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  
    34  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    35  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    36  	"github.com/1aal/kubeblocks/pkg/cli/types"
    37  	"github.com/1aal/kubeblocks/pkg/cli/util"
    38  	"github.com/1aal/kubeblocks/pkg/cli/util/flags"
    39  	"github.com/1aal/kubeblocks/pkg/cli/util/prompt"
    40  	cfgcm "github.com/1aal/kubeblocks/pkg/configuration/config_manager"
    41  	"github.com/1aal/kubeblocks/pkg/configuration/core"
    42  	"github.com/1aal/kubeblocks/pkg/controllerutil"
    43  )
    44  
    45  type configOpsOptions struct {
    46  	*OperationsOptions
    47  
    48  	editMode bool
    49  	wrapper  *configWrapper
    50  
    51  	// config file replace
    52  	replaceFile bool
    53  
    54  	// Reconfiguring options
    55  	ComponentName string
    56  	LocalFilePath string   `json:"localFilePath"`
    57  	Parameters    []string `json:"parameters"`
    58  }
    59  
    60  var (
    61  	createReconfigureExample = templates.Examples(`
    62  		# update component params 
    63  		kbcli cluster configure mycluster --component=mysql --config-spec=mysql-3node-tpl --config-file=my.cnf --set max_connections=1000,general_log=OFF
    64  
    65  		# if only one component, and one config spec, and one config file, simplify the searching process of configure. e.g:
    66  		# update mysql max_connections, cluster name is mycluster
    67  		kbcli cluster configure mycluster --set max_connections=2000
    68  	`)
    69  )
    70  
    71  func (o *configOpsOptions) Complete() error {
    72  	if o.Name == "" {
    73  		return makeMissingClusterNameErr()
    74  	}
    75  
    76  	if !o.editMode {
    77  		if err := o.validateReconfigureOptions(); err != nil {
    78  			return err
    79  		}
    80  	}
    81  
    82  	wrapper, err := newConfigWrapper(o.CreateOptions, o.Name, o.ComponentName, o.CfgTemplateName, o.CfgFile, o.KeyValues)
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	o.wrapper = wrapper
    88  	return wrapper.AutoFillRequiredParam()
    89  }
    90  
    91  func (o *configOpsOptions) validateReconfigureOptions() error {
    92  	if o.LocalFilePath != "" && o.CfgFile == "" {
    93  		return core.MakeError("config file is required when using --local-file")
    94  	}
    95  	if o.LocalFilePath != "" {
    96  		b, err := os.ReadFile(o.LocalFilePath)
    97  		if err != nil {
    98  			return err
    99  		}
   100  		o.FileContent = string(b)
   101  	} else {
   102  		kvs, err := o.parseUpdatedParams()
   103  		if err != nil {
   104  			return err
   105  		}
   106  		o.KeyValues = core.FromStringPointerMap(kvs)
   107  	}
   108  	return nil
   109  }
   110  
   111  // Validate command flags or args is legal
   112  func (o *configOpsOptions) Validate() error {
   113  	if err := o.wrapper.ValidateRequiredParam(o.replaceFile); err != nil {
   114  		return err
   115  	}
   116  
   117  	o.CfgFile = o.wrapper.ConfigFile()
   118  	o.CfgTemplateName = o.wrapper.ConfigSpecName()
   119  	o.ComponentNames = []string{o.wrapper.ComponentName()}
   120  
   121  	if o.editMode {
   122  		return nil
   123  	}
   124  	if err := o.validateConfigParams(o.wrapper.ConfigTemplateSpec()); err != nil {
   125  		return err
   126  	}
   127  	if err := util.ValidateParametersModified(o.wrapper.ConfigTemplateSpec(), sets.KeySet(o.KeyValues), o.Dynamic); err != nil {
   128  		return err
   129  	}
   130  	o.printConfigureTips()
   131  	return nil
   132  }
   133  
   134  func (o *configOpsOptions) validateConfigParams(tpl *appsv1alpha1.ComponentConfigSpec) error {
   135  	configConstraintKey := client.ObjectKey{
   136  		Namespace: "",
   137  		Name:      tpl.ConfigConstraintRef,
   138  	}
   139  	configConstraint := appsv1alpha1.ConfigConstraint{}
   140  	if err := util.GetResourceObjectFromGVR(types.ConfigConstraintGVR(), configConstraintKey, o.Dynamic, &configConstraint); err != nil {
   141  		return err
   142  	}
   143  
   144  	var err error
   145  	var newConfigData map[string]string
   146  	if o.FileContent != "" {
   147  		newConfigData = map[string]string{o.CfgFile: o.FileContent}
   148  	} else {
   149  		newConfigData, err = controllerutil.MergeAndValidateConfigs(configConstraint.Spec, map[string]string{o.CfgFile: ""}, tpl.Keys, []core.ParamPairs{{
   150  			Key:           o.CfgFile,
   151  			UpdatedParams: core.FromStringMap(o.KeyValues),
   152  		}})
   153  	}
   154  	if err != nil {
   155  		return err
   156  	}
   157  	return o.checkChangedParamsAndDoubleConfirm(&configConstraint.Spec, newConfigData, tpl)
   158  }
   159  
   160  func (o *configOpsOptions) checkChangedParamsAndDoubleConfirm(cc *appsv1alpha1.ConfigConstraintSpec, data map[string]string, tpl *appsv1alpha1.ComponentConfigSpec) error {
   161  	mockEmptyData := func(m map[string]string) map[string]string {
   162  		r := make(map[string]string, len(data))
   163  		for key := range m {
   164  			r[key] = ""
   165  		}
   166  		return r
   167  	}
   168  
   169  	if !cfgcm.IsSupportReload(cc.ReloadOptions) {
   170  		return o.confirmReconfigureWithRestart()
   171  	}
   172  
   173  	configPatch, restart, err := core.CreateConfigPatch(mockEmptyData(data), data, cc.FormatterConfig.Format, tpl.Keys, o.FileContent != "")
   174  	if err != nil {
   175  		return err
   176  	}
   177  	if restart {
   178  		return o.confirmReconfigureWithRestart()
   179  	}
   180  
   181  	dynamicUpdated, err := core.IsUpdateDynamicParameters(cc, configPatch)
   182  	if err != nil {
   183  		return nil
   184  	}
   185  	if dynamicUpdated {
   186  		return nil
   187  	}
   188  	return o.confirmReconfigureWithRestart()
   189  }
   190  
   191  func (o *configOpsOptions) confirmReconfigureWithRestart() error {
   192  	if o.autoApprove {
   193  		return nil
   194  	}
   195  	const confirmStr = "yes"
   196  	printer.Warning(o.Out, restartConfirmPrompt)
   197  	_, err := prompt.NewPrompt(fmt.Sprintf("Please type \"%s\" to confirm:", confirmStr),
   198  		func(input string) error {
   199  			if input != confirmStr {
   200  				return fmt.Errorf("typed \"%s\" not match \"%s\"", input, confirmStr)
   201  			}
   202  			return nil
   203  		}, o.In).Run()
   204  	return err
   205  }
   206  
   207  func (o *configOpsOptions) parseUpdatedParams() (map[string]string, error) {
   208  	if len(o.Parameters) == 0 && len(o.LocalFilePath) == 0 {
   209  		return nil, core.MakeError(missingUpdatedParametersErrMessage)
   210  	}
   211  
   212  	keyValues := make(map[string]string)
   213  	for _, param := range o.Parameters {
   214  		pp := strings.Split(param, ",")
   215  		for _, p := range pp {
   216  			fields := strings.SplitN(p, "=", 2)
   217  			if len(fields) != 2 {
   218  				return nil, core.MakeError("updated parameter format: key=value")
   219  			}
   220  			keyValues[fields[0]] = fields[1]
   221  		}
   222  	}
   223  	return keyValues, nil
   224  }
   225  
   226  func (o *configOpsOptions) printConfigureTips() {
   227  	fmt.Println("Will updated configure file meta:")
   228  	printer.PrintLineWithTabSeparator(
   229  		printer.NewPair("  ConfigSpec", printer.BoldYellow(o.CfgTemplateName)),
   230  		printer.NewPair("  ConfigFile", printer.BoldYellow(o.CfgFile)),
   231  		printer.NewPair("ComponentName", o.ComponentName),
   232  		printer.NewPair("ClusterName", o.Name))
   233  }
   234  
   235  // buildReconfigureCommonFlags build common flags for reconfigure command
   236  func (o *configOpsOptions) buildReconfigureCommonFlags(cmd *cobra.Command, f cmdutil.Factory) {
   237  	o.addCommonFlags(cmd, f)
   238  	cmd.Flags().StringSliceVar(&o.Parameters, "set", nil, "Specify parameters list to be updated. For more details, refer to 'kbcli cluster describe-config'.")
   239  	cmd.Flags().StringVar(&o.CfgTemplateName, "config-spec", "", "Specify the name of the configuration template to be updated (e.g. for apecloud-mysql: --config-spec=mysql-3node-tpl). "+
   240  		"For available templates and configs, refer to: 'kbcli cluster describe-config'.")
   241  	cmd.Flags().StringVar(&o.CfgFile, "config-file", "", "Specify the name of the configuration file to be updated (e.g. for mysql: --config-file=my.cnf). "+
   242  		"For available templates and configs, refer to: 'kbcli cluster describe-config'.")
   243  	flags.AddComponentFlag(f, cmd, &o.ComponentName, "Specify the name of Component to be updated. If the cluster has only one component, unset the parameter.")
   244  	cmd.Flags().BoolVar(&o.ForceRestart, "force-restart", false, "Boolean flag to restart component. Default with false.")
   245  	cmd.Flags().StringVar(&o.LocalFilePath, "local-file", "", "Specify the local configuration file to be updated.")
   246  	cmd.Flags().BoolVar(&o.replaceFile, "replace", false, "Boolean flag to enable replacing config file. Default with false.")
   247  }
   248  
   249  // NewReconfigureCmd creates a Reconfiguring command
   250  func NewReconfigureCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   251  	o := &configOpsOptions{
   252  		editMode:          false,
   253  		OperationsOptions: newBaseOperationsOptions(f, streams, appsv1alpha1.ReconfiguringType, false),
   254  	}
   255  	cmd := &cobra.Command{
   256  		Use:               "configure NAME --set key=value[,key=value] [--component=component-name] [--config-spec=config-spec-name] [--config-file=config-file]",
   257  		Short:             "Configure parameters with the specified components in the cluster.",
   258  		Example:           createReconfigureExample,
   259  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   260  		Run: func(cmd *cobra.Command, args []string) {
   261  			o.Args = args
   262  			cmdutil.BehaviorOnFatal(printer.FatalWithRedColor)
   263  			cmdutil.CheckErr(o.CreateOptions.Complete())
   264  			cmdutil.CheckErr(o.Complete())
   265  			cmdutil.CheckErr(o.Validate())
   266  			cmdutil.CheckErr(o.Run())
   267  		},
   268  	}
   269  
   270  	o.buildReconfigureCommonFlags(cmd, f)
   271  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before reconfiguring the cluster")
   272  	return cmd
   273  }