Repository: grafana/tanka
Pull request #203 (2020-02-10 22:28:15)

fix(spec): Remove viper
Removes the direct dependency on `spf13/viper`, as we don't actually need it anymore and it is a bloated one.

Also:
- fixes broken links in error messages
- improves the error when an invalid directory is supplied on the command line


Patch:
diff --git a/cmd/tk/env.go b/cmd/tk/env.go
index 8887007b..ba7c1f03 100644
--- a/cmd/tk/env.go
+++ b/cmd/tk/env.go
@@ -12,7 +12,6 @@ import (
 	"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"
@@ -87,11 +86,7 @@ func envSetCmd() *cobra.Command {
 			tmp.Spec.APIServer = server
 		}
 
-		viper.Reset()
 		cfg := setupConfiguration(path)
-		if cfg == nil {
-			log.Fatalf("Failed to load an environment at `%s`.\nMake sure it exists and is properly configured. See https://tanka.dev/environments/ for details.", path)
-		}
 		if tmp.Spec.APIServer != "" && tmp.Spec.APIServer != cfg.Spec.APIServer {
 			fmt.Printf("updated spec.apiServer (`%s -> `%s`)\n", cfg.Spec.APIServer, tmp.Spec.APIServer)
 			cfg.Spec.APIServer = tmp.Spec.APIServer
@@ -220,7 +215,6 @@ func envListCmd() *cobra.Command {
 			panic(err)
 		}
 		for _, dir := range dirs {
-			viper.Reset()
 			env := setupConfiguration(dir)
 			if env == nil {
 				log.Printf("Could not setup configuration from %q", dir)
diff --git a/cmd/tk/main.go b/cmd/tk/main.go
index b78363b6..21c000fd 100644
--- a/cmd/tk/main.go
+++ b/cmd/tk/main.go
@@ -9,7 +9,6 @@ import (
 
 	"github.com/posener/complete"
 	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
 	"golang.org/x/crypto/ssh/terminal"
 
 	"github.com/grafana/tanka/pkg/cli/cmp"
@@ -99,9 +98,6 @@ func setupConfiguration(baseDir string) *v1alpha1.Config {
 	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)
-		case viper.ConfigFileNotFoundError:
-			return nil
 		// the config includes deprecated fields
 		case spec.ErrDeprecated:
 			if verbose {
diff --git a/cmd/tk/util.go b/cmd/tk/util.go
index 72a070c5..a4cfb450 100644
--- a/cmd/tk/util.go
+++ b/cmd/tk/util.go
@@ -26,11 +26,11 @@ func pageln(i ...interface{}) {
 	pager := os.Getenv("PAGER")
 	var args []string
 	if pager == "" || pager == "less" {
-		// --raw-control-chars  Honors colors from diff.
+		// --RAW-CONTROL-CHARS  Honors colors from diff. Must be in all caps, otherwise display issues occur.
 		// --quit-if-one-screen Closer to the git experience.
 		// --no-init            Don't clear the screen when exiting.
 		pager = "less"
-		args = []string{"--raw-control-chars", "--quit-if-one-screen", "--no-init"}
+		args = []string{"--RAW-CONTROL-CHARS", "--quit-if-one-screen", "--no-init"}
 	}
 
 	// invoke pager
diff --git a/go.mod b/go.mod
index d9192c87..6d5f9d88 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,6 @@ require (
 	github.com/posener/complete v1.2.1
 	github.com/spf13/cobra v0.0.5
 	github.com/spf13/pflag v1.0.3
-	github.com/spf13/viper v1.3.2
 	github.com/stretchr/objx v0.2.0
 	github.com/stretchr/testify v1.3.0
 	github.com/thoas/go-funk v0.4.0
diff --git a/pkg/jsonnet/jpath/jpath.go b/pkg/jsonnet/jpath/jpath.go
index 4c12490a..e74bd251 100644
--- a/pkg/jsonnet/jpath/jpath.go
+++ b/pkg/jsonnet/jpath/jpath.go
@@ -9,10 +9,10 @@ import (
 
 var (
 	// ErrorNoRoot means no rootDir was found in the parents
-	ErrorNoRoot = errors.New("could not locate a tkrc.yaml or jsonnetfile.json in the parent directories, which is required to identify the project root. Refer to https://tanka.dev/directory-structure for more information")
+	ErrorNoRoot = errors.New("could not locate a tkrc.yaml or jsonnetfile.json in the parent directories, which is required to identify the project root.\nRefer to https://tanka.dev/directory-structure for more information")
 
 	// ErrorNoBase means no baseDir was found in the parents
-	ErrorNoBase = errors.New("could not locate a main.jsonnet in the parent directories, which is required as the entrypoint for the evaluation. Refer to https://tanka.dev/directory-structure for more information")
+	ErrorNoBase = errors.New("could not locate a main.jsonnet in the parent directories, which is required as the entrypoint for the evaluation.\nRefer to https://tanka.dev/directory-structure for more information")
 )
 
 // ErrorFileNotFound means that the searched file was not found
diff --git a/pkg/kubernetes/client/apply.go b/pkg/kubernetes/client/apply.go
index c40e3ab6..0524352b 100644
--- a/pkg/kubernetes/client/apply.go
+++ b/pkg/kubernetes/client/apply.go
@@ -2,7 +2,6 @@ package client
 
 import (
 	"os"
-	"os/exec"
 	"strings"
 
 	"github.com/grafana/tanka/pkg/kubernetes/manifest"
@@ -23,10 +22,7 @@ func (k Kubectl) Apply(data manifest.List, opts ApplyOpts) error {
 }
 
 func (k Kubectl) apply(data manifest.List, opts ApplyOpts) error {
-	argv := []string{"apply",
-		"--context", k.context.Get("name").MustStr(),
-		"-f", "-",
-	}
+	argv := []string{"-f", "-"}
 	if opts.Force {
 		argv = append(argv, "--force")
 	}
@@ -35,7 +31,7 @@ func (k Kubectl) apply(data manifest.List, opts ApplyOpts) error {
 		argv = append(argv, "--validate=false")
 	}
 
-	cmd := exec.Command("kubectl", argv...)
+	cmd := k.ctl("apply", argv...)
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 
diff --git a/pkg/kubernetes/client/context.go b/pkg/kubernetes/client/context.go
index b13922de..f04bc1b9 100644
--- a/pkg/kubernetes/client/context.go
+++ b/pkg/kubernetes/client/context.go
@@ -4,16 +4,22 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"strings"
 
+	"github.com/pkg/errors"
 	"github.com/stretchr/objx"
 	funk "github.com/thoas/go-funk"
 )
 
-// setupContext uses `kubectl config view` to obtain the KUBECONFIG and extracts the correct context from it
-func (k *Kubectl) setupContext() error {
+// setupContext makes sure the kubectl client is set up to use the correct
+// context for the cluster IP:
+// - find a context that matches the IP
+// - create a patch for it to set the default namespace
+func (k *Kubectl) setupContext(namespace string) error {
 	if k.context != nil {
 		return nil
 	}
@@ -23,9 +29,35 @@ func (k *Kubectl) setupContext() error {
 	if err != nil {
 		return err
 	}
+
+	nsPatch, err := writeNamespacePatch(k.context, namespace)
+	if err != nil {
+		return errors.Wrap(err, "creating $KUBECONFIG patch for default namespace")
+	}
+	k.nsPatch = nsPatch
+
 	return nil
 }
 
+func writeNamespacePatch(context objx.Map, namespace string) (string, error) {
+	context.Set("context.namespace", namespace)
+
+	kubectx := map[string]interface{}{
+		"contexts": []interface{}{context},
+	}
+	out, err := json.Marshal(kubectx)
+	if err != nil {
+		return "", err
+	}
+
+	f := filepath.Join(os.TempDir(), "tk-kubectx-namespace.yaml")
+	if err := ioutil.WriteFile(f, []byte(out), 0644); err != nil {
+		return "", err
+	}
+
+	return f, nil
+}
+
 // Kubeconfig returns the merged $KUBECONFIG of the host
 func Kubeconfig() (map[string]interface{}, error) {
 	cmd := exec.Command("kubectl", "config", "view", "-o", "json")
diff --git a/pkg/kubernetes/client/delete.go b/pkg/kubernetes/client/delete.go
index ace845eb..b9d8ebe9 100644
--- a/pkg/kubernetes/client/delete.go
+++ b/pkg/kubernetes/client/delete.go
@@ -22,10 +22,8 @@ func (k Kubectl) DeleteByLabels(namespace string, labels map[string]interface{},
 }
 
 func (k Kubectl) delete(namespace string, sel []string, opts DeleteOpts) error {
-	argv := append([]string{"delete",
-		"-n", namespace,
-		"--context", k.context.Get("name").MustStr(),
-	}, sel...)
+	argv := append([]string{"-n", namespace}, sel...)
+	k.ctl("delete", argv...)
 
 	if opts.Force {
 		argv = append(argv, "--force")
diff --git a/pkg/kubernetes/client/diff.go b/pkg/kubernetes/client/diff.go
index f0fc4bd8..18d804db 100644
--- a/pkg/kubernetes/client/diff.go
+++ b/pkg/kubernetes/client/diff.go
@@ -19,11 +19,7 @@ func (k Kubectl) DiffServerSide(data manifest.List) (*string, error) {
 	}
 
 	ready, missing := separateMissingNamespace(data, ns)
-	argv := []string{"diff",
-		"--context", k.context.Get("name").MustStr(),
-		"-f", "-",
-	}
-	cmd := exec.Command("kubectl", argv...)
+	cmd := k.ctl("diff", "-f", "-")
 
 	raw := bytes.Buffer{}
 	cmd.Stdout = &raw
@@ -58,7 +54,8 @@ func (k Kubectl) DiffServerSide(data manifest.List) (*string, error) {
 
 func separateMissingNamespace(in manifest.List, exists map[string]bool) (ready, missingNamespace manifest.List) {
 	for _, r := range in {
-		if !exists[r.Metadata().Namespace()] {
+		// namespace does not exist, also ignore implicit default ("")
+		if ns := r.Metadata().Namespace(); ns != "" && !exists[ns] {
 			missingNamespace = append(missingNamespace, r)
 			continue
 		}
diff --git a/pkg/kubernetes/client/exec.go b/pkg/kubernetes/client/exec.go
new file mode 100644
index 00000000..1f6ccc8a
--- /dev/null
+++ b/pkg/kubernetes/client/exec.go
@@ -0,0 +1,67 @@
+package client
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"sort"
+	"strings"
+)
+
+// ctl returns an `exec.Cmd` for `kubectl`. It also forces the correct context
+// and injects our patched $KUBECONFIG for the default namespace.
+func (k Kubectl) ctl(action string, args ...string) *exec.Cmd {
+	// prepare the arguments
+	argv := []string{action,
+		"--context", k.context.Get("name").MustStr(),
+	}
+	argv = append(argv, args...)
+
+	// prepare the cmd
+	cmd := exec.Command("kubectl", argv...)
+	cmd.Env = patchKubeconfig(k.nsPatch, os.Environ())
+
+	return cmd
+}
+
+func patchKubeconfig(file string, e []string) []string {
+	// prepend namespace patch to $KUBECONFIG
+	env := newEnv(e)
+	if _, ok := env["KUBECONFIG"]; !ok {
+		env["KUBECONFIG"] = filepath.Join(homeDir(), ".kube", "config") // kubectl default
+	}
+	env["KUBECONFIG"] = fmt.Sprintf("%s:%s", file, env["KUBECONFIG"])
+
+	return env.render()
+}
+
+// environment is a helper type for manipulating os.Environ() more easily
+type environment map[string]string
+
+func newEnv(e []string) environment {
+	env := make(environment)
+	for _, s := range e {
+		kv := strings.SplitN(s, "=", 2)
+		env[kv[0]] = kv[1]
+	}
+	return env
+}
+
+func (e environment) render() []string {
+	s := make([]string, 0, len(e))
+	for k, v := range e {
+		s = append(s, fmt.Sprintf("%s=%s", k, v))
+	}
+	sort.Strings(s)
+	return s
+}
+
+func homeDir() string {
+	home, err := os.UserHomeDir()
+	// unable to find homedir. Should never happen on the supported os/arch
+	if err != nil {
+		panic("Unable to find your $HOME directory. This should not have ever happened. Please open an issue on https://github.com/grafana/tanka/issues with your OS and ARCH.")
+	}
+	return home
+}
diff --git a/pkg/kubernetes/client/get.go b/pkg/kubernetes/client/get.go
index 0368e426..e5062f2e 100644
--- a/pkg/kubernetes/client/get.go
+++ b/pkg/kubernetes/client/get.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"os/exec"
 	"strings"
 
 	"github.com/grafana/tanka/pkg/kubernetes/manifest"
@@ -41,12 +40,11 @@ func (k Kubectl) GetByLabels(namespace string, labels map[string]interface{}) (m
 }
 
 func (k Kubectl) get(namespace string, sel []string) (manifest.Manifest, error) {
-	argv := append([]string{"get",
+	argv := append([]string{
 		"-o", "json",
 		"-n", namespace,
-		"--context", k.context.Get("name").MustStr(),
 	}, sel...)
-	cmd := exec.Command("kubectl", argv...)
+	cmd := k.ctl("get", argv...)
 
 	var sout, serr bytes.Buffer
 	cmd.Stdout = &sout
diff --git a/pkg/kubernetes/client/kubectl.go b/pkg/kubernetes/client/kubectl.go
index ed67c717..02802fd7 100644
--- a/pkg/kubernetes/client/kubectl.go
+++ b/pkg/kubernetes/client/kubectl.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"os"
-	"os/exec"
 	"regexp"
 
 	"github.com/Masterminds/semver"
@@ -17,17 +16,20 @@ import (
 
 // Kubectl uses the `kubectl` command to operate on a Kubernetes cluster
 type Kubectl struct {
-	context   objx.Map
-	cluster   objx.Map
+	// kubeconfig
+	nsPatch string
+	context objx.Map
+	cluster objx.Map
+
 	APIServer string
 }
 
 // New returns a instance of Kubectl with a correct context already discovered.
-func New(endpoint string) (*Kubectl, error) {
+func New(endpoint, namespace string) (*Kubectl, error) {
 	k := Kubectl{
 		APIServer: endpoint,
 	}
-	if err := k.setupContext(); err != nil {
+	if err := k.setupContext(namespace); err != nil {
 		return nil, errors.Wrap(err, "finding usable context")
 	}
 	return &k, nil
@@ -51,10 +53,7 @@ func (k Kubectl) Info() (*Info, error) {
 
 // Version returns the version of kubectl and the Kubernetes api server
 func (k Kubectl) version() (client, server *semver.Version, err error) {
-	cmd := exec.Command("kubectl", "version",
-		"-o", "json",
-		"--context", k.context.Get("name").MustStr(),
-	)
+	cmd := k.ctl("version", "-o", "json")
 	var buf bytes.Buffer
 	cmd.Stdout = &buf
 	cmd.Stderr = os.Stderr
@@ -69,12 +68,7 @@ func (k Kubectl) version() (client, server *semver.Version, err error) {
 
 // Namespaces of the cluster
 func (k Kubectl) Namespaces() (map[string]bool, error) {
-	argv := []string{"get",
-		"-o", "json",
-		"--context", k.context.Get("name").MustStr(),
-		"namespaces",
-	}
-	cmd := exec.Command("kubectl", argv...)
+	cmd := k.ctl("get", "namespaces", "-o", "json")
 
 	var sout bytes.Buffer
 	cmd.Stdout = &sout
diff --git a/pkg/kubernetes/errors.go b/pkg/kubernetes/errors.go
deleted file mode 100644
index ef7ed701..00000000
--- a/pkg/kubernetes/errors.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package kubernetes
-
-import (
-	"errors"
-)
-
-var (
-	// ErrorMissingConfig means that the `spec.json` is absent
-	ErrorMissingConfig = errors.New("This operation requires additional configuration. Refer to https://tanka.dev/environments for instructions")
-)
diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go
index 19d2a64b..3c17e66b 100644
--- a/pkg/kubernetes/kubernetes.go
+++ b/pkg/kubernetes/kubernetes.go
@@ -33,7 +33,7 @@ type Differ func(manifest.List) (*string, error)
 // New creates a new Kubernetes with an initialized client
 func New(s v1alpha1.Spec) (*Kubernetes, error) {
 	// setup client
-	ctl, err := client.New(s.APIServer)
+	ctl, err := client.New(s.APIServer, s.Namespace)
 	if err != nil {
 		return nil, errors.Wrap(err, "creating client")
 	}
diff --git a/pkg/kubernetes/reconcile.go b/pkg/kubernetes/reconcile.go
index 9de42cdc..ffc0d6f3 100644
--- a/pkg/kubernetes/reconcile.go
+++ b/pkg/kubernetes/reconcile.go
@@ -29,10 +29,6 @@ func Reconcile(raw map[string]interface{}, spec v1alpha1.Spec, targets []*regexp
 
 	out := make(manifest.List, 0, len(extracted))
 	for _, m := range extracted {
-		if spec.Namespace != "" && !m.Metadata().HasNamespace() {
-			m.Metadata()["namespace"] = spec.Namespace
-		}
-
 		out = append(out, m)
 	}
 
diff --git a/pkg/kubernetes/util/targetexp.go b/pkg/kubernetes/util/targetexp.go
index 79f1d5c9..d266b43d 100644
--- a/pkg/kubernetes/util/targetexp.go
+++ b/pkg/kubernetes/util/targetexp.go
@@ -11,7 +11,7 @@ type ErrBadTargetExp struct {
 }
 
 func (e ErrBadTargetExp) Error() string {
-	return fmt.Sprintf("%s.\nSee https://tanka.dev/targets/#regular-expressions for details on regular expressions.", strings.Title(e.inner.Error()))
+	return fmt.Sprintf("%s.\nSee https://tanka.dev/output-filtering/#regular-expressions for details on regular expressions.", strings.Title(e.inner.Error()))
 }
 
 // CompileTargetExps compiles the regular expression for each target
diff --git a/pkg/spec/depreciations.go b/pkg/spec/depreciations.go
deleted file mode 100644
index 32c7fa8a..00000000
--- a/pkg/spec/depreciations.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package spec
-
-import "fmt"
-
-type depreciation struct {
-	old, new string
-}
-
-// ErrDeprecated is a non-fatal error that occurs when deprecated fields are
-// used in the spec.json
-type ErrDeprecated []depreciation
-
-func (e ErrDeprecated) Error() string {
-	buf := ""
-	for _, d := range e {
-		buf += fmt.Sprintf("Warning: `%s` is deprecated, use `%s` instead.\n", d.old, d.new)
-	}
-	return buf
-}
diff --git a/pkg/spec/errors.go b/pkg/spec/errors.go
new file mode 100644
index 00000000..27aa6c5c
--- /dev/null
+++ b/pkg/spec/errors.go
@@ -0,0 +1,36 @@
+package spec
+
+import "fmt"
+
+type depreciation struct {
+	old, new string
+}
+
+// ErrDeprecated is a non-fatal error that occurs when deprecated fields are
+// used in the spec.json
+type ErrDeprecated []depreciation
+
+func (e ErrDeprecated) Error() string {
+	buf := ""
+	for _, d := range e {
+		buf += fmt.Sprintf("Warning: `%s` is deprecated, use `%s` instead.\n", d.old, d.new)
+	}
+	return buf
+}
+
+type ErrMistypedField struct {
+	name string
+	t    interface{}
+}
+
+func (e ErrMistypedField) Error() string {
+	return fmt.Sprintf("`%s` is of type %T but should be string", e.name, e.t)
+}
+
+type ErrNoSpec struct {
+	name string
+}
+
+func (e ErrNoSpec) Error() string {
+	return fmt.Sprintf("unable to find a spec.json for environment `%s`.\nRefer to https://tanka.dev/directory-structure#environments for instructions", e.name)
+}
diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go
index 7efad898..61523bda 100644
--- a/pkg/spec/spec.go
+++ b/pkg/spec/spec.go
@@ -1,35 +1,18 @@
 package spec
 
 import (
-	"bytes"
+	"encoding/json"
+	"io/ioutil"
 	"os"
+	"path/filepath"
 
 	"github.com/pkg/errors"
-	"github.com/spf13/viper"
 
 	"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{
-	{old: "namespace", new: "spec.namespace"},
-	{old: "server", new: "spec.apiServer"},
-	{old: "team", new: "metadata.labels.team"},
-}
-
-// Parse parses the json `data` into a `v1alpha1.Config` object.
-// `name` is the name of the environment
-func Parse(data []byte, name string) (*v1alpha1.Config, error) {
-	v := viper.New()
-	v.SetConfigType("json")
-	if err := v.ReadConfig(bytes.NewReader(data)); err != nil {
-		return nil, err
-	}
-	return parse(v, name)
-}
+const Specfile = "spec.json"
 
 // ParseDir parses the given environments `spec.json` into a `v1alpha1.Config`
 // object with the name set to the directories name
@@ -42,42 +25,79 @@ func ParseDir(baseDir, name string) (*v1alpha1.Config, error) {
 		return nil, errors.New("baseDir is not an directory")
 	}
 
-	v := viper.New()
-	v.SetConfigName("spec")
-	v.AddConfigPath(baseDir)
-
-	if err := v.ReadInConfig(); err != nil {
+	data, err := ioutil.ReadFile(filepath.Join(baseDir, Specfile))
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, ErrNoSpec{name}
+		}
 		return nil, err
 	}
 
-	return parse(v, name)
+	return Parse(data, name)
 }
 
-// parse accepts a viper.Viper already loaded with the actual config and
-// unmarshals it onto a v1alpha1.Config
-func parse(v *viper.Viper, name string) (*v1alpha1.Config, error) {
+// Parse parses the json `data` into a `v1alpha1.Config` object.
+// `name` is the name of the environment
+func Parse(data []byte, name string) (*v1alpha1.Config, error) {
+	config := v1alpha1.New()
+	if err := json.Unmarshal(data, config); err != nil {
+		return nil, errors.Wrap(err, "parsing spec.json")
+	}
+
+	// set the name field
+	config.Metadata.Name = name
+
+	if err := handleDeprecated(config, data); err != nil {
+		return config, err
+	}
+	return config, nil
+}
+
+func handleDeprecated(c *v1alpha1.Config, data []byte) error {
 	var errDepr ErrDeprecated
 
-	// handle deprecated ksonnet spec
-	for _, d := range deprecated {
-		if v.IsSet(d.old) && !v.IsSet(d.new) {
-			errDepr = append(errDepr, d)
-			v.Set(d.new, v.Get(d.old))
+	var msi map[string]interface{}
+	if err := json.Unmarshal(data, &msi); err != nil {
+		return err
+	}
+
+	// namespace -> spec.namespace
+	if n, ok := msi["namespace"]; ok && c.Spec.Namespace == "" {
+		n, ok := n.(string)
+		if !ok {
+			return ErrMistypedField{"namespace", n}
 		}
+
+		errDepr = append(errDepr, depreciation{"namespace", "spec.namespace"})
+		c.Spec.Namespace = n
 	}
 
-	config := v1alpha1.New()
-	if err := v.Unmarshal(config); err != nil {
-		return nil, errors.Wrap(err, "parsing spec.json")
+	// server -> spec.apiServer
+	if s, ok := msi["server"]; ok && c.Spec.APIServer == "" {
+		s, ok := s.(string)
+		if !ok {
+			return ErrMistypedField{"server", s}
+		}
+
+		errDepr = append(errDepr, depreciation{"server", "spec.apiServer"})
+		c.Spec.APIServer = s
 	}
 
-	// set the name field
-	config.Metadata.Name = name
+	// team -> metadata.labels.team
+	_, hasTeam := c.Metadata.Labels["team"]
+	if t, ok := msi["team"]; ok && !hasTeam {
+		t, ok := t.(string)
+		if !ok {
+			return ErrMistypedField{"team", t}
+		}
+
+		errDepr = append(errDepr, depreciation{"team", "metadata.labels.team"})
+		c.Metadata.Labels["team"] = t
+	}
 
-	if len(errDepr) == 0 {
-		return config, nil
+	if len(errDepr) != 0 {
+		return errDepr
 	}
 
-	// return depreciation notes in case any exist as well
-	return config, errDepr
+	return nil
 }
diff --git a/pkg/tanka/parse.go b/pkg/tanka/parse.go
index c1d48c99..d141f140 100644
--- a/pkg/tanka/parse.go
+++ b/pkg/tanka/parse.go
@@ -6,7 +6,6 @@ import (
 	"path/filepath"
 
 	"github.com/pkg/errors"
-	"github.com/spf13/viper"
 
 	"github.com/grafana/tanka/pkg/jsonnet"
 	"github.com/grafana/tanka/pkg/jsonnet/jpath"
@@ -88,9 +87,6 @@ func parseEnv(baseDir, rootDir string) (*v1alpha1.Config, error) {
 	config, err := spec.ParseDir(baseDir, name)
 	if err != nil {
 		switch err.(type) {
-		// config is missing
-		case viper.ConfigFileNotFoundError:
-			return nil, kubernetes.ErrorMissingConfig
 		// the config includes deprecated fields
 		case spec.ErrDeprecated:
 			log.Println(err)
diff --git a/pkg/tanka/tanka.go b/pkg/tanka/tanka.go
index 95c24b14..b9c8d822 100644
--- a/pkg/tanka/tanka.go
+++ b/pkg/tanka/tanka.go
@@ -45,7 +45,7 @@ func WithExtCode(code map[string]string) Modifier {
 }
 
 // WithTargets allows to submit regular expressions to limit the working set of
-// objects (https://tanka.dev/targets/).
+// objects (https://tanka.dev/output-filtering/).
 func WithTargets(t ...*regexp.Regexp) Modifier {
 	return func(opts *options) {
 		opts.targets = t


Test patch:
diff --git a/pkg/kubernetes/client/diff_test.go b/pkg/kubernetes/client/diff_test.go
new file mode 100644
index 00000000..34a66f98
--- /dev/null
+++ b/pkg/kubernetes/client/diff_test.go
@@ -0,0 +1,91 @@
+package client
+
+import (
+	"testing"
+
+	"github.com/grafana/tanka/pkg/kubernetes/manifest"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSeparateMissingNamespace(t *testing.T) {
+	cases := []struct {
+		name string
+		td   nsTd
+
+		missing bool
+	}{
+		// default should always exist
+		{
+			name: "default",
+			td: newNsTd(func(m manifest.Metadata) {
+				m["namespace"] = "default"
+			}, []string{}),
+			missing: false,
+		},
+		// implcit default (not specfiying an ns at all) also
+		{
+			name: "implicit-default",
+			td: newNsTd(func(m manifest.Metadata) {
+				delete(m, "namespace")
+			}, []string{}),
+			missing: false,
+		},
+		// custom ns that exists
+		{
+			name: "custom-ns",
+			td: newNsTd(func(m manifest.Metadata) {
+				m["namespace"] = "custom"
+			}, []string{"custom"}),
+			missing: false,
+		},
+		// custom ns that does not exist
+		{
+			name: "missing-ns",
+			td: newNsTd(func(m manifest.Metadata) {
+				m["namespace"] = "missing"
+			}, []string{}),
+			missing: true,
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			ready, missing := separateMissingNamespace(manifest.List{c.td.m}, c.td.ns)
+			if c.missing {
+				assert.Lenf(t, ready, 0, "expected manifest to be missing (ready = 0)")
+				assert.Lenf(t, missing, 1, "expected manifest to be missing (missing = 1)")
+			} else {
+				assert.Lenf(t, ready, 1, "expected manifest to be ready (ready = 1)")
+				assert.Lenf(t, missing, 0, "expected manifest to be ready (missing = 0)")
+			}
+		})
+	}
+}
+
+type nsTd struct {
+	m  manifest.Manifest
+	ns map[string]bool
+}
+
+func newNsTd(f func(m manifest.Metadata), ns []string) nsTd {
+	m := manifest.Manifest{
+		"apiVersion": "apps/v1",
+		"kind":       "Deployment",
+		"metadata":   map[string]interface{}{},
+	}
+	if f != nil {
+		f(m.Metadata())
+	}
+
+	nsMap := map[string]bool{
+		"default": true, // you can't get rid of this one ever
+	}
+	for _, n := range ns {
+		nsMap[n] = true
+	}
+
+	return nsTd{
+		m:  m,
+		ns: nsMap,
+	}
+}
diff --git a/pkg/kubernetes/client/exec_test.go b/pkg/kubernetes/client/exec_test.go
new file mode 100644
index 00000000..7d0aba3d
--- /dev/null
+++ b/pkg/kubernetes/client/exec_test.go
@@ -0,0 +1,40 @@
+package client
+
+import (
+	"fmt"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const patchFile = "/tmp/tk-nsPatch.yaml"
+
+func TestPatchKubeconfig(t *testing.T) {
+
+	cases := []struct {
+		name string
+		env  []string
+		want []string
+	}{
+		{
+			name: "none",
+			env:  []string{},
+			want: []string{
+				fmt.Sprintf("KUBECONFIG=%s:%s", patchFile, filepath.Join(homeDir(), ".kube", "config")),
+			},
+		},
+		{
+			name: "custom",
+			env:  []string{"KUBECONFIG=/home/user/.config/kube"},
+			want: []string{"KUBECONFIG=" + patchFile + ":/home/user/.config/kube"},
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			got := patchKubeconfig(patchFile, c.env)
+			assert.Equal(t, c.want, got)
+		})
+	}
+}
diff --git a/pkg/kubernetes/kubernetes_test.go b/pkg/kubernetes/kubernetes_test.go
index 89e6519c..a8641317 100644
--- a/pkg/kubernetes/kubernetes_test.go
+++ b/pkg/kubernetes/kubernetes_test.go
@@ -4,7 +4,6 @@ import (
 	"regexp"
 	"testing"
 
-	"github.com/stretchr/objx"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
@@ -60,30 +59,6 @@ func TestReconcile(t *testing.T) {
 				`DePlOyMeNt/GrAfAnA`,
 			),
 		},
-		{
-			name: "force-namespace",
-			spec: v1alpha1.Spec{Namespace: "tanka"},
-			deep: testDataFlat().Deep,
-			flat: func() manifest.List {
-				f := testDataFlat().Flat["."]
-				f.Metadata()["namespace"] = "tanka"
-				return manifest.List{f}
-			}(),
-		},
-		{
-			name: "custom-namespace",
-			spec: v1alpha1.Spec{Namespace: "tanka"},
-			deep: func() map[string]interface{} {
-				d := objx.New(testDataFlat().Deep)
-				d.Set("metadata.namespace", "custom")
-				return d
-			}(),
-			flat: func() manifest.List {
-				f := testDataFlat().Flat["."]
-				f.Metadata()["namespace"] = "custom"
-				return manifest.List{f}
-			}(),
-		},
 	}
 
 	for _, c := range tests {
diff --git a/pkg/spec/depreciations_test.go b/pkg/spec/depreciations_test.go
index 8e9e6d24..88621db4 100644
--- a/pkg/spec/depreciations_test.go
+++ b/pkg/spec/depreciations_test.go
@@ -13,14 +13,15 @@ import (
 // overwritten by the newer format.
 func TestDeprecated(t *testing.T) {
 	data := []byte(`
-	{
-      "spec": {
-        "namespace": "new"
-      },
-	  "server": "https://127.0.0.1",
-	  "team": "cool",
-      "namespace": "old"
-	}`)
+{
+	"spec": {
+		"namespace": "new"
+	},
+	"server": "https://127.0.0.1",
+	"team": "cool",
+	"namespace": "old"
+}
+`)
 
 	got, err := Parse(data, "test")
 	require.Equal(t, ErrDeprecated{