From fb294921f5c7ab44a8aa1999da8061349f172dc5 Mon Sep 17 00:00:00 2001 From: Anna Song Date: Thu, 26 Jan 2023 12:56:26 -0800 Subject: [PATCH] Localize helm fields (#4978) * Localize helm fields * Address readability feedback * Handle edge cases * Improve readability Split copyChartHome and use existence to prevent repeated copying. --- .../builtins/HelmChartInflationGenerator.go | 2 +- api/internal/localizer/localizer.go | 213 ++++++++++-- api/internal/localizer/localizer_test.go | 329 +++++++++++++++++- api/krusty/localizer/runner_test.go | 147 +++++++- api/types/helmchartargs.go | 2 + .../HelmChartInflationGenerator.go | 2 +- 6 files changed, 651 insertions(+), 44 deletions(-) diff --git a/api/internal/builtins/HelmChartInflationGenerator.go b/api/internal/builtins/HelmChartInflationGenerator.go index 887ba493c..24533ab69 100644 --- a/api/internal/builtins/HelmChartInflationGenerator.go +++ b/api/internal/builtins/HelmChartInflationGenerator.go @@ -83,7 +83,7 @@ func (p *HelmChartInflationGeneratorPlugin) validateArgs() (err error) { // the loader root (unless root restrictions are // disabled, in which case this can be an absolute path). if p.ChartHome == "" { - p.ChartHome = "charts" + p.ChartHome = types.HelmDefaultHome } // The ValuesFile may be consulted by the plugin, so it must diff --git a/api/internal/localizer/localizer.go b/api/internal/localizer/localizer.go index 88d2597bc..f3b72dfa5 100644 --- a/api/internal/localizer/localizer.go +++ b/api/internal/localizer/localizer.go @@ -4,7 +4,9 @@ package localizer import ( + "io/fs" "log" + "os" "path/filepath" "sigs.k8s.io/kustomize/api/ifc" @@ -119,11 +121,11 @@ func (lc *localizer) load() (*types.Kustomization, string, error) { // built-in understanding of. This excludes helm-related fields, such as `helmGlobals` and `helmCharts`. func (lc *localizer) localizeNativeFields(kust *types.Kustomization) error { if path, exists := kust.OpenAPI["path"]; exists { - newPath, err := lc.localizeFile(path) + locPath, err := lc.localizeFile(path) if err != nil { return errors.WrapPrefixf(err, "unable to localize openapi path") } - kust.OpenAPI["path"] = newPath + kust.OpenAPI["path"] = locPath } for fieldName, field := range map[string]struct { @@ -134,11 +136,11 @@ func (lc *localizer) localizeNativeFields(kust *types.Kustomization) error { // Allow use of deprecated field //nolint:staticcheck kust.Bases, - lc.localizeDir, + lc.localizeRoot, }, "components": { kust.Components, - lc.localizeDir, + lc.localizeRoot, }, "configurations": { kust.Configurations, @@ -172,6 +174,12 @@ func (lc *localizer) localizeNativeFields(kust *types.Kustomization) error { return errors.WrapPrefixf(err, "unable to localize secretGenerator") } } + if err := lc.localizeHelmInflationGenerator(kust); err != nil { + return err + } + if err := lc.localizeHelmCharts(kust); err != nil { + return err + } if err := lc.localizePatches(kust.Patches); err != nil { return errors.WrapPrefixf(err, "unable to localize patches") } @@ -181,34 +189,29 @@ func (lc *localizer) localizeNativeFields(kust *types.Kustomization) error { } //nolint:staticcheck for i, patch := range kust.PatchesStrategicMerge { - localizedPath, err := lc.localizeK8sResource(string(patch)) + locPath, err := lc.localizeK8sResource(string(patch)) if err != nil { return errors.WrapPrefixf(err, "unable to localize patchesStrategicMerge entry") } - if localizedPath != "" { - kust.PatchesStrategicMerge[i] = types.PatchStrategicMerge(localizedPath) + if locPath != "" { + kust.PatchesStrategicMerge[i] = types.PatchStrategicMerge(locPath) } } for i, replacement := range kust.Replacements { - if replacement.Path != "" { - newPath, err := lc.localizeFile(replacement.Path) - if err != nil { - return errors.WrapPrefixf(err, "unable to localize replacements entry") - } - kust.Replacements[i].Path = newPath + locPath, err := lc.localizeFile(replacement.Path) + if err != nil { + return errors.WrapPrefixf(err, "unable to localize replacements entry") } + kust.Replacements[i].Path = locPath } return nil } // localizeGenerator localizes the file paths on generator. -func (lc *localizer) localizeGenerator(generator *types.GeneratorArgs) (err error) { - var locEnvSrc string - if generator.EnvSource != "" { - locEnvSrc, err = lc.localizeFile(generator.EnvSource) - if err != nil { - return errors.WrapPrefixf(err, "unable to localize generator env file") - } +func (lc *localizer) localizeGenerator(generator *types.GeneratorArgs) error { + locEnvSrc, err := lc.localizeFile(generator.EnvSource) + if err != nil { + return errors.WrapPrefixf(err, "unable to localize generator env file") } locEnvs := make([]string, len(generator.EnvSources)) for i, env := range generator.EnvSources { @@ -238,16 +241,58 @@ func (lc *localizer) localizeGenerator(generator *types.GeneratorArgs) (err erro return nil } +// localizeHelmInflationGenerator localizes helmChartInflationGenerator on kust. +// localizeHelmInflationGenerator localizes values files and copies local chart homes. +func (lc *localizer) localizeHelmInflationGenerator(kust *types.Kustomization) error { + for i, chart := range kust.HelmChartInflationGenerator { + locFile, err := lc.localizeFile(chart.Values) + if err != nil { + return errors.WrapPrefixf(err, "unable to localize helmChartInflationGenerator entry %d values", i) + } + kust.HelmChartInflationGenerator[i].Values = locFile + + locDir, err := lc.copyChartHomeEntry(chart.ChartHome) + if err != nil { + return errors.WrapPrefixf(err, "unable to copy helmChartInflationGenerator entry %d", i) + } + kust.HelmChartInflationGenerator[i].ChartHome = locDir + } + return nil +} + +// localizeHelmCharts localizes helmCharts and helmGlobals on kust. +// localizeHelmCharts localizes values files and copies a local chart home. +func (lc *localizer) localizeHelmCharts(kust *types.Kustomization) error { + for i, chart := range kust.HelmCharts { + locFile, err := lc.localizeFile(chart.ValuesFile) + if err != nil { + return errors.WrapPrefixf(err, "unable to localize helmCharts entry %d valuesFile", i) + } + kust.HelmCharts[i].ValuesFile = locFile + } + if kust.HelmGlobals != nil { + locDir, err := lc.copyChartHomeEntry(kust.HelmGlobals.ChartHome) + if err != nil { + return errors.WrapPrefixf(err, "unable to copy helmGlobals") + } + kust.HelmGlobals.ChartHome = locDir + } else if len(kust.HelmCharts) > 0 { + _, err := lc.copyChartHomeEntry("") + if err != nil { + return errors.WrapPrefixf(err, "unable to copy default chart home") + } + } + return nil +} + // localizePatches localizes the file paths on patches if they are non-empty func (lc *localizer) localizePatches(patches []types.Patch) error { for i := range patches { - if patches[i].Path != "" { - newPath, err := lc.localizeFile(patches[i].Path) - if err != nil { - return err - } - patches[i].Path = newPath + locPath, err := lc.localizeFile(patches[i].Path) + if err != nil { + return err } + patches[i].Path = locPath } return nil } @@ -272,7 +317,7 @@ func (lc *localizer) localizeResource(path string) (string, error) { } if fileErr != nil { var rootErr error - locPath, rootErr = lc.localizeDir(path) + locPath, rootErr = lc.localizeRoot(path) if rootErr != nil { err := PathLocalizeError{ Path: path, @@ -285,8 +330,13 @@ func (lc *localizer) localizeResource(path string) (string, error) { return locPath, nil } -// localizeFile localizes file path and returns the localized path +// localizeFile localizes file path if set and returns the localized path func (lc *localizer) localizeFile(path string) (string, error) { + // Some localizable fields can be empty, for example, replacements.path. + // We rely on the build command to throw errors for the ones that cannot. + if path == "" { + return "", nil + } content, err := lc.ldr.Load(path) if err != nil { return "", errors.Wrap(err) @@ -324,8 +374,11 @@ func (lc *localizer) localizeFileWithContent(path string, content []byte) (strin return locPath, nil } -// localizeDir localizes root path and returns the localized path -func (lc *localizer) localizeDir(path string) (string, error) { +// localizeRoot localizes root path if set and returns the localized path +func (lc *localizer) localizeRoot(path string) (string, error) { + if path == "" { + return "", nil + } ldr, err := lc.ldr.New(path) if err != nil { return "", errors.Wrap(err) @@ -368,6 +421,106 @@ func (lc *localizer) localizeDir(path string) (string, error) { return locPath, nil } +// copyChartHomeEntry copies the helm chart home entry to lc dst +// at the same location relative to the root and returns said relative path. +// If entry is empty, copyChartHomeEntry returns the empty string. +// If entry does not exist, copyChartHome returns entry. +// +// copyChartHomeEntry copies the default home to the same location at dst, +// without following symlinks. An empty entry also indicates the default home. +func (lc *localizer) copyChartHomeEntry(entry string) (string, error) { + path := entry + if entry == "" { + path = types.HelmDefaultHome + } + if filepath.IsAbs(path) { + return "", errors.Errorf("absolute path %q not handled in alpha", path) + } + isDefault := lc.root.Join(path) == lc.root.Join(types.HelmDefaultHome) + locPath, err := lc.copyChartHome(path, !isDefault) + if err != nil { + return "", errors.WrapPrefixf(err, "unable to copy home %q", entry) + } + if entry == "" { + return "", nil + } + return locPath, nil +} + +// copyChartHome copies path relative to lc root to dst and returns the +// copied location relative to dst. If clean is true, copyChartHome uses path's +// delinked location as the copy destination. +// +// If path does not exist, copyChartHome returns path. +func (lc *localizer) copyChartHome(path string, clean bool) (string, error) { + path, err := filepath.Rel(lc.root.String(), lc.root.Join(path)) + if err != nil { + return "", errors.WrapPrefixf(err, "no path to chart home %q", path) + } + // Chart home may serve as untar destination. + // Note that we don't check if path is in scope. + if !lc.fSys.Exists(lc.root.Join(path)) { + return path, nil + } + // Perform localize directory checks. + ldr, err := lc.ldr.New(path) + if err != nil { + return "", errors.WrapPrefixf(err, "invalid chart home") + } + cleaned, err := filesys.ConfirmDir(lc.fSys, ldr.Root()) + if err != nil { + log.Panicf("unable to confirm validated directory %q: %s", ldr.Root(), err) + } + toDst := path + if clean { + toDst, err = filepath.Rel(lc.root.String(), cleaned.String()) + if err != nil { + log.Panicf("no path between scoped directories %q and %q: %s", lc.root, cleaned, err) + } + } + // Note this check does not guarantee that we copied the entire directory. + if dst := filepath.Join(lc.dst, toDst); !lc.fSys.Exists(dst) { + err = lc.copyDir(cleaned, filepath.Join(lc.dst, toDst)) + if err != nil { + return "", errors.WrapPrefixf(err, "unable to copy chart home %q", path) + } + } + return toDst, nil +} + +// copyDir copies src to dst. copyDir does not follow symlinks. +func (lc *localizer) copyDir(src filesys.ConfirmedDir, dst string) error { + err := lc.fSys.Walk(src.String(), + func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + pathToCreate, err := filepath.Rel(src.String(), path) + if err != nil { + log.Panicf("no path from %q to child file %q: %s", src, path, err) + } + pathInDst := filepath.Join(dst, pathToCreate) + if info.Mode()&os.ModeSymlink == os.ModeSymlink { + return nil + } + if info.IsDir() { + err = lc.fSys.MkdirAll(pathInDst) + } else { + var content []byte + content, err = lc.fSys.ReadFile(path) + if err != nil { + return errors.Wrap(err) + } + err = lc.fSys.WriteFile(pathInDst, content) + } + return errors.Wrap(err) + }) + if err != nil { + return errors.WrapPrefixf(err, "unable to copy directory %q", src) + } + return nil +} + // localizeBuiltinPlugins localizes built-in plugins on kust that can contain file paths. The built-in plugins // can be inline or in a file. This excludes the HelmChartInflationGenerator. // diff --git a/api/internal/localizer/localizer_test.go b/api/internal/localizer/localizer_test.go index f6b3197d0..817ce9005 100644 --- a/api/internal/localizer/localizer_test.go +++ b/api/internal/localizer/localizer_test.go @@ -72,6 +72,10 @@ replacements: delimiter: '=' index: 0 ` + + valuesFile = `minecraftServer: + difficulty: peaceful +` ) func makeMemoryFs(t *testing.T) filesys.FileSystem { @@ -208,10 +212,11 @@ patches: func TestLoadKustomizationName(t *testing.T) { kustomization := map[string]string{ "Kustomization": `apiVersion: kustomize.config.k8s.io/v1beta1 -commonLabels: - label-one: value-one - label-two: value-two kind: Kustomization +labels: +- pairs: + label-one: value-one + label-two: value-two `, } checkLocalizeInTargetSuccess(t, kustomization) @@ -235,14 +240,9 @@ func TestLoadGVKNN(t *testing.T) { func TestLoadLegacyFields(t *testing.T) { kustomization := map[string]string{ - // TODO(annasong): Adjust test once localize handles helm. "kustomization.yaml": `apiVersion: kustomize.config.k8s.io/v1beta1 -helmChartInflationGenerator: -- chartName: minecraft - chartRepoUrl: https://kubernetes-charts.storage.googleapis.com - chartVersion: v1.2.0 - releaseName: test - values: values.yaml +commonLabels: + app: bingo imageTags: - name: postgres newName: my-registry/my-postgres @@ -587,7 +587,7 @@ patches: expected, actual := makeFileSystems(t, "/a/b", kustAndPatch) err := Run("/a/b", "", "/dst", actual) - require.Error(t, err) + 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) } @@ -1124,3 +1124,310 @@ resources: 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 +`, + "file": 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) + + err := Run("/a", "", "/dst", actual) + require.NoError(t, err) + + 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) + + err := Run("/a/b", "/a/b", "/dst", actual) + require.NoError(t, err) + + 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")) + + err := Run("/a", "", "/dst", actual) + require.NoError(t, err) + + 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 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, + }) + }) + } +} diff --git a/api/krusty/localizer/runner_test.go b/api/krusty/localizer/runner_test.go index aba63eac4..f28e5b572 100644 --- a/api/krusty/localizer/runner_test.go +++ b/api/krusty/localizer/runner_test.go @@ -199,6 +199,10 @@ spec: maxReplicas: 10` urlQuery = "?submodules=0&ref=kustomize/v4.5.7&timeout=300" + + valuesFile = `minecraftServer: + difficulty: peaceful +` ) func link(t *testing.T, testDir filesys.ConfirmedDir, links map[string]string) { @@ -267,7 +271,7 @@ func TestWorkingDir(t *testing.T) { CheckFs(t, wd.String(), fsExpected, fsActual) } -func TestSymlinks(t *testing.T) { +func TestLoaderSymlinks(t *testing.T) { // test directory // - link to target // - link to base @@ -500,3 +504,144 @@ func TestExistingCacheDir(t *testing.T) { SetupDir(t, fsExpected, testDir.String(), file) CheckFs(t, testDir.String(), fsExpected, fsActual) } + +func TestHelmNestedHome(t *testing.T) { + files := map[string]string{ + "kustomization.yaml": fmt.Sprintf(`helmGlobals: + chartHome: %s +`, filepath.Join("nested", "dirs", "home")), + filepath.Join("nested", "dirs", "home", "name", "values.yaml"): ` +minecraftServer: + difficulty: peaceful +`, + } + fsExpected, fsActual, testDir := PrepareFs(t, []string{ + filepath.Join("nested", "dirs", "home", "name"), + }, files) + + dst := testDir.Join("dst") + err := localizer.Run(fsActual, testDir.String(), "", dst) + require.NoError(t, err) + + SetupDir(t, fsExpected, dst, files) + CheckFs(t, dst, fsExpected, fsActual) +} + +func TestHelmLinkedHome(t *testing.T) { + // scope + // - target + // - kustomization + // - myValues.yaml + // - link to home + // - home + // - name + // - values.yaml + fsExpected, fsActual, scope := PrepareFs(t, []string{ + "target", + filepath.Join("home", "name"), + }, + map[string]string{ + filepath.Join("target", "Kustomization"): `helmCharts: +- name: name + valuesFile: myValues.yaml +helmGlobals: + chartHome: home-link +`, + filepath.Join("target", "myValues.yaml"): valuesFile, + filepath.Join("home", "name", "values.yaml"): valuesFile, + }) + link(t, scope, map[string]string{ + filepath.Join("target", "home-link"): "home", + }) + + dst := scope.Join("dst") + err := localizer.Run(fsActual, scope.Join("target"), scope.String(), dst) + require.NoError(t, err) + + SetupDir(t, fsExpected, dst, map[string]string{ + filepath.Join("target", "Kustomization"): fmt.Sprintf(`helmCharts: +- name: name + valuesFile: myValues.yaml +helmGlobals: + chartHome: %s +`, filepath.Join("..", "home")), + filepath.Join("target", "myValues.yaml"): valuesFile, + filepath.Join("home", "name", "values.yaml"): valuesFile, + }) + CheckFs(t, dst, fsExpected, fsActual) +} + +func TestHelmLinkedDefaultHome(t *testing.T) { + // target + // - kustomization + // - link to home (named charts) + // - home + // - name + // - values.yaml + fsExpected, fsActual, target := PrepareFs(t, []string{ + filepath.Join("home", "default"), + filepath.Join("home", "same"), + }, map[string]string{ + "kustomization.yaml": fmt.Sprintf(`helmCharts: +- name: default +helmChartInflationGenerator: +- chartHome: %s + chartName: same +`, filepath.Join("home", "..", "charts")), + filepath.Join("home", "default", "values.yaml"): valuesFile, + filepath.Join("home", "same", "values.yaml"): valuesFile, + }) + link(t, target, map[string]string{"charts": "home"}) + + dst := target.Join("dst") + err := localizer.Run(fsActual, target.String(), "", dst) + require.NoError(t, err) + + SetupDir(t, fsExpected, dst, map[string]string{ + "kustomization.yaml": `helmChartInflationGenerator: +- chartHome: charts + chartName: same +helmCharts: +- name: default +`, + filepath.Join("charts", "default", "values.yaml"): valuesFile, + filepath.Join("charts", "same", "values.yaml"): valuesFile, + }) + CheckFs(t, dst, fsExpected, fsActual) +} + +func TestHelmHomeEscapesScope(t *testing.T) { + // test directory + // - dir + // - file + // - target (and scope) + // - kustomization + // - home + // - link to dir + // - link to file + fsExpected, fsActual, testDir := PrepareFs(t, []string{ + "dir", + filepath.Join("target", "home"), + }, map[string]string{ + "file": valuesFile, + filepath.Join("target", "kustomization.yaml"): `helmGlobals: + chartHome: home +`, + }) + link(t, testDir, map[string]string{ + filepath.Join("target", "home", "dir-link"): "dir", + filepath.Join("target", "home", "file-link"): "file", + }) + + dst := testDir.Join("dst") + err := localizer.Run(fsActual, testDir.Join("target"), "", dst) + require.NoError(t, err) + + SetupDir(t, fsExpected, dst, map[string]string{ + "kustomization.yaml": `helmGlobals: + chartHome: home +`, + }) + require.NoError(t, fsExpected.Mkdir(filepath.Join(dst, "home"))) + CheckFs(t, dst, fsExpected, fsActual) +} diff --git a/api/types/helmchartargs.go b/api/types/helmchartargs.go index 693012275..bbf7f5ff5 100644 --- a/api/types/helmchartargs.go +++ b/api/types/helmchartargs.go @@ -3,6 +3,8 @@ package types +const HelmDefaultHome = "charts" + type HelmGlobals struct { // ChartHome is a file path, relative to the kustomization root, // to a directory containing a subdirectory for each chart to be diff --git a/plugin/builtin/helmchartinflationgenerator/HelmChartInflationGenerator.go b/plugin/builtin/helmchartinflationgenerator/HelmChartInflationGenerator.go index 1956bac39..d0dee8bae 100644 --- a/plugin/builtin/helmchartinflationgenerator/HelmChartInflationGenerator.go +++ b/plugin/builtin/helmchartinflationgenerator/HelmChartInflationGenerator.go @@ -89,7 +89,7 @@ func (p *plugin) validateArgs() (err error) { // the loader root (unless root restrictions are // disabled, in which case this can be an absolute path). if p.ChartHome == "" { - p.ChartHome = "charts" + p.ChartHome = types.HelmDefaultHome } // The ValuesFile may be consulted by the plugin, so it must