github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/list/list.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 list
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"strings"
    27  
    28  	"github.com/spf13/cobra"
    29  	corev1 "k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/api/meta"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    37  	"k8s.io/apimachinery/pkg/util/sets"
    38  	"k8s.io/cli-runtime/pkg/genericclioptions"
    39  	"k8s.io/cli-runtime/pkg/genericiooptions"
    40  	"k8s.io/cli-runtime/pkg/printers"
    41  	"k8s.io/cli-runtime/pkg/resource"
    42  	"k8s.io/client-go/kubernetes/scheme"
    43  	"k8s.io/client-go/rest"
    44  	cmdget "k8s.io/kubectl/pkg/cmd/get"
    45  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    46  
    47  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    48  	"github.com/1aal/kubeblocks/pkg/cli/util"
    49  )
    50  
    51  type ListOptions struct {
    52  	Factory       cmdutil.Factory
    53  	Namespace     string
    54  	AllNamespaces bool
    55  	LabelSelector string
    56  	FieldSelector string
    57  	ShowLabels    bool
    58  	ToPrinter     func(*meta.RESTMapping, bool) (printers.ResourcePrinterFunc, error)
    59  
    60  	// Names are the resource names
    61  	Names  []string
    62  	GVR    schema.GroupVersionResource
    63  	Format printer.Format
    64  
    65  	// print the result or not, if true, use default printer to print, otherwise,
    66  	// only return the result to caller.
    67  	Print  bool
    68  	SortBy string
    69  	genericiooptions.IOStreams
    70  }
    71  
    72  func NewListOptions(f cmdutil.Factory, streams genericiooptions.IOStreams,
    73  	gvr schema.GroupVersionResource) *ListOptions {
    74  	return &ListOptions{
    75  		Factory:   f,
    76  		IOStreams: streams,
    77  		GVR:       gvr,
    78  		Print:     true,
    79  		SortBy:    ".metadata.name",
    80  	}
    81  }
    82  
    83  func (o *ListOptions) AddFlags(cmd *cobra.Command, isClusterScope ...bool) {
    84  	if len(isClusterScope) == 0 || !isClusterScope[0] {
    85  		cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
    86  	}
    87  	cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.")
    88  	cmd.Flags().BoolVar(&o.ShowLabels, "show-labels", false, "When printing, show all labels as the last column (default hide labels column)")
    89  	// Todo: --sortBy supports custom field sorting, now `list` is to sort using the `.metadata.name` field in default
    90  	printer.AddOutputFlag(cmd, &o.Format)
    91  }
    92  
    93  func (o *ListOptions) Complete() error {
    94  	var err error
    95  	o.Namespace, _, err = o.Factory.ToRawKubeConfigLoader().Namespace()
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	o.ToPrinter = func(mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinterFunc, error) {
   101  		var p printers.ResourcePrinter
   102  		var kind schema.GroupKind
   103  		if mapping != nil {
   104  			kind = mapping.GroupVersionKind.GroupKind()
   105  		}
   106  
   107  		switch o.Format {
   108  		case printer.JSON:
   109  			p = &printers.JSONPrinter{}
   110  		case printer.YAML:
   111  			p = &printers.YAMLPrinter{}
   112  		case printer.Table:
   113  			p = printers.NewTablePrinter(printers.PrintOptions{
   114  				Kind:          kind,
   115  				Wide:          false,
   116  				WithNamespace: o.AllNamespaces,
   117  				ShowLabels:    o.ShowLabels,
   118  			})
   119  		case printer.Wide:
   120  			p = printers.NewTablePrinter(printers.PrintOptions{
   121  				Kind:          kind,
   122  				Wide:          true,
   123  				WithNamespace: o.AllNamespaces,
   124  				ShowLabels:    o.ShowLabels,
   125  			})
   126  		default:
   127  			return nil, genericclioptions.NoCompatiblePrinterError{AllowedFormats: printer.Formats()}
   128  		}
   129  
   130  		p, err = printers.NewTypeSetter(scheme.Scheme).WrapToPrinter(p, nil)
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  
   135  		if o.Format.IsHumanReadable() {
   136  			p = &cmdget.SortingPrinter{Delegate: p, SortField: o.SortBy}
   137  			p = &cmdget.TablePrinter{Delegate: p}
   138  		}
   139  		return p.PrintObj, nil
   140  	}
   141  
   142  	return nil
   143  }
   144  
   145  func (o *ListOptions) Run() (*resource.Result, error) {
   146  	if err := o.Complete(); err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	r := o.Factory.NewBuilder().
   151  		Unstructured().
   152  		NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces).
   153  		LabelSelectorParam(o.LabelSelector).
   154  		FieldSelectorParam(o.FieldSelector).
   155  		ResourceTypeOrNameArgs(true, append([]string{util.GVRToString(o.GVR)}, o.Names...)...).
   156  		ContinueOnError().
   157  		Latest().
   158  		Flatten().
   159  		TransformRequests(o.transformRequests).
   160  		Do()
   161  
   162  	if err := r.Err(); err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	// if Print is true, use default printer to print the result, otherwise, only return the result,
   167  	// the caller needs to implement its own printer function to output the result.
   168  	if o.Print {
   169  		return r, o.printResult(r)
   170  	} else {
   171  		return r, nil
   172  	}
   173  }
   174  
   175  func (o *ListOptions) transformRequests(req *rest.Request) {
   176  	if !o.Format.IsHumanReadable() || !o.Print {
   177  		return
   178  	}
   179  
   180  	req.SetHeader("Accept", strings.Join([]string{
   181  		fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName),
   182  		fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName),
   183  		"application/json",
   184  	}, ","))
   185  	if len(o.SortBy) > 0 {
   186  		req.Param("includeObject", "Object")
   187  	}
   188  }
   189  
   190  func (o *ListOptions) printResult(r *resource.Result) error {
   191  	if !o.Format.IsHumanReadable() {
   192  		return o.printGeneric(r)
   193  	}
   194  
   195  	var allErrs []error
   196  	errs := sets.NewString()
   197  	infos, err := r.Infos()
   198  	if err != nil {
   199  		allErrs = append(allErrs, err)
   200  	}
   201  
   202  	objs := make([]runtime.Object, len(infos))
   203  	for ix := range infos {
   204  		objs[ix] = infos[ix].Object
   205  	}
   206  
   207  	var printer printers.ResourcePrinter
   208  	var lastMapping *meta.RESTMapping
   209  
   210  	tracingWriter := &trackingWriterWrapper{Delegate: o.Out}
   211  	separatorWriter := &separatorWriterWrapper{Delegate: tracingWriter}
   212  
   213  	w := printers.GetNewTabWriter(separatorWriter)
   214  	allResourceNamespaced := !o.AllNamespaces
   215  	for ix := range objs {
   216  		info := infos[ix]
   217  		mapping := info.Mapping
   218  
   219  		allResourceNamespaced = allResourceNamespaced && info.Namespaced()
   220  		printWithNamespace := o.AllNamespaces
   221  
   222  		if mapping != nil && mapping.Scope.Name() == meta.RESTScopeNameRoot {
   223  			printWithNamespace = false
   224  		}
   225  
   226  		if shouldGetNewPrinterForMapping(printer, lastMapping, mapping) {
   227  			w.Flush()
   228  			w.SetRememberedWidths(nil)
   229  
   230  			if lastMapping != nil && tracingWriter.Written > 0 {
   231  				separatorWriter.SetReady(true)
   232  			}
   233  
   234  			printer, err = o.ToPrinter(mapping, printWithNamespace)
   235  			if err != nil {
   236  				if !errs.Has(err.Error()) {
   237  					errs.Insert(err.Error())
   238  					allErrs = append(allErrs, err)
   239  				}
   240  				continue
   241  			}
   242  
   243  			lastMapping = mapping
   244  		}
   245  
   246  		err = printer.PrintObj(info.Object, w)
   247  		if err != nil {
   248  			if !errs.Has(err.Error()) {
   249  				errs.Insert(err.Error())
   250  				allErrs = append(allErrs, err)
   251  			}
   252  		}
   253  	}
   254  
   255  	w.Flush()
   256  	if tracingWriter.Written == 0 && len(allErrs) == 0 {
   257  		o.PrintNotFoundResources()
   258  	}
   259  	return utilerrors.NewAggregate(allErrs)
   260  }
   261  
   262  type trackingWriterWrapper struct {
   263  	Delegate io.Writer
   264  	Written  int
   265  }
   266  
   267  func (t *trackingWriterWrapper) Write(p []byte) (n int, err error) {
   268  	t.Written += len(p)
   269  	return t.Delegate.Write(p)
   270  }
   271  
   272  type separatorWriterWrapper struct {
   273  	Delegate io.Writer
   274  	Ready    bool
   275  }
   276  
   277  func (s *separatorWriterWrapper) Write(p []byte) (n int, err error) {
   278  	// If we're about to write non-empty bytes and `s` is ready,
   279  	// we prepend an empty line to `p` and reset `s.Read`.
   280  	if len(p) != 0 && s.Ready {
   281  		fmt.Fprintln(s.Delegate)
   282  		s.Ready = false
   283  	}
   284  	return s.Delegate.Write(p)
   285  }
   286  
   287  func (s *separatorWriterWrapper) SetReady(state bool) {
   288  	s.Ready = state
   289  }
   290  
   291  func shouldGetNewPrinterForMapping(printer printers.ResourcePrinter, lastMapping, mapping *meta.RESTMapping) bool {
   292  	return printer == nil || lastMapping == nil || mapping == nil || mapping.Resource != lastMapping.Resource
   293  }
   294  
   295  // printGeneric copied from kubectl get.go
   296  func (o *ListOptions) printGeneric(r *resource.Result) error {
   297  	var errs []error
   298  
   299  	singleItemImplied := false
   300  	infos, err := r.IntoSingleItemImplied(&singleItemImplied).Infos()
   301  	if err != nil {
   302  		if singleItemImplied {
   303  			return err
   304  		}
   305  		errs = append(errs, err)
   306  	}
   307  
   308  	if len(infos) == 0 {
   309  		return utilerrors.Reduce(utilerrors.Flatten(utilerrors.NewAggregate(errs)))
   310  	}
   311  
   312  	printer, err := o.ToPrinter(nil, false)
   313  	if err != nil {
   314  		return err
   315  	}
   316  
   317  	var obj runtime.Object
   318  	if !singleItemImplied || len(infos) != 1 {
   319  		list := corev1.List{
   320  			TypeMeta: metav1.TypeMeta{
   321  				Kind:       "List",
   322  				APIVersion: "v1",
   323  			},
   324  			ListMeta: metav1.ListMeta{},
   325  		}
   326  
   327  		for _, info := range infos {
   328  			list.Items = append(list.Items, runtime.RawExtension{Object: info.Object})
   329  		}
   330  
   331  		listData, err := json.Marshal(list)
   332  		if err != nil {
   333  			return err
   334  		}
   335  
   336  		converted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, listData)
   337  		if err != nil {
   338  			return err
   339  		}
   340  
   341  		obj = converted
   342  	} else {
   343  		obj = infos[0].Object
   344  	}
   345  
   346  	isList := meta.IsListType(obj)
   347  	if isList {
   348  		items, err := meta.ExtractList(obj)
   349  		if err != nil {
   350  			return err
   351  		}
   352  
   353  		list := &unstructured.UnstructuredList{
   354  			Object: map[string]interface{}{
   355  				"kind":       "List",
   356  				"apiVersion": "v1",
   357  				"metadata":   map[string]interface{}{},
   358  			},
   359  		}
   360  		if listMeta, err := meta.ListAccessor(obj); err == nil {
   361  			list.Object["metadata"] = map[string]interface{}{
   362  				"resourceVersion": listMeta.GetResourceVersion(),
   363  			}
   364  		}
   365  
   366  		for _, item := range items {
   367  			list.Items = append(list.Items, *item.(*unstructured.Unstructured))
   368  		}
   369  		if err := printer.PrintObj(list, o.Out); err != nil {
   370  			errs = append(errs, err)
   371  		}
   372  		return utilerrors.Reduce(utilerrors.Flatten(utilerrors.NewAggregate(errs)))
   373  	}
   374  
   375  	if printErr := printer.PrintObj(obj, o.Out); printErr != nil {
   376  		errs = append(errs, printErr)
   377  	}
   378  
   379  	return utilerrors.Reduce(utilerrors.Flatten(utilerrors.NewAggregate(errs)))
   380  }
   381  
   382  func (o *ListOptions) PrintNotFoundResources() {
   383  	if !o.AllNamespaces {
   384  		fmt.Fprintf(o.ErrOut, "No %s found in %s namespace.\n", o.GVR.Resource, o.Namespace)
   385  	} else {
   386  		fmt.Fprintf(o.ErrOut, "No %s found\n", o.GVR.Resource)
   387  	}
   388  }