diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources_test.go new file mode 100644 index 00000000000..c17782362c2 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources_test.go @@ -0,0 +1,299 @@ +/* +Copyright 2022 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 ( + "testing" + + "github.com/spf13/cobra" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +func TestAPIResourcesComplete(t *testing.T) { + tf := cmdtesting.NewTestFactory() + defer tf.Cleanup() + cmd := NewCmdAPIResources(tf, genericclioptions.NewTestIOStreamsDiscard()) + parentCmd := &cobra.Command{Use: "kubectl"} + parentCmd.AddCommand(cmd) + o := NewAPIResourceOptions(genericclioptions.NewTestIOStreamsDiscard()) + + err := o.Complete(cmd, []string{}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + err = o.Complete(cmd, []string{"foo"}) + if err == nil { + t.Fatalf("An error was expected but not returned") + } + expectedError := `unexpected arguments: [foo] +See 'kubectl api-resources -h' for help and examples` + if err.Error() != expectedError { + t.Fatalf("Unexpected error: %v\n expected: %v", err, expectedError) + } +} + +func TestAPIResourcesValidate(t *testing.T) { + testCases := []struct { + name string + optionSetupFn func(o *APIResourceOptions) + expectedError string + }{ + { + name: "no errors", + 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) { + o.SortBy = "foo" + }, + expectedError: "--sort-by accepts only name or kind", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(tt *testing.T) { + o := NewAPIResourceOptions(genericclioptions.NewTestIOStreamsDiscard()) + tc.optionSetupFn(o) + err := o.Validate() + if tc.expectedError == "" { + if err != nil { + tt.Fatalf("Unexpected error: %v", err) + } + } else { + if err == nil { + tt.Fatalf("An error was expected but not returned") + } + if err.Error() != tc.expectedError { + tt.Fatalf("Unexpected error: %v, expected: %v", err, tc.expectedError) + } + } + }) + } +} + +func TestAPIResourcesRun(t *testing.T) { + dc := cmdtesting.NewFakeCachedDiscoveryClient() + dc.PreferredResources = []*v1.APIResourceList{ + { + GroupVersion: "v1", + APIResources: []v1.APIResource{ + { + Name: "foos", + Namespaced: false, + Kind: "Foo", + Verbs: []string{"get", "list"}, + ShortNames: []string{"f", "fo"}, + }, + { + Name: "bars", + Namespaced: true, + Kind: "Bar", + Verbs: []string{"get", "list", "create"}, + ShortNames: []string{}, + }, + }, + }, + { + GroupVersion: "somegroup/v1", + APIResources: []v1.APIResource{ + { + Name: "bazzes", + Namespaced: true, + Kind: "Baz", + Verbs: []string{"get", "list", "create", "delete"}, + ShortNames: []string{"b"}, + }, + { + Name: "NoVerbs", + Namespaced: true, + Kind: "NoVerbs", + Verbs: []string{}, + ShortNames: []string{"b"}, + }, + }, + }, + { + GroupVersion: "someothergroup/v1", + APIResources: []v1.APIResource{}, + }, + } + tf := cmdtesting.NewTestFactory().WithDiscoveryClient(dc) + defer tf.Cleanup() + + testCases := []struct { + name string + commandSetupFn func(cmd *cobra.Command) + expectedOutput string + expectedInvalidations int + }{ + { + name: "defaults", + commandSetupFn: func(cmd *cobra.Command) {}, + expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND +bars v1 true Bar +foos f,fo v1 false Foo +bazzes b somegroup/v1 true Baz +`, + expectedInvalidations: 1, + }, + { + name: "no headers", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("no-headers", "true") + }, + expectedOutput: `bars v1 true Bar +foos f,fo v1 false Foo +bazzes b somegroup/v1 true Baz +`, + expectedInvalidations: 1, + }, + { + name: "specific api group", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("api-group", "somegroup") + }, + expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND +bazzes b somegroup/v1 true Baz +`, + expectedInvalidations: 1, + }, + { + name: "output wide", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("output", "wide") + }, + expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND VERBS +bars v1 true Bar [get list create] +foos f,fo v1 false Foo [get list] +bazzes b somegroup/v1 true Baz [get list create delete] +`, + expectedInvalidations: 1, + }, + { + name: "output name", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("output", "name") + }, + expectedOutput: `bars +foos +bazzes.somegroup +`, + expectedInvalidations: 1, + }, + { + name: "namespaced", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("namespaced", "true") + }, + expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND +bars v1 true Bar +bazzes b somegroup/v1 true Baz +`, + expectedInvalidations: 1, + }, + { + name: "single verb", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("verbs", "create") + }, + expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND +bars v1 true Bar +bazzes b somegroup/v1 true Baz +`, + expectedInvalidations: 1, + }, + { + name: "multiple verbs", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("verbs", "create,delete") + }, + expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND +bazzes b somegroup/v1 true Baz +`, + expectedInvalidations: 1, + }, + { + name: "sort by name", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("sort-by", "name") + }, + expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND +bars v1 true Bar +bazzes b somegroup/v1 true Baz +foos f,fo v1 false Foo +`, + expectedInvalidations: 1, + }, + { + name: "sort by kind", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("sort-by", "kind") + }, + expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND +bars v1 true Bar +bazzes b somegroup/v1 true Baz +foos f,fo v1 false Foo +`, + expectedInvalidations: 1, + }, + { + name: "cached", + commandSetupFn: func(cmd *cobra.Command) { + cmd.Flags().Set("cached", "true") + }, + expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND +bars v1 true Bar +foos f,fo v1 false Foo +bazzes b somegroup/v1 true Baz +`, + expectedInvalidations: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(tt *testing.T) { + dc.Invalidations = 0 + ioStreams, _, out, errOut := genericclioptions.NewTestIOStreams() + cmd := NewCmdAPIResources(tf, ioStreams) + tc.commandSetupFn(cmd) + cmd.Run(cmd, []string{}) + + if errOut.Len() > 0 { + t.Fatalf("unexpected error output: %s", errOut.String()) + } + if out.String() != tc.expectedOutput { + tt.Fatalf("unexpected output: %s\nexpected: %s", out.String(), tc.expectedOutput) + } + if dc.Invalidations != tc.expectedInvalidations { + tt.Fatalf("unexpected invalidations: %d, expected: %d", dc.Invalidations, tc.expectedInvalidations) + } + }) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiversions_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiversions_test.go new file mode 100644 index 00000000000..bdf4b9dae0c --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiversions_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2022 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 ( + "github.com/spf13/cobra" + "testing" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +func TestAPIVersionsComplete(t *testing.T) { + tf := cmdtesting.NewTestFactory() + defer tf.Cleanup() + cmd := NewCmdAPIVersions(tf, genericclioptions.NewTestIOStreamsDiscard()) + parentCmd := &cobra.Command{Use: "kubectl"} + parentCmd.AddCommand(cmd) + o := NewAPIVersionsOptions(genericclioptions.NewTestIOStreamsDiscard()) + + err := o.Complete(tf, cmd, []string{}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + err = o.Complete(tf, cmd, []string{"foo"}) + if err == nil { + t.Fatalf("An error was expected but not returned") + } + expectedError := `unexpected arguments: [foo] +See 'kubectl api-versions -h' for help and examples` + if err.Error() != expectedError { + t.Fatalf("Unexpected error: %v\n expected: %v", err, expectedError) + } +} + +func TestAPIVersionsRun(t *testing.T) { + dc := cmdtesting.NewFakeCachedDiscoveryClient() + dc.Groups = []*v1.APIGroup{ + { + Name: "", + Versions: []v1.GroupVersionForDiscovery{ + {GroupVersion: "v1"}, + }, + }, + { + Name: "foo", + Versions: []v1.GroupVersionForDiscovery{ + {GroupVersion: "foo/v1beta1"}, + {GroupVersion: "foo/v1"}, + {GroupVersion: "foo/v2"}, + }, + }, + { + Name: "bar", + Versions: []v1.GroupVersionForDiscovery{ + {GroupVersion: "bar/v1"}, + }, + }, + } + tf := cmdtesting.NewTestFactory().WithDiscoveryClient(dc) + defer tf.Cleanup() + + ioStreams, _, out, errOut := genericclioptions.NewTestIOStreams() + cmd := NewCmdAPIVersions(tf, ioStreams) + cmd.Run(cmd, []string{}) + + if errOut.Len() > 0 { + t.Fatalf("unexpected error output: %s", errOut.String()) + } + + expectedOutput := `bar/v1 +foo/v1 +foo/v1beta1 +foo/v2 +v1 +` + if out.String() != expectedOutput { + t.Fatalf("unexpected output: %s\nexpected: %s", out.String(), expectedOutput) + } + + expectedInvalidations := 1 + if dc.Invalidations != expectedInvalidations { + t.Fatalf("unexpected invalidations: %d, expected: %d", dc.Invalidations, expectedInvalidations) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go b/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go index c414ad41780..34ef091e294 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go @@ -364,19 +364,45 @@ func AddToScheme(scheme *runtime.Scheme) (meta.RESTMapper, runtime.Codec) { return mapper, codec } -type fakeCachedDiscoveryClient struct { +type FakeCachedDiscoveryClient struct { discovery.DiscoveryInterface + Groups []*metav1.APIGroup + Resources []*metav1.APIResourceList + PreferredResources []*metav1.APIResourceList + Invalidations int } -func (d *fakeCachedDiscoveryClient) Fresh() bool { +func NewFakeCachedDiscoveryClient() *FakeCachedDiscoveryClient { + return &FakeCachedDiscoveryClient{ + Groups: []*metav1.APIGroup{}, + Resources: []*metav1.APIResourceList{}, + PreferredResources: []*metav1.APIResourceList{}, + Invalidations: 0, + } +} + +func (d *FakeCachedDiscoveryClient) Fresh() bool { return true } -func (d *fakeCachedDiscoveryClient) Invalidate() { +func (d *FakeCachedDiscoveryClient) Invalidate() { + d.Invalidations++ } -func (d *fakeCachedDiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { - return []*metav1.APIGroup{}, []*metav1.APIResourceList{}, nil +func (d *FakeCachedDiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { + return d.Groups, d.Resources, nil +} + +func (d *FakeCachedDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) { + groupList := &metav1.APIGroupList{Groups: []metav1.APIGroup{}} + for _, g := range d.Groups { + groupList.Groups = append(groupList.Groups, *g) + } + return groupList, nil +} + +func (d *FakeCachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) { + return d.PreferredResources, nil } // TestFactory extends cmdutil.Factory @@ -447,6 +473,11 @@ func (f *TestFactory) WithClientConfig(clientConfig clientcmd.ClientConfig) *Tes return f } +func (f *TestFactory) WithDiscoveryClient(discoveryClient discovery.CachedDiscoveryInterface) *TestFactory { + f.kubeConfigFlags.WithDiscoveryClient(discoveryClient) + return f +} + // Cleanup cleans up TestFactory temp config file func (f *TestFactory) Cleanup() { if f.tempConfigFile == nil { @@ -618,7 +649,7 @@ func testRESTMapper() meta.RESTMapper { }, } - fakeDs := &fakeCachedDiscoveryClient{} + fakeDs := NewFakeCachedDiscoveryClient() expander := restmapper.NewShortcutExpander(mapper, fakeDs) return expander }