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
This commit is contained in:
Navid Shaikh 2020-08-04 17:12:28 +05:30 committed by GitHub
parent 4a3cf24550
commit cc1b68e068
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 297 additions and 15 deletions

View File

@ -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 {
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 {
sourceList.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "", Kind: "List"})
}
return &sourceList, nil

View File

@ -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 {

View File

@ -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
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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
}

View File

@ -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"),

View File

@ -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
}

View File

@ -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)

View File

@ -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"),
}
}

View File

@ -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)
}