From 6aa18fd947d90a328c75f8e577d06740b03d54f5 Mon Sep 17 00:00:00 2001
From: bryanl <bryanliles@gmail.com>
Date: Wed, 21 Mar 2018 21:12:58 -0700
Subject: [PATCH] Allow `ks` command to work anywhere in app

Allow ks command to work in any subdirectory of a ksonnet app. If
`app.yaml` is not found in the current directory, look for it in parent
directories.

Signed-off-by: bryanl <bryanliles@gmail.com>
---
 cmd/root.go              | 16 ++++-------
 metadata/app/app.go      | 34 ++++++++++++++++++++++-
 metadata/app/app_test.go | 60 ++++++++++++++++++++++++++++++++++++++++
 metadata/app/schema.go   | 13 +++++++--
 4 files changed, 109 insertions(+), 14 deletions(-)
 create mode 100644 metadata/app/app_test.go

diff --git a/cmd/root.go b/cmd/root.go
index 3401f641..eb508cd3 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -116,18 +116,14 @@ application configuration to remote clusters.
 			return err
 		}
 
-		appConfig := filepath.Join(wd, "app.yaml")
-		exists, err := afero.Exists(appFs, appConfig)
-		if err != nil {
-			return err
+		var isInit bool
+		if len(args) == 2 && args[0] == "init" {
+			isInit = true
 		}
 
-		if exists {
-			log.Debugf("loading configuration from %s", appConfig)
-			ka, err = app.Load(appFs, wd)
-			if err != nil {
-				return err
-			}
+		ka, err = app.Load(appFs, wd)
+		if err != nil && isInit {
+			return err
 		}
 
 		return nil
diff --git a/metadata/app/app.go b/metadata/app/app.go
index 663742fc..775714e8 100644
--- a/metadata/app/app.go
+++ b/metadata/app/app.go
@@ -129,7 +129,12 @@ func (ba *baseApp) EnvironmentParams(envName string) (string, error) {
 }
 
 // Load loads the application configuration.
-func Load(fs afero.Fs, appRoot string) (App, error) {
+func Load(fs afero.Fs, cwd string) (App, error) {
+	appRoot, err := findRoot(fs, cwd)
+	if err != nil {
+		return nil, err
+	}
+
 	spec, err := Read(fs, appRoot)
 	if err != nil {
 		return nil, err
@@ -261,3 +266,30 @@ func cleanEnv(fs afero.Fs, root string) error {
 
 	return nil
 }
+
+func findRoot(fs afero.Fs, cwd string) (string, error) {
+	prev := cwd
+
+	for {
+		path := filepath.Join(cwd, appYamlName)
+		exists, err := afero.Exists(fs, path)
+		if err != nil {
+			return "", err
+		}
+
+		if exists {
+			return cwd, nil
+		}
+
+		cwd, err = filepath.Abs(filepath.Join(cwd, ".."))
+		if err != nil {
+			return "", err
+		}
+
+		if cwd == prev {
+			return "", errors.Errorf("unable to find ksonnet project")
+		}
+
+		prev = cwd
+	}
+}
diff --git a/metadata/app/app_test.go b/metadata/app/app_test.go
new file mode 100644
index 00000000..d89799f2
--- /dev/null
+++ b/metadata/app/app_test.go
@@ -0,0 +1,60 @@
+package app
+
+import (
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_findRoot(t *testing.T) {
+	fs := afero.NewMemMapFs()
+	stageFile(t, fs, "app010_app.yaml", "/app/app.yaml")
+
+	dirs := []string{
+		"/app/components",
+		"/invalid",
+	}
+
+	for _, dir := range dirs {
+		err := fs.MkdirAll(dir, DefaultFilePermissions)
+		require.NoError(t, err)
+	}
+
+	cases := []struct {
+		name     string
+		expected string
+		isErr    bool
+	}{
+		{
+			name:     "/app",
+			expected: "/app",
+		},
+		{
+			name:     "/app/components",
+			expected: "/app",
+		},
+		{
+			name:  "/invalid",
+			isErr: true,
+		},
+		{
+			name:  "/missing",
+			isErr: true,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			root, err := findRoot(fs, tc.name)
+			if tc.isErr {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, root)
+		})
+	}
+
+}
diff --git a/metadata/app/schema.go b/metadata/app/schema.go
index 12c4a36e..00405f79 100644
--- a/metadata/app/schema.go
+++ b/metadata/app/schema.go
@@ -22,6 +22,7 @@ import (
 	"github.com/blang/semver"
 	"github.com/ghodss/yaml"
 	"github.com/pkg/errors"
+	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
 )
 
@@ -66,9 +67,13 @@ type Spec struct {
 	License      string           `json:"license,omitempty"`
 }
 
-// Read will return the specification for a ksonnet application.
-func Read(fs afero.Fs, appRoot string) (*Spec, error) {
-	bytes, err := afero.ReadFile(fs, specPath(appRoot))
+// Read will return the specification for a ksonnet application. It will navigate up directories
+// to search for `app.yaml` and return error if it hits the root directory.
+func Read(fs afero.Fs, root string) (*Spec, error) {
+	config := filepath.Join(root, appYamlName)
+	log.Debugf("loading configuration from %s", config)
+
+	bytes, err := afero.ReadFile(fs, config)
 	if err != nil {
 		return nil, err
 	}
@@ -97,6 +102,8 @@ func Read(fs afero.Fs, appRoot string) (*Spec, error) {
 	return schema, nil
 }
 
+
+
 // Write writes the provided spec to file system.
 func Write(fs afero.Fs, appRoot string, spec *Spec) error {
 	data, err := spec.Marshal()
-- 
GitLab