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:
parent
e7f17cb570
commit
3cb662b4be
10
go.mod
10
go.mod
|
|
@ -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
16
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue