Repository: grafana/tanka
Pull request #182 (2020-01-23 08:31:10)

fix: Stable environment name
Cherry-picks commits cbc2c66d3c8ca276e112b74fc6414448d4207576 and 8012c657f8dd7c7768eeb431d1c75c3d57698ea3 from #131, because they are a needed enhancement but the other PR is not going to be merged soon


Patch:
diff --git a/cmd/tk/env.go b/cmd/tk/env.go
index fe8bf016..8887007b 100644
--- a/cmd/tk/env.go
+++ b/cmd/tk/env.go
@@ -124,6 +124,14 @@ func envAddCmd() *cobra.Command {
 	cfg := v1alpha1.New()
 	envSettingsFlags(cfg, cmd.Flags())
 	cmd.Run = func(cmd *cobra.Command, args []string) {
+		if cmd.Flags().Changed("server-from-context") {
+			server, err := client.IPFromContext(cfg.Spec.APIServer)
+			if err != nil {
+				log.Fatalf("Resolving IP from context: %s", err)
+			}
+			cfg.Spec.APIServer = server
+		}
+
 		if err := addEnv(args[0], cfg); err != nil {
 			log.Fatalln(err)
 		}
diff --git a/cmd/tk/main.go b/cmd/tk/main.go
index fa1a06dc..b78363b6 100644
--- a/cmd/tk/main.go
+++ b/cmd/tk/main.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"log"
 	"os"
+	"path/filepath"
 
 	"github.com/posener/complete"
 	"github.com/spf13/cobra"
@@ -12,6 +13,7 @@ import (
 	"golang.org/x/crypto/ssh/terminal"
 
 	"github.com/grafana/tanka/pkg/cli/cmp"
+	"github.com/grafana/tanka/pkg/jsonnet/jpath"
 	"github.com/grafana/tanka/pkg/spec"
 	"github.com/grafana/tanka/pkg/spec/v1alpha1"
 )
@@ -86,7 +88,15 @@ func main() {
 }
 
 func setupConfiguration(baseDir string) *v1alpha1.Config {
-	config, err := spec.ParseDir(baseDir)
+	_, baseDir, rootDir, err := jpath.Resolve(baseDir)
+	if err != nil {
+		log.Fatalln("Resolving jpath:", err)
+	}
+
+	// name of the environment: relative path from rootDir
+	name, _ := filepath.Rel(rootDir, baseDir)
+
+	config, err := spec.ParseDir(baseDir, name)
 	if err != nil {
 		switch err.(type) {
 		// just run fine without config. Provider features won't work (apply, show, diff)
diff --git a/cmd/tk/tool.go b/cmd/tk/tool.go
index c223d9d2..4042f572 100644
--- a/cmd/tk/tool.go
+++ b/cmd/tk/tool.go
@@ -51,9 +51,12 @@ func jpathCmd() *cobra.Command {
 
 func importsCmd() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "imports <file>",
-		Short: "list all transitive imports of a file",
+		Use:   "imports <directory>",
+		Short: "list all transitive imports of an environment",
 		Args:  cobra.ExactArgs(1),
+		Annotations: map[string]string{
+			"args": "baseDir",
+		},
 		Run: func(cmd *cobra.Command, args []string) {
 			var modFiles []string
 			if cmd.Flag("check").Changed {
@@ -64,22 +67,28 @@ func importsCmd() *cobra.Command {
 				}
 			}
 
-			f, err := filepath.Abs(args[0])
+			dir, err := filepath.Abs(args[0])
 			if err != nil {
-				log.Fatalln("Opening file:", err)
+				log.Fatalln("Loading environment:", err)
 			}
 
-			deps, err := jsonnet.TransitiveImports(f)
+			fi, err := os.Stat(dir)
 			if err != nil {
-				log.Fatalln("resolving imports:", err)
+				log.Fatalln("Loading environment:", err)
+			}
+
+			if !fi.IsDir() {
+				log.Fatalln("The argument must be an environment's directory, but this does not seem to be the case.")
 			}
 
-			// include main.jsonnet as well
-			deps = append(deps, f)
+			deps, err := jsonnet.TransitiveImports(dir)
+			if err != nil {
+				log.Fatalln("Resolving imports:", err)
+			}
 
 			root, err := gitRoot()
 			if err != nil {
-				log.Fatalln("invoking git:", err)
+				log.Fatalln("Invoking git:", err)
 			}
 			if modFiles != nil {
 				for _, m := range modFiles {
diff --git a/pkg/jsonnet/imports.go b/pkg/jsonnet/imports.go
index f447196a..7cdbbcea 100644
--- a/pkg/jsonnet/imports.go
+++ b/pkg/jsonnet/imports.go
@@ -3,6 +3,7 @@ package jsonnet
 import (
 	"io/ioutil"
 	"path/filepath"
+	"sort"
 
 	jsonnet "github.com/google/go-jsonnet"
 	"github.com/google/go-jsonnet/ast"
@@ -13,14 +14,21 @@ import (
 	"github.com/grafana/tanka/pkg/jsonnet/native"
 )
 
-// TransitiveImports returns all recursive imports of a file
-func TransitiveImports(filename string) ([]string, error) {
-	sonnet, err := ioutil.ReadFile(filename)
+// TransitiveImports returns all recursive imports of an environment
+func TransitiveImports(dir string) ([]string, error) {
+	dir, err := filepath.Abs(dir)
+	if err != nil {
+		return nil, err
+	}
+
+	mainFile := filepath.Join(dir, "main.jsonnet")
+
+	sonnet, err := ioutil.ReadFile(mainFile)
 	if err != nil {
 		return nil, errors.Wrap(err, "opening file")
 	}
 
-	jpath, _, _, err := jpath.Resolve(filepath.Dir(filename))
+	jpath, _, rootDir, err := jpath.Resolve(dir)
 	if err != nil {
 		return nil, errors.Wrap(err, "resolving JPATH")
 	}
@@ -37,9 +45,17 @@ func TransitiveImports(filename string) ([]string, error) {
 	}
 
 	imports := make([]string, 0)
-	err = importRecursive(&imports, vm, node, "main.jsonnet")
+	if err = importRecursive(&imports, vm, node, "main.jsonnet"); err != nil {
+		return nil, err
+	}
+
+	uniq := append(uniqueStringSlice(imports), mainFile)
+	for i := range uniq {
+		uniq[i], _ = filepath.Rel(rootDir, uniq[i])
+	}
+	sort.Strings(uniq)
 
-	return uniqueStringSlice(imports), err
+	return uniq, nil
 }
 
 // importRecursive takes a Jsonnet VM and recursively imports the AST. Every
@@ -56,7 +72,8 @@ func importRecursive(list *[]string, vm *jsonnet.VM, node ast.Node, currentPath
 			return errors.Wrap(err, "importing jsonnet")
 		}
 
-		*list = append(*list, foundAt)
+		abs, _ := filepath.Abs(foundAt)
+		*list = append(*list, abs)
 
 		if err := importRecursive(list, vm, contents, foundAt); err != nil {
 			return err
@@ -71,7 +88,8 @@ func importRecursive(list *[]string, vm *jsonnet.VM, node ast.Node, currentPath
 			return errors.Wrap(err, "importing string")
 		}
 
-		*list = append(*list, foundAt)
+		abs, _ := filepath.Abs(foundAt)
+		*list = append(*list, abs)
 
 	// neither `import` nor `importstr`, probably object or similar: try children
 	default:
diff --git a/pkg/jsonnet/jpath/jpath.go b/pkg/jsonnet/jpath/jpath.go
index 7356c0a9..3558c32a 100644
--- a/pkg/jsonnet/jpath/jpath.go
+++ b/pkg/jsonnet/jpath/jpath.go
@@ -32,6 +32,11 @@ func (e ErrorFileNotFound) Error() string {
 // This results in predictable imports, as it doesn't matter whether the user called
 // called the command further down tree or not. A little bit like git.
 func Resolve(workdir string) (path []string, base, root string, err error) {
+	workdir, err = filepath.Abs(workdir)
+	if err != nil {
+		return nil, "", "", err
+	}
+
 	root, err = FindParentFile("jsonnetfile.json", workdir, "/")
 	if err != nil {
 		if _, ok := err.(ErrorFileNotFound); ok {
diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go
index d87aa6a7..7efad898 100644
--- a/pkg/spec/spec.go
+++ b/pkg/spec/spec.go
@@ -3,7 +3,6 @@ package spec
 import (
 	"bytes"
 	"os"
-	"path/filepath"
 
 	"github.com/pkg/errors"
 	"github.com/spf13/viper"
@@ -34,7 +33,7 @@ func Parse(data []byte, name string) (*v1alpha1.Config, error) {
 
 // ParseDir parses the given environments `spec.json` into a `v1alpha1.Config`
 // object with the name set to the directories name
-func ParseDir(baseDir string) (*v1alpha1.Config, error) {
+func ParseDir(baseDir, name string) (*v1alpha1.Config, error) {
 	fi, err := os.Stat(baseDir)
 	if err != nil {
 		return nil, err
@@ -51,7 +50,7 @@ func ParseDir(baseDir string) (*v1alpha1.Config, error) {
 		return nil, err
 	}
 
-	return parse(v, filepath.Base(baseDir))
+	return parse(v, name)
 }
 
 // parse accepts a viper.Viper already loaded with the actual config and
@@ -62,11 +61,7 @@ func parse(v *viper.Viper, name string) (*v1alpha1.Config, error) {
 	// handle deprecated ksonnet spec
 	for _, d := range deprecated {
 		if v.IsSet(d.old) && !v.IsSet(d.new) {
-			if errDepr == nil {
-				errDepr = ErrDeprecated{d}
-			} else {
-				errDepr = append(errDepr, d)
-			}
+			errDepr = append(errDepr, d)
 			v.Set(d.new, v.Get(d.old))
 		}
 	}
@@ -79,6 +74,10 @@ func parse(v *viper.Viper, name string) (*v1alpha1.Config, error) {
 	// set the name field
 	config.Metadata.Name = name
 
+	if len(errDepr) == 0 {
+		return config, nil
+	}
+
 	// return depreciation notes in case any exist as well
 	return config, errDepr
 }
diff --git a/pkg/tanka/parse.go b/pkg/tanka/parse.go
index 24d16765..118b6e44 100644
--- a/pkg/tanka/parse.go
+++ b/pkg/tanka/parse.go
@@ -2,8 +2,7 @@ package tanka
 
 import (
 	"encoding/json"
-	"fmt"
-	"os"
+	"log"
 	"path/filepath"
 
 	"github.com/pkg/errors"
@@ -34,13 +33,14 @@ func (p *ParseResult) newKube() (*kubernetes.Kubernetes, error) {
 
 // parse loads the `spec.json`, evaluates the jsonnet and returns both, the
 // kubernetes object and the reconciled manifests
-func parse(baseDir string, opts *options) (*ParseResult, error) {
-	raw, env, err := eval(baseDir, opts)
+func parse(dir string, opts *options) (*ParseResult, error) {
+	raw, env, err := eval(dir, opts.extCode)
 	if err != nil {
 		return nil, err
 	}
 
 	rec, err := kubernetes.Reconcile(raw, env.Spec, opts.targets)
+
 	if err != nil {
 		return nil, errors.Wrap(err, "reconciling")
 	}
@@ -51,40 +51,41 @@ func parse(baseDir string, opts *options) (*ParseResult, error) {
 	}, nil
 }
 
-// Eval returns the raw evaluated Jsonnet output (without any transformations)
-func Eval(baseDir string, mods ...Modifier) (raw map[string]interface{}, err error) {
-	opts := parseModifiers(mods)
-
-	r, _, err := eval(baseDir, opts)
+// eval returns the raw evaluated Jsonnet and the parsed env used for evaluation
+func eval(dir string, extCode map[string]string) (raw map[string]interface{}, env *v1alpha1.Config, err error) {
+	baseDir, env, err := loadDir(dir)
 	if err != nil {
-		return nil, err
+		return nil, nil, errors.Wrap(err, "loading environment")
 	}
-	return r, nil
-}
 
-// eval returns the raw evaluated Jsonnet and the parsed env used for evaluation
-func eval(baseDir string, opts *options) (raw map[string]interface{}, env *v1alpha1.Config, err error) {
-	if opts == nil {
-		opts = &options{}
+	raw, err = evalJsonnet(baseDir, env, extCode)
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "evaluating jsonnet")
 	}
 
-	env, err = parseEnv(baseDir, opts)
+	return raw, env, nil
+}
+
+func loadDir(dir string) (baseDir string, env *v1alpha1.Config, err error) {
+	_, baseDir, rootDir, err := jpath.Resolve(dir)
 	if err != nil {
-		return nil, nil, err
+		return "", nil, errors.Wrap(err, "resolving jpath")
 	}
 
-	raw, err = evalJsonnet(baseDir, env, opts.extCode)
+	env, err = parseEnv(baseDir, rootDir)
 	if err != nil {
-		return nil, nil, errors.Wrap(err, "evaluating jsonnet")
+		return "", nil, err
 	}
-
-	return raw, env, nil
+	return baseDir, env, nil
 }
 
 // parseEnv parses the `spec.json` of the environment and returns a
 // *kubernetes.Kubernetes from it
-func parseEnv(baseDir string, opts *options) (*v1alpha1.Config, error) {
-	config, err := spec.ParseDir(baseDir)
+func parseEnv(baseDir, rootDir string) (*v1alpha1.Config, error) {
+	// name of the environment: relative path from rootDir
+	name, _ := filepath.Rel(rootDir, baseDir)
+
+	config, err := spec.ParseDir(baseDir, name)
 	if err != nil {
 		switch err.(type) {
 		// config is missing
@@ -92,10 +93,7 @@ func parseEnv(baseDir string, opts *options) (*v1alpha1.Config, error) {
 			return nil, kubernetes.ErrorMissingConfig
 		// the config includes deprecated fields
 		case spec.ErrDeprecated:
-			if opts.wWarn == nil {
-				opts.wWarn = os.Stderr
-			}
-			fmt.Fprint(opts.wWarn, err)
+			log.Println(err)
 		// some other error
 		default:
 			return nil, errors.Wrap(err, "reading spec.json")
@@ -107,16 +105,7 @@ func parseEnv(baseDir string, opts *options) (*v1alpha1.Config, error) {
 
 // evalJsonnet evaluates the jsonnet environment at the given directory starting with
 // `main.jsonnet`
-func evalJsonnet(path string, env *v1alpha1.Config, extCode map[string]string) (map[string]interface{}, error) {
-	workdir, err := filepath.Abs(path)
-	if err != nil {
-		return nil, err
-	}
-	_, baseDir, _, err := jpath.Resolve(workdir)
-	if err != nil {
-		return nil, errors.Wrap(err, "resolving jpath")
-	}
-
+func evalJsonnet(baseDir string, env *v1alpha1.Config, extCode map[string]string) (map[string]interface{}, error) {
 	jsonEnv, err := json.Marshal(env)
 	if err != nil {
 		return nil, errors.Wrap(err, "marshalling environment config")
@@ -133,9 +122,6 @@ func evalJsonnet(path string, env *v1alpha1.Config, extCode map[string]string) (
 		filepath.Join(baseDir, "main.jsonnet"),
 		ext...,
 	)
-	if err != nil {
-		return nil, err
-	}
 
 	var dict map[string]interface{}
 	if err := json.Unmarshal([]byte(raw), &dict); err != nil {
diff --git a/pkg/tanka/tanka.go b/pkg/tanka/tanka.go
index 0875b883..a80a261e 100644
--- a/pkg/tanka/tanka.go
+++ b/pkg/tanka/tanka.go
@@ -5,7 +5,6 @@
 package tanka
 
 import (
-	"io"
 	"regexp"
 
 	"github.com/grafana/tanka/pkg/kubernetes"
@@ -21,9 +20,6 @@ func parseModifiers(mods []Modifier) *options {
 }
 
 type options struct {
-	// io.Writer to write warnings and notices to
-	wWarn io.Writer
-
 	// `std.extVar`
 	extCode map[string]string
 
@@ -41,14 +37,6 @@ type options struct {
 // functions for available options.
 type Modifier func(*options)
 
-// WithWarnWriter allows to provide a custom io.Writer that all warnings are
-// written to
-func WithWarnWriter(w io.Writer) Modifier {
-	return func(opts *options) {
-		opts.wWarn = w
-	}
-}
-
 // WithExtCode allows to pass external variables (jsonnet code) to the VM
 func WithExtCode(code map[string]string) Modifier {
 	return func(opts *options) {
diff --git a/pkg/tanka/workflow.go b/pkg/tanka/workflow.go
index 6f541ac8..69795600 100644
--- a/pkg/tanka/workflow.go
+++ b/pkg/tanka/workflow.go
@@ -2,7 +2,6 @@ package tanka
 
 import (
 	"fmt"
-	"os"
 
 	"github.com/pkg/errors"
 
@@ -35,11 +34,7 @@ func Apply(baseDir string, mods ...Modifier) error {
 		tmp := "Warning: There are no differences. Your apply may not do anything at all."
 		diff = &tmp
 	}
-
-	if opts.wWarn == nil {
-		opts.wWarn = os.Stderr
-	}
-	fmt.Fprintln(opts.wWarn, *diff)
+	fmt.Println(diff)
 
 	return kube.Apply(p.Resources, opts.apply)
 }
@@ -77,3 +72,14 @@ func Show(baseDir string, mods ...Modifier) (string, error) {
 
 	return p.Resources.String(), nil
 }
+
+// Eval returns the raw evaluated Jsonnet output (without any transformations)
+func Eval(dir string, mods ...Modifier) (raw map[string]interface{}, err error) {
+	opts := parseModifiers(mods)
+
+	r, _, err := eval(dir, opts.extCode)
+	if err != nil {
+		return nil, err
+	}
+	return r, nil
+}


Test patch:
diff --git a/pkg/jsonnet/imports_test.go b/pkg/jsonnet/imports_test.go
index d5d2499b..33fa1202 100644
--- a/pkg/jsonnet/imports_test.go
+++ b/pkg/jsonnet/imports_test.go
@@ -1,6 +1,7 @@
 package jsonnet
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -10,15 +11,15 @@ import (
 // TestTransitiveImports checks that TransitiveImports is able to report all
 // recursive imports of a file
 func TestTransitiveImports(t *testing.T) {
-	imports, err := TransitiveImports("testdata/main.jsonnet")
+	imports, err := TransitiveImports("testdata")
+	fmt.Println(imports)
 	require.NoError(t, err)
-	assert.ElementsMatch(t, []string{
-		"testdata/trees.jsonnet",
-
-		"testdata/trees/apple.jsonnet",
-		"testdata/trees/cherry.jsonnet",
-		"testdata/trees/peach.jsonnet",
-
-		"testdata/trees/generic.libsonnet",
+	assert.Equal(t, []string{
+		"main.jsonnet",
+		"trees.jsonnet",
+		"trees/apple.jsonnet",
+		"trees/cherry.jsonnet",
+		"trees/generic.libsonnet",
+		"trees/peach.jsonnet",
 	}, imports)
 }
diff --git a/pkg/kubernetes/kubernetes_test.go b/pkg/kubernetes/kubernetes_test.go
index 09ae31b2..dc7dd9d3 100644
--- a/pkg/kubernetes/kubernetes_test.go
+++ b/pkg/kubernetes/kubernetes_test.go
@@ -88,7 +88,9 @@ func TestReconcile(t *testing.T) {
 
 	for _, c := range tests {
 		t.Run(c.name, func(t *testing.T) {
-			got, err := Reconcile(c.deep.(map[string]interface{}), c.spec, c.targets)
+			config := v1alpha1.New()
+			config.Spec = c.spec
+			got, err := Reconcile(c.deep.(map[string]interface{}), config.Spec, c.targets)
 
 			require.Equal(t, c.err, err)
 			assert.ElementsMatch(t, c.flat, got)
@@ -99,7 +101,7 @@ func TestReconcile(t *testing.T) {
 func TestReconcileOrder(t *testing.T) {
 	got := make([]manifest.List, 10)
 	for i := 0; i < 10; i++ {
-		r, err := Reconcile(testDataDeep().deep.(map[string]interface{}), v1alpha1.Spec{}, nil)
+		r, err := Reconcile(testDataDeep().deep.(map[string]interface{}), v1alpha1.New().Spec, nil)
 		require.NoError(t, err)
 		got[i] = r
 	}