From 6295812aae73aaac5d5d21fd21c7fba8fff53509 Mon Sep 17 00:00:00 2001
From: bryanl <bryanliles@gmail.com>
Date: Tue, 13 Feb 2018 16:29:53 -0500
Subject: [PATCH] Introduce component namespaces

re: #292
Signed-off-by: bryanl <bryanliles@gmail.com>
---
 Makefile                    |   5 +-
 cmd/prototype.go            |   6 +-
 cmd/root.go                 |  41 ++--
 component/component.go      | 353 ++++++++++++++++++++++++++++++
 component/component_test.go | 426 ++++++++++++++++++++++++++++++++++++
 component/create.go         | 208 ++++++++++++++++++
 component/create_test.go    | 151 +++++++++++++
 metadata/component.go       | 141 ++++--------
 metadata/component_test.go  |  44 +---
 metadata/environment.go     |   9 +-
 metadata/interface.go       |   2 +-
 metadata/manager.go         |   3 +-
 pkg/kubecfg/param.go        |   9 +-
 template/expander.go        |   6 +-
 14 files changed, 1247 insertions(+), 157 deletions(-)
 create mode 100644 component/component.go
 create mode 100644 component/component_test.go
 create mode 100644 component/create.go
 create mode 100644 component/create_test.go

diff --git a/Makefile b/Makefile
index 06e6e3ec..a9e3d03a 100644
--- a/Makefile
+++ b/Makefile
@@ -30,8 +30,7 @@ GUESTBOOK_FILE = examples/guestbook.jsonnet
 DOC_GEN_FILE = ./docs/generate/update-generated-docs.sh
 DOC_TEST_FILE = ./docs/generate/verify-generated-docs.sh
 JSONNET_FILES = $(KCFG_TEST_FILE) $(GUESTBOOK_FILE)
-# TODO: Simplify this once ./... ignores ./vendor
-GO_PACKAGES = ./cmd/... ./utils/... ./pkg/... ./metadata/... ./prototype/...
+GO_PACKAGES = ./...
 
 # Default cluster from this config is used for integration tests
 KUBECONFIG = $(HOME)/.kube/config
@@ -60,7 +59,7 @@ integrationtest: ks
 	$(GINKGO) -tags 'integration' integration -- -fixtures $(INTEGRATION_TEST_FIXTURES) -kubeconfig $(KUBECONFIG) -ksonnet-bin $(abspath $<)
 
 vet:
-	$(GO) vet $(GO_FLAGS) $(GO_PACKAGES)
+	$(GO) vet $(GO_PACKAGES)
 
 fmt:
 	$(GOFMT) -s -w $(shell $(GO) list -f '{{.Dir}}' $(GO_PACKAGES))
diff --git a/cmd/prototype.go b/cmd/prototype.go
index 821c5892..771a0a2a 100644
--- a/cmd/prototype.go
+++ b/cmd/prototype.go
@@ -20,6 +20,8 @@ import (
 	"os"
 	"strings"
 
+	"github.com/ksonnet/ksonnet/component"
+
 	"github.com/spf13/pflag"
 
 	"github.com/ksonnet/ksonnet/metadata"
@@ -417,7 +419,9 @@ var prototypeUseCmd = &cobra.Command{
 			return err
 		}
 
-		text, err := expandPrototype(proto, templateType, params, componentName)
+		_, prototypeName := component.ExtractNamespacedComponent(appFs, cwd, componentName)
+
+		text, err := expandPrototype(proto, templateType, params, prototypeName)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/root.go b/cmd/root.go
index 9dd9f722..4311834c 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -38,6 +38,7 @@ import (
 	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/tools/clientcmd"
 
+	"github.com/ksonnet/ksonnet/component"
 	"github.com/ksonnet/ksonnet/metadata"
 	str "github.com/ksonnet/ksonnet/strings"
 	"github.com/ksonnet/ksonnet/template"
@@ -433,40 +434,54 @@ func (te *cmdObjExpander) Expand() ([]*unstructured.Unstructured, error) {
 	}
 
 	_, vendorPath := manager.LibPaths()
-	libPath, mainPath, paramsPath, err := manager.EnvPaths(te.config.env)
+	libPath, mainPath, _, err := manager.EnvPaths(te.config.env)
 	if err != nil {
 		return nil, err
 	}
 
 	expander.FlagJpath = append([]string{string(vendorPath), string(libPath)}, expander.FlagJpath...)
 
-	componentPaths, err := manager.ComponentPaths()
+	namespacedComponentPaths, err := component.MakePathsByNamespace(te.config.fs, manager, te.config.cwd, te.config.env)
 	if err != nil {
 		return nil, errors.Wrap(err, "component paths")
 	}
 
-	baseObj, err := constructBaseObj(componentPaths, te.config.components)
-	if err != nil {
-		return nil, errors.Wrap(err, "construct base object")
-	}
-
 	//
 	// Set up ExtCodes to resolve runtime variables such as the environment namespace.
 	//
 
-	params := importParams(string(paramsPath))
 	envSpec, err := importEnv(manager, te.config.env)
 	if err != nil {
 		return nil, err
 	}
 
-	expander.ExtCodes = append([]string{baseObj, params, envSpec}, expander.ExtCodes...)
+	baseCodes := expander.ExtCodes
 
-	//
-	// Expand the ksonnet app as rendered for environment `env`.
-	//
+	slUnstructured := make([]*unstructured.Unstructured, 0)
+	for ns, componentPaths := range namespacedComponentPaths {
+
+		paramsPath := ns.ParamsPath()
+		params := importParams(string(paramsPath))
+
+		baseObj, err := constructBaseObj(componentPaths, te.config.components)
+		if err != nil {
+			return nil, errors.Wrap(err, "construct base object")
+		}
+
+		//
+		// Expand the ksonnet app as rendered for environment `env`.
+		//
+		expander.ExtCodes = append([]string{baseObj, params, envSpec}, baseCodes...)
+		u, err := expander.Expand([]string{string(mainPath)})
+		if err != nil {
+			return nil, errors.Wrapf(err, "generate objects for namespace %s", ns.Path)
+		}
+
+		slUnstructured = append(slUnstructured, u...)
+	}
+
+	return slUnstructured, nil
 
-	return expander.Expand([]string{string(mainPath)})
 }
 
 // constructBaseObj constructs the base Jsonnet object that represents k-v
diff --git a/component/component.go b/component/component.go
new file mode 100644
index 00000000..4c3f6e5e
--- /dev/null
+++ b/component/component.go
@@ -0,0 +1,353 @@
+// 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 component
+
+import (
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/pkg/errors"
+	"github.com/spf13/afero"
+)
+
+const (
+	// componentsDir is the name of the directory which houses components.
+	componentsRoot = "components"
+	// paramsFile is the params file for a component namespace.
+	paramsFile = "params.libsonnet"
+)
+
+// Path returns returns the file system path for a component.
+func Path(fs afero.Fs, root, name string) (string, error) {
+	ns, localName := ExtractNamespacedComponent(fs, root, name)
+
+	fis, err := afero.ReadDir(fs, ns.Dir())
+	if err != nil {
+		return "", err
+	}
+
+	var fileName string
+	files := make(map[string]bool)
+
+	for _, fi := range fis {
+		if fi.IsDir() {
+			continue
+		}
+
+		base := strings.TrimSuffix(fi.Name(), filepath.Ext(fi.Name()))
+		if _, ok := files[base]; ok {
+			return "", errors.Errorf("Found multiple component files with component name %q", name)
+		}
+		files[base] = true
+
+		if base == localName {
+			fileName = fi.Name()
+		}
+	}
+
+	if fileName == "" {
+		return "", errors.Errorf("No component name %q found", name)
+	}
+
+	return filepath.Join(ns.Dir(), fileName), nil
+}
+
+// Namespace is a component namespace.
+type Namespace struct {
+	// Path is the path of the component namespace.
+	Path string
+
+	root string
+	fs   afero.Fs
+}
+
+// ExtractNamespacedComponent extracts a namespace and a component from a path.
+func ExtractNamespacedComponent(fs afero.Fs, root, path string) (Namespace, string) {
+	path, component := filepath.Split(path)
+	path = strings.TrimSuffix(path, "/")
+	ns := Namespace{Path: path, root: root, fs: fs}
+	return ns, component
+}
+
+// ParamsPath generates the path to params.libsonnet for a namespace.
+func (n *Namespace) ParamsPath() string {
+	return filepath.Join(n.Dir(), paramsFile)
+}
+
+// ComponentPaths are the absolute paths to all the components in a namespace.
+func (n *Namespace) ComponentPaths() ([]string, error) {
+	dir := n.Dir()
+	fis, err := afero.ReadDir(n.fs, dir)
+	if err != nil {
+		return nil, errors.Wrap(err, "read component dir")
+	}
+
+	var paths []string
+	for _, fi := range fis {
+		if fi.IsDir() {
+			continue
+		}
+
+		if strings.HasSuffix(fi.Name(), ".jsonnet") {
+			paths = append(paths, filepath.Join(dir, fi.Name()))
+		}
+	}
+
+	sort.Strings(paths)
+
+	return paths, nil
+}
+
+// Components returns the components in a namespace.
+func (n *Namespace) Components() ([]string, error) {
+	paths, err := n.ComponentPaths()
+	if err != nil {
+		return nil, err
+	}
+
+	dir := filepath.Join(n.root, componentsRoot) + "/"
+
+	var names []string
+	for _, path := range paths {
+		name := strings.TrimPrefix(path, dir)
+		name = strings.TrimSuffix(name, filepath.Ext(name))
+		names = append(names, name)
+	}
+
+	return names, nil
+}
+
+// Dir is the absolute directory for a namespace.
+func (n *Namespace) Dir() string {
+	path := []string{n.root, componentsRoot}
+	if n.Path != "" {
+		path = append(path, strings.Split(n.Path, "/")...)
+	}
+
+	return filepath.Join(path...)
+}
+
+// Namespaces returns all component namespaces
+func Namespaces(fs afero.Fs, root string) ([]Namespace, error) {
+	componentRoot := filepath.Join(root, componentsRoot)
+
+	var namespaces []Namespace
+
+	err := afero.Walk(fs, componentRoot, func(path string, fi os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if fi.IsDir() {
+			ok, err := isComponentDir(fs, path)
+			if err != nil {
+				return err
+			}
+
+			if ok {
+				nsPath := strings.TrimPrefix(path, componentRoot)
+				nsPath = strings.TrimPrefix(nsPath, "/")
+				ns := Namespace{Path: nsPath, fs: fs, root: root}
+				namespaces = append(namespaces, ns)
+			}
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		return nil, errors.Wrap(err, "walk component path")
+	}
+
+	sort.Slice(namespaces, func(i, j int) bool {
+		return namespaces[i].Path < namespaces[j].Path
+	})
+
+	return namespaces, nil
+}
+
+func isComponentDir(fs afero.Fs, path string) (bool, error) {
+	files, err := afero.ReadDir(fs, path)
+	if err != nil {
+		return false, errors.Wrapf(err, "read files in %s", path)
+	}
+
+	for _, file := range files {
+		if file.Name() == paramsFile {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
+
+// AppSpecer is implemented by any value that has a AppSpec method. The AppSpec method is
+// used to retrieve a ksonnet AppSpec.
+type AppSpecer interface {
+	AppSpec() (*app.Spec, error)
+}
+
+// MakePathsByNamespace creates a map of component paths categorized by namespace.
+func MakePathsByNamespace(fs afero.Fs, appSpecer AppSpecer, root, env string) (map[Namespace][]string, error) {
+	paths, err := MakePaths(fs, appSpecer, root, env)
+	if err != nil {
+		return nil, err
+	}
+
+	m := make(map[Namespace][]string)
+
+	for i := range paths {
+		prefix := root + "/components/"
+		if strings.HasSuffix(root, "/") {
+			prefix = root + "components/"
+		}
+		path := strings.TrimPrefix(paths[i], prefix)
+		ns, _ := ExtractNamespacedComponent(fs, root, path)
+		if _, ok := m[ns]; !ok {
+			m[ns] = make([]string, 0)
+		}
+
+		m[ns] = append(m[ns], paths[i])
+	}
+
+	return m, nil
+}
+
+// MakePaths creates a slice of component paths
+func MakePaths(fs afero.Fs, appSpecer AppSpecer, root, env string) ([]string, error) {
+	cpl, err := newComponentPathLocator(fs, appSpecer, env)
+	if err != nil {
+		return nil, errors.Wrap(err, "create component path locator")
+	}
+
+	return cpl.Locate(root)
+}
+
+type componentPathLocator struct {
+	fs      afero.Fs
+	envSpec *app.EnvironmentSpec
+}
+
+func newComponentPathLocator(fs afero.Fs, appSpecer AppSpecer, env string) (*componentPathLocator, error) {
+	if appSpecer == nil {
+		return nil, errors.New("appSpecer is nil")
+	}
+
+	if fs == nil {
+		return nil, errors.New("fs is nil")
+	}
+
+	appSpec, err := appSpecer.AppSpec()
+	if err != nil {
+		return nil, errors.Wrap(err, "lookup application spec")
+	}
+
+	envSpec, ok := appSpec.GetEnvironmentSpec(env)
+	if !ok {
+		return nil, errors.Errorf("can't find %s environment", env)
+	}
+
+	return &componentPathLocator{
+		fs:      fs,
+		envSpec: envSpec,
+	}, nil
+}
+
+func (cpl *componentPathLocator) Locate(root string) ([]string, error) {
+	if len(cpl.envSpec.Targets) == 0 {
+		return cpl.defaultPaths(root)
+	}
+
+	var paths []string
+
+	for _, target := range cpl.envSpec.Targets {
+		childPaths, err := cpl.expandPath(root, target)
+		if err != nil {
+			return nil, errors.Wrapf(err, "unable to expand %s", target)
+		}
+		paths = append(paths, childPaths...)
+	}
+
+	sort.Strings(paths)
+
+	return paths, nil
+}
+
+// expandPath take a root and a target and returns all the jsonnet components in descendant paths.
+func (cpl *componentPathLocator) expandPath(root, target string) ([]string, error) {
+	path := filepath.Join(root, componentsRoot, target)
+	fi, err := cpl.fs.Stat(path)
+	if err != nil {
+		return nil, err
+	}
+
+	var paths []string
+
+	walkFn := func(path string, fi os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if !fi.IsDir() && isComponent(path) {
+			paths = append(paths, path)
+		}
+
+		return nil
+	}
+
+	if fi.IsDir() {
+		rootPath := filepath.Join(root, componentsRoot, fi.Name())
+		if err := afero.Walk(cpl.fs, rootPath, walkFn); err != nil {
+			return nil, errors.Wrapf(err, "search for components in %s", fi.Name())
+		}
+	} else if isComponent(fi.Name()) {
+		paths = append(paths, path)
+	}
+
+	return paths, nil
+}
+
+func (cpl *componentPathLocator) defaultPaths(root string) ([]string, error) {
+	var paths []string
+
+	walkFn := func(path string, fi os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if !fi.IsDir() && isComponent(path) {
+			paths = append(paths, path)
+		}
+
+		return nil
+	}
+
+	componentRoot := filepath.Join(root, componentsRoot)
+
+	if err := afero.Walk(cpl.fs, componentRoot, walkFn); err != nil {
+		return nil, errors.Wrap(err, "search for components")
+	}
+
+	return paths, nil
+}
+
+// isComponent reports if a file is a component. Components have a `jsonnet` extension.
+func isComponent(path string) bool {
+	return filepath.Ext(path) == ".jsonnet"
+}
diff --git a/component/component_test.go b/component/component_test.go
new file mode 100644
index 00000000..30ee8fde
--- /dev/null
+++ b/component/component_test.go
@@ -0,0 +1,426 @@
+// 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 component
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+)
+
+var (
+	existingPaths = []string{
+		"/app.yaml",
+		"/components/a.jsonnet",
+		"/components/b.jsonnet",
+		"/components/other",
+		"/components/params.libsonnet",
+		"/components/nested/a.jsonnet",
+		"/components/nested/params.libsonnet",
+		"/components/nested/very/deeply/c.jsonnet",
+		"/components/nested/very/deeply/params.libsonnet",
+		"/components/shallow/c.jsonnet",
+		"/components/shallow/params.libsonnet",
+	}
+
+	invalidPaths = []string{
+		"/app.yaml",
+		"/components/a.jsonnet",
+		"/components/a.txt",
+	}
+)
+
+type stubAppSpecer struct {
+	appSpec *app.Spec
+	err     error
+}
+
+var _ AppSpecer = (*stubAppSpecer)(nil)
+
+func newStubAppSpecer(appSpec *app.Spec) *stubAppSpecer {
+	return &stubAppSpecer{appSpec: appSpec}
+}
+
+func (s *stubAppSpecer) AppSpec() (*app.Spec, error) {
+	return s.appSpec, s.err
+}
+
+func makePaths(t *testing.T, fs afero.Fs, paths []string) {
+	for _, path := range paths {
+		dir := filepath.Dir(path)
+		err := fs.MkdirAll(dir, 0755)
+		require.NoError(t, err)
+
+		_, err = fs.Create(path)
+		require.NoError(t, err)
+	}
+}
+
+func TestPath(t *testing.T) {
+
+	cases := []struct {
+		name     string
+		paths    []string
+		in       string
+		expected string
+		isErr    bool
+	}{
+		{
+			name:     "in root namespace",
+			paths:    existingPaths,
+			in:       "a",
+			expected: "/components/a.jsonnet",
+		},
+		{
+			name:     "in nested namespace",
+			paths:    existingPaths,
+			in:       "nested/a",
+			expected: "/components/nested/a.jsonnet",
+		},
+		{
+			name:  "not found",
+			paths: existingPaths,
+			in:    "z",
+			isErr: true,
+		},
+		{
+			name:  "invalid path",
+			paths: invalidPaths,
+			in:    "a",
+			isErr: true,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			fs := afero.NewMemMapFs()
+			makePaths(t, fs, tc.paths)
+
+			path, err := Path(fs, "/", tc.in)
+			if tc.isErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+
+				assert.Equal(t, tc.expected, path)
+			}
+		})
+	}
+}
+
+func TestExtractNamedspacedComponent(t *testing.T) {
+	cases := []struct {
+		name      string
+		path      string
+		nsPath    string
+		component string
+	}{
+		{
+			name:      "component in root namespace",
+			path:      "my-deployment",
+			nsPath:    "",
+			component: "my-deployment",
+		},
+		{
+			name:      "component in root namespace",
+			path:      "nested/my-deployment",
+			nsPath:    "nested",
+			component: "my-deployment",
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			fs := afero.NewMemMapFs()
+			root := "/"
+			ns, component := ExtractNamespacedComponent(fs, root, tc.path)
+			assert.Equal(t, tc.nsPath, ns.Path)
+			assert.Equal(t, component, component)
+		})
+	}
+}
+
+func TestNamespace_ParamsPath(t *testing.T) {
+	cases := []struct {
+		name     string
+		nsName   string
+		expected string
+	}{
+		{
+			name:     "root namespace",
+			expected: "/components/params.libsonnet",
+		},
+		{
+			name:     "nested namespace",
+			nsName:   "nested",
+			expected: "/components/nested/params.libsonnet",
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			ns := Namespace{Path: tc.nsName, root: "/"}
+			assert.Equal(t, tc.expected, ns.ParamsPath())
+		})
+	}
+}
+
+func TestNamespace_ComponentPaths(t *testing.T) {
+	cases := []struct {
+		name     string
+		nsPath   string
+		expected []string
+	}{
+		{
+			name: "root namespace",
+			expected: []string{
+				"/components/a.jsonnet",
+				"/components/b.jsonnet",
+			},
+		},
+		{
+			name:   "nested namespace",
+			nsPath: "nested",
+			expected: []string{
+				"/components/nested/a.jsonnet",
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			fs := afero.NewMemMapFs()
+			makePaths(t, fs, existingPaths)
+
+			ns := Namespace{Path: tc.nsPath, fs: fs, root: "/"}
+
+			paths, err := ns.ComponentPaths()
+			require.NoError(t, err)
+
+			require.Equal(t, tc.expected, paths)
+		})
+	}
+}
+
+func TestNamespace_Components(t *testing.T) {
+	cases := []struct {
+		name     string
+		nsPath   string
+		expected []string
+	}{
+		{
+			name: "root namespace",
+			expected: []string{
+				"a",
+				"b",
+			},
+		},
+		{
+			name:   "nested namespace",
+			nsPath: "nested",
+			expected: []string{
+				"nested/a",
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			fs := afero.NewMemMapFs()
+			makePaths(t, fs, existingPaths)
+
+			ns := Namespace{Path: tc.nsPath, fs: fs, root: "/"}
+
+			paths, err := ns.Components()
+			require.NoError(t, err)
+
+			require.Equal(t, tc.expected, paths)
+		})
+	}
+}
+
+func TestNamespaces(t *testing.T) {
+	fs := afero.NewMemMapFs()
+
+	makePaths(t, fs, existingPaths)
+
+	namespaces, err := Namespaces(fs, "/")
+	require.NoError(t, err)
+
+	expected := []Namespace{
+		{Path: "", fs: fs, root: "/"},
+		{Path: "nested", fs: fs, root: "/"},
+		{Path: "nested/very/deeply", fs: fs, root: "/"},
+		{Path: "shallow", fs: fs, root: "/"},
+	}
+
+	assert.Equal(t, expected, namespaces)
+}
+
+func TestMakePathsByNameSpace(t *testing.T) {
+	fs := afero.NewMemMapFs()
+	makePaths(t, fs, existingPaths)
+
+	cases := []struct {
+		name     string
+		targets  []string
+		expected map[Namespace][]string
+		isErr    bool
+	}{
+		{
+			name: "no target paths",
+			expected: map[Namespace][]string{
+				Namespace{fs: fs, root: "/"}: []string{
+					"/components/a.jsonnet",
+					"/components/b.jsonnet",
+				},
+				Namespace{fs: fs, root: "/", Path: "nested"}: []string{
+					"/components/nested/a.jsonnet",
+				},
+				Namespace{fs: fs, root: "/", Path: "nested/very/deeply"}: []string{
+					"/components/nested/very/deeply/c.jsonnet",
+				},
+				Namespace{fs: fs, root: "/", Path: "shallow"}: []string{
+					"/components/shallow/c.jsonnet",
+				},
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			envSpec := &app.EnvironmentSpec{
+				Targets: tc.targets,
+			}
+
+			appSpec := &app.Spec{
+				Environments: app.EnvironmentSpecs{"default": envSpec},
+			}
+			appSpecer := newStubAppSpecer(appSpec)
+
+			root := "/"
+			env := "default"
+
+			paths, err := MakePathsByNamespace(fs, appSpecer, root, env)
+			if tc.isErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				assert.Equal(t, tc.expected, paths)
+			}
+
+		})
+	}
+}
+
+func TestMakePaths(t *testing.T) {
+
+	cases := []struct {
+		name     string
+		targets  []string
+		expected []string
+		isErr    bool
+	}{
+		{
+			name: "no target paths",
+			expected: []string{
+				"/components/a.jsonnet",
+				"/components/b.jsonnet",
+				"/components/nested/a.jsonnet",
+				"/components/nested/very/deeply/c.jsonnet",
+				"/components/shallow/c.jsonnet",
+			},
+		},
+		{
+			name: "jsonnet target path file",
+			targets: []string{
+				"a.jsonnet",
+			},
+			expected: []string{"/components/a.jsonnet"},
+		},
+		{
+			name: "jsonnet target path dir",
+			targets: []string{
+				"nested",
+			},
+			expected: []string{
+				"/components/nested/a.jsonnet",
+				"/components/nested/very/deeply/c.jsonnet",
+			},
+		},
+		{
+			name: "jsonnet target path dir and files",
+			targets: []string{
+				"shallow/c.jsonnet",
+				"nested",
+			},
+			expected: []string{
+				"/components/nested/a.jsonnet",
+				"/components/nested/very/deeply/c.jsonnet",
+				"/components/shallow/c.jsonnet",
+			},
+		},
+		{
+			name:    "target points to missing path",
+			targets: []string{"missing"},
+			isErr:   true,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			fs := afero.NewMemMapFs()
+			makePaths(t, fs, existingPaths)
+
+			envSpec := &app.EnvironmentSpec{
+				Targets: tc.targets,
+			}
+
+			appSpec := &app.Spec{
+				Environments: app.EnvironmentSpecs{"default": envSpec},
+			}
+			appSpecer := newStubAppSpecer(appSpec)
+
+			root := "/"
+			env := "default"
+
+			paths, err := MakePaths(fs, appSpecer, root, env)
+			if tc.isErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				assert.Equal(t, tc.expected, paths)
+			}
+		})
+	}
+}
+
+func TestMakePaths_invalid_appSpecer(t *testing.T) {
+	fs := afero.NewMemMapFs()
+	_, err := MakePaths(fs, nil, "/", "default")
+	require.Error(t, err)
+}
+
+func TestMakePaths_invalid_fs(t *testing.T) {
+	appSpecer := newStubAppSpecer(nil)
+	_, err := MakePaths(nil, appSpecer, "/", "default")
+	require.Error(t, err)
+}
diff --git a/component/create.go b/component/create.go
new file mode 100644
index 00000000..ea875980
--- /dev/null
+++ b/component/create.go
@@ -0,0 +1,208 @@
+// 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 component
+
+import (
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	param "github.com/ksonnet/ksonnet/metadata/params"
+	"github.com/ksonnet/ksonnet/prototype"
+	"github.com/pkg/errors"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+)
+
+var (
+	// defaultFolderPermissions are the default permissions for a folder.
+	defaultFolderPermissions = os.FileMode(0755)
+	// defaultFilePermissions are the default permission for a file.
+	defaultFilePermissions = os.FileMode(0644)
+)
+
+// Create creates a component.
+func Create(fs afero.Fs, root, name, text string, params param.Params, templateType prototype.TemplateType) (string, error) {
+	cc, err := newComponentCreator(fs, root)
+	if err != nil {
+		return "", errors.Wrap(err, "initialize component creator")
+	}
+
+	return cc.Create(name, text, params, templateType)
+}
+
+type componentCreator struct {
+	fs   afero.Fs
+	root string
+}
+
+func newComponentCreator(fs afero.Fs, root string) (*componentCreator, error) {
+	if fs == nil {
+		return nil, errors.New("fs is nil")
+	}
+
+	if root == "" {
+		return nil, errors.New("invalid ksonnet root")
+	}
+
+	return &componentCreator{fs: fs, root: root}, nil
+}
+
+func (cc *componentCreator) Create(name, text string, params param.Params, templateType prototype.TemplateType) (string, error) {
+	if !isValidName(name) {
+		return "", errors.Errorf("Component name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
+	}
+
+	nsName, componentName := namespaceComponent(name)
+
+	componentDir, componentPath, err := cc.location(nsName, componentName, templateType)
+	if err != nil {
+		return "", errors.Wrap(err, "generate component location")
+	}
+
+	paramsPath := filepath.Join(componentDir, "params.libsonnet")
+
+	exists, err := afero.Exists(cc.fs, componentDir)
+	if err != nil {
+		return "", errors.Wrapf(err, "check if %s exists", componentDir)
+	}
+
+	if !exists {
+		if err = cc.createNamespace(componentDir, paramsPath); err != nil {
+			return "", err
+		}
+	}
+
+	exists, err = afero.Exists(cc.fs, componentPath)
+	if err != nil {
+		return "", errors.Wrapf(err, "check if %s exists", componentPath)
+	}
+
+	if exists {
+		return "", errors.Errorf("component with name '%s' already exists", name)
+	}
+
+	log.Infof("Writing component at '%s'", componentPath)
+	if err := afero.WriteFile(cc.fs, componentPath, []byte(text), defaultFilePermissions); err != nil {
+		return "", errors.Wrapf(err, "write component %s")
+	}
+
+	log.Debugf("Writing component parameters at '%s/%s", componentsRoot, name)
+
+	if err := cc.writeParams(componentName, paramsPath, params); err != nil {
+		return "", errors.Wrapf(err, "write parameters")
+	}
+
+	return componentPath, nil
+}
+
+// location returns the dir and full path for the component.
+func (cc *componentCreator) location(nsName, name string, templateType prototype.TemplateType) (string, string, error) {
+	componentDir := filepath.Join(cc.root, componentsRoot, nsName)
+	componentPath := filepath.Join(componentDir, name)
+	switch templateType {
+	case prototype.YAML:
+		componentPath = componentPath + ".yaml"
+	case prototype.JSON:
+		componentPath = componentPath + ".json"
+	case prototype.Jsonnet:
+		componentPath = componentPath + ".jsonnet"
+	default:
+		return "", "", errors.Errorf("Unrecognized prototype template type '%s'", templateType)
+	}
+
+	return componentDir, componentPath, nil
+}
+
+func (cc *componentCreator) createNamespace(componentDir, paramsPath string) error {
+	if err := cc.fs.MkdirAll(componentDir, defaultFolderPermissions); err != nil {
+		return errors.Wrapf(err, "create component dir %s", componentDir)
+	}
+
+	if err := afero.WriteFile(cc.fs, paramsPath, GenParamsContent(), defaultFilePermissions); err != nil {
+		return errors.Wrap(err, "create component params")
+	}
+
+	return nil
+}
+
+func (cc *componentCreator) writeParams(name, paramsPath string, params param.Params) error {
+	text, err := afero.ReadFile(cc.fs, paramsPath)
+	if err != nil {
+		return err
+	}
+
+	appended, err := param.AppendComponent(name, string(text), params)
+	if err != nil {
+		return err
+	}
+
+	return afero.WriteFile(cc.fs, paramsPath, []byte(appended), defaultFilePermissions)
+}
+
+// isValidName returns true if a name (e.g., for an environment) is valid.
+// A component is valid if it does not contain punctuation, whitespace, leading or
+// trailing slashes.
+func isValidName(name string) bool {
+	// No unicode whitespace is allowed. `Fields` doesn't handle trailing or
+	// leading whitespace.
+	fields := strings.Fields(name)
+	if len(fields) > 1 || len(strings.TrimSpace(name)) != len(name) {
+		return false
+	}
+
+	hasPunctuation := regexp.MustCompile(`[\\,;':!()?"{}\[\]*&%@$]+`).MatchString
+	hasTrailingSlashes := regexp.MustCompile(`/+$`).MatchString
+	hasLeadingSlashes := regexp.MustCompile(`^/+`).MatchString
+	return len(name) != 0 && !hasPunctuation(name) && !hasTrailingSlashes(name) && !hasLeadingSlashes(name)
+}
+
+func namespaceComponent(name string) (string, string) {
+	parts := strings.Split(name, "/")
+
+	if len(parts) == 1 {
+		return "", parts[0]
+	}
+
+	var nsName []string
+	var componentName string
+	for i := range parts {
+		if i == len(parts)-1 {
+			componentName = parts[i]
+			break
+		}
+
+		nsName = append(nsName, parts[i])
+	}
+
+	return strings.Join(nsName, "/"), componentName
+}
+
+// GenParamsContent is the default content for params.libsonnet.
+func GenParamsContent() []byte {
+	return []byte(`{
+  global: {
+    // User-defined global parameters; accessible to all component and environments, Ex:
+    // replicas: 4,
+  },
+  components: {
+    // Component-level parameters, defined initially from 'ks prototype use ...'
+    // Each object below should correspond to a component in the components/ directory
+  },
+}
+`)
+}
diff --git a/component/create_test.go b/component/create_test.go
new file mode 100644
index 00000000..f5119a0b
--- /dev/null
+++ b/component/create_test.go
@@ -0,0 +1,151 @@
+// 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 component
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/ksonnet/ksonnet/prototype"
+
+	"github.com/spf13/afero"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_Create(t *testing.T) {
+	cases := []struct {
+		name          string
+		isErr         bool
+		params        map[string]string
+		templateType  prototype.TemplateType
+		componentDir  string
+		ns            string
+		componentName string
+	}{
+		{
+			name: "jsonnet component",
+			params: map[string]string{
+				"name": "name",
+			},
+			templateType:  prototype.Jsonnet,
+			componentDir:  "/components",
+			componentName: "component",
+		},
+		{
+			name: "yaml component",
+			params: map[string]string{
+				"name": "name",
+			},
+			templateType:  prototype.YAML,
+			componentDir:  "/components",
+			componentName: "component",
+		},
+		{
+			name: "json component",
+			params: map[string]string{
+				"name": "name",
+			},
+			templateType:  prototype.JSON,
+			componentDir:  "/components",
+			componentName: "component",
+		},
+		{
+			name: "invalid component",
+			params: map[string]string{
+				"name": "name",
+			},
+			templateType: prototype.TemplateType("unknown"),
+			isErr:        true,
+		},
+		{
+			name:          "nested/component",
+			params:        map[string]string{"name": "name"},
+			templateType:  prototype.Jsonnet,
+			componentName: "nested/component",
+			componentDir:  "/components/nested",
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			fs := afero.NewMemMapFs()
+			root := "/"
+
+			name := filepath.Join(tc.ns, tc.componentName)
+
+			path, err := Create(fs, root, name, "content", tc.params, tc.templateType)
+			if tc.isErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+
+				checkPath(t, fs, path)
+
+				paramsPath := filepath.Join(tc.componentDir, tc.ns, "params.libsonnet")
+				checkPath(t, fs, paramsPath)
+
+				assertComponentExt(t, path, tc.templateType)
+			}
+		})
+	}
+}
+
+func assertComponentExt(t *testing.T, filename string, templateType prototype.TemplateType) {
+	ext := filepath.Ext(filename)
+
+	var got prototype.TemplateType
+	switch ext {
+	case ".yaml":
+		got = prototype.YAML
+	case ".json":
+		got = prototype.JSON
+	case ".jsonnet":
+		got = prototype.Jsonnet
+	default:
+		t.Errorf("unknown component extension: %s", ext)
+	}
+
+	assert.Equal(t, templateType, got)
+}
+
+func checkPath(t *testing.T, fs afero.Fs, path string) {
+	exists, err := afero.Exists(fs, path)
+	require.NoError(t, err)
+	require.True(t, exists, "expected %s to exist", path)
+}
+
+func Test_isValidName(t *testing.T) {
+	cases := []struct {
+		name    string
+		isValid bool
+	}{
+		{
+			name:    "component",
+			isValid: true,
+		},
+		{
+			name: "with spaces",
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			require.Equal(t, tc.isValid, isValidName(tc.name))
+		})
+	}
+}
diff --git a/metadata/component.go b/metadata/component.go
index b5ff387b..7cf8eaca 100644
--- a/metadata/component.go
+++ b/metadata/component.go
@@ -16,14 +16,15 @@
 package metadata
 
 import (
-	"fmt"
 	"os"
 	"path"
-	"strings"
+	"path/filepath"
 
+	"github.com/ksonnet/ksonnet/component"
 	param "github.com/ksonnet/ksonnet/metadata/params"
 	"github.com/ksonnet/ksonnet/prototype"
 	str "github.com/ksonnet/ksonnet/strings"
+	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
 )
@@ -49,64 +50,47 @@ func (m *manager) ComponentPaths() ([]string, error) {
 }
 
 func (m *manager) GetAllComponents() ([]string, error) {
-	componentPaths, err := m.ComponentPaths()
+	namespaces, err := component.Namespaces(m.appFS, m.rootPath)
 	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)
+	for _, ns := range namespaces {
+
+		comps, err := ns.Components()
+		if err != nil {
+			return nil, err
+		}
+
+		components = append(components, comps...)
 	}
 
 	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 := str.AppendToPath(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)
+	_, err := component.Create(m.appFS, m.rootPath, name, text, params, templateType)
 	if err != nil {
-		return err
+		return errors.Wrap(err, "create component")
 	}
 
-	log.Debugf("Writing component parameters at '%s/%s", componentsDir, name)
-	return m.writeComponentParams(name, params)
+	return nil
 }
 
 // DeleteComponent removes the component file and all references.
 // Write operations will happen at the end to minimalize failures that leave
 // the directory structure in a half-finished state.
 func (m *manager) DeleteComponent(name string) error {
-	componentPath, err := m.findComponentPath(name)
+	componentPath, err := component.Path(m.appFS, m.rootPath, name)
 	if err != nil {
 		return err
 	}
 
+	ns, _ := component.ExtractNamespacedComponent(m.appFS, m.rootPath, name)
+
 	// Build the new component/params.libsonnet file.
-	componentParamsFile, err := afero.ReadFile(m.appFS, m.componentParamsPath)
+	componentParamsFile, err := afero.ReadFile(m.appFS, ns.ParamsPath())
 	if err != nil {
 		return err
 	}
@@ -142,7 +126,7 @@ func (m *manager) DeleteComponent(name string) error {
 
 	// Remove the references in component/params.libsonnet.
 	log.Debugf("... deleting references in %s", m.componentParamsPath)
-	err = afero.WriteFile(m.appFS, m.componentParamsPath, []byte(componentJsonnet), defaultFilePermissions)
+	err = afero.WriteFile(m.appFS, ns.ParamsPath(), []byte(componentJsonnet), defaultFilePermissions)
 	if err != nil {
 		return err
 	}
@@ -168,11 +152,12 @@ func (m *manager) DeleteComponent(name string) error {
 	// TODO: Remove,
 	// references in main.jsonnet.
 	// component references in other component files (feature does not yet exist).
-	log.Infof("Succesfully deleted component '%s'", name)
+	log.Infof("Successfully deleted component '%s'", name)
 	return nil
 }
 
 func (m *manager) GetComponentParams(component string) (param.Params, error) {
+	log.Infof("get component params for %s", component)
 	text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
 	if err != nil {
 		return nil, err
@@ -181,81 +166,51 @@ func (m *manager) GetComponentParams(component string) (param.Params, error) {
 	return param.GetComponentParams(component, string(text))
 }
 
-func (m *manager) GetAllComponentParams() (map[string]param.Params, error) {
-	text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
+func (m *manager) GetAllComponentParams(root string) (map[string]param.Params, error) {
+	namespaces, err := component.Namespaces(m.appFS, root)
 	if err != nil {
-		return nil, err
+		return nil, errors.Wrap(err, "find component namespaces")
 	}
 
-	return param.GetAllComponentParams(string(text))
-}
+	out := make(map[string]param.Params)
 
-func (m *manager) SetComponentParams(component string, params param.Params) error {
-	text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
-	if err != nil {
-		return err
-	}
+	for _, ns := range namespaces {
+		paramsPath := filepath.Join(root, "components", ns.Path, "params.libsonnet")
 
-	jsonnet, err := param.SetComponentParams(component, string(text), params)
-	if err != nil {
-		return err
-	}
-
-	return afero.WriteFile(m.appFS, m.componentParamsPath, []byte(jsonnet), defaultFilePermissions)
-}
-
-func (m *manager) findComponentPath(name string) (string, error) {
-	componentPaths, err := m.ComponentPaths()
-	if err != nil {
-		log.Debugf("Failed to retrieve component paths")
-		return "", err
-	}
+		text, err := afero.ReadFile(m.appFS, paramsPath)
+		if err != nil {
+			return nil, err
+		}
 
-	var componentPath string
-	for _, p := range componentPaths {
-		fileName := path.Base(p)
-		component := strings.TrimSuffix(fileName, path.Ext(fileName))
+		params, err := param.GetAllComponentParams(string(text))
+		if err != nil {
+			return nil, errors.Wrapf(err, "get all component params for %s", ns.Path)
+		}
 
-		if component == name {
-			// need to make sure we don't have multiple files with the same component name
-			if componentPath != "" {
-				return "", fmt.Errorf("Found multiple component files with component name '%s'", name)
+		for k, v := range params {
+			if ns.Path != "" {
+				k = ns.Path + "/" + k
 			}
-			componentPath = p
+			out[k] = v
 		}
 	}
 
-	if componentPath == "" {
-		return "", fmt.Errorf("No component with name '%s' found", name)
-	}
-
-	return componentPath, nil
+	return out, nil
 }
 
-func (m *manager) writeComponentParams(componentName string, params param.Params) error {
-	text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
+func (m *manager) SetComponentParams(path string, params param.Params) error {
+	ns, componentName := component.ExtractNamespacedComponent(m.appFS, m.rootPath, path)
+	paramsPath := ns.ParamsPath()
+
+	text, err := afero.ReadFile(m.appFS, paramsPath)
 	if err != nil {
 		return err
 	}
 
-	appended, err := param.AppendComponent(componentName, string(text), params)
+	jsonnet, err := param.SetComponentParams(componentName, string(text), params)
 	if err != nil {
 		return err
 	}
 
-	return afero.WriteFile(m.appFS, m.componentParamsPath, []byte(appended), defaultFilePermissions)
-}
-
-func genComponentParamsContent() []byte {
-	return []byte(`{
-  global: {
-    // User-defined global parameters; accessible to all component and environments, Ex:
-    // replicas: 4,
-  },
-  components: {
-    // Component-level parameters, defined initially from 'ks prototype use ...'
-    // Each object below should correspond to a component in the components/ directory
-  },
-}
-`)
+	return afero.WriteFile(m.appFS, paramsPath, []byte(jsonnet), defaultFilePermissions)
 }
diff --git a/metadata/component_test.go b/metadata/component_test.go
index 4027284e..5693508d 100644
--- a/metadata/component_test.go
+++ b/metadata/component_test.go
@@ -17,7 +17,6 @@ package metadata
 import (
 	"fmt"
 	"os"
-	"path"
 	"sort"
 	"strings"
 	"testing"
@@ -97,6 +96,7 @@ func TestComponentPaths(t *testing.T) {
 	}
 }
 
+// TODO: this logic and tests should be moved to the components namespace.
 func TestGetAllComponents(t *testing.T) {
 	m := populateComponentPaths(t)
 	defer cleanComponentPaths(t)
@@ -107,51 +107,13 @@ func TestGetAllComponents(t *testing.T) {
 	}
 
 	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 len(components) != 1 {
+		t.Fatalf("Expected exactly 1 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)
-	}
-}
-
-func TestFindComponentPath(t *testing.T) {
-	m := populateComponentPaths(t)
-	defer cleanComponentPaths(t)
-
-	component := strings.TrimSuffix(componentFile1, path.Ext(componentFile1))
-	expected := fmt.Sprintf("%s/components/%s", componentsPath, componentFile1)
-	path, err := m.findComponentPath(component)
-	if err != nil {
-		t.Fatalf("Failed to find component path, %v", err)
-	}
-
-	if path != expected {
-		t.Fatalf("m.findComponentPath failed; expected '%s', got '%s'", expected, path)
-	}
-}
-
-func TestGenComponentParamsContent(t *testing.T) {
-	expected := `{
-  global: {
-    // User-defined global parameters; accessible to all component and environments, Ex:
-    // replicas: 4,
-  },
-  components: {
-    // Component-level parameters, defined initially from 'ks prototype use ...'
-    // Each object below should correspond to a component in the components/ directory
-  },
-}
-`
-	content := string(genComponentParamsContent())
-	if content != expected {
-		t.Fatalf("Expected to generate:\n%s\n, got:\n%s", expected, content)
-	}
 }
diff --git a/metadata/environment.go b/metadata/environment.go
index 73900d80..06aea484 100644
--- a/metadata/environment.go
+++ b/metadata/environment.go
@@ -18,12 +18,14 @@ package metadata
 import (
 	"bytes"
 	"fmt"
+	"os"
 	"path"
 	"path/filepath"
 
 	"github.com/ksonnet/ksonnet/metadata/app"
 	"github.com/ksonnet/ksonnet/metadata/lib"
 	str "github.com/ksonnet/ksonnet/strings"
+	"github.com/pkg/errors"
 
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
@@ -313,8 +315,13 @@ func (m *manager) GetEnvironmentParams(name string) (map[string]param.Params, er
 		return nil, err
 	}
 
+	cwd, err := os.Getwd()
+	if err != nil {
+		return nil, errors.Wrap(err, "get working directory")
+	}
+
 	// Get all component params
-	componentParams, err := m.GetAllComponentParams()
+	componentParams, err := m.GetAllComponentParams(cwd)
 	if err != nil {
 		return nil, err
 	}
diff --git a/metadata/interface.go b/metadata/interface.go
index 9fe0996a..88407d69 100644
--- a/metadata/interface.go
+++ b/metadata/interface.go
@@ -49,7 +49,7 @@ type Manager interface {
 	// Params API.
 	SetComponentParams(component string, params param.Params) error
 	GetComponentParams(name string) (param.Params, error)
-	GetAllComponentParams() (map[string]param.Params, error)
+	GetAllComponentParams(cwd string) (map[string]param.Params, error)
 	// GetEnvironmentParams will take the name of an environment and return a
 	// mapping of parameters of the form:
 	// componentName => {param key => param val}
diff --git a/metadata/manager.go b/metadata/manager.go
index 6c751d45..a6c07d02 100644
--- a/metadata/manager.go
+++ b/metadata/manager.go
@@ -21,6 +21,7 @@ import (
 	"path"
 	"path/filepath"
 
+	"github.com/ksonnet/ksonnet/component"
 	"github.com/ksonnet/ksonnet/metadata/app"
 	"github.com/ksonnet/ksonnet/metadata/registry"
 	str "github.com/ksonnet/ksonnet/strings"
@@ -231,7 +232,7 @@ func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte,
 	}{
 		{
 			m.componentParamsPath,
-			genComponentParamsContent(),
+			component.GenParamsContent(),
 		},
 		{
 			m.baseLibsonnetPath,
diff --git a/pkg/kubecfg/param.go b/pkg/kubecfg/param.go
index bcebf135..dba979f3 100644
--- a/pkg/kubecfg/param.go
+++ b/pkg/kubecfg/param.go
@@ -18,6 +18,7 @@ package kubecfg
 import (
 	"fmt"
 	"io"
+	"os"
 	"reflect"
 	"sort"
 	"strconv"
@@ -25,6 +26,7 @@ import (
 
 	param "github.com/ksonnet/ksonnet/metadata/params"
 	str "github.com/ksonnet/ksonnet/strings"
+	"github.com/pkg/errors"
 
 	"github.com/fatih/color"
 	log "github.com/sirupsen/logrus"
@@ -126,6 +128,11 @@ func NewParamListCmd(component, env string) *ParamListCmd {
 
 // Run executes the displaying of params.
 func (c *ParamListCmd) Run(out io.Writer) error {
+	cwd, err := os.Getwd()
+	if err != nil {
+		return errors.Wrap(err, "get current working directory")
+	}
+
 	manager, err := manager()
 	if err != nil {
 		return err
@@ -138,7 +145,7 @@ func (c *ParamListCmd) Run(out io.Writer) error {
 			return err
 		}
 	} else {
-		params, err = manager.GetAllComponentParams()
+		params, err = manager.GetAllComponentParams(cwd)
 		if err != nil {
 			return err
 		}
diff --git a/template/expander.go b/template/expander.go
index 1efe6d93..6ec4e7a6 100644
--- a/template/expander.go
+++ b/template/expander.go
@@ -9,6 +9,7 @@ import (
 
 	jsonnet "github.com/google/go-jsonnet"
 	"github.com/ksonnet/ksonnet/utils"
+	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
 )
@@ -42,17 +43,18 @@ func NewExpander(fs afero.Fs) Expander {
 func (spec *Expander) Expand(paths []string) ([]*unstructured.Unstructured, error) {
 	vm, err := spec.jsonnetVM()
 	if err != nil {
-		return nil, err
+		return nil, errors.Wrap(err, "initialize jsonnet VM")
 	}
 
 	res := []*unstructured.Unstructured{}
 	for _, path := range paths {
 		objs, err := utils.Read(spec.fs, vm, path)
 		if err != nil {
-			return nil, fmt.Errorf("Error reading %s: %v", path, err)
+			return nil, errors.Wrapf(err, "unable to read %s", path)
 		}
 		res = append(res, utils.FlattenToV1(objs)...)
 	}
+
 	return res, nil
 }
 
-- 
GitLab