Repository: grafana/tanka
Pull request #336 (2020-08-09 17:10:22)

feat: provide std.native('helmTemplate')
I've taken the same approach as to how the `helm template` command is build up:

```
$ helm template --help
...
Usage:
  helm template [NAME] [CHART] [flags]
...
```


```
local helm = import 'github.com/grafana/jsonnet-libs/helm-util/helm.libsonnet';

{
  version:: 'v0.16.0',

  values:: {
    installCRDs: true,
    global: {
      podSecurityPolicy: {
        enabled: true,
        useAppArmor: false,
      },
    },
  },

  chart: helm.template(
    name='cert-manager',
    chart='jetstack/cert-manager',
    conf={
      values: $.values,
      flags: [
        '--version=%s % $.version',
      ],
    },
  ),
}
```


original PoC: https://github.com/grafana/jsonnet-libs/pull/315


Patch:
diff --git a/.gitignore b/.gitignore
index 16c5327c..a5546f77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 dist
+tk
 
diff --git a/README.md b/README.md
index 0a2bac96..482ac9f1 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 
 <p align="center">
   <a href="https://cloud.drone.io/grafana/tanka">
-    <img src="https://img.shields.io/drone/build/grafana/tanka?style=flat-square">
+    <img src="https://img.shields.io/drone/build/grafana/tanka?style=flat-square&server=https%3A%2F%2Fdrone.grafana.net">
   </a>
   <a href="https://github.com/grafana/tanka/releases">
     <img src="https://img.shields.io/github/release/grafana/tanka?style=flat-square" />
diff --git a/cmd/tk/export.go b/cmd/tk/export.go
index 61d0e67e..d9324d83 100644
--- a/cmd/tk/export.go
+++ b/cmd/tk/export.go
@@ -33,7 +33,7 @@ func exportCmd() *cli.Command {
 	}
 
 	vars := workflowFlags(cmd.Flags())
-	getExtCode := extCodeParser(cmd.Flags())
+	getExtCode, getTLACode := cliCodeParser(cmd.Flags())
 	format := cmd.Flags().String("format", "{{.apiVersion}}.{{.kind}}-{{.metadata.name}}", "https://tanka.dev/exporting#filenames")
 	extension := cmd.Flags().String("extension", "yaml", "File extension")
 	merge := cmd.Flags().Bool("merge", false, "Allow merging with existing directory")
@@ -68,6 +68,7 @@ func exportCmd() *cli.Command {
 		// get the manifests
 		res, err := tanka.Show(args[0],
 			tanka.WithExtCode(getExtCode()),
+			tanka.WithTLACode(getTLACode()),
 			tanka.WithTargets(stringsToRegexps(vars.targets)),
 		)
 		if err != nil {
diff --git a/cmd/tk/jsonnet.go b/cmd/tk/jsonnet.go
index 01e39393..e42a0a25 100644
--- a/cmd/tk/jsonnet.go
+++ b/cmd/tk/jsonnet.go
@@ -6,9 +6,8 @@ import (
 	"log"
 	"strings"
 
-	"github.com/spf13/pflag"
-
 	"github.com/go-clix/cli"
+	"github.com/spf13/pflag"
 
 	"github.com/grafana/tanka/pkg/tanka"
 )
@@ -20,11 +19,12 @@ func evalCmd() *cli.Command {
 		Args:  workflowArgs,
 	}
 
-	getExtCode := extCodeParser(cmd.Flags())
+	getExtCode, getTLACode := cliCodeParser(cmd.Flags())
 
 	cmd.Run = func(cmd *cli.Command, args []string) error {
 		raw, err := tanka.Eval(args[0],
 			tanka.WithExtCode(getExtCode()),
+			tanka.WithTLACode(getTLACode()),
 		)
 
 		if err != nil {
@@ -43,29 +43,37 @@ func evalCmd() *cli.Command {
 	return cmd
 }
 
-func extCodeParser(fs *pflag.FlagSet) func() map[string]string {
+func cliCodeParser(fs *pflag.FlagSet) (func() map[string]string, func() map[string]string) {
 	// need to use StringArray instead of StringSlice, because pflag attempts to
 	// parse StringSlice using the csv parser, which breaks when passing objects
-	values := fs.StringArrayP("extCode", "e", nil, "Inject any Jsonnet from the outside (Format: key=<code>)")
-	strs := fs.StringArray("extVar", nil, "Inject a string from the outside (Format: key=value)")
+	extCode := fs.StringArray("ext-code", nil, "Set code value of extVar (Format: key=<code>)")
+	extStr := fs.StringArrayP("ext-str", "V", nil, "Set string value of extVar (Format: key=value)")
 
-	return func() map[string]string {
-		m := make(map[string]string)
-		for _, s := range *values {
-			split := strings.SplitN(s, "=", 2)
-			if len(split) != 2 {
-				log.Fatalf("extCode argument has wrong format: `%s`. Expected `key=<code>`", s)
+	tlaCode := fs.StringArray("tla-code", nil, "Set code value of top level function (Format: key=<code>)")
+	tlaStr := fs.StringArrayP("tla-str", "A", nil, "Set string value of top level function (Format: key=value)")
+
+	newParser := func(kind string, code, str *[]string) func() map[string]string {
+		return func() map[string]string {
+			m := make(map[string]string)
+			for _, s := range *code {
+				split := strings.SplitN(s, "=", 2)
+				if len(split) != 2 {
+					log.Fatalf(kind+"-code argument has wrong format: `%s`. Expected `key=<code>`", s)
+				}
+				m[split[0]] = split[1]
 			}
-			m[split[0]] = split[1]
-		}
 
-		for _, s := range *strs {
-			split := strings.SplitN(s, "=", 2)
-			if len(split) != 2 {
-				log.Fatalf("extCode argument has wrong format: `%s`. Expected `key=<value>`", s)
+			for _, s := range *str {
+				split := strings.SplitN(s, "=", 2)
+				if len(split) != 2 {
+					log.Fatalf(kind+"-str argument has wrong format: `%s`. Expected `key=<value>`", s)
+				}
+				m[split[0]] = fmt.Sprintf(`"%s"`, split[1])
 			}
-			m[split[0]] = fmt.Sprintf(`"%s"`, split[1])
+			return m
 		}
-		return m
 	}
+
+	return newParser("ext", extCode, extStr),
+		newParser("tla", tlaCode, tlaStr)
 }
diff --git a/cmd/tk/workflow.go b/cmd/tk/workflow.go
index 4c8f7a78..352a1b03 100644
--- a/cmd/tk/workflow.go
+++ b/cmd/tk/workflow.go
@@ -44,12 +44,14 @@ func applyCmd() *cli.Command {
 	force := cmd.Flags().Bool("force", false, "force applying (kubectl apply --force)")
 	validate := cmd.Flags().Bool("validate", true, "validation of resources (kubectl --validate=false)")
 	autoApprove := cmd.Flags().Bool("dangerous-auto-approve", false, "skip interactive approval. Only for automation!")
-	getExtCode := extCodeParser(cmd.Flags())
+
+	getExtCode, getTLACode := cliCodeParser(cmd.Flags())
 
 	cmd.Run = func(cmd *cli.Command, args []string) error {
 		err := tanka.Apply(args[0],
 			tanka.WithTargets(stringsToRegexps(vars.targets)),
 			tanka.WithExtCode(getExtCode()),
+			tanka.WithTLACode(getTLACode()),
 			tanka.WithApplyForce(*force),
 			tanka.WithApplyValidate(*validate),
 			tanka.WithApplyAutoApprove(*autoApprove),
@@ -69,13 +71,14 @@ func pruneCmd() *cli.Command {
 		Args:  workflowArgs,
 	}
 
-	getExtCode := extCodeParser(cmd.Flags())
+	getExtCode, getTLACode := cliCodeParser(cmd.Flags())
 	autoApprove := cmd.Flags().Bool("dangerous-auto-approve", false, "skip interactive approval. Only for automation!")
 	force := cmd.Flags().Bool("force", false, "force deleting (kubectl delete --force)")
 
 	cmd.Run = func(cmd *cli.Command, args []string) error {
 		return tanka.Prune(args[0],
 			tanka.WithExtCode(getExtCode()),
+			tanka.WithTLACode(getTLACode()),
 			tanka.WithApplyAutoApprove(*autoApprove),
 			tanka.WithApplyForce(*force),
 		)
@@ -95,12 +98,13 @@ func deleteCmd() *cli.Command {
 	force := cmd.Flags().Bool("force", false, "force deleting (kubectl delete --force)")
 	validate := cmd.Flags().Bool("validate", true, "validation of resources (kubectl --validate=false)")
 	autoApprove := cmd.Flags().Bool("dangerous-auto-approve", false, "skip interactive approval. Only for automation!")
-	getExtCode := extCodeParser(cmd.Flags())
+	getExtCode, getTLACode := cliCodeParser(cmd.Flags())
 
 	cmd.Run = func(cmd *cli.Command, args []string) error {
 		err := tanka.Delete(args[0],
 			tanka.WithTargets(stringsToRegexps(vars.targets)),
 			tanka.WithExtCode(getExtCode()),
+			tanka.WithTLACode(getTLACode()),
 			tanka.WithApplyForce(*force),
 			tanka.WithApplyValidate(*validate),
 			tanka.WithApplyAutoApprove(*autoApprove),
@@ -130,12 +134,13 @@ func diffCmd() *cli.Command {
 		summarize    = cmd.Flags().BoolP("summarize", "s", false, "quick summary of the differences, hides file contents")
 	)
 
-	getExtCode := extCodeParser(cmd.Flags())
+	getExtCode, getTLACode := cliCodeParser(cmd.Flags())
 
 	cmd.Run = func(cmd *cli.Command, args []string) error {
 		changes, err := tanka.Diff(args[0],
 			tanka.WithTargets(stringsToRegexps(vars.targets)),
 			tanka.WithExtCode(getExtCode()),
+			tanka.WithTLACode(getTLACode()),
 			tanka.WithDiffStrategy(*diffStrategy),
 			tanka.WithDiffSummarize(*summarize),
 		)
@@ -170,7 +175,7 @@ func showCmd() *cli.Command {
 	}
 	vars := workflowFlags(cmd.Flags())
 	allowRedirect := cmd.Flags().Bool("dangerous-allow-redirect", false, "allow redirecting output to a file or a pipe.")
-	getExtCode := extCodeParser(cmd.Flags())
+	getExtCode, getTLACode := cliCodeParser(cmd.Flags())
 	cmd.Run = func(cmd *cli.Command, args []string) error {
 		if !interactive && !*allowRedirect {
 			fmt.Fprintln(os.Stderr, `Redirection of the output of tk show is discouraged and disabled by default.
@@ -181,6 +186,7 @@ Otherwise run tk show --dangerous-allow-redirect to bypass this check.`)
 
 		pretty, err := tanka.Show(args[0],
 			tanka.WithExtCode(getExtCode()),
+			tanka.WithTLACode(getTLACode()),
 			tanka.WithTargets(stringsToRegexps(vars.targets)),
 		)
 		if err != nil {
diff --git a/docs/docs/env-vars.md b/docs/docs/env-vars.md
index 3ac22931..abe70b03 100644
--- a/docs/docs/env-vars.md
+++ b/docs/docs/env-vars.md
@@ -19,3 +19,8 @@ route: "/env-vars"
 
 **Description**: Print all calls to `kubectl`  
 **Default**: `false`
+
+### TANKA_HELM_PATH
+
+**Description**: Path to the `helm` executable
+**Default**: `$PATH/helm`
diff --git a/docs/docs/jsonnet/injecting-values.md b/docs/docs/jsonnet/injecting-values.md
new file mode 100644
index 00000000..7d296cbe
--- /dev/null
+++ b/docs/docs/jsonnet/injecting-values.md
@@ -0,0 +1,94 @@
+---
+name: "Injecting Values"
+route: "/jsonnet/injecting-values"
+menu: "Writing Jsonnet"
+---
+
+# Injecting Values
+
+Sometimes it might be required to pass externally acquired data into Jsonnet.
+
+There are three ways of doing so:
+
+1. [JSON files](#json-files)
+2. [External variables](#external-variables)
+3. [Top level arguments](#top-level-arguments)
+
+Also check out the [official Jsonnet docs on this
+topic](https://jsonnet.org/ref/language.html#passing-data-to-jsonnet).
+
+## JSON files
+
+Jsonnet is a superset of JSON, it treats any JSON as valid Jsonnet. Because many
+systems can be told to output their data in JSON format, this provides a pretty
+good interface between those.
+
+For example, your build tooling like `make` could acquire secrets from systems such as
+[Vault](https://www.vaultproject.io/), etc. and write that into `secrets.json`.
+
+```jsonnet
+local secrets = import "secrets.json";
+
+{
+  foo: secrets.myPassword,
+}
+```
+
+> **Note**: Using `import` with JSON treats it as Jsonnet, so make sure to not
+> use it with untrusted code.  
+> A safer, but more verbose, alternative is `std.parseJson(importstr 'path_to_json.json')`
+
+## External variables
+
+Another way of passing values from the outside are external variables, which are specified like so:
+
+```bash
+# strings
+$ tk show . --ext-str hello=world
+
+# any Jsonnet snippet
+$ tk show . --ext-code foo=4 --ext-code bar='[ 1, 3 ]'
+```
+
+They can be accessed using `std.extVar` and the name given to them on the command line:
+
+```jsonnet
+{
+  foo: std.extVar('foo'), // 4, integer
+  bar: std.extVar('bar'), // [ 1, 3 ], array
+}
+```
+
+> **Warning**: External variables are directly accessible in all parts of the
+> configuration, which can make it difficult to track where they are used and
+> what effect they have on the final result.
+> Try to use [Top Level Arguments](#top-level-arguments) instead.
+
+## Top Level Arguments
+
+Usually with Tanka, your `main.jsonnet` holds an object at the top level (most
+outer type in the generated JSON):
+
+```jsonnet
+// main.jsonnet
+{
+  /* your resources */
+}
+```
+
+Another type of Jsonnet that naturally accepts parameters is the `function`.
+When the Jsonnet compiler finds a function at the top level, it invokes it and
+allows passing parameter values from the command line:
+
+```jsonnet
+// Actual output (object) returned by function, which is taking parameters and default values
+function(who, msg="Hello %s!") {
+  hello: msg % who
+}
+```
+
+Here, `who` needs a value while `msg` has a default. This can be invoked like so:
+
+```bash
+$ tk show . --tla-str who=John
+```
diff --git a/docs/docs/targets.md b/docs/docs/targets.md
index 6b8edb25..e8fdb270 100644
--- a/docs/docs/targets.md
+++ b/docs/docs/targets.md
@@ -78,3 +78,14 @@ apiVersion: apps/v1
 kind: Deployment
 # ...
 ```
+
+## Excluding
+
+Sometimes it may be desirably to exclude a single object, instead of including all others.
+
+To do so, prepend the regular expression with an exclamation mark (`!`), like so:
+
+```bash
+# filter out all Deployments
+$ tk show . -t '!deployment/.*'
+```
diff --git a/pkg/helmraiser/helm.go b/pkg/helmraiser/helm.go
new file mode 100644
index 00000000..32569147
--- /dev/null
+++ b/pkg/helmraiser/helm.go
@@ -0,0 +1,161 @@
+package helmraiser
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"strings"
+
+	jsonnet "github.com/google/go-jsonnet"
+	"github.com/google/go-jsonnet/ast"
+	"github.com/pkg/errors"
+	yaml "gopkg.in/yaml.v3"
+)
+
+type HelmConf struct {
+	Values map[string]interface{}
+	Flags  []string
+}
+
+func confToArgs(conf HelmConf) ([]string, []string, error) {
+	var args []string
+	var tempFiles []string
+
+	// create file and append to args
+	if len(conf.Values) != 0 {
+		valuesYaml, err := yaml.Marshal(conf.Values)
+		if err != nil {
+			return nil, nil, err
+		}
+		tmpFile, err := ioutil.TempFile(os.TempDir(), "tanka-")
+		if err != nil {
+			return nil, nil, errors.Wrap(err, "cannot create temporary values.yaml")
+		}
+		tempFiles = append(tempFiles, tmpFile.Name())
+		if _, err = tmpFile.Write(valuesYaml); err != nil {
+			return nil, tempFiles, errors.Wrap(err, "failed to write to temporary values.yaml")
+		}
+		if err := tmpFile.Close(); err != nil {
+			return nil, tempFiles, err
+		}
+		args = append(args, fmt.Sprintf("--values=%s", tmpFile.Name()))
+	}
+
+	// append custom flags to args
+	args = append(args, conf.Flags...)
+
+	if len(args) == 0 {
+		args = nil
+	}
+
+	return args, tempFiles, nil
+}
+
+func parseYamlToMap(yamlFile []byte) (map[string]interface{}, error) {
+	files := make(map[string]interface{})
+	d := yaml.NewDecoder(bytes.NewReader(yamlFile))
+	for {
+		var doc, jsonDoc interface{}
+		if err := d.Decode(&doc); err != nil {
+			if err == io.EOF {
+				break
+			}
+			return nil, errors.Wrap(err, "parsing manifests")
+		}
+
+		jsonRaw, err := json.Marshal(doc)
+		if err != nil {
+			return nil, errors.Wrap(err, "marshaling mainfests")
+		}
+
+		if err := json.Unmarshal(jsonRaw, &jsonDoc); err != nil {
+			return nil, errors.Wrap(err, "unmarshaling manifests")
+		}
+
+		// Unmarshal name and kind
+		kindName := struct {
+			Kind     string `json:"kind"`
+			Metadata struct {
+				Name string `json:"name"`
+			} `json:"metadata"`
+		}{}
+		if err := json.Unmarshal(jsonRaw, &kindName); err != nil {
+			return nil, errors.Wrap(err, "subtracting kind/name through unmarshaling")
+		}
+
+		// snake_case string
+		normalizeName := func(s string) string {
+			s = strings.ReplaceAll(s, "-", "_")
+			s = strings.ReplaceAll(s, ":", "_")
+			s = strings.ToLower(s)
+			return s
+		}
+
+		// create a map of resources for ease of use in jsonnet
+		name := normalizeName(fmt.Sprintf("%s_%s", kindName.Metadata.Name, kindName.Kind))
+		if jsonDoc != nil {
+			files[name] = jsonDoc
+		}
+	}
+	return files, nil
+}
+
+// helmTemplate wraps and runs `helm template`
+// returns the generated manifests in a map
+func HelmTemplate() *jsonnet.NativeFunction {
+	return &jsonnet.NativeFunction{
+		Name: "helmTemplate",
+		// Lines up with `helm template [NAME] [CHART] [flags]` except 'conf' is a bit more elaborate
+		Params: ast.Identifiers{"name", "chart", "conf"},
+		Func: func(data []interface{}) (interface{}, error) {
+			name, chart := data[0].(string), data[1].(string)
+
+			c, err := json.Marshal(data[2])
+			if err != nil {
+				return "", err
+			}
+			var conf HelmConf
+			if err := json.Unmarshal(c, &conf); err != nil {
+				return "", err
+			}
+
+			// the basic arguments to make this work
+			args := []string{
+				"template",
+				name,
+				chart,
+			}
+
+			confArgs, tempFiles, err := confToArgs(conf)
+			if err != nil {
+				return "", nil
+			}
+			for _, file := range tempFiles {
+				defer os.Remove(file)
+			}
+			if confArgs != nil {
+				args = append(args, confArgs...)
+			}
+
+			helmBinary := "helm"
+			if hc := os.Getenv("TANKA_HELM_PATH"); hc != "" {
+				helmBinary = hc
+			}
+
+			// convert the values map into a yaml file
+			cmd := exec.Command(helmBinary, args...)
+			buf := bytes.Buffer{}
+			cmd.Stdout = &buf
+			cmd.Stderr = os.Stderr
+			if err := cmd.Run(); err != nil {
+				return nil, fmt.Errorf("running 'helm %s': %w", strings.Join(args, " "), err)
+			}
+
+			return parseYamlToMap(buf.Bytes())
+		},
+	}
+}
diff --git a/pkg/jsonnet/eval.go b/pkg/jsonnet/eval.go
index 34cf00e1..b2ae10fc 100644
--- a/pkg/jsonnet/eval.go
+++ b/pkg/jsonnet/eval.go
@@ -55,3 +55,11 @@ func WithExtCode(key, code string) Modifier {
 		return nil
 	}
 }
+
+// WithTLA allows to set the given code as a top level argument
+func WithTLA(key, code string) Modifier {
+	return func(vm *jsonnet.VM) error {
+		vm.TLACode(key, code)
+		return nil
+	}
+}
diff --git a/pkg/jsonnet/native/funcs.go b/pkg/jsonnet/native/funcs.go
index 938f5bbe..a22bffce 100644
--- a/pkg/jsonnet/native/funcs.go
+++ b/pkg/jsonnet/native/funcs.go
@@ -9,6 +9,7 @@ import (
 
 	jsonnet "github.com/google/go-jsonnet"
 	"github.com/google/go-jsonnet/ast"
+	"github.com/grafana/tanka/pkg/helmraiser"
 	"github.com/pkg/errors"
 	yaml "gopkg.in/yaml.v3"
 )
@@ -29,6 +30,8 @@ func Funcs() []*jsonnet.NativeFunction {
 		escapeStringRegex(),
 		regexMatch(),
 		regexSubst(),
+
+		helmraiser.HelmTemplate(),
 	}
 }
 
diff --git a/pkg/kubernetes/manifest/manifest.go b/pkg/kubernetes/manifest/manifest.go
index f2c1695f..688cb774 100644
--- a/pkg/kubernetes/manifest/manifest.go
+++ b/pkg/kubernetes/manifest/manifest.go
@@ -219,37 +219,22 @@ func (m Metadata) UID() string {
 }
 
 // Labels of the manifest
-func (m Metadata) Labels() map[string]string {
-	return safeStringMap(m, "labels")
+func (m Metadata) Labels() map[string]interface{} {
+	return safeMSI(m, "labels")
 }
 
 // Annotations of the manifest
-func (m Metadata) Annotations() map[string]string {
-	return safeStringMap(m, "annotations")
+func (m Metadata) Annotations() map[string]interface{} {
+	return safeMSI(m, "annotations")
 }
 
-// safeStringMap safely returns a string map:
-// - returns if map[string]string
-// - converts if map[string]interface{}
-// - zeroes if anything else
-func safeStringMap(m map[string]interface{}, key string) map[string]string {
+func safeMSI(m map[string]interface{}, key string) map[string]interface{} {
 	switch t := m[key].(type) {
-	case map[string]string:
-		return t
 	case map[string]interface{}:
-		mss := make(map[string]string)
-		for k, v := range t {
-			s, ok := v.(string)
-			if !ok {
-				continue
-			}
-			mss[k] = s
-		}
-		m[key] = mss
-		return m[key].(map[string]string)
+		return t
 	default:
-		m[key] = make(map[string]string)
-		return m[key].(map[string]string)
+		m[key] = make(map[string]interface{})
+		return m[key].(map[string]interface{})
 	}
 }
 
diff --git a/pkg/kubernetes/subsetdiff.go b/pkg/kubernetes/subsetdiff.go
index f3af4b2a..ce87dc45 100644
--- a/pkg/kubernetes/subsetdiff.go
+++ b/pkg/kubernetes/subsetdiff.go
@@ -4,7 +4,6 @@ import (
 	"strings"
 
 	"github.com/pkg/errors"
-	yaml "gopkg.in/yaml.v3"
 
 	"github.com/grafana/tanka/pkg/kubernetes/client"
 	"github.com/grafana/tanka/pkg/kubernetes/manifest"
@@ -93,60 +92,55 @@ func subsetDiff(c client.Client, m manifest.Manifest) (*difference, error) {
 		return nil, errors.Wrap(err, "getting state from cluster")
 	}
 
-	should, err := yaml.Marshal(m)
-	if err != nil {
-		return nil, err
-	}
+	should := m.String()
 
-	is, err := yaml.Marshal(subset(m, rawIs))
-	if err != nil {
-		return nil, err
-	}
-	if string(is) == "{}\n" {
-		is = []byte("")
+	sub := subset(m, rawIs)
+	is := manifest.Manifest(sub).String()
+	if is == "{}\n" {
+		is = ""
 	}
 
 	return &difference{
 		name:   name,
-		live:   string(is),
-		merged: string(should),
+		live:   is,
+		merged: should,
 	}, nil
 }
 
-// subset removes all keys from is, that are not present in should.
-// It makes is a subset of should.
+// subset removes all keys from big, that are not present in small.
+// It makes big a subset of small.
 // Kubernetes returns more keys than we can know about.
 // This means, we need to remove all keys from the kubectl output, that are not present locally.
-func subset(should, is map[string]interface{}) map[string]interface{} {
-	if should["namespace"] != nil {
-		is["namespace"] = should["namespace"]
+func subset(small, big map[string]interface{}) map[string]interface{} {
+	if small["namespace"] != nil {
+		big["namespace"] = small["namespace"]
 	}
 
 	// just ignore the apiVersion for now, too much bloat
-	if should["apiVersion"] != nil && is["apiVersion"] != nil {
-		is["apiVersion"] = should["apiVersion"]
+	if small["apiVersion"] != nil && big["apiVersion"] != nil {
+		big["apiVersion"] = small["apiVersion"]
 	}
 
-	for k, v := range is {
-		if should[k] == nil {
-			delete(is, k)
+	for k, v := range big {
+		if _, ok := small[k]; !ok {
+			delete(big, k)
 			continue
 		}
 
 		switch b := v.(type) {
 		case map[string]interface{}:
-			if a, ok := should[k].(map[string]interface{}); ok {
-				is[k] = subset(a, b)
+			if a, ok := small[k].(map[string]interface{}); ok {
+				big[k] = subset(a, b)
 			}
 		case []map[string]interface{}:
 			for i := range b {
-				if a, ok := should[k].([]map[string]interface{}); ok {
+				if a, ok := small[k].([]map[string]interface{}); ok {
 					b[i] = subset(a[i], b[i])
 				}
 			}
 		case []interface{}:
 			for i := range b {
-				if a, ok := should[k].([]interface{}); ok {
+				if a, ok := small[k].([]interface{}); ok {
 					if i >= len(a) {
 						// slice in config shorter than in live. Abort, as there are no entries to diff anymore
 						break
@@ -168,5 +162,5 @@ func subset(should, is map[string]interface{}) map[string]interface{} {
 			}
 		}
 	}
-	return is
+	return big
 }
diff --git a/pkg/process/filter.go b/pkg/process/filter.go
index 8e8d7e79..6b2b8c5d 100644
--- a/pkg/process/filter.go
+++ b/pkg/process/filter.go
@@ -9,12 +9,16 @@ import (
 )
 
 // Filter returns all elements of the list that match at least one expression
+// and are not ignored
 func Filter(list manifest.List, exprs Matchers) manifest.List {
 	out := make(manifest.List, 0, len(list))
 	for _, m := range list {
 		if !exprs.MatchString(m.KindName()) {
 			continue
 		}
+		if exprs.IgnoreString(m.KindName()) {
+			continue
+		}
 		out = append(out, m)
 	}
 	return out
@@ -26,7 +30,13 @@ type Matcher interface {
 	MatchString(string) bool
 }
 
+// Ignorer is like matcher, but for explicitely ignoring resources
+type Ignorer interface {
+	IgnoreString(string) bool
+}
+
 // Matchers is a collection of multiple expressions.
+// A matcher may also implement Ignorer to explicitely ignore fields
 type Matchers []Matcher
 
 // MatchString returns whether at least one expression (OR) matches the string
@@ -38,6 +48,18 @@ func (e Matchers) MatchString(s string) bool {
 	return b
 }
 
+func (e Matchers) IgnoreString(s string) bool {
+	b := false
+	for _, exp := range e {
+		i, ok := exp.(Ignorer)
+		if !ok {
+			continue
+		}
+		b = b || i.IgnoreString(s)
+	}
+	return b
+}
+
 // RegExps is a helper to construct Matchers from regular expressions
 func RegExps(rs []*regexp.Regexp) Matchers {
 	xprs := make(Matchers, 0, len(rs))
@@ -50,11 +72,20 @@ func RegExps(rs []*regexp.Regexp) Matchers {
 func StrExps(strs ...string) (Matchers, error) {
 	exps := make(Matchers, 0, len(strs))
 	for _, raw := range strs {
-		s := fmt.Sprintf(`(?i)^%s$`, raw)
+		// trim exlamation mark, not supported by regex
+		s := fmt.Sprintf(`(?i)^%s$`, strings.TrimPrefix(raw, "!"))
+
+		// create regexp matcher
+		var exp Matcher
 		exp, err := regexp.Compile(s)
 		if err != nil {
 			return nil, ErrBadExpr{err}
 		}
+
+		// if negative (!), invert regex behaviour
+		if strings.HasPrefix(raw, "!") {
+			exp = NegMatcher{exp: exp}
+		}
 		exps = append(exps, exp)
 	}
 	return exps, nil
@@ -76,3 +107,16 @@ type ErrBadExpr struct {
 func (e ErrBadExpr) Error() string {
 	return fmt.Sprintf("%s.\nSee https://tanka.dev/output-filtering/#regular-expressions for details on regular expressions.", strings.Title(e.inner.Error()))
 }
+
+// NexMatcher is a matcher that inverts the original behaviour
+type NegMatcher struct {
+	exp Matcher
+}
+
+func (n NegMatcher) MatchString(s string) bool {
+	return true
+}
+
+func (n NegMatcher) IgnoreString(s string) bool {
+	return n.exp.MatchString(s)
+}
diff --git a/pkg/tanka/tanka.go b/pkg/tanka/tanka.go
index fe4f58fa..bc58e93e 100644
--- a/pkg/tanka/tanka.go
+++ b/pkg/tanka/tanka.go
@@ -21,6 +21,7 @@ func parseModifiers(mods []Modifier) *options {
 type options struct {
 	// `std.extVar`
 	extCode map[string]string
+	tlaCode map[string]string
 
 	// target regular expressions to limit the working set
 	targets process.Matchers
@@ -44,6 +45,12 @@ func WithExtCode(code map[string]string) Modifier {
 	}
 }
 
+func WithTLACode(code map[string]string) Modifier {
+	return func(opts *options) {
+		opts.extCode = code
+	}
+}
+
 // WithTargets allows to submit regular expressions to limit the working set of
 // objects (https://tanka.dev/output-filtering/).
 func WithTargets(t process.Matchers) Modifier {


Test patch:
diff --git a/pkg/helmraiser/helm_test.go b/pkg/helmraiser/helm_test.go
new file mode 100644
index 00000000..3b17cff8
--- /dev/null
+++ b/pkg/helmraiser/helm_test.go
@@ -0,0 +1,191 @@
+package helmraiser
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConfToArgs_noconf(t *testing.T) {
+	conf := HelmConf{}
+	args, tempFiles, err := confToArgs(conf)
+	for _, file := range tempFiles {
+		defer os.Remove(file)
+	}
+
+	assert.Equal(t, []string(nil), args)
+	assert.Nil(t, err)
+}
+
+func TestConfToArgs_emptyconf(t *testing.T) {
+	conf := HelmConf{
+		Values: map[string]interface{}{},
+		Flags:  []string{},
+	}
+
+	args, tempFiles, err := confToArgs(conf)
+	for _, file := range tempFiles {
+		defer os.Remove(file)
+	}
+
+	assert.Equal(t, []string(nil), args)
+	assert.Nil(t, err)
+}
+
+func TestConfToArgs_flags(t *testing.T) {
+	conf := HelmConf{
+		Flags: []string{
+			"--version=v0.1",
+			"--random=arg",
+		},
+	}
+
+	args, tempFiles, err := confToArgs(conf)
+	for _, file := range tempFiles {
+		defer os.Remove(file)
+	}
+
+	assert.Equal(t, []string{
+		"--version=v0.1",
+		"--random=arg",
+	}, args)
+	assert.Nil(t, err)
+}
+
+func TestConfToArgs_values(t *testing.T) {
+	conf := HelmConf{
+		Values: map[string]interface{}{
+			"hasValues": "yes",
+		},
+	}
+
+	args, tempFiles, err := confToArgs(conf)
+	for _, file := range tempFiles {
+		defer os.Remove(file)
+	}
+
+	assert.FileExists(t, tempFiles[0])
+	assert.Equal(t, []string{fmt.Sprintf("--values=%s", tempFiles[0])}, args)
+	assert.Nil(t, err)
+}
+
+func TestConfToArgs_flagsvalues(t *testing.T) {
+	conf := HelmConf{
+		Values: map[string]interface{}{
+			"hasValues": "yes",
+		},
+		Flags: []string{
+			"--version=v0.1",
+			"--random=arg",
+		},
+	}
+
+	args, tempFiles, err := confToArgs(conf)
+	for _, file := range tempFiles {
+		defer os.Remove(file)
+	}
+
+	assert.Equal(t, []string{
+		fmt.Sprintf("--values=%s", tempFiles[0]),
+		"--version=v0.1",
+		"--random=arg",
+	}, args)
+	assert.Nil(t, err)
+}
+
+func TestParseYamlToMap_basic(t *testing.T) {
+	yamlFile := []byte(`---
+kind: testKind
+metadata:
+  name: testName`)
+	actual, err := parseYamlToMap(yamlFile)
+
+	expected := map[string]interface{}{
+		"testname_testkind": map[string]interface{}{
+			"kind": "testKind",
+			"metadata": map[string]interface{}{
+				"name": "testName",
+			},
+		},
+	}
+	assert.Equal(t, expected, actual)
+	assert.Nil(t, err)
+}
+
+func TestParseYamlToMap_dash(t *testing.T) {
+	yamlFile := []byte(`---
+kind: testKind
+metadata:
+  name: test-Name`)
+	actual, err := parseYamlToMap(yamlFile)
+
+	expected := map[string]interface{}{
+		"test_name_testkind": map[string]interface{}{
+			"kind": "testKind",
+			"metadata": map[string]interface{}{
+				"name": "test-Name",
+			},
+		},
+	}
+	assert.Equal(t, expected, actual)
+	assert.Nil(t, err)
+}
+
+func TestParseYamlToMap_colon(t *testing.T) {
+	yamlFile := []byte(`---
+kind: testKind
+metadata:
+  name: test:Name`)
+	actual, err := parseYamlToMap(yamlFile)
+
+	expected := map[string]interface{}{
+		"test_name_testkind": map[string]interface{}{
+			"kind": "testKind",
+			"metadata": map[string]interface{}{
+				"name": "test:Name",
+			},
+		},
+	}
+	assert.Equal(t, expected, actual)
+	assert.Nil(t, err)
+}
+
+func TestParseYamlToMap_empty(t *testing.T) {
+	yamlFile := []byte(`---`)
+	actual, err := parseYamlToMap(yamlFile)
+
+	expected := map[string]interface{}{}
+	assert.Equal(t, expected, actual)
+	assert.Nil(t, err)
+}
+
+func TestParseYamlToMap_multiple_files(t *testing.T) {
+	yamlFile := []byte(`---
+kind: testKind
+metadata:
+  name: testName
+---
+kind: testKind
+metadata:
+  name: testName2`)
+	actual, err := parseYamlToMap(yamlFile)
+
+	expected := map[string]interface{}{
+		"testname_testkind": map[string]interface{}{
+			"kind": "testKind",
+			"metadata": map[string]interface{}{
+				"name": "testName",
+			},
+		},
+		"testname2_testkind": map[string]interface{}{
+			"kind": "testKind",
+			"metadata": map[string]interface{}{
+				"name": "testName2",
+			},
+		},
+	}
+	assert.Equal(t, expected, actual)
+	assert.Nil(t, err)
+}
diff --git a/pkg/process/process_test.go b/pkg/process/process_test.go
index 5014b380..cf046159 100644
--- a/pkg/process/process_test.go
+++ b/pkg/process/process_test.go
@@ -64,6 +64,15 @@ func TestProcess(t *testing.T) {
 				`DePlOyMeNt/GrAfAnA`,
 			),
 		},
+		{
+			name: "targets-negative",
+			deep: testDataDeep().Deep,
+			flat: manifest.List{
+				testDataDeep().Flat[".app.web.frontend.nodejs.express.service"],
+				testDataDeep().Flat[".app.namespace"],
+			},
+			targets: MustStrExps(`!deployment/.*`),
+		},
 		{
 			name: "unwrap-list",
 			deep: loadFixture("list").Deep,