JSON & YAML output for kubectl api-resources (#132604)

* Add JSON & YAML output support for kubectl api-resources

Create a separate `PrintFlags` struct within the apiresources.go file
that handles printing only for `kubetl api-resources` because existing
output formats, i.e., wide and name, are already implemented
independently from HumanReadableFlags and NamePrintFlags.

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

* Use separate printer type for all options

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

* Unit tests for JSON & YAML outputs

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

* Separate file for print types

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

* Move JSON-YAML tests to separate function

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

* Fix broken unit test

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

* Unifying JSON & YAML unit test functions

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

* Fix linter errors

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

* PR feedback and linter again

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

---------

Signed-off-by: Dharmit Shah <shahdharmit@gmail.com>

Kubernetes-commit: cb33accc8fc4d44e902da4926eee7b828c5e51ec
This commit is contained in:
Dharmit Shah 2025-07-24 19:16:28 +05:30 committed by Kubernetes Publisher
parent e7f17cb570
commit 3cb662b4be
6 changed files with 412 additions and 89 deletions

10
go.mod
View File

@ -29,11 +29,11 @@ require (
go.yaml.in/yaml/v2 v2.4.2
golang.org/x/sys v0.31.0
gopkg.in/evanphx/json-patch.v4 v4.12.0
k8s.io/api v0.0.0-20250718010531-33ab3a26f4b3
k8s.io/apimachinery v0.0.0-20250717210244-b92abb2d8139
k8s.io/api v0.0.0-20250724104226-1560b8c850a7
k8s.io/apimachinery v0.0.0-20250723005633-58c4eb072ebf
k8s.io/cli-runtime v0.0.0-20250717174531-64776d0a280f
k8s.io/client-go v0.0.0-20250718010928-be36413bbca7
k8s.io/component-base v0.0.0-20250717172125-4e07767df717
k8s.io/client-go v0.0.0-20250724144911-764374b3242b
k8s.io/component-base v0.0.0-20250724065244-07ee182722a1
k8s.io/component-helpers v0.0.0-20250717172249-5095859f5100
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
@ -95,4 +95,4 @@ require (
sigs.k8s.io/kustomize/api v0.19.0 // indirect
)
replace k8s.io/code-generator => k8s.io/code-generator v0.0.0-20250718051115-9eb96548a40e
replace k8s.io/code-generator => k8s.io/code-generator v0.0.0-20250722051953-bd6c0b14fb10

16
go.sum
View File

@ -200,16 +200,16 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.0.0-20250718010531-33ab3a26f4b3 h1:UnuyCQyBmdFlYypApF2w6Ld0R0kAt8b+0Lt9dYAr23I=
k8s.io/api v0.0.0-20250718010531-33ab3a26f4b3/go.mod h1:K8dwhtttsRR0RHeSRF8XQ77gfMgyAj3q78/TkxEXhoc=
k8s.io/apimachinery v0.0.0-20250717210244-b92abb2d8139 h1:jWBClrBPuk+GEA9pJzMa9IvxncSBbw7fmvey15nVm0w=
k8s.io/apimachinery v0.0.0-20250717210244-b92abb2d8139/go.mod h1:v1p1Jsze3IHLy5gU17yVqR2qLO7jgYeX6mw3HZy2AEU=
k8s.io/api v0.0.0-20250724104226-1560b8c850a7 h1:Y5FVeGr2IQ3fbv6GeQAeiZKIyBwxyr7Nv0tWJog9OEo=
k8s.io/api v0.0.0-20250724104226-1560b8c850a7/go.mod h1:70o+sJgHYrO2nPMKeEsmpidpEUMobuxHMhjK/ud9+og=
k8s.io/apimachinery v0.0.0-20250723005633-58c4eb072ebf h1:R1l0xAevbhH2Bg0iJuabo8/i9m31D1ehh2ZJPFKh9bc=
k8s.io/apimachinery v0.0.0-20250723005633-58c4eb072ebf/go.mod h1:v1p1Jsze3IHLy5gU17yVqR2qLO7jgYeX6mw3HZy2AEU=
k8s.io/cli-runtime v0.0.0-20250717174531-64776d0a280f h1:E/GB1lzzKbz3HPJ6Zu1bJYrey6oDAIAA+RMEozCpPpU=
k8s.io/cli-runtime v0.0.0-20250717174531-64776d0a280f/go.mod h1:SybB6wdHGt8FXxaHyNQqsUAhWcZKIDPurWPB5mfFLD0=
k8s.io/client-go v0.0.0-20250718010928-be36413bbca7 h1:LNOJkn+3JlAEzdZzYheQM97gq6kKQfkrBN0GikI5nbc=
k8s.io/client-go v0.0.0-20250718010928-be36413bbca7/go.mod h1:a14VvgYhux7oUSE9mWdzBuFKDZSGtperboMjQ1JtVgc=
k8s.io/component-base v0.0.0-20250717172125-4e07767df717 h1:07oqkM0FzuGUw/bJw2rJubzccG7ShpGcTJ7SBDGp5Fc=
k8s.io/component-base v0.0.0-20250717172125-4e07767df717/go.mod h1:/ehREU84M2OxVgU8WfxuUIi4/c5XsT6rIsEGQfhgxEQ=
k8s.io/client-go v0.0.0-20250724144911-764374b3242b h1:7RltffV2NgCQA9jJNMHs2Xnn/vFTNHj+MFY34v6dg7w=
k8s.io/client-go v0.0.0-20250724144911-764374b3242b/go.mod h1:cgK+6wG3u4eub4z04TjSa9Y2WD320ZageTzIr5+E5Cg=
k8s.io/component-base v0.0.0-20250724065244-07ee182722a1 h1:OSPZY89+U2JKPaQnluxgzhsbY+L+gG/cxgYVFY5gYA0=
k8s.io/component-base v0.0.0-20250724065244-07ee182722a1/go.mod h1:w6VkDvQYhgRcM0VZp+pAMPfhFPflGqAAik6tDGgIyq0=
k8s.io/component-helpers v0.0.0-20250717172249-5095859f5100 h1:XEHmjwZgMNRuVgpqaRH/RR+n4BU0evfitU0RpWGPMUM=
k8s.io/component-helpers v0.0.0-20250717172249-5095859f5100/go.mod h1:yxuY+YMknW7H9Bj7B29INyMOacJBa6oEG7gi7IKUzEQ=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=

View File

@ -23,10 +23,11 @@ import (
"strings"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions"
@ -61,12 +62,10 @@ var (
// APIResourceOptions is the start of the data required to perform the operation.
// As new fields are added, add them here instead of referencing the cmd.Flags()
type APIResourceOptions struct {
Output string
SortBy string
APIGroup string
Namespaced bool
Verbs []string
NoHeaders bool
Cached bool
Categories []string
@ -76,13 +75,8 @@ type APIResourceOptions struct {
discoveryClient discovery.CachedDiscoveryInterface
genericiooptions.IOStreams
}
// groupResource contains the APIGroup and APIResource
type groupResource struct {
APIGroup string
APIGroupVersion string
APIResource metav1.APIResource
PrintFlags *PrintFlags
PrintObj printers.ResourcePrinterFunc
}
// NewAPIResourceOptions creates the options for APIResource
@ -90,6 +84,7 @@ func NewAPIResourceOptions(ioStreams genericiooptions.IOStreams) *APIResourceOpt
return &APIResourceOptions{
IOStreams: ioStreams,
Namespaced: true,
PrintFlags: NewPrintFlags(),
}
}
@ -109,8 +104,7 @@ func NewCmdAPIResources(restClientGetter genericclioptions.RESTClientGetter, ioS
},
}
cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).")
cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, `Output format. One of: (wide, name).`)
o.PrintFlags.AddFlags(cmd)
cmd.Flags().StringVar(&o.APIGroup, "api-group", o.APIGroup, "Limit to resources in the specified API group.")
cmd.Flags().BoolVar(&o.Namespaced, "namespaced", o.Namespaced, "If false, non-namespaced resources will be returned, otherwise returning namespaced resources by default.")
@ -123,10 +117,6 @@ func NewCmdAPIResources(restClientGetter genericclioptions.RESTClientGetter, ioS
// Validate checks to the APIResourceOptions to see if there is sufficient information run the command
func (o *APIResourceOptions) Validate() error {
supportedOutputTypes := sets.New[string]("", "wide", "name")
if !supportedOutputTypes.Has(o.Output) {
return fmt.Errorf("--output %v is not available", o.Output)
}
supportedSortTypes := sets.New[string]("", "name", "kind")
if len(o.SortBy) > 0 {
if !supportedSortTypes.Has(o.SortBy) {
@ -151,6 +141,28 @@ func (o *APIResourceOptions) Complete(restClientGetter genericclioptions.RESTCli
o.groupChanged = cmd.Flags().Changed("api-group")
o.nsChanged = cmd.Flags().Changed("namespaced")
var printer printers.ResourcePrinter
if o.PrintFlags.OutputFormat != nil {
printer, err = o.PrintFlags.ToPrinter()
if err != nil {
return err
}
o.PrintObj = func(object runtime.Object, out io.Writer) error {
errs := []error{}
if !*o.PrintFlags.NoHeaders &&
(o.PrintFlags.OutputFormat == nil || *o.PrintFlags.OutputFormat == "" || *o.PrintFlags.OutputFormat == "wide") {
if err = printContextHeaders(out, *o.PrintFlags.OutputFormat); err != nil {
errs = append(errs, err)
}
}
if err := printer.PrintObj(object, out); err != nil {
errs = append(errs, err)
}
return utilerrors.NewAggregate(errs)
}
}
return nil
}
@ -170,7 +182,7 @@ func (o *APIResourceOptions) RunAPIResources() error {
errs = append(errs, err)
}
resources := []groupResource{}
var allResources []*metav1.APIResourceList
for _, list := range lists {
if len(list.APIResources) == 0 {
@ -180,6 +192,14 @@ func (o *APIResourceOptions) RunAPIResources() error {
if err != nil {
continue
}
apiList := &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
Kind: "APIResourceList",
APIVersion: "v1",
},
GroupVersion: gv.String(),
}
var apiResources []metav1.APIResource
for _, resource := range list.APIResources {
if len(resource.Verbs) == 0 {
continue
@ -200,58 +220,32 @@ func (o *APIResourceOptions) RunAPIResources() error {
if len(o.Categories) > 0 && !sets.New[string](resource.Categories...).HasAll(o.Categories...) {
continue
}
resources = append(resources, groupResource{
APIGroup: gv.Group,
APIGroupVersion: gv.String(),
APIResource: resource,
})
// set these because we display a concatenation of these two values under APIVERSION column of human-readable output
resource.Group = gv.Group
resource.Version = gv.Version
apiResources = append(apiResources, resource)
}
apiList.APIResources = apiResources
allResources = append(allResources, apiList)
}
if o.NoHeaders == false && o.Output != "name" {
if err = printContextHeaders(w, o.Output); err != nil {
return err
}
flatList := &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
APIVersion: allResources[0].APIVersion,
Kind: allResources[0].Kind,
},
}
for _, resource := range allResources {
flatList.APIResources = append(flatList.APIResources, resource.APIResources...)
}
sort.Stable(sortableResource{resources, o.SortBy})
for _, r := range resources {
switch o.Output {
case "name":
name := r.APIResource.Name
if len(r.APIGroup) > 0 {
name += "." + r.APIGroup
}
if _, err := fmt.Fprintf(w, "%s\n", name); err != nil {
errs = append(errs, err)
}
case "wide":
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\t%v\t%v\n",
r.APIResource.Name,
strings.Join(r.APIResource.ShortNames, ","),
r.APIGroupVersion,
r.APIResource.Namespaced,
r.APIResource.Kind,
strings.Join(r.APIResource.Verbs, ","),
strings.Join(r.APIResource.Categories, ",")); err != nil {
errs = append(errs, err)
}
case "":
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n",
r.APIResource.Name,
strings.Join(r.APIResource.ShortNames, ","),
r.APIGroupVersion,
r.APIResource.Namespaced,
r.APIResource.Kind); err != nil {
errs = append(errs, err)
}
}
}
sort.Stable(sortableResource{flatList.APIResources, o.SortBy})
if len(errs) > 0 {
return errors.NewAggregate(errs)
err = o.PrintObj(flatList, w)
if err != nil {
errs = append(errs, err)
}
return nil
return utilerrors.NewAggregate(errs)
}
func printContextHeaders(out io.Writer, output string) error {
@ -264,7 +258,7 @@ func printContextHeaders(out io.Writer, output string) error {
}
type sortableResource struct {
resources []groupResource
resources []metav1.APIResource
sortBy string
}
@ -277,7 +271,7 @@ func (s sortableResource) Less(i, j int) bool {
if ret > 0 {
return false
} else if ret == 0 {
return strings.Compare(s.resources[i].APIResource.Name, s.resources[j].APIResource.Name) < 0
return strings.Compare(s.resources[i].Name, s.resources[j].Name) < 0
}
return true
}
@ -285,9 +279,9 @@ func (s sortableResource) Less(i, j int) bool {
func (s sortableResource) compareValues(i, j int) (string, string) {
switch s.sortBy {
case "name":
return s.resources[i].APIResource.Name, s.resources[j].APIResource.Name
return s.resources[i].Name, s.resources[j].Name
case "kind":
return s.resources[i].APIResource.Kind, s.resources[j].APIResource.Kind
return s.resources[i].Kind, s.resources[j].Kind
}
return s.resources[i].APIGroup, s.resources[j].APIGroup
return s.resources[i].Group, s.resources[j].Group
}

View File

@ -17,13 +17,16 @@ limitations under the License.
package apiresources
import (
"encoding/json"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
apiequality "k8s.io/apimachinery/pkg/api/equality"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericiooptions"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
"sigs.k8s.io/yaml"
)
func TestAPIResourcesComplete(t *testing.T) {
@ -48,6 +51,16 @@ See 'kubectl api-resources -h' for help and examples`
if err.Error() != expectedError {
t.Fatalf("Unexpected error: %v\n expected: %v", err, expectedError)
}
*o.PrintFlags.OutputFormat = "foo"
err = o.Complete(tf, cmd, []string{})
if err == nil {
t.Fatalf("An error was expected but not returned")
}
expectedError = `unable to match a printer suitable for the output format "foo", allowed formats are: json,name,wide,yaml`
if err.Error() != expectedError {
t.Fatalf("Unexpected error: %v\n expected: %v", err, expectedError)
}
}
func TestAPIResourcesValidate(t *testing.T) {
@ -61,13 +74,6 @@ func TestAPIResourcesValidate(t *testing.T) {
optionSetupFn: func(o *APIResourceOptions) {},
expectedError: "",
},
{
name: "invalid output",
optionSetupFn: func(o *APIResourceOptions) {
o.Output = "foo"
},
expectedError: "--output foo is not available",
},
{
name: "invalid sort by",
optionSetupFn: func(o *APIResourceOptions) {
@ -322,3 +328,92 @@ bazzes b somegroup/v1 true Baz
})
}
}
// TestAPIResourcesRunJsonYaml is doing same thing as TestAPIResourcesRun but for JSON and YAML outputs
// A separate test function is created because we are using apieqaulity.Semantic.DeepEqual
// to check equality between input and output
func TestAPIResourcesRunJsonYaml(t *testing.T) {
dc := cmdtesting.NewFakeCachedDiscoveryClient()
tf := cmdtesting.NewTestFactory().WithDiscoveryClient(dc)
defer tf.Cleanup()
testCases := []struct {
name string
expectedInvalidations int
preferredResources []*v1.APIResourceList
}{
{
name: "one",
preferredResources: []*v1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []v1.APIResource{
{
Name: "foos",
Namespaced: false,
Kind: "Foo",
Verbs: []string{"get", "list"},
ShortNames: []string{"f", "fo"},
Categories: []string{"some-category"},
},
},
},
},
},
{
name: "two",
preferredResources: []*v1.APIResourceList{
{
GroupVersion: "somegroup/v1",
APIResources: []v1.APIResource{
{
Name: "bazzes",
Namespaced: true,
Kind: "Baz",
Verbs: []string{"get", "list", "create", "delete"},
ShortNames: []string{"b"},
Categories: []string{"some-category", "another-category"},
},
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(tt *testing.T) {
dc.PreferredResources = tc.preferredResources
ioStreams, _, out, errOut := genericiooptions.NewTestIOStreams()
for _, v := range []string{"json", "yaml"} {
cmd := NewCmdAPIResources(tf, ioStreams)
err := cmd.Flags().Set("output", v)
require.NoError(tt, err)
cmd.Run(cmd, []string{})
if errOut.Len() > 0 {
t.Fatalf("unexpected error output: %s", errOut.String())
}
apiResourceList := v1.APIResourceList{}
switch v {
case "json":
err = json.Unmarshal(out.Bytes(), &apiResourceList)
case "yaml":
err = yaml.Unmarshal(out.Bytes(), &apiResourceList)
}
require.NoError(tt, err)
// this will undo custom value we add in RunAPIResources in the lines:
// resource.Group = gv.Group
// resource.Version = gv.Version
apiResourceList.GroupVersion = apiResourceList.APIResources[0].Group + "/" + apiResourceList.APIResources[0].Version
apiResourceList.APIResources[0].Version = ""
apiResourceList.APIResources[0].Group = ""
if !apiequality.Semantic.DeepEqual(tc.preferredResources[0].APIResources[0], apiResourceList.APIResources[0]) {
tt.Fatalf("expected output: [%v]\n, but got [%v]", tc.preferredResources[0].APIResources[0], apiResourceList.APIResources[0])
}
}
})
}
}

View File

@ -0,0 +1,233 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apiresources
import (
"fmt"
"io"
"strings"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/printers"
)
type PrintFlags struct {
JSONYamlPrintFlags *genericclioptions.JSONYamlPrintFlags
NamePrintFlags NamePrintFlags
HumanReadableFlags HumanPrintFlags
NoHeaders *bool
OutputFormat *string
}
func NewPrintFlags() *PrintFlags {
outputFormat := ""
noHeaders := false
return &PrintFlags{
OutputFormat: &outputFormat,
NoHeaders: &noHeaders,
JSONYamlPrintFlags: genericclioptions.NewJSONYamlPrintFlags(),
NamePrintFlags: APIResourcesNewNamePrintFlags(),
HumanReadableFlags: APIResourcesHumanReadableFlags(),
}
}
func (f *PrintFlags) AddFlags(cmd *cobra.Command) {
f.JSONYamlPrintFlags.AddFlags(cmd)
f.HumanReadableFlags.AddFlags(cmd)
f.NamePrintFlags.AddFlags(cmd)
if f.OutputFormat != nil {
cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf("Output format. One of: (%s).", strings.Join(f.AllowedFormats(), ", ")))
}
if f.NoHeaders != nil {
cmd.Flags().BoolVar(f.NoHeaders, "no-headers", *f.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).")
}
}
// PrintOptions struct defines a struct for various print options
type PrintOptions struct {
SortBy *string
NoHeaders bool
Wide bool
}
type HumanPrintFlags struct {
SortBy *string
NoHeaders bool
}
func (f *HumanPrintFlags) AllowedFormats() []string {
return []string{"wide"}
}
// AddFlags receives a *cobra.Command reference and binds
// flags related to human-readable printing to it
func (f *HumanPrintFlags) AddFlags(c *cobra.Command) {
if f.SortBy != nil {
c.Flags().StringVar(f.SortBy, "sort-by", *f.SortBy, "If non-empty, sort list types using this field specification. The field specification is expressed as a JSONPath expression (e.g. '{.metadata.name}'). The field in the API resource specified by this JSONPath expression must be an integer or a string.")
}
}
// ToPrinter receives an outputFormat and returns a printer capable of
// handling human-readable output.
func (f *HumanPrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) {
if len(outputFormat) > 0 && outputFormat != "wide" {
return nil, genericclioptions.NoCompatiblePrinterError{Options: f, AllowedFormats: f.AllowedFormats()}
}
p := HumanReadablePrinter{
options: PrintOptions{
NoHeaders: f.NoHeaders,
Wide: outputFormat == "wide",
},
}
return p, nil
}
type HumanReadablePrinter struct {
options PrintOptions
}
func (f HumanReadablePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
flatList, ok := obj.(*metav1.APIResourceList)
if !ok {
return fmt.Errorf("object is not a APIResourceList")
}
var errs []error
for _, r := range flatList.APIResources {
gv, err := schema.ParseGroupVersion(strings.Join([]string{r.Group, r.Version}, "/"))
if err != nil {
errs = append(errs, err)
continue
}
if f.options.Wide {
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\t%v\t%v\n",
r.Name,
strings.Join(r.ShortNames, ","),
gv.String(),
r.Namespaced,
r.Kind,
strings.Join(r.Verbs, ","),
strings.Join(r.Categories, ",")); err != nil {
errs = append(errs, err)
}
continue
}
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n",
r.Name,
strings.Join(r.ShortNames, ","),
gv.String(),
r.Namespaced,
r.Kind); err != nil {
errs = append(errs, err)
}
}
return utilerrors.NewAggregate(errs)
}
type NamePrintFlags struct{}
func APIResourcesNewNamePrintFlags() NamePrintFlags {
return NamePrintFlags{}
}
func (f *NamePrintFlags) AllowedFormats() []string {
return []string{"name"}
}
// AddFlags receives a *cobra.Command reference and binds
// flags related to name printing to it
func (f *NamePrintFlags) AddFlags(_ *cobra.Command) {}
// ToPrinter receives an outputFormat and returns a printer capable of
// handling human-readable output.
func (f *NamePrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) {
if outputFormat == "name" {
return NamePrinter{}, nil
}
return nil, genericclioptions.NoCompatiblePrinterError{Options: f, AllowedFormats: f.AllowedFormats()}
}
type NamePrinter struct{}
func (f NamePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
flatList, ok := obj.(*metav1.APIResourceList)
if !ok {
return fmt.Errorf("object is not a APIResourceList")
}
var errs []error
for _, r := range flatList.APIResources {
name := r.Name
if len(r.Group) > 0 {
name += "." + r.Group
}
if _, err := fmt.Fprintf(w, "%s\n", name); err != nil {
errs = append(errs, err)
}
}
return utilerrors.NewAggregate(errs)
}
func APIResourcesHumanReadableFlags() HumanPrintFlags {
return HumanPrintFlags{
SortBy: nil,
NoHeaders: false,
}
}
func (f *PrintFlags) AllowedFormats() []string {
ret := []string{}
ret = append(ret, f.JSONYamlPrintFlags.AllowedFormats()...)
ret = append(ret, f.NamePrintFlags.AllowedFormats()...)
ret = append(ret, f.HumanReadableFlags.AllowedFormats()...)
return ret
}
func (f *PrintFlags) ToPrinter() (printers.ResourcePrinter, error) {
outputFormat := ""
if f.OutputFormat != nil {
outputFormat = *f.OutputFormat
}
noHeaders := false
if f.NoHeaders != nil {
noHeaders = *f.NoHeaders
}
f.HumanReadableFlags.NoHeaders = noHeaders
if p, err := f.JSONYamlPrintFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) {
return p, err
}
if p, err := f.HumanReadableFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) {
return p, err
}
if p, err := f.NamePrintFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) {
return p, err
}
return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: f.AllowedFormats()}
}

View File

@ -25,6 +25,7 @@ import (
"time"
"github.com/spf13/cobra"
"k8s.io/utils/ptr"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
@ -321,7 +322,7 @@ func compGetResourceList(restClientGetter genericclioptions.RESTClientGetter, cm
o.Complete(restClientGetter, cmd, nil)
// Get the list of resources
o.Output = "name"
o.PrintFlags.OutputFormat = ptr.To("name")
o.Cached = true
o.Verbs = []string{"get"}
// TODO:Should set --request-timeout=5s