From 0a4b962d4eda03bf4ffebacc5fc0e9fc5eb42fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=8E=AE=E6=96=87?= Date: Wed, 23 Sep 2026 20:51:35 +0800 Subject: [PATCH 1/3] refactor(action): return []byte from renderResources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderResources returned *bytes.Buffer, but every caller only ever read it — .String() in install/upgrade/tests, .Bytes() once in upgrade for validateManifest (which takes []byte natively). Internally b was used purely as an io.Writer (five fmt.Fprintf sites, no Grow/Reset/WriteTo). The *bytes.Buffer type bought nothing on either side; it only forced a dead "if manifestDoc != nil" guard in install (renderResources always returned a non-nil buffer, so the guard was always true) and a string->buffer round-trip (install did bytes.NewBufferString(rel.Manifest) to feed KubeClient.Build, despite having just held the bytes). Return []byte instead: b is a []byte written via fmt.Appendf, returns stay "return hs, b, ...", callers take the bytes directly. install passes bytes.NewReader(manifest) to Build (no round-trip) and upgrade hands the slice straight to validateManifest. Pure mechanical refactor, no behavior change. pkg/action and pkg/cmd tests pass. Signed-off-by: 胡玮文 --- pkg/action/action.go | 14 +++++++------- pkg/action/action_test.go | 10 +++++----- pkg/action/install.go | 10 ++++------ pkg/action/upgrade.go | 6 +++--- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index e93d6181f..015fa2736 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -276,9 +276,9 @@ func splitAndDeannotate(postrendered, fallbackPrefix string) (map[string]string, // 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(ctx context.Context, ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool, postRenderStrategy PostRenderStrategy) ([]*release.Hook, *bytes.Buffer, string, error) { +func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool, postRenderStrategy PostRenderStrategy) ([]*release.Hook, []byte, string, error) { var hs []*release.Hook - b := bytes.NewBuffer(nil) + var b []byte caps, err := cfg.getCapabilities() if err != nil { @@ -355,7 +355,7 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, if strings.TrimSpace(content) == "" { continue } - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) + b = fmt.Appendf(b, "---\n# Source: %s\n%s\n", name, content) } return hs, b, "", err } @@ -473,7 +473,7 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, if strings.TrimSpace(content) == "" { continue } - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) + b = fmt.Appendf(b, "---\n# Source: %s\n%s\n", name, content) } return hs, b, "", err } @@ -484,7 +484,7 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, if includeCrds { for _, crd := range ch.CRDObjects() { if outputDir == "" { - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Filename, string(crd.File.Data)) + b = fmt.Appendf(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]) if err != nil { @@ -498,9 +498,9 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, for _, m := range manifests { if outputDir == "" { if hideSecret && m.Head.Kind == "Secret" && m.Head.Version == "v1" { - fmt.Fprintf(b, "---\n# Source: %s\n# HIDDEN: The Secret output has been suppressed\n", m.Name) + b = fmt.Appendf(b, "---\n# Source: %s\n# HIDDEN: The Secret output has been suppressed\n", m.Name) } else { - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) + b = fmt.Appendf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) } } else { newDir := outputDir diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 056c539a5..d4294f00a 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -1845,7 +1845,7 @@ data: name: value ` - assert.Equal(t, expectedBuf, buf.String()) + assert.Equal(t, expectedBuf, string(buf)) assert.Len(t, hooks, 1) assert.Equal(t, expectedHook, hooks[0].Manifest) } @@ -1941,7 +1941,7 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) { assert.Empty(t, notes) // Notes should be empty for this test // Verify that the post-renderer modifications are present in the output - output := buf.String() + output := string(buf) expected := `--- # Source: hello/templates/goodbye goodbye: world @@ -2036,8 +2036,8 @@ spec: require.NoError(t, err) assert.Len(t, hooks, 1) assert.Equal(t, "my-app", hooks[0].Name) - assert.Contains(t, buf.String(), "kind: Deployment") - assert.Contains(t, buf.String(), "kind: ServiceAccount") + assert.Contains(t, string(buf), "kind: Deployment") + assert.Contains(t, string(buf), "kind: ServiceAccount") } func TestRenderResources_PostRenderer_CombinedInvokesOnceWithEverything(t *testing.T) { @@ -2221,7 +2221,7 @@ metadata: // Hooks still round-trip through the release so they can execute. require.Len(t, hooks, 1) assert.Contains(t, hooks[0].Manifest, "hook-cm") - assert.Contains(t, manifestDoc.String(), "template-cm") + assert.Contains(t, string(manifestDoc), "template-cm") } func TestRenderResources_PostRenderer_NoHooksWithOnlyHooks(t *testing.T) { diff --git a/pkg/action/install.go b/pkg/action/install.go index 6fc919366..c2c054968 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -374,12 +374,10 @@ 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(ctx, chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret, i.PostRenderStrategy) + var manifest []byte + rel.Hooks, manifest, rel.Info.Notes, err = i.cfg.renderResources(ctx, chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret, i.PostRenderStrategy) // Even for errors, attach this if available - if manifestDoc != nil { - rel.Manifest = manifestDoc.String() - } + rel.Manifest = string(manifest) // Check error from render if err != nil { rel.SetStatus(rcommon.StatusFailed, "failed to render resource: "+err.Error()) @@ -391,7 +389,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st rel.SetStatus(rcommon.StatusPendingInstall, "Initial install underway") var toBeAdopted kube.ResourceList - resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation) + resources, err := i.cfg.KubeClient.Build(bytes.NewReader(manifest), !i.DisableOpenAPIValidation) if err != nil { return nil, fmt.Errorf("unable to build kubernetes objects from release manifest: %w", err) } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 85904124b..ffc013306 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -299,7 +299,7 @@ func (u *Upgrade) prepareUpgrade(ctx context.Context, name string, chart *chartv return nil, nil, false, err } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(ctx, chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret, u.PostRenderStrategy) + hooks, manifest, notesTxt, err := u.cfg.renderResources(ctx, chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret, u.PostRenderStrategy) if err != nil { return nil, nil, false, err } @@ -328,7 +328,7 @@ func (u *Upgrade) prepareUpgrade(ctx context.Context, name string, chart *chartv Description: "Preparing upgrade", // This should be overwritten later. }, Version: revision, - Manifest: manifestDoc.String(), + Manifest: string(manifest), Hooks: hooks, Labels: mergeCustomLabels(lastRelease.Labels, u.Labels), ApplyMethod: string(determineReleaseSSApplyMethod(serverSideApply)), @@ -337,7 +337,7 @@ func (u *Upgrade) prepareUpgrade(ctx context.Context, name string, chart *chartv if notesTxt != "" { upgradedRelease.Info.Notes = notesTxt } - err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes(), !u.DisableOpenAPIValidation) + err = validateManifest(u.cfg.KubeClient, manifest, !u.DisableOpenAPIValidation) return currentRelease, upgradedRelease, serverSideApply, err } From bcc3acd9fe46fb9b227118c70d8db5c2c67bd819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=8E=AE=E6=96=87?= Date: Wed, 23 Sep 2026 23:58:30 +0800 Subject: [PATCH 2/3] fix(action): preserve trailing newlines when writing manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manifest splitting preserves trailing newlines so post-renderers can parse block scalars correctly. The output writers still append another newline, changing |+ and >+ values in installed resources as well as adding blank lines to rendered YAML. Trim one newline before the format string adds it back. This preserves all existing newlines and adds one only if missing. Document separators need a preceding line break; adding a missing newline also matches how Kubernetes' YAMLOrJSONDecoder handles an unterminated final line. Apply this rule to renderResources and its output-dir writer, leaving manifest splitting and post-renderer parsing unchanged. Test all six block scalar indicators with zero, one and two authored newlines, in final and non-final positions. Check exact output bytes and decoded values for both buffer and file output, including Kubernetes' EOF normalization. Signed-off-by: 胡玮文 --- pkg/action/action.go | 18 +++-- pkg/action/action_test.go | 78 +++++++++++++++++-- pkg/action/install.go | 2 +- pkg/action/testdata/rbac.txt | 2 - .../install-dry-run-with-secret-hidden.txt | 1 - .../output/install-dry-run-with-secret.txt | 2 - pkg/cmd/testdata/output/issue-9027.txt | 1 - pkg/cmd/testdata/output/object-order.txt | 13 ---- .../output/template-name-template.txt | 5 -- pkg/cmd/testdata/output/template-set.txt | 5 -- .../output/template-show-only-glob.txt | 2 - .../output/template-show-only-multiple.txt | 1 - .../testdata/output/template-skip-tests.txt | 5 -- .../output/template-subchart-cm-set-file.txt | 5 -- .../output/template-subchart-cm-set.txt | 5 -- .../testdata/output/template-subchart-cm.txt | 5 -- .../testdata/output/template-values-files.txt | 5 -- .../output/template-with-api-version.txt | 5 -- .../testdata/output/template-with-crds.txt | 6 -- .../output/template-with-kube-version.txt | 5 -- pkg/cmd/testdata/output/template.txt | 5 -- 21 files changed, 86 insertions(+), 90 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index 015fa2736..90b57c97e 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -270,6 +270,14 @@ func splitAndDeannotate(postrendered, fallbackPrefix string) (map[string]string, return reconstructed, nil } +func appendSourceManifest(b []byte, source, body string) []byte { + // Preserve existing trailing newlines: block scalars may retain them. + // Adding a missing newline can change a scalar's value, but document + // separators require a line break and Kubernetes' YAMLOrJSONDecoder + // also adds one to an unterminated final line. + return fmt.Appendf(b, "---\n# Source: %s\n%s\n", source, strings.TrimSuffix(body, "\n")) +} + // renderResources renders the templates in a chart // // TODO: This function is badly in need of a refactor. @@ -355,7 +363,7 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, if strings.TrimSpace(content) == "" { continue } - b = fmt.Appendf(b, "---\n# Source: %s\n%s\n", name, content) + b = appendSourceManifest(b, name, content) } return hs, b, "", err } @@ -473,7 +481,7 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, if strings.TrimSpace(content) == "" { continue } - b = fmt.Appendf(b, "---\n# Source: %s\n%s\n", name, content) + b = appendSourceManifest(b, name, content) } return hs, b, "", err } @@ -484,7 +492,7 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, if includeCrds { for _, crd := range ch.CRDObjects() { if outputDir == "" { - b = fmt.Appendf(b, "---\n# Source: %s\n%s\n", crd.Filename, string(crd.File.Data)) + b = appendSourceManifest(b, crd.Filename, string(crd.File.Data)) } else { err = writeToFile(outputDir, crd.Filename, string(crd.File.Data), fileWritten[crd.Filename]) if err != nil { @@ -498,9 +506,9 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, for _, m := range manifests { if outputDir == "" { if hideSecret && m.Head.Kind == "Secret" && m.Head.Version == "v1" { - b = fmt.Appendf(b, "---\n# Source: %s\n# HIDDEN: The Secret output has been suppressed\n", m.Name) + b = appendSourceManifest(b, m.Name, "# HIDDEN: The Secret output has been suppressed\n") } else { - b = fmt.Appendf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) + b = appendSourceManifest(b, m.Name, m.Content) } } else { newDir := outputDir diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index d4294f00a..58cc2d4d1 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -22,12 +22,16 @@ import ( "fmt" "io" "log/slog" + "os" + "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" + k8syaml "k8s.io/apimachinery/pkg/util/yaml" fakeclientset "k8s.io/client-go/kubernetes/fake" "helm.sh/helm/v4/internal/logging" @@ -1826,15 +1830,12 @@ func TestRenderResources_PostRenderer_Success(t *testing.T) { expectedBuf := `--- # Source: yellow/templates/foodpie foodpie: world - --- # Source: yellow/templates/with-partials yellow: Earth - --- # Source: yellow/templates/yellow yellow: world - ` expectedHook := `kind: ConfigMap metadata: @@ -1946,17 +1947,14 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) { # Source: hello/templates/goodbye goodbye: world color: blue - --- # Source: hello/templates/hello hello: world color: blue - --- # Source: hello/templates/with-partials hello: Earth color: blue - ` assert.Contains(t, output, "color: blue") assert.Equal(t, 3, strings.Count(output, "color: blue")) @@ -2276,6 +2274,74 @@ metadata: assert.ErrorContains(t, err, "bogus") } +func TestRenderResources_BlockScalarChomping(t *testing.T) { + tests := []struct { + name string + indicator string + value string + wantNewlines [3]int // indexed by the number of authored trailing newlines + }{ + {"literal_clip", "|", "line1\nline2", [3]int{1, 1, 1}}, + {"literal_strip", "|-", "line1\nline2", [3]int{0, 0, 0}}, + {"literal_keep", "|+", "line1\nline2", [3]int{1, 1, 2}}, + {"folded_clip", ">", "line1 line2", [3]int{1, 1, 1}}, + {"folded_strip", ">-", "line1 line2", [3]int{0, 0, 0}}, + {"folded_keep", ">+", "line1 line2", [3]int{1, 1, 2}}, + } + for _, tc := range tests { + for trailing := range 3 { + for _, output := range []string{"buffer", "output-dir"} { + t.Run(fmt.Sprintf("%s/%d_newlines/%s", tc.name, trailing, output), func(t *testing.T) { + var files []*common.File + var wantOutput strings.Builder + for _, name := range []string{"first", "last"} { + body := fmt.Sprintf("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: %s\ndata:\n cfg: %s\n line1\n line2", name, tc.indicator) + path := "templates/" + name + ".yaml" + files = append(files, &common.File{Name: path, Data: []byte(body + strings.Repeat("\n", trailing))}) + fmt.Fprintf(&wantOutput, "---\n# Source: hello/%s\n%s%s", path, body, strings.Repeat("\n", max(1, trailing))) + } + + outputDir := "" + if output == "output-dir" { + outputDir = t.TempDir() + } + cfg := actionConfigFixture(t) + _, rendered, _, err := cfg.renderResources( + t.Context(), buildChartWithTemplates(files), nil, "test-release", outputDir, false, false, false, + nil, false, false, false, PostRenderStrategyCombined, + ) + require.NoError(t, err) + if outputDir != "" { + assert.Empty(t, rendered) + for _, file := range files { + data, err := os.ReadFile(filepath.Join(outputDir, "hello", file.Name)) + require.NoError(t, err) + rendered = append(rendered, data...) + } + } + assert.Equal(t, wantOutput.String(), string(rendered)) + + var cm struct { + Data map[string]string `json:"data" yaml:"data"` + } + wantValue := tc.value + strings.Repeat("\n", tc.wantNewlines[trailing]) + // Kubernetes also normalizes a missing newline in a standalone document. + require.NoError(t, k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(files[0].Data), 4096).Decode(&cm)) + assert.Equal(t, wantValue, cm.Data["cfg"]) + + // Decode the whole stream without Kubernetes' implicit EOF newline. + decoder := yaml.NewDecoder(bytes.NewReader(rendered)) + for range 2 { + require.NoError(t, decoder.Decode(&cm)) + assert.Equal(t, wantValue, cm.Data["cfg"]) + } + require.ErrorIs(t, decoder.Decode(&cm), io.EOF) + }) + } + } + } +} + func TestDetermineReleaseSSAApplyMethod(t *testing.T) { assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false)) assert.Equal(t, release.ApplyMethodServerSideApply, determineReleaseSSApplyMethod(true)) diff --git a/pkg/action/install.go b/pkg/action/install.go index c2c054968..5e6b08aff 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -730,7 +730,7 @@ func writeToFile(outputDir, name, data string, appendData bool) error { defer f.Close() - _, err = fmt.Fprintf(f, "---\n# Source: %s\n%s\n", name, data) + _, err = fmt.Fprintf(f, "---\n# Source: %s\n%s\n", name, strings.TrimSuffix(data, "\n")) if err != nil { return err } diff --git a/pkg/action/testdata/rbac.txt b/pkg/action/testdata/rbac.txt index c8c2fd66e..12f102137 100644 --- a/pkg/action/testdata/rbac.txt +++ b/pkg/action/testdata/rbac.txt @@ -9,7 +9,6 @@ rules: resources: ["pods", "pods/exec", "pods/log"] verbs: ["*"] - --- # Source: hello/templates/rbac apiVersion: rbac.authorization.k8s.io/v1 @@ -25,4 +24,3 @@ subjects: - kind: ServiceAccount name: schedule-agents namespace: spaced - diff --git a/pkg/cmd/testdata/output/install-dry-run-with-secret-hidden.txt b/pkg/cmd/testdata/output/install-dry-run-with-secret-hidden.txt index c2219d8c4..eb770967f 100644 --- a/pkg/cmd/testdata/output/install-dry-run-with-secret-hidden.txt +++ b/pkg/cmd/testdata/output/install-dry-run-with-secret-hidden.txt @@ -19,4 +19,3 @@ metadata: data: foo: bar - diff --git a/pkg/cmd/testdata/output/install-dry-run-with-secret.txt b/pkg/cmd/testdata/output/install-dry-run-with-secret.txt index 62bd78018..d22c1437f 100644 --- a/pkg/cmd/testdata/output/install-dry-run-with-secret.txt +++ b/pkg/cmd/testdata/output/install-dry-run-with-secret.txt @@ -15,7 +15,6 @@ metadata: name: test-secret stringData: foo: bar - --- # Source: chart-with-secret/templates/configmap.yaml apiVersion: v1 @@ -25,4 +24,3 @@ metadata: data: foo: bar - diff --git a/pkg/cmd/testdata/output/issue-9027.txt b/pkg/cmd/testdata/output/issue-9027.txt index f43032499..1227336e4 100644 --- a/pkg/cmd/testdata/output/issue-9027.txt +++ b/pkg/cmd/testdata/output/issue-9027.txt @@ -15,7 +15,6 @@ hash: key4: 4 key5: 5 key6: 6 - --- # Source: issue-9027/templates/values.yaml global: diff --git a/pkg/cmd/testdata/output/object-order.txt b/pkg/cmd/testdata/output/object-order.txt index 96e3ba053..8ca93377c 100644 --- a/pkg/cmd/testdata/output/object-order.txt +++ b/pkg/cmd/testdata/output/object-order.txt @@ -11,7 +11,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/01-a.yml # 2 @@ -25,7 +24,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/01-a.yml # 3 @@ -39,7 +37,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 5 @@ -53,7 +50,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 7 @@ -67,7 +63,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 8 @@ -81,7 +76,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 9 @@ -95,7 +89,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 10 @@ -109,7 +102,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 11 @@ -123,7 +115,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 12 @@ -137,7 +128,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 13 @@ -151,7 +141,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 14 @@ -165,7 +154,6 @@ spec: - Egress - Ingress - --- # Source: object-order/templates/02-b.yml # 15 (11th object within 02-b.yml, in order to test `SplitManifests` which assigns `manifest-10` @@ -179,7 +167,6 @@ spec: policyTypes: - Egress - Ingress - --- # Source: object-order/templates/01-a.yml # 4 (Deployment should come after all NetworkPolicy manifests, since 'helm template' outputs in install order) diff --git a/pkg/cmd/testdata/output/template-name-template.txt b/pkg/cmd/testdata/output/template-name-template.txt index b1077012e..485b2f59c 100644 --- a/pkg/cmd/testdata/output/template-name-template.txt +++ b/pkg/cmd/testdata/output/template-name-template.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -15,7 +14,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -30,7 +28,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -48,7 +45,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -66,7 +62,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-set.txt b/pkg/cmd/testdata/output/template-set.txt index 1ecb8707b..cc14c2b7e 100644 --- a/pkg/cmd/testdata/output/template-set.txt +++ b/pkg/cmd/testdata/output/template-set.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -15,7 +14,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -30,7 +28,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -48,7 +45,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -66,7 +62,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-show-only-glob.txt b/pkg/cmd/testdata/output/template-show-only-glob.txt index 160f08a06..28bc465b2 100644 --- a/pkg/cmd/testdata/output/template-show-only-glob.txt +++ b/pkg/cmd/testdata/output/template-show-only-glob.txt @@ -9,7 +9,6 @@ rules: resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -25,4 +24,3 @@ subjects: name: subchart-sa namespace: default - diff --git a/pkg/cmd/testdata/output/template-show-only-multiple.txt b/pkg/cmd/testdata/output/template-show-only-multiple.txt index 3f9aa6f2f..97f995286 100644 --- a/pkg/cmd/testdata/output/template-show-only-multiple.txt +++ b/pkg/cmd/testdata/output/template-show-only-multiple.txt @@ -38,4 +38,3 @@ spec: selector: app.kubernetes.io/name: subcharta - diff --git a/pkg/cmd/testdata/output/template-skip-tests.txt b/pkg/cmd/testdata/output/template-skip-tests.txt index 4c5af8df3..5c907b563 100644 --- a/pkg/cmd/testdata/output/template-skip-tests.txt +++ b/pkg/cmd/testdata/output/template-skip-tests.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -15,7 +14,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -30,7 +28,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -48,7 +45,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -66,7 +62,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt b/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt index 227d05903..67fda94c7 100644 --- a/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt +++ b/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/configmap.yaml apiVersion: v1 @@ -23,7 +22,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -38,7 +36,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -56,7 +53,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -74,7 +70,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-subchart-cm-set.txt b/pkg/cmd/testdata/output/template-subchart-cm-set.txt index dd8be4db9..a280e4466 100644 --- a/pkg/cmd/testdata/output/template-subchart-cm-set.txt +++ b/pkg/cmd/testdata/output/template-subchart-cm-set.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/configmap.yaml apiVersion: v1 @@ -23,7 +22,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -38,7 +36,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -56,7 +53,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -74,7 +70,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-subchart-cm.txt b/pkg/cmd/testdata/output/template-subchart-cm.txt index c4600a798..5338e8e0e 100644 --- a/pkg/cmd/testdata/output/template-subchart-cm.txt +++ b/pkg/cmd/testdata/output/template-subchart-cm.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/configmap.yaml apiVersion: v1 @@ -23,7 +22,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -38,7 +36,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -56,7 +53,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -74,7 +70,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-values-files.txt b/pkg/cmd/testdata/output/template-values-files.txt index 1ecb8707b..cc14c2b7e 100644 --- a/pkg/cmd/testdata/output/template-values-files.txt +++ b/pkg/cmd/testdata/output/template-values-files.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -15,7 +14,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -30,7 +28,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -48,7 +45,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -66,7 +62,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-with-api-version.txt b/pkg/cmd/testdata/output/template-with-api-version.txt index ae726e624..81346ff91 100644 --- a/pkg/cmd/testdata/output/template-with-api-version.txt +++ b/pkg/cmd/testdata/output/template-with-api-version.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -15,7 +14,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -30,7 +28,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -48,7 +45,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -66,7 +62,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-with-crds.txt b/pkg/cmd/testdata/output/template-with-crds.txt index 1d63265ec..b17088b76 100644 --- a/pkg/cmd/testdata/output/template-with-crds.txt +++ b/pkg/cmd/testdata/output/template-with-crds.txt @@ -14,14 +14,12 @@ spec: shortNames: - tc singular: authconfig - --- # Source: subchart/templates/subdir/serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -32,7 +30,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -47,7 +44,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -65,7 +61,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -83,7 +78,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-with-kube-version.txt b/pkg/cmd/testdata/output/template-with-kube-version.txt index 2c42e2e84..e088a9085 100644 --- a/pkg/cmd/testdata/output/template-with-kube-version.txt +++ b/pkg/cmd/testdata/output/template-with-kube-version.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -15,7 +14,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -30,7 +28,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -48,7 +45,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -66,7 +62,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template.txt b/pkg/cmd/testdata/output/template.txt index ddbfebe9d..c3f33b919 100644 --- a/pkg/cmd/testdata/output/template.txt +++ b/pkg/cmd/testdata/output/template.txt @@ -4,7 +4,6 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa - --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -15,7 +14,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -30,7 +28,6 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -48,7 +45,6 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -66,7 +62,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb - --- # Source: subchart/templates/service.yaml apiVersion: v1 From 2ceaa23818d1c4c30f65d30c0c7d25471c237266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=8E=AE=E6=96=87?= Date: Thu, 24 Sep 2026 00:03:41 +0800 Subject: [PATCH 3/3] fix(template): preserve manifest newlines across output modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit helm template trims the release manifest with TrimSpace, losing intentional trailing newlines from its final document. Its hook, --show-only and file writers can instead add an extra newline. Keep the assembled output as []byte and write it directly, without trimming. For hooks, selected manifests and output files, remove at most one newline before the format string restores it. Each document stays newline-terminated without adding or dropping authored blank lines. Add golden-file coverage for stdout, --no-hooks, reordered --show-only and --output-dir. A fixture chart covers zero, one and two trailing newlines, including multiple regular and hook documents per file. Signed-off-by: 胡玮文 --- pkg/cmd/template.go | 14 ++-- pkg/cmd/template_test.go | 61 +++++++++++++++++ pkg/cmd/testdata/output/object-order.txt | 1 - .../output/template-name-template.txt | 2 - .../output/template-newlines-hooks.txt | 25 +++++++ .../output/template-newlines-no-hooks.txt | 31 +++++++++ .../template-newlines-no-newline-hook.txt | 12 ++++ .../output/template-newlines-no-newline.txt | 10 +++ .../output/template-newlines-regular.txt | 21 ++++++ .../output/template-newlines-reversed.txt | 68 +++++++++++++++++++ pkg/cmd/testdata/output/template-newlines.txt | 68 +++++++++++++++++++ pkg/cmd/testdata/output/template-set.txt | 2 - .../output/template-show-only-glob.txt | 2 - .../output/template-show-only-multiple.txt | 2 - .../output/template-show-only-one.txt | 1 - .../output/template-subchart-cm-set-file.txt | 2 - .../output/template-subchart-cm-set.txt | 2 - .../testdata/output/template-subchart-cm.txt | 2 - .../testdata/output/template-values-files.txt | 2 - .../output/template-with-api-version.txt | 2 - .../testdata/output/template-with-crds.txt | 2 - .../output/template-with-kube-version.txt | 2 - pkg/cmd/testdata/output/template.txt | 2 - .../testcharts/trailing-newlines/Chart.yaml | 3 + .../testcharts/trailing-newlines/README.md | 1 + .../templates/00-no-newline-hook.yaml | 10 +++ .../templates/00-no-newline.yaml | 8 +++ .../trailing-newlines/templates/hooks.yaml | 22 ++++++ .../trailing-newlines/templates/regular.yaml | 18 +++++ 29 files changed, 364 insertions(+), 34 deletions(-) create mode 100644 pkg/cmd/testdata/output/template-newlines-hooks.txt create mode 100644 pkg/cmd/testdata/output/template-newlines-no-hooks.txt create mode 100644 pkg/cmd/testdata/output/template-newlines-no-newline-hook.txt create mode 100644 pkg/cmd/testdata/output/template-newlines-no-newline.txt create mode 100644 pkg/cmd/testdata/output/template-newlines-regular.txt create mode 100644 pkg/cmd/testdata/output/template-newlines-reversed.txt create mode 100644 pkg/cmd/testdata/output/template-newlines.txt create mode 100644 pkg/cmd/testdata/testcharts/trailing-newlines/Chart.yaml create mode 100644 pkg/cmd/testdata/testcharts/trailing-newlines/README.md create mode 100644 pkg/cmd/testdata/testcharts/trailing-newlines/templates/00-no-newline-hook.yaml create mode 100644 pkg/cmd/testdata/testcharts/trailing-newlines/templates/00-no-newline.yaml create mode 100644 pkg/cmd/testdata/testcharts/trailing-newlines/templates/hooks.yaml create mode 100644 pkg/cmd/testdata/testcharts/trailing-newlines/templates/regular.yaml diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 5645ab33d..09d138a0c 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -17,7 +17,6 @@ limitations under the License. package cmd import ( - "bytes" "errors" "fmt" "io" @@ -118,8 +117,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { // We ignore a potential error here because, when the --debug flag was specified, // we always want to print the YAML, even if it is not valid. The error is still returned afterwards. if rel != nil { - var manifests bytes.Buffer - fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest)) + manifests := []byte(rel.Manifest) if !client.DisableHooks { fileWritten := make(map[string]bool) for _, m := range rel.Hooks { @@ -127,7 +125,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { continue } if client.OutputDir == "" { - fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest) + manifests = fmt.Appendf(manifests, "---\n# Source: %s\n%s\n", m.Path, strings.TrimSuffix(m.Manifest, "\n")) } else { newDir := client.OutputDir if client.UseReleaseName { @@ -151,7 +149,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if len(showFiles) > 0 { // This is necessary to ensure consistent manifest ordering when using --show-only // with globs or directory names. - splitManifests := releaseutil.SplitManifests(manifests.String()) + splitManifests := releaseutil.SplitManifests(string(manifests)) manifestsKeys := make([]string, 0, len(splitManifests)) for k := range splitManifests { manifestsKeys = append(manifestsKeys, k) @@ -195,10 +193,10 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } } for _, m := range manifestsToRender { - fmt.Fprintf(out, "---\n%s\n", m) + fmt.Fprintf(out, "---\n%s\n", strings.TrimSuffix(m, "\n")) } } else { - fmt.Fprintf(out, "%s", manifests.String()) + out.Write(manifests) } } @@ -253,7 +251,7 @@ func writeToFile(outputDir, name, data string, appendData bool) error { defer f.Close() - _, err = fmt.Fprintf(f, "---\n# Source: %s\n%s\n", name, data) + _, err = fmt.Fprintf(f, "---\n# Source: %s\n%s\n", name, strings.TrimSuffix(data, "\n")) if err != nil { return err } diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index d508a0505..2bc047237 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -20,12 +20,17 @@ import ( "fmt" "path/filepath" "testing" + + "github.com/stretchr/testify/require" + + "helm.sh/helm/v4/internal/test" ) var chartPath = "testdata/testcharts/subchart" func TestTemplateCmd(t *testing.T) { deletevalchart := "testdata/testcharts/issue-9027" + newlinesChart := "testdata/testcharts/trailing-newlines" tests := []cmdTestCase{ { @@ -172,10 +177,66 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath), golden: "output/template-subchart-cm-set-file.txt", }, + { + name: "trailing newlines", + cmd: fmt.Sprintf("template '%s'", newlinesChart), + golden: "output/template-newlines.txt", + }, + { + name: "trailing newlines without hooks", + cmd: fmt.Sprintf("template '%s' --no-hooks", newlinesChart), + golden: "output/template-newlines-no-hooks.txt", + }, + { + name: "trailing newlines with reversed show-only order", + cmd: fmt.Sprintf("template '%s' --show-only templates/hooks.yaml --show-only templates/00-no-newline-hook.yaml --show-only templates/regular.yaml --show-only templates/00-no-newline.yaml", newlinesChart), + golden: "output/template-newlines-reversed.txt", + }, + { + name: "trailing newlines with show-only regular manifests", + cmd: fmt.Sprintf("template '%s' --show-only templates/regular.yaml", newlinesChart), + golden: "output/template-newlines-regular.txt", + }, + { + name: "trailing newlines with show-only hooks", + cmd: fmt.Sprintf("template '%s' --show-only templates/hooks.yaml", newlinesChart), + golden: "output/template-newlines-hooks.txt", + }, + { + name: "missing final newline with show-only", + cmd: fmt.Sprintf("template '%s' --show-only templates/00-no-newline.yaml", newlinesChart), + golden: "output/template-newlines-no-newline.txt", + }, + { + name: "missing final newline with show-only hook", + cmd: fmt.Sprintf("template '%s' --show-only templates/00-no-newline-hook.yaml", newlinesChart), + golden: "output/template-newlines-no-newline-hook.txt", + }, } runTestCmd(t, tests) } +func TestTemplateCmdOutputDir(t *testing.T) { + t.Cleanup(resetEnv()) + outputDir := t.TempDir() + _, _, err := executeActionCommand(fmt.Sprintf("template testdata/testcharts/trailing-newlines --output-dir %q", outputDir)) + require.NoError(t, err) + + for _, tc := range []struct { + file string + golden string + }{ + {"regular.yaml", "output/template-newlines-regular.txt"}, + {"hooks.yaml", "output/template-newlines-hooks.txt"}, + {"00-no-newline.yaml", "output/template-newlines-no-newline.txt"}, + {"00-no-newline-hook.yaml", "output/template-newlines-no-newline-hook.txt"}, + } { + t.Run(tc.file, func(t *testing.T) { + test.AssertGoldenFile(t, filepath.Join(outputDir, "trailing-newlines", "templates", tc.file), tc.golden) + }) + } +} + func TestTemplateVersionCompletion(t *testing.T) { repoFile := "testdata/helmhome/helm/repositories.yaml" repoCache := "testdata/helmhome/helm/repository" diff --git a/pkg/cmd/testdata/output/object-order.txt b/pkg/cmd/testdata/output/object-order.txt index 8ca93377c..985022951 100644 --- a/pkg/cmd/testdata/output/object-order.txt +++ b/pkg/cmd/testdata/output/object-order.txt @@ -202,4 +202,3 @@ spec: - Egress - Ingress - diff --git a/pkg/cmd/testdata/output/template-name-template.txt b/pkg/cmd/testdata/output/template-name-template.txt index 485b2f59c..9406048dd 100644 --- a/pkg/cmd/testdata/output/template-name-template.txt +++ b/pkg/cmd/testdata/output/template-name-template.txt @@ -93,7 +93,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -113,4 +112,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/output/template-newlines-hooks.txt b/pkg/cmd/testdata/output/template-newlines-hooks.txt new file mode 100644 index 000000000..3062af92b --- /dev/null +++ b/pkg/cmd/testdata/output/template-newlines-hooks.txt @@ -0,0 +1,25 @@ +--- +# Source: trailing-newlines/templates/hooks.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-one-newline + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/hooks.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-two-newlines + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 + diff --git a/pkg/cmd/testdata/output/template-newlines-no-hooks.txt b/pkg/cmd/testdata/output/template-newlines-no-hooks.txt new file mode 100644 index 000000000..ade45d45c --- /dev/null +++ b/pkg/cmd/testdata/output/template-newlines-no-hooks.txt @@ -0,0 +1,31 @@ +--- +# Source: trailing-newlines/templates/00-no-newline.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-no-newline +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/regular.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-one-newline +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/regular.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-two-newlines +data: + cfg: |+ + line1 + line2 + diff --git a/pkg/cmd/testdata/output/template-newlines-no-newline-hook.txt b/pkg/cmd/testdata/output/template-newlines-no-newline-hook.txt new file mode 100644 index 000000000..6ad15b529 --- /dev/null +++ b/pkg/cmd/testdata/output/template-newlines-no-newline-hook.txt @@ -0,0 +1,12 @@ +--- +# Source: trailing-newlines/templates/00-no-newline-hook.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-no-newline + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 diff --git a/pkg/cmd/testdata/output/template-newlines-no-newline.txt b/pkg/cmd/testdata/output/template-newlines-no-newline.txt new file mode 100644 index 000000000..8b233153f --- /dev/null +++ b/pkg/cmd/testdata/output/template-newlines-no-newline.txt @@ -0,0 +1,10 @@ +--- +# Source: trailing-newlines/templates/00-no-newline.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-no-newline +data: + cfg: |+ + line1 + line2 diff --git a/pkg/cmd/testdata/output/template-newlines-regular.txt b/pkg/cmd/testdata/output/template-newlines-regular.txt new file mode 100644 index 000000000..e014ef387 --- /dev/null +++ b/pkg/cmd/testdata/output/template-newlines-regular.txt @@ -0,0 +1,21 @@ +--- +# Source: trailing-newlines/templates/regular.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-one-newline +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/regular.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-two-newlines +data: + cfg: |+ + line1 + line2 + diff --git a/pkg/cmd/testdata/output/template-newlines-reversed.txt b/pkg/cmd/testdata/output/template-newlines-reversed.txt new file mode 100644 index 000000000..7cc169cb7 --- /dev/null +++ b/pkg/cmd/testdata/output/template-newlines-reversed.txt @@ -0,0 +1,68 @@ +--- +# Source: trailing-newlines/templates/hooks.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-one-newline + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/hooks.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-two-newlines + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 + +--- +# Source: trailing-newlines/templates/00-no-newline-hook.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-no-newline + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/regular.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-one-newline +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/regular.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-two-newlines +data: + cfg: |+ + line1 + line2 + +--- +# Source: trailing-newlines/templates/00-no-newline.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-no-newline +data: + cfg: |+ + line1 + line2 diff --git a/pkg/cmd/testdata/output/template-newlines.txt b/pkg/cmd/testdata/output/template-newlines.txt new file mode 100644 index 000000000..a9d434dd8 --- /dev/null +++ b/pkg/cmd/testdata/output/template-newlines.txt @@ -0,0 +1,68 @@ +--- +# Source: trailing-newlines/templates/00-no-newline.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-no-newline +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/regular.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-one-newline +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/regular.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-two-newlines +data: + cfg: |+ + line1 + line2 + +--- +# Source: trailing-newlines/templates/00-no-newline-hook.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-no-newline + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/hooks.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-one-newline + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 +--- +# Source: trailing-newlines/templates/hooks.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-two-newlines + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 + diff --git a/pkg/cmd/testdata/output/template-set.txt b/pkg/cmd/testdata/output/template-set.txt index cc14c2b7e..4040991cf 100644 --- a/pkg/cmd/testdata/output/template-set.txt +++ b/pkg/cmd/testdata/output/template-set.txt @@ -93,7 +93,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -113,4 +112,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/output/template-show-only-glob.txt b/pkg/cmd/testdata/output/template-show-only-glob.txt index 28bc465b2..b2d2b1c2d 100644 --- a/pkg/cmd/testdata/output/template-show-only-glob.txt +++ b/pkg/cmd/testdata/output/template-show-only-glob.txt @@ -8,7 +8,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] - --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -23,4 +22,3 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default - diff --git a/pkg/cmd/testdata/output/template-show-only-multiple.txt b/pkg/cmd/testdata/output/template-show-only-multiple.txt index 97f995286..1aac3081a 100644 --- a/pkg/cmd/testdata/output/template-show-only-multiple.txt +++ b/pkg/cmd/testdata/output/template-show-only-multiple.txt @@ -19,7 +19,6 @@ spec: name: nginx selector: app.kubernetes.io/name: subchart - --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -37,4 +36,3 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta - diff --git a/pkg/cmd/testdata/output/template-show-only-one.txt b/pkg/cmd/testdata/output/template-show-only-one.txt index 27a56c03c..9cc34f515 100644 --- a/pkg/cmd/testdata/output/template-show-only-one.txt +++ b/pkg/cmd/testdata/output/template-show-only-one.txt @@ -19,4 +19,3 @@ spec: name: nginx selector: app.kubernetes.io/name: subchart - diff --git a/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt b/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt index 67fda94c7..56844e292 100644 --- a/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt +++ b/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt @@ -101,7 +101,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -121,4 +120,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/output/template-subchart-cm-set.txt b/pkg/cmd/testdata/output/template-subchart-cm-set.txt index a280e4466..e52f7c234 100644 --- a/pkg/cmd/testdata/output/template-subchart-cm-set.txt +++ b/pkg/cmd/testdata/output/template-subchart-cm-set.txt @@ -101,7 +101,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -121,4 +120,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/output/template-subchart-cm.txt b/pkg/cmd/testdata/output/template-subchart-cm.txt index 5338e8e0e..9cc9e2296 100644 --- a/pkg/cmd/testdata/output/template-subchart-cm.txt +++ b/pkg/cmd/testdata/output/template-subchart-cm.txt @@ -101,7 +101,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -121,4 +120,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/output/template-values-files.txt b/pkg/cmd/testdata/output/template-values-files.txt index cc14c2b7e..4040991cf 100644 --- a/pkg/cmd/testdata/output/template-values-files.txt +++ b/pkg/cmd/testdata/output/template-values-files.txt @@ -93,7 +93,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -113,4 +112,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/output/template-with-api-version.txt b/pkg/cmd/testdata/output/template-with-api-version.txt index 81346ff91..8b6074cdb 100644 --- a/pkg/cmd/testdata/output/template-with-api-version.txt +++ b/pkg/cmd/testdata/output/template-with-api-version.txt @@ -95,7 +95,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -115,4 +114,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/output/template-with-crds.txt b/pkg/cmd/testdata/output/template-with-crds.txt index b17088b76..a0c78ea12 100644 --- a/pkg/cmd/testdata/output/template-with-crds.txt +++ b/pkg/cmd/testdata/output/template-with-crds.txt @@ -109,7 +109,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -129,4 +128,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/output/template-with-kube-version.txt b/pkg/cmd/testdata/output/template-with-kube-version.txt index e088a9085..9d326f328 100644 --- a/pkg/cmd/testdata/output/template-with-kube-version.txt +++ b/pkg/cmd/testdata/output/template-with-kube-version.txt @@ -93,7 +93,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -113,4 +112,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/output/template.txt b/pkg/cmd/testdata/output/template.txt index c3f33b919..58c480b47 100644 --- a/pkg/cmd/testdata/output/template.txt +++ b/pkg/cmd/testdata/output/template.txt @@ -93,7 +93,6 @@ metadata: "helm.sh/hook": test data: message: Hello World - --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -113,4 +112,3 @@ spec: - echo - "$message" restartPolicy: Never - diff --git a/pkg/cmd/testdata/testcharts/trailing-newlines/Chart.yaml b/pkg/cmd/testdata/testcharts/trailing-newlines/Chart.yaml new file mode 100644 index 000000000..6df0457c4 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/trailing-newlines/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: trailing-newlines +version: 0.1.0 diff --git a/pkg/cmd/testdata/testcharts/trailing-newlines/README.md b/pkg/cmd/testdata/testcharts/trailing-newlines/README.md new file mode 100644 index 000000000..4653b029a --- /dev/null +++ b/pkg/cmd/testdata/testcharts/trailing-newlines/README.md @@ -0,0 +1 @@ +The `00-no-newline*.yaml` templates intentionally have no final newline. The other templates contain two documents ending in one and two newlines, respectively. Keep chomping (`|+`) makes these newlines part of the ConfigMap value. diff --git a/pkg/cmd/testdata/testcharts/trailing-newlines/templates/00-no-newline-hook.yaml b/pkg/cmd/testdata/testcharts/trailing-newlines/templates/00-no-newline-hook.yaml new file mode 100644 index 000000000..63c90f588 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/trailing-newlines/templates/00-no-newline-hook.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-no-newline + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 \ No newline at end of file diff --git a/pkg/cmd/testdata/testcharts/trailing-newlines/templates/00-no-newline.yaml b/pkg/cmd/testdata/testcharts/trailing-newlines/templates/00-no-newline.yaml new file mode 100644 index 000000000..75cad133a --- /dev/null +++ b/pkg/cmd/testdata/testcharts/trailing-newlines/templates/00-no-newline.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-no-newline +data: + cfg: |+ + line1 + line2 \ No newline at end of file diff --git a/pkg/cmd/testdata/testcharts/trailing-newlines/templates/hooks.yaml b/pkg/cmd/testdata/testcharts/trailing-newlines/templates/hooks.yaml new file mode 100644 index 000000000..4dd98465e --- /dev/null +++ b/pkg/cmd/testdata/testcharts/trailing-newlines/templates/hooks.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-one-newline + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-two-newlines + annotations: + helm.sh/hook: pre-install +data: + cfg: |+ + line1 + line2 + diff --git a/pkg/cmd/testdata/testcharts/trailing-newlines/templates/regular.yaml b/pkg/cmd/testdata/testcharts/trailing-newlines/templates/regular.yaml new file mode 100644 index 000000000..9484b9f00 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/trailing-newlines/templates/regular.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-one-newline +data: + cfg: |+ + line1 + line2 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: regular-two-newlines +data: + cfg: |+ + line1 + line2 +