fix(template): preserve manifest newlines across output modes

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: 胡玮文 <huweiwen.hww@alibaba-inc.com>
pull/32683/head
胡玮文 1 week ago
parent bcc3acd9fe
commit 2ceaa23818

@ -17,7 +17,6 @@ limitations under the License.
package cmd package cmd
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "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 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. // we always want to print the YAML, even if it is not valid. The error is still returned afterwards.
if rel != nil { if rel != nil {
var manifests bytes.Buffer manifests := []byte(rel.Manifest)
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
if !client.DisableHooks { if !client.DisableHooks {
fileWritten := make(map[string]bool) fileWritten := make(map[string]bool)
for _, m := range rel.Hooks { for _, m := range rel.Hooks {
@ -127,7 +125,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
continue continue
} }
if client.OutputDir == "" { 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 { } else {
newDir := client.OutputDir newDir := client.OutputDir
if client.UseReleaseName { if client.UseReleaseName {
@ -151,7 +149,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if len(showFiles) > 0 { if len(showFiles) > 0 {
// This is necessary to ensure consistent manifest ordering when using --show-only // This is necessary to ensure consistent manifest ordering when using --show-only
// with globs or directory names. // with globs or directory names.
splitManifests := releaseutil.SplitManifests(manifests.String()) splitManifests := releaseutil.SplitManifests(string(manifests))
manifestsKeys := make([]string, 0, len(splitManifests)) manifestsKeys := make([]string, 0, len(splitManifests))
for k := range splitManifests { for k := range splitManifests {
manifestsKeys = append(manifestsKeys, k) manifestsKeys = append(manifestsKeys, k)
@ -195,10 +193,10 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
} }
for _, m := range manifestsToRender { for _, m := range manifestsToRender {
fmt.Fprintf(out, "---\n%s\n", m) fmt.Fprintf(out, "---\n%s\n", strings.TrimSuffix(m, "\n"))
} }
} else { } 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() 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 { if err != nil {
return err return err
} }

@ -20,12 +20,17 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test"
) )
var chartPath = "testdata/testcharts/subchart" var chartPath = "testdata/testcharts/subchart"
func TestTemplateCmd(t *testing.T) { func TestTemplateCmd(t *testing.T) {
deletevalchart := "testdata/testcharts/issue-9027" deletevalchart := "testdata/testcharts/issue-9027"
newlinesChart := "testdata/testcharts/trailing-newlines"
tests := []cmdTestCase{ tests := []cmdTestCase{
{ {
@ -172,10 +177,66 @@ func TestTemplateCmd(t *testing.T) {
cmd: fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath), cmd: fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath),
golden: "output/template-subchart-cm-set-file.txt", 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) 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) { func TestTemplateVersionCompletion(t *testing.T) {
repoFile := "testdata/helmhome/helm/repositories.yaml" repoFile := "testdata/helmhome/helm/repositories.yaml"
repoCache := "testdata/helmhome/helm/repository" repoCache := "testdata/helmhome/helm/repository"

@ -202,4 +202,3 @@ spec:
- Egress - Egress
- Ingress - Ingress

@ -93,7 +93,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -113,4 +112,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -93,7 +93,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -113,4 +112,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -8,7 +8,6 @@ rules:
- apiGroups: [""] - apiGroups: [""]
resources: ["pods"] resources: ["pods"]
verbs: ["get","list","watch"] verbs: ["get","list","watch"]
--- ---
# Source: subchart/templates/subdir/rolebinding.yaml # Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
@ -23,4 +22,3 @@ subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: subchart-sa name: subchart-sa
namespace: default namespace: default

@ -19,7 +19,6 @@ spec:
name: nginx name: nginx
selector: selector:
app.kubernetes.io/name: subchart app.kubernetes.io/name: subchart
--- ---
# Source: subchart/charts/subcharta/templates/service.yaml # Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1 apiVersion: v1
@ -37,4 +36,3 @@ spec:
name: apache name: apache
selector: selector:
app.kubernetes.io/name: subcharta app.kubernetes.io/name: subcharta

@ -19,4 +19,3 @@ spec:
name: nginx name: nginx
selector: selector:
app.kubernetes.io/name: subchart app.kubernetes.io/name: subchart

@ -101,7 +101,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -121,4 +120,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -101,7 +101,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -121,4 +120,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -101,7 +101,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -121,4 +120,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -93,7 +93,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -113,4 +112,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -95,7 +95,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -115,4 +114,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -109,7 +109,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -129,4 +128,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -93,7 +93,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -113,4 +112,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -93,7 +93,6 @@ metadata:
"helm.sh/hook": test "helm.sh/hook": test
data: data:
message: Hello World message: Hello World
--- ---
# Source: subchart/templates/tests/test-nothing.yaml # Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1 apiVersion: v1
@ -113,4 +112,3 @@ spec:
- echo - echo
- "$message" - "$message"
restartPolicy: Never restartPolicy: Never

@ -0,0 +1,3 @@
apiVersion: v2
name: trailing-newlines
version: 0.1.0

@ -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.

@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: hook-no-newline
annotations:
helm.sh/hook: pre-install
data:
cfg: |+
line1
line2

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: regular-no-newline
data:
cfg: |+
line1
line2

@ -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

@ -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
Loading…
Cancel
Save