feat(template): add --skip-chart-dir and --skip-templates-dir flags

Add two new flags to make --output-dir fully configurable:

- --skip-chart-dir: skips adding the chart name directory
- --skip-templates-dir: skips adding the templates subdirectory

This allows users to customize the output directory structure for better
alignment with GitOps workflows where manifests need to be stored in
specific file structures.

Example usage:
  helm template my-app ./mychart --output-dir gitops/cluster/app \
    --skip-chart-dir --skip-templates-dir

Before: gitops/cluster/app/mychart/templates/deployment.yaml
After:  gitops/cluster/app/deployment.yaml

Fixes #31747

Signed-off-by: Dmitriy Aratin <dima.aratin@mail.ru>
pull/31762/head
Dmitriy Aratin 1 week ago
parent f928025cdb
commit 5bbe84ec4f

@ -213,13 +213,48 @@ func splitAndDeannotate(postrendered string) (map[string]string, error) {
return reconstructed, nil
}
// transformManifestPath modifies the manifest path based on the skipChartNameDir and skipTemplatesDir flags.
// The input path is typically in the format "chart-name/templates/file.yaml" or "chart-name/charts/subchart/templates/file.yaml"
// - skipChartNameDir: removes the root chart name directory
// - skipTemplatesDir: removes all "templates" directories from the path
func transformManifestPath(name string, skipChartNameDir, skipTemplatesDir bool) string {
if !skipChartNameDir && !skipTemplatesDir {
return name
}
parts := strings.Split(name, "/")
if len(parts) == 0 {
return name
}
var result []string
for i, part := range parts {
// Skip the first part (chart name) if skipChartNameDir is true
if i == 0 && skipChartNameDir {
continue
}
// Skip "templates" directories if skipTemplatesDir is true
if skipTemplatesDir && part == "templates" {
continue
}
result = append(result, part)
}
if len(result) == 0 {
return name
}
return strings.Join(result, "/")
}
// renderResources renders the templates in a chart
//
// TODO: This function is badly in need of a refactor.
// TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed
//
// This code has to do with writing files to disk.
func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) {
func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret, skipChartNameDir, skipTemplatesDir bool) ([]*release.Hook, *bytes.Buffer, string, error) {
var hs []*release.Hook
b := bytes.NewBuffer(nil)
@ -336,11 +371,12 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
if outputDir == "" {
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Filename, string(crd.File.Data[:]))
} else {
err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Filename])
transformedName := transformManifestPath(crd.Filename, skipChartNameDir, skipTemplatesDir)
err = writeToFile(outputDir, transformedName, string(crd.File.Data[:]), fileWritten[transformedName])
if err != nil {
return hs, b, "", err
}
fileWritten[crd.Filename] = true
fileWritten[transformedName] = true
}
}
}
@ -361,11 +397,12 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
// output dir is only used by `helm template`. In the next major
// release, we should move this logic to template only as it is not
// used by install or upgrade
err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
transformedName := transformManifestPath(m.Name, skipChartNameDir, skipTemplatesDir)
err = writeToFile(newDir, transformedName, m.Content, fileWritten[transformedName])
if err != nil {
return hs, b, "", err
}
fileWritten[m.Name] = true
fileWritten[transformedName] = true
}
}

@ -799,7 +799,7 @@ func TestRenderResources_PostRenderer_Success(t *testing.T) {
hooks, buf, notes, err := cfg.renderResources(
ch, values, "test-release", "", false, false, false,
mockPR, false, false, false,
mockPR, false, false, false, false, false,
)
assert.NoError(t, err)
@ -842,7 +842,7 @@ func TestRenderResources_PostRenderer_Error(t *testing.T) {
_, _, _, err := cfg.renderResources(
ch, values, "test-release", "", false, false, false,
mockPR, false, false, false,
mockPR, false, false, false, false, false,
)
assert.Error(t, err)
@ -870,7 +870,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) {
_, _, _, err := cfg.renderResources(
ch, values, "test-release", "", false, false, false,
mockPR, false, false, false,
mockPR, false, false, false, false, false,
)
assert.Error(t, err)
@ -892,7 +892,7 @@ func TestRenderResources_PostRenderer_SplitError(t *testing.T) {
_, _, _, err := cfg.renderResources(
ch, values, "test-release", "", false, false, false,
mockPR, false, false, false,
mockPR, false, false, false, false, false,
)
assert.Error(t, err)
@ -913,7 +913,7 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) {
hooks, buf, notes, err := cfg.renderResources(
ch, values, "test-release", "", false, false, false,
mockPR, false, false, false,
mockPR, false, false, false, false, false,
)
assert.NoError(t, err)
@ -949,7 +949,7 @@ func TestRenderResources_NoPostRenderer(t *testing.T) {
hooks, buf, notes, err := cfg.renderResources(
ch, values, "test-release", "", false, false, false,
nil, false, false, false,
nil, false, false, false, false, false,
)
assert.NoError(t, err)
@ -974,3 +974,98 @@ func TestInteractWithServer(t *testing.T) {
assert.False(t, interactWithServer(DryRunClient))
assert.True(t, interactWithServer(DryRunServer))
}
func TestTransformManifestPath(t *testing.T) {
tests := []struct {
name string
input string
skipChartNameDir bool
skipTemplatesDir bool
expected string
}{
{
name: "no transformation",
input: "mychart/templates/deployment.yaml",
skipChartNameDir: false,
skipTemplatesDir: false,
expected: "mychart/templates/deployment.yaml",
},
{
name: "skip chart name only",
input: "mychart/templates/deployment.yaml",
skipChartNameDir: true,
skipTemplatesDir: false,
expected: "templates/deployment.yaml",
},
{
name: "skip templates dir only",
input: "mychart/templates/deployment.yaml",
skipChartNameDir: false,
skipTemplatesDir: true,
expected: "mychart/deployment.yaml",
},
{
name: "skip both chart name and templates dir",
input: "mychart/templates/deployment.yaml",
skipChartNameDir: true,
skipTemplatesDir: true,
expected: "deployment.yaml",
},
{
name: "subchart path - skip chart name",
input: "mychart/charts/subchart/templates/deployment.yaml",
skipChartNameDir: true,
skipTemplatesDir: false,
expected: "charts/subchart/templates/deployment.yaml",
},
{
name: "subchart path - skip templates",
input: "mychart/charts/subchart/templates/deployment.yaml",
skipChartNameDir: false,
skipTemplatesDir: true,
expected: "mychart/charts/subchart/deployment.yaml",
},
{
name: "subchart path - skip both",
input: "mychart/charts/subchart/templates/deployment.yaml",
skipChartNameDir: true,
skipTemplatesDir: true,
expected: "charts/subchart/deployment.yaml",
},
{
name: "crds path - skip chart name",
input: "mychart/crds/crd.yaml",
skipChartNameDir: true,
skipTemplatesDir: false,
expected: "crds/crd.yaml",
},
{
name: "crds path - skip templates (no effect)",
input: "mychart/crds/crd.yaml",
skipChartNameDir: false,
skipTemplatesDir: true,
expected: "mychart/crds/crd.yaml",
},
{
name: "single filename - skip both returns original",
input: "deployment.yaml",
skipChartNameDir: true,
skipTemplatesDir: true,
expected: "deployment.yaml",
},
{
name: "empty string",
input: "",
skipChartNameDir: true,
skipTemplatesDir: true,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := transformManifestPath(tt.input, tt.skipChartNameDir, tt.skipTemplatesDir)
assert.Equal(t, tt.expected, result)
})
}
}

@ -127,6 +127,12 @@ type Install struct {
// Used by helm template to add the release as part of OutputDir path
// OutputDir/<ReleaseName>
UseReleaseName bool
// SkipChartNameDir skips adding the chart name directory when writing to OutputDir
// When true: OutputDir/templates/file.yaml instead of OutputDir/chart-name/templates/file.yaml
SkipChartNameDir bool
// SkipTemplatesDir skips adding the "templates" subdirectory when writing to OutputDir
// When true: OutputDir/chart-name/file.yaml instead of OutputDir/chart-name/templates/file.yaml
SkipTemplatesDir bool
// TakeOwnership will ignore the check for helm annotations and take ownership of the resources.
TakeOwnership bool
PostRenderer postrenderer.PostRenderer
@ -355,7 +361,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
rel := i.createRelease(chrt, vals, i.Labels)
var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret)
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret, i.SkipChartNameDir, i.SkipTemplatesDir)
// Even for errors, attach this if available
if manifestDoc != nil {
rel.Manifest = manifestDoc.String()

@ -296,7 +296,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
return nil, nil, false, err
}
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret)
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret, false, false)
if err != nil {
return nil, nil, false, err
}

@ -132,12 +132,13 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if client.UseReleaseName {
newDir = filepath.Join(client.OutputDir, client.ReleaseName)
}
_, err := os.Stat(filepath.Join(newDir, m.Path))
transformedPath := transformManifestPath(m.Path, client.SkipChartNameDir, client.SkipTemplatesDir)
_, err := os.Stat(filepath.Join(newDir, transformedPath))
if err == nil {
fileWritten[m.Path] = true
fileWritten[transformedPath] = true
}
err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path])
err = writeToFile(newDir, transformedPath, m.Manifest, fileWritten[transformedPath])
if err != nil {
return err
}
@ -214,6 +215,8 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
f.StringSliceVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions (multiple can be specified)")
f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
f.BoolVar(&client.SkipChartNameDir, "skip-chart-dir", false, "skip adding the chart name directory when writing to output-dir")
f.BoolVar(&client.SkipTemplatesDir, "skip-templates-dir", false, "skip adding the templates subdirectory when writing to output-dir")
f.String(
"dry-run",
"client",
@ -275,3 +278,38 @@ func ensureDirectoryForFile(file string) error {
return os.MkdirAll(baseDir, 0755)
}
// transformManifestPath modifies the manifest path based on the skipChartNameDir and skipTemplatesDir flags.
// The input path is typically in the format "chart-name/templates/file.yaml" or "chart-name/charts/subchart/templates/file.yaml"
// - skipChartNameDir: removes the root chart name directory
// - skipTemplatesDir: removes all "templates" directories from the path
func transformManifestPath(name string, skipChartNameDir, skipTemplatesDir bool) string {
if !skipChartNameDir && !skipTemplatesDir {
return name
}
parts := strings.Split(name, "/")
if len(parts) == 0 {
return name
}
var result []string
for i, part := range parts {
// Skip the first part (chart name) if skipChartNameDir is true
if i == 0 && skipChartNameDir {
continue
}
// Skip "templates" directories if skipTemplatesDir is true
if skipTemplatesDir && part == "templates" {
continue
}
result = append(result, part)
}
if len(result) == 0 {
return name
}
return strings.Join(result, "/")
}

Loading…
Cancel
Save