Repository: grafana/tanka
Pull request #578 (2021-06-25 10:25:48)

feat: Add support to specify `--kube-version` in Helm template
This PR adds the support to specify a custom Kubernetes version when using `helm` feature. 

It would enable to use  helm charts such as [this](https://github.com/dexidp/helm-charts/blob/master/charts/dex/templates/ingress.yaml#L4)  for older kubernetes version easier. We use  `tk export` in CI (docker). Thus, helm cannot detect server's version and fall back to default built-in. 
As workaround, I downgraded the helm to the version where buit-in KubeVersion matches k8s version of the k8s clusters.
It would be much nicer to define the k8s version natively.



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/docs/docs/helm.mdx b/docs/docs/helm.mdx
index 1301db7a..5a4d79f3 100644
--- a/docs/docs/helm.mdx
+++ b/docs/docs/helm.mdx
@@ -83,6 +83,8 @@ The following options control how the command is invoked:
     includeCrds: true,
     // Equivalent to: --api-versions v1 --api-versions apps/v1
     apiVersions: ['v1', 'apps/v1']
+    // Equivalent to: --kube-version v1.20.0
+    kubeVersion: 'v1.20.0'
     // Equivalent to: --no-hooks
     noHooks: true,
 }
diff --git a/pkg/helm/template.go b/pkg/helm/template.go
index 3e56b52c..bf64c493 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"
@@ -67,6 +66,8 @@ type TemplateOpts struct {
 	// IncludeCRDs specifies whether CustomResourceDefinitions are included in
 	// the template output
 	IncludeCRDs bool
+	// Kubernetes version used for Capabilities.KubeVersion
+	KubeVersion string
 	// Namespace scope for this request
 	Namespace string
 	// NoHooks specifies whether hooks should be excluded from the template output
@@ -78,8 +79,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)
 	}
 
@@ -87,6 +87,10 @@ func (t TemplateOpts) Flags() []string {
 		flags = append(flags, "--include-crds")
 	}
 
+	if t.KubeVersion != "" {
+		flags = append(flags, "--kube-version="+t.KubeVersion)
+	}
+
 	if t.NoHooks {
 		flags = append(flags, "--no-hooks")
 	}
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]