diff --git a/pkg/action/action.go b/pkg/action/action.go index c2a27940f..c0a444032 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -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 } } diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 85ee42d64..5e57abfa6 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -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) + }) + } +} diff --git a/pkg/action/install.go b/pkg/action/install.go index 0fe1f1a6e..2b589cad9 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -127,6 +127,12 @@ type Install struct { // Used by helm template to add the release as part of OutputDir path // OutputDir/ 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() diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 4c93855b1..a25be7b75 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -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 } diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 14f85042b..e4eb678e7 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -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, "/") +}