Repository: grafana/tanka (go)

Issue:
Helm template --api-versions flag incorrectly formatted
If you pass `apiVersions` to the Helm template function:

```jsonnet
{
  local tanka = import 'github.com/grafana/jsonnet-libs/tanka-util/main.libsonnet',
  local helm = tanka.helm.new(std.thisFile),
  app: helm.template('my-app', '../../charts/my-app', {
    values: {
      serviceMonitor: {
        enabled: true,
      },
    },
    apiVersions: ['v1', 'apps/v1', 'monitoring.coreos.com/v1'],
  }),
}
```

then Helm will be called with the API versions as a comma-separated list:

```sh
--api-versions=v1,apps/v1,monitoring.coreos.com/v1
```

However, the [`--api-versions` flag](https://github.com/helm/helm/blob/47c8f48f209f363807c8fb9f17ef072d7ae32139/cmd/helm/template.go#L184) is [supposed to be specified multiple times](https://pkg.go.dev/github.com/spf13/pflag#StringArrayVarP), not as a comma-separated list:

```sh
--api-versions=v1 --api-versions=apps/v1 --api-versions=monitoring.coreos.com/v1
```

Pull request:
fix(helm): Pass multiple --api-versions flags to Helm 
Helm expects the --api-versions flag to be passed for each API, not once with a comma-separated list of APIs.

Fixes #573.

Patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..008074e2
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+    "go.formatTool": "gofmt"
+}
\ No newline at end of file
diff --git a/pkg/helm/template.go b/pkg/helm/template.go
index 3e56b52c..908a1d7a 100644
--- a/pkg/helm/template.go
+++ b/pkg/helm/template.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"io"
 	"os"
-	"strings"
 
 	"github.com/grafana/tanka/pkg/kubernetes/manifest"
 	"github.com/pkg/errors"
@@ -78,8 +77,7 @@ type TemplateOpts struct {
 func (t TemplateOpts) Flags() []string {
 	var flags []string
 
-	if t.APIVersions != nil {
-		value := strings.Join(t.APIVersions, ",")
+	for _, value := range t.APIVersions {
 		flags = append(flags, "--api-versions="+value)
 	}
 
diff --git a/pkg/tanka/evaluators.go b/pkg/tanka/evaluators.go
index 927419e3..9e9338c3 100644
--- a/pkg/tanka/evaluators.go
+++ b/pkg/tanka/evaluators.go
@@ -21,7 +21,7 @@ func evalJsonnet(path string, opts jsonnet.Opts) (raw string, err error) {
 	if opts.EvalScript != "" {
 		var tla []string
 		for k := range opts.TLACode {
-			tla = append(tla, k)
+			tla = append(tla, k+"="+k)
 		}
 		evalScript := fmt.Sprintf(`
   local main = (import '%s');
diff --git a/pkg/tanka/export.go b/pkg/tanka/export.go
index 8916bd0a..69109595 100644
--- a/pkg/tanka/export.go
+++ b/pkg/tanka/export.go
@@ -175,7 +175,7 @@ func writeExportFile(path string, data []byte) error {
 
 func createTemplate(format string, env manifest.Manifest) (*template.Template, error) {
 	// Replace all os.path separators in string with BelRune for creating subfolders
-	replaceFormat := strings.Replace(format, string(os.PathSeparator), BelRune, -1)
+	replaceFormat := replaceTmplText(format, string(os.PathSeparator), BelRune)
 
 	envMap := template.FuncMap{"env": func() manifest.Manifest { return env }}
 
@@ -189,6 +189,24 @@ func createTemplate(format string, env manifest.Manifest) (*template.Template, e
 	return template, nil
 }
 
+func replaceTmplText(s, old, new string) string {
+	parts := []string{}
+	l := strings.Index(s, "{{")
+	r := strings.Index(s, "}}") + 2
+
+	for l != -1 && l < r {
+		// replace only in text between template action blocks
+		text := strings.ReplaceAll(s[:l], old, new)
+		action := s[l:r]
+		parts = append(parts, text, action)
+		s = s[r:]
+		l = strings.Index(s, "{{")
+		r = strings.Index(s, "}}") + 2
+	}
+	parts = append(parts, strings.ReplaceAll(s, old, new))
+	return strings.Join(parts, "")
+}
+
 func applyTemplate(template *template.Template, m manifest.Manifest) (path string, err error) {
 	buf := bytes.Buffer{}
 	if err := template.Execute(&buf, m); err != nil {


Test patch:
diff --git a/pkg/tanka/evaluators_test.go b/pkg/tanka/evaluators_test.go
new file mode 100644
index 00000000..0d3013c9
--- /dev/null
+++ b/pkg/tanka/evaluators_test.go
@@ -0,0 +1,33 @@
+package tanka
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/grafana/tanka/pkg/jsonnet"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEvalJsonnet(t *testing.T) {
+	var tlaCode jsonnet.InjectedCode
+	// Pass in the mandatory parameters as TLA codes, but note that only `foo`
+	// contains `data`, which is a valid key inside the `o` object defined in
+	// testdata/cases/withtlas/main.jsonnet. If they are passed as positional
+	// parameters, then their names are ignored, which will lead to arbitrary
+	// failures because the order in which they're passed is random.
+	// `evalJsonnet` has been updated to pass them as named parameters.
+	tlaCode.Set("foo", "'data'")
+	tlaCode.Set("bar", "'kaboom'")
+	tlaCode.Set("baz", "'kaboom'")
+
+	opts := jsonnet.Opts{
+		EvalScript: "main",
+		TLACode:    tlaCode,
+	}
+
+	// This will fail intermittently if TLAs are passed as positional
+	// parameters.
+	json, err := evalJsonnet("testdata/cases/withtlas", opts)
+	assert.NoError(t, err)
+	assert.Equal(t, `"foovalue"`, strings.TrimSpace(json))
+}
diff --git a/pkg/tanka/export_test.go b/pkg/tanka/export_test.go
new file mode 100644
index 00000000..9663c695
--- /dev/null
+++ b/pkg/tanka/export_test.go
@@ -0,0 +1,33 @@
+package tanka
+
+import "testing"
+
+func Test_replaceTmplText(t *testing.T) {
+	type args struct {
+		s   string
+		old string
+		new string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{"text only", args{"a", "a", "b"}, "b"},
+		{"action blocks", args{"{{a}}{{.}}", "a", "b"}, "{{a}}{{.}}"},
+		{"mixed", args{"a{{a}}a{{a}}a", "a", "b"}, "b{{a}}b{{a}}b"},
+		{"invalid template format handled as text", args{"a}}a{{a", "a", "b"}, "b}}b{{b"},
+		{
+			name: "keep path separator in action block",
+			args: args{`{{index .metadata.labels "app.kubernetes.io/name"}}/{{.metadata.name}}`, "/", BelRune},
+			want: "{{index .metadata.labels \"app.kubernetes.io/name\"}}\u0007{{.metadata.name}}",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := replaceTmplText(tt.args.s, tt.args.old, tt.args.new); got != tt.want {
+				t.Errorf("replaceInTmplText() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/tanka/testdata/cases/withtlas/main.jsonnet b/pkg/tanka/testdata/cases/withtlas/main.jsonnet
new file mode 100644
index 00000000..c6442130
--- /dev/null
+++ b/pkg/tanka/testdata/cases/withtlas/main.jsonnet
@@ -0,0 +1,5 @@
+local o = {
+  data: 'foovalue',
+};
+
+function(foo, bar, baz) o[foo]