Repository: grafana/tanka
Pull request #879 (2023-06-12 09:02:55)

Bump golang from 1.20.4-alpine to 1.20.5-alpine
Bumps golang from 1.20.4-alpine to 1.20.5-alpine.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang&package-manager=docker&previous-version=1.20.4-alpine&new-version=1.20.5-alpine)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

github-actions[bot]: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.4.4
:---:
:rocket: Deployed preview to https://grafana.github.io/tanka/pr-preview/pr-879/
on branch [`gh-pages`](https://github.com/grafana/tanka/tree/gh-pages) at 2023-06-12 09:06 UTC
<!-- Sticky Pull Request Commentpr-preview -->

Patch:
diff --git a/Dockerfile b/Dockerfile
index bfa7c250..b26b15c3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # download kubectl
-FROM golang:1.20.4-alpine as kubectl
+FROM golang:1.20.5-alpine as kubectl
 RUN apk add --no-cache curl
 RUN export VERSION=$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt) &&\
     export OS=$(go env GOOS) && \
@@ -8,7 +8,7 @@ RUN export VERSION=$(curl -s https://storage.googleapis.com/kubernetes-release/r
     chmod +x /usr/local/bin/kubectl
 
 # build jsonnet-bundler
-FROM golang:1.20.4-alpine as jb
+FROM golang:1.20.5-alpine as jb
 WORKDIR /tmp
 RUN apk add --no-cache git make bash &&\
     git clone https://github.com/jsonnet-bundler/jsonnet-bundler &&\
@@ -17,7 +17,7 @@ RUN apk add --no-cache git make bash &&\
     make static &&\
     mv _output/jb /usr/local/bin/jb
 
-FROM golang:1.20.4-alpine as helm
+FROM golang:1.20.5-alpine as helm
 WORKDIR /tmp/helm
 RUN apk add --no-cache jq curl
 RUN export TAG=$(curl --silent "https://api.github.com/repos/helm/helm/releases/latest" | jq -r .tag_name) &&\
@@ -26,7 +26,7 @@ RUN export TAG=$(curl --silent "https://api.github.com/repos/helm/helm/releases/
     curl -SL "https://get.helm.sh/helm-${TAG}-${OS}-${ARCH}.tar.gz" > helm.tgz && \
     tar -xvf helm.tgz --strip-components=1
 
-FROM golang:1.20.4-alpine as kustomize
+FROM golang:1.20.5-alpine as kustomize
 WORKDIR /tmp/kustomize
 RUN apk add --no-cache jq curl
 # Get the latest version of kustomize
diff --git a/cmd/tk/export.go b/cmd/tk/export.go
index 0b68fc92..e6a0e896 100644
--- a/cmd/tk/export.go
+++ b/cmd/tk/export.go
@@ -40,6 +40,7 @@ func exportCmd() *cli.Command {
 		panic(err)
 	}
 	mergeStrategy := cmd.Flags().String("merge-strategy", "", "What to do when exporting to an existing directory. The default setting is to disallow exporting to an existing directory. Values: 'fail-on-conflicts', 'replace-envs'")
+	mergeDeletedEnvs := cmd.Flags().StringArray("merge-deleted-envs", nil, "Tanka main files that have been deleted. This is used when using a merge strategy to also delete the files of these deleted environments.")
 
 	vars := workflowFlags(cmd.Flags())
 	getJsonnetOpts := jsonnetFlags(cmd.Flags())
@@ -65,8 +66,9 @@ func exportCmd() *cli.Command {
 				Filters:     filters,
 				Name:        vars.name,
 			},
-			Selector:    getLabelSelector(),
-			Parallelism: *parallel,
+			Selector:         getLabelSelector(),
+			Parallelism:      *parallel,
+			MergeDeletedEnvs: *mergeDeletedEnvs,
 		}
 
 		if opts.MergeStrategy, err = determineMergeStrategy(*merge, *mergeStrategy); err != nil {
diff --git a/cmd/tk/tool.go b/cmd/tk/tool.go
index 1e15e8d7..9ed56e65 100644
--- a/cmd/tk/tool.go
+++ b/cmd/tk/tool.go
@@ -137,9 +137,10 @@ func importsCmd() *cli.Command {
 
 func importersCmd() *cli.Command {
 	cmd := &cli.Command{
-		Use:   "importers <file> <file...>",
+		Use:   "importers <file> <file...> <deleted:file...>",
 		Short: "list all environments that either directly or transitively import the given files",
 		Long: `list all environments that either directly or transitively import the given files
+If the file being looked up was deleted, it should be prefixed with "deleted:".
 
 As optimization,
 if the file is not a vendored (located at <tk-root>/vendor/) or a lib file (located at <tk-root>/lib/), we assume:
@@ -162,6 +163,9 @@ if the file is not a vendored (located at <tk-root>/vendor/) or a lib file (loca
 		}
 
 		for _, f := range args {
+			if strings.HasPrefix(f, "deleted:") {
+				continue
+			}
 			if _, err := os.Stat(f); os.IsNotExist(err) {
 				return fmt.Errorf("file %q does not exist", f)
 			}
diff --git a/docs/docs/exporting.md b/docs/docs/exporting.md
index 0d90f304..076a1d34 100644
--- a/docs/docs/exporting.md
+++ b/docs/docs/exporting.md
@@ -113,6 +113,31 @@ When these flags are passed, Tanka will:
 2. Generate the manifests for the targeted environments into the output directory.
 3. Add in the new manifests entries into the `manifest.json` file and re-export it.
 
+#### Finding out which environments to export
+
+Tanka provides the `tk tool importers` command to figure out which `main.jsonnet` need to be re-exported based on what files were modified in a workspace.
+
+If, for example, the `lib/my-lib/main.libsonnet` file was modified, you could run the command like this to find which files to export:
+
+```console
+# Find out which envs to re-export
+$ tk tool importers lib/my-lib/main.libsonnet
+my-repo-path/jsonnet/environments/my-env/main.jsonnet
+
+# Re-export the envs
+$ tk export myoutputdir my-repo-path/jsonnet/environments/my-env/main.jsonnet --merge-strategy=replace-envs
+```
+
+Note that deleted environments need special consideration when doing this.
+The `tk tool importers` utility only works with existing files so deleting an environment will result in stale `manifest.json` entries and moving an environment will result in manifest conflicts.
+In order to correctly handle deleted environments, they need to be passed to the export command:
+
+```console
+$ tk export myoutputdir my-repo-path/jsonnet/environments/my-new-env-path/main.jsonnet --merge-strategy=replace-envs \
+  --merge-deleted-envs my-repo-path/jsonnet/environments/my-old-env-path/main.jsonnet \
+  --merge-deleted-envs my-repo-path/jsonnet/environments/other-deleted-env-path/main.jsonnet
+```
+
 ### Using a memory ballast
 
 _Read [this blog post](https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/) for more information about memory ballasts._
diff --git a/docs/docs/jsonnet/native-functions.md b/docs/docs/jsonnet/native-functions.md
index 38ed6dfb..da7eb4b9 100644
--- a/docs/docs/jsonnet/native-functions.md
+++ b/docs/docs/jsonnet/native-functions.md
@@ -18,6 +18,32 @@ To use them in your code, you need to access them using `std.native` from the st
 
 `std.native` takes the native function's name as a `string` argument and returns a `function`, which is called using the second set of parentheses.
 
+## sha256
+
+### Signature
+
+```ts
+sha256(string str) string
+```
+
+`sha256` computes the SHA256 sum of the given string.
+
+### Examples
+
+```jsonnet
+{
+  sum: std.native('sha256')('Hello, World!'),
+}
+```
+
+Evaluating with Tanka results in the JSON:
+
+```json
+{
+  "sum": "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f"
+}
+```
+
 ## parseJson
 
 ### Signature
diff --git a/go.mod b/go.mod
index 38c980e2..6a4fdecd 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,7 @@ require (
 	golang.org/x/text v0.9.0
 	gopkg.in/yaml.v2 v2.4.0
 	gopkg.in/yaml.v3 v3.0.1
-	k8s.io/apimachinery v0.27.2
+	k8s.io/apimachinery v0.27.3
 	k8s.io/utils v0.0.0-20230209194617-a36077c30491
 	sigs.k8s.io/yaml v1.3.0
 )
diff --git a/go.sum b/go.sum
index 873aa2c5..d7160b89 100644
--- a/go.sum
+++ b/go.sum
@@ -133,8 +133,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/apimachinery v0.27.2 h1:vBjGaKKieaIreI+oQwELalVG4d8f3YAMNpWLzDXkxeg=
-k8s.io/apimachinery v0.27.2/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
+k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM=
+k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
 k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
 k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
 k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
diff --git a/pkg/jsonnet/find_importers.go b/pkg/jsonnet/find_importers.go
index 0ca17101..14e41d96 100644
--- a/pkg/jsonnet/find_importers.go
+++ b/pkg/jsonnet/find_importers.go
@@ -1,6 +1,7 @@
 package jsonnet
 
 import (
+	"fmt"
 	"os"
 	"path/filepath"
 	"sort"
@@ -34,12 +35,33 @@ func FindImporterForFiles(root string, files []string) ([]string, error) {
 
 	importers := map[string]struct{}{}
 
-	if files, err = expandSymlinksInFiles(root, files); err != nil {
+	// Handle files prefixed with `deleted:`. They need to be made absolute and we shouldn't try to find symlinks for them
+	var filesToCheck, existingFiles []string
+	for _, file := range files {
+		if strings.HasPrefix(file, "deleted:") {
+			deletedFile := strings.TrimPrefix(file, "deleted:")
+			// Try with both the absolute path and the path relative to the root
+			if !filepath.IsAbs(deletedFile) {
+				absFilePath, err := filepath.Abs(deletedFile)
+				if err != nil {
+					return nil, err
+				}
+				filesToCheck = append(filesToCheck, absFilePath)
+				filesToCheck = append(filesToCheck, filepath.Clean(filepath.Join(root, deletedFile)))
+			}
+			continue
+		}
+
+		existingFiles = append(existingFiles, file)
+	}
+
+	if existingFiles, err = expandSymlinksInFiles(root, existingFiles); err != nil {
 		return nil, err
 	}
+	filesToCheck = append(filesToCheck, existingFiles...)
 
 	// Loop through all given files and add their importers to the list
-	for _, file := range files {
+	for _, file := range filesToCheck {
 		if filepath.Base(file) == jpath.DefaultEntrypoint {
 			importers[file] = struct{}{}
 		}
@@ -171,15 +193,14 @@ func findImporters(root string, searchForFile string, chain map[string]struct{})
 	searchedFileIsLibOrVendored := isFileLibOrVendored(searchForFile)
 	if !searchedFileIsLibOrVendored {
 		searchedDir := filepath.Dir(searchForFile)
-		searchedFileEntrypoint, err := jpath.Entrypoint(searchedDir)
-		if err == nil && searchedFileEntrypoint != "" {
+		if entrypoint := findEntrypoint(searchedDir); entrypoint != "" {
 			// Found the main file for the searched file, add it as an importer
-			importers = append(importers, searchedFileEntrypoint)
-		} else {
+			importers = append(importers, entrypoint)
+		} else if _, err := os.Stat(searchedDir); err == nil {
 			// No main file found, add all main files in child dirs as importers
 			files, err := FindFiles(searchedDir, nil)
 			if err != nil {
-				return nil, err
+				return nil, fmt.Errorf("failed to find files in %s: %w", searchedDir, err)
 			}
 			for _, file := range files {
 				if filepath.Base(file) == jpath.DefaultEntrypoint {
@@ -245,9 +266,8 @@ func findImporters(root string, searchForFile string, chain map[string]struct{})
 			if isImporter {
 				if jsonnetFileContent.IsMainFile {
 					importers = append(importers, jsonnetFilePath)
-				} else {
-					intermediateImporters = append(intermediateImporters, jsonnetFilePath)
 				}
+				intermediateImporters = append(intermediateImporters, jsonnetFilePath)
 				break
 			}
 		}
@@ -317,6 +337,21 @@ func createJsonnetFileCache(root string) (map[string]*cachedJsonnetFile, error)
 	return jsonnetFilesCache[root], nil
 }
 
+// findEntrypoint finds the nearest main.jsonnet file in the given file's directory or parent directories
+func findEntrypoint(searchedDir string) string {
+	for {
+		if _, err := os.Stat(searchedDir); err == nil {
+			break
+		}
+		searchedDir = filepath.Dir(searchedDir)
+	}
+	searchedFileEntrypoint, err := jpath.Entrypoint(searchedDir)
+	if err != nil {
+		return ""
+	}
+	return searchedFileEntrypoint
+}
+
 func pathMatches(path1, path2 string) bool {
 	if path1 == path2 {
 		return true
diff --git a/pkg/jsonnet/native/funcs.go b/pkg/jsonnet/native/funcs.go
index e6ef0f06..d42fc33d 100644
--- a/pkg/jsonnet/native/funcs.go
+++ b/pkg/jsonnet/native/funcs.go
@@ -2,7 +2,9 @@ package native
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"encoding/json"
+	"fmt"
 	"io"
 	"regexp"
 	"strings"
@@ -32,6 +34,9 @@ func Funcs() []*jsonnet.NativeFunction {
 		regexMatch(),
 		regexSubst(),
 
+		// Hash functions
+		hashSha256(),
+
 		helm.NativeFunc(helm.ExecHelm{}),
 		kustomize.NativeFunc(kustomize.ExecKustomize{}),
 	}
@@ -50,6 +55,18 @@ func parseJSON() *jsonnet.NativeFunction {
 	}
 }
 
+func hashSha256() *jsonnet.NativeFunction {
+	return &jsonnet.NativeFunction{
+		Name:   "sha256",
+		Params: ast.Identifiers{"str"},
+		Func: func(dataString []interface{}) (interface{}, error) {
+			h := sha256.New()
+			h.Write([]byte(dataString[0].(string)))
+			return fmt.Sprintf("%x", h.Sum(nil)), nil
+		},
+	}
+}
+
 // parseYAML wraps `yaml.Unmarshal` to convert a string of yaml document(s) into a (set of) dicts
 func parseYAML() *jsonnet.NativeFunction {
 	return &jsonnet.NativeFunction{
diff --git a/pkg/tanka/export.go b/pkg/tanka/export.go
index 3ec5fa73..f8f76cc7 100644
--- a/pkg/tanka/export.go
+++ b/pkg/tanka/export.go
@@ -56,6 +56,9 @@ type ExportEnvOpts struct {
 	// - fail-on-conflicts: fail when an exported file already exists
 	// - replace-envs: delete files previously exported by the targeted envs and re-export them
 	MergeStrategy ExportMergeStrategy
+	// Environments (main.jsonnet files) that have been deleted since the last export.
+	// This is used when using a merge strategy to delete the files of these deleted environments.
+	MergeDeletedEnvs []string
 }
 
 func ExportEnvironments(envs []*v1alpha1.Environment, to string, opts *ExportEnvOpts) error {
@@ -73,11 +76,16 @@ func ExportEnvironments(envs []*v1alpha1.Environment, to string, opts *ExportEnv
 
 	// delete files previously exported by the targeted envs.
 	if opts.MergeStrategy == ExportMergeStrategyReplaceEnvs {
-		if err := deletePreviouslyExportedManifests(to, envs); err != nil {
+		if err := deletePreviouslyExportedManifestsFromTankaEnvs(to, envs); err != nil {
 			return fmt.Errorf("deleting previously exported manifests: %w", err)
 		}
 	}
 
+	// delete files that were exported by environments that have been deleted since the last export.
+	if err := deletePreviouslyExportedManifests(to, opts.MergeDeletedEnvs); err != nil {
+		return fmt.Errorf("deleting previously exported manifests from deleted environments: %w", err)
+	}
+
 	// get all environments for paths
 	loadedEnvs, err := parallelLoadEnvironments(envs, parallelOpts{
 		Opts:        opts.Opts,
@@ -174,7 +182,24 @@ func dirEmpty(dir string) (bool, error) {
 	return false, err
 }
 
-func deletePreviouslyExportedManifests(path string, envs []*v1alpha1.Environment) error {
+func deletePreviouslyExportedManifestsFromTankaEnvs(path string, envs []*v1alpha1.Environment) error {
+	envNames := []string{}
+	for _, env := range envs {
+		envNames = append(envNames, env.Metadata.Namespace)
+	}
+	return deletePreviouslyExportedManifests(path, envNames)
+}
+
+func deletePreviouslyExportedManifests(path string, tankaEnvNames []string) error {
+	if len(tankaEnvNames) == 0 {
+		return nil
+	}
+
+	envNamesMap := make(map[string]struct{})
+	for _, envName := range tankaEnvNames {
+		envNamesMap[envName] = struct{}{}
+	}
+
 	fileToEnvMap := make(map[string]string)
 
 	manifestFilePath := filepath.Join(path, manifestFile)
@@ -190,14 +215,9 @@ func deletePreviouslyExportedManifests(path string, envs []*v1alpha1.Environment
 		return err
 	}
 
-	envNames := make(map[string]struct{})
-	for _, env := range envs {
-		envNames[env.Metadata.Namespace] = struct{}{}
-	}
-
 	var deletedManifestKeys []string
 	for exportedManifest, manifestEnv := range fileToEnvMap {
-		if _, ok := envNames[manifestEnv]; ok {
+		if _, ok := envNamesMap[manifestEnv]; ok {
 			deletedManifestKeys = append(deletedManifestKeys, exportedManifest)
 			if err := os.Remove(filepath.Join(path, exportedManifest)); err != nil {
 				return err


Test patch:
diff --git a/pkg/jsonnet/find_importers_test.go b/pkg/jsonnet/find_importers_test.go
index 4ba636ad..8f376505 100644
--- a/pkg/jsonnet/find_importers_test.go
+++ b/pkg/jsonnet/find_importers_test.go
@@ -3,6 +3,7 @@ package jsonnet
 import (
 	"fmt"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/grafana/tanka/pkg/jsonnet/jpath"
@@ -143,10 +144,74 @@ func findImportersTestCases(t testing.TB) []findImportersTestCase {
 				absPath(t, "testdata/findImporters/environments/lib-import-relative-to-env/folder1/folder2/main.jsonnet"),
 			},
 		},
+		{
+			name: "unused deleted file",
+			files: []string{
+				"deleted:testdata/findImporters/vendor/deleted-vendor/main.libsonnet",
+			},
+			expectedImporters: nil,
+		},
+		{
+			name: "deleted local path that is still potentially imported",
+			files: []string{
+				"deleted:testdata/findImporters/environments/using-deleted-stuff/my-import-dir/main.libsonnet",
+			},
+			expectedImporters: []string{
+				absPath(t, "testdata/findImporters/environments/using-deleted-stuff/main.jsonnet"),
+			},
+		},
+		{
+			name: "deleted lib that is still potentially imported",
+			files: []string{
+				"deleted:testdata/findImporters/lib/my-import-dir/main.libsonnet",
+			},
+			expectedImporters: []string{
+				absPath(t, "testdata/findImporters/environments/using-deleted-stuff/main.jsonnet"),
+			},
+		},
+		{
+			name: "deleted vendor that is still potentially imported",
+			files: []string{
+				"deleted:testdata/findImporters/vendor/my-import-dir/main.libsonnet",
+			},
+			expectedImporters: []string{
+				absPath(t, "testdata/findImporters/environments/using-deleted-stuff/main.jsonnet"),
+			},
+		},
+		{
+			name: "deleted lib that is still potentially imported, relative path from root",
+			files: []string{
+				"deleted:lib/my-import-dir/main.libsonnet",
+			},
+			expectedImporters: []string{
+				absPath(t, "testdata/findImporters/environments/using-deleted-stuff/main.jsonnet"),
+			},
+		},
+		{
+			// All files in an environment are considered to be imported by the main file, so the same should apply for deleted files
+			name: "deleted dir in environment",
+			files: []string{
+				"deleted:testdata/findImporters/environments/no-imports/deleted-dir/deleted-file.libsonnet",
+			},
+			expectedImporters: []string{
+				absPath(t, "testdata/findImporters/environments/no-imports/main.jsonnet"),
+			},
+		},
+		{
+			name: "imports through a main file are followed",
+			files: []string{
+				"testdata/findImporters/environments/import-other-main-file/env2/file.libsonnet",
+			},
+			expectedImporters: []string{
+				absPath(t, "testdata/findImporters/environments/import-other-main-file/env1/main.jsonnet"),
+				absPath(t, "testdata/findImporters/environments/import-other-main-file/env2/main.jsonnet"),
+			},
+		},
 	}
 }
 
 func TestFindImportersForFiles(t *testing.T) {
+	// Sanity check
 	// Make sure the main files all eval correctly
 	// We want to make sure that the importers command works correctly,
 	// but there's no point in testing on invalid jsonnet files
@@ -154,6 +219,11 @@ func TestFindImportersForFiles(t *testing.T) {
 	require.NoError(t, err)
 	require.NotEmpty(t, files)
 	for _, file := range files {
+		// This project is known to be invalid (as the name suggests)
+		if strings.Contains(file, "using-deleted-stuff") {
+			continue
+		}
+
 		// Skip non-main files
 		if filepath.Base(file) != jpath.DefaultEntrypoint {
 			continue
diff --git a/pkg/jsonnet/native/funcs_test.go b/pkg/jsonnet/native/funcs_test.go
index 5cb8b70e..afa13a8f 100644
--- a/pkg/jsonnet/native/funcs_test.go
+++ b/pkg/jsonnet/native/funcs_test.go
@@ -21,6 +21,14 @@ func callNative(name string, data []interface{}) (res interface{}, err error, ca
 	return nil, nil, fmt.Errorf("could not find native function %s", name)
 }
 
+func TestSha256(t *testing.T) {
+	ret, err, callerr := callNative("sha256", []interface{}{"foo"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", ret)
+	assert.Empty(t, err)
+}
+
 func TestParseJSONEmptyDict(t *testing.T) {
 	ret, err, callerr := callNative("parseJson", []interface{}{"{}"})
 
diff --git a/pkg/jsonnet/testdata/findImporters/environments/import-other-main-file/env1/main.jsonnet b/pkg/jsonnet/testdata/findImporters/environments/import-other-main-file/env1/main.jsonnet
new file mode 100644
index 00000000..ad4c2df9
--- /dev/null
+++ b/pkg/jsonnet/testdata/findImporters/environments/import-other-main-file/env1/main.jsonnet
@@ -0,0 +1,3 @@
+{
+  otherMain: import '../env2/main.jsonnet',
+}
diff --git a/pkg/jsonnet/testdata/findImporters/environments/import-other-main-file/env2/file.libsonnet b/pkg/jsonnet/testdata/findImporters/environments/import-other-main-file/env2/file.libsonnet
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/pkg/jsonnet/testdata/findImporters/environments/import-other-main-file/env2/file.libsonnet
@@ -0,0 +1,1 @@
+{}
diff --git a/pkg/jsonnet/testdata/findImporters/environments/import-other-main-file/env2/main.jsonnet b/pkg/jsonnet/testdata/findImporters/environments/import-other-main-file/env2/main.jsonnet
new file mode 100644
index 00000000..d5f00fa5
--- /dev/null
+++ b/pkg/jsonnet/testdata/findImporters/environments/import-other-main-file/env2/main.jsonnet
@@ -0,0 +1,3 @@
+{
+  myfile: import 'file.libsonnet',
+}
diff --git a/pkg/jsonnet/testdata/findImporters/environments/using-deleted-stuff/main.jsonnet b/pkg/jsonnet/testdata/findImporters/environments/using-deleted-stuff/main.jsonnet
new file mode 100644
index 00000000..96567dc3
--- /dev/null
+++ b/pkg/jsonnet/testdata/findImporters/environments/using-deleted-stuff/main.jsonnet
@@ -0,0 +1,3 @@
+{
+  myimport: import 'my-import-dir/main.libsonnet',
+}
diff --git a/pkg/tanka/export_test.go b/pkg/tanka/export_test.go
index 842e0b20..ae9e4b8f 100644
--- a/pkg/tanka/export_test.go
+++ b/pkg/tanka/export_test.go
@@ -121,6 +121,25 @@ func TestExportEnvironments(t *testing.T) {
     "static/updated-deployment.yaml": "test-export-envs/static-env/main.jsonnet",
     "static/updated-service.yaml": "test-export-envs/static-env/main.jsonnet"
 }`)
+
+	// Re-export and delete the files of one env
+	opts.Opts.ExtCode = jsonnet.InjectedCode{
+		"deploymentName": "'updated-again-deployment'",
+		"serviceName":    "'updated-again-service'",
+	}
+	opts.MergeDeletedEnvs = []string{"test-export-envs/inline-envs/main.jsonnet"}
+	require.NoError(t, ExportEnvironments(staticEnv, tempDir, opts))
+	checkFiles(t, tempDir, []string{
+		filepath.Join(tempDir, "static", "updated-again-deployment.yaml"),
+		filepath.Join(tempDir, "static", "updated-again-service.yaml"),
+		filepath.Join(tempDir, "manifest.json"),
+	})
+	manifestContent, err = os.ReadFile(filepath.Join(tempDir, "manifest.json"))
+	require.NoError(t, err)
+	assert.Equal(t, string(manifestContent), `{
+    "static/updated-again-deployment.yaml": "test-export-envs/static-env/main.jsonnet",
+    "static/updated-again-service.yaml": "test-export-envs/static-env/main.jsonnet"
+}`)
 }
 
 func BenchmarkExportEnvironmentsWithReplaceEnvs(b *testing.B) {