From a0e527be219388819d3be8a378ac7f3a05e10b97 Mon Sep 17 00:00:00 2001 From: Navid Shaikh Date: Tue, 14 Jul 2020 14:23:13 +0530 Subject: [PATCH] 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`. --- pkg/dynamic/client.go | 53 ++++++++++++++++++++++-- pkg/dynamic/lib.go | 22 ++++++++++ pkg/errors/errors.go | 6 ++- pkg/errors/errors_test.go | 6 +-- pkg/errors/factory.go | 15 ++++++- pkg/errors/factory_test.go | 2 +- pkg/kn/commands/source/list.go | 12 +++++- pkg/kn/commands/source/list_types.go | 36 +++++++++++++++- pkg/sources/v1alpha2/apiserver_client.go | 2 +- pkg/sources/v1alpha2/client.go | 14 +++++++ 10 files changed, 154 insertions(+), 14 deletions(-) diff --git a/pkg/dynamic/client.go b/pkg/dynamic/client.go index f5ca6f4126..f8ef5999f5 100644 --- a/pkg/dynamic/client.go +++ b/pkg/dynamic/client.go @@ -15,6 +15,8 @@ package dynamic import ( + "strings" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -48,6 +50,9 @@ type KnDynamicClient interface { // ListSources returns list of available source objects ListSources(types ...WithType) (*unstructured.UnstructuredList, error) + // ListSources 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,7 +112,7 @@ 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 { @@ -141,14 +146,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 +} + +// ListSources 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/lib.go b/pkg/dynamic/lib.go index 06b677567d..01d0286f5d 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,24 @@ func (types WithTypes) List() []string { } return stypes } + +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/errors/errors.go b/pkg/errors/errors.go index c9a0cd5454..b646e6aa1a 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -22,7 +22,7 @@ import ( func newInvalidCRD(apiGroup string) *KNError { parts := strings.Split(apiGroup, ".") name := parts[0] - msg := fmt.Sprintf("no Knative %s API found on the backend, please verify the installation", name) + msg := fmt.Sprintf("404: no Knative %s API found on the backend, please verify the installation", name) return NewKNError(msg) } @@ -37,3 +37,7 @@ func newNoRouteToHost(errString string) error { func newNoKubeConfig(errString string) error { return NewKNError("no kubeconfig has been provided, please use a valid configuration to connect to the cluster") } + +func newForbidden(code int32, msg string) *KNError { + return NewKNError(fmt.Sprintf("%d: %s", code, msg)) +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go index 6e3df075b5..5cbfdb237f 100644 --- a/pkg/errors/errors_test.go +++ b/pkg/errors/errors_test.go @@ -22,12 +22,12 @@ import ( func TestNewInvalidCRD(t *testing.T) { err := newInvalidCRD("serving.knative.dev") - assert.Error(t, err, "no Knative serving API found on the backend, please verify the installation") + assert.Error(t, err, "404: no Knative serving API found on the backend, please verify the installation") err = newInvalidCRD("eventing") - assert.Error(t, err, "no Knative eventing API found on the backend, please verify the installation") + assert.Error(t, err, "404: no Knative eventing API found on the backend, please verify the installation") err = newInvalidCRD("") - assert.Error(t, err, "no Knative API found on the backend, please verify the installation") + assert.Error(t, err, "404: no Knative API found on the backend, please verify the installation") } diff --git a/pkg/errors/factory.go b/pkg/errors/factory.go index d0b473856f..6a98296562 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" @@ -38,6 +39,10 @@ func isEmptyConfigError(err error) bool { return strings.Contains(err.Error(), "no configuration has been provided") } +func IsForbiddenError(status api_errors.APIStatus) bool { + return status.Status().Code == http.StatusForbidden +} + //Retrieves a custom error struct based on the original error APIStatus struct //Returns the original error struct in case it can't identify the kind of APIStatus error func GetError(err error) error { @@ -55,11 +60,17 @@ func GetError(err error) error { return err } var knerr *KNError - if isCRDError(apiStatus) { + switch { + case isCRDError(apiStatus): knerr = newInvalidCRD(apiStatus.Status().Details.Group) knerr.Status = apiStatus return knerr + case IsForbiddenError(apiStatus): + knerr = newForbidden(apiStatus.Status().Code, apiStatus.Status().Message) + knerr.Status = apiStatus + return knerr + default: + return err } - return err } } diff --git a/pkg/errors/factory_test.go b/pkg/errors/factory_test.go index 9c94b94952..dcd1fecb81 100644 --- a/pkg/errors/factory_test.go +++ b/pkg/errors/factory_test.go @@ -48,7 +48,7 @@ func TestKnErrorsStatusErrors(t *testing.T) { } return statusError }, - ExpectedMsg: "no Knative serving API found on the backend, please verify the installation", + ExpectedMsg: "404: no Knative serving API found on the backend, please verify the installation", Validate: func(t *testing.T, err error, msg string) { assert.Error(t, err, msg) }, diff --git a/pkg/kn/commands/source/list.go b/pkg/kn/commands/source/list.go index a45c0afd9f..1d187418cd 100644 --- a/pkg/kn/commands/source/list.go +++ b/pkg/kn/commands/source/list.go @@ -16,13 +16,16 @@ package source import ( "fmt" + "strings" "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 = ` @@ -58,8 +61,15 @@ func NewListCommand(p *commands.KnParams) *cobra.Command { } sourceList, err := dynamicClient.ListSources(filters...) if err != nil { - return err + if strings.HasPrefix(knerrors.GetError(err).Error(), "403") { + gvks := sourcesv1alpha2.BuiltInSourcesGVKs() + sourceList, err = dynamicClient.ListSourcesUsingGVKs(&gvks, filters...) + if err != nil { + return knerrors.GetError(err) + } + } } + if 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_types.go b/pkg/kn/commands/source/list_types.go index 67f44131be..2342c9835d 100644 --- a/pkg/kn/commands/source/list_types.go +++ b/pkg/kn/commands/source/list_types.go @@ -16,11 +16,16 @@ package source import ( "fmt" + "strings" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "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` @@ -48,11 +53,21 @@ func NewListTypesCommand(p *commands.KnParams) *cobra.Command { sourceListTypes, err := dynamicClient.ListSourcesTypes() if err != nil { - return err + if strings.HasPrefix(knerrors.GetError(err).Error(), "403") { + sourcesClient, err := p.NewSourcesClient(namespace) + if err != nil { + return err + } + + sourceListTypes, err = listBuiltInSourceTypes(sourcesClient) + if err != nil { + return knerrors.GetError(err) + } + } } if len(sourceListTypes.Items) == 0 { - fmt.Fprintf(cmd.OutOrStdout(), "No sources found.\n") + fmt.Fprintf(cmd.OutOrStdout(), "404: no sources found on the backend, please verify the installation\n") return nil } @@ -73,3 +88,20 @@ func NewListTypesCommand(p *commands.KnParams) *cobra.Command { listTypesFlags.AddFlags(listTypesCommand) return listTypesCommand } + +func listBuiltInSourceTypes(c sourcesv1alpha2.KnSourcesClient) (*unstructured.UnstructuredList, error) { + _, err := c.APIServerSourcesClient().ListAPIServerSource() + if err != nil { + if strings.HasPrefix(err.Error(), "404") { + return nil, err + } + } + + uList := unstructured.UnstructuredList{} + gvks := sourcesv1alpha2.BuiltInSourcesGVKs() + for _, gvk := range gvks { + u := dynamic.UnstructuredCRDFromGVK(gvk) + uList.Items = append(uList.Items, *u) + } + 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..a3e653549a 100644 --- a/pkg/sources/v1alpha2/client.go +++ b/pkg/sources/v1alpha2/client.go @@ -15,6 +15,10 @@ package v1alpha2 import ( + "strings" + + "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 +65,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"), + } +}