Repository: knative/client (go)

Issue:
kn source list - list all source instances (built-in, plugin, untyped)
kn source list should list sources of all source CRDs, unlike the normal list which only lists one type. It should have an option to list only one type, too.

See the [spec](https://docs.google.com/document/d/1B8VK-5nuxt3D-i1Up3onmUbzcztQXE0GRgf1lfx49P8/edit#bookmark=id.y3zojnunoufp) for details:

For listing all sources, kn queries all instances of source CRDs. So it first picks up all CRDs that are describing Sources and then queries each CRD type in turn to look up its COs and also checks whether this CO is supported by a typed source handler (built-in/plugin).

```
# List all created sources
$ kn source list

NAME            TYPE     RESOURCE                                 SINK    
twitter-knative twitter  twittersource.sources.k8spatterns.io     myservice 
mystorage       storage  storages.storages.events.cloud.run       broker:default
```

This list can be filtered by type (--type) or on the resource name (--source-name). 

The general algorithm described above in pseudo-code:

```
var pluginTypes map[string]string
// sourcePlugins: All kn plugins prefixed with "kn-source-"
for _, plugin := range sourcePlugins {
    // Actually execute a command on the plugin ("manifest") 
    // and parse the output. See Source Plugins for details.
    for _, crd := range plugin.CRDs() {
        pluginTypes[crd] = plugin.Name
    }
}
var crds map[string]string
// Reach out to the cluster and query for all CRDs with a certain label
for _, crd := range listK8sCRDsWithSourceLabel() {
    crds[crd.Name] = "generic"
    if plugin, ok := pluginTypes[crd.Name]; ok {
        crds[crd.Name] = plugin
    }
}
for crd, plugin := range crds {
    printCustomObjectsUsingPluginIfNotGeneric(plugin, crd, namespace)
}
```


Pull request description:
feat(source): Add 'kn source list'
/lint
Fixes #480

## Proposed Changes
- Add 'kn source list' listing the available sources COs
- Use dynamic client to find available source objects
- Add --type flag to filter source list based on source type
- Printed a source with machine readable format:
   - If source objects are of one type: `apiVersion` and `kind` is set to respective source type's object
   - If source objects are of different types in list: `apiVersion` is set to `""` and `kind` is to `List`.
   - If source objects are of different types but only one `--type` filter is given to subset source objects, the machine readable format sets `apiVersion` and `List` to respective source's type.

#### Default output:
```
kn source list
NAME   TYPE              RESOURCE                                        SINK         READY
a1     ApiServerSource   apiserversources.sources.eventing.knative.dev   svc:eshow2   True
c1     CronJobSource     cronjobsources.sources.eventing.knative.dev     svc:eshow3   True
p1     PingSource        pingsources.sources.knative.dev                 svc:eshow1   True
```

#### Using (case insensitive) type filters:

1. Single source type
```
➜  client git:(pr/source-list) kn source list --type ApiServerSource
NAME   TYPE              RESOURCE                                        SINK         READY
a1     ApiServerSource   apiserversources.sources.eventing.knative.dev   svc:eshow2   True
```

2. Multiple source types
```
➜  client git:(pr/source-list) kn source list --type ApiServerSource --type pingsource
NAME   TYPE              RESOURCE                                        SINK         READY
a1     ApiServerSource   apiserversources.sources.eventing.knative.dev   svc:eshow2   True
p1     PingSource        pingsources.sources.knative.dev                 svc:eshow1   True
```

#### See `kind` and `apiVersion` of `List` for machine readable format:

1. For multiple types of source objects
```
➜  client git:(pr/source-list) kn source list -oyaml| grep kind | tail -1
kind: List
```

```
➜  client git:(pr/source-list) kn source list -oyaml| grep apiVersion | head -1
apiVersion: ""
```

2. For single source type objects (or sub-set a single type using `--type` filter):
```
➜  client git:(pr/source-list) kn source list --type apiserversource -oyaml | grep kind | tail -1 
kind: ApiServerSourceList
```

```
➜  client git:(pr/source-list) kn source list --type apiserversource -oyaml | grep apiVersion | head -1
apiVersion: sources.eventing.knative.dev/v1alpha1
```

Patch:
diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md
index f45ccff0..3df83170 100644
--- a/.github/pull-request-template.md
+++ b/.github/pull-request-template.md
@@ -1,5 +1,5 @@
 
-## Desciption
+## Description
 
 <!-- Please add a short summary about what the pull request is going to bring on the table -->
 
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 0d6981c6..bd470c1f 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -18,6 +18,10 @@
 |===
 | | Description | PR
 
+| 🎁
+| Add `kn source list`
+| https://github.com/knative/client/pull/666[#666]
+
 | 🎁
 | Add JSON/YAML output format for version command
 | https://github.com/knative/client/pull/709[#709]
diff --git a/docs/cmd/kn_source.md b/docs/cmd/kn_source.md
index cfb73fb8..5872fe46 100644
--- a/docs/cmd/kn_source.md
+++ b/docs/cmd/kn_source.md
@@ -29,6 +29,7 @@ kn source [flags]
 * [kn](kn.md)	 - Knative client
 * [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 list](kn_source_list.md)	 - List available sources
 * [kn source list-types](kn_source_list-types.md)	 - List available source types
 * [kn source ping](kn_source_ping.md)	 - Ping source command group
 
diff --git a/docs/cmd/kn_source_list.md b/docs/cmd/kn_source_list.md
new file mode 100644
index 00000000..b9bfd557
--- /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 ~/.config/kn/config.yaml)
+      --kubeconfig string   kubectl config file (default is ~/.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 b165d83f..7941a665 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-20200304201134-fcc8ea80e26f // indirect
-	github.com/magiconair/properties v1.8.0
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/openzipkin/zipkin-go v0.2.2 // indirect
 	github.com/pkg/errors v0.8.1
diff --git a/pkg/dynamic/client.go b/pkg/dynamic/client.go
index 35d5ca52..f5ca6f41 100644
--- a/pkg/dynamic/client.go
+++ b/pkg/dynamic/client.go
@@ -20,6 +20,8 @@ import (
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/dynamic"
+
+	"knative.dev/client/pkg/util"
 )
 
 const (
@@ -43,6 +45,9 @@ type KnDynamicClient interface {
 	// ListSourceCRDs returns list of eventing sources CRDs
 	ListSourcesTypes() (*unstructured.UnstructuredList, error)
 
+	// ListSources returns list of available source objects
+	ListSources(types ...WithType) (*unstructured.UnstructuredList, error)
+
 	// RawClient returns the raw dynamic client interface
 	RawClient() dynamic.Interface
 }
@@ -94,3 +99,57 @@ func (c *knDynamicClient) ListSourcesTypes() (*unstructured.UnstructuredList, er
 func (c knDynamicClient) RawClient() dynamic.Interface {
 	return c.client
 }
+
+// ListSources returns list of available sources objects
+// Provide the list of source types as for example: WithTypes("pingsource", "apiserversource"...) to list
+// only given types of source objects
+func (c *knDynamicClient) ListSources(types ...WithType) (*unstructured.UnstructuredList, error) {
+	var (
+		sourceList               unstructured.UnstructuredList
+		options                  metav1.ListOptions
+		numberOfsourceTypesFound int
+	)
+	sourceTypes, err := c.ListSourcesTypes()
+	if err != nil {
+		return nil, err
+	}
+	namespace := c.Namespace()
+	filters := WithTypes(types).List()
+	// For each source type available, find out each source types objects
+	for _, source := range sourceTypes.Items {
+		// find source kind before hand to fail early
+		sourceKind, err := kindFromUnstructured(&source)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(filters) > 0 && !util.SliceContainsIgnoreCase(filters, sourceKind) {
+			continue
+		}
+
+		// find source's GVR from unstructured source type object
+		gvr, err := gvrFromUnstructured(&source)
+		if err != nil {
+			return nil, err
+		}
+
+		// 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
+}
diff --git a/pkg/dynamic/lib.go b/pkg/dynamic/lib.go
new file mode 100644
index 00000000..06b67756
--- /dev/null
+++ b/pkg/dynamic/lib.go
@@ -0,0 +1,125 @@
+// 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 dynamic
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// gvrFromUnstructured takes a unstructured object of CRD type and finds GVR from its spec
+func gvrFromUnstructured(u *unstructured.Unstructured) (gvr schema.GroupVersionResource, err error) {
+	group, err := groupFromUnstructured(u)
+	if err != nil {
+		return gvr, err
+	}
+
+	version, err := versionFromUnstructured(u)
+	if err != nil {
+		return gvr, err
+	}
+
+	resource, err := resourceFromUnstructured(u)
+	if err != nil {
+		return gvr, err
+	}
+
+	return schema.GroupVersionResource{
+		Group:    group,
+		Version:  version,
+		Resource: resource,
+	}, nil
+}
+
+func groupFromUnstructured(u *unstructured.Unstructured) (string, error) {
+	content := u.UnstructuredContent()
+	group, found, err := unstructured.NestedString(content, "spec", "group")
+	if err != nil || !found {
+		return "", fmt.Errorf("can't find group for source GVR: %v", err)
+	}
+	return group, nil
+}
+
+func versionFromUnstructured(u *unstructured.Unstructured) (version string, err error) {
+	content := u.UnstructuredContent()
+	versions, found, err := unstructured.NestedSlice(content, "spec", "versions")
+	if err != nil || !found || len(versions) == 0 {
+		// fallback to .spec.version
+		version, found, err = unstructured.NestedString(content, "spec", "version")
+		if err != nil || !found {
+			return version, fmt.Errorf("can't find version for source GVR: %v", err)
+		}
+	} else {
+		for _, v := range versions {
+			if vmap, ok := v.(map[string]interface{}); ok {
+				// find the version name which is being served
+				if vmap["served"] == true {
+					version = vmap["name"].(string)
+					break
+				}
+			}
+		}
+	}
+	// if we could find the version at all
+	if version == "" {
+		err = fmt.Errorf("can't find version for source GVR")
+	}
+	return version, err
+}
+
+func resourceFromUnstructured(u *unstructured.Unstructured) (string, error) {
+	content := u.UnstructuredContent()
+	resource, found, err := unstructured.NestedString(content, "spec", "names", "plural")
+	if err != nil || !found {
+		return "", fmt.Errorf("can't find resource for source GVR: %v", err)
+	}
+	return 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 from source CRD: %v", err)
+	}
+	return kind, nil
+}
+
+// TypesFilter for keeping list of sources types to filter upo
+type TypesFilter []string
+
+// WithType function for easy filtering on source types
+type WithType func(filters *TypesFilter)
+
+// WithTypes for recording the source type filtering function WithType
+type WithTypes []WithType
+
+// WithTypeFilter can be used to filter based on source type name
+func WithTypeFilter(name string) WithType {
+	return func(filters *TypesFilter) {
+		*filters = append(*filters, name)
+	}
+}
+
+// List returns the source type name list recorded via WithTypeFilter
+func (types WithTypes) List() []string {
+	var stypes TypesFilter
+	for _, f := range types {
+		f(&stypes)
+	}
+	return stypes
+}
diff --git a/pkg/kn/commands/flags/listfilters.go b/pkg/kn/commands/flags/listfilters.go
new file mode 100644
index 00000000..f2a00b3e
--- /dev/null
+++ b/pkg/kn/commands/flags/listfilters.go
@@ -0,0 +1,32 @@
+// 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 flags
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+// SourceTypeFilters defines flags used for kn source list to filter sources on types
+type SourceTypeFilters struct {
+	Filters []string
+}
+
+// Add attaches the SourceTypeFilters flags to given command
+func (s *SourceTypeFilters) Add(cmd *cobra.Command, what string) {
+	usage := fmt.Sprintf("Filter list on given %s. This flag can be given multiple times.", what)
+	cmd.Flags().StringSliceVarP(&s.Filters, "type", "t", nil, usage)
+}
diff --git a/pkg/kn/commands/source/duck/multisourcelist.go b/pkg/kn/commands/source/duck/multisourcelist.go
new file mode 100644
index 00000000..dd02f908
--- /dev/null
+++ b/pkg/kn/commands/source/duck/multisourcelist.go
@@ -0,0 +1,177 @@
+// 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 duck
+
+import (
+	"fmt"
+	"strings"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	eventinglegacy "knative.dev/eventing/pkg/apis/legacysources/v1alpha1"
+	sourcesv1alpha1 "knative.dev/eventing/pkg/apis/sources/v1alpha1"
+	pkgduck "knative.dev/pkg/apis/duck"
+	pkgduckv1beta1 "knative.dev/pkg/apis/duck/v1beta1"
+
+	"knative.dev/client/pkg/kn/commands"
+	"knative.dev/client/pkg/kn/commands/flags"
+)
+
+// Source struct holds common properties between different eventing sources
+// which we want to print for commands like 'kn source list'.
+// The properties held in this struct is meant for simple access by human readable
+// printer function
+type Source struct {
+	metav1.TypeMeta
+	// Name of the created source object
+	Name string
+	// Namespace of object, used for printing with namespace for example: 'kn source list -A'
+	Namespace string
+	// Kind of the source object created
+	SourceKind string
+	// Resource this source object represent
+	Resource string
+	// Sink configured for this source object
+	Sink string
+	// String representation if source is ready
+	Ready string
+}
+
+// SourceList for holding list of Source type objects
+type SourceList struct {
+	metav1.TypeMeta
+	Items []Source
+}
+
+const DSListKind = "List"
+
+// GetNamespace returns the namespace of the Source, used for printing
+// sources with namespace for commands like 'kn source list -A'
+func (s *Source) GetNamespace() string { return s.Namespace }
+
+// DeepCopyObject noop method to satisfy Object interface
+func (s *Source) DeepCopyObject() runtime.Object { return s }
+
+// DeepCopyObject noop method to satisfy Object interface
+func (s *SourceList) DeepCopyObject() runtime.Object { return s }
+
+// toSource transforms eventing source object received as Unstructured object
+// into Source object
+func toSource(u *unstructured.Unstructured) Source {
+	ds := Source{}
+	ds.Name = u.GetName()
+	ds.Namespace = u.GetNamespace()
+	ds.SourceKind = u.GetKind()
+	ds.Resource = getSourceTypeName(u)
+	ds.Sink = findSink(u)
+	ds.Ready = isReady(u)
+	// set empty GVK
+	ds.APIVersion, ds.Kind = schema.GroupVersionKind{}.ToAPIVersionAndKind()
+	return ds
+}
+
+// ToSourceList transforms list of eventing sources objects received as
+// UnstructuredList object into SourceList object
+func ToSourceList(uList *unstructured.UnstructuredList) *SourceList {
+	dsl := SourceList{Items: []Source{}}
+	//dsl.Items = make(Source, 0, len(uList.Items))
+	for _, u := range uList.Items {
+		dsl.Items = append(dsl.Items, toSource(&u))
+	}
+	// set empty group, version and non empty kind
+	dsl.APIVersion, dsl.Kind = schema.GroupVersion{}.WithKind(DSListKind).ToAPIVersionAndKind()
+	return &dsl
+}
+
+func getSourceTypeName(source *unstructured.Unstructured) string {
+	return fmt.Sprintf("%s%s.%s",
+		strings.ToLower(source.GetKind()),
+		"s",
+		strings.Split(source.GetAPIVersion(), "/")[0],
+	)
+}
+
+func findSink(source *unstructured.Unstructured) string {
+	switch source.GetKind() {
+	case "ApiServerSource":
+		var apiSource eventinglegacy.ApiServerSource
+		if err := pkgduck.FromUnstructured(source, &apiSource); err == nil {
+			return sinkToString(apiSource.Spec.Sink)
+		}
+	case "CronJobSource":
+		var cronSource eventinglegacy.CronJobSource
+		if err := pkgduck.FromUnstructured(source, &cronSource); err == nil {
+			return sinkToString(cronSource.Spec.Sink)
+		}
+	case "SinkBinding":
+		var binding sourcesv1alpha1.SinkBinding
+		if err := pkgduck.FromUnstructured(source, &binding); err == nil {
+			return flags.SinkToString(binding.Spec.Sink)
+		}
+	case "PingSource":
+		var pingSource sourcesv1alpha1.PingSource
+		if err := pkgduck.FromUnstructured(source, &pingSource); err == nil {
+			return flags.SinkToString(*pingSource.Spec.Sink)
+		}
+	}
+	// TODO: Find out how to find sink in untyped sources
+	return "<unknown>"
+}
+
+func isReady(source *unstructured.Unstructured) string {
+	switch source.GetKind() {
+	case "ApiServerSource":
+		var tSource eventinglegacy.ApiServerSource
+		if err := pkgduck.FromUnstructured(source, &tSource); err == nil {
+			return commands.ReadyCondition(tSource.Status.Conditions)
+		}
+	case "CronJobSource":
+		var tSource eventinglegacy.CronJobSource
+		if err := pkgduck.FromUnstructured(source, &tSource); err == nil {
+			return commands.ReadyCondition(tSource.Status.Conditions)
+		}
+	case "SinkBinding":
+		var tSource eventinglegacy.SinkBinding
+		if err := pkgduck.FromUnstructured(source, &tSource); err == nil {
+			return commands.ReadyCondition(tSource.Status.Conditions)
+		}
+	case "PingSource":
+		var tSource sourcesv1alpha1.PingSource
+		if err := pkgduck.FromUnstructured(source, &tSource); err == nil {
+			return commands.ReadyCondition(tSource.Status.Conditions)
+		}
+	}
+	// TODO: Find out how to find ready conditions for untyped sources
+	return "<unknown>"
+}
+
+// temporary sinkToString for deprecated sources
+func sinkToString(sink *pkgduckv1beta1.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 ""
+}
diff --git a/pkg/kn/commands/source/human_readable_flags.go b/pkg/kn/commands/source/human_readable_flags.go
index 539704f4..ba85125a 100644
--- a/pkg/kn/commands/source/human_readable_flags.go
+++ b/pkg/kn/commands/source/human_readable_flags.go
@@ -22,7 +22,8 @@ import (
 	metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
 	"k8s.io/apimachinery/pkg/runtime"
 
-	hprinters "knative.dev/client/pkg/printers"
+	clientduck "knative.dev/client/pkg/kn/commands/source/duck"
+	"knative.dev/client/pkg/printers"
 )
 
 var sourceTypeDescription = map[string]string{
@@ -31,12 +32,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},
@@ -46,8 +43,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")
@@ -62,12 +72,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 {
@@ -83,3 +93,41 @@ func printSourceTypesList(sourceTypesList *unstructured.UnstructuredList, option
 	}
 	return rows, nil
 }
+
+// printSource populates a single row of source list table
+func printSource(source *clientduck.Source, options printers.PrintOptions) ([]metav1beta1.TableRow, error) {
+	row := metav1beta1.TableRow{
+		Object: runtime.RawExtension{Object: source},
+	}
+
+	if options.AllNamespaces {
+		row.Cells = append(row.Cells, source.GetNamespace())
+	}
+
+	row.Cells = append(row.Cells,
+		source.Name,
+		source.SourceKind,
+		source.Resource,
+		source.Sink,
+		source.Ready,
+	)
+	return []metav1beta1.TableRow{row}, nil
+}
+
+// printSourceList populates the source list table rows
+func printSourceList(sourceList *clientduck.SourceList, 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].Name < sourceList.Items[j].Name
+	})
+	for _, source := range sourceList.Items {
+		row, err := printSource(&source, options)
+		if err != nil {
+			return nil, err
+		}
+
+		rows = append(rows, row...)
+	}
+	return rows, nil
+}
diff --git a/pkg/kn/commands/source/list.go b/pkg/kn/commands/source/list.go
new file mode 100644
index 00000000..943143d4
--- /dev/null
+++ b/pkg/kn/commands/source/list.go
@@ -0,0 +1,91 @@
+// 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"
+	"knative.dev/client/pkg/kn/commands/source/duck"
+)
+
+var listExample = `
+  # 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`
+
+// NewListCommand defines and processes `kn source list`
+func NewListCommand(p *commands.KnParams) *cobra.Command {
+	filterFlags := &flags.SourceTypeFilters{}
+	listFlags := flags.NewListPrintFlags(ListHandlers)
+	listCommand := &cobra.Command{
+		Use:     "list",
+		Short:   "List available sources",
+		Example: listExample,
+		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
+			}
+			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
+			}
+			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
+			}
+			if listFlags.GenericPrintFlags.OutputFlagSpecified() {
+				return printer.PrintObj(sourceList, cmd.OutOrStdout())
+			}
+			// Convert the source list to DuckSourceList only if human readable table printing requested
+			sourceDuckList := duck.ToSourceList(sourceList)
+			err = printer.PrintObj(sourceDuckList, cmd.OutOrStdout())
+			if err != nil {
+				return err
+			}
+			return nil
+		},
+	}
+	commands.AddNamespaceFlags(listCommand.Flags(), true)
+	listFlags.AddFlags(listCommand)
+	filterFlags.Add(listCommand, "source type")
+	return listCommand
+}
diff --git a/pkg/kn/commands/source/list_types.go b/pkg/kn/commands/source/list_types.go
index 38c7c82e..c10074f0 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/source.go b/pkg/kn/commands/source/source.go
index d90f709c..0e08a56e 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(ping.NewPingCommand(p))
 	sourceCmd.AddCommand(binding.NewBindingCommand(p))
 	return sourceCmd
diff --git a/pkg/util/compare.go b/pkg/util/compare.go
index 252cc217..0d28395f 100644
--- a/pkg/util/compare.go
+++ b/pkg/util/compare.go
@@ -73,3 +73,13 @@ func ContainsNone(target string, substrings ...string) cmp.Comparison {
 		return cmp.ResultSuccess
 	}
 }
+
+// SliceContainsIgnoreCase checks (case insensitive) if given target string is present in slice
+func SliceContainsIgnoreCase(slice []string, target string) bool {
+	for _, each := range slice {
+		if strings.EqualFold(target, each) {
+			return true
+		}
+	}
+	return false
+}
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 d0f27046..00000000
--- 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 d47c3a75..79ab3380 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -78,7 +78,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


Test patch:
diff --git a/pkg/dynamic/client_test.go b/pkg/dynamic/client_test.go
index 077508f9..55f7b1ec 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,107 @@ func TestListSourceTypes(t *testing.T) {
 	})
 }
 
+func TestListSources(t *testing.T) {
+	t.Run("No GVRs set", func(t *testing.T) {
+		obj := newSourceCRDObj("foo")
+		client := createFakeKnDynamicClient(testNamespace, obj)
+		assert.Check(t, client.RawClient() != nil)
+		_, err := client.ListSources()
+		assert.Check(t, err != nil)
+		assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "source", "kind", "CRD"))
+	})
+
+	t.Run("source list empty", func(t *testing.T) {
+		client := createFakeKnDynamicClient(testNamespace,
+			newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"),
+		)
+		sources, err := client.ListSources()
+		assert.NilError(t, err)
+		assert.Equal(t, len(sources.Items), 0)
+	})
+
+	t.Run("source list non empty", func(t *testing.T) {
+		client := createFakeKnDynamicClient(testNamespace,
+			newSourceCRDObjWithSpec("pingsources", "sources.knative.dev", "v1alpha1", "PingSource"),
+			newSourceCRDObjWithSpec("apiserversources", "sources.knative.dev", "v1alpha1", "ApiServerSource"),
+			newSourceCRDObjWithSpec("cronjobsources", "sources.knative.dev", "v1alpha1", "CronJobSource"),
+			newSourceUnstructuredObj("p1", "sources.knative.dev/v1alpha1", "PingSource"),
+			newSourceUnstructuredObj("a1", "sources.knative.dev/v1alpha1", "ApiServerSource"),
+			newSourceUnstructuredObj("c1", "sources.knative.dev/v1alpha1", "CronJobSource"),
+		)
+		sources, err := client.ListSources(WithTypeFilter("pingsource"), WithTypeFilter("ApiServerSource"))
+		assert.NilError(t, err)
+		assert.Equal(t, len(sources.Items), 2)
+	})
+}
+
 // 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 {
 	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/dynamic/lib_test.go b/pkg/dynamic/lib_test.go
new file mode 100644
index 00000000..7ed81c1d
--- /dev/null
+++ b/pkg/dynamic/lib_test.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 dynamic
+
+import (
+	"testing"
+
+	"gotest.tools/assert"
+
+	"knative.dev/client/pkg/util"
+)
+
+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 TestGVRFromUnstructured(t *testing.T) {
+	obj := newSourceCRDObj("foo")
+
+	obj.Object["spec"] = map[string]interface{}{}
+	_, err := gvrFromUnstructured(obj)
+	assert.Check(t, err != nil)
+	assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "group"))
+
+	obj.Object["spec"] = map[string]interface{}{
+		"group": "sources.knative.dev",
+	}
+	_, err = gvrFromUnstructured(obj)
+	assert.Check(t, err != nil)
+	assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "version"))
+
+	// with deprecated CRD field spec version
+	obj.Object["spec"] = map[string]interface{}{
+		"group":   "sources.knative.dev",
+		"version": "v1alpha1",
+	}
+	_, err = gvrFromUnstructured(obj)
+	assert.Check(t, err != nil)
+	assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "resource"))
+
+	// with CRD field spec versions
+	obj.Object["spec"] = map[string]interface{}{
+		"group": "sources.knative.dev",
+		"versions": []interface{}{
+			map[string]interface{}{"name": "v1alpha1", "served": true},
+		},
+	}
+	_, err = gvrFromUnstructured(obj)
+	assert.Check(t, err != nil)
+	assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "resource"))
+
+	obj.Object["spec"] = map[string]interface{}{
+		"group": "sources.knative.dev",
+		"versions": []interface{}{
+			map[string]interface{}{},
+		},
+	}
+	_, err = gvrFromUnstructured(obj)
+	assert.Check(t, err != nil)
+	assert.Check(t, util.ContainsAll(err.Error(), "can't", "find", "version"))
+}
diff --git a/pkg/kn/commands/flags/listfilters_test.go b/pkg/kn/commands/flags/listfilters_test.go
new file mode 100644
index 00000000..a51e828f
--- /dev/null
+++ b/pkg/kn/commands/flags/listfilters_test.go
@@ -0,0 +1,29 @@
+// 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 flags
+
+import (
+	"testing"
+
+	"github.com/spf13/cobra"
+	"gotest.tools/assert"
+)
+
+func TestSourceListFlags(t *testing.T) {
+	filters := &SourceTypeFilters{}
+	cmd := &cobra.Command{}
+	filters.Add(cmd, "foo")
+	assert.Check(t, cmd.Flag("type") != nil)
+}
diff --git a/pkg/kn/commands/source/list_test.go b/pkg/kn/commands/source/list_test.go
new file mode 100644
index 00000000..30c15a84
--- /dev/null
+++ b/pkg/kn/commands/source/list_test.go
@@ -0,0 +1,145 @@
+// 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 registered to dynamic client
+func sourceFakeCmd(args []string, objects ...runtime.Object) (output []string, err error) {
+	knParams := &commands.KnParams{}
+	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.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.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.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.knative.dev", "svc:foo", "<unknown>"))
+	assert.Check(t, util.ContainsAll(output[2], "c1", "CronJobSource", "cronjobsources.sources.eventing.knative.dev", "svc:foo", "<unknown>"))
+	assert.Check(t, util.ContainsAll(output[3], "p1", "PingSource", "pingsources.sources.knative.dev", "svc:foo", "<unknown>"))
+	assert.Check(t, util.ContainsAll(output[4], "s1", "SinkBinding", "sinkbindings.sources.knative.dev", "svc:foo", "<unknown>"))
+}
+
+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_test.go b/pkg/kn/commands/source/list_types_test.go
deleted file mode 100644
index ef55c473..00000000
--- 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/util/compare_test.go b/pkg/util/compare_test.go
index 04c1f494..55552831 100644
--- a/pkg/util/compare_test.go
+++ b/pkg/util/compare_test.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 	"testing"
 
+	"gotest.tools/assert"
 	"gotest.tools/assert/cmp"
 )
 
@@ -146,3 +147,12 @@ func TestContainsNone(t *testing.T) {
 		}
 	}
 }
+
+func TestSliceContainsIgnoreCase(t *testing.T) {
+	assert.Equal(t,
+		SliceContainsIgnoreCase([]string{"FOO", "bar"}, "foo"),
+		true)
+	assert.Equal(t,
+		SliceContainsIgnoreCase([]string{"BAR", "bar"}, "foo"),
+		false)
+}
diff --git a/test/e2e/source_apiserver_test.go b/test/e2e/source_apiserver_test.go
index 2a943e77..cf94091f 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.ContainsAll(output, "NAME", "TYPE", "RESOURCE", "SINK", "READY"))
+	assert.Check(t, util.ContainsAll(output, "testapisource0", "ApiServerSource", "apiserversources.sources.knative.dev", "svc:testsvc0"))
+	assert.Check(t, util.ContainsAll(output, "testapisource1", "ApiServerSource", "apiserversources.sources.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 5a4fd8cf..faceaa1b 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", "Ping", "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
+}