Skip to content
Snippets Groups Projects
Commit 6295812a authored by bryanl's avatar bryanl
Browse files

Introduce component namespaces


re: #292
Signed-off-by: default avatarbryanl <bryanliles@gmail.com>
parent 9bb797ae
No related branches found
No related tags found
No related merge requests found
...@@ -30,8 +30,7 @@ GUESTBOOK_FILE = examples/guestbook.jsonnet ...@@ -30,8 +30,7 @@ GUESTBOOK_FILE = examples/guestbook.jsonnet
DOC_GEN_FILE = ./docs/generate/update-generated-docs.sh DOC_GEN_FILE = ./docs/generate/update-generated-docs.sh
DOC_TEST_FILE = ./docs/generate/verify-generated-docs.sh DOC_TEST_FILE = ./docs/generate/verify-generated-docs.sh
JSONNET_FILES = $(KCFG_TEST_FILE) $(GUESTBOOK_FILE) JSONNET_FILES = $(KCFG_TEST_FILE) $(GUESTBOOK_FILE)
# TODO: Simplify this once ./... ignores ./vendor GO_PACKAGES = ./...
GO_PACKAGES = ./cmd/... ./utils/... ./pkg/... ./metadata/... ./prototype/...
# Default cluster from this config is used for integration tests # Default cluster from this config is used for integration tests
KUBECONFIG = $(HOME)/.kube/config KUBECONFIG = $(HOME)/.kube/config
...@@ -60,7 +59,7 @@ integrationtest: ks ...@@ -60,7 +59,7 @@ integrationtest: ks
$(GINKGO) -tags 'integration' integration -- -fixtures $(INTEGRATION_TEST_FIXTURES) -kubeconfig $(KUBECONFIG) -ksonnet-bin $(abspath $<) $(GINKGO) -tags 'integration' integration -- -fixtures $(INTEGRATION_TEST_FIXTURES) -kubeconfig $(KUBECONFIG) -ksonnet-bin $(abspath $<)
vet: vet:
$(GO) vet $(GO_FLAGS) $(GO_PACKAGES) $(GO) vet $(GO_PACKAGES)
fmt: fmt:
$(GOFMT) -s -w $(shell $(GO) list -f '{{.Dir}}' $(GO_PACKAGES)) $(GOFMT) -s -w $(shell $(GO) list -f '{{.Dir}}' $(GO_PACKAGES))
......
...@@ -20,6 +20,8 @@ import ( ...@@ -20,6 +20,8 @@ import (
"os" "os"
"strings" "strings"
"github.com/ksonnet/ksonnet/component"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/ksonnet/ksonnet/metadata" "github.com/ksonnet/ksonnet/metadata"
...@@ -417,7 +419,9 @@ var prototypeUseCmd = &cobra.Command{ ...@@ -417,7 +419,9 @@ var prototypeUseCmd = &cobra.Command{
return err 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 { if err != nil {
return err return err
} }
......
...@@ -38,6 +38,7 @@ import ( ...@@ -38,6 +38,7 @@ import (
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"github.com/ksonnet/ksonnet/component"
"github.com/ksonnet/ksonnet/metadata" "github.com/ksonnet/ksonnet/metadata"
str "github.com/ksonnet/ksonnet/strings" str "github.com/ksonnet/ksonnet/strings"
"github.com/ksonnet/ksonnet/template" "github.com/ksonnet/ksonnet/template"
...@@ -433,40 +434,54 @@ func (te *cmdObjExpander) Expand() ([]*unstructured.Unstructured, error) { ...@@ -433,40 +434,54 @@ func (te *cmdObjExpander) Expand() ([]*unstructured.Unstructured, error) {
} }
_, vendorPath := manager.LibPaths() _, vendorPath := manager.LibPaths()
libPath, mainPath, paramsPath, err := manager.EnvPaths(te.config.env) libPath, mainPath, _, err := manager.EnvPaths(te.config.env)
if err != nil { if err != nil {
return nil, err return nil, err
} }
expander.FlagJpath = append([]string{string(vendorPath), string(libPath)}, expander.FlagJpath...) 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 { if err != nil {
return nil, errors.Wrap(err, "component paths") 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. // Set up ExtCodes to resolve runtime variables such as the environment namespace.
// //
params := importParams(string(paramsPath))
envSpec, err := importEnv(manager, te.config.env) envSpec, err := importEnv(manager, te.config.env)
if err != nil { if err != nil {
return nil, err return nil, err
} }
expander.ExtCodes = append([]string{baseObj, params, envSpec}, expander.ExtCodes...) baseCodes := expander.ExtCodes
// slUnstructured := make([]*unstructured.Unstructured, 0)
// Expand the ksonnet app as rendered for environment `env`. 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 // constructBaseObj constructs the base Jsonnet object that represents k-v
......
// 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"
}
// 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)
}
// 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
},
}
`)
}
// 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))
})
}
}
...@@ -16,14 +16,15 @@ ...@@ -16,14 +16,15 @@
package metadata package metadata
import ( import (
"fmt"
"os" "os"
"path" "path"
"strings" "path/filepath"
"github.com/ksonnet/ksonnet/component"
param "github.com/ksonnet/ksonnet/metadata/params" param "github.com/ksonnet/ksonnet/metadata/params"
"github.com/ksonnet/ksonnet/prototype" "github.com/ksonnet/ksonnet/prototype"
str "github.com/ksonnet/ksonnet/strings" str "github.com/ksonnet/ksonnet/strings"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
...@@ -49,64 +50,47 @@ func (m *manager) ComponentPaths() ([]string, error) { ...@@ -49,64 +50,47 @@ func (m *manager) ComponentPaths() ([]string, error) {
} }
func (m *manager) GetAllComponents() ([]string, error) { func (m *manager) GetAllComponents() ([]string, error) {
componentPaths, err := m.ComponentPaths() namespaces, err := component.Namespaces(m.appFS, m.rootPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var components []string var components []string
for _, p := range componentPaths { for _, ns := range namespaces {
component := strings.TrimSuffix(path.Base(p), path.Ext(p))
components = append(components, component) comps, err := ns.Components()
if err != nil {
return nil, err
}
components = append(components, comps...)
} }
return components, nil return components, nil
} }
func (m *manager) CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error { func (m *manager) CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error {
if !isValidName(name) || strings.Contains(name, "/") { _, err := component.Create(m.appFS, m.rootPath, name, text, params, templateType)
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)
if err != nil { if err != nil {
return err return errors.Wrap(err, "create component")
} }
log.Debugf("Writing component parameters at '%s/%s", componentsDir, name) return nil
return m.writeComponentParams(name, params)
} }
// DeleteComponent removes the component file and all references. // DeleteComponent removes the component file and all references.
// Write operations will happen at the end to minimalize failures that leave // Write operations will happen at the end to minimalize failures that leave
// the directory structure in a half-finished state. // the directory structure in a half-finished state.
func (m *manager) DeleteComponent(name string) error { func (m *manager) DeleteComponent(name string) error {
componentPath, err := m.findComponentPath(name) componentPath, err := component.Path(m.appFS, m.rootPath, name)
if err != nil { if err != nil {
return err return err
} }
ns, _ := component.ExtractNamespacedComponent(m.appFS, m.rootPath, name)
// Build the new component/params.libsonnet file. // 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 { if err != nil {
return err return err
} }
...@@ -142,7 +126,7 @@ func (m *manager) DeleteComponent(name string) error { ...@@ -142,7 +126,7 @@ func (m *manager) DeleteComponent(name string) error {
// Remove the references in component/params.libsonnet. // Remove the references in component/params.libsonnet.
log.Debugf("... deleting references in %s", m.componentParamsPath) 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 { if err != nil {
return err return err
} }
...@@ -168,11 +152,12 @@ func (m *manager) DeleteComponent(name string) error { ...@@ -168,11 +152,12 @@ func (m *manager) DeleteComponent(name string) error {
// TODO: Remove, // TODO: Remove,
// references in main.jsonnet. // references in main.jsonnet.
// component references in other component files (feature does not yet exist). // 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 return nil
} }
func (m *manager) GetComponentParams(component string) (param.Params, error) { 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) text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -181,81 +166,51 @@ func (m *manager) GetComponentParams(component string) (param.Params, error) { ...@@ -181,81 +166,51 @@ func (m *manager) GetComponentParams(component string) (param.Params, error) {
return param.GetComponentParams(component, string(text)) return param.GetComponentParams(component, string(text))
} }
func (m *manager) GetAllComponentParams() (map[string]param.Params, error) { func (m *manager) GetAllComponentParams(root string) (map[string]param.Params, error) {
text, err := afero.ReadFile(m.appFS, m.componentParamsPath) namespaces, err := component.Namespaces(m.appFS, root)
if err != nil { 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 { for _, ns := range namespaces {
text, err := afero.ReadFile(m.appFS, m.componentParamsPath) paramsPath := filepath.Join(root, "components", ns.Path, "params.libsonnet")
if err != nil {
return err
}
jsonnet, err := param.SetComponentParams(component, string(text), params) text, err := afero.ReadFile(m.appFS, paramsPath)
if err != nil { if err != nil {
return err return nil, 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
}
var componentPath string params, err := param.GetAllComponentParams(string(text))
for _, p := range componentPaths { if err != nil {
fileName := path.Base(p) return nil, errors.Wrapf(err, "get all component params for %s", ns.Path)
component := strings.TrimSuffix(fileName, path.Ext(fileName)) }
if component == name { for k, v := range params {
// need to make sure we don't have multiple files with the same component name if ns.Path != "" {
if componentPath != "" { k = ns.Path + "/" + k
return "", fmt.Errorf("Found multiple component files with component name '%s'", name)
} }
componentPath = p out[k] = v
} }
} }
if componentPath == "" { return out, nil
return "", fmt.Errorf("No component with name '%s' found", name)
}
return componentPath, nil
} }
func (m *manager) writeComponentParams(componentName string, params param.Params) error { func (m *manager) SetComponentParams(path string, params param.Params) error {
text, err := afero.ReadFile(m.appFS, m.componentParamsPath) ns, componentName := component.ExtractNamespacedComponent(m.appFS, m.rootPath, path)
paramsPath := ns.ParamsPath()
text, err := afero.ReadFile(m.appFS, paramsPath)
if err != nil { if err != nil {
return err return err
} }
appended, err := param.AppendComponent(componentName, string(text), params) jsonnet, err := param.SetComponentParams(componentName, string(text), params)
if err != nil { if err != nil {
return err return err
} }
return afero.WriteFile(m.appFS, m.componentParamsPath, []byte(appended), defaultFilePermissions) return afero.WriteFile(m.appFS, paramsPath, []byte(jsonnet), 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
},
}
`)
} }
...@@ -17,7 +17,6 @@ package metadata ...@@ -17,7 +17,6 @@ package metadata
import ( import (
"fmt" "fmt"
"os" "os"
"path"
"sort" "sort"
"strings" "strings"
"testing" "testing"
...@@ -97,6 +96,7 @@ func TestComponentPaths(t *testing.T) { ...@@ -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) { func TestGetAllComponents(t *testing.T) {
m := populateComponentPaths(t) m := populateComponentPaths(t)
defer cleanComponentPaths(t) defer cleanComponentPaths(t)
...@@ -107,51 +107,13 @@ func TestGetAllComponents(t *testing.T) { ...@@ -107,51 +107,13 @@ func TestGetAllComponents(t *testing.T) {
} }
expected1 := strings.TrimSuffix(componentFile1, ".jsonnet") expected1 := strings.TrimSuffix(componentFile1, ".jsonnet")
expected2 := strings.TrimSuffix(componentFile2, ".jsonnet")
if len(components) != 2 { if len(components) != 1 {
t.Fatalf("Expected exactly 2 components, got %d", len(components)) t.Fatalf("Expected exactly 1 components, got %d", len(components))
} }
if components[0] != expected1 { if components[0] != expected1 {
t.Fatalf("Expected component %s, got %s", expected1, components) 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)
}
} }
...@@ -18,12 +18,14 @@ package metadata ...@@ -18,12 +18,14 @@ package metadata
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os"
"path" "path"
"path/filepath" "path/filepath"
"github.com/ksonnet/ksonnet/metadata/app" "github.com/ksonnet/ksonnet/metadata/app"
"github.com/ksonnet/ksonnet/metadata/lib" "github.com/ksonnet/ksonnet/metadata/lib"
str "github.com/ksonnet/ksonnet/strings" str "github.com/ksonnet/ksonnet/strings"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/afero" "github.com/spf13/afero"
...@@ -313,8 +315,13 @@ func (m *manager) GetEnvironmentParams(name string) (map[string]param.Params, er ...@@ -313,8 +315,13 @@ func (m *manager) GetEnvironmentParams(name string) (map[string]param.Params, er
return nil, err return nil, err
} }
cwd, err := os.Getwd()
if err != nil {
return nil, errors.Wrap(err, "get working directory")
}
// Get all component params // Get all component params
componentParams, err := m.GetAllComponentParams() componentParams, err := m.GetAllComponentParams(cwd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -49,7 +49,7 @@ type Manager interface { ...@@ -49,7 +49,7 @@ type Manager interface {
// Params API. // Params API.
SetComponentParams(component string, params param.Params) error SetComponentParams(component string, params param.Params) error
GetComponentParams(name string) (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 // GetEnvironmentParams will take the name of an environment and return a
// mapping of parameters of the form: // mapping of parameters of the form:
// componentName => {param key => param val} // componentName => {param key => param val}
......
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"github.com/ksonnet/ksonnet/component"
"github.com/ksonnet/ksonnet/metadata/app" "github.com/ksonnet/ksonnet/metadata/app"
"github.com/ksonnet/ksonnet/metadata/registry" "github.com/ksonnet/ksonnet/metadata/registry"
str "github.com/ksonnet/ksonnet/strings" str "github.com/ksonnet/ksonnet/strings"
...@@ -231,7 +232,7 @@ func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte, ...@@ -231,7 +232,7 @@ func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte,
}{ }{
{ {
m.componentParamsPath, m.componentParamsPath,
genComponentParamsContent(), component.GenParamsContent(),
}, },
{ {
m.baseLibsonnetPath, m.baseLibsonnetPath,
......
...@@ -18,6 +18,7 @@ package kubecfg ...@@ -18,6 +18,7 @@ package kubecfg
import ( import (
"fmt" "fmt"
"io" "io"
"os"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
...@@ -25,6 +26,7 @@ import ( ...@@ -25,6 +26,7 @@ import (
param "github.com/ksonnet/ksonnet/metadata/params" param "github.com/ksonnet/ksonnet/metadata/params"
str "github.com/ksonnet/ksonnet/strings" str "github.com/ksonnet/ksonnet/strings"
"github.com/pkg/errors"
"github.com/fatih/color" "github.com/fatih/color"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
...@@ -126,6 +128,11 @@ func NewParamListCmd(component, env string) *ParamListCmd { ...@@ -126,6 +128,11 @@ func NewParamListCmd(component, env string) *ParamListCmd {
// Run executes the displaying of params. // Run executes the displaying of params.
func (c *ParamListCmd) Run(out io.Writer) error { 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() manager, err := manager()
if err != nil { if err != nil {
return err return err
...@@ -138,7 +145,7 @@ func (c *ParamListCmd) Run(out io.Writer) error { ...@@ -138,7 +145,7 @@ func (c *ParamListCmd) Run(out io.Writer) error {
return err return err
} }
} else { } else {
params, err = manager.GetAllComponentParams() params, err = manager.GetAllComponentParams(cwd)
if err != nil { if err != nil {
return err return err
} }
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
jsonnet "github.com/google/go-jsonnet" jsonnet "github.com/google/go-jsonnet"
"github.com/ksonnet/ksonnet/utils" "github.com/ksonnet/ksonnet/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
...@@ -42,17 +43,18 @@ func NewExpander(fs afero.Fs) Expander { ...@@ -42,17 +43,18 @@ func NewExpander(fs afero.Fs) Expander {
func (spec *Expander) Expand(paths []string) ([]*unstructured.Unstructured, error) { func (spec *Expander) Expand(paths []string) ([]*unstructured.Unstructured, error) {
vm, err := spec.jsonnetVM() vm, err := spec.jsonnetVM()
if err != nil { if err != nil {
return nil, err return nil, errors.Wrap(err, "initialize jsonnet VM")
} }
res := []*unstructured.Unstructured{} res := []*unstructured.Unstructured{}
for _, path := range paths { for _, path := range paths {
objs, err := utils.Read(spec.fs, vm, path) objs, err := utils.Read(spec.fs, vm, path)
if err != nil { 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)...) res = append(res, utils.FlattenToV1(objs)...)
} }
return res, nil return res, nil
} }
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment