From fe3962e383505bf2ce9f6e02d08ea6da4800f386 Mon Sep 17 00:00:00 2001
From: Jessica Yuen <im.jessicayuen@gmail.com>
Date: Fri, 5 Jan 2018 13:20:24 -0800
Subject: [PATCH] Introduce 'component' commands, starting with 'component
 list'

This adds a high level 'component' command and a 'component list'
command. 'component list' will pretty print all the components in
ksonnet application directory.

To accomplish this, an API is added to the metadata manager that returns
all components. Components are the individual files in /components, with
the path extension trimmed.

Signed-off-by: Jessica Yuen <im.jessicayuen@gmail.com>
---
 cmd/component.go                        | 64 ++++++++++++++++++++++
 docs/cli-reference/ks.md                |  1 +
 docs/cli-reference/ks_component.md      | 23 ++++++++
 docs/cli-reference/ks_component_list.md | 34 ++++++++++++
 metadata/component.go                   | 23 +++++++-
 metadata/component_test.go              | 61 ++++++++++++++++++---
 metadata/interface.go                   |  1 +
 pkg/kubecfg/component.go                | 73 +++++++++++++++++++++++++
 pkg/kubecfg/component_test.go           | 61 +++++++++++++++++++++
 9 files changed, 331 insertions(+), 10 deletions(-)
 create mode 100644 cmd/component.go
 create mode 100644 docs/cli-reference/ks_component.md
 create mode 100644 docs/cli-reference/ks_component_list.md
 create mode 100644 pkg/kubecfg/component.go
 create mode 100644 pkg/kubecfg/component_test.go

diff --git a/cmd/component.go b/cmd/component.go
new file mode 100644
index 00000000..9fae44c9
--- /dev/null
+++ b/cmd/component.go
@@ -0,0 +1,64 @@
+// Copyright 2017 The ksonnet 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 cmd
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"github.com/ksonnet/ksonnet/pkg/kubecfg"
+)
+
+func init() {
+	RootCmd.AddCommand(componentCmd)
+
+	componentCmd.AddCommand(componentListCmd)
+}
+
+var componentCmd = &cobra.Command{
+	Use:   "component",
+	Short: "Manage ksonnet components",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) != 0 {
+			return fmt.Errorf("%s is not a valid subcommand\n\n%s", strings.Join(args, " "), cmd.UsageString())
+		}
+		return fmt.Errorf("Command 'component' requires a subcommand\n\n%s", cmd.UsageString())
+	},
+}
+
+var componentListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List known components",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) != 0 {
+			return fmt.Errorf("'component list' takes zero arguments")
+		}
+
+		c := kubecfg.NewComponentListCmd()
+
+		return c.Run(cmd.OutOrStdout())
+	},
+	Long: `
+The ` + "`list`" + ` command displays all known components.
+
+### Syntax
+`,
+	Example: `
+# List all components
+ks component list`,
+}
diff --git a/docs/cli-reference/ks.md b/docs/cli-reference/ks.md
index 043fd039..23cd79af 100644
--- a/docs/cli-reference/ks.md
+++ b/docs/cli-reference/ks.md
@@ -20,6 +20,7 @@ application configuration to remote clusters.
 
 ### SEE ALSO
 * [ks apply](ks_apply.md)	 - Apply local Kubernetes manifests (components) to remote clusters
+* [ks component](ks_component.md)	 - Manage ksonnet components
 * [ks delete](ks_delete.md)	 - Remove component-specified Kubernetes resources from remote clusters
 * [ks diff](ks_diff.md)	 - Compare manifests, based on environment or location (local or remote)
 * [ks env](ks_env.md)	 - Manage ksonnet environments
diff --git a/docs/cli-reference/ks_component.md b/docs/cli-reference/ks_component.md
new file mode 100644
index 00000000..2a860d9d
--- /dev/null
+++ b/docs/cli-reference/ks_component.md
@@ -0,0 +1,23 @@
+## ks component
+
+Manage ksonnet components
+
+### Synopsis
+
+
+Manage ksonnet components
+
+```
+ks component
+```
+
+### Options inherited from parent commands
+
+```
+  -v, --verbose count[=-1]   Increase verbosity. May be given multiple times.
+```
+
+### SEE ALSO
+* [ks](ks.md)	 - Configure your application to deploy to a Kubernetes cluster
+* [ks component list](ks_component_list.md)	 - List known components
+
diff --git a/docs/cli-reference/ks_component_list.md b/docs/cli-reference/ks_component_list.md
new file mode 100644
index 00000000..0f7722cb
--- /dev/null
+++ b/docs/cli-reference/ks_component_list.md
@@ -0,0 +1,34 @@
+## ks component list
+
+List known components
+
+### Synopsis
+
+
+
+The `list` command displays all known components.
+
+### Syntax
+
+
+```
+ks component list
+```
+
+### Examples
+
+```
+
+# List all components
+ks component list
+```
+
+### Options inherited from parent commands
+
+```
+  -v, --verbose count[=-1]   Increase verbosity. May be given multiple times.
+```
+
+### SEE ALSO
+* [ks component](ks_component.md)	 - Manage ksonnet components
+
diff --git a/metadata/component.go b/metadata/component.go
index 991cc48d..e8a6f661 100644
--- a/metadata/component.go
+++ b/metadata/component.go
@@ -18,6 +18,7 @@ package metadata
 import (
 	"fmt"
 	"os"
+	"path"
 	"strings"
 
 	param "github.com/ksonnet/ksonnet/metadata/params"
@@ -28,13 +29,14 @@ import (
 
 func (m *manager) ComponentPaths() (AbsPaths, error) {
 	paths := AbsPaths{}
-	err := afero.Walk(m.appFS, string(m.componentsPath), func(path string, info os.FileInfo, err error) error {
+	err := afero.Walk(m.appFS, string(m.componentsPath), func(p string, info os.FileInfo, err error) error {
 		if err != nil {
 			return err
 		}
 
-		if !info.IsDir() {
-			paths = append(paths, path)
+		// Only add file paths and exclude the params.libsonnet file
+		if !info.IsDir() && path.Base(p) != componentParamsFile {
+			paths = append(paths, p)
 		}
 		return nil
 	})
@@ -45,6 +47,21 @@ func (m *manager) ComponentPaths() (AbsPaths, error) {
 	return paths, nil
 }
 
+func (m *manager) GetAllComponents() ([]string, error) {
+	componentPaths, err := m.ComponentPaths()
+	if err != nil {
+		return nil, err
+	}
+
+	var components []string
+	for _, p := range componentPaths {
+		component := strings.TrimSuffix(path.Base(p), path.Ext(p))
+		components = append(components, component)
+	}
+
+	return components, nil
+}
+
 func (m *manager) CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error {
 	if !isValidName(name) || strings.Contains(name, "/") {
 		return fmt.Errorf("Component name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
diff --git a/metadata/component_test.go b/metadata/component_test.go
index 77064347..c4a244a5 100644
--- a/metadata/component_test.go
+++ b/metadata/component_test.go
@@ -18,16 +18,24 @@ import (
 	"fmt"
 	"os"
 	"sort"
+	"strings"
 	"testing"
 )
 
-func TestComponentPaths(t *testing.T) {
+const (
+	componentsPath  = "/componentsPath"
+	componentSubdir = "subdir"
+	componentFile1  = "component1.jsonnet"
+	componentFile2  = "component2.jsonnet"
+)
+
+func populateComponentPaths(t *testing.T) *manager {
 	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
 	if err != nil {
 		t.Fatalf("Failed to parse cluster spec: %v", err)
 	}
 
-	appPath := AbsPath("/componentPaths")
+	appPath := AbsPath(componentsPath)
 	reg := newMockRegistryManager("incubator")
 	m, err := initManager("componentPaths", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS)
 	if err != nil {
@@ -36,7 +44,7 @@ func TestComponentPaths(t *testing.T) {
 
 	// Create empty app file.
 	components := appendToAbsPath(appPath, componentsDir)
-	appFile1 := appendToAbsPath(components, "component1.jsonnet")
+	appFile1 := appendToAbsPath(components, componentFile1)
 	f1, err := testFS.OpenFile(string(appFile1), os.O_RDONLY|os.O_CREATE, 0777)
 	if err != nil {
 		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
@@ -44,12 +52,12 @@ func TestComponentPaths(t *testing.T) {
 	f1.Close()
 
 	// Create empty file in a nested directory.
-	appSubdir := appendToAbsPath(components, "appSubdir")
+	appSubdir := appendToAbsPath(components, componentSubdir)
 	err = testFS.MkdirAll(string(appSubdir), os.ModePerm)
 	if err != nil {
 		t.Fatalf("Failed to create directory '%s'\n%v", appSubdir, err)
 	}
-	appFile2 := appendToAbsPath(appSubdir, "component2.jsonnet")
+	appFile2 := appendToAbsPath(appSubdir, componentFile2)
 	f2, err := testFS.OpenFile(string(appFile2), os.O_RDONLY|os.O_CREATE, 0777)
 	if err != nil {
 		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
@@ -63,6 +71,17 @@ func TestComponentPaths(t *testing.T) {
 		t.Fatalf("Failed to create directory '%s'\n%v", unlistedDir, err)
 	}
 
+	return m
+}
+
+func cleanComponentPaths(t *testing.T) {
+	testFS.RemoveAll(componentsPath)
+}
+
+func TestComponentPaths(t *testing.T) {
+	m := populateComponentPaths(t)
+	defer cleanComponentPaths(t)
+
 	paths, err := m.ComponentPaths()
 	if err != nil {
 		t.Fatalf("Failed to find component paths: %v", err)
@@ -70,7 +89,35 @@ func TestComponentPaths(t *testing.T) {
 
 	sort.Slice(paths, func(i, j int) bool { return paths[i] < paths[j] })
 
-	if len(paths) != 3 || paths[0] != string(appFile2) || paths[1] != string(appFile1) {
-		t.Fatalf("m.ComponentPaths failed; expected '%s', got '%s'", []string{string(appFile1), string(appFile2)}, paths)
+	expectedPath1 := fmt.Sprintf("%s/components/%s", componentsPath, componentFile1)
+	expectedPath2 := fmt.Sprintf("%s/components/%s/%s", componentsPath, componentSubdir, componentFile2)
+
+	if len(paths) != 2 || paths[0] != expectedPath1 || paths[1] != expectedPath2 {
+		t.Fatalf("m.ComponentPaths failed; expected '%s', got '%s'", []string{expectedPath1, expectedPath2}, paths)
+	}
+}
+
+func TestGetAllComponents(t *testing.T) {
+	m := populateComponentPaths(t)
+	defer cleanComponentPaths(t)
+
+	components, err := m.GetAllComponents()
+	if err != nil {
+		t.Fatalf("Failed to get all components, %v", err)
+	}
+
+	expected1 := strings.TrimSuffix(componentFile1, ".jsonnet")
+	expected2 := strings.TrimSuffix(componentFile2, ".jsonnet")
+
+	if len(components) != 2 {
+		t.Fatalf("Expected exactly 2 components, got %d", len(components))
+	}
+
+	if components[0] != expected1 {
+		t.Fatalf("Expected component %s, got %s", expected1, components)
+	}
+
+	if components[1] != expected2 {
+		t.Fatalf("Expected component %s, got %s", expected2, components)
 	}
 }
diff --git a/metadata/interface.go b/metadata/interface.go
index 560e4faa..e406a571 100644
--- a/metadata/interface.go
+++ b/metadata/interface.go
@@ -49,6 +49,7 @@ type Manager interface {
 
 	// Components API.
 	ComponentPaths() (AbsPaths, error)
+	GetAllComponents() ([]string, error)
 	CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error
 
 	// Params API.
diff --git a/pkg/kubecfg/component.go b/pkg/kubecfg/component.go
new file mode 100644
index 00000000..cdb16d93
--- /dev/null
+++ b/pkg/kubecfg/component.go
@@ -0,0 +1,73 @@
+// Copyright 2017 The kubecfg 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 kubecfg
+
+import (
+	"fmt"
+	"io"
+	"sort"
+	"strings"
+
+	"github.com/ksonnet/ksonnet/utils"
+)
+
+const (
+	componentNameHeader = "COMPONENT"
+)
+
+// ComponentListCmd stores the information necessary to display components.
+type ComponentListCmd struct {
+}
+
+// NewComponentListCmd acts as a constructor for ComponentListCmd.
+func NewComponentListCmd() *ComponentListCmd {
+	return &ComponentListCmd{}
+}
+
+// Run executes the displaying of components.
+func (c *ComponentListCmd) Run(out io.Writer) error {
+	manager, err := manager()
+	if err != nil {
+		return err
+	}
+
+	components, err := manager.GetAllComponents()
+	if err != nil {
+		return err
+	}
+
+	_, err = printComponents(out, components)
+	return err
+}
+
+func printComponents(out io.Writer, components []string) (string, error) {
+	rows := [][]string{
+		[]string{componentNameHeader},
+		[]string{strings.Repeat("=", len(componentNameHeader))},
+	}
+
+	sort.Strings(components)
+	for _, component := range components {
+		rows = append(rows, []string{component})
+	}
+
+	formatted, err := utils.PadRows(rows)
+	if err != nil {
+		return "", err
+	}
+	_, err = fmt.Fprint(out, formatted)
+	return formatted, err
+}
diff --git a/pkg/kubecfg/component_test.go b/pkg/kubecfg/component_test.go
new file mode 100644
index 00000000..e2cc526a
--- /dev/null
+++ b/pkg/kubecfg/component_test.go
@@ -0,0 +1,61 @@
+// Copyright 2017 The ksonnet 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 kubecfg
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestPrintComponents(t *testing.T) {
+	for _, tc := range []struct {
+		components []string
+		expected   string
+	}{
+		{
+			components: []string{"a", "b"},
+			expected: `COMPONENT
+=========
+a
+b
+`,
+		},
+		// Check that components are displayed in alphabetical order
+		{
+			components: []string{"b", "a"},
+			expected: `COMPONENT
+=========
+a
+b
+`,
+		},
+		// Check empty components scenario
+		{
+			components: []string{},
+			expected: `COMPONENT
+=========
+`,
+		},
+	} {
+		out, err := printComponents(os.Stdout, tc.components)
+		if err != nil {
+			t.Fatalf(err.Error())
+		}
+		require.EqualValues(t, tc.expected, out)
+	}
+}
-- 
GitLab