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  }