Skip to content

Commit

Permalink
List inbuilt sources if CRD access is restricted (knative#948)
Browse files Browse the repository at this point in the history
* List inbuilt sources if CRD access is restricted

 Fixes knative#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
  • Loading branch information
navidshaikh committed Aug 18, 2020
1 parent e0df5f1 commit 2acd186
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 15 deletions.
61 changes: 57 additions & 4 deletions pkg/dynamic/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions pkg/dynamic/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions pkg/dynamic/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package dynamic

import (
"fmt"
"strings"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -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
}
20 changes: 20 additions & 0 deletions pkg/dynamic/lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"

"gotest.tools/assert"
"k8s.io/apimachinery/pkg/runtime/schema"

"knative.dev/client/pkg/util"
)
Expand Down Expand Up @@ -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")
}
4 changes: 2 additions & 2 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ 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], " ")))
}
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")
}
9 changes: 9 additions & 0 deletions pkg/errors/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package errors

import (
"net/http"
"strings"

api_errors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -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
}
31 changes: 31 additions & 0 deletions pkg/errors/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
})
}
}
18 changes: 15 additions & 3 deletions pkg/kn/commands/source/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
Expand Down Expand Up @@ -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
}
Expand Down
22 changes: 22 additions & 0 deletions pkg/kn/commands/source/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down
Loading

0 comments on commit 2acd186

Please sign in to comment.