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 }