Repository: grafana/tanka
Pull request #173 (2020-01-21 15:03:50)

style(docs): filename typo in abstraction.mdx
grafana.libsonnet changed to grafana.jsonnet

malcolmholmes: Thanks for spotting. Usually, any imported file is named libsonnet, so perhaps we should name them `grafana.libsonnet` and `prometheus.libsonnet`, but you are totally right that the filenames should match!!
sh0rez: > Usually, any imported file is named libsonnet
While this pattern exists, I am not sure if it was designed this way and if, what's the point then.

If we always name every file that could be potentially imported at some point `libsonnet`, we could call them all like this apart from `main.jsonnet`.  But just calling them like this does not really add any value.

I was under the impression, it was a little bit differently:

- `.jsonnet`: Meant for everything that creates (and returns) components (k8s objects). In this case, both `prometheus.jsonnet` and `grafana.jsonnet` do this, they just contain code factored out of `main.jsonnet`, but are still working on their own.

- `.libsonnet` is for **helper** code ... being functions, etc. that are useless on their own and need to be imported and composed inside of `.jsonnet` files.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df2f1abb..123c8f60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,98 @@
 # Changelog
 
+## 0.7.0 (2020-01-21)
+
+The promised big update is here! In the last couple of weeks a lot has happened.
+
+Grafana Labs [announced Tanka to the
+public](https://grafana.com/blog/2020/01/09/introducing-tanka-our-way-of-deploying-to-kubernetes/),
+and the project got a lot of positive feedback, shown both on HackerNews and in
+a 500+ increase in GitHub stars!
+
+While we do not ship big new features this time, we ironed out many annoyances
+and made the overall experience a lot better:
+
+#### Better website + tutorial ([#134](https://github.com/grafana/tanka/pull/134))
+
+Our [new website](https://tanka.dev) is published! It does not only look super
+sleek and performs like a supercar, we also revisited (and rewrote) the most of
+the content, to provide especially new users a good experience.
+
+This especially includes the **[new
+tutorial](https://tanka.dev/tutorial/overview)**, which gives new and probably
+even more experienced users a good insight into how Tanka is meant to be used.
+
+#### :rotating_light::rotating_light: Disabling `import ".yaml"` ([#176](https://github.com/grafana/tanka/pull/176)) :rotating_light::rotating_light:
+
+Unfortunately, we **had to disable the feature** that allowed to directly import
+YAML files using the familiar `import` syntax, introduced in v0.6.0, because it
+caused serious issues with `importstr`, which became unusable.
+
+While our extensions to the Jsonnet language are cool, it is a no-brainer that
+compatibility with upstream Jsonnet is more important. We will work with the
+maintainers of Jsonnet to find a solution to enable both, `importstr` and
+`import ".yaml"`
+
+**Workaround:**
+
+```diff
+- import "foo.yaml"
++ std.parseYaml(importstr "foo.yaml")
+```
+
+#### `k.libsonnet` installation ([#140](https://github.com/grafana/tanka/pull/140))
+
+Previously, installing `k.libsonnet` was no fun. While the library is required
+for nearly every Tanka project, it was not possible to install it properly using
+`jb`, manual work was required.
+
+From now on, **Tanka automatically takes care of this**. A regular `tk init`
+installs everything you need. In case you prefer another solution, disable this
+new thing using `tk init --k8s=false`.
+
+### Features
+
+- **cli**, **kubernetes**: `k.libsonnet` is now automatically installed on `tk init` **([#140](https://github.com/grafana/tanka/pull/140))**:  
+  Before, installing `k.libsonnet` was a time consuming manual task. Tanka now
+  takes care of this, as long as `jb` is present on the `$PATH`. See
+  https://tanka.dev/tutorial/k-lib#klibsonnet for more details.
+- **cli**: `tk env --server-from-context`:  
+  This new flag allows to infer the cluster IP from an already set up `kubectl`
+  context. No need to remember IP's anymore – and they are even autocompleted on
+  the shell. **([#145](https://github.com/grafana/tanka/pull/145))**
+- **cli**, **jsonnet**: extCode, extVar:  
+  `-e` / `--extCode` and `--extVar` allow using `std.extVar()` in Tanka as well.
+  In general, `-e` is the flag to use, because it correctly handles all Jsonnet
+  types (string, int, bool). Strings need quoting!
+  **([#178](https://github.com/grafana/tanka/pull/178))**
+
+* **jsonnet**: The contents of `spec.json` are now accessible from Jsonnet using
+  `(import "tk").env`. **([#163](https://github.com/grafana/tanka/pull/163))**
+* **jsonnet**: Lists (`[ ]`) are now fully supported, at an arbitrary level of
+  nesting! **([#166](https://github.com/grafana/tanka/pull/166))**
+
+### Bug Fixes
+
+- **jsonnet**: `nil` values are ignored from the output. This allows to disable
+  objects using the `if ... then {}` pattern, which returns nil if `false`
+  **([#162](https://github.com/grafana/tanka/pull/162))**.
+- **cli**: `-t` / `--target` is now case-insensitive
+  **([#130](https://github.com/grafana/tanka/pull/130))**
+
+---
+
 ## 0.6.1 (2020-01-06)
+
 First release of the new year! This one is a quick patch that lived on master
 for some time, fixing an issue with the recent "missing namespaces" enhancement
 leading to `apply` being impossible when no namespace is included in Jsonnet.
 
 More to come soon :D
 
+---
+
 ## 0.6.0 (2019-11-27)
+
 It has been quite some time since the last release during which Tanka has become
 much more mature, especially regarding the code quality and structure.
 
@@ -16,6 +101,7 @@ Furthermore, Tanka has just hit the 100 Stars :tada:
 Notable changes include:
 
 #### API ([#97](https://github.com/grafana/tanka/commit/c5edb8b0153ef991765f2f555c839b0f9a487e75))
+
 The most notable change is probably the **Go API**, available at
 `https://godoc.org/github.com/grafana/tanka/pkg/tanka`, which allows to use all
 features of Tanka directly from any other Golang application, without needing to
@@ -23,35 +109,40 @@ exec the binary. The API is inspired by the command line parameters and should
 feel very similar.
 
 #### Importing YAML ([#106](https://github.com/grafana/tanka/commit/8029efa44461b5f7ba83a218ccc45bd758c8a322))
+
 It is now possible to import `.yaml` documents directly from Jsonnet. Just use
 the familiar syntax `import "foo.yaml"` like you would with JSON.
 
 #### Missing Namespaces ([#120](https://github.com/grafana/tanka/commit/3b9fac1563a75a571b512887602eb53f82e565bf))
+
 Tanka now handles namespaces that are not yet created, in a more user friendly
-way than `kubectl** does natively.  
+way than `kubectl\*\* does natively.  
 During diff, all objects of an in-existent namespace are shown as new and when
 applying, namespaces are applied first to allow applying in a single step.
 
 ### Features
-* **tool/imports**: import analysis using upstream jsonnet: Due to recent
+
+- **tool/imports**: import analysis using upstream jsonnet: Due to recent
   changes to google/jsonnet, we can now use the upstream compiler for static
   import analysis
   ([#84](https://github.com/grafana/tanka/commit/394cb12b28beb0ea05d065594b6cf5c3f92de5e4))
-* **Array output**: The output of Jsonnet may now be an array of Manifests.
+- **Array output**: The output of Jsonnet may now be an array of Manifests.
   Nested arrays are not supported yet.
   ([#112](https://github.com/grafana/tanka/commit/eb647793ff5515bc828e4f91186655c143bb6a04))
 
-
 ### Bug Fixes
-* **Command Usage Guidelines**: Tanka now uses the [command description
+
+- **Command Usage Guidelines**: Tanka now uses the [command description
   syntax](https://en.wikipedia.org/wiki/Command-line_interface#Command_description_syntax)
   ([#94](https://github.com/grafana/tanka/commit/13238e5941bd6e68f410d3938d1a285224c2f91d))
-* **cli/env** resolved panic on missing `spec.json`
+- **cli/env** resolved panic on missing `spec.json`
   ([#108](https://github.com/grafana/tanka/commit/9bd15e6b4226164efe45f50c9ed41c4a5673ea2d))
 
+---
+
 ## 0.5.0 (2019-09-20)
-This version adds a set of commands to manipulate environments (`tk env add, rm,
-set, list`) ([#73](https://github.com/grafana/tanka/pull/73)). The commands are
+
+This version adds a set of commands to manipulate environments (`tk env add, rm, set, list`) ([#73](https://github.com/grafana/tanka/pull/73)). The commands are
 mostly `ks env` compatible, allowing `tk env` be used as a drop-in replacement
 in scripts.
 
@@ -59,35 +150,42 @@ Furthermore, an error message has been improved, to make sure users can
 differentiate between parse issues in `.jsonnet` and `spec.json`
 ([#71](https://github.com/grafana/tanka/pull/71)).
 
+---
+
 ## 0.4.0 (2019-09-06)
+
 After nearly a month, the next feature packed release of Tanka is ready!
 Highlights include the new documentation website https://tanka.dev, regular
 expression support for targets, diff histograms and several bug-fixes.
 
 ### Features
-* **cli**: `tk show` now aborts by default, when invoked in a non-interactive
+
+- **cli**: `tk show` now aborts by default, when invoked in a non-interactive
   session. Use `--dangerous-allow-redirect` to disable this safe-guard
   ([#47](https://github.com/grafana/tanka/issues/47)).
-* **kubernetes**: Regexp Targets: It is now possible to use regular expressions
+- **kubernetes**: Regexp Targets: It is now possible to use regular expressions
   when specifying the targets using `--target` / `-t`. Use it to easily select
   multiple objects at once: https://tanka.dev/targets/#regular-expressions
   ([#64](https://github.com/grafana/tanka/issues/64)).
-* **kubernetes**: Diff histogram: Tanka now allows to summarize the differences
+- **kubernetes**: Diff histogram: Tanka now allows to summarize the differences
   between the live configuration and the local one, by using the unix
   `diffstat(1)` utility. Gain a sneek peek at a change using `tk diff -s .`!
   ([#67](https://github.com/grafana/tanka/issues/67))
 
 ### Bug Fixes
-* **kubernetes**: Tanka does not fail anymore, when the configuration file
+
+- **kubernetes**: Tanka does not fail anymore, when the configuration file
   `spec.json` is missing from an Environment. While you cannot apply or diff,
   the show operation works totally fine
   ([#56](https://github.com/grafana/tanka/issues/56),
   [#63](https://github.com/grafana/tanka/issues/63)).
-* **kubernetes**: Errors from `kubectl` are now correctly passed to the user
+- **kubernetes**: Errors from `kubectl` are now correctly passed to the user
   ([#61](https://github.com/grafana/tanka/issues/61)).
-* **cli**: `tk diff` does not output useless empty lines (`\n`) anymore
+- **cli**: `tk diff` does not output useless empty lines (`\n`) anymore
   ([#62](https://github.com/grafana/tanka/issues/62)).
 
+---
+
 ## 0.3.0 (2019-08-13)
 
 Tanka v0.3.0 is here!
@@ -101,32 +199,36 @@ Enjoy target support, enhancements to the diff UX and an improved CLI experience
 The most important feature is **target support** ([#30](https://github.com/tbraack/tanka/issues/30)) ([caf205a](https://github.com/tbraack/tanka/commit/caf205a)): Using `--target=kind/name`, you can limit your working set to a subset of the objects, e.g. to do a staged rollout.
 
 There where some other features added:
-* **cli:** autoApprove, forceApply ([#35](https://github.com/tbraack/tanka/issues/35)) ([626b097](https://github.com/tbraack/tanka/commit/626b097)): allows to skip the interactive verification. Furthermore, `kubectl` can now be invoked with `--force`.
-* **cli:** print deprecated warnings in verbose mode. ([#39](https://github.com/tbraack/tanka/issues/39)) ([6de170d](https://github.com/tbraack/tanka/commit/6de170d)): Warnings about the deprecated configs are only printed in verbose mode
-* **kubernetes:** add namespace to apply preamble ([#23](https://github.com/tbraack/tanka/issues/23)) ([9e2d927](https://github.com/tbraack/tanka/commit/9e2d927)): The interactive verification now shows the `metadata.namespace` as well.
-* **cli:** diff UX enhancements ([#34](https://github.com/tbraack/tanka/issues/34)) ([7602a19](https://github.com/tbraack/tanka/commit/7602a19)): The user experience of the `tk diff` subcommand has been improved:
-  * if the output is too long to fit on a single screen, the systems `PAGER` is invoked
-  * if differences are found, the exit status is set to `16`.
-  * When `tk apply` is invoked, the diff is shown again, to make sure you apply what you want
+
+- **cli:** autoApprove, forceApply ([#35](https://github.com/tbraack/tanka/issues/35)) ([626b097](https://github.com/tbraack/tanka/commit/626b097)): allows to skip the interactive verification. Furthermore, `kubectl` can now be invoked with `--force`.
+- **cli:** print deprecated warnings in verbose mode. ([#39](https://github.com/tbraack/tanka/issues/39)) ([6de170d](https://github.com/tbraack/tanka/commit/6de170d)): Warnings about the deprecated configs are only printed in verbose mode
+- **kubernetes:** add namespace to apply preamble ([#23](https://github.com/tbraack/tanka/issues/23)) ([9e2d927](https://github.com/tbraack/tanka/commit/9e2d927)): The interactive verification now shows the `metadata.namespace` as well.
+- **cli:** diff UX enhancements ([#34](https://github.com/tbraack/tanka/issues/34)) ([7602a19](https://github.com/tbraack/tanka/commit/7602a19)): The user experience of the `tk diff` subcommand has been improved:
+  - if the output is too long to fit on a single screen, the systems `PAGER` is invoked
+  - if differences are found, the exit status is set to `16`.
+  - When `tk apply` is invoked, the diff is shown again, to make sure you apply what you want
 
 ### Bug Fixes
 
-* **cli:** invalid command being executed twice ([#42](https://github.com/tbraack/tanka/issues/42)) ([28c6898](https://github.com/tbraack/tanka/commit/28c6898)): When the command failed, it was executed twice, due to an error in the error handling of the CLI.
-* **cli**: config miss ([#22](https://github.com/tbraack/tanka/issues/22)) ([32bc8a4](https://github.com/tbraack/tanka/commit/32bc8a4)): It was not possible to use the new configuration format, due to an error in the config parsing.
-* **cli:** remove datetime from log ([#24](https://github.com/tbraack/tanka/issues/24)) ([1e37b20](https://github.com/tbraack/tanka/commit/1e37b20))
-* **kubernetes:** correct diff type on 1.13 ([#31](https://github.com/tbraack/tanka/issues/31)) ([574f946](https://github.com/tbraack/tanka/commit/574f946)): On kubernetes 1.13.0, `subset` was used, although `native` is already supported.
-* **kubernetes:** Nil pointer deference in subset diff. ([#36](https://github.com/tbraack/tanka/issues/36)) ([f53c2b5](https://github.com/tbraack/tanka/commit/f53c2b5))
-* **kubernetes:** sort during reconcile ([#33](https://github.com/tbraack/tanka/issues/33)) ([ab9c43a](https://github.com/tbraack/tanka/commit/ab9c43a)): The output of the reconcilation phase is now stable in ordering
+- **cli:** invalid command being executed twice ([#42](https://github.com/tbraack/tanka/issues/42)) ([28c6898](https://github.com/tbraack/tanka/commit/28c6898)): When the command failed, it was executed twice, due to an error in the error handling of the CLI.
+- **cli**: config miss ([#22](https://github.com/tbraack/tanka/issues/22)) ([32bc8a4](https://github.com/tbraack/tanka/commit/32bc8a4)): It was not possible to use the new configuration format, due to an error in the config parsing.
+- **cli:** remove datetime from log ([#24](https://github.com/tbraack/tanka/issues/24)) ([1e37b20](https://github.com/tbraack/tanka/commit/1e37b20))
+- **kubernetes:** correct diff type on 1.13 ([#31](https://github.com/tbraack/tanka/issues/31)) ([574f946](https://github.com/tbraack/tanka/commit/574f946)): On kubernetes 1.13.0, `subset` was used, although `native` is already supported.
+- **kubernetes:** Nil pointer deference in subset diff. ([#36](https://github.com/tbraack/tanka/issues/36)) ([f53c2b5](https://github.com/tbraack/tanka/commit/f53c2b5))
+- **kubernetes:** sort during reconcile ([#33](https://github.com/tbraack/tanka/issues/33)) ([ab9c43a](https://github.com/tbraack/tanka/commit/ab9c43a)): The output of the reconcilation phase is now stable in ordering
 
+---
 
 ## [0.2.0](https://github.com/tbraack/tanka/compare/v0.1.0...v0.2.0) (2019-08-07)
 
 ### Features
 
-* **cli:** Completions ([#7](https://github.com/tbraack/tanka/issues/7)) ([aea3bdf](https://github.com/tbraack/tanka/commit/aea3bdf)): Tanka is now able auto-complete most of the command line arguments and flags. Supported shells are `bash`, `zsh` and `fish`.
-* **cmd:** allow the baseDir to be passed as an argument ([#6](https://github.com/tbraack/tanka/issues/6)) ([55adf80](https://github.com/tbraack/tanka/commit/55adf80)), ([#12](https://github.com/tbraack/tanka/issues/12)) ([3248bb9](https://github.com/tbraack/tanka/commit/3248bb9)): `tk` **breaks** with the current behaviour and requires the baseDir / environment to be passed explicitely on the command line, instead of assuming it as `pwd`. This is because it allows more `go`-like UX. It is also very handy for scripts not needing to switch the directory.
-* **kubernetes:** subset-diff ([#11](https://github.com/tbraack/tanka/issues/11)) ([13f6fdd](https://github.com/tbraack/tanka/commit/13f6fdd)): `tk diff` support for version below Kubernetes `1.13` is here :tada:! The strategy is called *subset diff* and effectively compares only the fields already present in the config. This allows the (hopefully) most bloat-free experience possible without server side diff.
-* **tooling:** import analysis ([#10](https://github.com/tbraack/tanka/issues/10)) ([ce2b0d3](https://github.com/tbraack/tanka/commit/ce2b0d3)): Adds `tk tool imports`, which allows to list all imports of a single file (even transitive ones). Optionally pass a git commit hash, to check whether any of the changed files is imported, to figure out which environments need to be re-applied.
+- **cli:** Completions ([#7](https://github.com/tbraack/tanka/issues/7)) ([aea3bdf](https://github.com/tbraack/tanka/commit/aea3bdf)): Tanka is now able auto-complete most of the command line arguments and flags. Supported shells are `bash`, `zsh` and `fish`.
+- **cmd:** allow the baseDir to be passed as an argument ([#6](https://github.com/tbraack/tanka/issues/6)) ([55adf80](https://github.com/tbraack/tanka/commit/55adf80)), ([#12](https://github.com/tbraack/tanka/issues/12)) ([3248bb9](https://github.com/tbraack/tanka/commit/3248bb9)): `tk` **breaks** with the current behaviour and requires the baseDir / environment to be passed explicitely on the command line, instead of assuming it as `pwd`. This is because it allows more `go`-like UX. It is also very handy for scripts not needing to switch the directory.
+- **kubernetes:** subset-diff ([#11](https://github.com/tbraack/tanka/issues/11)) ([13f6fdd](https://github.com/tbraack/tanka/commit/13f6fdd)): `tk diff` support for version below Kubernetes `1.13` is here :tada:! The strategy is called _subset diff_ and effectively compares only the fields already present in the config. This allows the (hopefully) most bloat-free experience possible without server side diff.
+- **tooling:** import analysis ([#10](https://github.com/tbraack/tanka/issues/10)) ([ce2b0d3](https://github.com/tbraack/tanka/commit/ce2b0d3)): Adds `tk tool imports`, which allows to list all imports of a single file (even transitive ones). Optionally pass a git commit hash, to check whether any of the changed files is imported, to figure out which environments need to be re-applied.
+
+---
 
 ## 0.1.0 (2019-07-31)
 
@@ -139,9 +241,9 @@ Stay tuned!
 
 ### Features
 
-* **kubernetes:** Show ([7c4bee8](https://github.com/tbraack/tanka/commit/7c4bee8)): Equivalent to `ks show`, allows previewing the generated yaml.
-* **kubernetes:** Diff ([a959f38](https://github.com/tbraack/tanka/commit/a959f38)): Uses the `kubectl diff` to obtain a sanitized difference betweent the current and the desired state. Requires Kubernetes 1.13+
-* **kubernetes:** Apply ([8fcb4c1](https://github.com/tbraack/tanka/commit/8fcb4c1)): Applies the changes to the cluster (like `ks apply`)
-* **kubernetes:** Apply approval ([4c6414f](https://github.com/tbraack/tanka/commit/4c6414f)): Requires a typed `yes` to apply, gives the user the chance to verify cluster and context.
-* **kubernetes:** Smart context ([2b3fd3c](https://github.com/tbraack/tanka/commit/2b3fd3c)): Infers the correct context from the `spec.json`. Prevents applying the correct config to the wrong cluster.
-* Init Command ([ff8857c](https://github.com/tbraack/tanka/commit/ff8857c)): Initializes a new repository with the suggested directory structure.
+- **kubernetes:** Show ([7c4bee8](https://github.com/tbraack/tanka/commit/7c4bee8)): Equivalent to `ks show`, allows previewing the generated yaml.
+- **kubernetes:** Diff ([a959f38](https://github.com/tbraack/tanka/commit/a959f38)): Uses the `kubectl diff` to obtain a sanitized difference betweent the current and the desired state. Requires Kubernetes 1.13+
+- **kubernetes:** Apply ([8fcb4c1](https://github.com/tbraack/tanka/commit/8fcb4c1)): Applies the changes to the cluster (like `ks apply`)
+- **kubernetes:** Apply approval ([4c6414f](https://github.com/tbraack/tanka/commit/4c6414f)): Requires a typed `yes` to apply, gives the user the chance to verify cluster and context.
+- **kubernetes:** Smart context ([2b3fd3c](https://github.com/tbraack/tanka/commit/2b3fd3c)): Infers the correct context from the `spec.json`. Prevents applying the correct config to the wrong cluster.
+- Init Command ([ff8857c](https://github.com/tbraack/tanka/commit/ff8857c)): Initializes a new repository with the suggested directory structure.
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/jsonnet.go b/cmd/tk/jsonnet.go
index e78f1e7d..8f54c162 100644
--- a/cmd/tk/jsonnet.go
+++ b/cmd/tk/jsonnet.go
@@ -2,9 +2,12 @@ package main
 
 import (
 	"encoding/json"
+	"fmt"
 	"log"
+	"strings"
 
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 
 	"github.com/grafana/tanka/pkg/tanka"
 )
@@ -19,8 +22,13 @@ func evalCmd() *cobra.Command {
 		},
 	}
 
+	getExtCode := extCodeParser(cmd.Flags())
+
 	cmd.Run = func(cmd *cobra.Command, args []string) {
-		raw, _, err := tanka.Eval(args[0], nil)
+		raw, err := tanka.Eval(args[0],
+			tanka.WithExtCode(getExtCode()),
+		)
+
 		if err != nil {
 			log.Fatalln(err, nil)
 		}
@@ -35,3 +43,30 @@ func evalCmd() *cobra.Command {
 
 	return cmd
 }
+
+func extCodeParser(fs *pflag.FlagSet) 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)")
+
+	return func() map[string]string {
+		m := make(map[string]string)
+		for _, s := range *values {
+			split := strings.SplitN(s, "=", 2)
+			if len(split) != 2 {
+				log.Fatalln("extCode argument has wrong format:", s+".", "Expected 'key=<code>'")
+			}
+			m[split[0]] = split[1]
+		}
+
+		for _, s := range *strs {
+			split := strings.SplitN(s, "=", 2)
+			if len(split) != 2 {
+				log.Fatalln("extVar argument has wrong format:", s+".", "Expected 'key=value'")
+			}
+			m[split[0]] = fmt.Sprintf(`"%s"`, split[1])
+		}
+		return m
+	}
+}
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/cmd/tk/workflow.go b/cmd/tk/workflow.go
index 1fab26d8..e3927766 100644
--- a/cmd/tk/workflow.go
+++ b/cmd/tk/workflow.go
@@ -44,11 +44,16 @@ func applyCmd() *cobra.Command {
 	}
 	vars := workflowFlags(cmd.Flags())
 	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())
+
 	cmd.Run = func(cmd *cobra.Command, args []string) {
 		err := tanka.Apply(args[0],
 			tanka.WithTargets(stringsToRegexps(vars.targets)...),
+			tanka.WithExtCode(getExtCode()),
 			tanka.WithApplyForce(*force),
+			tanka.WithApplyValidate(*validate),
 			tanka.WithApplyAutoApprove(*autoApprove),
 		)
 		if err != nil {
@@ -79,9 +84,12 @@ func diffCmd() *cobra.Command {
 		summarize    = cmd.Flags().BoolP("summarize", "s", false, "quick summary of the differences, hides file contents")
 	)
 
+	getExtCode := extCodeParser(cmd.Flags())
+
 	cmd.Run = func(cmd *cobra.Command, args []string) {
 		changes, err := tanka.Diff(args[0],
 			tanka.WithTargets(stringsToRegexps(vars.targets)...),
+			tanka.WithExtCode(getExtCode()),
 			tanka.WithDiffStrategy(*diffStrategy),
 			tanka.WithDiffSummarize(*summarize),
 		)
@@ -118,6 +126,7 @@ func showCmd() *cobra.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())
 	cmd.Run = func(cmd *cobra.Command, args []string) {
 		if !interactive && !*allowRedirect {
 			fmt.Fprintln(os.Stderr, "Redirection of the output of tk show is discouraged and disabled by default. Run tk show --dangerous-allow-redirect to enable.")
@@ -125,6 +134,7 @@ func showCmd() *cobra.Command {
 		}
 
 		pretty, err := tanka.Show(args[0],
+			tanka.WithExtCode(getExtCode()),
 			tanka.WithTargets(stringsToRegexps(vars.targets)...),
 		)
 		if err != nil {
diff --git a/docs/docs/tutorial/abstraction.mdx b/docs/docs/tutorial/abstraction.mdx
index ac232680..426099f1 100644
--- a/docs/docs/tutorial/abstraction.mdx
+++ b/docs/docs/tutorial/abstraction.mdx
@@ -110,10 +110,10 @@ on the root object before. Do the same for `/environments/default/prometheus.jso
 
 > **Clarification**:  
 > It might seem odd at first sight, that this code works, because
-> `grafana.libsonnet` still refers to the root object using `$`, even
+> `grafana.jsonnet` still refers to the root object using `$`, even
 > though it is outside of the file's scope.  
 > However, Jsonnet is lazy-evaluated which means that the contents of
-> `grafana.libsonnet` are **first "copied"** into `main.jsonnet` (the root
+> `grafana.jsonnet` are **first "copied"** into `main.jsonnet` (the root
 > object) and **then evaluated**. This means the above code actually consists of
 > all three objects joined to one big object, which is then converted to JSON.
 
diff --git a/docs/docs/tutorial/environments.mdx b/docs/docs/tutorial/environments.mdx
index 582bfa9c..bbeeeeae 100644
--- a/docs/docs/tutorial/environments.mdx
+++ b/docs/docs/tutorial/environments.mdx
@@ -6,7 +6,7 @@ menu: Tutorial
 
 # Environments
 
-At this point, our is configuration is already flexible and concise, but not
+At this point, our configuration is already flexible and concise, but not
 really reusable. Let's take a look at Tanka's third buzzword as well: **Environments**.
 
 These days, the same piece of software is usually deployed many times inside a
diff --git a/pkg/jsonnet/importer.go b/pkg/jsonnet/importer.go
index ee42c68e..22d90b90 100644
--- a/pkg/jsonnet/importer.go
+++ b/pkg/jsonnet/importer.go
@@ -39,7 +39,10 @@ func NewExtendedImporter(jpath []string) *ExtendedImporter {
 				JPaths: jpath,
 			})},
 		processors: []importProcessor{
-			yamlProcessor,
+			// TODO: re-enable this once we can without side-effects
+			// (https://github.com/grafana/tanka/issues/135)
+			//
+			// yamlProcessor,
 		},
 	}
 }
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..dd6d3007 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 {
@@ -48,10 +53,12 @@ func Resolve(workdir string) (path []string, base, root string, err error) {
 		return nil, "", "", err
 	}
 
+	// The importer iterates through this list in reverse order
 	return []string{
-		base,
 		filepath.Join(root, "vendor"),
+		filepath.Join(base, "vendor"), // Look for a vendor folder in the base dir before using the root vendor
 		filepath.Join(root, "lib"),
+		base,
 	}, base, root, nil
 }
 
diff --git a/pkg/jsonnet/testdata/main.jsonnet b/pkg/jsonnet/testdata/importTree/main.jsonnet
similarity index 100%
rename from pkg/jsonnet/testdata/main.jsonnet
rename to pkg/jsonnet/testdata/importTree/main.jsonnet
diff --git a/pkg/jsonnet/testdata/test.svg b/pkg/jsonnet/testdata/importTree/test.svg
similarity index 100%
rename from pkg/jsonnet/testdata/test.svg
rename to pkg/jsonnet/testdata/importTree/test.svg
diff --git a/pkg/jsonnet/testdata/trees.jsonnet b/pkg/jsonnet/testdata/importTree/trees.jsonnet
similarity index 100%
rename from pkg/jsonnet/testdata/trees.jsonnet
rename to pkg/jsonnet/testdata/importTree/trees.jsonnet
diff --git a/pkg/jsonnet/testdata/trees/apple.jsonnet b/pkg/jsonnet/testdata/importTree/trees/apple.jsonnet
similarity index 100%
rename from pkg/jsonnet/testdata/trees/apple.jsonnet
rename to pkg/jsonnet/testdata/importTree/trees/apple.jsonnet
diff --git a/pkg/jsonnet/testdata/trees/cherry.jsonnet b/pkg/jsonnet/testdata/importTree/trees/cherry.jsonnet
similarity index 100%
rename from pkg/jsonnet/testdata/trees/cherry.jsonnet
rename to pkg/jsonnet/testdata/importTree/trees/cherry.jsonnet
diff --git a/pkg/jsonnet/testdata/trees/generic.libsonnet b/pkg/jsonnet/testdata/importTree/trees/generic.libsonnet
similarity index 100%
rename from pkg/jsonnet/testdata/trees/generic.libsonnet
rename to pkg/jsonnet/testdata/importTree/trees/generic.libsonnet
diff --git a/pkg/jsonnet/testdata/trees/peach.jsonnet b/pkg/jsonnet/testdata/importTree/trees/peach.jsonnet
similarity index 100%
rename from pkg/jsonnet/testdata/trees/peach.jsonnet
rename to pkg/jsonnet/testdata/importTree/trees/peach.jsonnet
diff --git a/pkg/kubernetes/client/apply.go b/pkg/kubernetes/client/apply.go
index 6ac6dcbf..c40e3ab6 100644
--- a/pkg/kubernetes/client/apply.go
+++ b/pkg/kubernetes/client/apply.go
@@ -31,6 +31,10 @@ func (k Kubectl) apply(data manifest.List, opts ApplyOpts) error {
 		argv = append(argv, "--force")
 	}
 
+	if !opts.Validate {
+		argv = append(argv, "--validate=false")
+	}
+
 	cmd := exec.Command("kubectl", argv...)
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
diff --git a/pkg/kubernetes/client/client.go b/pkg/kubernetes/client/client.go
index a67e0c21..14710c85 100644
--- a/pkg/kubernetes/client/client.go
+++ b/pkg/kubernetes/client/client.go
@@ -51,6 +51,9 @@ type ApplyOpts struct {
 	// force allows to ignore checks and force the operation
 	Force bool
 
+	// validate allows to enable/disable kubectl validation
+	Validate bool
+
 	// autoApprove allows to skip the interactive approval
 	AutoApprove bool
 }
diff --git a/pkg/kubernetes/reconcile.go b/pkg/kubernetes/reconcile.go
index 71e7ac01..9de42cdc 100644
--- a/pkg/kubernetes/reconcile.go
+++ b/pkg/kubernetes/reconcile.go
@@ -2,7 +2,6 @@ package kubernetes
 
 import (
 	"fmt"
-	"reflect"
 	"regexp"
 	"sort"
 	"strings"
@@ -87,17 +86,11 @@ func walkJSON(ptr interface{}, extracted map[string]manifest.Manifest, path trac
 		return walkList(v, extracted, path)
 	}
 
-	// Lists other than []interface{} need to be handled separately
-	s, ok := tryCoerceSlice(ptr)
-	if !ok {
-		// object and list tried, so not a collection.
-		return ErrorPrimitiveReached{
-			path:      path.Base(),
-			key:       path.Name(),
-			primitive: ptr,
-		}
+	return ErrorPrimitiveReached{
+		path:      path.Base(),
+		key:       path.Name(),
+		primitive: ptr,
 	}
-	return walkJSON(s, extracted, path)
 }
 
 func walkList(list []interface{}, extracted map[string]manifest.Manifest, path trace) error {
@@ -139,25 +132,6 @@ func walkObj(obj objx.Map, extracted map[string]manifest.Manifest, path trace) e
 	return nil
 }
 
-// tryCoerceSlice attempts to construct a []interface{} from any other kind of
-// array/slice.
-// If the argument is not a list at all, ok will be false
-func tryCoerceSlice(input interface{}) ([]interface{}, bool) {
-	v := reflect.ValueOf(input)
-	if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
-		return nil, false
-	}
-
-	l := v.Len()
-	s := make([]interface{}, l)
-
-	for i := 0; i < l; i++ {
-		s[i] = v.Index(i).Interface()
-	}
-
-	return s, true
-}
-
 type trace []string
 
 func (t trace) Full() string {
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 07dde6af..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,18 +51,14 @@ func parse(baseDir string, opts *options) (*ParseResult, error) {
 	}, 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{}
-	}
-
-	env, err = parseEnv(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, nil, err
+		return nil, nil, errors.Wrap(err, "loading environment")
 	}
 
-	raw, err = evalJsonnet(baseDir, env)
+	raw, err = evalJsonnet(baseDir, env, extCode)
 	if err != nil {
 		return nil, nil, errors.Wrap(err, "evaluating jsonnet")
 	}
@@ -70,10 +66,26 @@ func Eval(baseDir string, opts *options) (raw map[string]interface{}, env *v1alp
 	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, errors.Wrap(err, "resolving jpath")
+	}
+
+	env, err = parseEnv(baseDir, rootDir)
+	if err != nil {
+		return "", nil, err
+	}
+	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
@@ -81,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")
@@ -96,28 +105,23 @@ 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) (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")
 	}
 
+	ext := []jsonnet.Modifier{
+		jsonnet.WithExtCode(spec.APIGroup+"/environment", string(jsonEnv)),
+	}
+	for k, v := range extCode {
+		ext = append(ext, jsonnet.WithExtCode(k, v))
+	}
+
 	raw, err := jsonnet.EvaluateFile(
 		filepath.Join(baseDir, "main.jsonnet"),
-		jsonnet.WithExtCode(spec.APIGroup+"/environment", string(jsonEnv)),
+		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 59a8a254..95c24b14 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,8 +20,8 @@ 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
 
 	// target regular expressions to limit the working set
 	targets []*regexp.Regexp
@@ -38,11 +37,10 @@ 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 {
+// WithExtCode allows to pass external variables (jsonnet code) to the VM
+func WithExtCode(code map[string]string) Modifier {
 	return func(opts *options) {
-		opts.wWarn = w
+		opts.extCode = code
 	}
 }
 
@@ -79,6 +77,13 @@ func WithApplyForce(b bool) Modifier {
 	}
 }
 
+// WithApplyValidate allows to invoke `kubectl apply` with the `--validate=false` flag
+func WithApplyValidate(b bool) Modifier {
+	return func(opts *options) {
+		opts.apply.Validate = b
+	}
+}
+
 // WithApplyAutoApprove allows to skip the interactive approval
 func WithApplyAutoApprove(b bool) 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..a7c2f05e 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/importTree")
+	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/jsonnet/jpath/jpath_test.go b/pkg/jsonnet/jpath/jpath_test.go
new file mode 100644
index 00000000..ebb4742b
--- /dev/null
+++ b/pkg/jsonnet/jpath/jpath_test.go
@@ -0,0 +1,29 @@
+package jpath_test
+
+import (
+	"encoding/json"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/grafana/tanka/pkg/jsonnet"
+)
+
+func TestResolve(t *testing.T) {
+	s, err := jsonnet.EvaluateFile(filepath.Join("./testdata/precedence/environments/default/main.jsonnet"))
+	require.NoError(t, err)
+
+	want := map[string]string{
+		"baseDir":        "baseDir",
+		"lib":            "/lib",
+		"baseDir-vendor": "baseDir-vendor",
+		"vendor":         "/vendor",
+	}
+
+	w, err := json.Marshal(want)
+	require.NoError(t, err)
+
+	assert.JSONEq(t, string(w), s)
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/README.md b/pkg/jsonnet/jpath/testdata/precedence/README.md
new file mode 100644
index 00000000..588003b1
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/README.md
@@ -0,0 +1,32 @@
+## `precedence` test
+
+This directory contains some Jsonnet testdata for testing correct import precedence.
+
+The desired precedence looks like the following (most specific wins):
+
+1. consider baseDir (dir containing `main.jsonnet`) first: The current
+   environment is most important
+2. then `/lib`: project libraries are more specific than any vendors
+3. then `<baseDir>/vendor`: to allow overriding project vendor on a environment level
+4. finally `/vendor`: external packages are least specific
+
+## internals
+
+How the test works:
+
+We basically put the same jsonnet file in multiple locations:
+
+```jsonnet
+{
+    value: "<location>"
+}
+```
+
+For example to check for `/lib` to precede both vendor folders, the following is used:
+
+- `/vendor/project_lib.jsonnet`: `value: "/vendor"`
+- `/<baseDir>/vendor/project_lib.jsonnet`: `value: "/baseDir-vendor"`
+- `/lib/project_lib.jsonnet`: `value: "/lib"`
+
+Then in `<baseDir>/main.jsonnet` we put `import "project_lib.jsonnet"` and
+expect `value` to be `/lib`.
diff --git a/pkg/jsonnet/jpath/testdata/precedence/environments/default/baseDir.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/environments/default/baseDir.jsonnet
new file mode 100644
index 00000000..012884cb
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/environments/default/baseDir.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "baseDir",
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/environments/default/main.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/environments/default/main.jsonnet
new file mode 100644
index 00000000..27844368
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/environments/default/main.jsonnet
@@ -0,0 +1,6 @@
+{
+  baseDir: (import "baseDir.jsonnet").value,
+  lib: (import "lib.jsonnet").value,
+  "baseDir-vendor": (import "baseDir-vendor.jsonnet").value,
+  vendor: (import "vendor.jsonnet").value,
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/environments/default/spec.json b/pkg/jsonnet/jpath/testdata/precedence/environments/default/spec.json
new file mode 100644
index 00000000..2bed8424
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/environments/default/spec.json
@@ -0,0 +1,11 @@
+{
+  "apiVersion": "tanka.dev/v1alpha1",
+  "kind": "Environment",
+  "metadata": {
+    "name": "env-vendor"
+  },
+  "spec": {
+    "apiServer": "",
+    "namespace": "default"
+  }
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/environments/default/vendor/baseDir-vendor.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/environments/default/vendor/baseDir-vendor.jsonnet
new file mode 100644
index 00000000..3c1d5af9
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/environments/default/vendor/baseDir-vendor.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "baseDir-vendor"
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/environments/default/vendor/baseDir.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/environments/default/vendor/baseDir.jsonnet
new file mode 100644
index 00000000..a5b70573
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/environments/default/vendor/baseDir.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "/baseDir-vendor"
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/environments/default/vendor/lib.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/environments/default/vendor/lib.jsonnet
new file mode 100644
index 00000000..a5b70573
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/environments/default/vendor/lib.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "/baseDir-vendor"
+}
diff --git a/pkg/jsonnet/testdata/jsonnetfile.json b/pkg/jsonnet/jpath/testdata/precedence/jsonnetfile.json
similarity index 100%
rename from pkg/jsonnet/testdata/jsonnetfile.json
rename to pkg/jsonnet/jpath/testdata/precedence/jsonnetfile.json
diff --git a/pkg/jsonnet/jpath/testdata/precedence/lib/baseDir.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/lib/baseDir.jsonnet
new file mode 100644
index 00000000..a2803b65
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/lib/baseDir.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "/lib"
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/lib/lib.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/lib/lib.jsonnet
new file mode 100644
index 00000000..a2803b65
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/lib/lib.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "/lib"
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/vendor/baseDir-vendor.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/vendor/baseDir-vendor.jsonnet
new file mode 100644
index 00000000..59da02d6
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/vendor/baseDir-vendor.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "/vendor"
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/vendor/baseDir.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/vendor/baseDir.jsonnet
new file mode 100644
index 00000000..59da02d6
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/vendor/baseDir.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "/vendor"
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/vendor/lib.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/vendor/lib.jsonnet
new file mode 100644
index 00000000..59da02d6
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/vendor/lib.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "/vendor"
+}
diff --git a/pkg/jsonnet/jpath/testdata/precedence/vendor/vendor.jsonnet b/pkg/jsonnet/jpath/testdata/precedence/vendor/vendor.jsonnet
new file mode 100644
index 00000000..59da02d6
--- /dev/null
+++ b/pkg/jsonnet/jpath/testdata/precedence/vendor/vendor.jsonnet
@@ -0,0 +1,3 @@
+{
+  value: "/vendor"
+}
diff --git a/pkg/jsonnet/testdata/README.md b/pkg/jsonnet/testdata/importTree/README.md
similarity index 100%
rename from pkg/jsonnet/testdata/README.md
rename to pkg/jsonnet/testdata/importTree/README.md
diff --git a/pkg/jsonnet/testdata/importTree/jsonnetfile.json b/pkg/jsonnet/testdata/importTree/jsonnetfile.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/pkg/jsonnet/testdata/importTree/jsonnetfile.json
@@ -0,0 +1,1 @@
+{}
diff --git a/pkg/kubernetes/data_test.go b/pkg/kubernetes/data_test.go
index 0ab62ea8..9cd65f3d 100644
--- a/pkg/kubernetes/data_test.go
+++ b/pkg/kubernetes/data_test.go
@@ -1,257 +1,64 @@
 package kubernetes
 
-import "github.com/grafana/tanka/pkg/kubernetes/manifest"
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"strings"
 
-// This file  contains data for testing
+	"github.com/grafana/tanka/pkg/jsonnet"
+	"github.com/grafana/tanka/pkg/kubernetes/manifest"
+)
 
 // testData holds data for tests
 type testData struct {
-	deep interface{}
-	flat map[string]manifest.Manifest
+	Deep interface{}                  `json:"deep"`
+	Flat map[string]manifest.Manifest `json:"flat"`
+}
+
+func loadFixture(name string) testData {
+	raw, err := ioutil.ReadFile("./testdata/td" + strings.Title(name) + ".jsonnet")
+	if err != nil {
+		panic(fmt.Sprint("loading fixture:", err))
+	}
+
+	data, err := jsonnet.Evaluate(string(raw), []string{"./testdata"})
+	if err != nil {
+		panic(fmt.Sprint("loading fixture:", err))
+	}
+
+	var d testData
+	if err := json.Unmarshal([]byte(data), &d); err != nil {
+		panic(fmt.Sprint("loading fixture:", err))
+	}
+
+	return d
 }
 
 // testDataRegular is a regular output of jsonnet without special things, but it
 // is nested.
 func testDataRegular() testData {
-	return (testData{
-		deep: map[string]interface{}{
-			"deployment": map[string]interface{}{
-				"apiVersion": "apps/v1",
-				"kind":       "Deployment",
-				"metadata": map[string]interface{}{
-					"name": "nginx",
-				},
-				"spec": map[string]interface{}{
-					"containers": []interface{}{
-						map[string]interface{}{
-							"name":  "nginx",
-							"image": "nginx",
-						},
-					},
-				},
-			},
-			"service": map[string]interface{}{
-				"apiVersion": "v1",
-				"kind":       "Service",
-				"metadata": map[string]interface{}{
-					"name": "nginx",
-				},
-				"spec": map[string]interface{}{
-					"selector": map[string]interface{}{
-						"app": "app",
-					},
-				},
-			},
-		},
-		flat: map[string]manifest.Manifest{
-			".deployment": {
-				"apiVersion": "apps/v1",
-				"kind":       "Deployment",
-				"metadata": map[string]interface{}{
-					"name": "nginx",
-				},
-				"spec": map[string]interface{}{
-					"containers": []interface{}{
-						map[string]interface{}{
-							"name":  "nginx",
-							"image": "nginx",
-						},
-					},
-				},
-			},
-			".service": {
-				"apiVersion": "v1",
-				"kind":       "Service",
-				"metadata": map[string]interface{}{
-					"name": "nginx",
-				},
-				"spec": map[string]interface{}{
-					"selector": map[string]interface{}{
-						"app": "app",
-					},
-				},
-			},
-		},
-	})
+	return loadFixture("regular")
 }
 
 // testDataFlat is a flat manifest that does not need reconciliation
 func testDataFlat() testData {
-	return testData{
-		deep: map[string]interface{}{
-			"apiVersion": "apps/v1",
-			"kind":       "Deployment",
-			"metadata": map[string]interface{}{
-				"name": "nginx",
-			},
-			"spec": map[string]interface{}{
-				"containers": []interface{}{
-					map[string]interface{}{
-						"name":  "nginx",
-						"image": "nginx",
-					},
-				},
-			},
-		},
-		flat: map[string]manifest.Manifest{
-			".": {
-				"apiVersion": "apps/v1",
-				"kind":       "Deployment",
-				"metadata": map[string]interface{}{
-					"name": "nginx",
-				},
-				"spec": map[string]interface{}{
-					"containers": []interface{}{
-						map[string]interface{}{
-							"name":  "nginx",
-							"image": "nginx",
-						},
-					},
-				},
-			},
-		},
-	}
+	return loadFixture("flat")
 }
 
 // testDataPrimitive is an invalid manifest, because it ends with a primitive
 // without including required fields
 func testDataPrimitive() testData {
-	return testData{
-		deep: map[string]interface{}{
-			"nginx": map[string]interface{}{
-				"deployment": map[string]interface{}{
-					"apiVersion": "apps/v1",
-					"kind":       "Deployment",
-					"metadata": map[string]interface{}{
-						"name": "nginx",
-					},
-				},
-				"service": map[string]interface{}{
-					"note": "invalid because apiVersion and kind are missing",
-				},
-			},
-		},
-	}
+	return loadFixture("invalidPrimitive")
 }
 
 // testDataDeep is super deeply nested on multiple levels
 func testDataDeep() testData {
-	return testData{
-		deep: map[string]interface{}{
-			"app": map[string]interface{}{
-				"web": map[string]interface{}{
-					"backend": map[string]interface{}{
-						"server": map[string]interface{}{
-							"nginx": map[string]interface{}{
-								"deployment": map[string]interface{}{
-									"kind":       "Deployment",
-									"apiVersion": "apps/v1",
-									"metadata": map[string]interface{}{
-										"name": "nginx",
-									},
-								},
-							},
-						},
-					},
-					"frontend": map[string]interface{}{
-						"nodejs": map[string]interface{}{
-							"express": map[string]interface{}{
-								"service": map[string]interface{}{
-									"kind":       "Service",
-									"apiVersion": "v1",
-									"metadata": map[string]interface{}{
-										"name": "frontend",
-									},
-								},
-								"deployment": map[string]interface{}{
-									"kind":       "Deployment",
-									"apiVersion": "apps/v1",
-									"metadata": map[string]interface{}{
-										"name": "frontend",
-									},
-								},
-							},
-						},
-					},
-				},
-				"namespace": map[string]interface{}{
-					"kind":       "Namespace",
-					"apiVersion": "v1",
-					"metadata": map[string]interface{}{
-						"name": "app",
-					},
-				},
-			},
-		},
-		flat: map[string]manifest.Manifest{
-			".app.web.backend.server.nginx.deployment": {
-				"kind":       "Deployment",
-				"apiVersion": "apps/v1",
-				"metadata": map[string]interface{}{
-					"name": "nginx",
-				},
-			},
-			".app.web.frontend.nodejs.express.service": {
-				"kind":       "Service",
-				"apiVersion": "v1",
-				"metadata": map[string]interface{}{
-					"name": "frontend",
-				},
-			},
-			".app.web.frontend.nodejs.express.deployment": {
-				"kind":       "Deployment",
-				"apiVersion": "apps/v1",
-				"metadata": map[string]interface{}{
-					"name": "frontend",
-				},
-			},
-			".app.namespace": {
-				"kind":       "Namespace",
-				"apiVersion": "v1",
-				"metadata": map[string]interface{}{
-					"name": "app",
-				},
-			},
-		},
-	}
+	return loadFixture("deep")
 }
 
 // testDataArray is an array of (deeply nested) dicts that should be fully
 // flattened
 func testDataArray() testData {
-	return testData{
-		deep: []map[string]interface{}{
-			testDataDeep().deep.(map[string]interface{}),
-			testDataFlat().deep.(map[string]interface{}),
-		},
-
-		flat: map[string]manifest.Manifest{
-			".[0].app.web.backend.server.nginx.deployment":    testDataDeep().flat[".app.web.backend.server.nginx.deployment"],
-			".[0].app.web.frontend.nodejs.express.service":    testDataDeep().flat[".app.web.frontend.nodejs.express.service"],
-			".[0].app.web.frontend.nodejs.express.deployment": testDataDeep().flat[".app.web.frontend.nodejs.express.deployment"],
-			".[0].app.namespace":                              testDataDeep().flat[".app.namespace"],
-			".[1]":                                            testDataFlat().flat["."],
-		},
-	}
-}
-
-// testDataDeepArray is an array of (deeply nested) dicts that should be fully
-// flattened
-func testDataDeepArray() testData {
-	return testData{
-		deep: [][]map[string]interface{}{
-			{
-				testDataDeep().deep.(map[string]interface{}),
-				testDataFlat().deep.(map[string]interface{}),
-			},
-		},
-
-		flat: map[string]manifest.Manifest{
-			".[0].[0].app.web.backend.server.nginx.deployment":    testDataDeep().flat[".app.web.backend.server.nginx.deployment"],
-			".[0].[0].app.web.frontend.nodejs.express.service":    testDataDeep().flat[".app.web.frontend.nodejs.express.service"],
-			".[0].[0].app.web.frontend.nodejs.express.deployment": testDataDeep().flat[".app.web.frontend.nodejs.express.deployment"],
-			".[0].[0].app.namespace":                              testDataDeep().flat[".app.namespace"],
-			".[0].[1]":                                            testDataFlat().flat["."],
-		},
-	}
+	return loadFixture("array")
 }
diff --git a/pkg/kubernetes/kubernetes_test.go b/pkg/kubernetes/kubernetes_test.go
index 09ae31b2..89e6519c 100644
--- a/pkg/kubernetes/kubernetes_test.go
+++ b/pkg/kubernetes/kubernetes_test.go
@@ -26,46 +26,46 @@ func TestReconcile(t *testing.T) {
 	}{
 		{
 			name: "regular",
-			deep: testDataRegular().deep,
-			flat: mapToList(testDataRegular().flat),
+			deep: testDataRegular().Deep,
+			flat: mapToList(testDataRegular().Flat),
 		},
 		{
 			name: "targets",
-			deep: testDataDeep().deep,
+			deep: testDataDeep().Deep,
 			flat: manifest.List{
-				testDataDeep().flat[".app.web.backend.server.nginx.deployment"],
-				testDataDeep().flat[".app.web.frontend.nodejs.express.service"],
+				testDataDeep().Flat[".app.web.backend.server.grafana.deployment"],
+				testDataDeep().Flat[".app.web.frontend.nodejs.express.service"],
 			},
 			targets: util.MustCompileTargetExps(
-				`deployment/nginx`,
+				`deployment/grafana`,
 				`service/frontend`,
 			),
 		},
 		{
 			name: "targets-regex",
-			deep: testDataDeep().deep,
+			deep: testDataDeep().Deep,
 			flat: manifest.List{
-				testDataDeep().flat[".app.web.backend.server.nginx.deployment"],
-				testDataDeep().flat[".app.web.frontend.nodejs.express.deployment"],
+				testDataDeep().Flat[".app.web.backend.server.grafana.deployment"],
+				testDataDeep().Flat[".app.web.frontend.nodejs.express.deployment"],
 			},
 			targets: util.MustCompileTargetExps(`deployment/.*`),
 		},
 		{
 			name: "targets-caseInsensitive",
-			deep: testDataDeep().deep,
+			deep: testDataDeep().Deep,
 			flat: manifest.List{
-				testDataDeep().flat[".app.web.backend.server.nginx.deployment"],
+				testDataDeep().Flat[".app.web.backend.server.grafana.deployment"],
 			},
 			targets: util.MustCompileTargetExps(
-				`DePlOyMeNt/NgInX`,
+				`DePlOyMeNt/GrAfAnA`,
 			),
 		},
 		{
 			name: "force-namespace",
 			spec: v1alpha1.Spec{Namespace: "tanka"},
-			deep: testDataFlat().deep,
+			deep: testDataFlat().Deep,
 			flat: func() manifest.List {
-				f := testDataFlat().flat["."]
+				f := testDataFlat().Flat["."]
 				f.Metadata()["namespace"] = "tanka"
 				return manifest.List{f}
 			}(),
@@ -74,12 +74,12 @@ func TestReconcile(t *testing.T) {
 			name: "custom-namespace",
 			spec: v1alpha1.Spec{Namespace: "tanka"},
 			deep: func() map[string]interface{} {
-				d := objx.New(testDataFlat().deep)
+				d := objx.New(testDataFlat().Deep)
 				d.Set("metadata.namespace", "custom")
 				return d
 			}(),
 			flat: func() manifest.List {
-				f := testDataFlat().flat["."]
+				f := testDataFlat().Flat["."]
 				f.Metadata()["namespace"] = "custom"
 				return manifest.List{f}
 			}(),
@@ -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
 	}
diff --git a/pkg/kubernetes/reconcile_test.go b/pkg/kubernetes/reconcile_test.go
index d88c7974..9a594091 100644
--- a/pkg/kubernetes/reconcile_test.go
+++ b/pkg/kubernetes/reconcile_test.go
@@ -24,7 +24,7 @@ func TestExtract(t *testing.T) {
 		{
 			name: "primitive",
 			data: testDataPrimitive(),
-			err:  ErrorPrimitiveReached{path: ".nginx.service", key: "note", primitive: "invalid because apiVersion and kind are missing"},
+			err:  ErrorPrimitiveReached{path: ".service", key: "note", primitive: "invalid because apiVersion and kind are missing"},
 		},
 		{
 			name: "deep",
@@ -34,15 +34,11 @@ func TestExtract(t *testing.T) {
 			name: "array",
 			data: testDataArray(),
 		},
-		{
-			name: "deep array",
-			data: testDataDeepArray(),
-		},
 		{
 			name: "nil",
 			data: func() testData {
 				d := testDataRegular()
-				d.deep.(map[string]interface{})["disabledObject"] = nil
+				d.Deep.(map[string]interface{})["disabledObject"] = nil
 				return d
 			}(),
 			err: nil, // we expect no error, just the result of testDataRegular
@@ -51,10 +47,10 @@ func TestExtract(t *testing.T) {
 
 	for _, c := range tests {
 		t.Run(c.name, func(t *testing.T) {
-			extracted, err := extract(c.data.deep)
+			extracted, err := extract(c.data.Deep)
 
 			require.Equal(t, c.err, err)
-			assert.EqualValues(t, c.data.flat, extracted)
+			assert.EqualValues(t, c.data.Flat, extracted)
 		})
 	}
 }
diff --git a/pkg/kubernetes/testdata/k8s.libsonnet b/pkg/kubernetes/testdata/k8s.libsonnet
new file mode 100644
index 00000000..c0851f64
--- /dev/null
+++ b/pkg/kubernetes/testdata/k8s.libsonnet
@@ -0,0 +1,37 @@
+// a very basic ripoff from `k.libsonnet`, because we can't vendor in tests
+
+{
+  deployment(name="grafana", image="grafana/grafana"):: {
+    apiVersion: "apps/v1",
+    kind: "Deployment",
+    metadata: { name: name },
+    spec: {
+      replicas: 1,
+      template: {
+        containers: [{
+            name: name,
+            image: image,
+        }],
+        metadata: { labels: { app: name }}
+      }
+    }
+  },
+  service(name="grafana", image="grafana/grafana"):: {
+    apiVersion: "v1",
+    kind: "Service",
+    metadata: { name: name },
+    spec: {
+      selector: { app: name },
+      ports: [{
+        name: name,
+        port: 3000,
+        targetPort: 3000
+      }]
+    }
+  },
+  namespace(name="default"):: {
+    apiVersion: "v1",
+    kind: "Namespace",
+    metadata: { name: name }
+  }
+}
diff --git a/pkg/kubernetes/testdata/resources.jsonnet b/pkg/kubernetes/testdata/resources.jsonnet
new file mode 100644
index 00000000..0b2e1be8
--- /dev/null
+++ b/pkg/kubernetes/testdata/resources.jsonnet
@@ -0,0 +1,6 @@
+local k = (import "./k8s.libsonnet");
+{
+  deployment: k.deployment(),
+  service: k.service(),
+  namespace: k.namespace(),
+}
diff --git a/pkg/kubernetes/testdata/tdArray.jsonnet b/pkg/kubernetes/testdata/tdArray.jsonnet
new file mode 100644
index 00000000..7234e98c
--- /dev/null
+++ b/pkg/kubernetes/testdata/tdArray.jsonnet
@@ -0,0 +1,10 @@
+local utils = import "./utils.libsonnet";
+
+local deep = import "./tdDeep.jsonnet";
+local flat = import "./tdFlat.jsonnet";
+
+{
+  deep: [deep.deep, [flat.deep]],
+  flat: utils.indexify(deep.flat, [0]) +
+        utils.indexify(flat.flat, [1, 0])
+}
diff --git a/pkg/kubernetes/testdata/tdDeep.jsonnet b/pkg/kubernetes/testdata/tdDeep.jsonnet
new file mode 100644
index 00000000..306b6407
--- /dev/null
+++ b/pkg/kubernetes/testdata/tdDeep.jsonnet
@@ -0,0 +1,27 @@
+local backendDeploy = (import "./resources.jsonnet").deployment;
+local namespace = (import "./resources.jsonnet").namespace;
+local frontendService = (import "./k8s.libsonnet").service(name="frontend");
+local frontendDeploy = (import "./k8s.libsonnet").deployment(name="frontend");
+
+{
+  deep: {
+    app: {
+      namespace: namespace,
+      web: {
+        backend: { server: { grafana: {
+            deployment: backendDeploy,
+        }}},
+        frontend: { nodejs: { express: {
+            service: frontendService,
+            deployment: frontendDeploy,
+        }}},
+      }
+    }
+  },
+  flat: {
+    ".app.web.backend.server.grafana.deployment": backendDeploy,
+    ".app.web.frontend.nodejs.express.service": frontendService,
+    ".app.web.frontend.nodejs.express.deployment": frontendDeploy,
+    ".app.namespace": namespace,
+  }
+}
diff --git a/pkg/kubernetes/testdata/tdFlat.jsonnet b/pkg/kubernetes/testdata/tdFlat.jsonnet
new file mode 100644
index 00000000..8f8b1e7f
--- /dev/null
+++ b/pkg/kubernetes/testdata/tdFlat.jsonnet
@@ -0,0 +1,6 @@
+{
+  deep: (import "./resources.jsonnet").deployment,
+  flat: {
+    ".": $.deep
+  }
+}
diff --git a/pkg/kubernetes/testdata/tdInvalidPrimitive.jsonnet b/pkg/kubernetes/testdata/tdInvalidPrimitive.jsonnet
new file mode 100644
index 00000000..5d88acdc
--- /dev/null
+++ b/pkg/kubernetes/testdata/tdInvalidPrimitive.jsonnet
@@ -0,0 +1,8 @@
+{
+  deep: {
+    deploy: (import "./resources.jsonnet").deployment,
+    service: {
+      "note": "invalid because apiVersion and kind are missing",
+    }
+  }
+}
diff --git a/pkg/kubernetes/testdata/tdRegular.jsonnet b/pkg/kubernetes/testdata/tdRegular.jsonnet
new file mode 100644
index 00000000..0ee1e233
--- /dev/null
+++ b/pkg/kubernetes/testdata/tdRegular.jsonnet
@@ -0,0 +1,13 @@
+local deployment = (import "./resources.jsonnet").deployment;
+local service = (import "./resources.jsonnet").service;
+
+{
+  deep: {
+    deployment: deployment,
+    service: service,
+  },
+  flat: {
+    ".deployment": deployment,
+    ".service": service,
+  }
+}
diff --git a/pkg/kubernetes/testdata/utils.libsonnet b/pkg/kubernetes/testdata/utils.libsonnet
new file mode 100644
index 00000000..fc76d644
--- /dev/null
+++ b/pkg/kubernetes/testdata/utils.libsonnet
@@ -0,0 +1,12 @@
+{
+  mkIndex(arr)::
+    local idxs = [ "[%s]" % i for i in arr];
+    "." +std.join(".", idxs),
+
+  mkKey(k):: if k == "." then "" else k,
+
+  indexify(obj, index):: {
+    [$.mkIndex(index) + $.mkKey(key)]: obj[key],
+    for key in std.objectFields(obj)
+  },
+}