github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/describe_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  	"reflect"
    25  	"sort"
    26  	"strings"
    27  
    28  	"github.com/spf13/cobra"
    29  	"golang.org/x/exp/maps"
    30  	corev1 "k8s.io/api/core/v1"
    31  	"k8s.io/apimachinery/pkg/api/resource"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/cli-runtime/pkg/genericiooptions"
    34  	"k8s.io/client-go/dynamic"
    35  	clientset "k8s.io/client-go/kubernetes"
    36  	"k8s.io/client-go/kubernetes/scheme"
    37  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    38  	"k8s.io/kubectl/pkg/util/templates"
    39  
    40  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    41  	"github.com/1aal/kubeblocks/pkg/cli/cluster"
    42  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    43  	"github.com/1aal/kubeblocks/pkg/cli/types"
    44  	"github.com/1aal/kubeblocks/pkg/cli/util"
    45  )
    46  
    47  var (
    48  	describeOpsExample = templates.Examples(`
    49  		# describe a specified OpsRequest
    50  		kbcli cluster describe-ops mysql-restart-82zxv`)
    51  )
    52  
    53  type describeOpsOptions struct {
    54  	factory   cmdutil.Factory
    55  	client    clientset.Interface
    56  	dynamic   dynamic.Interface
    57  	namespace string
    58  
    59  	// resource type and names
    60  	gvr   schema.GroupVersionResource
    61  	names []string
    62  
    63  	genericiooptions.IOStreams
    64  }
    65  
    66  type opsObject interface {
    67  	appsv1alpha1.VerticalScaling | appsv1alpha1.HorizontalScaling | appsv1alpha1.OpsRequestVolumeClaimTemplate | appsv1alpha1.VolumeExpansion
    68  }
    69  
    70  func newDescribeOpsOptions(f cmdutil.Factory, streams genericiooptions.IOStreams) *describeOpsOptions {
    71  	return &describeOpsOptions{
    72  		factory:   f,
    73  		IOStreams: streams,
    74  		gvr:       types.OpsGVR(),
    75  	}
    76  }
    77  
    78  func NewDescribeOpsCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    79  	o := newDescribeOpsOptions(f, streams)
    80  	cmd := &cobra.Command{
    81  		Use:               "describe-ops",
    82  		Short:             "Show details of a specific OpsRequest.",
    83  		Aliases:           []string{"desc-ops"},
    84  		Example:           describeOpsExample,
    85  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.OpsGVR()),
    86  		Run: func(cmd *cobra.Command, args []string) {
    87  			util.CheckErr(o.complete(args))
    88  			util.CheckErr(o.run())
    89  		},
    90  	}
    91  	return cmd
    92  }
    93  
    94  // getCommandFlagsSlice returns the targetName slice by getName function and opsObject slice, their lengths are equal.
    95  func getCommandFlagsSlice[T opsObject](opsSt []T,
    96  	convertObject func(t T) any,
    97  	getName func(t T) string) ([][]string, []any) {
    98  	// returns the index of the first occurrence of v in s,s or -1 if not present.
    99  	indexFromAnySlice := func(s []any, v any) int {
   100  		for i := range s {
   101  			if reflect.DeepEqual(s[i], v) {
   102  				return i
   103  			}
   104  		}
   105  		return -1
   106  	}
   107  	opsObjectSlice := make([]any, 0, len(opsSt))
   108  	targetNameSlice := make([][]string, 0, len(opsSt))
   109  	for _, v := range opsSt {
   110  		index := indexFromAnySlice(opsObjectSlice, convertObject(v))
   111  		if index == -1 {
   112  			opsObjectSlice = append(opsObjectSlice, convertObject(v))
   113  			targetNameSlice = append(targetNameSlice, []string{getName(v)})
   114  			continue
   115  		}
   116  		targetNameSlice[index] = append(targetNameSlice[index], getName(v))
   117  	}
   118  	return targetNameSlice, opsObjectSlice
   119  }
   120  
   121  func (o *describeOpsOptions) complete(args []string) error {
   122  	var err error
   123  
   124  	if len(args) == 0 {
   125  		return fmt.Errorf("OpsRequest name should be specified")
   126  	}
   127  
   128  	o.names = args
   129  
   130  	if o.client, err = o.factory.KubernetesClientSet(); err != nil {
   131  		return err
   132  	}
   133  
   134  	if o.dynamic, err = o.factory.DynamicClient(); err != nil {
   135  		return err
   136  	}
   137  
   138  	if o.namespace, _, err = o.factory.ToRawKubeConfigLoader().Namespace(); err != nil {
   139  		return err
   140  	}
   141  	return nil
   142  }
   143  
   144  func (o *describeOpsOptions) run() error {
   145  	for _, name := range o.names {
   146  		if err := o.describeOps(name); err != nil {
   147  			return err
   148  		}
   149  	}
   150  	return nil
   151  }
   152  
   153  // describeOps gets the OpsRequest by name and describes it.
   154  func (o *describeOpsOptions) describeOps(name string) error {
   155  	opsRequest := &appsv1alpha1.OpsRequest{}
   156  	if err := cluster.GetK8SClientObject(o.dynamic, opsRequest, o.gvr, o.namespace, name); err != nil {
   157  		return err
   158  	}
   159  	return o.printOpsRequest(opsRequest)
   160  }
   161  
   162  // printOpsRequest prints the information of OpsRequest for describing command.
   163  func (o *describeOpsOptions) printOpsRequest(ops *appsv1alpha1.OpsRequest) error {
   164  	printer.PrintLine("Spec:")
   165  	printer.PrintLineWithTabSeparator(
   166  		// first pair string
   167  		printer.NewPair("  Name", ops.Name),
   168  		printer.NewPair("NameSpace", ops.Namespace),
   169  		printer.NewPair("Cluster", ops.Spec.ClusterRef),
   170  		printer.NewPair("Type", string(ops.Spec.Type)),
   171  	)
   172  
   173  	o.printOpsCommand(ops)
   174  
   175  	// print the last configuration of the cluster.
   176  	o.printLastConfiguration(ops.Status.LastConfiguration, ops.Spec.Type)
   177  
   178  	// print the OpsRequest.status
   179  	o.printOpsRequestStatus(&ops.Status)
   180  
   181  	// print the OpsRequest.status.conditions
   182  	printer.PrintConditions(ops.Status.Conditions, o.Out)
   183  
   184  	// get all events about cluster
   185  	events, err := o.client.CoreV1().Events(o.namespace).Search(scheme.Scheme, ops)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	// print the warning events
   191  	printer.PrintAllWarningEvents(events, o.Out)
   192  
   193  	return nil
   194  }
   195  
   196  // printOpsCommand prints the kbcli command by OpsRequest.spec.
   197  func (o *describeOpsOptions) printOpsCommand(opsRequest *appsv1alpha1.OpsRequest) {
   198  	if opsRequest == nil {
   199  		return
   200  	}
   201  	var commands []string
   202  	switch opsRequest.Spec.Type {
   203  	case appsv1alpha1.RestartType:
   204  		commands = o.getRestartCommand(opsRequest.Spec)
   205  	case appsv1alpha1.UpgradeType:
   206  		commands = o.getUpgradeCommand(opsRequest.Spec)
   207  	case appsv1alpha1.HorizontalScalingType:
   208  		commands = o.getHorizontalScalingCommand(opsRequest.Spec)
   209  	case appsv1alpha1.VerticalScalingType:
   210  		commands = o.getVerticalScalingCommand(opsRequest.Spec)
   211  	case appsv1alpha1.VolumeExpansionType:
   212  		commands = o.getVolumeExpansionCommand(opsRequest.Spec)
   213  	case appsv1alpha1.ReconfiguringType:
   214  		commands = o.getReconfiguringCommand(opsRequest.Spec)
   215  	}
   216  	if len(commands) == 0 {
   217  		printer.PrintLine("\nCommand: " + printer.NoneString)
   218  		return
   219  	}
   220  	printer.PrintTitle("Command")
   221  	for i := range commands {
   222  		command := fmt.Sprintf("%s --namespace=%s", commands[i], opsRequest.Namespace)
   223  		printer.PrintLine("  " + command)
   224  	}
   225  }
   226  
   227  // getRestartCommand gets the command of the Restart OpsRequest.
   228  func (o *describeOpsOptions) getRestartCommand(spec appsv1alpha1.OpsRequestSpec) []string {
   229  	if len(spec.RestartList) == 0 {
   230  		return nil
   231  	}
   232  	componentNames := make([]string, len(spec.RestartList))
   233  	for i, v := range spec.RestartList {
   234  		componentNames[i] = v.ComponentName
   235  	}
   236  	return []string{
   237  		fmt.Sprintf("kbcli cluster restart %s --components=%s", spec.ClusterRef,
   238  			strings.Join(componentNames, ",")),
   239  	}
   240  }
   241  
   242  // getUpgradeCommand gets the command of the Upgrade OpsRequest.
   243  func (o *describeOpsOptions) getUpgradeCommand(spec appsv1alpha1.OpsRequestSpec) []string {
   244  	return []string{
   245  		fmt.Sprintf("kbcli cluster upgrade %s --cluster-version=%s", spec.ClusterRef,
   246  			spec.Upgrade.ClusterVersionRef),
   247  	}
   248  }
   249  
   250  // addResourceFlag adds resource flag for VerticalScaling OpsRequest.
   251  func (o *describeOpsOptions) addResourceFlag(key string, value *resource.Quantity) string {
   252  	if !value.IsZero() {
   253  		return fmt.Sprintf(" --%s=%s", key, value)
   254  	}
   255  	return ""
   256  }
   257  
   258  // getVerticalScalingCommand gets the command of the VerticalScaling OpsRequest
   259  func (o *describeOpsOptions) getVerticalScalingCommand(spec appsv1alpha1.OpsRequestSpec) []string {
   260  	if len(spec.VerticalScalingList) == 0 {
   261  		return nil
   262  	}
   263  	convertObject := func(h appsv1alpha1.VerticalScaling) any {
   264  		return h.ResourceRequirements
   265  	}
   266  	getCompName := func(h appsv1alpha1.VerticalScaling) string {
   267  		return h.ComponentName
   268  	}
   269  	componentNameSlice, resourceSlice := getCommandFlagsSlice[appsv1alpha1.VerticalScaling](
   270  		spec.VerticalScalingList, convertObject, getCompName)
   271  	commands := make([]string, len(componentNameSlice))
   272  	for i := range componentNameSlice {
   273  		commands[i] = fmt.Sprintf("kbcli cluster vscale %s --components=%s",
   274  			spec.ClusterRef, strings.Join(componentNameSlice[i], ","))
   275  		clsRef := spec.VerticalScalingList[i].ClassDefRef
   276  		if clsRef != nil {
   277  			class := clsRef.Class
   278  			if clsRef.Name != "" {
   279  				class = fmt.Sprintf("%s:%s", clsRef.Name, class)
   280  			}
   281  			commands[i] += fmt.Sprintf("--class=%s", class)
   282  		} else {
   283  			resource := resourceSlice[i].(corev1.ResourceRequirements)
   284  			commands[i] += o.addResourceFlag("cpu", resource.Limits.Cpu())
   285  			commands[i] += o.addResourceFlag("memory", resource.Limits.Memory())
   286  		}
   287  	}
   288  	return commands
   289  }
   290  
   291  // getHorizontalScalingCommand gets the command of the HorizontalScaling OpsRequest.
   292  func (o *describeOpsOptions) getHorizontalScalingCommand(spec appsv1alpha1.OpsRequestSpec) []string {
   293  	if len(spec.HorizontalScalingList) == 0 {
   294  		return nil
   295  	}
   296  	convertObject := func(h appsv1alpha1.HorizontalScaling) any {
   297  		return h.Replicas
   298  	}
   299  	getCompName := func(h appsv1alpha1.HorizontalScaling) string {
   300  		return h.ComponentName
   301  	}
   302  	componentNameSlice, replicasSlice := getCommandFlagsSlice[appsv1alpha1.HorizontalScaling](
   303  		spec.HorizontalScalingList, convertObject, getCompName)
   304  	commands := make([]string, len(componentNameSlice))
   305  	for i := range componentNameSlice {
   306  		commands[i] = fmt.Sprintf("kbcli cluster hscale %s --components=%s --replicas=%d",
   307  			spec.ClusterRef, strings.Join(componentNameSlice[i], ","), replicasSlice[i].(int32))
   308  	}
   309  	return commands
   310  }
   311  
   312  // getVolumeExpansionCommand gets the command of the VolumeExpansion command.
   313  func (o *describeOpsOptions) getVolumeExpansionCommand(spec appsv1alpha1.OpsRequestSpec) []string {
   314  	convertObject := func(v appsv1alpha1.OpsRequestVolumeClaimTemplate) any {
   315  		return v.Storage
   316  	}
   317  	getVCTName := func(v appsv1alpha1.OpsRequestVolumeClaimTemplate) string {
   318  		return v.Name
   319  	}
   320  	commands := make([]string, 0)
   321  	for _, v := range spec.VolumeExpansionList {
   322  		vctNameSlice, storageSlice := getCommandFlagsSlice[appsv1alpha1.OpsRequestVolumeClaimTemplate](
   323  			v.VolumeClaimTemplates, convertObject, getVCTName)
   324  		for i := range vctNameSlice {
   325  			storage := storageSlice[i].(resource.Quantity)
   326  			commands = append(commands, fmt.Sprintf("kbcli cluster volume-expand %s --components=%s --volume-claim-template-names=%s --storage=%s",
   327  				spec.ClusterRef, v.ComponentName, strings.Join(vctNameSlice[i], ","), storage.String()))
   328  		}
   329  	}
   330  	return commands
   331  }
   332  
   333  // getReconfiguringCommand gets the command of the VolumeExpansion command.
   334  func (o *describeOpsOptions) getReconfiguringCommand(spec appsv1alpha1.OpsRequestSpec) []string {
   335  	var (
   336  		updatedParams = spec.Reconfigure
   337  		componentName = updatedParams.ComponentName
   338  	)
   339  
   340  	if len(updatedParams.Configurations) == 0 {
   341  		return nil
   342  	}
   343  
   344  	configuration := updatedParams.Configurations[0]
   345  	if len(configuration.Keys) == 0 {
   346  		return nil
   347  	}
   348  
   349  	commandArgs := make([]string, 0)
   350  	commandArgs = append(commandArgs, "kbcli")
   351  	commandArgs = append(commandArgs, "cluster")
   352  	commandArgs = append(commandArgs, "configure")
   353  	commandArgs = append(commandArgs, spec.ClusterRef)
   354  	commandArgs = append(commandArgs, fmt.Sprintf("--components=%s", componentName))
   355  	commandArgs = append(commandArgs, fmt.Sprintf("--config-spec=%s", configuration.Name))
   356  
   357  	config := configuration.Keys[0]
   358  	commandArgs = append(commandArgs, fmt.Sprintf("--config-file=%s", config.Key))
   359  	for _, p := range config.Parameters {
   360  		if p.Value == nil {
   361  			continue
   362  		}
   363  		commandArgs = append(commandArgs, fmt.Sprintf("--set %s=%s", p.Key, *p.Value))
   364  	}
   365  	return []string{strings.Join(commandArgs, " ")}
   366  }
   367  
   368  // printOpsRequestStatus prints the OpsRequest status infos.
   369  func (o *describeOpsOptions) printOpsRequestStatus(opsStatus *appsv1alpha1.OpsRequestStatus) {
   370  	printer.PrintTitle("Status")
   371  	startTime := opsStatus.StartTimestamp
   372  	if !startTime.IsZero() {
   373  		printer.PrintPairStringToLine("Start Time", util.TimeFormat(&startTime))
   374  	}
   375  	completeTime := opsStatus.CompletionTimestamp
   376  	if !completeTime.IsZero() {
   377  		printer.PrintPairStringToLine("Completion Time", util.TimeFormat(&completeTime))
   378  	}
   379  	if !startTime.IsZero() {
   380  		printer.PrintPairStringToLine("Duration", util.GetHumanReadableDuration(startTime, completeTime))
   381  	}
   382  	printer.PrintPairStringToLine("Status", string(opsStatus.Phase))
   383  	o.printProgressDetails(opsStatus)
   384  }
   385  
   386  // printLastConfiguration prints the last configuration of the cluster before doing the OpsRequest.
   387  func (o *describeOpsOptions) printLastConfiguration(configuration appsv1alpha1.LastConfiguration, opsType appsv1alpha1.OpsType) {
   388  	if reflect.DeepEqual(configuration, appsv1alpha1.LastConfiguration{}) {
   389  		return
   390  	}
   391  	printer.PrintTitle("Last Configuration")
   392  	switch opsType {
   393  	case appsv1alpha1.UpgradeType:
   394  		printer.PrintPairStringToLine("Cluster Version", configuration.ClusterVersionRef)
   395  	case appsv1alpha1.VerticalScalingType:
   396  		handleVScale := func(tbl *printer.TablePrinter, cName string, compConf appsv1alpha1.LastComponentConfiguration) {
   397  			tbl.AddRow(cName, compConf.Requests.Cpu().String(), compConf.Requests.Memory().String(), compConf.Limits.Cpu().String(), compConf.Limits.Memory().String())
   398  		}
   399  		headers := []interface{}{"COMPONENT", "REQUEST-CPU", "REQUEST-MEMORY", "LIMIT-CPU", "LIMIT-MEMORY"}
   400  		o.printLastConfigurationByOpsType(configuration, headers, handleVScale)
   401  	case appsv1alpha1.HorizontalScalingType:
   402  		handleHScale := func(tbl *printer.TablePrinter, cName string, compConf appsv1alpha1.LastComponentConfiguration) {
   403  			tbl.AddRow(cName, *compConf.Replicas)
   404  		}
   405  		headers := []interface{}{"COMPONENT", "REPLICAS"}
   406  		o.printLastConfigurationByOpsType(configuration, headers, handleHScale)
   407  	case appsv1alpha1.VolumeExpansionType:
   408  		handleVolumeExpansion := func(tbl *printer.TablePrinter, cName string, compConf appsv1alpha1.LastComponentConfiguration) {
   409  			vcts := compConf.VolumeClaimTemplates
   410  			for _, v := range vcts {
   411  				tbl.AddRow(cName, v.Name, v.Storage.String())
   412  			}
   413  		}
   414  		headers := []interface{}{"COMPONENT", "VOLUME-CLAIM-TEMPLATE", "STORAGE"}
   415  		o.printLastConfigurationByOpsType(configuration, headers, handleVolumeExpansion)
   416  	}
   417  }
   418  
   419  // printLastConfigurationByOpsType prints the last configuration by ops type.
   420  func (o *describeOpsOptions) printLastConfigurationByOpsType(configuration appsv1alpha1.LastConfiguration,
   421  	headers []interface{},
   422  	handleOpsObject func(tbl *printer.TablePrinter, cName string, compConf appsv1alpha1.LastComponentConfiguration),
   423  ) {
   424  	tbl := printer.NewTablePrinter(o.Out)
   425  	tbl.SetHeader(headers...)
   426  	keys := maps.Keys(configuration.Components)
   427  	sort.Strings(keys)
   428  	for _, cName := range keys {
   429  		handleOpsObject(tbl, cName, configuration.Components[cName])
   430  	}
   431  	tbl.Print()
   432  }
   433  
   434  // printProgressDetails prints the progressDetails of all components in this OpsRequest.
   435  func (o *describeOpsOptions) printProgressDetails(opsStatus *appsv1alpha1.OpsRequestStatus) {
   436  	printer.PrintPairStringToLine("Progress", opsStatus.Progress)
   437  	keys := maps.Keys(opsStatus.Components)
   438  	sort.Strings(keys)
   439  	tbl := printer.NewTablePrinter(o.Out)
   440  	tbl.SetHeader(fmt.Sprintf("%-22s%s", "", "OBJECT-KEY"), "STATUS", "DURATION", "MESSAGE")
   441  	for _, cName := range keys {
   442  		progressDetails := opsStatus.Components[cName].ProgressDetails
   443  		for _, v := range progressDetails {
   444  			var groupStr string
   445  			if len(v.Group) > 0 {
   446  				groupStr = fmt.Sprintf("(%s)", v.Group)
   447  			}
   448  			tbl.AddRow(fmt.Sprintf("%-22s%s%s", "", v.ObjectKey, groupStr),
   449  				v.Status, util.GetHumanReadableDuration(v.StartTime, v.EndTime), v.Message)
   450  		}
   451  	}
   452  	//  "-/-" is the progress default value.
   453  	if opsStatus.Progress != "-/-" {
   454  		tbl.Print()
   455  	}
   456  }