github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/config_observer.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 "encoding/json" 25 "fmt" 26 "sort" 27 "strings" 28 29 "github.com/spf13/cobra" 30 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/cli-runtime/pkg/genericiooptions" 34 cmdutil "k8s.io/kubectl/pkg/cmd/util" 35 "k8s.io/kubectl/pkg/util/templates" 36 37 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 38 "github.com/1aal/kubeblocks/pkg/cli/printer" 39 "github.com/1aal/kubeblocks/pkg/cli/types" 40 "github.com/1aal/kubeblocks/pkg/cli/util" 41 "github.com/1aal/kubeblocks/pkg/cli/util/flags" 42 cfgcore "github.com/1aal/kubeblocks/pkg/configuration/core" 43 "github.com/1aal/kubeblocks/pkg/configuration/openapi" 44 cfgutil "github.com/1aal/kubeblocks/pkg/configuration/util" 45 "github.com/1aal/kubeblocks/pkg/constant" 46 ) 47 48 type configObserverOptions struct { 49 *describeOpsOptions 50 51 clusterName string 52 componentNames []string 53 configSpecs []string 54 55 isExplain bool 56 truncEnum bool 57 truncDocument bool 58 paramName string 59 60 keys []string 61 showDetail bool 62 } 63 64 var ( 65 describeReconfigureExample = templates.Examples(` 66 # describe a cluster, e.g. cluster name is mycluster 67 kbcli cluster describe-config mycluster 68 69 # describe a component, e.g. cluster name is mycluster, component name is mysql 70 kbcli cluster describe-config mycluster --component=mysql 71 72 # describe all configuration files. 73 kbcli cluster describe-config mycluster --component=mysql --show-detail 74 75 # describe a content of configuration file. 76 kbcli cluster describe-config mycluster --component=mysql --config-file=my.cnf --show-detail`) 77 explainReconfigureExample = templates.Examples(` 78 # explain a cluster, e.g. cluster name is mycluster 79 kbcli cluster explain-config mycluster 80 81 # explain a specified configure template, e.g. cluster name is mycluster 82 kbcli cluster explain-config mycluster --component=mysql --config-specs=mysql-3node-tpl 83 84 # explain a specified configure template, e.g. cluster name is mycluster 85 kbcli cluster explain-config mycluster --component=mysql --config-specs=mysql-3node-tpl --trunc-document=false --trunc-enum=false 86 87 # explain a specified parameters, e.g. cluster name is mycluster 88 kbcli cluster explain-config mycluster --param=sql_mode`) 89 ) 90 91 func (r *configObserverOptions) addCommonFlags(cmd *cobra.Command, f cmdutil.Factory) { 92 cmd.Flags().StringSliceVar(&r.configSpecs, "config-specs", nil, "Specify the name of the configuration template to describe. (e.g. for apecloud-mysql: --config-specs=mysql-3node-tpl)") 93 flags.AddComponentsFlag(f, cmd, &r.componentNames, "Specify the name of Component to describe (e.g. for apecloud-mysql: --component=mysql). If the cluster has only one component, unset the parameter.\"") 94 } 95 96 func (r *configObserverOptions) complete2(args []string) error { 97 if len(args) == 0 { 98 return makeMissingClusterNameErr() 99 } 100 r.clusterName = args[0] 101 return r.complete(args) 102 } 103 104 func (r *configObserverOptions) run(printFn func(objects *ConfigRelatedObjects, component string) error) error { 105 objects, err := New(r.clusterName, r.namespace, r.dynamic, r.componentNames...).GetObjects() 106 if err != nil { 107 return err 108 } 109 110 components := r.componentNames 111 if len(components) == 0 { 112 components = getComponentNames(objects.Cluster) 113 } 114 115 for _, component := range components { 116 fmt.Fprintf(r.Out, "component: %s\n", component) 117 if _, ok := objects.ConfigSpecs[component]; !ok { 118 fmt.Fprintf(r.Out, "not found component: %s and pass\n\n", component) 119 } 120 if err := printFn(objects, component); err != nil { 121 return err 122 } 123 } 124 return nil 125 } 126 127 func (r *configObserverOptions) printComponentConfigSpecsDescribe(objects *ConfigRelatedObjects, component string) error { 128 configSpecs, ok := objects.ConfigSpecs[component] 129 if !ok { 130 return cfgcore.MakeError("not found component: %s", component) 131 } 132 configs, err := r.getReconfigureMeta(configSpecs) 133 if err != nil { 134 return err 135 } 136 if r.showDetail { 137 r.printConfigureContext(configs, component) 138 } 139 printer.PrintComponentConfigMeta(configs, r.clusterName, component, r.Out) 140 return r.printConfigureHistory(component) 141 } 142 143 func (r *configObserverOptions) printComponentExplainConfigure(objects *ConfigRelatedObjects, component string) error { 144 configSpecs := r.configSpecs 145 if len(configSpecs) == 0 { 146 configSpecs = objects.ConfigSpecs[component].listConfigSpecs(true) 147 } 148 for _, templateName := range configSpecs { 149 fmt.Println("template meta:") 150 printer.PrintLineWithTabSeparator( 151 printer.NewPair(" ConfigSpec", templateName), 152 printer.NewPair("ComponentName", component), 153 printer.NewPair("ClusterName", r.clusterName), 154 ) 155 if err := r.printExplainConfigure(objects.ConfigSpecs[component], templateName); err != nil { 156 return err 157 } 158 } 159 return nil 160 } 161 162 func (r *configObserverOptions) printExplainConfigure(configSpecs configSpecsType, tplName string) error { 163 tpl := configSpecs.findByName(tplName) 164 if tpl == nil { 165 return nil 166 } 167 168 confSpec := tpl.ConfigConstraint.Spec 169 if confSpec.ConfigurationSchema == nil { 170 fmt.Printf("\n%s\n", fmt.Sprintf(notConfigSchemaPrompt, printer.BoldYellow(tplName))) 171 return nil 172 } 173 174 schema := confSpec.ConfigurationSchema.DeepCopy() 175 if schema.Schema == nil { 176 if schema.CUE == "" { 177 fmt.Printf("\n%s\n", fmt.Sprintf(notConfigSchemaPrompt, printer.BoldYellow(tplName))) 178 return nil 179 } 180 apiSchema, err := openapi.GenerateOpenAPISchema(schema.CUE, confSpec.CfgSchemaTopLevelName) 181 if err != nil { 182 return cfgcore.WrapError(err, "failed to generate open api schema") 183 } 184 if apiSchema == nil { 185 fmt.Printf("\n%s\n", cue2openAPISchemaFailedPrompt) 186 return nil 187 } 188 schema.Schema = apiSchema 189 } 190 return r.printConfigConstraint(schema.Schema, cfgutil.NewSet(confSpec.StaticParameters...), cfgutil.NewSet(confSpec.DynamicParameters...)) 191 } 192 193 func (r *configObserverOptions) getReconfigureMeta(configSpecs configSpecsType) ([]types.ConfigTemplateInfo, error) { 194 configs := make([]types.ConfigTemplateInfo, 0) 195 configList := r.configSpecs 196 if len(configList) == 0 { 197 configList = configSpecs.listConfigSpecs(false) 198 } 199 for _, tplName := range configList { 200 tpl := configSpecs.findByName(tplName) 201 if tpl == nil || tpl.ConfigSpec == nil { 202 fmt.Fprintf(r.Out, "not found config spec: %s, and pass\n", tplName) 203 continue 204 } 205 if tpl.ConfigSpec == nil { 206 fmt.Fprintf(r.Out, "current configSpec[%s] not support reconfiguring and pass\n", tplName) 207 continue 208 } 209 configs = append(configs, types.ConfigTemplateInfo{ 210 Name: tplName, 211 TPL: *tpl.ConfigSpec, 212 CMObj: tpl.ConfigMap, 213 }) 214 } 215 return configs, nil 216 } 217 218 func (r *configObserverOptions) printConfigureContext(configs []types.ConfigTemplateInfo, component string) { 219 printer.PrintTitle("Configures Context[${component-name}/${config-spec}/${file-name}]") 220 221 keys := cfgutil.NewSet(r.keys...) 222 for _, info := range configs { 223 for key, context := range info.CMObj.Data { 224 if keys.Length() != 0 && !keys.InArray(key) { 225 continue 226 } 227 fmt.Fprintf(r.Out, "%s%s\n", 228 printer.BoldYellow(fmt.Sprintf("%s/%s/%s:\n", component, info.Name, key)), context) 229 } 230 } 231 } 232 233 func (r *configObserverOptions) printConfigureHistory(component string) error { 234 printer.PrintTitle("History modifications") 235 236 // filter reconfigure 237 // kubernetes not support fieldSelector with CRD: https://github.com/kubernetes/kubernetes/issues/51046 238 listOptions := metav1.ListOptions{ 239 LabelSelector: strings.Join([]string{constant.AppInstanceLabelKey, r.clusterName}, "="), 240 } 241 242 opsList, err := r.dynamic.Resource(types.OpsGVR()).Namespace(r.namespace).List(context.TODO(), listOptions) 243 if err != nil { 244 return err 245 } 246 // sort the unstructured objects with the creationTimestamp in positive order 247 sort.Sort(unstructuredList(opsList.Items)) 248 tbl := printer.NewTablePrinter(r.Out) 249 tbl.SetHeader("OPS-NAME", "CLUSTER", "COMPONENT", "CONFIG-SPEC-NAME", "FILE", "STATUS", "POLICY", "PROGRESS", "CREATED-TIME", "VALID-UPDATED") 250 for _, obj := range opsList.Items { 251 ops := &appsv1alpha1.OpsRequest{} 252 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, ops); err != nil { 253 return err 254 } 255 if ops.Spec.Type != appsv1alpha1.ReconfiguringType { 256 continue 257 } 258 components := getComponentNameFromOps(ops) 259 if !strings.Contains(components, component) { 260 continue 261 } 262 phase := string(ops.Status.Phase) 263 tplNames := getTemplateNameFromOps(ops.Spec) 264 keyNames := getKeyNameFromOps(ops.Spec) 265 tbl.AddRow(ops.Name, 266 ops.Spec.ClusterRef, 267 components, 268 tplNames, 269 keyNames, 270 phase, 271 getReconfigurePolicy(ops.Status), 272 ops.Status.Progress, 273 util.TimeFormat(&ops.CreationTimestamp), 274 getValidUpdatedParams(ops.Status)) 275 } 276 tbl.Print() 277 return nil 278 } 279 280 func (r *configObserverOptions) hasSpecificParam() bool { 281 return len(r.paramName) != 0 282 } 283 284 func (r *configObserverOptions) isSpecificParam(paramName string) bool { 285 return r.paramName == paramName 286 } 287 288 func (r *configObserverOptions) printConfigConstraint(schema *apiext.JSONSchemaProps, 289 staticParameters, dynamicParameters *cfgutil.Sets) error { 290 var ( 291 maxDocumentLength = 100 292 maxEnumLength = 20 293 spec = schema.Properties[openapi.DefaultSchemaName] 294 params = make([]*parameterSchema, 0) 295 ) 296 297 for key, property := range openapi.FlattenSchema(spec).Properties { 298 if property.Type == openapi.SchemaStructType { 299 continue 300 } 301 if r.hasSpecificParam() && !r.isSpecificParam(key) { 302 continue 303 } 304 305 pt, err := generateParameterSchema(key, property) 306 if err != nil { 307 return err 308 } 309 pt.scope = "Global" 310 pt.dynamic = isDynamicType(pt, staticParameters, dynamicParameters) 311 312 if r.hasSpecificParam() { 313 printSingleParameterSchema(pt) 314 return nil 315 } 316 if !r.hasSpecificParam() && r.truncDocument && len(pt.description) > maxDocumentLength { 317 pt.description = pt.description[:maxDocumentLength] + "..." 318 } 319 params = append(params, pt) 320 } 321 322 if !r.truncEnum { 323 maxEnumLength = -1 324 } 325 printConfigParameterSchema(params, r.Out, maxEnumLength) 326 return nil 327 } 328 329 func getReconfigurePolicy(status appsv1alpha1.OpsRequestStatus) string { 330 if status.ReconfiguringStatus == nil || len(status.ReconfiguringStatus.ConfigurationStatus) == 0 { 331 return "" 332 } 333 334 var policy string 335 reStatus := status.ReconfiguringStatus.ConfigurationStatus[0] 336 switch reStatus.UpdatePolicy { 337 case appsv1alpha1.AutoReload: 338 policy = "reload" 339 case appsv1alpha1.NormalPolicy, appsv1alpha1.RestartPolicy, appsv1alpha1.RollingPolicy: 340 policy = "restart" 341 default: 342 return "" 343 } 344 return printer.BoldYellow(policy) 345 } 346 347 func getValidUpdatedParams(status appsv1alpha1.OpsRequestStatus) string { 348 if status.ReconfiguringStatus == nil || len(status.ReconfiguringStatus.ConfigurationStatus) == 0 { 349 return "" 350 } 351 352 reStatus := status.ReconfiguringStatus.ConfigurationStatus[0] 353 if len(reStatus.UpdatedParameters.UpdatedKeys) == 0 { 354 return "" 355 } 356 b, err := json.Marshal(reStatus.UpdatedParameters.UpdatedKeys) 357 if err != nil { 358 return err.Error() 359 } 360 return string(b) 361 } 362 363 func isDynamicType(pt *parameterSchema, staticParameters, dynamicParameters *cfgutil.Sets) bool { 364 switch { 365 case staticParameters.InArray(pt.name): 366 return false 367 case dynamicParameters.InArray(pt.name): 368 return true 369 case dynamicParameters.Length() == 0 && staticParameters.Length() != 0: 370 return true 371 case dynamicParameters.Length() != 0 && staticParameters.Length() == 0: 372 return false 373 default: 374 return false 375 } 376 } 377 378 // NewDescribeReconfigureCmd shows details of history modifications or configuration file of reconfiguring operations 379 func NewDescribeReconfigureCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 380 o := &configObserverOptions{ 381 isExplain: false, 382 showDetail: false, 383 describeOpsOptions: newDescribeOpsOptions(f, streams), 384 } 385 cmd := &cobra.Command{ 386 Use: "describe-config", 387 Short: "Show details of a specific reconfiguring.", 388 Aliases: []string{"desc-config"}, 389 Example: describeReconfigureExample, 390 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 391 Run: func(cmd *cobra.Command, args []string) { 392 util.CheckErr(o.complete2(args)) 393 util.CheckErr(o.run(o.printComponentConfigSpecsDescribe)) 394 }, 395 } 396 o.addCommonFlags(cmd, f) 397 cmd.Flags().BoolVar(&o.showDetail, "show-detail", o.showDetail, "If true, the content of the files specified by config-file will be printed.") 398 cmd.Flags().StringSliceVar(&o.keys, "config-file", nil, "Specify the name of the configuration file to be describe (e.g. for mysql: --config-file=my.cnf). If unset, all files.") 399 return cmd 400 } 401 402 // NewExplainReconfigureCmd shows details of modifiable parameters. 403 func NewExplainReconfigureCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 404 o := &configObserverOptions{ 405 isExplain: true, 406 truncEnum: true, 407 truncDocument: false, 408 describeOpsOptions: newDescribeOpsOptions(f, streams), 409 } 410 cmd := &cobra.Command{ 411 Use: "explain-config", 412 Short: "List the constraint for supported configuration params.", 413 Aliases: []string{"ex-config"}, 414 Example: explainReconfigureExample, 415 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 416 Run: func(cmd *cobra.Command, args []string) { 417 util.CheckErr(o.complete2(args)) 418 util.CheckErr(o.run(o.printComponentExplainConfigure)) 419 }, 420 } 421 o.addCommonFlags(cmd, f) 422 cmd.Flags().BoolVar(&o.truncEnum, "trunc-enum", o.truncEnum, "If the value list length of the parameter is greater than 20, it will be truncated.") 423 cmd.Flags().BoolVar(&o.truncDocument, "trunc-document", o.truncDocument, "If the document length of the parameter is greater than 100, it will be truncated.") 424 cmd.Flags().StringVar(&o.paramName, "param", o.paramName, "Specify the name of parameter to be query. It clearly display the details of the parameter.") 425 return cmd 426 }