diff --git a/cmd/component.go b/cmd/component.go new file mode 100644 index 0000000000000000000000000000000000000000..9fae44c9bc01f5e9960d12f1438e271c04a9857c --- /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 043fd039b998372d8632c04efdc46614692c19d8..23cd79afaf38f1ab9dfbdc0d3c8912989824a3c6 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 0000000000000000000000000000000000000000..2a860d9dd9fa8b7ab7e7f745dd6960ef54624ce5 --- /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 0000000000000000000000000000000000000000..0f7722cb4c9e9f69176c80f2d19587a8b373e648 --- /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 991cc48d0ec3d02ef11f9c36025b43451e975972..e8a6f6611d65e87afbc5949cbc42428d658f3455 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 770643479acd95b90eb56bdaaa78ad0dcfc0d41c..c4a244a507b125f97e2c57454c6128323c5eef13 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 560e4faacd3d1da34c63970b77209b917b82e1af..e406a5715e3990d0b3b2927e71573dbd7c9d4d93 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 0000000000000000000000000000000000000000..cdb16d93c7e9b8ae533fb9a45c7b5f00c3370a35 --- /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 0000000000000000000000000000000000000000..e2cc526a422d11b6873617c5a78cfc763f366d86 --- /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) + } +}