diff --git a/pkg/kubectl/cmd/util/factory_object_mapping.go b/pkg/kubectl/cmd/util/factory_object_mapping.go index 8f946304f2e9..b2413ddf66cb 100644 --- a/pkg/kubectl/cmd/util/factory_object_mapping.go +++ b/pkg/kubectl/cmd/util/factory_object_mapping.go @@ -119,17 +119,22 @@ func (f *ring1Factory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectType } func (f *ring1Factory) CategoryExpander() resource.CategoryExpander { - var categoryExpander resource.CategoryExpander - categoryExpander = resource.LegacyCategoryExpander + legacyExpander := resource.LegacyCategoryExpander + discoveryClient, err := f.clientAccessFactory.DiscoveryClient() if err == nil { - // wrap with discovery based filtering - categoryExpander, err = resource.NewDiscoveryFilteredExpander(categoryExpander, discoveryClient) - // you only have an error on missing discoveryClient, so this shouldn't fail. Check anyway. + // fallback is the legacy expander wrapped with discovery based filtering + fallbackExpander, err := resource.NewDiscoveryFilteredExpander(legacyExpander, discoveryClient) CheckErr(err) + + // by default use the expander that discovers based on "categories" field from the API + discoveryCategoryExpander, err := resource.NewDiscoveryCategoryExpander(fallbackExpander, discoveryClient) + CheckErr(err) + + return discoveryCategoryExpander } - return categoryExpander + return legacyExpander } func (f *ring1Factory) ClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) { diff --git a/pkg/kubectl/resource/BUILD b/pkg/kubectl/resource/BUILD index 1858e130ba40..441d31133c67 100644 --- a/pkg/kubectl/resource/BUILD +++ b/pkg/kubectl/resource/BUILD @@ -60,7 +60,9 @@ go_test( "//pkg/api:go_default_library", "//pkg/api/testapi:go_default_library", "//pkg/api/testing:go_default_library", + "//vendor/github.com/emicklei/go-restful-swagger12:go_default_library", "//vendor/github.com/ghodss/yaml:go_default_library", + "//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", @@ -72,7 +74,10 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/serializer/streaming:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/version:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", + "//vendor/k8s.io/client-go/discovery:go_default_library", + "//vendor/k8s.io/client-go/rest:go_default_library", "//vendor/k8s.io/client-go/rest/fake:go_default_library", "//vendor/k8s.io/client-go/rest/watch:go_default_library", "//vendor/k8s.io/client-go/util/testing:go_default_library", diff --git a/pkg/kubectl/resource/categories.go b/pkg/kubectl/resource/categories.go index 9c602a996c25..bee08a05c530 100644 --- a/pkg/kubectl/resource/categories.go +++ b/pkg/kubectl/resource/categories.go @@ -17,8 +17,6 @@ limitations under the License. package resource import ( - "errors" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" ) @@ -36,6 +34,58 @@ func (e SimpleCategoryExpander) Expand(category string) ([]schema.GroupResource, return ret, ok } +type discoveryCategoryExpander struct { + fallbackExpander CategoryExpander + discoveryClient discovery.DiscoveryInterface +} + +// NewDiscoveryCategoryExpander returns a category expander that makes use of the "categories" fields from +// the API, found through the discovery client. In case of any error or no category found (which likely +// means we're at a cluster prior to categories support, fallback to the expander provided. +func NewDiscoveryCategoryExpander(fallbackExpander CategoryExpander, client discovery.DiscoveryInterface) (discoveryCategoryExpander, error) { + if client == nil { + panic("Please provide discovery client to shortcut expander") + } + return discoveryCategoryExpander{fallbackExpander: fallbackExpander, discoveryClient: client}, nil +} + +func (e discoveryCategoryExpander) Expand(category string) ([]schema.GroupResource, bool) { + apiResourceLists, _ := e.discoveryClient.ServerResources() + if len(apiResourceLists) == 0 { + return e.fallbackExpander.Expand(category) + } + + discoveredExpansions := map[string][]schema.GroupResource{} + + for _, apiResourceList := range apiResourceLists { + gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) + if err != nil { + return e.fallbackExpander.Expand(category) + } + + for _, apiResource := range apiResourceList.APIResources { + if categories := apiResource.Categories; len(categories) > 0 { + for _, category := range categories { + groupResource := schema.GroupResource{ + Group: gv.Group, + Resource: apiResource.Name, + } + discoveredExpansions[category] = append(discoveredExpansions[category], groupResource) + } + } + } + } + + if len(discoveredExpansions) == 0 { + // We don't know if the server really don't have any resource with categories, + // or we're on a cluster version prior to categories support. Anyways, fallback. + return e.fallbackExpander.Expand(category) + } + + ret, ok := discoveredExpansions[category] + return ret, ok +} + type discoveryFilteredExpander struct { delegate CategoryExpander @@ -46,7 +96,7 @@ type discoveryFilteredExpander struct { // what the server has available func NewDiscoveryFilteredExpander(delegate CategoryExpander, client discovery.DiscoveryInterface) (discoveryFilteredExpander, error) { if client == nil { - return discoveryFilteredExpander{}, errors.New("Please provide discovery client to shortcut expander") + panic("Please provide discovery client to shortcut expander") } return discoveryFilteredExpander{delegate: delegate, discoveryClient: client}, nil } diff --git a/pkg/kubectl/resource/categories_test.go b/pkg/kubectl/resource/categories_test.go index 69314d1848c7..5c5a9d44f798 100644 --- a/pkg/kubectl/resource/categories_test.go +++ b/pkg/kubectl/resource/categories_test.go @@ -20,7 +20,15 @@ import ( "reflect" "testing" + swagger "github.com/emicklei/go-restful-swagger12" + + "github.com/googleapis/gnostic/OpenAPIv2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/rest/fake" ) func TestCategoryExpansion(t *testing.T) { @@ -65,3 +73,120 @@ func TestCategoryExpansion(t *testing.T) { } } } + +func TestDiscoveryCategoryExpander(t *testing.T) { + tests := []struct { + category string + serverResponse []*metav1.APIResourceList + expected []schema.GroupResource + }{ + { + category: "all", + serverResponse: []*metav1.APIResourceList{ + { + GroupVersion: "batch/v1", + APIResources: []metav1.APIResource{ + { + Name: "jobs", + ShortNames: []string{"jz"}, + Categories: []string{"all"}, + }, + }, + }, + }, + expected: []schema.GroupResource{ + { + Group: "batch", + Resource: "jobs", + }, + }, + }, + { + category: "all", + serverResponse: []*metav1.APIResourceList{ + { + GroupVersion: "batch/v1", + APIResources: []metav1.APIResource{ + { + Name: "jobs", + ShortNames: []string{"jz"}, + }, + }, + }, + }, + }, + { + category: "targaryens", + serverResponse: []*metav1.APIResourceList{ + { + GroupVersion: "batch/v1", + APIResources: []metav1.APIResource{ + { + Name: "jobs", + ShortNames: []string{"jz"}, + Categories: []string{"all"}, + }, + }, + }, + }, + }, + } + + dc := &fakeDiscoveryClient{} + for _, test := range tests { + dc.serverResourcesHandler = func() ([]*metav1.APIResourceList, error) { + return test.serverResponse, nil + } + expander, err := NewDiscoveryCategoryExpander(SimpleCategoryExpander{}, dc) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + expanded, _ := expander.Expand(test.category) + if !reflect.DeepEqual(expanded, test.expected) { + t.Errorf("expected %v, got %v", test.expected, expanded) + } + } + +} + +type fakeDiscoveryClient struct { + serverResourcesHandler func() ([]*metav1.APIResourceList, error) +} + +var _ discovery.DiscoveryInterface = &fakeDiscoveryClient{} + +func (c *fakeDiscoveryClient) RESTClient() restclient.Interface { + return &fake.RESTClient{} +} + +func (c *fakeDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) { + return nil, nil +} + +func (c *fakeDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { + return &metav1.APIResourceList{}, nil +} + +func (c *fakeDiscoveryClient) ServerResources() ([]*metav1.APIResourceList, error) { + return c.serverResourcesHandler() +} + +func (c *fakeDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) { + return nil, nil +} + +func (c *fakeDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { + return nil, nil +} + +func (c *fakeDiscoveryClient) ServerVersion() (*version.Info, error) { + return &version.Info{}, nil +} + +func (c *fakeDiscoveryClient) SwaggerSchema(version schema.GroupVersion) (*swagger.ApiDeclaration, error) { + return &swagger.ApiDeclaration{}, nil +} + +func (c *fakeDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) { + return &openapi_v2.Document{}, nil +}