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 new file mode 100644 index 0000000000000000000000000000000000000000..e8a6f6611d65e87afbc5949cbc42428d658f3455 --- /dev/null +++ b/metadata/component.go @@ -0,0 +1,96 @@ +// 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 metadata + +import ( + "fmt" + "os" + "path" + "strings" + + param "github.com/ksonnet/ksonnet/metadata/params" + "github.com/ksonnet/ksonnet/prototype" + log "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +func (m *manager) ComponentPaths() (AbsPaths, error) { + paths := AbsPaths{} + err := afero.Walk(m.appFS, string(m.componentsPath), func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Only add file paths and exclude the params.libsonnet file + if !info.IsDir() && path.Base(p) != componentParamsFile { + paths = append(paths, p) + } + return nil + }) + if err != nil { + return nil, err + } + + 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) + } + + componentPath := string(appendToAbsPath(m.componentsPath, name)) + switch templateType { + case prototype.YAML: + componentPath = componentPath + ".yaml" + case prototype.JSON: + componentPath = componentPath + ".json" + case prototype.Jsonnet: + componentPath = componentPath + ".jsonnet" + default: + return fmt.Errorf("Unrecognized prototype template type '%s'", templateType) + } + + if exists, err := afero.Exists(m.appFS, componentPath); exists { + return fmt.Errorf("Component with name '%s' already exists", name) + } else if err != nil { + return fmt.Errorf("Could not check whether component '%s' exists:\n\n%v", name, err) + } + + log.Infof("Writing component at '%s/%s'", componentsDir, name) + err := afero.WriteFile(m.appFS, componentPath, []byte(text), defaultFilePermissions) + if err != nil { + return err + } + + log.Debugf("Writing component parameters at '%s/%s", componentsDir, name) + return m.writeComponentParams(name, params) +} diff --git a/metadata/component_test.go b/metadata/component_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c4a244a507b125f97e2c57454c6128323c5eef13 --- /dev/null +++ b/metadata/component_test.go @@ -0,0 +1,123 @@ +// 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 metadata + +import ( + "fmt" + "os" + "sort" + "strings" + "testing" +) + +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(componentsPath) + reg := newMockRegistryManager("incubator") + m, err := initManager("componentPaths", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS) + if err != nil { + t.Fatalf("Failed to init cluster spec: %v", err) + } + + // Create empty app file. + components := appendToAbsPath(appPath, componentsDir) + 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) + } + f1.Close() + + // Create empty file in a nested directory. + 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, 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) + } + f2.Close() + + // Create a directory that won't be listed in the call to `ComponentPaths`. + unlistedDir := string(appendToAbsPath(components, "doNotListMe")) + err = testFS.MkdirAll(unlistedDir, os.ModePerm) + if err != nil { + 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) + } + + sort.Slice(paths, func(i, j int) bool { return paths[i] < paths[j] }) + + 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/metadata/manager.go b/metadata/manager.go index 0865e10529401252c0b018d4f333cc6105113f6b..4aab3ffeff6dc5e9c25673a2ee52132107ac8ebb 100644 --- a/metadata/manager.go +++ b/metadata/manager.go @@ -17,17 +17,14 @@ package metadata import ( "fmt" - "os" "os/user" "path" "path/filepath" - "strings" "github.com/ghodss/yaml" "github.com/ksonnet/ksonnet/metadata/app" param "github.com/ksonnet/ksonnet/metadata/params" "github.com/ksonnet/ksonnet/metadata/registry" - "github.com/ksonnet/ksonnet/prototype" log "github.com/sirupsen/logrus" "github.com/spf13/afero" ) @@ -200,58 +197,6 @@ func (m *manager) Root() AbsPath { return m.rootPath } -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 { - if err != nil { - return err - } - - if !info.IsDir() { - paths = append(paths, path) - } - return nil - }) - if err != nil { - return nil, err - } - - return paths, 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) - } - - componentPath := string(appendToAbsPath(m.componentsPath, name)) - switch templateType { - case prototype.YAML: - componentPath = componentPath + ".yaml" - case prototype.JSON: - componentPath = componentPath + ".json" - case prototype.Jsonnet: - componentPath = componentPath + ".jsonnet" - default: - return fmt.Errorf("Unrecognized prototype template type '%s'", templateType) - } - - if exists, err := afero.Exists(m.appFS, componentPath); exists { - return fmt.Errorf("Component with name '%s' already exists", name) - } else if err != nil { - return fmt.Errorf("Could not check whether component '%s' exists:\n\n%v", name, err) - } - - log.Infof("Writing component at '%s/%s'", componentsDir, name) - err := afero.WriteFile(m.appFS, componentPath, []byte(text), defaultFilePermissions) - if err != nil { - return err - } - - log.Debugf("Writing component parameters at '%s/%s", componentsDir, name) - return m.writeComponentParams(name, params) -} - func (m *manager) LibPaths(envName string) (libPath, vendorPath, envLibPath, envComponentPath, envParamsPath AbsPath) { envPath := appendToAbsPath(m.environmentsPath, envName) return m.libPath, m.vendorPath, appendToAbsPath(envPath, metadataDirName), diff --git a/metadata/manager_test.go b/metadata/manager_test.go index 6ea8b7b24b90c29f7cedda9874c8b0c5289f4354..72cb32d2f43aac834fd0e2b6e04f40bb817bba0d 100644 --- a/metadata/manager_test.go +++ b/metadata/manager_test.go @@ -19,7 +19,6 @@ import ( "os" "os/user" "path" - "sort" "testing" "github.com/spf13/afero" @@ -207,60 +206,6 @@ func TestFindSuccess(t *testing.T) { findSuccess(t, appPath, appFile) } -func TestComponentPaths(t *testing.T) { - spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS) - if err != nil { - t.Fatalf("Failed to parse cluster spec: %v", err) - } - - appPath := AbsPath("/componentPaths") - reg := newMockRegistryManager("incubator") - m, err := initManager("componentPaths", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS) - if err != nil { - t.Fatalf("Failed to init cluster spec: %v", err) - } - - // Create empty app file. - components := appendToAbsPath(appPath, componentsDir) - appFile1 := appendToAbsPath(components, "component1.jsonnet") - 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) - } - f1.Close() - - // Create empty file in a nested directory. - appSubdir := appendToAbsPath(components, "appSubdir") - 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") - 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) - } - f2.Close() - - // Create a directory that won't be listed in the call to `ComponentPaths`. - unlistedDir := string(appendToAbsPath(components, "doNotListMe")) - err = testFS.MkdirAll(unlistedDir, os.ModePerm) - if err != nil { - t.Fatalf("Failed to create directory '%s'\n%v", unlistedDir, err) - } - - paths, err := m.ComponentPaths() - if err != nil { - t.Fatalf("Failed to find component paths: %v", err) - } - - 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) - } -} - func TestLibPaths(t *testing.T) { appName := "test-lib-paths" expectedVendorPath := path.Join(appName, vendorDir) 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) + } +}