Repository: grafana/tanka
Pull request #457 (2020-12-30 19:24:24)

refactor(api): use EvalScript on JsonnetOpts
Another iteration on evaluators. I hope this reduces code complexity by some degree, instead of passing functions along
through `ParseOpts`, this now passes an `EvalScript` along. It should make the API more flexible as arbitrary jsonnet
can be injected before evaluation.

Note: This (re)moves the `EvalPattern` option as it can now be called by the CLI function that uses it.

Review tip: hide whitespace changes.


Patch:
diff --git a/cmd/tk/jsonnet.go b/cmd/tk/jsonnet.go
index cb578835..16fb5f08 100644
--- a/cmd/tk/jsonnet.go
+++ b/cmd/tk/jsonnet.go
@@ -27,7 +27,7 @@ func evalCmd() *cli.Command {
 		jsonnetOpts := tanka.Opts{
 			JsonnetOpts: getJsonnetOpts(),
 		}
-		jsonnetOpts.EvalPattern = *evalPattern
+		jsonnetOpts.EvalScript = "(import '%s')." + *evalPattern
 		raw, err := tanka.Eval(args[0], jsonnetOpts)
 
 		if raw == nil && err != nil {
diff --git a/cmd/tk/main.go b/cmd/tk/main.go
index f3f220ae..f0de6499 100644
--- a/cmd/tk/main.go
+++ b/cmd/tk/main.go
@@ -79,7 +79,7 @@ func setupConfiguration(baseDir string) *v1alpha1.Environment {
 			}
 		// no spec.json is found, try parsing main.jsonnet
 		case spec.ErrNoSpec:
-			_, config, err := tanka.ParseEnv(baseDir, tanka.ParseOpts{Evaluator: tanka.EnvsOnlyEvaluator})
+			_, config, err := tanka.ParseEnv(baseDir, tanka.JsonnetOpts{EvalScript: tanka.EnvsOnlyEvalScript})
 			if err != nil {
 				switch err.(type) {
 				case tanka.ErrNoEnv:
diff --git a/pkg/jsonnet/eval.go b/pkg/jsonnet/eval.go
index 2741c1a0..812475f5 100644
--- a/pkg/jsonnet/eval.go
+++ b/pkg/jsonnet/eval.go
@@ -29,7 +29,7 @@ type Opts struct {
 	ExtCode     InjectedCode
 	TLACode     InjectedCode
 	ImportPaths []string
-	EvalPattern string
+	EvalScript  string
 }
 
 // MakeVM returns a Jsonnet VM with some extensions of Tanka, including:
diff --git a/pkg/tanka/evaluators.go b/pkg/tanka/evaluators.go
index 33f0bc2f..a6e8a766 100644
--- a/pkg/tanka/evaluators.go
+++ b/pkg/tanka/evaluators.go
@@ -9,20 +9,16 @@ import (
 	"github.com/grafana/tanka/pkg/jsonnet/jpath"
 )
 
-// Evaluator signature for implementing arbitrary Jsonnet evaluators
-type Evaluator func(path string, opts jsonnet.Opts) (string, error)
-
-// DefaultEvaluator evaluates the jsonnet environment at the given file system path
-func DefaultEvaluator(path string, opts jsonnet.Opts) (string, error) {
+// EvalJsonnet evaluates the jsonnet environment at the given file system path
+func EvalJsonnet(path string, opts jsonnet.Opts) (raw string, err error) {
 	entrypoint, err := jpath.Entrypoint(path)
 	if err != nil {
 		return "", err
 	}
 
 	// evaluate Jsonnet
-	var raw string
-	if opts.EvalPattern != "" {
-		evalScript := fmt.Sprintf("(import '%s').%s", entrypoint, opts.EvalPattern)
+	if opts.EvalScript != "" {
+		evalScript := fmt.Sprintf(opts.EvalScript, entrypoint)
 		raw, err = jsonnet.Evaluate(entrypoint, evalScript, opts)
 		if err != nil {
 			return "", errors.Wrap(err, "evaluating jsonnet")
@@ -36,49 +32,33 @@ func DefaultEvaluator(path string, opts jsonnet.Opts) (string, error) {
 	return raw, nil
 }
 
-// EnvsOnlyEvaluator finds the Environment object (without its .data object) at
-// the given file system path intended for use by the `tk env` command
-func EnvsOnlyEvaluator(path string, opts jsonnet.Opts) (string, error) {
-	entrypoint, err := jpath.Entrypoint(path)
-	if err != nil {
-		return "", err
-	}
-
-	// Snippet to find all Environment objects and remove the .data object for faster evaluation
-	noData := `
+// EnvsOnlyEvalScript finds the Environment object (without its .data object)
+const EnvsOnlyEvalScript = `
 local noDataEnv(object) =
-  if std.isObject(object)
-  then
-    if std.objectHas(object, 'apiVersion')
-       && std.objectHas(object, 'kind')
+  std.prune(
+    if std.isObject(object)
     then
-      if object.kind == 'Environment'
-      then object { data:: {} }
-      else {}
-    else
-      std.mapWithKey(
-        function(key, obj)
+      if std.objectHas(object, 'apiVersion')
+         && std.objectHas(object, 'kind')
+      then
+        if object.kind == 'Environment'
+        then object { data:: {} }
+        else {}
+      else
+        std.mapWithKey(
+          function(key, obj)
+            noDataEnv(obj),
+          object
+        )
+    else if std.isArray(object)
+    then
+      std.map(
+        function(obj)
           noDataEnv(obj),
         object
       )
-  else if std.isArray(object)
-  then
-    std.map(
-      function(obj)
-        noDataEnv(obj),
-      object
-    )
-  else {};
+    else {}
+  );
 
 noDataEnv(import '%s')
 `
-
-	// evaluate Jsonnet with noData snippet
-	var raw string
-	evalScript := fmt.Sprintf(noData, entrypoint)
-	raw, err = jsonnet.Evaluate(entrypoint, evalScript, opts)
-	if err != nil {
-		return "", errors.Wrap(err, "evaluating jsonnet")
-	}
-	return raw, nil
-}
diff --git a/pkg/tanka/parse.go b/pkg/tanka/parse.go
index 30808003..5f6adc13 100644
--- a/pkg/tanka/parse.go
+++ b/pkg/tanka/parse.go
@@ -64,7 +64,7 @@ func (p *loaded) connect() (*kubernetes.Kubernetes, error) {
 
 // load runs all processing stages described at the Processed type
 func load(path string, opts Opts) (*loaded, error) {
-	_, env, err := ParseEnv(path, ParseOpts{JsonnetOpts: opts.JsonnetOpts})
+	_, env, err := ParseEnv(path, opts.JsonnetOpts)
 	if err != nil {
 		return nil, err
 	}
@@ -123,7 +123,7 @@ func parseSpec(path string) (*v1alpha1.Environment, error) {
 
 // ParseEnv evaluates the jsonnet environment at the given file system path and
 // optionally also returns and Environment object
-func ParseEnv(path string, opts ParseOpts) (interface{}, *v1alpha1.Environment, error) {
+func ParseEnv(path string, opts JsonnetOpts) (interface{}, *v1alpha1.Environment, error) {
 	specEnv, err := parseSpec(path)
 	if err != nil {
 		switch err.(type) {
@@ -139,14 +139,10 @@ func ParseEnv(path string, opts ParseOpts) (interface{}, *v1alpha1.Environment,
 		if err != nil {
 			return nil, nil, errors.Wrap(err, "marshalling environment config")
 		}
-		opts.JsonnetOpts.ExtCode.Set(spec.APIGroup+"/environment", string(jsonEnv))
+		opts.ExtCode.Set(spec.APIGroup+"/environment", string(jsonEnv))
 	}
 
-	if opts.Evaluator == nil {
-		opts.Evaluator = DefaultEvaluator
-	}
-
-	raw, err := opts.Evaluator(path, opts.JsonnetOpts)
+	raw, err := EvalJsonnet(path, opts)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -156,8 +152,8 @@ func ParseEnv(path string, opts ParseOpts) (interface{}, *v1alpha1.Environment,
 		return nil, nil, errors.Wrap(err, "unmarshalling data")
 	}
 
-	if opts.JsonnetOpts.EvalPattern != "" {
-		// EvalPattern has no affinity with an environment, behave as jsonnet interpreter
+	if opts.EvalScript != "" {
+		// EvalScript has no affinity with an environment, behave as jsonnet interpreter
 		return data, nil, nil
 	}
 
diff --git a/pkg/tanka/tanka.go b/pkg/tanka/tanka.go
index 4bdd1afa..5564a5d4 100644
--- a/pkg/tanka/tanka.go
+++ b/pkg/tanka/tanka.go
@@ -18,8 +18,3 @@ type Opts struct {
 	// Filters are used to optionally select a subset of the resources
 	Filters process.Matchers
 }
-
-type ParseOpts struct {
-	JsonnetOpts
-	Evaluator Evaluator
-}
diff --git a/pkg/tanka/workflow.go b/pkg/tanka/workflow.go
index ecfa5f53..1a3f5d2c 100644
--- a/pkg/tanka/workflow.go
+++ b/pkg/tanka/workflow.go
@@ -184,7 +184,7 @@ func Show(baseDir string, opts Opts) (manifest.List, error) {
 
 // Eval returns the raw evaluated Jsonnet output (without any transformations)
 func Eval(dir string, opts Opts) (raw interface{}, err error) {
-	r, _, err := ParseEnv(dir, ParseOpts{JsonnetOpts: opts.JsonnetOpts})
+	r, _, err := ParseEnv(dir, opts.JsonnetOpts)
 	switch err.(type) {
 	case ErrNoEnv, ErrMultipleEnvs:
 		return r, err


Test patch:
diff --git a/pkg/tanka/parse_test.go b/pkg/tanka/parse_test.go
index 4145d298..ba960c8c 100644
--- a/pkg/tanka/parse_test.go
+++ b/pkg/tanka/parse_test.go
@@ -114,7 +114,7 @@ func TestEvalJsonnet(t *testing.T) {
 	}
 
 	for _, test := range cases {
-		data, env, e := ParseEnv(test.baseDir, ParseOpts{})
+		data, env, e := ParseEnv(test.baseDir, JsonnetOpts{})
 		if data == nil {
 			assert.NoError(t, e)
 		} else if e != nil {