// Copyright 2022 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package localizer_test import ( "fmt" "io/fs" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" . "sigs.k8s.io/kustomize/api/internal/localizer" "sigs.k8s.io/kustomize/kyaml/filesys" ) const ( podConfiguration = `apiVersion: v1 kind: Pod metadata: name: pod spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80` replacementTransformerWithPath = `apiVersion: builtin kind: ReplacementTransformer metadata: name: replacement replacements: - path: replacement.yaml - source: fieldPath: metadata.[name=my-pod] group: apps namespace: test version: v1 targets: - fieldPaths: - spec.containers.0.name select: name: another-pod ` replacements = ` - source: name: src fieldPath: path options: delimiter: '=' index: 1 targets: - select: kind: Pod reject: version: v1 fieldPaths: - metadata.annotations.config\.kubernetes\.io/local-config - sequence.* - source: kind: Deployment fieldPath: sequence.- targets: - select: namespace: my fieldPaths: - path options: delimiter: '=' index: 0 ` valuesFile = `minecraftServer: difficulty: peaceful ` ) func makeMemoryFs(t *testing.T) filesys.FileSystem { t.Helper() req := require.New(t) fSys := filesys.MakeFsInMemory() req.NoError(fSys.MkdirAll("/a/b")) req.NoError(fSys.WriteFile("/a/pod.yaml", []byte(podConfiguration))) dirChain := "/alpha/beta/gamma/delta" req.NoError(fSys.MkdirAll(dirChain)) req.NoError(fSys.WriteFile(filepath.Join(dirChain, "deployment.yaml"), []byte("deployment configuration"))) req.NoError(fSys.Mkdir("/alpha/beta/say")) return fSys } func addFiles(t *testing.T, fSys filesys.FileSystem, parentDir string, files map[string]string) { t.Helper() // in-memory file system makes all necessary dirs when writing files for file, content := range files { require.NoError(t, fSys.WriteFile(filepath.Join(parentDir, file), []byte(content))) } } func checkRun(t *testing.T, fSys filesys.FileSystem, target, scope, dst string) { t.Helper() actualDst, err := Run(target, scope, dst, fSys) require.NoError(t, err) require.Equal(t, dst, actualDst) } func makeFileSystems(t *testing.T, target string, files map[string]string) (expected filesys.FileSystem, actual filesys.FileSystem) { t.Helper() copies := make([]filesys.FileSystem, 2) for i := range copies { copies[i] = makeMemoryFs(t) addFiles(t, copies[i], target, files) } return copies[0], copies[1] } func checkFSys(t *testing.T, fSysExpected filesys.FileSystem, fSysActual filesys.FileSystem) { t.Helper() assert.Equal(t, fSysExpected, fSysActual) if t.Failed() { reportFSysDiff(t, fSysExpected, fSysActual) } } func reportFSysDiff(t *testing.T, fSysExpected filesys.FileSystem, fSysActual filesys.FileSystem) { t.Helper() visited := make(map[string]struct{}) err := fSysActual.Walk("/", func(path string, info fs.FileInfo, err error) error { require.NoError(t, err) visited[path] = struct{}{} if info.IsDir() { assert.Truef(t, fSysExpected.IsDir(path), "unexpected directory %q", path) } else { actualContent, readErr := fSysActual.ReadFile(path) require.NoError(t, readErr) expectedContent, findErr := fSysExpected.ReadFile(path) require.NoErrorf(t, findErr, "unexpected file %q", path) if findErr == nil { assert.Equal(t, string(expectedContent), string(actualContent)) } } return nil }) require.NoError(t, err) err = fSysExpected.Walk("/", func(path string, info fs.FileInfo, err error) error { require.NoError(t, err) _, exists := visited[path] assert.Truef(t, exists, "expected path %q not found", path) return nil }) require.NoError(t, err) } func checkLocalizeInTargetSuccess(t *testing.T, files map[string]string) { t.Helper() fSys := makeMemoryFs(t) addFiles(t, fSys, "/a", files) checkRun(t, fSys, "/a", "/", "/dst") fSysExpected := makeMemoryFs(t) addFiles(t, fSysExpected, "/a", files) addFiles(t, fSysExpected, "/dst/a", files) checkFSys(t, fSysExpected, fSys) } func TestTargetIsScope(t *testing.T) { kustomization := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namePrefix: my- `, } fSysExpected, fSysActual := makeFileSystems(t, "/a", kustomization) checkRun(t, fSysActual, "/a", "/a", "/a/b/dst") addFiles(t, fSysExpected, "/a/b/dst", kustomization) checkFSys(t, fSysExpected, fSysActual) } func TestTargetNestedInScope(t *testing.T) { kustomization := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization patches: - patch: |- - op: replace path: /some/existing/path value: new value target: kind: Deployment labelSelector: env=dev `, } fSysExpected, fSysActual := makeFileSystems(t, "/a/b", kustomization) checkRun(t, fSysActual, "/a/b", "/", "/a/b/dst") addFiles(t, fSysExpected, "/a/b/dst/a/b", kustomization) checkFSys(t, fSysExpected, fSysActual) } func TestLoadKustomizationName(t *testing.T) { kustomization := map[string]string{ "Kustomization": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization labels: - pairs: label-one: value-one label-two: value-two `, } checkLocalizeInTargetSuccess(t, kustomization) } func TestLoadGVKNN(t *testing.T) { for name, kustomization := range map[string]string{ "missing": `namePrefix: my- `, "wrong": `kind: NotChecked `, } { t.Run(name, func(t *testing.T) { files := map[string]string{ "kustomization.yaml": kustomization, } checkLocalizeInTargetSuccess(t, files) }) } } func TestLoadLegacyFields(t *testing.T) { kustomization := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 commonLabels: app: bingo imageTags: - name: postgres newName: my-registry/my-postgres newTag: v1 kind: Kustomization `, } checkLocalizeInTargetSuccess(t, kustomization) } func TestLoadUnknownKustFields(t *testing.T) { fSysExpected, fSysTest := makeFileSystems(t, "/a", map[string]string{ "kustomization.yaml": `namePrefix: valid suffix: invalid`, }) _, err := Run("/a", "", "", fSysTest) require.EqualError(t, err, `unable to localize target "/a": invalid Kustomization: json: unknown field "suffix"`) checkFSys(t, fSysExpected, fSysTest) } func TestLocalizeFileName(t *testing.T) { for name, path := range map[string]string{ "nested_directories": "a/b/c/d/patch.yaml", "localize_dir_name_when_no_remote": LocalizeDir, "in_localize_dir_name_when_no_remote": fmt.Sprintf("%s/patch.yaml", LocalizeDir), "no_file_extension": "patch", "kustomization_name": "a/kustomization.yaml", } { t.Run(name, func(t *testing.T) { kustAndPatch := map[string]string{ "kustomization.yaml": fmt.Sprintf(`apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization patches: - path: %s `, path), path: podConfiguration, } checkLocalizeInTargetSuccess(t, kustAndPatch) }) } } func TestLocalizeFileCleaned(t *testing.T) { kustAndPatch := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization patches: - path: ../gamma/../../../alpha/beta/./gamma/patch.yaml `, "patch.yaml": podConfiguration, } expected, actual := makeFileSystems(t, "/alpha/beta/gamma", kustAndPatch) checkRun(t, actual, "/alpha/beta/gamma", "/", "/localized-gamma") addFiles(t, expected, "/localized-gamma/alpha/beta/gamma", map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization patches: - path: patch.yaml `, "patch.yaml": podConfiguration, }) checkFSys(t, expected, actual) } func TestLocalizeUnreferencedIgnored(t *testing.T) { targetAndUnreferenced := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 configMapGenerator: - envs: - env name: referenced-file kind: Kustomization `, "env": "APPLE=orange", "env.properties": "USERNAME=password", "dir/resource.yaml": podConfiguration, } expected, actual := makeFileSystems(t, "/alpha/beta", targetAndUnreferenced) checkRun(t, actual, "/alpha/beta", "/alpha", "/beta") addFiles(t, expected, "/beta/beta", map[string]string{ "kustomization.yaml": targetAndUnreferenced["kustomization.yaml"], "env": targetAndUnreferenced["env"], }) checkFSys(t, expected, actual) } func TestLocalizePatches(t *testing.T) { kustAndPatch := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization patches: - patch: |- apiVersion: v1 kind: Deployment metadata: labels: app.kubernetes.io/version: 1.21.0 name: dummy-app target: labelSelector: app.kubernetes.io/name=nginx - options: allowNameChange: true path: patch.yaml `, "patch.yaml": podConfiguration, } checkLocalizeInTargetSuccess(t, kustAndPatch) } func TestLocalizeOpenAPI(t *testing.T) { type testCase struct { name string files map[string]string } for _, test := range []testCase{ { name: "no_path", files: map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization openapi: version: v1.20.4 `, }, }, { name: "path", files: map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization openapi: path: openapi.json `, "openapi.json": `{ "definitions": { "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { "properties": { "name": { "type": "string" } }, "type": "object" } } }`, }, }, } { t.Run(test.name, func(t *testing.T) { checkLocalizeInTargetSuccess(t, test.files) }) } } func TestLocalizeConfigurations(t *testing.T) { kustAndConfigs := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 configurations: - commonLabels.yaml - namePrefix.yaml kind: Kustomization `, "commonLabels.yaml": `commonLabels: - path: new/path create: true`, "namePrefix.yaml": `namePrefix: - version: v1 path: metadata/name - group: custom path: metadata/name`, } checkLocalizeInTargetSuccess(t, kustAndConfigs) } func TestLocalizeCrds(t *testing.T) { kustAndCrds := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 crds: - crd1.yaml - crd2.yaml kind: Kustomization `, "crd1.yaml": `apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: controller.stable.example.com`, "crd2.yaml": `apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: crontabs.stable.example.com scope: Cluster`, } checkLocalizeInTargetSuccess(t, kustAndCrds) } func TestLocalizePatchesJson(t *testing.T) { kustAndPatches := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization patchesJson6902: - path: patch.yaml target: annotationSelector: zone=west name: pod version: v1 - patch: '[{"op": "add", "path": "/new/path", "value": "value"}]' target: group: apps kind: Pod - path: patch.json target: namespace: my `, "patch.yaml": `- op: add path: /some/new/path value: value - op: replace path: /some/existing/path value: new value`, "patch.json": ` [ {"op": "copy", "from": "/here", "path": "/there"}, {"op": "remove", "path": "/some/existing/path"}, ]`, } checkLocalizeInTargetSuccess(t, kustAndPatches) } func TestLocalizePatchesSM(t *testing.T) { kustAndPatches := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization patchesStrategicMerge: - |- apiVersion: v1 kind: ConfigMap metadata: name: map data: - APPLE: orange - patch.yaml `, "patch.yaml": podConfiguration, } checkLocalizeInTargetSuccess(t, kustAndPatches) } func TestLocalizeReplacements(t *testing.T) { kustAndReplacement := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization replacements: - path: replacement.yaml - source: fieldPath: path.0 name: map targets: - fieldPaths: - path select: name: my-map `, "replacement.yaml": replacements, } checkLocalizeInTargetSuccess(t, kustAndReplacement) } func TestLocalizeConfigMapGenerator(t *testing.T) { kustAndData := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 configMapGenerator: - env: single.env envs: - standard.env namespace: my options: immutable: true - behavior: merge files: - key.properties literals: - PEAR=pineapple kind: Kustomization metadata: name: test `, "single.env": `MAY=contain MORE=than ONE=pair`, "standard.env": `SIZE=0.1 IS_GLOBAL=true`, "key.properties": "value", } checkLocalizeInTargetSuccess(t, kustAndData) } func TestLocalizeSecretGenerator(t *testing.T) { kustAndData := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization secretGenerator: - behavior: create files: - key=b/value.properties - b/value name: secret - envs: - crt - key type: kubernetes.io/tls - literals: - APPLE=orange - PLUM=pluot name: no-files - env: more-fruit `, "crt": "tls.crt=LS0tLS1CRUd...0tLQo=", "key": "tls.key=LS0tLS1CRUd...0tLQo=", "more-fruit": "GRAPE=lime", "b/value.properties": "value", "b/value": "value", } checkLocalizeInTargetSuccess(t, kustAndData) } func TestLocalizeFileNoFile(t *testing.T) { kustAndPatch := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization patches: - path: name-DNE.yaml `, } expected, actual := makeFileSystems(t, "/a/b", kustAndPatch) _, err := Run("/a/b", "", "/dst", actual) require.EqualError(t, err, `unable to localize target "/a/b": unable to localize patches: invalid file reference: '/a/b/name-DNE.yaml' doesn't exist`) checkFSys(t, expected, actual) } func TestLocalizePluginsInlineAndFile(t *testing.T) { for _, test := range []struct { name string files map[string]string }{ { name: "generators", files: map[string]string{ "kustomization.yaml": `generators: - generator.yaml - | apiVersion: builtin env: second.properties kind: ConfigMapGenerator metadata: name: inline `, "generator.yaml": `apiVersion: builtin env: first.properties kind: ConfigMapGenerator metadata: name: file `, "first.properties": "APPLE=orange", "second.properties": "BANANA=pear", }, }, { name: "transformers", files: map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization transformers: - | apiVersion: builtin kind: PatchTransformer metadata: name: inline path: patchSM-one.yaml - patch.yaml `, "patch.yaml": `apiVersion: builtin kind: PatchTransformer metadata: name: file path: patchSM-two.yaml `, "patchSM-one.yaml": podConfiguration, "patchSM-two.yaml": podConfiguration, }, }, { name: "validators", files: map[string]string{ "kustomization.yaml": `validators: - | apiVersion: builtin kind: ReplacementTransformer metadata: name: inline replacements: - path: first.yaml - second.yaml `, "first.yaml": replacementTransformerWithPath, "second.yaml": replacementTransformerWithPath, "replacement.yaml": replacements, }, }, } { t.Run(test.name, func(t *testing.T) { checkLocalizeInTargetSuccess(t, test.files) }) } } func TestLocalizeMultiplePluginsInEntry(t *testing.T) { kustAndPlugins := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization transformers: - | apiVersion: builtin kind: PatchTransformer metadata: name: one path: patchSM-one.yaml --- apiVersion: builtin kind: PatchTransformer metadata: name: two path: patchSM-two.yaml `, "patchSM-one.yaml": podConfiguration, "patchSM-two.yaml": podConfiguration, } checkLocalizeInTargetSuccess(t, kustAndPlugins) } func TestLocalizeCleanedPathInPath(t *testing.T) { const patchf = `apiVersion: builtin kind: PatchTransformer metadata: name: cleaned-path path: %s ` kustAndPlugins := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization transformers: - patch.yaml `, "patch.yaml": fmt.Sprintf(patchf, "../a/patchSM.yaml"), "patchSM.yaml": podConfiguration, } expected, actual := makeFileSystems(t, "/a", kustAndPlugins) checkRun(t, actual, "/a", "/a", "/dst") addFiles(t, expected, "/dst", map[string]string{ "kustomization.yaml": kustAndPlugins["kustomization.yaml"], "patch.yaml": fmt.Sprintf(patchf, "patchSM.yaml"), "patchSM.yaml": kustAndPlugins["patchSM.yaml"], }) checkFSys(t, expected, actual) } func TestLocalizeGeneratorsConfigMap(t *testing.T) { files := map[string]string{ "kustomization.yaml": `generators: - configMapGenerator `, "configMapGenerator": `apiVersion: builtin behavior: create env: one.env envs: - two.env - three.env files: - four.properties - key=five.properties kind: ConfigMapGenerator metadata: name: custom-generator options: disableNameSuffix: true `, "one.env": "key1=value1", "two.env": "key2=value2", "three.env": "key3=value3", "four.properties": "key4=value4", "five.properties": "key5=value5", } checkLocalizeInTargetSuccess(t, files) } func TestLocalizeGeneratorsSecret(t *testing.T) { files := map[string]string{ "kustomization.yaml": `generators: - secretGenerator `, "secretGenerator": `apiVersion: builtin env: one.env envs: - two.env - three.env files: - four.properties - key=five.properties kind: SecretGenerator literals: - key6=value6 metadata: name: custom-generator `, "one.env": "key1=value1", "two.env": "key2=value2", "three.env": "key3=value3", "four.properties": "key4=value4", "five.properties": "key5=value5", } checkLocalizeInTargetSuccess(t, files) } func TestLocalizeTransformersPatch(t *testing.T) { kustAndPatches := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization transformers: - | apiVersion: builtin kind: PatchTransformer metadata: name: no-path patch: '[{"op": "add", "path": "/path", "value": "value"}]' target: name: pod - patch.yaml `, "patch.yaml": `apiVersion: builtin kind: PatchTransformer metadata: name: path path: patchSM.yaml `, "patchSM.yaml": podConfiguration, } checkLocalizeInTargetSuccess(t, kustAndPatches) } func TestLocalizeTransformersPatchJson(t *testing.T) { kustAndPatches := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization transformers: - patch.yaml `, "patch.yaml": `apiVersion: builtin kind: PatchJson6902Transformer metadata: name: path path: nested-patch.yaml target: name: pod namespace: test --- apiVersion: builtin jsonOp: |- op: replace path: /path value: new value kind: PatchJson6902Transformer metadata: name: patch6902 target: name: deployment `, "nested-patch.yaml": ` [ {"op": "copy", "from": "/existing/path", "path": "/another/path"}, ] `, } checkLocalizeInTargetSuccess(t, kustAndPatches) } func TestLocalizeTransformersPatchSM(t *testing.T) { kustAndPatches := map[string]string{ "kustomization.yaml": `transformers: - patch.yaml `, "patch.yaml": `apiVersion: builtin kind: PatchStrategicMergeTransformer metadata: name: path paths: - nested-patch.yaml - |- apiVersion: v1 kind: Pod metadata: name: my-pod `, "nested-patch.yaml": podConfiguration, } checkLocalizeInTargetSuccess(t, kustAndPatches) } func TestLocalizeTransformersReplacement(t *testing.T) { kustAndReplacements := map[string]string{ "kustomization.yaml": `transformers: - replacement-transformer.yaml `, "replacement-transformer.yaml": replacementTransformerWithPath, "replacement.yaml": replacements, } checkLocalizeInTargetSuccess(t, kustAndReplacements) } func TestLocalizePluginsNoPaths(t *testing.T) { kustAndPlugins := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization transformers: - | apiVersion: different kind: MyTransformer metadata: name: still-copied path: /nothing/special - prefix.yaml `, "prefix.yaml": `apiVersion: builtin kind: PrefixTransformer metadata: name: other-built-ins-still-copied prefix: copy `, } checkLocalizeInTargetSuccess(t, kustAndPlugins) } func TestLocalizeValidators(t *testing.T) { kustAndPlugin := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization validators: - replacement-no-change.yaml `, "replacement-no-change.yaml": replacementTransformerWithPath, "replacement.yaml": replacements, } checkLocalizeInTargetSuccess(t, kustAndPlugin) } func TestLocalizeBuiltinPlugins_SequenceScalarEquivalence(t *testing.T) { kustomization := map[string]string{ "kustomization.yaml": `transformers: - | apiVersion: builtin kind: PatchTransformer metadata: name: path-should-be-scalar-but-accept-sequence path: - patchSM.yaml `, "patchSM.yaml": podConfiguration, } checkLocalizeInTargetSuccess(t, kustomization) } func TestLocalizeBuiltinPlugins_NotResource(t *testing.T) { type testCase struct { name string files map[string]string errPrefix string inlineErrMsg string fileErrMsg string } for _, test := range []testCase{ { name: "bad_inline_resource", files: map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 generators: - | apiVersion: builtin kind: ConfigMapGenerator kind: Kustomization `, }, errPrefix: `unable to load generators entry: unable to load resource entry "apiVersion: builtin\nkind: ConfigMapGenerator\n"`, inlineErrMsg: `missing metadata.name in object {{builtin ConfigMapGenerator} {{ } map[] map[]}}`, fileErrMsg: `invalid file reference: '/apiVersion: builtin kind: ConfigMapGenerator ' doesn't exist`, }, { name: "bad_file_resource", files: map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization transformers: - plugin.yaml `, "plugin.yaml": `apiVersion: builtin metadata: name: PatchTransformer `, }, errPrefix: `unable to load transformers entry: unable to load resource entry "plugin.yaml"`, inlineErrMsg: `missing Resource metadata`, fileErrMsg: `missing kind in object {{builtin } {{PatchTransformer } map[] map[]}}`, }, } { t.Run(test.name, func(t *testing.T) { expected, actual := makeFileSystems(t, "/", test.files) _, err := Run("/", "", "/dst", actual) var actualErr ResourceLoadError require.ErrorAs(t, err, &actualErr) require.EqualError(t, actualErr.InlineError, test.inlineErrMsg) require.EqualError(t, actualErr.FileError, test.fileErrMsg) require.EqualError(t, err, fmt.Sprintf(`unable to localize target "/": %s: when parsing as inline received error: %s when parsing as filepath received error: %s`, test.errPrefix, test.inlineErrMsg, test.fileErrMsg)) checkFSys(t, expected, actual) }) } } func TestLocalizeBuiltinPlugins_Errors(t *testing.T) { for name, test := range map[string]struct { files map[string]string fieldSpecErr string locErr string }{ "file_dne": { files: map[string]string{ "kustomization.yaml": `transformers: - | apiVersion: builtin kind: PatchTransformer metadata: name: file-does-not-exist path: patchSM.yaml `, }, fieldSpecErr: "considering field 'path' of object PatchTransformer.builtin.[noGrp]/file-does-not-exist.[noNs]", locErr: "invalid file reference: '/a/patchSM.yaml' doesn't exist", }, "not_sequence_or_scalar": { files: map[string]string{ "kustomization.yaml": `transformers: - | apiVersion: builtin kind: PatchTransformer metadata: name: path-node-has-wrong-kind path: mappingNode: patchSM.yaml `, "patchSM.yaml": podConfiguration, }, fieldSpecErr: "considering field 'path' of object PatchTransformer.builtin.[noGrp]/path-node-has-wrong-kind.[noNs]", locErr: "expected sequence or scalar node", }, } { t.Run(name, func(t *testing.T) { expected, actual := makeFileSystems(t, "/a", test.files) _, err := Run("/a", "", "/dst", actual) const errPrefix = `unable to localize target "/a"` require.EqualError(t, err, fmt.Sprintf( "%s: %s: %s", errPrefix, test.fieldSpecErr, test.locErr)) checkFSys(t, expected, actual) }) } } func TestLocalizeDirInTarget(t *testing.T) { type testCase struct { name string files map[string]string } for _, tc := range []testCase{ { name: "multi_nested_child", files: map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 components: - delta/epsilon kind: Kustomization `, "delta/epsilon/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component namespace: kustomize-namespace `, }, }, { name: "recursive", files: map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 components: - delta kind: Kustomization `, "delta/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1alpha1 components: - epsilon kind: Component `, "delta/epsilon/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component namespace: kustomize-namespace `, }, }, { name: "file_in_dir", files: map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 components: - delta kind: Kustomization `, "delta/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component patches: - path: patch.yaml `, "delta/patch.yaml": podConfiguration, }, }, { name: "multiple_calls", files: map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 components: - delta - delta/epsilon kind: Kustomization `, "delta/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component namespace: kustomize-namespace `, "delta/epsilon/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1alpha1 buildMetadata: - managedByLabel kind: Component `, }, }, { name: "localize_directory_name_when_no_remote", files: map[string]string{ "kustomization.yaml": fmt.Sprintf(`apiVersion: kustomize.config.k8s.io/v1beta1 components: - %s kind: Kustomization `, LocalizeDir), fmt.Sprintf("%s/kustomization.yaml", LocalizeDir): `apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component namespace: kustomize-namespace `, }, }, } { t.Run(tc.name, func(t *testing.T) { checkLocalizeInTargetSuccess(t, tc.files) }) } } func TestLocalizeDirCleanedSibling(t *testing.T) { kustAndComponents := map[string]string{ // This test checks that winding paths that might traverse through directories // outside of scope, which will not be present at destination, are cleaned. "beta/gamma/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 components: - delta/../../../../a/b/../../alpha/beta/sibling kind: Kustomization`, "beta/sibling/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component namespace: kustomize-namespace `, } expected, actual := makeFileSystems(t, "/alpha", kustAndComponents) checkRun(t, actual, "/alpha/beta/gamma", "/alpha", "/alpha/beta/dst") cleanedFiles := map[string]string{ "beta/gamma/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 components: - ../sibling kind: Kustomization `, "beta/sibling/kustomization.yaml": kustAndComponents["beta/sibling/kustomization.yaml"], } addFiles(t, expected, "/alpha/beta/dst", cleanedFiles) checkFSys(t, expected, actual) } func TestLocalizeBases(t *testing.T) { kustAndBases := map[string]string{ "kustomization.yaml": `bases: - b - c/d `, "b/kustomization.yaml": `kind: Kustomization `, "c/d/kustomization.yaml": `kind: Kustomization `, } checkLocalizeInTargetSuccess(t, kustAndBases) } func TestLocalizeComponents(t *testing.T) { kustAndComponents := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 components: - a - alpha kind: Kustomization `, "a/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component namePrefix: my- `, "alpha/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component nameSuffix: -test `, } checkLocalizeInTargetSuccess(t, kustAndComponents) } func TestLocalizeResources(t *testing.T) { kustAndResources := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - pod.yaml - ../../alpha `, "pod.yaml": podConfiguration, "../../alpha/kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namePrefix: my- `, } expected, actual := makeFileSystems(t, "/a/b", kustAndResources) checkRun(t, actual, "/a/b", "/", "/localized-b") addFiles(t, expected, "/localized-b/a/b", kustAndResources) checkFSys(t, expected, actual) } func TestLocalizePathError(t *testing.T) { kustAndResources := map[string]string{ "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - b `, } expected, actual := makeFileSystems(t, "/a", kustAndResources) _, err := Run("/a", "/", "", actual) const expectedFileErr = `invalid file reference: '/a/b' must resolve to a file` const expectedRootErr = `unable to localize root "b": unable to find one of 'kustomization.yaml', 'kustomization.yml' or 'Kustomization' in directory '/a/b'` var actualErr PathLocalizeError require.ErrorAs(t, err, &actualErr) require.Equal(t, "b", actualErr.Path) require.EqualError(t, actualErr.FileError, expectedFileErr) require.EqualError(t, actualErr.RootError, expectedRootErr) const expectedErrPrefix = `unable to localize target "/a": unable to localize resources entry` require.EqualError(t, err, fmt.Sprintf(`%s: could not localize path "b" as file: %s; could not localize path "b" as directory: %s`, expectedErrPrefix, expectedFileErr, expectedRootErr)) checkFSys(t, expected, actual) } func TestLocalizeHelmChartInflationGenerator(t *testing.T) { helmKust := map[string]string{ "kustomization.yaml": `helmChartInflationGenerator: - chartName: nothing-to-localize chartRepoUrl: https://itzg.github.io/warcraft-server-charts releaseName: moria - chartName: localize-values values: minecraftValues.yaml valuesLocal: minecraftServer: eula: true valuesMerge: replace - chartHome: home chartName: copy-chartHome `, "minecraftValues.yaml": valuesFile, "charts/localize-values/values.yaml": valuesFile, "home/copy-chartHome/values.yaml": valuesFile, } checkLocalizeInTargetSuccess(t, helmKust) } func TestLocalizeHelmCharts(t *testing.T) { for _, test := range []struct { name string files map[string]string }{ { name: "charts_only", files: map[string]string{ "kustomization.yaml": `helmCharts: - name: nothing-to-localize repo: https://helm.releases.hashicorp.com version: 1.0.0 - includeCRDs: true name: localize-valuesFile valuesFile: file - additionalValuesFiles: - another - third `, "file": valuesFile, "another": valuesFile, "third": valuesFile, "charts/nothing-to-localize/values.yaml": valuesFile, "charts/localize-valuesFile/values.yaml": valuesFile, }, }, { name: "charts_globals_no_home", files: map[string]string{ "kustomization.yaml": `helmCharts: - name: default helmGlobals: configHome: . `, "charts/default/values.yaml": valuesFile, }, }, { name: "home_only", files: map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: home `, "home/name/values.yaml": valuesFile, }, }, } { t.Run(test.name, func(t *testing.T) { checkLocalizeInTargetSuccess(t, test.files) }) } } func TestLocalizeHelmChartsNoDefault(t *testing.T) { files := map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: home `, "home/name/values.yaml": valuesFile, "charts/name/values.yaml": valuesFile, } expected, actual := makeFileSystems(t, "/a", files) checkRun(t, actual, "/a", "/a", "/dst") addFiles(t, expected, "/dst", map[string]string{ "kustomization.yaml": files["kustomization.yaml"], "home/name/values.yaml": valuesFile, }) checkFSys(t, expected, actual) } func TestCopyChartHomeSimple(t *testing.T) { for _, test := range []struct { name string files map[string]string }{ { name: "does_not_exist", files: map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: untar-dir `, }, }, { name: "chart_home_structure", files: map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: home `, "home/minecraft-3.1.3.tgz": "blah", "home/terraform-1.0.0.tgz": "la", "home/minecraft/Chart.yaml": `annotations: artifacthub.io/links: | - name: source url: https://minecraft.net/ `, "home/terraform/Chart.yaml": `description: Minecraft server `, }, }, } { t.Run(test.name, func(t *testing.T) { checkLocalizeInTargetSuccess(t, test.files) }) } } func TestCopyChartHomeChanges(t *testing.T) { for name, test := range map[string]struct { files map[string]string copiedFiles map[string]string }{ "clean_does_not_exist": { files: map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: ../b/home `, }, copiedFiles: map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: home `, }, }, "clean_default": { files: map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: ../b/charts `, "charts/name/values.yaml": valuesFile, }, copiedFiles: map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: charts `, "charts/name/values.yaml": valuesFile, }, }, "not_copied": { files: map[string]string{ "kustomization.yaml": `helmCharts: - name: name valuesFile: home/name/values.yaml helmGlobals: chartHome: home `, "home/name/values.yaml": valuesFile, "home/name/many-other-files": "other contents", }, copiedFiles: map[string]string{ "kustomization.yaml": `helmCharts: - name: name valuesFile: home/name/values.yaml helmGlobals: chartHome: home `, "home/name/values.yaml": valuesFile, }, }, "does_not_exist_exits_scope": { files: map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: ../home `, "../../home/will-exist-at-dst/values.yaml": valuesFile, }, copiedFiles: map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: ../home `, }, }, } { t.Run(name, func(t *testing.T) { expected, actual := makeFileSystems(t, "/a/b", test.files) checkRun(t, actual, "/a/b", "/a/b", "/dst") addFiles(t, expected, "/dst", test.copiedFiles) checkFSys(t, expected, actual) }) } } func TestCopyChartHomeEmpty(t *testing.T) { kustomization := map[string]string{ "kustomization.yaml": `helmGlobals: chartHome: home `, } expected, actual := makeFileSystems(t, "/a", kustomization) require.NoError(t, actual.Mkdir("/a/home")) require.NoError(t, expected.Mkdir("/a/home")) checkRun(t, actual, "/a", "/a", "/dst") addFiles(t, expected, "/dst", kustomization) require.NoError(t, expected.Mkdir("/dst/home")) checkFSys(t, expected, actual) } func TestCopyChartHomeError(t *testing.T) { for name, test := range map[string]struct { err string files map[string]string }{ "absolute": { err: `unable to copy helmGlobals: absolute path "/a/b/home" not handled in alpha`, files: map[string]string{ "a/b/kustomization.yaml": `helmGlobals: chartHome: /a/b/home `, "a/b/home/name/values.yaml": valuesFile, }, }, "file": { err: `unable to copy helmGlobals: unable to copy home "home": invalid chart home: invalid root reference: must build at directory: '/a/b/home': file is not directory`, files: map[string]string{ "a/b/kustomization.yaml": `helmGlobals: chartHome: home `, "a/b/home": valuesFile, }, }, "scope": { err: `unable to copy helmGlobals: unable to copy home "../../alpha/home": invalid chart home: root "/alpha/home" outside localize scope "/a"`, files: map[string]string{ "a/b/kustomization.yaml": `helmGlobals: chartHome: ../../alpha/home `, "alpha/home/values.yaml": valuesFile, }, }, } { t.Run(name, func(t *testing.T) { expected, actual := makeFileSystems(t, "/", test.files) _, err := Run("/a/b", "/a", "/dst", actual) const prefix = `unable to localize target "/a/b"` require.EqualError(t, err, fmt.Sprintf("%s: %s", prefix, test.err)) checkFSys(t, expected, actual) }) } } func TestLocalizeGeneratorsHelm(t *testing.T) { files := map[string]string{ "kustomization.yaml": `generators: - default.yaml - explicit.yaml `, "default.yaml": `apiVersion: builtin kind: HelmChartInflationGenerator metadata: name: no-explicit-references name: minecraft releaseName: moria repo: https://itzg.github.io/minecraft-server-charts version: 3.1.3 `, "explicit.yaml": `additionalValuesFiles: - time.yaml - life.yaml - light.yaml apiVersion: builtin chartHome: home kind: HelmChartInflationGenerator metadata: name: explicit-references name: mapleStory valuesFile: mapleValues.yaml `, "time.yaml": valuesFile, "life.yaml": valuesFile, "light.yaml": valuesFile, "mapleValues.yaml": valuesFile, "home/mapleStory/values.yaml": valuesFile, "charts/minecraft/values.yaml": valuesFile, } checkLocalizeInTargetSuccess(t, files) } func TestLocalizeGeneratorsNoHelm(t *testing.T) { files := map[string]string{ "kustomization.yaml": `generators: - configMap.yaml `, "configMap.yaml": `apiVersion: builtin kind: ConfigMapGenerator literals: - APPLE=orange metadata: name: not-helm-shouldn't-copy-default-helm-chart-home `, "charts/minecraft/values.yaml": valuesFile, } expected, actual := makeFileSystems(t, "/a", files) checkRun(t, actual, "/a", "/a", "/dst") addFiles(t, expected, "/dst", map[string]string{ "kustomization.yaml": files["kustomization.yaml"], "configMap.yaml": files["configMap.yaml"], }) checkFSys(t, expected, actual) } func TestLocalizeEmpty(t *testing.T) { for name, kustomization := range map[string]string{ "file": `configurations: - "" `, "root": `bases: - "" `, "resource": `resources: - "" `, "generator_file_src": `configMapGenerator: - files: - "" `, "patchesStrategicMerge": `patchesStrategicMerge: - "" `, "custom_transformers": `transformers: - "" `, "custom_transformer_field": `transformers: - | apiVersion: builtin kind: PatchStrategicMergeTransformer metadata: name: empty paths: - "" `, } { t.Run(name, func(t *testing.T) { checkLocalizeInTargetSuccess(t, map[string]string{ "kustomization.yaml": kustomization, }) }) } }