diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c7d3bf4cdb..3945439285 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -20,6 +20,10 @@ |=== | | Description | PR +| 🎁 +| Add `kn source list` +| https://github.com/knative/client/pull/666[#666] + | 🧽 | Support multiple revisions on `kn revision delete` | https://github.com/knative/client/pull/657[#657] diff --git a/docs/cmd/kn_source.md b/docs/cmd/kn_source.md index 60aa5102f5..489cea59f5 100644 --- a/docs/cmd/kn_source.md +++ b/docs/cmd/kn_source.md @@ -30,5 +30,6 @@ kn source [flags] * [kn source apiserver](kn_source_apiserver.md) - Kubernetes API Server Event Source command group * [kn source binding](kn_source_binding.md) - Sink binding command group * [kn source cronjob](kn_source_cronjob.md) - CronJob source command group +* [kn source list](kn_source_list.md) - List available sources * [kn source list-types](kn_source_list-types.md) - List available source types diff --git a/docs/cmd/kn_source_list.md b/docs/cmd/kn_source_list.md new file mode 100644 index 0000000000..479344787d --- /dev/null +++ b/docs/cmd/kn_source_list.md @@ -0,0 +1,51 @@ +## kn source list + +List available sources + +### Synopsis + +List available sources + +``` +kn source list [flags] +``` + +### Examples + +``` + + # List available eventing sources + kn source list + + # List PingSource type sources + kn source list --type=PingSource + + # List PingSource and ApiServerSource types sources + kn source list --type=PingSource --type=apiserversource +``` + +### Options + +``` + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + --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 + -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]. + -t, --type strings Filter list on given source type. This flag can be given multiple times. +``` + +### Options inherited from parent commands + +``` + --config string kn config file (default is $HOME/.kn/config.yaml) + --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --log-http log http traffic +``` + +### SEE ALSO + +* [kn source](kn_source.md) - Event source command group + diff --git a/go.mod b/go.mod index 09b20bed92..0a22d21ada 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ require ( contrib.go.opencensus.io/exporter/prometheus v0.1.0 // indirect contrib.go.opencensus.io/exporter/stackdriver v0.13.0 // indirect github.com/google/go-containerregistry v0.0.0-20200212224832-c629a66d7231 // indirect - github.com/magiconair/properties v1.8.0 github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/openzipkin/zipkin-go v0.2.2 // indirect diff --git a/pkg/dynamic/client.go b/pkg/dynamic/client.go index 35d5ca52bc..1fc6e91a10 100644 --- a/pkg/dynamic/client.go +++ b/pkg/dynamic/client.go @@ -15,6 +15,10 @@ package dynamic import ( + "fmt" + "strings" + + "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -31,6 +35,16 @@ const ( sourcesLabelValue = "true" ) +// SourceListFilters defines flags used for kn source list to filter sources on types +type SourceListFilters struct { + filters []string +} + +// Add attaches the SourceListFilters flags to given command +func (s *SourceListFilters) Add(cmd *cobra.Command) { + cmd.Flags().StringSliceVarP(&s.filters, "type", "t", nil, "Filter list on given source type. This flag can be given multiple times.") +} + // KnDynamicClient to client-go Dynamic client. All methods are relative to the // namespace specified during construction type KnDynamicClient interface { @@ -43,6 +57,9 @@ type KnDynamicClient interface { // ListSourceCRDs returns list of eventing sources CRDs ListSourcesTypes() (*unstructured.UnstructuredList, error) + // ListSources returns list of available sources COs + ListSources(f *SourceListFilters) (*unstructured.UnstructuredList, error) + // RawClient returns the raw dynamic client interface RawClient() dynamic.Interface } @@ -94,3 +111,86 @@ func (c *knDynamicClient) ListSourcesTypes() (*unstructured.UnstructuredList, er func (c knDynamicClient) RawClient() dynamic.Interface { return c.client } + +// ListSources returns list of available sources COs +func (c *knDynamicClient) ListSources(f *SourceListFilters) (*unstructured.UnstructuredList, error) { + var sourceList unstructured.UnstructuredList + options := metav1.ListOptions{} + sourceTypes, err := c.ListSourcesTypes() + if err != nil { + return nil, err + } + + namespace := c.Namespace() + // For each source type available, find out CO + for _, source := range sourceTypes.Items { + // only find COs if this source type is given in filter + if f != nil && f.filters != nil { + // find source kind before hand to fail early + sourceKind, err := kindFromUnstructured(&source) + if err != nil { + return nil, err + } + + // if this source is not given in filter flags continue + if !sliceContainsIgnoreCase(sourceKind, f.filters) { + continue + } + } + + gvr, err := gvrFromUnstructured(&source) + if err != nil { + return nil, err + } + + sList, err := c.client.Resource(gvr).Namespace(namespace).List(options) + if err != nil { + return nil, err + } + + if len(sList.Items) > 0 { + sourceList.Items = append(sourceList.Items, sList.Items...) + } + sourceList.SetGroupVersionKind(sList.GetObjectKind().GroupVersionKind()) + } + return &sourceList, nil +} + +func gvrFromUnstructured(u *unstructured.Unstructured) (gvr schema.GroupVersionResource, err error) { + content := u.UnstructuredContent() + group, found, err := unstructured.NestedString(content, "spec", "group") + if err != nil || !found { + return gvr, fmt.Errorf("can't find source GVR: %v", err) + } + version, found, err := unstructured.NestedString(content, "spec", "version") + if err != nil || !found { + return gvr, fmt.Errorf("can't find source GVR: %v", err) + } + resource, found, err := unstructured.NestedString(content, "spec", "names", "plural") + if err != nil || !found { + return gvr, fmt.Errorf("can't find source GVR: %v", err) + } + return schema.GroupVersionResource{ + Group: group, + Version: version, + Resource: resource, + }, nil +} + +func kindFromUnstructured(u *unstructured.Unstructured) (string, error) { + content := u.UnstructuredContent() + kind, found, err := unstructured.NestedString(content, "spec", "names", "kind") + if !found || err != nil { + return "", fmt.Errorf("can't find source kind: %v", err) + } + return kind, nil +} + +func sliceContainsIgnoreCase(s string, slice []string) bool { + for _, each := range slice { + if strings.EqualFold(s, each) { + return true + } + } + return false +} diff --git a/pkg/dynamic/client_test.go b/pkg/dynamic/client_test.go index fa6104cc6f..951c594852 100644 --- a/pkg/dynamic/client_test.go +++ b/pkg/dynamic/client_test.go @@ -15,9 +15,10 @@ package dynamic import ( + "strings" "testing" - "github.com/magiconair/properties/assert" + "gotest.tools/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -26,45 +27,29 @@ import ( dynamicfake "k8s.io/client-go/dynamic/fake" eventingv1alpha1 "knative.dev/eventing/pkg/apis/eventing/v1alpha1" servingv1 "knative.dev/serving/pkg/apis/serving/v1" -) -const testNamespace = "testns" + "knative.dev/client/pkg/util" +) -func newUnstructured(name string) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": crdGroup + "/" + crdVersion, - "kind": crdKind, - "metadata": map[string]interface{}{ - "namespace": testNamespace, - "name": name, - "labels": map[string]interface{}{ - sourcesLabelKey: sourcesLabelValue, - }, - }, - }, - } -} +const testNamespace = "current" func TestNamespace(t *testing.T) { - client := createFakeKnDynamicClient(testNamespace, newUnstructured("foo")) + client := createFakeKnDynamicClient(testNamespace, newSourceCRDObj("foo")) assert.Equal(t, client.Namespace(), testNamespace) } func TestListCRDs(t *testing.T) { client := createFakeKnDynamicClient( testNamespace, - newUnstructured("foo"), - newUnstructured("bar"), + newSourceCRDObj("foo"), + newSourceCRDObj("bar"), ) + assert.Check(t, client.RawClient() != nil) t.Run("List CRDs with match", func(t *testing.T) { options := metav1.ListOptions{} uList, err := client.ListCRDs(options) - if err != nil { - t.Fatal(err) - } - + assert.NilError(t, err) assert.Equal(t, len(uList.Items), 2) }) @@ -84,8 +69,8 @@ func TestListCRDs(t *testing.T) { func TestListSourceTypes(t *testing.T) { client := createFakeKnDynamicClient( testNamespace, - newUnstructured("foo"), - newUnstructured("bar"), + newSourceCRDObj("foo"), + newSourceCRDObj("bar"), ) t.Run("List source types", func(t *testing.T) { @@ -100,12 +85,140 @@ func TestListSourceTypes(t *testing.T) { }) } +func TestListSources(t *testing.T) { + t.Run("No GVRs set", func(t *testing.T) { + var f *SourceListFilters + obj := newSourceCRDObj("foo") + client := createFakeKnDynamicClient(testNamespace, obj) + assert.Check(t, client.RawClient() != nil) + _, err := client.ListSources(f) + assert.Check(t, err != nil) + assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "GVR")) + }) + + t.Run("source list empty", func(t *testing.T) { + var f *SourceListFilters + client := createFakeKnDynamicClient(testNamespace, + newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), + ) + sources, err := client.ListSources(f) + assert.NilError(t, err) + assert.Equal(t, len(sources.Items), 0) + }) + + t.Run("source list non empty", func(t *testing.T) { + var f *SourceListFilters + client := createFakeKnDynamicClient(testNamespace, + newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), + newSourceUnstructuredObj("p1", "sources.knative.dev/v1alpha1", "PingSource"), + ) + sources, err := client.ListSources(f) + assert.NilError(t, err) + assert.Equal(t, len(sources.Items), 1) + }) +} + +func TestKindFromUnstructured(t *testing.T) { + kind, err := kindFromUnstructured( + newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), + ) + assert.NilError(t, err) + assert.Equal(t, kind, "PingSource") + _, err = kindFromUnstructured(newSourceCRDObj("foo")) + assert.Check(t, err != nil) +} + +func TestSliceContainsIgnoreCase(t *testing.T) { + assert.Equal(t, + sliceContainsIgnoreCase("foo", []string{"FOO", "bar"}), + true) + assert.Equal(t, + sliceContainsIgnoreCase("foo", []string{"BAR", "bar"}), + false) +} + +func TestGVRFromUnstructured(t *testing.T) { + obj := newSourceCRDObj("foo") + obj.Object["spec"] = map[string]interface{}{ + "group": "sources.knative.dev", + } + _, err := gvrFromUnstructured(obj) + assert.Check(t, err != nil) + obj.Object["spec"] = map[string]interface{}{ + "group": "sources.knative.dev", + "version": "v1alpha1", + } + _, err = gvrFromUnstructured(obj) + assert.Check(t, err != nil) +} + // createFakeKnDynamicClient gives you a dynamic client for testing contianing 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 { 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"}, &eventingv1alpha1.Broker{}) + client := dynamicfake.NewSimpleDynamicClient(scheme, objects...) return NewKnDynamicClient(client, testNamespace) } + +func newSourceCRDObj(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{sourcesLabelKey: sourcesLabelValue}) + return obj +} + +func newSourceCRDObjWithSpec(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{sourcesLabelKey: sourcesLabelValue}) + return obj +} + +func newSourceUnstructuredObj(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{}{ + "kind": "Service", + "name": "foo", + }, + }, + }, + }, + } +} diff --git a/pkg/kn/commands/source/human_readable_flags.go b/pkg/kn/commands/source/human_readable_flags.go index e7548519db..6d48b17dfb 100644 --- a/pkg/kn/commands/source/human_readable_flags.go +++ b/pkg/kn/commands/source/human_readable_flags.go @@ -15,14 +15,21 @@ package source import ( + "encoding/json" "fmt" "sort" + "strings" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" + eventinglegacy "knative.dev/eventing/pkg/apis/legacysources/v1alpha1" + sourcesv1alpha1 "knative.dev/eventing/pkg/apis/sources/v1alpha1" + duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" - hprinters "knative.dev/client/pkg/printers" + "knative.dev/client/pkg/kn/commands" + "knative.dev/client/pkg/kn/commands/flags" + "knative.dev/client/pkg/printers" ) var sourceTypeDescription = map[string]string{ @@ -33,12 +40,8 @@ var sourceTypeDescription = map[string]string{ "PingSource": "Send periodically ping events to a sink", } -func getSourceTypeDescription(kind string) string { - return sourceTypeDescription[kind] -} - -// ListTypesHandlers handles printing human readable table for `kn source list-types` command's output -func ListTypesHandlers(h hprinters.PrintHandler) { +// ListTypesHandlers handles printing human readable table for `kn source list-types` +func ListTypesHandlers(h printers.PrintHandler) { sourceTypesColumnDefinitions := []metav1beta1.TableColumnDefinition{ {Name: "Type", Type: "string", Description: "Kind / Type of the source type", Priority: 1}, {Name: "Name", Type: "string", Description: "Name of the source type", Priority: 1}, @@ -48,8 +51,21 @@ func ListTypesHandlers(h hprinters.PrintHandler) { h.TableHandler(sourceTypesColumnDefinitions, printSourceTypesList) } +// ListHandlers handles printing human readable table for `kn source list` +func ListHandlers(h printers.PrintHandler) { + sourceListColumnDefinitions := []metav1beta1.TableColumnDefinition{ + {Name: "Name", Type: "string", Description: "Name of the created source", Priority: 1}, + {Name: "Type", Type: "string", Description: "Type of the source", Priority: 1}, + {Name: "Resource", Type: "string", Description: "Source type name", Priority: 1}, + {Name: "Sink", Type: "string", Description: "Sink of the source", Priority: 1}, + {Name: "Ready", Type: "string", Description: "Ready condition status", Priority: 1}, + } + h.TableHandler(sourceListColumnDefinitions, printSource) + h.TableHandler(sourceListColumnDefinitions, printSourceList) +} + // printSourceTypes populates a single row of source types list table -func printSourceTypes(sourceType unstructured.Unstructured, options hprinters.PrintOptions) ([]metav1beta1.TableRow, error) { +func printSourceTypes(sourceType unstructured.Unstructured, options printers.PrintOptions) ([]metav1beta1.TableRow, error) { name := sourceType.GetName() content := sourceType.UnstructuredContent() kind, found, err := unstructured.NestedString(content, "spec", "names", "kind") @@ -64,12 +80,12 @@ func printSourceTypes(sourceType unstructured.Unstructured, options hprinters.Pr row := metav1beta1.TableRow{ Object: runtime.RawExtension{Object: &sourceType}, } - row.Cells = append(row.Cells, kind, name, getSourceTypeDescription(kind)) + row.Cells = append(row.Cells, kind, name, sourceTypeDescription[kind]) return []metav1beta1.TableRow{row}, nil } // printSourceTypesList populates the source types list table rows -func printSourceTypesList(sourceTypesList *unstructured.UnstructuredList, options hprinters.PrintOptions) ([]metav1beta1.TableRow, error) { +func printSourceTypesList(sourceTypesList *unstructured.UnstructuredList, options printers.PrintOptions) ([]metav1beta1.TableRow, error) { rows := make([]metav1beta1.TableRow, 0, len(sourceTypesList.Items)) sort.SliceStable(sourceTypesList.Items, func(i, j int) bool { @@ -85,3 +101,148 @@ func printSourceTypesList(sourceTypesList *unstructured.UnstructuredList, option } return rows, nil } + +// printSource populates a single row of source list table +func printSource(source unstructured.Unstructured, options printers.PrintOptions) ([]metav1beta1.TableRow, error) { + name := source.GetName() + sourceType := source.GetKind() + sourceTypeName := getSourceTypeName(source) + sink := findSink(source) + ready := isReady(source) + + row := metav1beta1.TableRow{ + Object: runtime.RawExtension{Object: &source}, + } + + if options.AllNamespaces { + row.Cells = append(row.Cells, source.GetNamespace()) + } + + row.Cells = append(row.Cells, name, sourceType, sourceTypeName, sink, ready) + return []metav1beta1.TableRow{row}, nil +} + +// printSourceList populates the source list table rows +func printSourceList(sourceList *unstructured.UnstructuredList, options printers.PrintOptions) ([]metav1beta1.TableRow, error) { + rows := make([]metav1beta1.TableRow, 0, len(sourceList.Items)) + + sort.SliceStable(sourceList.Items, func(i, j int) bool { + return sourceList.Items[i].GetName() < sourceList.Items[j].GetName() + }) + for _, item := range sourceList.Items { + row, err := printSource(item, options) + if err != nil { + return nil, err + } + + rows = append(rows, row...) + } + return rows, nil +} + +func findSink(source unstructured.Unstructured) string { + sourceType := source.GetKind() + sourceJSON, err := source.MarshalJSON() + if err != nil { + return "" + } + + switch sourceType { + case "ApiServerSource": + var apiSource eventinglegacy.ApiServerSource + err := json.Unmarshal(sourceJSON, &apiSource) + if err != nil { + return "" + } + return sinkToString(apiSource.Spec.Sink) + case "CronJobSource": + var cronSource eventinglegacy.CronJobSource + err := json.Unmarshal(sourceJSON, &cronSource) + if err != nil { + return "" + } + return sinkToString(cronSource.Spec.Sink) + case "SinkBinding": + var binding sourcesv1alpha1.SinkBinding + err := json.Unmarshal(sourceJSON, &binding) + if err != nil { + return "" + } + return flags.SinkToString(binding.Spec.Sink) + case "PingSource": + var pingSource sourcesv1alpha1.PingSource + err := json.Unmarshal(sourceJSON, &pingSource) + if err != nil { + return "" + } + return flags.SinkToString(*pingSource.Spec.Sink) + // TODO: Find out how to find sink in untyped sources + default: + return "" + } +} + +func isReady(source unstructured.Unstructured) string { + var err error + sourceType := source.GetKind() + sourceJSON, err := source.MarshalJSON() + if err != nil { + return "" + } + + switch sourceType { + case "ApiServerSource": + var tSource eventinglegacy.ApiServerSource + err = json.Unmarshal(sourceJSON, &tSource) + if err == nil { + return commands.ReadyCondition(tSource.Status.Conditions) + } + case "CronJobSource": + var tSource eventinglegacy.CronJobSource + err = json.Unmarshal(sourceJSON, &tSource) + if err == nil { + return commands.ReadyCondition(tSource.Status.Conditions) + } + + case "SinkBinding": + var tSource eventinglegacy.SinkBinding + err = json.Unmarshal(sourceJSON, &tSource) + if err == nil { + return commands.ReadyCondition(tSource.Status.Conditions) + } + + case "PingSource": + var tSource sourcesv1alpha1.PingSource + err = json.Unmarshal(sourceJSON, &tSource) + if err == nil { + return commands.ReadyCondition(tSource.Status.Conditions) + } + } + + return "" +} + +// temporary sinkToString for deprecated sources +func sinkToString(sink *duckv1beta1.Destination) string { + if sink != nil { + if sink.Ref != nil { + if sink.Ref.Kind == "Service" { + return fmt.Sprintf("svc:%s", sink.Ref.Name) + } + return fmt.Sprintf("%s:%s", strings.ToLower(sink.Ref.Kind), sink.Ref.Name) + } + + if sink.URI != nil { + return sink.URI.String() + } + } + return "" +} + +func getSourceTypeName(source unstructured.Unstructured) string { + return fmt.Sprintf("%s%s.%s", + strings.ToLower(source.GetKind()), + "s", + strings.Split(source.GetAPIVersion(), "/")[0], + ) +} diff --git a/pkg/kn/commands/source/list.go b/pkg/kn/commands/source/list.go new file mode 100644 index 0000000000..2f157e171e --- /dev/null +++ b/pkg/kn/commands/source/list.go @@ -0,0 +1,79 @@ +// 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 source + +import ( + "fmt" + + "github.com/spf13/cobra" + + "knative.dev/client/pkg/dynamic" + "knative.dev/client/pkg/kn/commands" + "knative.dev/client/pkg/kn/commands/flags" +) + +// NewListCommand defines and processes `kn source list` +func NewListCommand(p *commands.KnParams) *cobra.Command { + filterFlags := &dynamic.SourceListFilters{} + listFlags := flags.NewListPrintFlags(ListHandlers) + listCommand := &cobra.Command{ + Use: "list", + Short: "List available sources", + Example: ` + # List available eventing sources + kn source list + + # List PingSource type sources + kn source list --type=PingSource + + # List PingSource and ApiServerSource types sources + kn source list --type=PingSource --type=apiserversource`, + 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 + } + sourceList, err := dynamicClient.ListSources(filterFlags) + if err != nil { + return err + } + if len(sourceList.Items) == 0 { + fmt.Fprintf(cmd.OutOrStdout(), "No sources found in %s namespace.\n", namespace) + return nil + } + // empty namespace indicates all-namespaces flag is specified + if namespace == "" { + listFlags.EnsureWithNamespace() + } + printer, err := listFlags.ToPrinter() + if err != nil { + return nil + } + err = printer.PrintObj(sourceList, cmd.OutOrStdout()) + if err != nil { + return err + } + return nil + }, + } + commands.AddNamespaceFlags(listCommand.Flags(), true) + listFlags.AddFlags(listCommand) + filterFlags.Add(listCommand) + return listCommand +} diff --git a/pkg/kn/commands/source/list_test.go b/pkg/kn/commands/source/list_test.go new file mode 100644 index 0000000000..d0cbaea2df --- /dev/null +++ b/pkg/kn/commands/source/list_test.go @@ -0,0 +1,150 @@ +// Copyright © 2019 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 source + +import ( + "strings" + "testing" + + "gotest.tools/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + + "knative.dev/client/pkg/kn/commands" + "knative.dev/client/pkg/util" +) + +const ( + crdGroup = "apiextensions.k8s.io" + crdVersion = "v1beta1" + crdKind = "CustomResourceDefinition" + crdKinds = "customresourcedefinitions" + sourcesLabelKey = "duck.knative.dev/source" + sourcesLabelValue = "true" + testNamespace = "current" +) + +// sourceFakeCmd takes cmd to be executed using dynamic client +// pass the objects to be registed to dynamic clients +func sourceFakeCmd(args []string, objects ...runtime.Object) (output []string, err error) { + knParams := &commands.KnParams{} + // not using the fake dynamic client returned here + cmd, _, buf := commands.CreateDynamicTestKnCommand(NewSourceCommand(knParams), knParams, objects...) + + cmd.SetArgs(args) + err = cmd.Execute() + if err != nil { + return + } + + output = strings.Split(buf.String(), "\n") + return +} + +func TestSourceListTypes(t *testing.T) { + output, err := sourceFakeCmd([]string{"source", "list-types"}, + newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), + newSourceCRDObjWithSpec("apiserversources", "sources.eventing.knative.dev", "v1alpha1", "ApiServerSource"), + ) + assert.NilError(t, err) + assert.Check(t, util.ContainsAll(output[0], "TYPE", "NAME", "DESCRIPTION")) + assert.Check(t, util.ContainsAll(output[1], "ApiServerSource", "apiserversources")) + assert.Check(t, util.ContainsAll(output[2], "PingSource", "pingsources")) +} + +func TestSourceListTypesNoHeaders(t *testing.T) { + output, err := sourceFakeCmd([]string{"source", "list-types", "--no-headers"}, + newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), + ) + assert.NilError(t, err) + assert.Check(t, util.ContainsNone(output[0], "TYPE", "NAME", "DESCRIPTION")) + assert.Check(t, util.ContainsAll(output[0], "PingSource")) +} + +func TestSourceList(t *testing.T) { + output, err := sourceFakeCmd([]string{"source", "list"}, + newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), + newSourceCRDObjWithSpec("sinkbindings", "sources.knative.dev", "v1alpha1", "SinkBinding"), + newSourceCRDObjWithSpec("apiserversources", "sources.eventing.knative.dev", "v1alpha1", "ApiServerSource"), + newSourceCRDObjWithSpec("cronjobsources", "sources.eventing.knative.dev", "v1alpha1", "CronJobSource"), + newSourceUnstructuredObj("p1", "sources.knative.dev/v1alpha1", "PingSource"), + newSourceUnstructuredObj("s1", "sources.knative.dev/v1alpha1", "SinkBinding"), + newSourceUnstructuredObj("a1", "sources.eventing.knative.dev/v1alpha1", "ApiServerSource"), + newSourceUnstructuredObj("c1", "sources.eventing.knative.dev/v1alpha1", "CronJobSource"), + ) + assert.NilError(t, err) + assert.Check(t, util.ContainsAll(output[0], "NAME", "TYPE", "RESOURCE", "SINK", "READY")) + assert.Check(t, util.ContainsAll(output[1], "a1", "ApiServerSource", "apiserversources.sources.eventing.knative.dev", "svc:foo", "")) + assert.Check(t, util.ContainsAll(output[2], "c1", "CronJobSource", "cronjobsources.sources.eventing.knative.dev", "svc:foo", "")) + assert.Check(t, util.ContainsAll(output[3], "p1", "PingSource", "pingsources.sources.knative.dev", "svc:foo", "")) + assert.Check(t, util.ContainsAll(output[4], "s1", "SinkBinding", "sinkbindings.sources.knative.dev", "svc:foo", "")) + +} + +func TestSourceListNoHeaders(t *testing.T) { + output, err := sourceFakeCmd([]string{"source", "list", "--no-headers"}, + newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"), + newSourceUnstructuredObj("p1", "sources.knative.dev/v1alpha1", "PingSource"), + ) + assert.NilError(t, err) + assert.Check(t, util.ContainsNone(output[0], "NAME", "TYPE", "RESOURCE", "SINK", "READY")) + assert.Check(t, util.ContainsAll(output[0], "p1")) +} + +func newSourceCRDObjWithSpec(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{sourcesLabelKey: sourcesLabelValue}) + return obj +} + +func newSourceUnstructuredObj(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{}{ + "kind": "Service", + "name": "foo", + }, + }, + }, + }, + } +} diff --git a/pkg/kn/commands/source/list_types.go b/pkg/kn/commands/source/list_types.go index 067a703217..7375386e7d 100644 --- a/pkg/kn/commands/source/list_types.go +++ b/pkg/kn/commands/source/list_types.go @@ -18,11 +18,12 @@ import ( "fmt" "github.com/spf13/cobra" + "knative.dev/client/pkg/kn/commands" "knative.dev/client/pkg/kn/commands/flags" ) -// NewListTypesCommand defines and processes `kn source list-types` command operations +// NewListTypesCommand defines and processes `kn source list-types` func NewListTypesCommand(p *commands.KnParams) *cobra.Command { listTypesFlags := flags.NewListPrintFlags(ListTypesHandlers) listTypesCommand := &cobra.Command{ diff --git a/pkg/kn/commands/source/list_types_test.go b/pkg/kn/commands/source/list_types_test.go deleted file mode 100644 index ef55c4730d..0000000000 --- a/pkg/kn/commands/source/list_types_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright © 2019 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 source - -import ( - "gotest.tools/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "knative.dev/client/pkg/kn/commands" - "knative.dev/client/pkg/util" - - "strings" - "testing" -) - -func newUnstructured(name string) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apiextensions.k8s.io/v1beta1", - "kind": "CustomResourceDefinition", - "metadata": map[string]interface{}{ - "namespace": "current", - "name": name, - "labels": map[string]interface{}{ - "duck.knative.dev/source": "true", - }, - }, - }, - } -} - -func newUnstructuredWithSpecNames(name string, value map[string]interface{}) *unstructured.Unstructured { - u := newUnstructured(name) - u.Object["spec"] = map[string]interface{}{"names": value} - return u -} - -func fakeListTypes(args []string, objects ...runtime.Object) (output []string, err error) { - knParams := &commands.KnParams{} - // not using the fake dynamic client returned here - cmd, _, buf := commands.CreateDynamicTestKnCommand(NewSourceCommand(knParams), knParams, objects...) - - cmd.SetArgs(args) - err = cmd.Execute() - if err != nil { - return - } - - output = strings.Split(buf.String(), "\n") - return -} - -func TestSourceListTypes(t *testing.T) { - output, err := fakeListTypes([]string{"source", "list-types"}, - newUnstructuredWithSpecNames("foo.in", map[string]interface{}{"kind": "foo"}), - newUnstructuredWithSpecNames("bar.in", map[string]interface{}{"kind": "bar"}), - ) - assert.NilError(t, err) - assert.Check(t, util.ContainsAll(output[0], "TYPE", "NAME", "DESCRIPTION")) - assert.Check(t, util.ContainsAll(output[1], "bar", "bar.in")) - assert.Check(t, util.ContainsAll(output[2], "foo", "foo.in")) -} - -func TestSourceListTypesNoHeaders(t *testing.T) { - output, err := fakeListTypes([]string{"source", "list-types", "--no-headers"}, - newUnstructuredWithSpecNames("foo.in", map[string]interface{}{"kind": "foo"}), - newUnstructuredWithSpecNames("bar.in", map[string]interface{}{"kind": "bar"}), - ) - assert.NilError(t, err) - assert.Check(t, util.ContainsNone(output[0], "TYPE", "NAME", "DESCRIPTION")) -} diff --git a/pkg/kn/commands/source/source.go b/pkg/kn/commands/source/source.go index 7aa3f71cde..d72516a936 100644 --- a/pkg/kn/commands/source/source.go +++ b/pkg/kn/commands/source/source.go @@ -28,8 +28,9 @@ func NewSourceCommand(p *commands.KnParams) *cobra.Command { Use: "source", Short: "Event source command group", } - sourceCmd.AddCommand(apiserver.NewAPIServerCommand(p)) sourceCmd.AddCommand(NewListTypesCommand(p)) + sourceCmd.AddCommand(NewListCommand(p)) + sourceCmd.AddCommand(apiserver.NewAPIServerCommand(p)) sourceCmd.AddCommand(cronjob.NewCronJobCommand(p)) sourceCmd.AddCommand(binding.NewBindingCommand(p)) return sourceCmd diff --git a/test/e2e/source_apiserver_test.go b/test/e2e/source_apiserver_test.go index 2e065d1deb..ec629b7ee0 100644 --- a/test/e2e/source_apiserver_test.go +++ b/test/e2e/source_apiserver_test.go @@ -24,6 +24,7 @@ import ( "github.com/pkg/errors" "gotest.tools/assert" + "knative.dev/client/pkg/util" ) @@ -55,6 +56,16 @@ func TestSourceApiServer(t *testing.T) { test.apiServerSourceCreate(t, r, "testapisource0", "Event:v1:true", "testsa", "svc:testsvc0") test.apiServerSourceCreate(t, r, "testapisource1", "Event:v1", "testsa", "svc:testsvc0") + t.Log("list sources") + output := test.sourceList(t, r) + assert.Check(t, util.ContainsNone(output, "NAME", "TYPE", "RESOURCE", "SINK", "READY")) + assert.Check(t, util.ContainsAll(output, "testapisource0", "ApiServerSource", "apiserversources.sources.eventing.knative.dev", "svc:testsvc0")) + assert.Check(t, util.ContainsAll(output, "testapisource1", "ApiServerSource", "apiserversources.sources.eventing.knative.dev", "svc:testsvc0")) + + t.Log("list sources in YAML format") + output = test.sourceList(t, r, "-oyaml") + assert.Check(t, util.ContainsAll(output, "testapisource1", "ApiServerSource", "Service", "testsvc0")) + t.Log("delete apiserver sources") test.apiServerSourceDelete(t, r, "testapisource0") test.apiServerSourceDelete(t, r, "testapisource1") diff --git a/test/e2e/source_list_types_test.go b/test/e2e/source_list_test.go similarity index 66% rename from test/e2e/source_list_types_test.go rename to test/e2e/source_list_test.go index e8f998cb00..2f2ab97a54 100644 --- a/test/e2e/source_list_types_test.go +++ b/test/e2e/source_list_test.go @@ -21,6 +21,7 @@ import ( "testing" "gotest.tools/assert" + "knative.dev/client/pkg/util" ) @@ -45,9 +46,35 @@ func TestSourceListTypes(t *testing.T) { assert.Check(t, util.ContainsAll(output, "apiextensions.k8s.io/v1beta1", "CustomResourceDefinition", "CronJob", "ApiServer")) } +func TestSourceList(t *testing.T) { + t.Parallel() + test, err := NewE2eTest() + assert.NilError(t, err) + defer func() { + assert.NilError(t, test.Teardown()) + }() + + r := NewKnRunResultCollector(t) + defer r.DumpIfFailed() + + t.Log("List sources empty case") + output := test.sourceList(t, r) + assert.Check(t, util.ContainsAll(output, "No", "sources", "found", "namespace")) + assert.Check(t, util.ContainsNone(output, "NAME", "TYPE", "RESOURCE", "SINK", "READY")) + + // non empty list case is tested in test/e2e/source_apiserver_test.go where source setup is present +} + func (test *e2eTest) sourceListTypes(t *testing.T, r *KnRunResultCollector, args ...string) string { cmd := append([]string{"source", "list-types"}, args...) out := test.kn.Run(cmd...) r.AssertNoError(out) return out.Stdout } + +func (test *e2eTest) sourceList(t *testing.T, r *KnRunResultCollector, args ...string) string { + cmd := append([]string{"source", "list"}, args...) + out := test.kn.Run(cmd...) + r.AssertNoError(out) + return out.Stdout +} diff --git a/vendor/github.com/magiconair/properties/assert/assert.go b/vendor/github.com/magiconair/properties/assert/assert.go deleted file mode 100644 index d0f2704670..0000000000 --- a/vendor/github.com/magiconair/properties/assert/assert.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2018 Frank Schroeder. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package assert provides helper functions for testing. -package assert - -import ( - "fmt" - "path/filepath" - "reflect" - "regexp" - "runtime" - "strings" - "testing" -) - -// skip defines the default call depth -const skip = 2 - -// Equal asserts that got and want are equal as defined by -// reflect.DeepEqual. The test fails with msg if they are not equal. -func Equal(t *testing.T, got, want interface{}, msg ...string) { - if x := equal(2, got, want, msg...); x != "" { - fmt.Println(x) - t.Fail() - } -} - -func equal(skip int, got, want interface{}, msg ...string) string { - if !reflect.DeepEqual(got, want) { - return fail(skip, "got %v want %v %s", got, want, strings.Join(msg, " ")) - } - return "" -} - -// Panic asserts that function fn() panics. -// It assumes that recover() either returns a string or -// an error and fails if the message does not match -// the regular expression in 'matches'. -func Panic(t *testing.T, fn func(), matches string) { - if x := doesPanic(2, fn, matches); x != "" { - fmt.Println(x) - t.Fail() - } -} - -func doesPanic(skip int, fn func(), expr string) (err string) { - defer func() { - r := recover() - if r == nil { - err = fail(skip, "did not panic") - return - } - var v string - switch r.(type) { - case error: - v = r.(error).Error() - case string: - v = r.(string) - } - err = matches(skip, v, expr) - }() - fn() - return "" -} - -// Matches asserts that a value matches a given regular expression. -func Matches(t *testing.T, value, expr string) { - if x := matches(2, value, expr); x != "" { - fmt.Println(x) - t.Fail() - } -} - -func matches(skip int, value, expr string) string { - ok, err := regexp.MatchString(expr, value) - if err != nil { - return fail(skip, "invalid pattern %q. %s", expr, err) - } - if !ok { - return fail(skip, "got %s which does not match %s", value, expr) - } - return "" -} - -func fail(skip int, format string, args ...interface{}) string { - _, file, line, _ := runtime.Caller(skip) - return fmt.Sprintf("\t%s:%d: %s\n", filepath.Base(file), line, fmt.Sprintf(format, args...)) -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 3a13617aa4..50a2de9818 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -151,7 +151,6 @@ github.com/json-iterator/go github.com/liggitt/tabwriter # github.com/magiconair/properties v1.8.0 github.com/magiconair/properties -github.com/magiconair/properties/assert # github.com/mailru/easyjson v0.7.0 github.com/mailru/easyjson/buffer github.com/mailru/easyjson/jlexer