github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/config_edit.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  	"fmt"
    25  	"io"
    26  	"os"
    27  	"strings"
    28  
    29  	"github.com/spf13/cobra"
    30  	"golang.org/x/exp/slices"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	"k8s.io/cli-runtime/pkg/genericiooptions"
    33  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    34  	"k8s.io/kubectl/pkg/cmd/util/editor"
    35  	"k8s.io/kubectl/pkg/util/templates"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  
    38  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    39  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    40  	"github.com/1aal/kubeblocks/pkg/cli/types"
    41  	"github.com/1aal/kubeblocks/pkg/cli/util"
    42  	"github.com/1aal/kubeblocks/pkg/cli/util/prompt"
    43  	cfgcm "github.com/1aal/kubeblocks/pkg/configuration/config_manager"
    44  	"github.com/1aal/kubeblocks/pkg/configuration/core"
    45  	"github.com/1aal/kubeblocks/pkg/configuration/validate"
    46  )
    47  
    48  type editConfigOptions struct {
    49  	configOpsOptions
    50  
    51  	enableDelete bool
    52  }
    53  
    54  var (
    55  	editConfigUse = "edit-config NAME [--component=component-name] [--config-spec=config-spec-name] [--config-file=config-file]"
    56  
    57  	editConfigExample = templates.Examples(`
    58  		# update mysql max_connections, cluster name is mycluster
    59  		kbcli cluster edit-config mycluster
    60  	`)
    61  )
    62  
    63  func (o *editConfigOptions) Run(fn func() error) error {
    64  	wrapper := o.wrapper
    65  	cfgEditContext := newConfigContext(o.CreateOptions, o.Name, wrapper.ComponentName(), wrapper.ConfigSpecName(), wrapper.ConfigFile())
    66  	if err := cfgEditContext.prepare(); err != nil {
    67  		return err
    68  	}
    69  	reader, err := o.getReaderWrapper()
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	editor := editor.NewDefaultEditor([]string{
    75  		"KUBE_EDITOR",
    76  		"EDITOR",
    77  	})
    78  	if err := cfgEditContext.editConfig(editor, reader); err != nil {
    79  		return err
    80  	}
    81  
    82  	diff, err := util.GetUnifiedDiffString(cfgEditContext.original, cfgEditContext.edited, "Original", "Current", 3)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	if diff == "" {
    87  		fmt.Println("Edit cancelled, no changes made.")
    88  		return nil
    89  	}
    90  	util.DisplayDiffWithColor(o.IOStreams.Out, diff)
    91  
    92  	configSpec := wrapper.ConfigTemplateSpec()
    93  	if configSpec.ConfigConstraintRef != "" {
    94  		return o.runWithConfigConstraints(cfgEditContext, configSpec, fn)
    95  	}
    96  
    97  	yes, err := o.confirmReconfigure(fmt.Sprintf(fullRestartConfirmPrompt, printer.BoldRed(o.CfgFile)))
    98  	if err != nil {
    99  		return err
   100  	}
   101  	if !yes {
   102  		return nil
   103  	}
   104  
   105  	o.HasPatch = false
   106  	o.FileContent = cfgEditContext.getEdited()
   107  	return fn()
   108  }
   109  
   110  func (o *editConfigOptions) runWithConfigConstraints(cfgEditContext *configEditContext, configSpec *appsv1alpha1.ComponentConfigSpec, fn func() error) error {
   111  	oldVersion := map[string]string{
   112  		o.CfgFile: cfgEditContext.getOriginal(),
   113  	}
   114  	newVersion := map[string]string{
   115  		o.CfgFile: cfgEditContext.getEdited(),
   116  	}
   117  
   118  	configConstraintKey := client.ObjectKey{
   119  		Namespace: "",
   120  		Name:      configSpec.ConfigConstraintRef,
   121  	}
   122  	configConstraint := appsv1alpha1.ConfigConstraint{}
   123  	if err := util.GetResourceObjectFromGVR(types.ConfigConstraintGVR(), configConstraintKey, o.Dynamic, &configConstraint); err != nil {
   124  		return err
   125  	}
   126  	formatterConfig := configConstraint.Spec.FormatterConfig
   127  	if formatterConfig == nil {
   128  		return core.MakeError("config spec[%s] not support reconfiguring!", configSpec.Name)
   129  	}
   130  	configPatch, fileUpdated, err := core.CreateConfigPatch(oldVersion, newVersion, formatterConfig.Format, configSpec.Keys, true)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	if !fileUpdated && !configPatch.IsModify {
   135  		fmt.Println("No parameters changes made.")
   136  		return nil
   137  	}
   138  
   139  	fmt.Fprintf(o.Out, "Config patch(updated parameters): \n%s\n\n", string(configPatch.UpdateConfig[o.CfgFile]))
   140  	if !o.enableDelete {
   141  		if err := core.ValidateConfigPatch(configPatch, configConstraint.Spec.FormatterConfig); err != nil {
   142  			return err
   143  		}
   144  	}
   145  
   146  	params := core.GenerateVisualizedParamsList(configPatch, configConstraint.Spec.FormatterConfig, nil)
   147  	// check immutable parameters
   148  	if len(configConstraint.Spec.ImmutableParameters) > 0 {
   149  		if err = util.ValidateParametersModified2(sets.KeySet(fromKeyValuesToMap(params, o.CfgFile)), configConstraint.Spec); err != nil {
   150  			return err
   151  		}
   152  	}
   153  
   154  	confirmPrompt, err := generateReconfiguringPrompt(fileUpdated, configPatch, &configConstraint.Spec, o.CfgFile)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	yes, err := o.confirmReconfigure(confirmPrompt)
   159  	if err != nil {
   160  		return err
   161  	}
   162  	if !yes {
   163  		return nil
   164  	}
   165  
   166  	validatedData := map[string]string{
   167  		o.CfgFile: cfgEditContext.getEdited(),
   168  	}
   169  	options := validate.WithKeySelector(configSpec.Keys)
   170  	if err = validate.NewConfigValidator(&configConstraint.Spec, options).Validate(validatedData); err != nil {
   171  		return core.WrapError(err, "failed to validate edited config")
   172  	}
   173  	o.KeyValues = fromKeyValuesToMap(params, o.CfgFile)
   174  	return fn()
   175  }
   176  
   177  func generateReconfiguringPrompt(fileUpdated bool, configPatch *core.ConfigPatchInfo, cc *appsv1alpha1.ConfigConstraintSpec, fileName string) (string, error) {
   178  	if fileUpdated {
   179  		return restartConfirmPrompt, nil
   180  	}
   181  
   182  	dynamicUpdated, err := core.IsUpdateDynamicParameters(cc, configPatch)
   183  	if err != nil {
   184  		return "", nil
   185  	}
   186  
   187  	confirmPrompt := confirmApplyReconfigurePrompt
   188  	if !dynamicUpdated || !cfgcm.IsSupportReload(cc.ReloadOptions) {
   189  		confirmPrompt = restartConfirmPrompt
   190  	}
   191  	return confirmPrompt, nil
   192  }
   193  
   194  func (o *editConfigOptions) confirmReconfigure(promptStr string) (bool, error) {
   195  	const yesStr = "yes"
   196  	const noStr = "no"
   197  
   198  	confirmStr := []string{yesStr, noStr}
   199  	printer.Warning(o.Out, promptStr)
   200  	input, err := prompt.NewPrompt("Please type [Yes/No] to confirm:",
   201  		func(input string) error {
   202  			if !slices.Contains(confirmStr, strings.ToLower(input)) {
   203  				return fmt.Errorf("typed \"%s\" does not match \"%s\"", input, confirmStr)
   204  			}
   205  			return nil
   206  		}, o.In).Run()
   207  	if err != nil {
   208  		return false, err
   209  	}
   210  	return strings.ToLower(input) == yesStr, nil
   211  }
   212  
   213  func (o *editConfigOptions) getReaderWrapper() (io.Reader, error) {
   214  	var reader io.Reader
   215  	if o.replaceFile && o.LocalFilePath != "" {
   216  		b, err := os.ReadFile(o.LocalFilePath)
   217  		if err != nil {
   218  			return nil, err
   219  		}
   220  		reader = bytes.NewReader(b)
   221  	}
   222  	return reader, nil
   223  }
   224  
   225  // NewEditConfigureCmd shows the difference between two configuration version.
   226  func NewEditConfigureCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   227  	o := &editConfigOptions{
   228  		configOpsOptions: configOpsOptions{
   229  			editMode:          true,
   230  			OperationsOptions: newBaseOperationsOptions(f, streams, appsv1alpha1.ReconfiguringType, false),
   231  		}}
   232  
   233  	cmd := &cobra.Command{
   234  		Use:               editConfigUse,
   235  		Short:             "Edit the config file of the component.",
   236  		Example:           editConfigExample,
   237  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   238  		Run: func(cmd *cobra.Command, args []string) {
   239  			o.Args = args
   240  			cmdutil.CheckErr(o.CreateOptions.Complete())
   241  			util.CheckErr(o.Complete())
   242  			util.CheckErr(o.Validate())
   243  			util.CheckErr(o.Run(o.CreateOptions.Run))
   244  		},
   245  	}
   246  	o.buildReconfigureCommonFlags(cmd, f)
   247  	cmd.Flags().BoolVar(&o.enableDelete, "enable-delete", false, "Boolean flag to enable delete configuration. Default with false.")
   248  	return cmd
   249  }