github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/config_diff.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  	"reflect"
    25  
    26  	"github.com/spf13/cast"
    27  	"github.com/spf13/cobra"
    28  	"k8s.io/cli-runtime/pkg/genericiooptions"
    29  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    30  	"k8s.io/kubectl/pkg/util/templates"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  
    33  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    34  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    35  	"github.com/1aal/kubeblocks/pkg/cli/types"
    36  	"github.com/1aal/kubeblocks/pkg/cli/util"
    37  	"github.com/1aal/kubeblocks/pkg/configuration/core"
    38  	"github.com/1aal/kubeblocks/pkg/constant"
    39  	"github.com/1aal/kubeblocks/pkg/unstructured"
    40  )
    41  
    42  type configDiffOptions struct {
    43  	baseOptions *describeOpsOptions
    44  
    45  	clusterName   string
    46  	componentName string
    47  	templateNames []string
    48  	baseVersion   *appsv1alpha1.OpsRequest
    49  	diffVersion   *appsv1alpha1.OpsRequest
    50  }
    51  
    52  var (
    53  	diffConfigureExample = templates.Examples(`
    54  		# compare config files
    55  		kbcli cluster diff-config opsrequest1 opsrequest2`)
    56  )
    57  
    58  func (o *configDiffOptions) complete(args []string) error {
    59  	isValidReconfigureOps := func(ops *appsv1alpha1.OpsRequest) bool {
    60  		return ops.Spec.Type == appsv1alpha1.ReconfiguringType && ops.Spec.Reconfigure != nil
    61  	}
    62  
    63  	if len(args) != 2 {
    64  		return core.MakeError("missing opsrequest name")
    65  	}
    66  
    67  	if err := o.baseOptions.complete(args); err != nil {
    68  		return err
    69  	}
    70  
    71  	baseVersion := &appsv1alpha1.OpsRequest{}
    72  	diffVersion := &appsv1alpha1.OpsRequest{}
    73  	if err := util.GetResourceObjectFromGVR(types.OpsGVR(), client.ObjectKey{
    74  		Namespace: o.baseOptions.namespace,
    75  		Name:      args[0],
    76  	}, o.baseOptions.dynamic, baseVersion); err != nil {
    77  		return core.WrapError(err, "failed to get ops CR [%s]", args[0])
    78  	}
    79  	if err := util.GetResourceObjectFromGVR(types.OpsGVR(), client.ObjectKey{
    80  		Namespace: o.baseOptions.namespace,
    81  		Name:      args[1],
    82  	}, o.baseOptions.dynamic, diffVersion); err != nil {
    83  		return core.WrapError(err, "failed to get ops CR [%s]", args[1])
    84  	}
    85  
    86  	if !isValidReconfigureOps(baseVersion) {
    87  		return core.MakeError("opsrequest is not valid reconfiguring operation [%s]", client.ObjectKeyFromObject(baseVersion))
    88  	}
    89  
    90  	if !isValidReconfigureOps(diffVersion) {
    91  		return core.MakeError("opsrequest is not valid reconfiguring operation [%s]", client.ObjectKeyFromObject(diffVersion))
    92  	}
    93  
    94  	if !o.maybeCompareOps(baseVersion, diffVersion) {
    95  		return core.MakeError("failed to diff, not same cluster, or same component, or template.")
    96  	}
    97  
    98  	o.baseVersion = baseVersion
    99  	o.diffVersion = diffVersion
   100  	return nil
   101  }
   102  
   103  func findTemplateStatusByName(status *appsv1alpha1.ReconfiguringStatus, tplName string) *appsv1alpha1.ConfigurationItemStatus {
   104  	if status == nil {
   105  		return nil
   106  	}
   107  
   108  	for i := range status.ConfigurationStatus {
   109  		s := &status.ConfigurationStatus[i]
   110  		if s.Name == tplName {
   111  			return s
   112  		}
   113  	}
   114  	return nil
   115  }
   116  
   117  func (o *configDiffOptions) validate() error {
   118  	var (
   119  		baseStatus = o.baseVersion.Status
   120  		diffStatus = o.diffVersion.Status
   121  	)
   122  
   123  	if baseStatus.Phase != appsv1alpha1.OpsSucceedPhase {
   124  		return core.MakeError("require reconfiguring phase is success!, name: %s, phase: %s", o.baseVersion.Name, baseStatus.Phase)
   125  	}
   126  	if diffStatus.Phase != appsv1alpha1.OpsSucceedPhase {
   127  		return core.MakeError("require reconfiguring phase is success!, name: %s, phase: %s", o.diffVersion.Name, diffStatus.Phase)
   128  	}
   129  
   130  	for _, tplName := range o.templateNames {
   131  		s1 := findTemplateStatusByName(baseStatus.ReconfiguringStatus, tplName)
   132  		s2 := findTemplateStatusByName(diffStatus.ReconfiguringStatus, tplName)
   133  		if s1 == nil || len(s1.LastAppliedConfiguration) == 0 {
   134  			return core.MakeError("invalid reconfiguring status. CR[%v]", client.ObjectKeyFromObject(o.baseVersion))
   135  		}
   136  		if s2 == nil || len(s2.LastAppliedConfiguration) == 0 {
   137  			return core.MakeError("invalid reconfiguring status. CR[%v]", client.ObjectKeyFromObject(o.diffVersion))
   138  		}
   139  	}
   140  	return nil
   141  }
   142  
   143  func (o *configDiffOptions) run() error {
   144  	configDiffs := make(map[string][]core.VisualizedParam, len(o.templateNames))
   145  	baseConfigs := make(map[string]map[string]unstructured.ConfigObject)
   146  	for _, tplName := range o.templateNames {
   147  		diff, baseObj, err := o.diffConfig(tplName)
   148  		if err != nil {
   149  			return err
   150  		}
   151  		configDiffs[tplName] = diff
   152  		baseConfigs[tplName] = baseObj
   153  	}
   154  
   155  	printer.PrintTitle("DIFF-CONFIG RESULT")
   156  	for tplName, diff := range configDiffs {
   157  		configObjects := baseConfigs[tplName]
   158  		for _, params := range diff {
   159  			printer.PrintLineWithTabSeparator(
   160  				printer.NewPair("  ConfigFile", printer.BoldYellow(params.Key)),
   161  				printer.NewPair("TemplateName", tplName),
   162  				printer.NewPair("ComponentName", o.componentName),
   163  				printer.NewPair("ClusterName", o.clusterName),
   164  				printer.NewPair("UpdateType", string(params.UpdateType)),
   165  			)
   166  			fmt.Fprintf(o.baseOptions.Out, "\n")
   167  			tbl := printer.NewTablePrinter(o.baseOptions.Out)
   168  			tbl.SetHeader("ParameterName", o.baseVersion.Name, o.diffVersion.Name)
   169  			configObj := configObjects[params.Key]
   170  			for _, v := range params.Parameters {
   171  				baseValue := "null"
   172  				if configObj != nil {
   173  					baseValue = cast.ToString(configObj.Get(v.Key))
   174  				}
   175  				tbl.AddRow(v.Key, baseValue, v.Value)
   176  			}
   177  			tbl.Print()
   178  			fmt.Fprintf(o.baseOptions.Out, "\n\n")
   179  		}
   180  	}
   181  	return nil
   182  }
   183  
   184  func (o *configDiffOptions) maybeCompareOps(base *appsv1alpha1.OpsRequest, diff *appsv1alpha1.OpsRequest) bool {
   185  	getClusterName := func(ops client.Object) string {
   186  		labels := ops.GetLabels()
   187  		if len(labels) == 0 {
   188  			return ""
   189  		}
   190  		return labels[constant.AppInstanceLabelKey]
   191  	}
   192  	getComponentName := func(ops appsv1alpha1.OpsRequestSpec) string {
   193  		return ops.Reconfigure.ComponentName
   194  	}
   195  	getTemplateName := func(ops appsv1alpha1.OpsRequestSpec) []string {
   196  		configs := ops.Reconfigure.Configurations
   197  		names := make([]string, len(configs))
   198  		for i, config := range configs {
   199  			names[i] = config.Name
   200  		}
   201  		return names
   202  	}
   203  
   204  	clusterName := getClusterName(base)
   205  	if len(clusterName) == 0 || clusterName != getClusterName(diff) {
   206  		return false
   207  	}
   208  	componentName := getComponentName(base.Spec)
   209  	if len(componentName) == 0 || componentName != getComponentName(diff.Spec) {
   210  		return false
   211  	}
   212  	templateNames := getTemplateName(base.Spec)
   213  	if len(templateNames) == 0 || !reflect.DeepEqual(templateNames, getTemplateName(diff.Spec)) {
   214  		return false
   215  	}
   216  
   217  	o.clusterName = clusterName
   218  	o.componentName = componentName
   219  	o.templateNames = templateNames
   220  	return true
   221  }
   222  
   223  func (o *configDiffOptions) diffConfig(tplName string) ([]core.VisualizedParam, map[string]unstructured.ConfigObject, error) {
   224  	var (
   225  		tpl              *appsv1alpha1.ComponentConfigSpec
   226  		configConstraint = &appsv1alpha1.ConfigConstraint{}
   227  	)
   228  
   229  	tplList, err := util.GetConfigTemplateList(o.clusterName, o.baseOptions.namespace, o.baseOptions.dynamic, o.componentName, true)
   230  	if err != nil {
   231  		return nil, nil, err
   232  	}
   233  	if tpl = findTplByName(tplList, tplName); tpl == nil {
   234  		return nil, nil, core.MakeError("not found template: %s", tplName)
   235  	}
   236  	if err := util.GetResourceObjectFromGVR(types.ConfigConstraintGVR(), client.ObjectKey{
   237  		Namespace: "",
   238  		Name:      tpl.ConfigConstraintRef,
   239  	}, o.baseOptions.dynamic, configConstraint); err != nil {
   240  		return nil, nil, err
   241  	}
   242  
   243  	formatCfg := configConstraint.Spec.FormatterConfig
   244  
   245  	base := findTemplateStatusByName(o.baseVersion.Status.ReconfiguringStatus, tplName)
   246  	diff := findTemplateStatusByName(o.diffVersion.Status.ReconfiguringStatus, tplName)
   247  	patch, _, err := core.CreateConfigPatch(base.LastAppliedConfiguration, diff.LastAppliedConfiguration, formatCfg.Format, tpl.Keys, false)
   248  	if err != nil {
   249  		return nil, nil, err
   250  	}
   251  
   252  	baseConfigObj, err := core.LoadRawConfigObject(base.LastAppliedConfiguration, formatCfg, tpl.Keys)
   253  	if err != nil {
   254  		return nil, nil, err
   255  	}
   256  	return core.GenerateVisualizedParamsList(patch, formatCfg, nil), baseConfigObj, nil
   257  }
   258  
   259  // NewDiffConfigureCmd shows the difference between two configuration version.
   260  func NewDiffConfigureCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   261  	o := &configDiffOptions{baseOptions: newDescribeOpsOptions(f, streams)}
   262  	cmd := &cobra.Command{
   263  		Use:               "diff-config",
   264  		Short:             "Show the difference in parameters between the two submitted OpsRequest.",
   265  		Aliases:           []string{"diff"},
   266  		Example:           diffConfigureExample,
   267  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.OpsGVR()),
   268  		Run: func(cmd *cobra.Command, args []string) {
   269  			util.CheckErr(o.complete(args))
   270  			util.CheckErr(o.validate())
   271  			util.CheckErr(o.run())
   272  		},
   273  	}
   274  	return cmd
   275  }