Repository: grafana/tanka
Pull request #146 (2020-01-09 10:35:40)

Update tanka docs.
Fix/update the tanka docs.

Signed-off-by: Milan Plzik <milan.plzik@grafana.com>


Patch:
diff --git a/cmd/tk/env.go b/cmd/tk/env.go
index f1b0acbd..fe8bf016 100644
--- a/cmd/tk/env.go
+++ b/cmd/tk/env.go
@@ -9,11 +9,14 @@ import (
 	"text/tabwriter"
 
 	"github.com/pkg/errors"
+	"github.com/posener/complete"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
 
 	"github.com/grafana/tanka/pkg/cli"
+	"github.com/grafana/tanka/pkg/cli/cmp"
+	"github.com/grafana/tanka/pkg/kubernetes/client"
 	"github.com/grafana/tanka/pkg/spec/v1alpha1"
 )
 
@@ -34,6 +37,7 @@ func envCmd() *cobra.Command {
 
 func envSettingsFlags(env *v1alpha1.Config, fs *pflag.FlagSet) {
 	fs.StringVar(&env.Spec.APIServer, "server", env.Spec.APIServer, "endpoint of the Kubernetes API")
+	fs.StringVar(&env.Spec.APIServer, "server-from-context", env.Spec.APIServer, "set the server to a known one from $KUBECONFIG")
 	fs.StringVar(&env.Spec.Namespace, "namespace", env.Spec.Namespace, "namespace to create objects in")
 	fs.StringVar(&env.Spec.DiffStrategy, "diff-strategy", env.Spec.DiffStrategy, "specify diff-strategy. Automatically detected otherwise.")
 }
@@ -44,12 +48,24 @@ func envSetCmd() *cobra.Command {
 		Short: "update properties of an environment",
 		Args:  cobra.ExactArgs(1),
 		Annotations: map[string]string{
-			"args": "baseDir",
+			"args":                      "baseDir",
+			"flags/server-from-context": "kubectlContexts",
 		},
 	}
+
+	// handler for server-from-context
+	cmp.Handlers.Add("kubectlContexts", complete.PredictFunc(
+		func(complete.Args) []string {
+			c, _ := client.Contexts()
+			return c
+		},
+	))
+
+	// flags
 	tmp := v1alpha1.Config{}
 	envSettingsFlags(&tmp, cmd.Flags())
 
+	// removed name flag
 	name := cmd.Flags().String("name", "", "")
 	_ = cmd.Flags().MarkHidden("name")
 
@@ -63,6 +79,14 @@ func envSetCmd() *cobra.Command {
 			log.Fatalln(err)
 		}
 
+		if cmd.Flags().Changed("server-from-context") {
+			server, err := client.IPFromContext(tmp.Spec.APIServer)
+			if err != nil {
+				log.Fatalf("Resolving IP from context: %s", err)
+			}
+			tmp.Spec.APIServer = server
+		}
+
 		viper.Reset()
 		cfg := setupConfiguration(path)
 		if cfg == nil {
diff --git a/cmd/tk/jsonnet.go b/cmd/tk/jsonnet.go
index 64cba68c..e78f1e7d 100644
--- a/cmd/tk/jsonnet.go
+++ b/cmd/tk/jsonnet.go
@@ -1,14 +1,12 @@
 package main
 
 import (
+	"encoding/json"
 	"log"
-	"path/filepath"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
-	"github.com/grafana/tanka/pkg/jsonnet"
-	"github.com/grafana/tanka/pkg/jsonnet/jpath"
+	"github.com/grafana/tanka/pkg/tanka"
 )
 
 func evalCmd() *cobra.Command {
@@ -22,28 +20,18 @@ func evalCmd() *cobra.Command {
 	}
 
 	cmd.Run = func(cmd *cobra.Command, args []string) {
-		json, err := eval(args[0])
+		raw, _, err := tanka.Eval(args[0], nil)
 		if err != nil {
-			log.Fatalln("evaluating:", err)
+			log.Fatalln(err, nil)
 		}
-		pageln(json)
-	}
 
-	return cmd
-}
+		out, err := json.MarshalIndent(raw, "", "  ")
+		if err != nil {
+			log.Fatalln(err)
+		}
 
-func eval(workdir string) (string, error) {
-	pwd, err := filepath.Abs(workdir)
-	if err != nil {
-		return "", err
+		pageln(string(out))
 	}
-	_, baseDir, _, err := jpath.Resolve(pwd)
-	if err != nil {
-		return "", errors.Wrap(err, "resolving jpath")
-	}
-	json, err := jsonnet.EvaluateFile(filepath.Join(baseDir, "main.jsonnet"))
-	if err != nil {
-		return "", err
-	}
-	return json, nil
+
+	return cmd
 }
diff --git a/docs/docs/faq.md b/docs/docs/faq.md
index 111fc1d2..ef03c743 100644
--- a/docs/docs/faq.md
+++ b/docs/docs/faq.md
@@ -5,6 +5,20 @@ route: "/faq"
 
 # Frequently asked questions
 
+## What is Jsonnet?
+
+Jsonnet is a data templating language, originally created by Google.
+
+It is a superset of JSON, which adds common structures from full programming
+languages to data modeling. Because it being a superset of JSON and ultimately
+always compiling to JSON, it is guaranteed that the output will be valid JSON
+(or YAML).
+
+By allowing _functions_ and _imports_, rich abstraction is possible, even across
+project boundaries.
+
+For more, refer to the official documentation: https://jsonnet.org/
+
 ## How is this different from ksonnet?
 
 Tanka aims to be a fully compatible, drop-in replacement for the main workflow
@@ -20,6 +34,14 @@ designing Tanka, we felt these add more complexity for the user than they
 provide additional value. To keep Tanka as minimal as possible, these are **not
 available** and are not likely to be ever added.
 
+## What about kubecfg ?
+
+Tanka development has started at the time when kubecfg was a part of
+already-deprecated `ksonnet` project. Although these projects are similar, Tanka
+aims to provide continuity for `ksonnet` users, whereas `kubecfg` is (according
+to the project's [README.md](https://github.com/bitnami/kubecfg/blob/master/README.md)
+really just a thin Kubernetes-specific wrapper around jsonnet evaluation.
+
 ## Why not Helm?
 
 Helm relies heavily on _string templating_ `.yaml` files. We feel this is the
@@ -34,17 +56,3 @@ first.
 
 Jsonnet on the other hand got you covered by supporting mixing (patching,
 deep-merging) objects on top of the libraries output if required.
-
-## What is Jsonnet?
-
-Jsonnet is a data templating language, originally created by Google.
-
-It is a superset of JSON, which adds common structures from full programming
-languages to data modeling. Because it being a superset of JSON and ultimately
-always compiling to JSON, it is guaranteed that the output will be valid JSON
-(or YAML).
-
-By allowing _functions_ and _imports_, rich abstraction is possible, even across
-project boundaries.
-
-For more, refer to the official documentation: https://jsonnet.org/
diff --git a/docs/docs/installation.md b/docs/docs/installation.md
index 7526423e..1539ec38 100644
--- a/docs/docs/installation.md
+++ b/docs/docs/installation.md
@@ -27,7 +27,8 @@ Just grab the latest version from there, download it and put somewhere in your
 
 ## From source
 
-In case the above won't work for you, try with `go get`:
+In case the above won't work for you, you can try building the most recent
+code using `go get`:
 
 ```bash
 $ go get -u github.com/grafana/tanka/cmd/tk
diff --git a/docs/docs/jsonnet/main.md b/docs/docs/jsonnet/main.md
index e5da5adc..846e33de 100644
--- a/docs/docs/jsonnet/main.md
+++ b/docs/docs/jsonnet/main.md
@@ -18,9 +18,6 @@ extract your objects first. To do this, it traverses the tree until it finds
 something that looks like a Kubernetes manifest. An object is considered valid
 when it has both, `kind` and `apiVersion` set.
 
-> This behaviour is going to change in the future, `metadata.name` will also
-> become required.
-
 To ensure Tanka can find your manifests, the output of your Jsonnet needs to
 have one of the following structures:
 
diff --git a/docs/docs/jsonnet/overview.md b/docs/docs/jsonnet/overview.md
index fd40e7cb..6cb4bcd0 100644
--- a/docs/docs/jsonnet/overview.md
+++ b/docs/docs/jsonnet/overview.md
@@ -10,7 +10,10 @@ menu: Writing Jsonnet
 expressing what shall be deployed to your Kubernetes cluster. Understanding
 Jsonnet is crucial to using Tanka effectively.
 
-This page covers the Jsonnet language itself. For more information on how to use Jsonnet with Kubernetes, see [the tutorial](/tutorial/jsonnet).
+This page covers the Jsonnet language itself. For more information on how to
+use Jsonnet with Kubernetes, see [the tutorial](/tutorial/jsonnet). There's
+also [the official Jsonnet tutorial](https://jsonnet.org/learning/tutorial.html)
+that provides a more detailed review of language features.
 
 ## Syntax
 
diff --git a/docs/docs/known-issues.md b/docs/docs/known-issues.md
index a2204c50..07f624d2 100644
--- a/docs/docs/known-issues.md
+++ b/docs/docs/known-issues.md
@@ -23,16 +23,21 @@ import paths. While `ksonnet` used to magically include them, Tanka follows a
 more explicit approach and requires you to install them using `jb`:
 
 ```bash
-$ jb install github.com/ksonnet/ksonnet-lib/ksonnet.beta.4/k.libsonnet
-$ jb install github.com/ksonnet/ksonnet-lib/ksonnet.beta.4/k8s.libsonnet
+$ jb install github.com/ksonnet/ksonnet-lib/ksonnet.beta.4
+$ echo 'import "ksonnet.beta.4/k.libsonnet"' > lib/k.libsonnet
 ```
 
-This installs version `beta.3` of the libraries, matching Kubernetes version
-`1.8.0`. If you need another version, take a look at
+This does 2 things:
+
+1) It installs the ksonnet library (in `vendor/ksonnet.beta.4`).
+If you need a specific version, take a look at
 https://github.com/ksonnet/ksonnet-lib. When a pre-compiled version is
 available, install it using `jb`, otherwise compile it yourself and place it
 under `lib/`.
 
+2) It makes an alias for libraries importing `k.libsonnet` directly. See
+https://tanka.dev/tutorial/k-lib#aliasing for the alias rationale.
+
 ### Unexpected diff if the same port number is used for UDP and TCP
 
 A
diff --git a/docs/docs/tutorial/abstraction.mdx b/docs/docs/tutorial/abstraction.mdx
index da446347..ac232680 100644
--- a/docs/docs/tutorial/abstraction.mdx
+++ b/docs/docs/tutorial/abstraction.mdx
@@ -70,7 +70,7 @@ Let's start cleaning this up by separating logical pieces into distinct files:
       },
       spec: {
         ports: [{
-            name: '%s-ui' % $._config.grafana.port,
+            name: '%s-ui' % $._config.grafana.name,
             port: $._config.grafana.port,
             targetPort: $._config.grafana.port,
         }],
diff --git a/docs/docs/tutorial/jsonnet.mdx b/docs/docs/tutorial/jsonnet.mdx
index 49bb9e28..b2af65f3 100644
--- a/docs/docs/tutorial/jsonnet.mdx
+++ b/docs/docs/tutorial/jsonnet.mdx
@@ -82,13 +82,13 @@ Similar to JSON, each `.jsonnet` file holds a single object. The one returned by
 ```jsonnet
 // main.jsonnet
 {
-    "deployment": { /* ... */ },
-    "service": { /* ... */ }
+    "some_deployment": { /* ... */ },
+    "some_service": { /* ... */ }
 }
 ```
 
 They may be deeply nested, Tanka extracts everything that looks like a
-Kubebernetes resource automatically.
+Kubernetes resource automatically.
 
 So let's rewrite the [previous `.yaml`](/tutorial/refresher#writing-the-yaml) to
 very basic `.jsonnet`:
@@ -286,12 +286,28 @@ clusters just work.
 This allows us to make sure that you will never accidentally apply to the wrong
 cluster.
 
-> **Note**: Tanka won't create the namespace for you. Either include it in
-> Jsonnet, or with the following command:
+> **Note**: Tanka won't create the namespace for you -- you need to include it in
+> Jsonnet:
+> ```jsonnet
+> {
+>   my_namespace: {
+>     apiVersion: "v1",
+>     kind: "Namespace",
+>     metadata: {
+>       name: "my-namespace"
+>     }
+>   }
+> }
+> ```
+>
+> Alternatively, you can create the namespace manually:
 >
 > ```bash
 > $ kubectl create ns monitoring
 > ```
+>
+> This, however, will create an object that is not tracked by Tanka
+> and thus needs to be taken care of via other means.
 
 ## Verifying the changes
 
diff --git a/docs/docs/tutorial/k-lib.mdx b/docs/docs/tutorial/k-lib.mdx
index d6d6c40a..d1b621e8 100644
--- a/docs/docs/tutorial/k-lib.mdx
+++ b/docs/docs/tutorial/k-lib.mdx
@@ -29,7 +29,7 @@ available at https://github.com/ksonnet/ksonnet-lib.
 However while using it internally we have discovered that the exposed API has
 several annoyances. To address them, we developed another library that builds on
 top of the generated one but improves the developer experience:
-https://github.com/grafana/jsonnet-libs/ksonnet-util
+https://github.com/grafana/jsonnet-libs/tree/master/ksonnet-util
 
 If you do not have any strong reasons against it, just adopt the wrapper as
 well, it will ease your work. Ultimately, we hope to integrate our enhancements
diff --git a/docs/docs/tutorial/parameterizing.mdx b/docs/docs/tutorial/parameterizing.mdx
index fff55854..3fa6d7c0 100644
--- a/docs/docs/tutorial/parameterizing.mdx
+++ b/docs/docs/tutorial/parameterizing.mdx
@@ -56,7 +56,7 @@ We can then replace hardcoded values with a reference to this object:
       spec: {
         ports: [{
 -           name: 'grafana-ui',
-+           name: '%s-ui' % $._config.grafana.port, // printf-style formatting
++           name: '%s-ui' % $._config.grafana.name, // printf-style formatting
 -           port: 3000,
 +           port: $._config.grafana.port,
 -           targetPort: 3000,
diff --git a/docs/docs/tutorial/refresher.mdx b/docs/docs/tutorial/refresher.mdx
index 13b8c91a..a737f1e3 100644
--- a/docs/docs/tutorial/refresher.mdx
+++ b/docs/docs/tutorial/refresher.mdx
@@ -17,8 +17,8 @@ for the task of deploying Grafana and Prometheus to Kubernetes:
      Prometheus.
 2. Grafana
    - Another `Deployment` is required for the Grafana server.
-   - To connect to the webinterface, we also need a `Service` of type
-     `NodePort`.
+   - To connect to the web interface, we will be using a `Service` of type
+   `NodePort`.
 
 Before taking a look how Tanka can help doing so, let's recall how to do it with
 plain `kubectl`.
@@ -122,7 +122,7 @@ furthermore harms readability a lot.
 To actually apply those resources, copy them into `.yaml` files and use:
 
 ```bash
-$ kubectl apply -f prometheus.yaml grafana.yaml
+$ kubectl apply -f prometheus.yaml -f grafana.yaml
 deployment.apps/grafana created
 deployment.apps/prometheus created
 service/grafana created
@@ -136,7 +136,7 @@ Grafana can connect to Prometheus!
 
 ```bash
 # Temporarily forward Grafana to localhost
-kubectl port-forward deployments/grafana 3000:8080
+kubectl port-forward deployments/grafana 8080:3000
 ```
 
 Now go to http://localhost:8080 in your browser and login using `admin:admin`.
@@ -155,7 +155,7 @@ Let's explore how Tanka can help us here in the next section!
 Let's remove everything we created to start fresh with Jsonnet in the next section:
 
 ```bash
-$ kubectl delete -f prometheus.yaml grafana.yaml
+$ kubectl delete -f prometheus.yaml -f grafana.yaml
 ```
 
 import TutorialButtons from "./TutorialButtons"
diff --git a/pkg/jsonnet/eval.go b/pkg/jsonnet/eval.go
index 5c85ffb8..07b07c38 100644
--- a/pkg/jsonnet/eval.go
+++ b/pkg/jsonnet/eval.go
@@ -11,8 +11,12 @@ import (
 	"github.com/grafana/tanka/pkg/jsonnet/native"
 )
 
+// Modifiers allow to set optional paramters on the Jsonnet VM.
+// See jsonnet.With* for this.
+type Modifier func(vm *jsonnet.VM) error
+
 // EvaluateFile opens the file, reads it into memory and evaluates it afterwards (`Evaluate()`)
-func EvaluateFile(jsonnetFile string) (string, error) {
+func EvaluateFile(jsonnetFile string, mods ...Modifier) (string, error) {
 	bytes, err := ioutil.ReadFile(jsonnetFile)
 	if err != nil {
 		return "", err
@@ -22,16 +26,32 @@ func EvaluateFile(jsonnetFile string) (string, error) {
 	if err != nil {
 		return "", errors.Wrap(err, "resolving jpath")
 	}
-	return Evaluate(string(bytes), jpath)
+	return Evaluate(string(bytes), jpath, mods...)
 }
 
 // Evaluate renders the given jsonnet into a string
-func Evaluate(sonnet string, jpath []string) (string, error) {
+func Evaluate(sonnet string, jpath []string, mods ...Modifier) (string, error) {
 	vm := jsonnet.MakeVM()
 	vm.Importer(NewExtendedImporter(jpath))
+
+	for _, mod := range mods {
+		if err := mod(vm); err != nil {
+			return "", err
+		}
+	}
+
 	for _, nf := range native.Funcs() {
 		vm.NativeFunction(nf)
 	}
 
 	return vm.EvaluateSnippet("main.jsonnet", sonnet)
 }
+
+// WithExtCode allows to make the supplied snippet available to Jsonnet as an
+// ext var
+func WithExtCode(key, code string) Modifier {
+	return func(vm *jsonnet.VM) error {
+		vm.ExtCode(key, code)
+		return nil
+	}
+}
diff --git a/pkg/jsonnet/importer.go b/pkg/jsonnet/importer.go
index 7221e122..ee42c68e 100644
--- a/pkg/jsonnet/importer.go
+++ b/pkg/jsonnet/importer.go
@@ -1,67 +1,130 @@
 package jsonnet
 
 import (
-	"bytes"
 	"encoding/json"
 	"io"
 	"path/filepath"
+	"strings"
 
 	jsonnet "github.com/google/go-jsonnet"
 	"github.com/pkg/errors"
 	"gopkg.in/yaml.v3"
 )
 
+const locationInternal = "<internal>"
+
 // ExtendedImporter wraps jsonnet.FileImporter to add additional functionality:
 // - `import "file.yaml"`
+// - `import "tk"`
 type ExtendedImporter struct {
-	fi *jsonnet.FileImporter
+	loaders    []importLoader    // for loading jsonnet from somewhere. First one that returns non-nil is used
+	processors []importProcessor // for post-processing (e.g. yaml -> json)
 }
 
+// importLoader are executed before the actual importing. If they return
+// something, this value is used.
+type importLoader func(importedFrom, importedPath string) (c *jsonnet.Contents, foundAt string, err error)
+
+// importProcessor are executed after the file import and may modify the result
+// further
+type importProcessor func(contents, foundAt string) (c *jsonnet.Contents, err error)
+
 // NewExtendedImporter returns a new instance of ExtendedImporter with the
 // correct jpaths set up
 func NewExtendedImporter(jpath []string) *ExtendedImporter {
 	return &ExtendedImporter{
-		fi: &jsonnet.FileImporter{
-			JPaths: jpath,
+		loaders: []importLoader{
+			tkLoader,
+			newFileLoader(&jsonnet.FileImporter{
+				JPaths: jpath,
+			})},
+		processors: []importProcessor{
+			yamlProcessor,
 		},
 	}
 }
 
 // Import implements the functionality offered by the ExtendedImporter
 func (i *ExtendedImporter) Import(importedFrom, importedPath string) (contents jsonnet.Contents, foundAt string, err error) {
-	// regularly import
-	contents, foundAt, err = i.fi.Import(importedFrom, importedPath)
-	if err != nil {
-		return jsonnet.Contents{}, "", err
+	// load using loader
+	for _, loader := range i.loaders {
+		c, f, err := loader(importedFrom, importedPath)
+		if err != nil {
+			return jsonnet.Contents{}, "", err
+		}
+		if c != nil {
+			contents = *c
+			foundAt = f
+			break
+		}
 	}
 
-	// if yaml -> convert to json
-	ext := filepath.Ext(foundAt)
-	if ext == ".yaml" || ext == ".yml" {
-		ret := []interface{}{}
-		d := yaml.NewDecoder(bytes.NewReader([]byte(contents.String())))
-		for {
-			var doc interface{}
-			if err := d.Decode(&doc); err != nil {
-				if err == io.EOF {
-					break
-				}
-				return jsonnet.Contents{}, "", errors.Wrapf(err, "unmarshalling yaml import '%s'", foundAt)
-			}
-			ret = append(ret, doc)
+	// check if needs postprocessing
+	for _, processor := range i.processors {
+		c, err := processor(contents.String(), foundAt)
+		if err != nil {
+			return jsonnet.Contents{}, "", err
 		}
-		var data interface{}
-		if len(ret) == 1 {
-			data = ret[0]
-		} else {
-			data = ret
+		if c != nil {
+			contents = *c
+			break
 		}
-		out, err := json.Marshal(data)
-		if err != nil {
-			return jsonnet.Contents{}, "", errors.Wrapf(err, "converting '%s' to json", foundAt)
+	}
+
+	return contents, foundAt, nil
+}
+
+// tkLoader provides `tk.libsonnet` from memory (builtin)
+func tkLoader(importedFrom, importedPath string) (contents *jsonnet.Contents, foundAt string, err error) {
+	if importedPath != "tk" {
+		return nil, "", nil
+	}
+
+	s := jsonnet.MakeContents(tkLibsonnet)
+	return &s, filepath.Join(locationInternal, "tk.libsonnet"), nil
+}
+
+// newFileLoader returns an importLoader that uses jsonnet.FileImporter to source
+// files from the local filesystem
+func newFileLoader(fi *jsonnet.FileImporter) importLoader {
+	return func(importedFrom, importedPath string) (contents *jsonnet.Contents, foundAt string, err error) {
+		var c jsonnet.Contents
+		c, foundAt, err = fi.Import(importedFrom, importedPath)
+		return &c, foundAt, err
+	}
+}
+
+// yamlProcessor catches yaml files and converts them to JSON so that they can
+// be used with `import`
+func yamlProcessor(contents, foundAt string) (c *jsonnet.Contents, err error) {
+	ext := filepath.Ext(foundAt)
+	if yaml := ext == ".yaml" || ext == ".yml"; !yaml {
+		return nil, nil
+	}
+
+	ret := []interface{}{}
+	d := yaml.NewDecoder(strings.NewReader(contents))
+	for {
+		var doc interface{}
+		if err := d.Decode(&doc); err != nil {
+			if err == io.EOF {
+				break
+			}
+			return nil, errors.Wrapf(err, "unmarshalling yaml import '%s'", foundAt)
 		}
-		contents = jsonnet.MakeContents(string(out))
+		ret = append(ret, doc)
+	}
+
+	var data interface{} = ret
+	if len(ret) == 1 {
+		data = ret[0]
+	}
+
+	out, err := json.Marshal(data)
+	if err != nil {
+		return nil, errors.Wrapf(err, "converting '%s' to json", foundAt)
 	}
 
-	return
+	s := jsonnet.MakeContents(string(out))
+	return &s, nil
 }
diff --git a/pkg/jsonnet/tk.libsonnet.go b/pkg/jsonnet/tk.libsonnet.go
new file mode 100644
index 00000000..0041bd6c
--- /dev/null
+++ b/pkg/jsonnet/tk.libsonnet.go
@@ -0,0 +1,7 @@
+package jsonnet
+
+const tkLibsonnet = `
+{
+  env: std.extVar("tanka.dev/environment"),
+}
+`
diff --git a/pkg/kubernetes/client/context.go b/pkg/kubernetes/client/context.go
index 3b3d1d84..b13922de 100644
--- a/pkg/kubernetes/client/context.go
+++ b/pkg/kubernetes/client/context.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
+	"strings"
 
 	"github.com/stretchr/objx"
 	funk "github.com/thoas/go-funk"
@@ -17,47 +18,90 @@ func (k *Kubectl) setupContext() error {
 		return nil
 	}
 
+	var err error
+	k.cluster, k.context, err = ContextFromIP(k.APIServer)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// Kubeconfig returns the merged $KUBECONFIG of the host
+func Kubeconfig() (map[string]interface{}, error) {
 	cmd := exec.Command("kubectl", "config", "view", "-o", "json")
 	cfgJSON := bytes.Buffer{}
 	cmd.Stdout = &cfgJSON
 	cmd.Stderr = os.Stderr
 	if err := cmd.Run(); err != nil {
-		return err
+		return nil, err
 	}
 	var cfg map[string]interface{}
 	if err := json.Unmarshal(cfgJSON.Bytes(), &cfg); err != nil {
-		return err
+		return nil, err
 	}
+	return cfg, nil
+}
 
-	var err error
-	k.cluster, k.context, err = contextFromKubeconfig(cfg, k.APIServer)
-	if err != nil {
-		return err
+func Contexts() ([]string, error) {
+	cmd := exec.Command("kubectl", "config", "get-contexts", "-o=name")
+	buf := bytes.Buffer{}
+	cmd.Stdout = &buf
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
+		return nil, err
 	}
-	return nil
+
+	return strings.Split(buf.String(), "\n"), nil
 }
 
-// contextFromKubeconfig searches a kubeconfig for a context of a cluster that matches the apiServer
-func contextFromKubeconfig(kubeconfig map[string]interface{}, apiServer string) (cluster, context objx.Map, err error) {
+// ContextFromIP searches the $KUBECONFIG for a context of a cluster that matches the apiServer
+func ContextFromIP(apiServer string) (cluster, context objx.Map, err error) {
+	kubeconfig, err := Kubeconfig()
+	if err != nil {
+		return nil, nil, err
+	}
 	cfg := objx.New(kubeconfig)
 
 	// find the correct cluster
-	cluster = objx.New(funk.Find(cfg.Get("clusters").MustMSISlice(), func(x map[string]interface{}) bool {
-		host := objx.New(x).Get("cluster.server").MustStr()
-		return host == apiServer
-	}))
-	if len(cluster) == 0 { // empty map means no result
+	cluster = find(cfg.Get("clusters").MustMSISlice(), "cluster.server", apiServer)
+	if cluster == nil { // empty map means no result
 		return nil, nil, fmt.Errorf("no cluster that matches the apiServer `%s` was found. Please check your $KUBECONFIG", apiServer)
 	}
 
 	// find a context that uses the cluster
-	context = objx.New(funk.Find(cfg.Get("contexts").MustMSISlice(), func(x map[string]interface{}) bool {
-		c := objx.New(x)
-		return c.Get("context.cluster").MustStr() == cluster.Get("name").MustStr()
-	}))
-	if !(len(context) > 0) {
+	context = find(cfg.Get("contexts").MustMSISlice(), "context.cluster", cluster.Get("name").MustStr())
+	if context == nil {
 		return nil, nil, fmt.Errorf("no context that matches the cluster `%s` was found. Please check your $KUBECONFIG", cluster.Get("name").MustStr())
 	}
 
 	return cluster, context, nil
 }
+
+func IPFromContext(name string) (ip string, err error) {
+	kubeconfig, err := Kubeconfig()
+	if err != nil {
+		return "", err
+	}
+	cfg := objx.New(kubeconfig)
+
+	// find the context
+	context := find(cfg.Get("contexts").MustMSISlice(), "name", name)
+	if context == nil {
+		return "", fmt.Errorf("no context named `%s` was found. Please check your $KUBECONFIG", name)
+	}
+
+	clusterName := context.Get("context.cluster").MustStr()
+	cluster := find(cfg.Get("clusters").MustMSISlice(), "name", clusterName)
+	if cluster == nil { // empty map means no result
+		return "", fmt.Errorf("no cluster named `%s` as required by context `%s` was found. Please check your $KUBECONFIG", clusterName, name)
+	}
+
+	return cluster.Get("cluster.server").MustStr(), nil
+}
+
+func find(list []map[string]interface{}, prop string, expected string) objx.Map {
+	return objx.New(funk.Find(list, func(x map[string]interface{}) bool {
+		got := objx.New(x).Get(prop).MustStr()
+		return got == expected
+	}))
+}
diff --git a/pkg/kubernetes/manifest/manifest.go b/pkg/kubernetes/manifest/manifest.go
index 19cab78e..64194402 100644
--- a/pkg/kubernetes/manifest/manifest.go
+++ b/pkg/kubernetes/manifest/manifest.go
@@ -3,6 +3,7 @@ package manifest
 import (
 	"bytes"
 	"encoding/json"
+	"strings"
 
 	"github.com/pkg/errors"
 	"github.com/stretchr/objx"
@@ -48,11 +49,15 @@ func (m Manifest) Verify() error {
 	if !o.Get("apiVersion").IsStr() {
 		err.add("apiVersion")
 	}
-	if !o.Get("metadata").IsMSI() {
-		err.add("metadata")
-	}
-	if !o.Get("metadata.name").IsStr() && m.Kind() != "List" {
-		err.add("metadata.name")
+
+	// Lists don't have `metadata`
+	if !strings.HasSuffix(m.Kind(), "List") {
+		if !o.Get("metadata").IsMSI() {
+			err.add("metadata")
+		}
+		if !o.Get("metadata.name").IsStr() {
+			err.add("metadata.name")
+		}
 	}
 
 	if len(err.fields) == 0 {
@@ -74,6 +79,9 @@ func (m Manifest) APIVersion() string {
 
 // Metadata returns the metadata of this object
 func (m Manifest) Metadata() Metadata {
+	if m["metadata"] == nil {
+		m["metadata"] = make(map[string]interface{})
+	}
 	return Metadata(m["metadata"].(map[string]interface{}))
 }
 
@@ -104,7 +112,11 @@ type Metadata map[string]interface{}
 
 // Name of the manifest
 func (m Metadata) Name() string {
-	return m["name"].(string)
+	name, ok := m["name"]
+	if !ok {
+		return ""
+	}
+	return name.(string)
 }
 
 // HasNamespace returns whether the manifest has a namespace set
diff --git a/pkg/kubernetes/reconcile.go b/pkg/kubernetes/reconcile.go
index 78d1a8cc..b8ecf551 100644
--- a/pkg/kubernetes/reconcile.go
+++ b/pkg/kubernetes/reconcile.go
@@ -99,9 +99,13 @@ func walkJSON(deep interface{}, extracted map[string]manifest.Manifest, path tra
 
 	// walk it
 	for key, d := range deep.(map[string]interface{}) {
-		if key == "__ksonnet" {
+		switch {
+		case key == "__ksonnet": // exclude ksonnet object
+			continue
+		case d == nil: // result from false if condition in Jsonnet
 			continue
 		}
+
 		path := append(path, key)
 
 		if _, ok := d.(map[string]interface{}); !ok {
diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go
index 18cb0e6a..d87aa6a7 100644
--- a/pkg/spec/spec.go
+++ b/pkg/spec/spec.go
@@ -11,6 +11,8 @@ import (
 	"github.com/grafana/tanka/pkg/spec/v1alpha1"
 )
 
+const APIGroup = "tanka.dev"
+
 // list of deprecated config keys and their alternatives
 // however, they still work and are aliased internally
 var deprecated = []depreciation{
diff --git a/pkg/tanka/parse.go b/pkg/tanka/parse.go
index 23735023..07dde6af 100644
--- a/pkg/tanka/parse.go
+++ b/pkg/tanka/parse.go
@@ -35,16 +35,11 @@ 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) {
-	env, err := parseEnv(baseDir, opts)
+	raw, env, err := Eval(baseDir, opts)
 	if err != nil {
 		return nil, err
 	}
 
-	raw, err := eval(baseDir)
-	if err != nil {
-		return nil, errors.Wrap(err, "evaluating jsonnet")
-	}
-
 	rec, err := kubernetes.Reconcile(raw, env.Spec, opts.targets)
 	if err != nil {
 		return nil, errors.Wrap(err, "reconciling")
@@ -56,6 +51,25 @@ 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)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	raw, err = evalJsonnet(baseDir, env)
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "evaluating jsonnet")
+	}
+
+	return raw, 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) {
@@ -80,9 +94,9 @@ func parseEnv(baseDir string, opts *options) (*v1alpha1.Config, error) {
 	return config, nil
 }
 
-// eval evaluates the jsonnet environment at the given directory starting with
+// evalJsonnet evaluates the jsonnet environment at the given directory starting with
 // `main.jsonnet`
-func eval(path string) (map[string]interface{}, error) {
+func evalJsonnet(path string, env *v1alpha1.Config) (map[string]interface{}, error) {
 	workdir, err := filepath.Abs(path)
 	if err != nil {
 		return nil, err
@@ -91,7 +105,16 @@ func eval(path string) (map[string]interface{}, error) {
 	if err != nil {
 		return nil, errors.Wrap(err, "resolving jpath")
 	}
-	raw, err := jsonnet.EvaluateFile(filepath.Join(baseDir, "main.jsonnet"))
+
+	jsonEnv, err := json.Marshal(env)
+	if err != nil {
+		return nil, errors.Wrap(err, "marshalling environment config")
+	}
+
+	raw, err := jsonnet.EvaluateFile(
+		filepath.Join(baseDir, "main.jsonnet"),
+		jsonnet.WithExtCode(spec.APIGroup+"/environment", string(jsonEnv)),
+	)
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/pkg/kubernetes/reconcile_test.go b/pkg/kubernetes/reconcile_test.go
index fdecf160..d16c13d3 100644
--- a/pkg/kubernetes/reconcile_test.go
+++ b/pkg/kubernetes/reconcile_test.go
@@ -34,6 +34,15 @@ func TestExtract(t *testing.T) {
 			name: "array",
 			data: testDataArray(),
 		},
+		{
+			name: "nil",
+			data: func() testData {
+				d := testDataRegular()
+				d.deep.(map[string]interface{})["disabledObject"] = nil
+				return d
+			}(),
+			err: nil, // we expect no error, just the result of testDataRegular
+		},
 	}
 
 	for _, c := range tests {