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 }