Merge branch 'master' into issue4928-append-honors-key-style

This commit is contained in:
Ed Overton
2023-08-30 12:30:49 -04:00
105 changed files with 861 additions and 596 deletions

View File

@@ -6,7 +6,7 @@ package patchjson6902
import (
"strings"
jsonpatch "github.com/evanphx/json-patch"
jsonpatch "gopkg.in/evanphx/json-patch.v5"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
k8syaml "sigs.k8s.io/yaml"

View File

@@ -126,8 +126,8 @@ func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targetSelectors []
}
// filter targets by matching resource IDs
for i, id := range ids {
if id.IsSelectedBy(selector.Select.ResId) && !rejectId(selector.Reject, &ids[i]) {
for _, id := range ids {
if id.IsSelectedBy(selector.Select.ResId) && !containsRejectId(selector.Reject, ids) {
err := copyValueToTarget(possibleTarget, value, selector)
if err != nil {
return nil, err
@@ -168,10 +168,15 @@ func matchesAnnoAndLabelSelector(n *yaml.RNode, selector *types.Selector) (bool,
return annoMatch && labelMatch, nil
}
func rejectId(rejects []*types.Selector, id *resid.ResId) bool {
func containsRejectId(rejects []*types.Selector, ids []resid.ResId) bool {
for _, r := range rejects {
if !r.ResId.IsEmpty() && id.IsSelectedBy(r.ResId) {
return true
if r.ResId.IsEmpty() {
continue
}
for _, id := range ids {
if id.IsSelectedBy(r.ResId) {
return true
}
}
}
return false

View File

@@ -3,14 +3,14 @@ module sigs.k8s.io/kustomize/api
go 1.20
require (
github.com/evanphx/json-patch v4.12.0+incompatible
github.com/go-errors/errors v1.4.2
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/imdario/mergo v0.3.6
github.com/stretchr/testify v1.8.1
gopkg.in/evanphx/json-patch.v5 v5.6.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/kube-openapi v0.0.0-20230601164746-7562a1006961
sigs.k8s.io/kustomize/kyaml v0.14.2
sigs.k8s.io/kustomize/kyaml v0.14.3
sigs.k8s.io/yaml v1.3.0
)

View File

@@ -5,8 +5,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
@@ -75,6 +73,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v5 v5.6.0 h1:BMT6KIwBD9CaU91PJCZIe46bDmBWa9ynTQgJIOpfQBk=
gopkg.in/evanphx/json-patch.v5 v5.6.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

View File

@@ -6,7 +6,7 @@ package builtins
import (
"fmt"
jsonpatch "github.com/evanphx/json-patch"
jsonpatch "gopkg.in/evanphx/json-patch.v5"
"sigs.k8s.io/kustomize/api/filters/patchjson6902"
"sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/resmap"

View File

@@ -7,7 +7,7 @@ import (
"fmt"
"strings"
jsonpatch "github.com/evanphx/json-patch"
jsonpatch "gopkg.in/evanphx/json-patch.v5"
"sigs.k8s.io/kustomize/api/filters/patchjson6902"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"

View File

@@ -8,7 +8,7 @@ import (
"sort"
"sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/konfig/builtinpluginconsts"
"sigs.k8s.io/kustomize/api/internal/konfig/builtinpluginconsts"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/errors"
)

View File

@@ -4,15 +4,31 @@
package krusty_test
import (
"fmt"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/require"
. "sigs.k8s.io/kustomize/api/internal/target"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/krusty"
kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest"
)
const validResource = `
apiVersion: v1
kind: Service
metadata:
name: myService
spec:
selector:
backend: bungie
ports:
- port: 7002
`
func TestTargetMustHaveKustomizationFile(t *testing.T) {
th := kusttest_test.MakeHarness(t)
th.WriteF("service.yaml", `
@@ -59,17 +75,7 @@ func TestBaseMustHaveKustomizationFile(t *testing.T) {
resources:
- base
`)
th.WriteF("base/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: myService
spec:
selector:
backend: bungie
ports:
- port: 7002
`)
th.WriteF("base/service.yaml", validResource)
err := th.RunWithErr(".", th.MakeDefaultOptions())
if err == nil {
t.Fatalf("expected an error")
@@ -160,3 +166,145 @@ spec:
secretName: cert-tls
`)
}
func TestAccumulateResourcesErrors(t *testing.T) {
type testcase struct {
name string
resource string
isAbsolute bool
files map[string]string
// errFile, errDir are regex for the expected error message output
// when kustomize tries to accumulate resource as file and dir,
// respectively. The test substitutes occurrences of "%s" in the
// error strings with the absolute path where kustomize looks for it.
errFile, errDir string
}
populateAbsolutePaths := func(tc testcase, dir string) testcase {
filePaths := make(map[string]string, len(tc.files)+1)
for file, content := range tc.files {
filePaths[filepath.Join(dir, file)] = content
}
resourcePath := filepath.Join(dir, tc.resource)
if tc.isAbsolute {
tc.resource = resourcePath
}
filePaths[filepath.Join(dir, "kustomization.yaml")] = fmt.Sprintf(`
resources:
- %s
`, tc.resource)
tc.files = filePaths
regPath := regexp.QuoteMeta(resourcePath)
tc.errFile = strings.ReplaceAll(tc.errFile, "%s", regPath)
tc.errDir = strings.ReplaceAll(tc.errDir, "%s", regPath)
return tc
}
buildError := func(tc testcase) string {
const (
prefix = "accumulating resources"
filePrefixf = "accumulating resources from '%s'"
fileWrapperIfDirf = "accumulation err='%s'"
separator = ": "
)
parts := []string{
prefix,
strings.Join([]string{
fmt.Sprintf(filePrefixf, regexp.QuoteMeta(tc.resource)),
tc.errFile,
}, separator),
}
if tc.errDir != "" {
parts[1] = fmt.Sprintf(fileWrapperIfDirf, parts[1])
parts = append(parts, tc.errDir)
}
return strings.Join(parts, separator)
}
for _, test := range []testcase{
{
name: "remote file not considered repo",
// The example.com second-level domain is reserved and
// safe to access, see RFC 2606.
resource: "https://example.com/segments-too-few-to-be-repo",
// It's acceptable for the error output of a remote file-like
// resource to not indicate the resource's status as a
// local directory. Though it is possible for a remote file-like
// resource to be a local directory, it is very unlikely.
errFile: `HTTP Error: status code 404 \(Not Found\)\z`,
},
{
name: "remote file qualifies as repo",
resource: "https://example.com/long/enough/to/have/org/and/repo",
// TODO(4788): This error message is technically wrong. Just
// because we fail to GET a resource does not mean the resource is
// not a remote file. We should return the GET status code as well.
errFile: "URL is a git repository",
errDir: `failed to run \S+/git fetch --depth=1 .+`,
},
{
name: "local file qualifies as repo",
// The .example top level domain is reserved for example purposes,
// see RFC 2606.
resource: "package@v1.28.0.example/configs/base",
errFile: `evalsymlink failure on '%s' .+`,
errDir: `failed to run \S+/git fetch --depth=1 .+`,
},
{
name: "relative path does not exist",
resource: "file-or-directory",
errFile: `evalsymlink failure on '%s' .+`,
errDir: `must build at directory: not a valid directory: evalsymlink failure .+`,
},
{
name: "absolute path does not exist",
resource: "file-or-directory",
isAbsolute: true,
errFile: `evalsymlink failure on '%s' .+`,
errDir: `new root '%s' cannot be absolute`,
},
{
name: "relative file violates restrictions",
resource: "../base/resource.yaml",
files: map[string]string{
"../base/resource.yaml": validResource,
},
errFile: "security; file '%s' is not in or below .+",
// TODO(4348): Over-inclusion of directory error message when we
// know resource is file.
errDir: "must build at directory: '%s': file is not directory",
},
{
name: "absolute file violates restrictions",
resource: "../base/resource.yaml",
isAbsolute: true,
files: map[string]string{
"../base/resource.yaml": validResource,
},
errFile: "security; file '%s' is not in or below .+",
// TODO(4348): Over-inclusion of directory error message when we
// know resource is file.
errDir: `new root '%s' cannot be absolute`,
},
} {
t.Run(test.name, func(t *testing.T) {
// Should use real file system to indicate that we are creating
// new temporary directories on disk when we attempt to fetch repos.
fs, tmpDir := kusttest_test.Setup(t)
root := tmpDir.Join("root")
require.NoError(t, fs.Mkdir(root))
test = populateAbsolutePaths(test, root)
for file, content := range test.files {
dir := filepath.Dir(file)
require.NoError(t, fs.MkdirAll(dir))
require.NoError(t, fs.WriteFile(file, []byte(content)))
}
b := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
_, err := b.Run(fs, root)
require.Regexp(t, buildError(test), err.Error())
})
}
// TODO(annasong): add tests that check accumulateResources errors for
// - repos
// - local directories
// - files that yield malformed yaml errors
}

View File

@@ -19,7 +19,7 @@ import (
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/loader"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/kyaml/filesys"
kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
@@ -28,6 +28,7 @@ func TestRemoteLoad_LocalProtocol(t *testing.T) {
root string
simple string
noSuffix string
hash string
multiBaseDev string
withSubmodule string
}
@@ -36,9 +37,10 @@ func TestRemoteLoad_LocalProtocol(t *testing.T) {
// root/
// simple.git/ - base with just a pod
// nosuffix/ - same as simple.git/ without the .git suffix
// hash-xx/ - same as simple.git/ with random hash at xx
// multibase.git/ - base with a dev overlay
// with-submodule.git/ - includes `simple` as a submodule
// submodule/ - the submodule referencing `simple
// submodule/ - the submodule referencing `simple`
createGitRepos := func(t *testing.T) testRepos {
t.Helper()
@@ -50,10 +52,16 @@ func TestRemoteLoad_LocalProtocol(t *testing.T) {
}
}
root := t.TempDir()
hashPath, err := os.MkdirTemp(root, "hash-")
require.NoError(t, err)
hashDir := filepath.Base(hashPath)
bash(fmt.Sprintf(`
set -eux
export ROOT="%s"
export HASH_DIR="%s"
export GIT_AUTHOR_EMAIL=nobody@kustomize.io
export GIT_AUTHOR_NAME=Nobody
export GIT_COMMITTER_EMAIL=nobody@kustomize.io
@@ -85,19 +93,27 @@ cp -r testdata/remoteload/multibase $ROOT/multibase.git
git add .
git commit -m "import"
)
cp -r testdata/remoteload/with-submodule $ROOT/with-submodule.git # see README
cp -r $ROOT/simple.git/. $ROOT/$HASH_DIR
(
mkdir $ROOT/with-submodule.git
cd $ROOT/with-submodule.git
git init --initial-branch=main
git submodule add $ROOT/simple.git submodule
git add .
git commit -m "import"
git checkout -b relative-submodule
git submodule add ../$HASH_DIR submodule
git commit -m "relative submodule"
git checkout main
git submodule add $ROOT/simple.git submodule
git commit -m "submodule"
)
`, root))
`, root, hashDir))
return testRepos{
root: root,
// The strings below aren't currently used, and more serve as documentation.
simple: "simple.git",
noSuffix: "nosuffix",
hash: hashDir,
multiBaseDev: "multibase.git",
withSubmodule: "with-submodule.git",
}
@@ -183,6 +199,15 @@ resources:
`,
expected: simpleBuild,
},
{
name: "has relative submodule",
kustomization: `
resources:
- file://$ROOT/with-submodule.git/submodule?ref=relative-submodule
`,
// TODO(annasong): Replace with simpleBuild once #5131 is fixed.
err: `failed to run '\S+/git submodule update --init --recursive'`,
},
{
name: "has timeout",
kustomization: `
@@ -264,7 +289,7 @@ resources:
}
kust := strings.ReplaceAll(test.kustomization, "$ROOT", repos.root)
fSys, tmpDir := createKustDir(t, kust)
fSys, tmpDir := kusttest_test.CreateKustDir(t, kust)
b := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
m, err := b.Run(
@@ -273,7 +298,7 @@ resources:
if test.err != "" {
require.Error(t, err)
require.Contains(t, err.Error(), test.err)
require.Regexp(t, test.err, err.Error())
} else {
require.NoError(t, err)
checkYaml(t, m, strings.ReplaceAll(test.expected, "$ROOT", repos.root))
@@ -368,7 +393,7 @@ resources:
if test.beforeTest != nil {
test.beforeTest(t)
}
fSys, tmpDir := createKustDir(t, test.kustomization)
fSys, tmpDir := kusttest_test.CreateKustDir(t, test.kustomization)
b := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
m, err := b.Run(
@@ -424,16 +449,6 @@ func configureGitSSHCommand(t *testing.T) {
})
}
func createKustDir(t *testing.T, content string) (filesys.FileSystem, filesys.ConfirmedDir) {
t.Helper()
fSys := filesys.MakeFsOnDisk()
tmpDir, err := filesys.NewTmpConfirmedDir()
require.NoError(t, err)
require.NoError(t, fSys.WriteFile(filepath.Join(tmpDir.String(), "kustomization.yaml"), []byte(content)))
return fSys, tmpDir
}
func checkYaml(t *testing.T, actual resmap.ResMap, expected string) {
t.Helper()

View File

@@ -546,3 +546,71 @@ metadata:
name: red-dc6gc5btkc
`)
}
func TestReplacementTransformerWithSuffixTransformerAndReject(t *testing.T) {
th := kusttest_test.MakeEnhancedHarness(t)
defer th.Reset()
th.WriteF("base/app.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: original-name
spec:
template:
spec:
containers:
- image: app1:1.0
name: app
`)
th.WriteK("base", `
resources:
- app.yaml
`)
th.WriteK("overlay", `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
nameSuffix: -dev
resources:
- ../base
configMapGenerator:
- name: app-config
literals:
- name=something-else
replacements:
- source:
kind: ConfigMap
name: app-config
fieldPath: data.name
targets:
- fieldPaths:
- spec.template.spec.containers.0.name
select:
kind: Deployment
reject:
- name: original-name
`)
m := th.Run("overlay", th.MakeDefaultOptions())
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: original-name-dev
spec:
template:
spec:
containers:
- image: app1:1.0
name: app
---
apiVersion: v1
data:
name: something-else
kind: ConfigMap
metadata:
name: app-config-dev-97544dk6t8
`)
}

View File

@@ -0,0 +1,10 @@
# submodule
This repo demonstrates kustomize's ability to download git repos
with submodules. The following branches contain
* main: submodule via absolute path
* relative-submodule: submodule via relative path
For the submodule accessed via a relative path, we include a random hash in the
submodule name to avoid accessing an unintended directory in the case kustomize
contains loader bugs (issue #5131).

View File

@@ -31,6 +31,11 @@ func TestIsRemoteFile(t *testing.T) {
"https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/examples/helloWorld/configMap.yaml",
true,
},
"malformed https": {
// TODO(annasong): Maybe we want to fix this. Needs more research.
"https:/raw.githubusercontent.com/kubernetes-sigs/kustomize/master/examples/helloWorld/configMap.yaml",
true,
},
"https dir": {
"https://github.com/kubernetes-sigs/kustomize//examples/helloWorld/",
true,
@@ -196,13 +201,52 @@ func TestLoaderBadRelative(t *testing.T) {
require.Error(err)
}
func TestLoaderMisc(t *testing.T) {
l := makeLoader()
_, err := l.New("")
func TestNewEmptyLoader(t *testing.T) {
_, err := makeLoader().New("")
require.Error(t, err)
}
_, err = l.New("https://google.com/project")
require.Error(t, err)
func TestNewRemoteLoaderDoesNotExist(t *testing.T) {
_, err := makeLoader().New("https://example.com/org/repo")
require.ErrorContains(t, err, "fetch")
}
func TestLoaderLocalScheme(t *testing.T) {
// It is unlikely but possible for a reference with a url scheme to
// actually refer to a local file or directory.
t.Run("file", func(t *testing.T) {
fSys, dir := setupOnDisk(t)
parts := []string{
"ssh:",
"resource.yaml",
}
require.NoError(t, fSys.Mkdir(dir.Join(parts[0])))
const content = "resource config"
require.NoError(t, fSys.WriteFile(
dir.Join(filepath.Join(parts...)),
[]byte(content),
))
actualContent, err := newLoaderOrDie(RestrictionRootOnly,
fSys,
dir.String(),
).Load(strings.Join(parts, "//"))
require.NoError(t, err)
require.Equal(t, content, string(actualContent))
})
t.Run("directory", func(t *testing.T) {
fSys, dir := setupOnDisk(t)
parts := []string{
"https:",
"root",
}
require.NoError(t, fSys.MkdirAll(dir.Join(filepath.Join(parts...))))
ldr, err := newLoaderOrDie(RestrictionRootOnly,
fSys,
dir.String(),
).New(strings.Join(parts, "//"))
require.NoError(t, err)
require.Empty(t, ldr.Repo())
})
}
const (
@@ -212,17 +256,17 @@ const (
// Create a structure like this
//
// /tmp/kustomize-test-random
// ├── base
// │ ├── okayData
// │ ├── symLinkToOkayData -> okayData
// │ └── symLinkToExteriorData -> ../exteriorData
// └── exteriorData
//
// /tmp/kustomize-test-random
// ├── base
// │ ├── okayData
// │ ├── symLinkToOkayData -> okayData
// │ └── symLinkToExteriorData -> ../exteriorData
// └── exteriorData
func commonSetupForLoaderRestrictionTest(t *testing.T) (string, filesys.FileSystem) {
t.Helper()
dir := t.TempDir()
fSys := filesys.MakeFsOnDisk()
fSys, tmpDir := setupOnDisk(t)
dir := tmpDir.String()
fSys.Mkdir(filepath.Join(dir, "base"))
fSys.WriteFile(
@@ -446,12 +490,8 @@ func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
}
func TestLoaderDisallowsRemoteBaseExitRepo(t *testing.T) {
fSys := filesys.MakeFsOnDisk()
dir, err := filesys.NewTmpConfirmedDir()
require.NoError(t, err)
t.Cleanup(func() {
_ = fSys.RemoveAll(dir.String())
})
fSys, dir := setupOnDisk(t)
repo := dir.Join("repo")
require.NoError(t, fSys.Mkdir(repo))
@@ -618,3 +658,19 @@ func TestLoaderHTTP(t *testing.T) {
require.Error(err)
}
}
// setupOnDisk sets up a file system on disk and directory that is cleaned after
// test completion.
// TODO(annasong): Move all loader tests that require real file system into
// api/krusty.
func setupOnDisk(t *testing.T) (filesys.FileSystem, filesys.ConfirmedDir) {
t.Helper()
fSys := filesys.MakeFsOnDisk()
dir, err := filesys.NewTmpConfirmedDir()
require.NoError(t, err)
t.Cleanup(func() {
_ = fSys.RemoveAll(dir.String())
})
return fSys, dir
}

View File

@@ -7,8 +7,8 @@ import (
"path/filepath"
"testing"
"sigs.k8s.io/kustomize/api/internal/konfig/builtinpluginconsts"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/konfig/builtinpluginconsts"
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types"

View File

@@ -0,0 +1,37 @@
// Copyright 2023 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kusttest_test
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
// Setup sets up a file system on disk and directory that is cleaned after
// test completion.
func Setup(t *testing.T) (filesys.FileSystem, filesys.ConfirmedDir) {
t.Helper()
fSys := filesys.MakeFsOnDisk()
dir, err := filesys.NewTmpConfirmedDir()
require.NoError(t, err)
t.Cleanup(func() {
_ = fSys.RemoveAll(dir.String())
})
return fSys, dir
}
// CreateKustDir creates a file system on disk and a new directory
// that holds a kustomization file with content. The directory is removed on
// test completion.
func CreateKustDir(t *testing.T, content string) (filesys.FileSystem, filesys.ConfirmedDir) {
t.Helper()
fSys, tmpDir := Setup(t)
require.NoError(t, fSys.WriteFile(filepath.Join(tmpDir.String(), "kustomization.yaml"), []byte(content)))
return fSys, tmpDir
}