From dd320279bbf63b043589cd9f6824780ee307bd8b Mon Sep 17 00:00:00 2001 From: Kaustubh pande Date: Fri, 28 Aug 2020 14:31:47 +0530 Subject: [PATCH 1/2] add channel list-types --- CHANGELOG.adoc | 4 + docs/cmd/kn_channel.md | 1 + docs/cmd/kn_channel_list-types.md | 46 +++++ lib/test/channel.go | 9 + pkg/dynamic/client.go | 91 ++++++++-- pkg/dynamic/client_mock.go | 22 +++ pkg/dynamic/client_mock_test.go | 5 + pkg/dynamic/client_test.go | 127 ++++++++++++- pkg/dynamic/fake/fake.go | 4 +- pkg/kn/commands/channel/channel.go | 4 +- pkg/kn/commands/channel/flags.go | 56 ++++++ pkg/kn/commands/channel/list_types.go | 103 +++++++++++ pkg/kn/commands/channel/list_types_test.go | 196 +++++++++++++++++++++ pkg/messaging/v1beta1/client.go | 8 + test/e2e/channels_test.go | 10 +- 15 files changed, 670 insertions(+), 16 deletions(-) create mode 100644 docs/cmd/kn_channel_list-types.md create mode 100644 pkg/kn/commands/channel/list_types.go create mode 100644 pkg/kn/commands/channel/list_types_test.go diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 2e7bf185b2..2e1d9cd3c6 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -38,6 +38,10 @@ | Add alias to commands | https://github.com/knative/client/pull/1041[#1041] +| 🎁 +| Add channel list-types +| https://github.com/knative/client/pull/1027[#1027] + |=== ## v0.17.0 (2020-08-26) diff --git a/docs/cmd/kn_channel.md b/docs/cmd/kn_channel.md index 9d6f3ba570..e5f2078f6e 100644 --- a/docs/cmd/kn_channel.md +++ b/docs/cmd/kn_channel.md @@ -31,4 +31,5 @@ kn channel COMMAND * [kn channel delete](kn_channel_delete.md) - Delete a channel * [kn channel describe](kn_channel_describe.md) - Show details of a channel * [kn channel list](kn_channel_list.md) - List channels (alias: 'ls') +* [kn channel list-types](kn_channel_list-types.md) - List channel types diff --git a/docs/cmd/kn_channel_list-types.md b/docs/cmd/kn_channel_list-types.md new file mode 100644 index 0000000000..42bb0f8839 --- /dev/null +++ b/docs/cmd/kn_channel_list-types.md @@ -0,0 +1,46 @@ +## kn channel list-types + +List channel types + +### Synopsis + +List channel types + +``` +kn channel list-types +``` + +### Examples + +``` + + # List available channel types + kn channel list-types + + # List available channel types in YAML format + kn channel list-types -o yaml +``` + +### Options + +``` + --allow-missing-template-keys If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. (default true) + -h, --help help for list-types + -n, --namespace string Specify the namespace to operate in. + --no-headers When using the default output format, don't print headers (default: print headers). + -o, --output string Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file. + --template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]. +``` + +### Options inherited from parent commands + +``` + --config string kn configuration file (default: ~/.config/kn/config.yaml) + --kubeconfig string kubectl configuration file (default: ~/.kube/config) + --log-http log http traffic +``` + +### SEE ALSO + +* [kn channel](kn_channel.md) - Manage event channels + diff --git a/lib/test/channel.go b/lib/test/channel.go index 5eedeb6c78..8679dae7e0 100644 --- a/lib/test/channel.go +++ b/lib/test/channel.go @@ -53,3 +53,12 @@ func ChannelDelete(r *KnRunResultCollector, cname string) { r.AssertNoError(out) assert.Check(r.T(), util.ContainsAllIgnoreCase(out.Stdout, "channel", cname, "deleted")) } + +//ChannelListTypes return available channel types +func ChannelListTypes(r *KnRunResultCollector, args ...string) string { + cmd := []string{"channel", "list-types"} + cmd = append(cmd, args...) + out := r.KnTest().Kn().Run(cmd...) + r.AssertNoError(out) + return out.Stdout +} diff --git a/pkg/dynamic/client.go b/pkg/dynamic/client.go index 5c6f0bce79..7007015451 100644 --- a/pkg/dynamic/client.go +++ b/pkg/dynamic/client.go @@ -20,24 +20,29 @@ import ( "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" - "knative.dev/client/pkg/util" + "knative.dev/eventing/pkg/apis/messaging" ) const ( - crdGroup = "apiextensions.k8s.io" - crdVersion = "v1beta1" - crdKind = "CustomResourceDefinition" - crdKinds = "customresourcedefinitions" - sourcesLabelKey = "duck.knative.dev/source" - sourcesLabelValue = "true" - sourceListGroup = "client.knative.dev" - sourceListVersion = "v1alpha1" - sourceListKind = "SourceList" + crdGroup = "apiextensions.k8s.io" + crdVersion = "v1beta1" + crdKind = "CustomResourceDefinition" + crdKinds = "customresourcedefinitions" + sourcesLabelKey = "duck.knative.dev/source" + sourcesLabelValue = "true" + sourceListGroup = "client.knative.dev" + sourceListVersion = "v1alpha1" + sourceListKind = "SourceList" + channelLabelValue = "true" + channelListVersion = "v1beta1" + channelListKind = "ChannelList" + channelKind = "Channel" ) // KnDynamicClient to client-go Dynamic client. All methods are relative to the @@ -58,6 +63,12 @@ type KnDynamicClient interface { // ListSourcesUsingGVKs returns list of available source objects using given list of GVKs ListSourcesUsingGVKs(*[]schema.GroupVersionKind, ...WithType) (*unstructured.UnstructuredList, error) + // ListChannelsTypes returns installed knative channel CRDs + ListChannelsTypes() (*unstructured.UnstructuredList, error) + + // ListChannelsUsingGVKs returns list of available channel objects using given list of GVKs + ListChannelsUsingGVKs(*[]schema.GroupVersionKind, ...WithType) (*unstructured.UnstructuredList, error) + // RawClient returns the raw dynamic client interface RawClient() dynamic.Interface } @@ -106,6 +117,30 @@ func (c *knDynamicClient) ListSourcesTypes() (*unstructured.UnstructuredList, er return c.ListCRDs(options) } +// ListChannelsTypes returns installed knative channel CRDs +func (c *knDynamicClient) ListChannelsTypes() (*unstructured.UnstructuredList, error) { + var ChannelTypeList unstructured.UnstructuredList + options := metav1.ListOptions{} + channelsLabels := labels.Set{messaging.SubscribableDuckVersionAnnotation: channelLabelValue} + options.LabelSelector = channelsLabels.String() + uList, err := c.ListCRDs(options) + if err != nil { + return nil, err + } + ChannelTypeList.Object = uList.Object + for _, channelType := range uList.Items { + content := channelType.UnstructuredContent() + channelTypeKind, _, err := unstructured.NestedString(content, "spec", "names", "kind") + if err != nil { + return nil, err + } + if !util.SliceContainsIgnoreCase([]string{channelKind}, channelTypeKind) { + ChannelTypeList.Items = append(ChannelTypeList.Items, channelType) + } + } + return &ChannelTypeList, nil +} + func (c knDynamicClient) RawClient() dynamic.Interface { return c.client } @@ -198,3 +233,39 @@ func (c *knDynamicClient) ListSourcesUsingGVKs(gvks *[]schema.GroupVersionKind, } return &sourceList, nil } + +// ListChannelsUsingGVKs returns list of available channel objects using given list of GVKs +func (c *knDynamicClient) ListChannelsUsingGVKs(gvks *[]schema.GroupVersionKind, types ...WithType) (*unstructured.UnstructuredList, error) { + if gvks == nil { + return nil, nil + } + + var ( + channelList unstructured.UnstructuredList + options metav1.ListOptions + ) + 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 chaneel type with this GVR + cList, err := c.client.Resource(gvr).Namespace(namespace).List(context.TODO(), options) + if err != nil { + return nil, err + } + + if len(cList.Items) > 0 { + channelList.Items = append(channelList.Items, cList.Items...) + } + } + if len(channelList.Items) > 0 { + channelList.SetGroupVersionKind(schema.GroupVersionKind{Group: messaging.GroupName, Version: channelListVersion, Kind: channelListKind}) + } + return &channelList, nil +} diff --git a/pkg/dynamic/client_mock.go b/pkg/dynamic/client_mock.go index 051667d2cd..7a5fe0667e 100644 --- a/pkg/dynamic/client_mock.go +++ b/pkg/dynamic/client_mock.go @@ -82,6 +82,17 @@ func (c *MockKnDynamicClient) ListSourcesTypes() (*unstructured.UnstructuredList return call.Result[0].(*unstructured.UnstructuredList), mock.ErrorOrNil(call.Result[1]) } +// ListChannelsTypes returns installed knative messaging CRDs +func (dr *ClientRecorder) ListChannelsTypes(ulist *unstructured.UnstructuredList, err error) { + dr.r.Add("ListChannelsTypes", []interface{}{}, []interface{}{ulist, err}) +} + +// ListChannelsTypes returns installed knative messaging CRDs +func (c *MockKnDynamicClient) ListChannelsTypes() (*unstructured.UnstructuredList, error) { + call := c.recorder.r.VerifyCall("ListChannelsTypes") + return call.Result[0].(*unstructured.UnstructuredList), mock.ErrorOrNil(call.Result[1]) +} + // ListSources returns list of available sources objects func (dr *ClientRecorder) ListSources(types interface{}, ulist *unstructured.UnstructuredList, err error) { dr.r.Add("ListSources", []interface{}{types}, []interface{}{ulist, err}) @@ -119,3 +130,14 @@ func (c *MockKnDynamicClient) ListSourcesUsingGVKs(gvks *[]schema.GroupVersionKi func (dr *ClientRecorder) Validate() { dr.r.CheckThatAllRecordedMethodsHaveBeenCalled() } + +// ListChannelsUsingGVKs returns list of available channel objects using given list of GVKs +func (dr *ClientRecorder) ListChannelsUsingGVKs(gvks interface{}, types interface{}, ulist *unstructured.UnstructuredList, err error) { + dr.r.Add("ListChannelsUsingGVKs", []interface{}{gvks, types}, []interface{}{ulist, err}) +} + +// ListChannelsUsingGVKs returns list of available channel objects using given list of GVKs +func (c *MockKnDynamicClient) ListChannelsUsingGVKs(gvks *[]schema.GroupVersionKind, types ...WithType) (*unstructured.UnstructuredList, error) { + call := c.recorder.r.VerifyCall("ListChannelsUsingGVKs") + return call.Result[0].(*unstructured.UnstructuredList), mock.ErrorOrNil(call.Result[1]) +} diff --git a/pkg/dynamic/client_mock_test.go b/pkg/dynamic/client_mock_test.go index 38ca3f74a5..31edf63954 100644 --- a/pkg/dynamic/client_mock_test.go +++ b/pkg/dynamic/client_mock_test.go @@ -32,14 +32,19 @@ func TestMockKnDynamicClient(t *testing.T) { recorder.ListCRDs(mock.Any(), nil, nil) recorder.ListSourcesTypes(nil, nil) recorder.ListSources(mock.Any(), nil, nil) + recorder.ListChannelsTypes(nil, nil) recorder.RawClient(&fake.FakeDynamicClient{}) recorder.ListSourcesUsingGVKs(mock.Any(), mock.Any(), nil, nil) + recorder.ListChannelsUsingGVKs(mock.Any(), mock.Any(), nil, nil) client.ListCRDs(metav1.ListOptions{}) client.ListSourcesTypes() + client.ListChannelsTypes() client.ListSources(WithTypeFilter("blub")) client.RawClient() client.ListSourcesUsingGVKs(&[]schema.GroupVersionKind{}, WithTypeFilter("blub")) + client.ListChannelsUsingGVKs(&[]schema.GroupVersionKind{}, WithTypeFilter("blub")) + // Validate recorder.Validate() } diff --git a/pkg/dynamic/client_test.go b/pkg/dynamic/client_test.go index b0b927dc6a..e3225156a9 100644 --- a/pkg/dynamic/client_test.go +++ b/pkg/dynamic/client_test.go @@ -26,6 +26,8 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" eventingv1beta1 "knative.dev/eventing/pkg/apis/eventing/v1beta1" + "knative.dev/eventing/pkg/apis/messaging" + messagingv1beta1 "knative.dev/eventing/pkg/apis/messaging/v1beta1" servingv1 "knative.dev/serving/pkg/apis/serving/v1" "knative.dev/client/pkg/util" @@ -172,7 +174,7 @@ func createFakeKnDynamicClient(testNamespace string, objects ...runtime.Object) scheme := runtime.NewScheme() scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "serving.knative.dev", Version: "v1alpha1", Kind: "Service"}, &servingv1.Service{}) scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "eventing.knative.dev", Version: "v1alpha1", Kind: "Broker"}, &eventingv1beta1.Broker{}) - + scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "messaging.knative.dev", Version: "v1beta1", Kind: "Channel"}, &messagingv1beta1.Channel{}) client := dynamicfake.NewSimpleDynamicClient(scheme, objects...) return NewKnDynamicClient(client, testNamespace) } @@ -236,3 +238,126 @@ func newSourceUnstructuredObj(name, apiVersion, kind string) *unstructured.Unstr }, } } + +func newChannelCRDObj(name string) *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": crdGroup + "/" + crdVersion, + "kind": crdKind, + "metadata": map[string]interface{}{ + "namespace": testNamespace, + "name": name, + }, + }, + } + obj.SetLabels(labels.Set{messaging.SubscribableDuckVersionAnnotation: channelLabelValue}) + return obj +} + +func newChannelCRDObjWithSpec(name, group, version, kind string) *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": crdGroup + "/" + crdVersion, + "kind": crdKind, + "metadata": map[string]interface{}{ + "namespace": testNamespace, + "name": name, + }, + }, + } + + obj.Object["spec"] = map[string]interface{}{ + "group": group, + "version": version, + "names": map[string]interface{}{ + "kind": kind, + "plural": strings.ToLower(kind) + "s", + }, + } + obj.SetLabels(labels.Set{messaging.SubscribableDuckVersionAnnotation: channelLabelValue}) + return obj +} + +func newChannelUnstructuredObj(name, apiVersion, kind string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "namespace": "current", + "name": name, + }, + "spec": map[string]interface{}{ + "sink": map[string]interface{}{ + "ref": map[string]interface{}{ + "name": "foo", + }, + }, + }, + }, + } +} +func TestListChannelsTypes(t *testing.T) { + t.Run("List channel types", func(t *testing.T) { + client := createFakeKnDynamicClient( + testNamespace, + newChannelCRDObjWithSpec("Channel", "messaging.knative.dev", "v1beta1", "Channel"), + newChannelCRDObjWithSpec("InMemoryChannel", "messaging.knative.dev", "v1beta1", "InMemoryChannel"), + ) + + uList, err := client.ListChannelsTypes() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, len(uList.Items), 1) + assert.Equal(t, uList.Items[0].GetName(), "InMemoryChannel") + }) + + t.Run("List channel types error", func(t *testing.T) { + client := createFakeKnDynamicClient( + testNamespace, + newChannelCRDObj("foo"), + ) + uList, err := client.ListChannelsTypes() + assert.Check(t, err == nil) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, len(uList.Items), 1) + assert.Equal(t, uList.Items[0].GetName(), "foo") + }) +} + +func TestListChannelsUsingGVKs(t *testing.T) { + t.Run("No GVKs given", func(t *testing.T) { + client := createFakeKnDynamicClient(testNamespace) + assert.Check(t, client.RawClient() != nil) + s, err := client.ListChannelsUsingGVKs(nil) + assert.NilError(t, err) + assert.Check(t, s == nil) + }) + + t.Run("channel list with given GVKs", func(t *testing.T) { + client := createFakeKnDynamicClient(testNamespace, + newChannelCRDObjWithSpec("InMemoryChannel", "messaging.knative.dev", "v1beta1", "InMemoryChannel"), + newChannelUnstructuredObj("i1", "messaging.knative.dev/v1beta1", "InMemoryChannel"), + ) + assert.Check(t, client.RawClient() != nil) + gv := schema.GroupVersion{"messaging.knative.dev", "v1beta1"} + gvks := []schema.GroupVersionKind{gv.WithKind("InMemoryChannel")} + + s, err := client.ListChannelsUsingGVKs(&gvks) + assert.NilError(t, err) + assert.Check(t, s != nil) + assert.Equal(t, len(s.Items), 1) + assert.DeepEqual(t, s.GroupVersionKind(), schema.GroupVersionKind{messaging.GroupName, channelListVersion, channelListKind}) + + // withType + s, err = client.ListChannelsUsingGVKs(&gvks, WithTypeFilter("InMemoryChannel")) + assert.NilError(t, err) + assert.Check(t, s != nil) + assert.Equal(t, len(s.Items), 1) + assert.DeepEqual(t, s.GroupVersionKind(), schema.GroupVersionKind{messaging.GroupName, channelListVersion, channelListKind}) + }) + +} diff --git a/pkg/dynamic/fake/fake.go b/pkg/dynamic/fake/fake.go index a4529fa5a5..78dfc3224a 100644 --- a/pkg/dynamic/fake/fake.go +++ b/pkg/dynamic/fake/fake.go @@ -19,11 +19,10 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" + "knative.dev/client/pkg/dynamic" eventingv1beta1 "knative.dev/eventing/pkg/apis/eventing/v1beta1" messagingv1beta1 "knative.dev/eventing/pkg/apis/messaging/v1beta1" servingv1 "knative.dev/serving/pkg/apis/serving/v1" - - "knative.dev/client/pkg/dynamic" ) // CreateFakeKnDynamicClient gives you a dynamic client for testing containing the given objects. @@ -32,6 +31,7 @@ func CreateFakeKnDynamicClient(testNamespace string, objects ...runtime.Object) scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "serving.knative.dev", Version: "v1", Kind: "Service"}, &servingv1.Service{}) scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "eventing.knative.dev", Version: "v1beta1", Kind: "Broker"}, &eventingv1beta1.Broker{}) scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "eventing.knative.dev", Version: "v1beta1", Kind: "Subscription"}, &messagingv1beta1.Subscription{}) + scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "messaging.knative.dev", Version: "v1beta1", Kind: "Channel"}, &messagingv1beta1.Channel{}) client := dynamicfake.NewSimpleDynamicClient(scheme, objects...) return dynamic.NewKnDynamicClient(client, testNamespace) } diff --git a/pkg/kn/commands/channel/channel.go b/pkg/kn/commands/channel/channel.go index 6d01829928..8e2f1c4abd 100644 --- a/pkg/kn/commands/channel/channel.go +++ b/pkg/kn/commands/channel/channel.go @@ -18,10 +18,9 @@ import ( "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" - clientv1beta1 "knative.dev/eventing/pkg/client/clientset/versioned/typed/messaging/v1beta1" - "knative.dev/client/pkg/kn/commands" messagingv1beta1 "knative.dev/client/pkg/messaging/v1beta1" + clientv1beta1 "knative.dev/eventing/pkg/client/clientset/versioned/typed/messaging/v1beta1" ) // NewChannelCommand to manage event channels @@ -35,6 +34,7 @@ func NewChannelCommand(p *commands.KnParams) *cobra.Command { channelCmd.AddCommand(NewChannelListCommand(p)) channelCmd.AddCommand(NewChannelDeleteCommand(p)) channelCmd.AddCommand(NewChannelDescribeCommand(p)) + channelCmd.AddCommand(NewChannelListTypesCommand(p)) return channelCmd } diff --git a/pkg/kn/commands/channel/flags.go b/pkg/kn/commands/channel/flags.go index 1ecf03004a..633de5af37 100644 --- a/pkg/kn/commands/channel/flags.go +++ b/pkg/kn/commands/channel/flags.go @@ -15,17 +15,25 @@ package channel import ( + "fmt" "sort" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" "knative.dev/client/pkg/kn/commands" + "knative.dev/client/pkg/printers" hprinters "knative.dev/client/pkg/printers" messagingv1beta1 "knative.dev/eventing/pkg/apis/messaging/v1beta1" ) +var channelTypeDescription = map[string]string{ + "InMemoryChannel": "The events are stored in memory", + "KafkaChannel": "The events are stored in a Kafka cluster (must be installed separately)", +} + // ListHandlers handles printing human readable table for `kn channel list` command's output func ListHandlers(h hprinters.PrintHandler) { channelColumnDefinitions := []metav1beta1.TableColumnDefinition{ @@ -120,3 +128,51 @@ func printChannelListWithNamespace(channelList *messagingv1beta1.ChannelList, op return append(rows, others...), nil } + +// ListTypesHandlers handles printing human readable table for `kn channel list-types` +func ListTypesHandlers(h printers.PrintHandler) { + channelTypesColumnDefinitions := []metav1beta1.TableColumnDefinition{ + {Name: "Type", Type: "string", Description: "Kind / Type of the channel", Priority: 1}, + {Name: "Name", Type: "string", Description: "Name of the channel type", Priority: 1}, + {Name: "Description", Type: "string", Description: "Description of the channel type", Priority: 1}, + } + h.TableHandler(channelTypesColumnDefinitions, printChannelTypes) + h.TableHandler(channelTypesColumnDefinitions, printChannelTypesList) +} + +// printChannelTypes populates a single row of channel types list table +func printChannelTypes(channelType unstructured.Unstructured, options printers.PrintOptions) ([]metav1beta1.TableRow, error) { + name := channelType.GetName() + content := channelType.UnstructuredContent() + kind, found, err := unstructured.NestedString(content, "spec", "names", "kind") + if err != nil { + return nil, err + } + if !found { + return nil, fmt.Errorf("can't find specs.names.kind for %s", name) + } + + row := metav1beta1.TableRow{ + Object: runtime.RawExtension{Object: &channelType}, + } + row.Cells = append(row.Cells, kind, name, channelTypeDescription[kind]) + + return []metav1beta1.TableRow{row}, nil +} + +// printChannelTypesList populates the channel types list table rows +func printChannelTypesList(channelTypesList *unstructured.UnstructuredList, options printers.PrintOptions) ([]metav1beta1.TableRow, error) { + rows := make([]metav1beta1.TableRow, 0, len(channelTypesList.Items)) + + sort.SliceStable(channelTypesList.Items, func(i, j int) bool { + return channelTypesList.Items[i].GetName() < channelTypesList.Items[j].GetName() + }) + for _, item := range channelTypesList.Items { + row, err := printChannelTypes(item, options) + if err != nil { + return nil, err + } + rows = append(rows, row...) + } + return rows, nil +} diff --git a/pkg/kn/commands/channel/list_types.go b/pkg/kn/commands/channel/list_types.go new file mode 100644 index 0000000000..6cdeadd68a --- /dev/null +++ b/pkg/kn/commands/channel/list_types.go @@ -0,0 +1,103 @@ +/* +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 channel + +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" + messagingv1beta1 "knative.dev/client/pkg/messaging/v1beta1" +) + +// NewChannelListTypesCommand defines and processes `kn channel list-types` +func NewChannelListTypesCommand(p *commands.KnParams) *cobra.Command { + listTypesFlags := flags.NewListPrintFlags(ListTypesHandlers) + listTypesCommand := &cobra.Command{ + Use: "list-types", + Short: "List channel types", + Example: ` + # List available channel types + kn channel list-types + + # List available channel types in YAML format + kn channel list-types -o yaml`, + RunE: func(cmd *cobra.Command, args []string) error { + namespace, err := p.GetNamespace(cmd) + if err != nil { + return err + } + + dynamicClient, err := p.NewDynamicClient(namespace) + if err != nil { + return err + } + + channelListTypes, err := dynamicClient.ListChannelsTypes() + switch { + case knerrors.IsForbiddenError(err): + if channelListTypes, err = listBuiltInChannelTypes(dynamicClient); err != nil { + return knerrors.GetError(err) + } + case err != nil: + return knerrors.GetError(err) + } + + if channelListTypes == nil || len(channelListTypes.Items) == 0 { + return fmt.Errorf("no channels found on the backend, please verify the installation") + } + + printer, err := listTypesFlags.ToPrinter() + if err != nil { + return nil + } + + err = printer.PrintObj(channelListTypes, cmd.OutOrStdout()) + if err != nil { + return err + } + + return nil + }, + } + commands.AddNamespaceFlags(listTypesCommand.Flags(), false) + listTypesFlags.AddFlags(listTypesCommand) + return listTypesCommand +} + +func listBuiltInChannelTypes(d dynamic.KnDynamicClient) (*unstructured.UnstructuredList, error) { + var err error + uList := unstructured.UnstructuredList{} + gvks := messagingv1beta1.BuiltInChannelGVKs() + for _, gvk := range gvks { + _, err = d.ListChannelsUsingGVKs(&[]schema.GroupVersionKind{gvk}) + if err != nil { + continue + } + u := dynamic.UnstructuredCRDFromGVK(gvk) + uList.Items = append(uList.Items, *u) + } + // if not even one channel is found + if len(uList.Items) == 0 && err != nil { + return nil, knerrors.GetError(err) + } + return &uList, nil +} diff --git a/pkg/kn/commands/channel/list_types_test.go b/pkg/kn/commands/channel/list_types_test.go new file mode 100644 index 0000000000..39c2873e20 --- /dev/null +++ b/pkg/kn/commands/channel/list_types_test.go @@ -0,0 +1,196 @@ +/* +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 channel + +import ( + "strings" + "testing" + + "gotest.tools/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + + dynamicfake "k8s.io/client-go/dynamic/fake" + dynamicfakeClient "knative.dev/client/pkg/dynamic/fake" + + "knative.dev/client/pkg/dynamic" + clientdynamic "knative.dev/client/pkg/dynamic" + "knative.dev/client/pkg/kn/commands" + "knative.dev/client/pkg/util" + "knative.dev/eventing/pkg/apis/messaging" +) + +const ( + crdGroup = "apiextensions.k8s.io" + crdVersion = "v1beta1" + crdKind = "CustomResourceDefinition" + crdKinds = "customresourcedefinitions" + testNamespace = "current" + channelLabelValue = "true" + channelListVersion = "v1beta1" + channelListKind = "ChannelList" + inMemoryChannel = "InMemoryChannel" +) + +// channelFakeCmd takes cmd to be executed using dynamic client +// pass the objects to be registered to dynamic client +func channelFakeCmd(args []string, dynamicClient clientdynamic.KnDynamicClient, objects ...runtime.Object) (output []string, err error) { + knParams := &commands.KnParams{} + cmd, _, buf := commands.CreateDynamicTestKnCommand(NewChannelCommand(knParams), knParams, objects...) + cmd.SetArgs(args) + knParams.NewDynamicClient = func(namespace string) (clientdynamic.KnDynamicClient, error) { + return dynamicClient, nil + } + err = cmd.Execute() + if err != nil { + return + } + output = strings.Split(buf.String(), "\n") + return +} + +func TestChannelListTypesNoChannelInstalled(t *testing.T) { + dynamicClient := dynamicfakeClient.CreateFakeKnDynamicClient(testNamespace) + assert.Equal(t, dynamicClient.Namespace(), testNamespace) + + _, err := channelFakeCmd([]string{"channel", "list-types"}, dynamicClient) + assert.Check(t, err != nil) + assert.Check(t, util.ContainsAll(err.Error(), "no channels found on the backend, please verify the installation")) +} + +func TestChannelListTypesErrorDynamicClient(t *testing.T) { + dynamicClient := dynamicfakeClient.CreateFakeKnDynamicClient("") + assert.Check(t, dynamicClient.Namespace() != testNamespace) + + _, err := channelFakeCmd([]string{"channel", "list-types"}, dynamicClient) + assert.Check(t, err != nil) + assert.Check(t, util.ContainsAll(err.Error(), "no channels found on the backend, please verify the installation")) +} + +func TestChannelListTypes(t *testing.T) { + dynamicClient := dynamicfakeClient.CreateFakeKnDynamicClient(testNamespace, + newChannelCRDObjWithSpec("InMemoryChannel", "messaging.knative.dev", "v1beta1", "InMemoryChannel"), + ) + assert.Equal(t, dynamicClient.Namespace(), testNamespace) + output, err := channelFakeCmd([]string{"channel", "list-types"}, dynamicClient) + assert.NilError(t, err) + assert.Check(t, util.ContainsAll(output[0], "TYPE", "NAME", "DESCRIPTION")) + assert.Check(t, util.ContainsAll(output[1], "InMemoryChannel", "InMemoryChannel")) +} + +func TestChannelListTypesNoHeaders(t *testing.T) { + dynamicClient := dynamicfakeClient.CreateFakeKnDynamicClient(testNamespace, + newChannelCRDObjWithSpec("InMemoryChannel", "messaging.knative.dev", "v1beta1", "InMemoryChannel"), + ) + assert.Equal(t, dynamicClient.Namespace(), testNamespace) + output, err := channelFakeCmd([]string{"channel", "list-types", "--no-headers"}, dynamicClient) + assert.NilError(t, err) + assert.Check(t, util.ContainsNone(output[0], "TYPE", "NAME", "DESCRIPTION")) + assert.Check(t, util.ContainsAll(output[0], "InMemoryChannel", "InMemoryChannel")) +} + +func TestListBuiltInChannelTypes(t *testing.T) { + fakeDynamic := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme()) + channel, err := listBuiltInChannelTypes(dynamic.NewKnDynamicClient(fakeDynamic, "current")) + assert.NilError(t, err) + assert.Check(t, channel != nil) + assert.Equal(t, len(channel.Items), 1) +} + +func newChannelCRDObjWithSpec(name, group, version, kind string) *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": crdGroup + "/" + crdVersion, + "kind": crdKind, + "metadata": map[string]interface{}{ + "namespace": testNamespace, + "name": name, + }, + }, + } + obj.Object["spec"] = map[string]interface{}{ + "group": group, + "version": version, + "names": map[string]interface{}{ + "kind": kind, + "plural": strings.ToLower(kind) + "s", + }, + } + obj.SetLabels(labels.Set{messaging.SubscribableDuckVersionAnnotation: channelLabelValue}) + return obj +} + +func newChannelCRDObj(name string) *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": crdGroup + "/" + crdVersion, + "kind": crdKind, + "metadata": map[string]interface{}{ + "namespace": testNamespace, + "name": name, + }, + }, + } + obj.SetLabels(labels.Set{messaging.SubscribableDuckVersionAnnotation: channelLabelValue}) + return obj +} + +func TestChannelListTypeErrors(t *testing.T) { + dynamicClient := dynamicfakeClient.CreateFakeKnDynamicClient(testNamespace, newChannelCRDObj("InMemoryChannel")) + assert.Equal(t, dynamicClient.Namespace(), testNamespace) + + output, err := channelFakeCmd([]string{"channel", "list-types"}, dynamicClient) + assert.Check(t, err != nil) + assert.Error(t, err, "can't find specs.names.kind for InMemoryChannel") + + obj := newChannelCRDObj(inMemoryChannel) + obj.Object["spec"] = map[string]interface{}{ + "group": messaging.GroupName, + "version": channelListVersion, + "names": map[string]interface{}{}, + } + dynamicClient = dynamicfakeClient.CreateFakeKnDynamicClient(testNamespace, obj) + output, err = channelFakeCmd([]string{"channel", "list-types"}, dynamicClient) + assert.Check(t, err != nil) + assert.Error(t, err, "can't find specs.names.kind for InMemoryChannel") + + obj.Object["spec"] = map[string]interface{}{ + "group": messaging.GroupName, + "version": channelListVersion, + "names": map[string]interface{}{ + "kind": true, + "plural": strings.ToLower("kind") + "s", + }, + } + + dynamicClient = dynamicfakeClient.CreateFakeKnDynamicClient(testNamespace, obj) + output, err = channelFakeCmd([]string{"channel", "list-types"}, dynamicClient) + assert.Check(t, err != nil) + assert.Error(t, err, ".spec.names.kind accessor error: true is of the type bool, expected string") + + dynamicClient = dynamicfakeClient.CreateFakeKnDynamicClient(testNamespace, + newChannelCRDObjWithSpec("InMemoryChannel", "messaging.knative.dev", "v1beta1", "InMemoryChannel"), + ) + _, err = channelFakeCmd([]string{"channel", "list-types", "--noheader"}, dynamicClient) + assert.Check(t, err != nil) + assert.Error(t, err, "unknown flag: --noheader") + + output, err = channelFakeCmd([]string{"channel", "list-types"}, dynamicClient) + assert.NilError(t, err) + assert.Check(t, util.ContainsAll(output[0], "TYPE", "NAME", "DESCRIPTION")) + assert.Check(t, util.ContainsAll(output[1], "InMemoryChannel", "InMemoryChannel")) +} diff --git a/pkg/messaging/v1beta1/client.go b/pkg/messaging/v1beta1/client.go index 9a6c4868f1..7ea635521b 100644 --- a/pkg/messaging/v1beta1/client.go +++ b/pkg/messaging/v1beta1/client.go @@ -16,6 +16,7 @@ package v1beta1 import ( "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" messagingv1beta1 "knative.dev/eventing/pkg/apis/messaging/v1beta1" "knative.dev/eventing/pkg/client/clientset/versioned/scheme" clientv1beta1 "knative.dev/eventing/pkg/client/clientset/versioned/typed/messaging/v1beta1" @@ -61,3 +62,10 @@ func (c *messagingClient) SubscriptionsClient() KnSubscriptionsClient { func updateMessagingGVK(obj runtime.Object) error { return util.UpdateGroupVersionKindWithScheme(obj, messagingv1beta1.SchemeGroupVersion, scheme.Scheme) } + +// BuiltInChannelGVKs returns the GVKs for built in channel +func BuiltInChannelGVKs() []schema.GroupVersionKind { + return []schema.GroupVersionKind{ + messagingv1beta1.SchemeGroupVersion.WithKind("InMemoryChannel"), + } +} diff --git a/test/e2e/channels_test.go b/test/e2e/channels_test.go index bebea23eaf..edacb08d1f 100644 --- a/test/e2e/channels_test.go +++ b/test/e2e/channels_test.go @@ -23,7 +23,6 @@ import ( "testing" "gotest.tools/assert" - "knative.dev/client/lib/test" "knative.dev/client/pkg/util" ) @@ -101,4 +100,13 @@ func TestChannels(t *testing.T) { test.ChannelDelete(r, "c0") test.ChannelDelete(r, "c1") test.ChannelDelete(r, "c2") + + t.Log("List channel types") + listout = test.ChannelListTypes(r) + assert.Check(t, util.ContainsAll(listout, "TYPE", "NAME", "DESCRIPTION", "InMemoryChannel")) + + t.Log("List channel types no header") + listout = test.ChannelListTypes(r, "--no-headers") + assert.Check(t, util.ContainsNone(listout, "TYPE", "NAME", "DESCRIPTION")) + assert.Check(t, util.ContainsAll(listout, "InMemoryChannel")) } From 295244dc8607e5362423f308f036a2571f92c3e9 Mon Sep 17 00:00:00 2001 From: Kaustubh pande Date: Tue, 6 Oct 2020 18:45:43 +0530 Subject: [PATCH 2/2] add kn channel list-types doc --- docs/cmd/kn_channel_list-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cmd/kn_channel_list-types.md b/docs/cmd/kn_channel_list-types.md index 42bb0f8839..e6b65ab9fd 100644 --- a/docs/cmd/kn_channel_list-types.md +++ b/docs/cmd/kn_channel_list-types.md @@ -42,5 +42,5 @@ kn channel list-types ### SEE ALSO -* [kn channel](kn_channel.md) - Manage event channels +* [kn channel](kn_channel.md) - Manage event channels (alias: channels)