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 }