Repository: grafana/tanka
Pull request #435 (2020-12-04 20:01:57)

feat: add metadata.Namespace
This PR sets `metadata.Namespace` at runtime, the path relative to the root. This removes the assumption that name equals
path and also shows up in tk env list --json, which can in turn be used by other tools that want to perform bulk actions
on Tanka environments.

Pulled out of #433 as I think this is useful in the context of inline environments while the pruneMark is way more
controversial.


Patch:
diff --git a/Dockerfile b/Dockerfile
index bf58b8cc..d1ca901e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,11 +13,15 @@ RUN git clone https://github.com/jsonnet-bundler/jsonnet-bundler &&\
     make static &&\
     mv _output/jb /usr/local/bin/jb
 
+FROM alpine as helm
+RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing helm
+
 # assemble final container
 FROM alpine
 RUN apk add --no-cache coreutils diffutils less git openssh-client
 COPY tk /usr/local/bin/tk
 COPY --from=kubectl /usr/local/bin/kubectl /usr/local/bin/kubectl
 COPY --from=jb /usr/local/bin/jb /usr/local/bin/jb
+COPY --from=helm /usr/bin/helm /usr/local/bin/helm
 WORKDIR /app
 ENTRYPOINT ["/usr/local/bin/tk"]
diff --git a/cmd/tk/tool.go b/cmd/tk/tool.go
index 6bf1bc44..84b1fc3f 100644
--- a/cmd/tk/tool.go
+++ b/cmd/tk/tool.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -30,29 +31,40 @@ func toolCmd() *cli.Command {
 
 func jpathCmd() *cli.Command {
 	cmd := &cli.Command{
-		Short: "print information about the jpath",
-		Use:   "jpath",
-		Run: func(cmd *cli.Command, args []string) error {
-			pwd, err := os.Getwd()
-			if err != nil {
-				return err
-			}
-			path, base, root, err := jpath.Resolve(pwd)
-			if err != nil {
-				return fmt.Errorf("Resolving JPATH: %s", err)
-			}
-			entrypoint, err := jpath.Entrypoint(base)
-			if err != nil {
-				return fmt.Errorf("Resolving JPATH: %s", err)
-			}
-			fmt.Println("main:", entrypoint)
-			fmt.Println("rootDir:", root)
-			fmt.Println("baseDir:", base)
-			fmt.Println("jpath:", path)
+		Short: "export JSONNET_PATH for use with other jsonnet tools",
+		Use:   "jpath [<file/dir>]",
+		Args:  workflowArgs,
+	}
+
+	debug := cmd.Flags().BoolP("debug", "d", false, "show debug info")
+
+	cmd.Run = func(cmd *cli.Command, args []string) error {
+		path := args[0]
 
-			return nil
-		},
+		entrypoint, err := jpath.Entrypoint(path)
+		if err != nil {
+			return fmt.Errorf("Resolving JPATH: %s", err)
+		}
+
+		jsonnetpath, base, root, err := jpath.Resolve(entrypoint)
+		if err != nil {
+			return fmt.Errorf("Resolving JPATH: %s", err)
+		}
+
+		if *debug {
+			// log to debug info to stderr
+			log.Println("main:", entrypoint)
+			log.Println("rootDir:", root)
+			log.Println("baseDir:", base)
+			log.Println("jpath:", jsonnetpath)
+		}
+
+		// print export JSONNET_PATH to stdout
+		fmt.Printf("%s", strings.Join(jsonnetpath, ":"))
+
+		return nil
 	}
+
 	return cmd
 }
 
diff --git a/pkg/helm/charts.go b/pkg/helm/charts.go
index 0876349b..341e467f 100644
--- a/pkg/helm/charts.go
+++ b/pkg/helm/charts.go
@@ -68,6 +68,11 @@ type Charts struct {
 	Helm Helm
 }
 
+// chartManifest represents a Helm chart's Chart.yaml
+type chartManifest struct {
+	Version string `yaml:"version"`
+}
+
 // ChartDir returns the directory pulled charts are saved in
 func (c Charts) ChartDir() string {
 	return filepath.Join(c.projectRoot, c.Manifest.Directory)
@@ -95,12 +100,33 @@ func (c Charts) Vendor() error {
 	for _, r := range c.Manifest.Requires {
 		chartName := parseReqName(r.Chart)
 		chartPath := filepath.Join(dir, chartName)
-		if _, err := os.Stat(chartPath); err == nil {
-			log.Printf(" %s@%s exists", r.Chart, r.Version.String())
-			continue
+
+		_, err := os.Stat(chartPath)
+		if err == nil {
+			chartManifestPath := filepath.Join(chartPath, "Chart.yaml")
+			chartManifestBytes, err := ioutil.ReadFile(chartManifestPath)
+			if err != nil {
+				return fmt.Errorf("reading chart manifest: %w", err)
+			}
+			var chartYAML chartManifest
+			if err := yaml.Unmarshal(chartManifestBytes, &chartYAML); err != nil {
+				return fmt.Errorf("unmarshalling chart manifest: %w", err)
+			}
+
+			if chartYAML.Version == r.Version.String() {
+				log.Printf(" %s@%s exists", r.Chart, r.Version.String())
+				continue
+			} else {
+				log.Printf("Removing %s@%s", r.Chart, r.Version.String())
+				if err := os.RemoveAll(chartPath); err != nil {
+					return err
+				}
+			}
+		} else if !os.IsNotExist(err) {
+			return err
 		}
 
-		err := c.Helm.Pull(r.Chart, r.Version.String(), PullOpts{
+		err = c.Helm.Pull(r.Chart, r.Version.String(), PullOpts{
 			Destination: dir,
 			Opts:        Opts{Repositories: c.Manifest.Repositories},
 		})
diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go
index 93171e8b..2d0185f8 100644
--- a/pkg/spec/spec.go
+++ b/pkg/spec/spec.go
@@ -20,7 +20,7 @@ const Specfile = "spec.json"
 
 // ParseDir parses the given environments `spec.json` into a `v1alpha1.Environment`
 // object with the name set to the directories name
-func ParseDir(baseDir, name string) (*v1alpha1.Environment, error) {
+func ParseDir(baseDir, path string) (*v1alpha1.Environment, error) {
 	fi, err := os.Stat(baseDir)
 	if err != nil {
 		return nil, err
@@ -33,23 +33,24 @@ func ParseDir(baseDir, name string) (*v1alpha1.Environment, error) {
 	if err != nil {
 		if os.IsNotExist(err) {
 			c := v1alpha1.New()
-			c.Metadata.Name = name
-			return c, ErrNoSpec{name}
+			c.Metadata.Name = path // legacy behavior
+			c.Metadata.Namespace = path
+			return c, ErrNoSpec{path}
 		}
 		return nil, err
 	}
 
-	c, err := Parse(data)
+	c, err := Parse(data, path)
 	if c != nil {
 		// set the name field
-		c.Metadata.Name = name
+		c.Metadata.Name = path // legacy behavior
 	}
 
 	return c, err
 }
 
 // Parse parses the json `data` into a `v1alpha1.Environment` object.
-func Parse(data []byte) (*v1alpha1.Environment, error) {
+func Parse(data []byte, path string) (*v1alpha1.Environment, error) {
 	config := v1alpha1.New()
 	if err := json.Unmarshal(data, config); err != nil {
 		return nil, errors.Wrap(err, "parsing spec.json")
@@ -64,6 +65,8 @@ func Parse(data []byte) (*v1alpha1.Environment, error) {
 		config.Spec.APIServer = "https://" + config.Spec.APIServer
 	}
 
+	config.Metadata.Namespace = path
+
 	return config, nil
 }
 
diff --git a/pkg/spec/v1alpha1/environment.go b/pkg/spec/v1alpha1/environment.go
index e6031ed0..413676ed 100644
--- a/pkg/spec/v1alpha1/environment.go
+++ b/pkg/spec/v1alpha1/environment.go
@@ -29,8 +29,9 @@ type Environment struct {
 
 // Metadata is meant for humans and not parsed
 type Metadata struct {
-	Name   string            `json:"name,omitempty"`
-	Labels map[string]string `json:"labels,omitempty"`
+	Name      string            `json:"name,omitempty"`
+	Namespace string            `json:"namespace,omitempty"`
+	Labels    map[string]string `json:"labels,omitempty"`
 }
 
 // Has and Get make Metadata a simple wrapper for labels.Labels to use our map in their querier
diff --git a/pkg/tanka/parse.go b/pkg/tanka/parse.go
index c79a85ea..30808003 100644
--- a/pkg/tanka/parse.go
+++ b/pkg/tanka/parse.go
@@ -184,7 +184,11 @@ func ParseEnv(path string, opts ParseOpts) (interface{}, *v1alpha1.Environment,
 		if err != nil {
 			return nil, nil, err
 		}
-		env, err = spec.Parse(marshalled)
+		relpath, err := relativeEntrypoint(path)
+		if err != nil {
+			return nil, nil, err
+		}
+		env, err = spec.Parse(marshalled, relpath)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -244,3 +248,16 @@ func extractEnvironments(data interface{}) (manifest.List, error) {
 	// Extract only object of Kind: Environment
 	return process.Filter(out, process.MustStrExps("Environment/.*")), nil
 }
+
+func relativeEntrypoint(path string) (string, error) {
+	entrypoint, err := jpath.Entrypoint(path)
+	if err != nil {
+		return "", err
+	}
+	_, _, rootDir, err := jpath.Resolve(entrypoint)
+	if err != nil {
+		return "", errors.Wrap(err, "resolving jpath")
+	}
+
+	return filepath.Rel(rootDir, entrypoint)
+}


Test patch:
diff --git a/pkg/spec/depreciations_test.go b/pkg/spec/depreciations_test.go
index 2644a0ff..374afb60 100644
--- a/pkg/spec/depreciations_test.go
+++ b/pkg/spec/depreciations_test.go
@@ -26,7 +26,7 @@ func TestDeprecated(t *testing.T) {
 }
 `)
 
-	got, err := Parse(data)
+	got, err := Parse(data, "test")
 	require.Equal(t, ErrDeprecated{
 		{old: "server", new: "spec.apiServer"},
 		{old: "team", new: "metadata.labels.team"},
diff --git a/pkg/tanka/parse_test.go b/pkg/tanka/parse_test.go
index 520a484d..4145d298 100644
--- a/pkg/tanka/parse_test.go
+++ b/pkg/tanka/parse_test.go
@@ -43,8 +43,9 @@ func TestEvalJsonnet(t *testing.T) {
 				APIVersion: v1alpha1.New().APIVersion,
 				Kind:       v1alpha1.New().Kind,
 				Metadata: v1alpha1.Metadata{
-					Name:   "cases/withspecjson",
-					Labels: v1alpha1.New().Metadata.Labels,
+					Name:      "cases/withspecjson",
+					Namespace: "cases/withspecjson",
+					Labels:    v1alpha1.New().Metadata.Labels,
 				},
 				Spec: v1alpha1.Spec{
 					APIServer: "https://localhost",
@@ -64,8 +65,9 @@ func TestEvalJsonnet(t *testing.T) {
 				APIVersion: v1alpha1.New().APIVersion,
 				Kind:       v1alpha1.New().Kind,
 				Metadata: v1alpha1.Metadata{
-					Name:   "cases/withspecjson",
-					Labels: v1alpha1.New().Metadata.Labels,
+					Name:      "cases/withspecjson",
+					Namespace: "cases/withspecjson",
+					Labels:    v1alpha1.New().Metadata.Labels,
 				},
 				Spec: v1alpha1.Spec{
 					APIServer: "https://localhost",
@@ -96,8 +98,9 @@ func TestEvalJsonnet(t *testing.T) {
 				APIVersion: v1alpha1.New().APIVersion,
 				Kind:       v1alpha1.New().Kind,
 				Metadata: v1alpha1.Metadata{
-					Name:   "withenv",
-					Labels: v1alpha1.New().Metadata.Labels,
+					Name:      "withenv",
+					Namespace: "cases/withenv/main.jsonnet",
+					Labels:    v1alpha1.New().Metadata.Labels,
 				},
 				Spec: v1alpha1.Spec{
 					APIServer: "https://localhost",