Skip to content

Commit

Permalink
feature: Add 'kn source list'
Browse files Browse the repository at this point in the history
 Fixes #480

 - Add 'kn source list' listing the available sources COs
 - Use dynamic client to
       - find out available source types
       - find the COs in given namespace for each source type
  • Loading branch information
navidshaikh committed Feb 14, 2020
1 parent 8bca0bd commit 78efb62
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/cmd/kn_source.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

47 changes: 47 additions & 0 deletions docs/cmd/kn_source_list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## kn source list

List available sources

### Synopsis

List available sources

```
kn source list [flags]
```

### Examples

```
# List available eventing sources
kn source list
# List available eventing sources in YAML format
kn source list -o yaml
```

### 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].
```

### 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

58 changes: 58 additions & 0 deletions pkg/dynamic/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package dynamic

import (
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
Expand Down Expand Up @@ -43,6 +45,9 @@ type KnDynamicClient interface {
// ListSourceCRDs returns list of eventing sources CRDs
ListSourcesTypes() (*unstructured.UnstructuredList, error)

// ListSources returns list of available sources COs
ListSources() (*unstructured.UnstructuredList, error)

// RawClient returns the raw dynamic client interface
RawClient() dynamic.Interface
}
Expand Down Expand Up @@ -94,3 +99,56 @@ 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() (*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 {
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("failed to find source GVR: %v", err)
}
version, found, err := unstructured.NestedString(content, "spec", "version")
if err != nil || !found {
return gvr, fmt.Errorf("failed to find source GVR: %v", err)
}
resource, found, err := unstructured.NestedString(content, "spec", "names", "plural")
if err != nil || !found {
return gvr, fmt.Errorf("failed to find source GVR: %v", err)
}
return schema.GroupVersionResource{
Group: group,
Version: version,
Resource: resource,
}, nil
}
181 changes: 180 additions & 1 deletion pkg/kn/commands/source/human_readable_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +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"

"knative.dev/client/pkg/kn/commands"
knflags "knative.dev/client/pkg/kn/commands/flags"
eventingv1alpha1 "knative.dev/eventing/pkg/apis/legacysources/v1alpha1"

duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1"

hprinters "knative.dev/client/pkg/printers"
)

Expand All @@ -37,7 +45,7 @@ func getSourceTypeDescription(kind string) string {
return sourceTypeDescription[kind]
}

// ListTypesHandlers handles printing human readable table for `kn source list-types` command's output
// ListTypesHandlers handles printing human readable table for `kn source list-types`
func ListTypesHandlers(h hprinters.PrintHandler) {
sourceTypesColumnDefinitions := []metav1beta1.TableColumnDefinition{
{Name: "Type", Type: "string", Description: "Kind / Type of the source type", Priority: 1},
Expand All @@ -48,6 +56,23 @@ func ListTypesHandlers(h hprinters.PrintHandler) {
h.TableHandler(sourceTypesColumnDefinitions, printSourceTypesList)
}

// SourceListHandlers handles printing human readable table for `kn source list`
func SourceListHandlers(h hprinters.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: "Conditions", Type: "string", Description: "Conditions describing statuses", Priority: 1},
{Name: "Ready", Type: "string", Description: "Ready condition status", Priority: 1},
{Name: "Reason", Type: "string", Description: "Reason for non-ready condition", 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) {
name := sourceType.GetName()
Expand Down Expand Up @@ -85,3 +110,157 @@ func printSourceTypesList(sourceTypesList *unstructured.UnstructuredList, option
}
return rows, nil
}

// printSource populates a single row of source list table
func printSource(source unstructured.Unstructured, options hprinters.PrintOptions) ([]metav1beta1.TableRow, error) {
name := source.GetName()
sourceType := source.GetKind()
sourceTypeName := getSourceTypeName(source)
sink := findSink(source)

//conditions, ready, reason := getConditionsReadyReason(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)
return []metav1beta1.TableRow{row}, nil
}

// printSourceList populates the source list table rows
func printSourceList(sourceList *unstructured.UnstructuredList, options hprinters.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 eventingv1alpha1.ApiServerSource
err := json.Unmarshal(sourceJSON, &apiSource)
if err != nil {
return ""
}
return sinkToString(apiSource.Spec.Sink)
case "CronJobSource":
var cronSource eventingv1alpha1.CronJobSource
err := json.Unmarshal(sourceJSON, &cronSource)
if err != nil {
return ""
}
return sinkToString(cronSource.Spec.Sink)
case "SinkBinding":
var binding eventingv1alpha1.SinkBinding
err := json.Unmarshal(sourceJSON, &binding)
if err != nil {
return ""
}
return knflags.SinkToString(binding.Spec.Sink)
case "PingSource":
var pingSource eventingv1alpha1.SinkBinding
err := json.Unmarshal(sourceJSON, &pingSource)
if err != nil {
return ""
}
return knflags.SinkToString(pingSource.Spec.Sink)
// TODO: Find out how to find sink in untyped sources
default:
return "Unknown"
}
}

func getConditionsReadyReason(source unstructured.Unstructured) (string, string, string) {
var err error
sourceType := source.GetKind()
sourceJSON, err := source.MarshalJSON()
if err != nil {
return "Unknown", "Unknown", "Unknown"
}

switch sourceType {
case "ApiServerSource":
var tSource eventingv1alpha1.ApiServerSource
err = json.Unmarshal(sourceJSON, &tSource)
if err == nil {
return commands.ConditionsValue(tSource.Status.Conditions),
commands.ReadyCondition(tSource.Status.Conditions),
commands.NonReadyConditionReason(tSource.Status.Conditions)
}
case "CronJobSource":
var tSource eventingv1alpha1.CronJobSource
err = json.Unmarshal(sourceJSON, &tSource)
if err == nil {
return commands.ConditionsValue(tSource.Status.Conditions),
commands.ReadyCondition(tSource.Status.Conditions),
commands.NonReadyConditionReason(tSource.Status.Conditions)
}

case "SinkBinding":
var tSource eventingv1alpha1.SinkBinding
err = json.Unmarshal(sourceJSON, &tSource)
if err == nil {
return commands.ConditionsValue(tSource.Status.Conditions),
commands.ReadyCondition(tSource.Status.Conditions),
commands.NonReadyConditionReason(tSource.Status.Conditions)
}

case "PingSource":
var tSource eventingv1alpha1.SinkBinding
err = json.Unmarshal(sourceJSON, &tSource)
if err == nil {
return commands.ConditionsValue(tSource.Status.Conditions),
commands.ReadyCondition(tSource.Status.Conditions),
commands.NonReadyConditionReason(tSource.Status.Conditions)
}
}

return "Unknown", "Unknown", "Unknown"
}

// 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)
} else {
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],
)
}
Loading

0 comments on commit 78efb62

Please sign in to comment.