From cc1b68e0687b15be360397d1bb6ad44075bcb255 Mon Sep 17 00:00:00 2001 From: Navid Shaikh Date: Tue, 4 Aug 2020 17:12:28 +0530 Subject: [PATCH] List inbuilt sources if CRD access is restricted (#948) * List inbuilt sources if CRD access is restricted Fixes #947 - Identify restricted access error - If server returns restricted access error, fallback to listing only eventing inbuilt sources using their GVKs. - List any inbuilt source (ApiServerSource) object and read the error to know if eventing is installed for `kn source list-types`. * Fix golint warnings * Remove unused imports * Verify each built in source before listing source types * Improve the check if sources are not installed in the cluster * Update finding forbidden error * Update finding errors * Add unit tests for IsForbiddenError util * Add unit tests * Add tests for dynamic pkg library * Add unit tests for case when no sources are installed * Update test name --- pkg/dynamic/client.go | 61 ++++++++++++++++++++++-- pkg/dynamic/client_test.go | 41 ++++++++++++++++ pkg/dynamic/lib.go | 23 +++++++++ pkg/dynamic/lib_test.go | 20 ++++++++ pkg/errors/errors.go | 4 +- pkg/errors/factory.go | 9 ++++ pkg/errors/factory_test.go | 31 ++++++++++++ pkg/kn/commands/source/list.go | 18 +++++-- pkg/kn/commands/source/list_test.go | 22 +++++++++ pkg/kn/commands/source/list_types.go | 38 +++++++++++++-- pkg/sources/v1alpha2/apiserver_client.go | 2 +- pkg/sources/v1alpha2/client.go | 12 +++++ pkg/sources/v1alpha2/client_test.go | 31 ++++++++++++ 13 files changed, 297 insertions(+), 15 deletions(-) create mode 100644 pkg/sources/v1alpha2/client_test.go diff --git a/pkg/dynamic/client.go b/pkg/dynamic/client.go index f5ca6f4126..fb97beaf9d 100644 --- a/pkg/dynamic/client.go +++ b/pkg/dynamic/client.go @@ -15,6 +15,9 @@ package dynamic import ( + "errors" + "strings" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -42,12 +45,15 @@ type KnDynamicClient interface { // ListCRDs returns list of CRDs with their type and name ListCRDs(options metav1.ListOptions) (*unstructured.UnstructuredList, error) - // ListSourceCRDs returns list of eventing sources CRDs + // ListSourcesTypes returns list of eventing sources CRDs ListSourcesTypes() (*unstructured.UnstructuredList, error) // ListSources returns list of available source objects ListSources(types ...WithType) (*unstructured.UnstructuredList, error) + // ListSourcesUsingGVKs returns list of available source objects using given list of GVKs + ListSourcesUsingGVKs(*[]schema.GroupVersionKind, ...WithType) (*unstructured.UnstructuredList, error) + // RawClient returns the raw dynamic client interface RawClient() dynamic.Interface } @@ -107,12 +113,17 @@ func (c *knDynamicClient) ListSources(types ...WithType) (*unstructured.Unstruct var ( sourceList unstructured.UnstructuredList options metav1.ListOptions - numberOfsourceTypesFound int + numberOfSourceTypesFound int ) sourceTypes, err := c.ListSourcesTypes() if err != nil { return nil, err } + + if sourceTypes == nil || len(sourceTypes.Items) == 0 { + return nil, errors.New("no sources found on the backend, please verify the installation") + } + namespace := c.Namespace() filters := WithTypes(types).List() // For each source type available, find out each source types objects @@ -141,14 +152,56 @@ func (c *knDynamicClient) ListSources(types ...WithType) (*unstructured.Unstruct if len(sList.Items) > 0 { // keep a track if we found source objects of different types - numberOfsourceTypesFound++ + numberOfSourceTypesFound++ + sourceList.Items = append(sourceList.Items, sList.Items...) + sourceList.SetGroupVersionKind(sList.GetObjectKind().GroupVersionKind()) + } + } + // Clear the Group and Version for list if there are multiple types of source objects found + // Keep the source's GVK if there is only one type of source objects found or requested via --type filter + if numberOfSourceTypesFound > 1 { + sourceList.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "", Kind: "List"}) + } + return &sourceList, nil +} + +// ListSourcesUsingGVKs returns list of available source objects using given list of GVKs +func (c *knDynamicClient) ListSourcesUsingGVKs(gvks *[]schema.GroupVersionKind, types ...WithType) (*unstructured.UnstructuredList, error) { + if gvks == nil { + return nil, nil + } + + var ( + sourceList unstructured.UnstructuredList + options metav1.ListOptions + numberOfSourceTypesFound int + ) + namespace := c.Namespace() + filters := WithTypes(types).List() + + for _, gvk := range *gvks { + if len(filters) > 0 && !util.SliceContainsIgnoreCase(filters, gvk.Kind) { + continue + } + + gvr := gvk.GroupVersion().WithResource(strings.ToLower(gvk.Kind) + "s") + + // list objects of source type with this GVR + sList, err := c.client.Resource(gvr).Namespace(namespace).List(options) + if err != nil { + return nil, err + } + + if len(sList.Items) > 0 { + // keep a track if we found source objects of different types + numberOfSourceTypesFound++ sourceList.Items = append(sourceList.Items, sList.Items...) sourceList.SetGroupVersionKind(sList.GetObjectKind().GroupVersionKind()) } } // Clear the Group and Version for list if there are multiple types of source objects found // Keep the source's GVK if there is only one type of source objects found or requested via --type filter - if numberOfsourceTypesFound > 1 { + if numberOfSourceTypesFound > 1 { sourceList.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "", Kind: "List"}) } return &sourceList, nil diff --git a/pkg/dynamic/client_test.go b/pkg/dynamic/client_test.go index 7343fc86bf..aae3df4c00 100644 --- a/pkg/dynamic/client_test.go +++ b/pkg/dynamic/client_test.go @@ -95,6 +95,13 @@ func TestListSources(t *testing.T) { assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "source", "kind", "CRD")) }) + t.Run("sources not installed", func(t *testing.T) { + client := createFakeKnDynamicClient(testNamespace) + _, err := client.ListSources() + assert.Check(t, err != nil) + assert.Check(t, util.ContainsAll(err.Error(), "no sources", "found", "backend", "verify", "installation")) + }) + t.Run("source list empty", func(t *testing.T) { client := createFakeKnDynamicClient(testNamespace, newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), @@ -118,6 +125,40 @@ func TestListSources(t *testing.T) { }) } +func TestListSourcesUsingGVKs(t *testing.T) { + t.Run("No GVKs given", func(t *testing.T) { + client := createFakeKnDynamicClient(testNamespace) + assert.Check(t, client.RawClient() != nil) + s, err := client.ListSourcesUsingGVKs(nil) + assert.NilError(t, err) + assert.Check(t, s == nil) + }) + + t.Run("source list with given GVKs", func(t *testing.T) { + client := createFakeKnDynamicClient(testNamespace, + newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), + newSourceCRDObjWithSpec("apiserversources", "sources.knative.dev", "v1alpha1", "ApiServerSource"), + newSourceUnstructuredObj("p1", "sources.knative.dev/v1alpha1", "PingSource"), + newSourceUnstructuredObj("a1", "sources.knative.dev/v1alpha1", "ApiServerSource"), + ) + assert.Check(t, client.RawClient() != nil) + gv := schema.GroupVersion{"sources.knative.dev", "v1alpha1"} + gvks := []schema.GroupVersionKind{gv.WithKind("ApiServerSource"), gv.WithKind("PingSource")} + + s, err := client.ListSourcesUsingGVKs(&gvks) + assert.NilError(t, err) + assert.Check(t, s != nil) + assert.Equal(t, len(s.Items), 2) + + // withType + s, err = client.ListSourcesUsingGVKs(&gvks, WithTypeFilter("PingSource")) + assert.NilError(t, err) + assert.Check(t, s != nil) + assert.Equal(t, len(s.Items), 1) + }) + +} + // createFakeKnDynamicClient gives you a dynamic client for testing containing the given objects. // See also the one in the fake package. Duplicated here to avoid a dependency loop. func createFakeKnDynamicClient(testNamespace string, objects ...runtime.Object) KnDynamicClient { diff --git a/pkg/dynamic/lib.go b/pkg/dynamic/lib.go index 06b677567d..7ad14956c3 100644 --- a/pkg/dynamic/lib.go +++ b/pkg/dynamic/lib.go @@ -16,6 +16,7 @@ package dynamic import ( "fmt" + "strings" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -123,3 +124,25 @@ func (types WithTypes) List() []string { } return stypes } + +// UnstructuredCRDFromGVK constructs an unstructured object using the given GVK +func UnstructuredCRDFromGVK(gvk schema.GroupVersionKind) *unstructured.Unstructured { + name := fmt.Sprintf("%ss.%s", strings.ToLower(gvk.Kind), gvk.Group) + plural := fmt.Sprintf("%ss", strings.ToLower(gvk.Kind)) + u := &unstructured.Unstructured{} + u.SetUnstructuredContent(map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": name, + }, + "spec": map[string]interface{}{ + "group": gvk.Group, + "version": gvk.Version, + "names": map[string]interface{}{ + "kind": gvk.Kind, + "plural": plural, + }, + }, + }) + + return u +} diff --git a/pkg/dynamic/lib_test.go b/pkg/dynamic/lib_test.go index 7ed81c1d37..57d5c31935 100644 --- a/pkg/dynamic/lib_test.go +++ b/pkg/dynamic/lib_test.go @@ -18,6 +18,7 @@ import ( "testing" "gotest.tools/assert" + "k8s.io/apimachinery/pkg/runtime/schema" "knative.dev/client/pkg/util" ) @@ -77,3 +78,22 @@ func TestGVRFromUnstructured(t *testing.T) { assert.Check(t, err != nil) assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "version")) } + +func TestUnstructuredCRDFromGVK(t *testing.T) { + u := UnstructuredCRDFromGVK(schema.GroupVersionKind{"sources.knative.dev", "v1alpha2", "ApiServerSource"}) + g, err := groupFromUnstructured(u) + assert.NilError(t, err) + assert.Equal(t, g, "sources.knative.dev") + + v, err := versionFromUnstructured(u) + assert.NilError(t, err) + assert.Equal(t, v, "v1alpha2") + + k, err := kindFromUnstructured(u) + assert.NilError(t, err) + assert.Equal(t, k, "ApiServerSource") + + r, err := resourceFromUnstructured(u) + assert.NilError(t, err) + assert.Equal(t, r, "apiserversources") +} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index c9a0cd5454..e58f3b4cc7 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -26,7 +26,7 @@ func newInvalidCRD(apiGroup string) *KNError { return NewKNError(msg) } -func newNoRouteToHost(errString string) error { +func newNoRouteToHost(errString string) *KNError { parts := strings.SplitAfter(errString, "dial tcp") if len(parts) == 2 { return NewKNError(fmt.Sprintf("error connecting to the cluster, please verify connection at: %s", strings.Trim(parts[1], " "))) @@ -34,6 +34,6 @@ func newNoRouteToHost(errString string) error { return NewKNError(fmt.Sprintf("error connecting to the cluster: %s", errString)) } -func newNoKubeConfig(errString string) error { +func newNoKubeConfig(errString string) *KNError { return NewKNError("no kubeconfig has been provided, please use a valid configuration to connect to the cluster") } diff --git a/pkg/errors/factory.go b/pkg/errors/factory.go index d0b473856f..48b076ee55 100644 --- a/pkg/errors/factory.go +++ b/pkg/errors/factory.go @@ -15,6 +15,7 @@ package errors import ( + "net/http" "strings" api_errors "k8s.io/apimachinery/pkg/api/errors" @@ -63,3 +64,11 @@ func GetError(err error) error { return err } } + +// IsForbiddenError returns true if given error can be converted to API status and of type forbidden access else false +func IsForbiddenError(err error) bool { + if status, ok := err.(api_errors.APIStatus); ok { + return status.Status().Code == int32(http.StatusForbidden) + } + return false +} diff --git a/pkg/errors/factory_test.go b/pkg/errors/factory_test.go index 9c94b94952..991f4c3b31 100644 --- a/pkg/errors/factory_test.go +++ b/pkg/errors/factory_test.go @@ -120,6 +120,11 @@ func TestKnErrors(t *testing.T) { Error: errors.New("no route to host 192.168.1.1"), ExpectedMsg: "error connecting to the cluster: no route to host 192.168.1.1", }, + { + Name: "foo error which cant be converted to APIStatus", + Error: errors.New("foo error"), + ExpectedMsg: "foo error", + }, } for _, tc := range cases { tc := tc @@ -130,3 +135,29 @@ func TestKnErrors(t *testing.T) { }) } } + +func TestIsForbiddenError(t *testing.T) { + cases := []struct { + Name string + Error error + Forbidden bool + }{ + { + Name: "forbidden error", + Error: api_errors.NewForbidden(schema.GroupResource{Group: "apiextensions.k8s.io", Resource: "CustomResourceDefinition"}, "", nil), + Forbidden: true, + }, + { + Name: "non forbidden error", + Error: errors.New("panic"), + Forbidden: false, + }, + } + for _, tc := range cases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, IsForbiddenError(tc.Error), tc.Forbidden) + }) + } +} diff --git a/pkg/kn/commands/source/list.go b/pkg/kn/commands/source/list.go index a45c0afd9f..1f5cb3c157 100644 --- a/pkg/kn/commands/source/list.go +++ b/pkg/kn/commands/source/list.go @@ -20,9 +20,11 @@ import ( "github.com/spf13/cobra" "knative.dev/client/pkg/dynamic" + knerrors "knative.dev/client/pkg/errors" "knative.dev/client/pkg/kn/commands" "knative.dev/client/pkg/kn/commands/flags" "knative.dev/client/pkg/kn/commands/source/duck" + sourcesv1alpha2 "knative.dev/client/pkg/sources/v1alpha2" ) var listExample = ` @@ -52,15 +54,25 @@ func NewListCommand(p *commands.KnParams) *cobra.Command { if err != nil { return err } + var filters dynamic.WithTypes for _, filter := range filterFlags.Filters { filters = append(filters, dynamic.WithTypeFilter(filter)) } + sourceList, err := dynamicClient.ListSources(filters...) - if err != nil { - return err + + switch { + case knerrors.IsForbiddenError(err): + gvks := sourcesv1alpha2.BuiltInSourcesGVKs() + if sourceList, err = dynamicClient.ListSourcesUsingGVKs(&gvks, filters...); err != nil { + return knerrors.GetError(err) + } + case err != nil: + return knerrors.GetError(err) } - if len(sourceList.Items) == 0 { + + if sourceList == nil || len(sourceList.Items) == 0 { fmt.Fprintf(cmd.OutOrStdout(), "No sources found in %s namespace.\n", namespace) return nil } diff --git a/pkg/kn/commands/source/list_test.go b/pkg/kn/commands/source/list_test.go index 71541d4d23..4550bfe592 100644 --- a/pkg/kn/commands/source/list_test.go +++ b/pkg/kn/commands/source/list_test.go @@ -23,6 +23,8 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + dynamicfake "k8s.io/client-go/dynamic/fake" + clientdynamic "knative.dev/client/pkg/dynamic" "knative.dev/client/pkg/kn/commands" "knative.dev/client/pkg/util" ) @@ -51,6 +53,12 @@ func sourceFakeCmd(args []string, objects ...runtime.Object) (output []string, e return } +func TestSourceListTypesNoSourcesInstalled(t *testing.T) { + _, err := sourceFakeCmd([]string{"source", "list-types"}) + assert.Check(t, err != nil) + assert.Check(t, util.ContainsAll(err.Error(), "no sources", "found", "backend", "verify", "installation")) +} + func TestSourceListTypes(t *testing.T) { output, err := sourceFakeCmd([]string{"source", "list-types"}, newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), @@ -71,6 +79,20 @@ func TestSourceListTypesNoHeaders(t *testing.T) { assert.Check(t, util.ContainsAll(output[0], "PingSource")) } +func TestListBuiltInSourceTypes(t *testing.T) { + fakeDynamic := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme()) + sources, err := listBuiltInSourceTypes(clientdynamic.NewKnDynamicClient(fakeDynamic, "current")) + assert.NilError(t, err) + assert.Check(t, sources != nil) + assert.Equal(t, len(sources.Items), 4) +} + +func TestSourceListNoSourcesInstalled(t *testing.T) { + _, err := sourceFakeCmd([]string{"source", "list"}) + assert.Check(t, err != nil) + assert.Check(t, util.ContainsAll(err.Error(), "no sources", "found", "backend", "verify", "installation")) +} + func TestSourceList(t *testing.T) { output, err := sourceFakeCmd([]string{"source", "list"}, newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), diff --git a/pkg/kn/commands/source/list_types.go b/pkg/kn/commands/source/list_types.go index 67f44131be..3454a7a93c 100644 --- a/pkg/kn/commands/source/list_types.go +++ b/pkg/kn/commands/source/list_types.go @@ -18,9 +18,14 @@ import ( "fmt" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "knative.dev/client/pkg/dynamic" + knerrors "knative.dev/client/pkg/errors" "knative.dev/client/pkg/kn/commands" "knative.dev/client/pkg/kn/commands/flags" + sourcesv1alpha2 "knative.dev/client/pkg/sources/v1alpha2" ) // NewListTypesCommand defines and processes `kn source list-types` @@ -47,13 +52,17 @@ func NewListTypesCommand(p *commands.KnParams) *cobra.Command { } sourceListTypes, err := dynamicClient.ListSourcesTypes() - if err != nil { - return err + switch { + case knerrors.IsForbiddenError(err): + if sourceListTypes, err = listBuiltInSourceTypes(dynamicClient); err != nil { + return knerrors.GetError(err) + } + case err != nil: + return knerrors.GetError(err) } - if len(sourceListTypes.Items) == 0 { - fmt.Fprintf(cmd.OutOrStdout(), "No sources found.\n") - return nil + if sourceListTypes == nil || len(sourceListTypes.Items) == 0 { + return fmt.Errorf("no sources found on the backend, please verify the installation") } printer, err := listTypesFlags.ToPrinter() @@ -73,3 +82,22 @@ func NewListTypesCommand(p *commands.KnParams) *cobra.Command { listTypesFlags.AddFlags(listTypesCommand) return listTypesCommand } + +func listBuiltInSourceTypes(d dynamic.KnDynamicClient) (*unstructured.UnstructuredList, error) { + var err error + uList := unstructured.UnstructuredList{} + gvks := sourcesv1alpha2.BuiltInSourcesGVKs() + for _, gvk := range gvks { + _, err = d.ListSourcesUsingGVKs(&[]schema.GroupVersionKind{gvk}) + if err != nil { + continue + } + u := dynamic.UnstructuredCRDFromGVK(gvk) + uList.Items = append(uList.Items, *u) + } + // if not even one source is found + if len(uList.Items) == 0 && err != nil { + return nil, knerrors.GetError(err) + } + return &uList, nil +} diff --git a/pkg/sources/v1alpha2/apiserver_client.go b/pkg/sources/v1alpha2/apiserver_client.go index 3f66511c6d..cb972e3545 100644 --- a/pkg/sources/v1alpha2/apiserver_client.go +++ b/pkg/sources/v1alpha2/apiserver_client.go @@ -111,7 +111,7 @@ func (c *apiServerSourcesClient) Namespace() string { func (c *apiServerSourcesClient) ListAPIServerSource() (*v1alpha2.ApiServerSourceList, error) { sourceList, err := c.client.List(metav1.ListOptions{}) if err != nil { - return nil, err + return nil, knerrors.GetError(err) } return updateAPIServerSourceListGVK(sourceList) diff --git a/pkg/sources/v1alpha2/client.go b/pkg/sources/v1alpha2/client.go index c97129a394..0ea81c68f4 100644 --- a/pkg/sources/v1alpha2/client.go +++ b/pkg/sources/v1alpha2/client.go @@ -15,6 +15,8 @@ package v1alpha2 import ( + "k8s.io/apimachinery/pkg/runtime/schema" + sourcesv1alpha2 "knative.dev/eventing/pkg/apis/sources/v1alpha2" clientv1alpha2 "knative.dev/eventing/pkg/client/clientset/versioned/typed/sources/v1alpha2" ) @@ -61,3 +63,13 @@ func (c *sourcesClient) SinkBindingClient() KnSinkBindingClient { func (c *sourcesClient) APIServerSourcesClient() KnAPIServerSourcesClient { return newKnAPIServerSourcesClient(c.client.ApiServerSources(c.namespace), c.namespace) } + +// BuiltInSourcesGVKs returns the GVKs for built in sources +func BuiltInSourcesGVKs() []schema.GroupVersionKind { + return []schema.GroupVersionKind{ + sourcesv1alpha2.SchemeGroupVersion.WithKind("ApiServerSource"), + sourcesv1alpha2.SchemeGroupVersion.WithKind("ContainerSource"), + sourcesv1alpha2.SchemeGroupVersion.WithKind("PingSource"), + sourcesv1alpha2.SchemeGroupVersion.WithKind("SinkBinding"), + } +} diff --git a/pkg/sources/v1alpha2/client_test.go b/pkg/sources/v1alpha2/client_test.go new file mode 100644 index 0000000000..6f48399b24 --- /dev/null +++ b/pkg/sources/v1alpha2/client_test.go @@ -0,0 +1,31 @@ +// Copyright © 2020 The Knative 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 v1alpha2 + +import ( + "testing" + + "gotest.tools/assert" + + sourcesv1alpha2 "knative.dev/eventing/pkg/apis/sources/v1alpha2" +) + +func TestBuiltInSourcesGVks(t *testing.T) { + gvks := BuiltInSourcesGVKs() + for _, each := range gvks { + assert.DeepEqual(t, each.GroupVersion(), sourcesv1alpha2.SchemeGroupVersion) + } + assert.Equal(t, len(gvks), 4) +}