github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/list_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  	"context"
    24  	"fmt"
    25  	"sort"
    26  	"strings"
    27  
    28  	"github.com/spf13/cobra"
    29  	"golang.org/x/exp/slices"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    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/list"
    39  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    40  	"github.com/1aal/kubeblocks/pkg/cli/types"
    41  	"github.com/1aal/kubeblocks/pkg/cli/util"
    42  )
    43  
    44  var (
    45  	listOpsExample = templates.Examples(`
    46  		# list all opsRequests
    47  		kbcli cluster list-ops
    48  
    49  		# list all opsRequests of specified cluster
    50  		kbcli cluster list-ops mycluster`)
    51  
    52  	defaultDisplayPhase = []string{"pending", "creating", "running", "canceling", "failed"}
    53  )
    54  
    55  type opsListOptions struct {
    56  	*list.ListOptions
    57  	status         []string
    58  	opsType        []string
    59  	opsRequestName string
    60  }
    61  
    62  func NewListOpsCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    63  	o := &opsListOptions{
    64  		ListOptions: list.NewListOptions(f, streams, types.OpsGVR()),
    65  	}
    66  	cmd := &cobra.Command{
    67  		Use:               "list-ops",
    68  		Short:             "List all opsRequests.",
    69  		Aliases:           []string{"ls-ops"},
    70  		Example:           listOpsExample,
    71  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
    72  		Run: func(cmd *cobra.Command, args []string) {
    73  			// build label selector for listing ops
    74  			o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args)
    75  			// args are the cluster names. we only use the label selector to get ops, so resources names
    76  			// are not needed.
    77  			o.Names = nil
    78  			util.CheckErr(o.Complete())
    79  			util.CheckErr(o.printOpsList())
    80  		},
    81  	}
    82  	o.AddFlags(cmd)
    83  	cmd.Flags().StringSliceVar(&o.opsType, "type", nil, "The OpsRequest type")
    84  	cmd.Flags().StringSliceVar(&o.status, "status", defaultDisplayPhase, fmt.Sprintf("Options include all, %s. by default, outputs the %s OpsRequest.",
    85  		strings.Join(defaultDisplayPhase, ", "), strings.Join(defaultDisplayPhase, "/")))
    86  	cmd.Flags().StringVar(&o.opsRequestName, "name", "", "The OpsRequest name to get the details.")
    87  	return cmd
    88  }
    89  
    90  func (o *opsListOptions) printOpsList() error {
    91  	// if format is JSON or YAML, use default printer to output the result.
    92  	if o.Format == printer.JSON || o.Format == printer.YAML {
    93  		if o.opsRequestName != "" {
    94  			o.Names = []string{o.opsRequestName}
    95  		}
    96  		_, err := o.Run()
    97  		return err
    98  	}
    99  
   100  	dynamic, err := o.Factory.DynamicClient()
   101  	if err != nil {
   102  		return err
   103  	}
   104  	listOptions := metav1.ListOptions{
   105  		LabelSelector: o.LabelSelector,
   106  		FieldSelector: o.FieldSelector,
   107  	}
   108  	if o.AllNamespaces {
   109  		o.Namespace = ""
   110  	}
   111  	opsList, err := dynamic.Resource(types.OpsGVR()).Namespace(o.Namespace).List(context.TODO(), listOptions)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	if len(opsList.Items) == 0 {
   116  		o.PrintNotFoundResources()
   117  		return nil
   118  	}
   119  	// sort the unstructured objects with the creationTimestamp in positive order
   120  	sort.Sort(unstructuredList(opsList.Items))
   121  
   122  	// check if specified with "all" keyword for status.
   123  	isAllStatus := o.isAllStatus()
   124  	tblPrinter := printer.NewTablePrinter(o.Out)
   125  	tblPrinter.SetHeader("NAME", "NAMESPACE", "TYPE", "CLUSTER", "COMPONENT", "STATUS", "PROGRESS", "CREATED-TIME")
   126  	for _, obj := range opsList.Items {
   127  		ops := &appsv1alpha1.OpsRequest{}
   128  		if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, ops); err != nil {
   129  			return err
   130  		}
   131  		phase := string(ops.Status.Phase)
   132  		opsType := string(ops.Spec.Type)
   133  		if len(o.opsRequestName) != 0 {
   134  			if ops.Name == o.opsRequestName {
   135  				tblPrinter.AddRow(ops.Name, ops.GetNamespace(), opsType, ops.Spec.ClusterRef, getComponentNameFromOps(ops), phase, ops.Status.Progress, util.TimeFormat(&ops.CreationTimestamp))
   136  			}
   137  			continue
   138  		}
   139  		// if the OpsRequest phase is not expected, continue
   140  		if !isAllStatus && !o.containsIgnoreCase(o.status, phase) {
   141  			continue
   142  		}
   143  
   144  		if len(o.opsType) != 0 && !o.containsIgnoreCase(o.opsType, opsType) {
   145  			continue
   146  		}
   147  		tblPrinter.AddRow(ops.Name, ops.GetNamespace(), opsType, ops.Spec.ClusterRef, getComponentNameFromOps(ops), phase, ops.Status.Progress, util.TimeFormat(&ops.CreationTimestamp))
   148  	}
   149  	if tblPrinter.Tbl.Length() != 0 {
   150  		tblPrinter.Print()
   151  		return nil
   152  	}
   153  	message := "No opsRequests found"
   154  	if len(o.opsRequestName) == 0 && !o.isAllStatus() {
   155  		message += ", you can try as follows:\n\tkbcli cluster list-ops --status all"
   156  	}
   157  	printer.PrintLine(message)
   158  	return nil
   159  }
   160  
   161  func getComponentNameFromOps(ops *appsv1alpha1.OpsRequest) string {
   162  	components := make([]string, 0)
   163  	opsSpec := ops.Spec
   164  	switch opsSpec.Type {
   165  	case appsv1alpha1.ReconfiguringType:
   166  		components = append(components, opsSpec.Reconfigure.ComponentName)
   167  	case appsv1alpha1.HorizontalScalingType:
   168  		for _, item := range opsSpec.HorizontalScalingList {
   169  			components = append(components, item.ComponentName)
   170  		}
   171  	case appsv1alpha1.VolumeExpansionType:
   172  		for _, item := range opsSpec.VolumeExpansionList {
   173  			components = append(components, item.ComponentName)
   174  		}
   175  	case appsv1alpha1.RestartType:
   176  		for _, item := range opsSpec.RestartList {
   177  			components = append(components, item.ComponentName)
   178  		}
   179  	case appsv1alpha1.VerticalScalingType:
   180  		for _, item := range opsSpec.VerticalScalingList {
   181  			components = append(components, item.ComponentName)
   182  		}
   183  	default:
   184  		for k := range ops.Status.Components {
   185  			components = append(components, k)
   186  		}
   187  		slices.Sort(components)
   188  	}
   189  	return strings.Join(components, ",")
   190  }
   191  
   192  func getTemplateNameFromOps(ops appsv1alpha1.OpsRequestSpec) string {
   193  	if ops.Type != appsv1alpha1.ReconfiguringType {
   194  		return ""
   195  	}
   196  
   197  	tpls := make([]string, 0)
   198  	for _, config := range ops.Reconfigure.Configurations {
   199  		tpls = append(tpls, config.Name)
   200  	}
   201  	return strings.Join(tpls, ",")
   202  }
   203  
   204  func getKeyNameFromOps(ops appsv1alpha1.OpsRequestSpec) string {
   205  	if ops.Type != appsv1alpha1.ReconfiguringType {
   206  		return ""
   207  	}
   208  
   209  	keys := make([]string, 0)
   210  	for _, config := range ops.Reconfigure.Configurations {
   211  		for _, key := range config.Keys {
   212  			keys = append(keys, key.Key)
   213  		}
   214  	}
   215  	return strings.Join(keys, ",")
   216  }
   217  
   218  func (o *opsListOptions) containsIgnoreCase(s []string, e string) bool {
   219  	for i := range s {
   220  		if strings.EqualFold(s[i], e) {
   221  			return true
   222  		}
   223  	}
   224  	return false
   225  }
   226  
   227  // isAllStatus checks if the status flag contains "all" keyword.
   228  func (o *opsListOptions) isAllStatus() bool {
   229  	return slices.Contains(o.status, "all")
   230  }
   231  
   232  type unstructuredList []unstructured.Unstructured
   233  
   234  func (us unstructuredList) Len() int {
   235  	return len(us)
   236  }
   237  func (us unstructuredList) Swap(i, j int) {
   238  	us[i], us[j] = us[j], us[i]
   239  }
   240  func (us unstructuredList) Less(i, j int) bool {
   241  	createTimeForJ := us[j].GetCreationTimestamp()
   242  	createTimeForI := us[i].GetCreationTimestamp()
   243  	return createTimeForI.Before(&createTimeForJ)
   244  }