fix: insert newline after doc separators glued to content by template trimming

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
pull/31868/head
Matheus Pimenta 2 weeks ago
parent bd334848b4
commit af94abf976
No known key found for this signature in database
GPG Key ID: 4639F038AE28FBFF

@ -144,6 +144,39 @@ const (
filenameAnnotation = "postrenderer.helm.sh/postrender-filename"
)
// fixDocSeparators ensures YAML document separators ("---") are always
// followed by a newline in rendered template content. Go template whitespace
// trimming ({{-) can remove the newline after "---", producing e.g.
// "---apiVersion: v1" which is not a valid YAML document separator.
// This function inserts a newline after any "---" at the start of a line
// that is immediately followed by non-whitespace content.
func fixDocSeparators(content string) string {
var b strings.Builder
remaining := content
for {
// Find "---" at the start of a line (or start of content).
idx := strings.Index(remaining, "---")
if idx == -1 {
b.WriteString(remaining)
break
}
// "---" must be at the start of a line: either idx==0 or preceded by '\n'.
if idx > 0 && remaining[idx-1] != '\n' {
b.WriteString(remaining[:idx+3])
remaining = remaining[idx+3:]
continue
}
b.WriteString(remaining[:idx+3])
remaining = remaining[idx+3:]
// If "---" is followed by non-whitespace (e.g. "---apiVersion"),
// insert a newline to make it a proper document separator.
if len(remaining) > 0 && remaining[0] != '\n' && remaining[0] != '\r' && remaining[0] != ' ' && remaining[0] != '\t' {
b.WriteByte('\n')
}
}
return b.String()
}
// annotateAndMerge combines multiple YAML files into a single stream of documents,
// adding filename annotations to each document for later reconstruction.
func annotateAndMerge(files map[string]string) (string, error) {
@ -159,6 +192,13 @@ func annotateAndMerge(files map[string]string) (string, error) {
continue
}
// Fix document separators where Go template whitespace trimming
// ({{-) has removed the newline after "---", producing e.g.
// "---apiVersion: v1" which is not a valid YAML document
// separator. Insert the missing newline so kio.ParseAll can
// parse the content correctly.
content = fixDocSeparators(content)
manifests, err := kio.ParseAll(content)
if err != nil {
return "", fmt.Errorf("parsing %s: %w", fname, err)

@ -403,6 +403,96 @@ func (m *mockPostRenderer) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer,
return bytes.NewBufferString(content), nil
}
func TestFixDocSeparators(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "no separator",
input: "apiVersion: v1\nkind: Service\n",
expected: "apiVersion: v1\nkind: Service\n",
},
{
name: "separator on its own line",
input: "---\napiVersion: v1\nkind: Service\n",
expected: "---\napiVersion: v1\nkind: Service\n",
},
{
name: "leading separator glued to content",
input: "---apiVersion: v1\nkind: Service\n",
expected: "---\napiVersion: v1\nkind: Service\n",
},
{
name: "mid-content separator glued to content",
input: "apiVersion: v1\nkind: ConfigMap\n---apiVersion: v1\nkind: Service\n",
expected: "apiVersion: v1\nkind: ConfigMap\n---\napiVersion: v1\nkind: Service\n",
},
{
name: "multiple separators all proper",
input: "---\napiVersion: v1\n---\napiVersion: v1\n",
expected: "---\napiVersion: v1\n---\napiVersion: v1\n",
},
{
name: "multiple separators some glued",
input: "---apiVersion: v1\nkind: ConfigMap\n---apiVersion: v1\nkind: Service\n",
expected: "---\napiVersion: v1\nkind: ConfigMap\n---\napiVersion: v1\nkind: Service\n",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "only separator",
input: "---\n",
expected: "---\n",
},
{
name: "triple dash in a value is not a separator",
input: "data:\n key: ---value\n",
expected: "data:\n key: ---value\n",
},
{
name: "realistic multi-doc template output",
input: "apiVersion: v1\nkind: Deployment\n---\napiVersion: v1\nkind: Ingress\n---apiVersion: v1\nkind: Service\n",
expected: "apiVersion: v1\nkind: Deployment\n---\napiVersion: v1\nkind: Ingress\n---\napiVersion: v1\nkind: Service\n",
},
{
name: "separator followed by carriage return",
input: "---\r\napiVersion: v1\n",
expected: "---\r\napiVersion: v1\n",
},
{
name: "separator followed by space",
input: "--- \napiVersion: v1\n",
expected: "--- \napiVersion: v1\n",
},
{
name: "separator followed by tab",
input: "---\t\napiVersion: v1\n",
expected: "---\t\napiVersion: v1\n",
},
{
name: "four dashes on its own line",
input: "----\napiVersion: v1\n",
expected: "---\n-\napiVersion: v1\n",
},
{
name: "four dashes followed by text",
input: "----more\napiVersion: v1\n",
expected: "---\n-more\napiVersion: v1\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, fixDocSeparators(tt.input))
})
}
}
func TestAnnotateAndMerge(t *testing.T) {
tests := []struct {
name string
@ -543,6 +633,65 @@ metadata:
},
expectedError: "parsing templates/invalid.yaml",
},
{
name: "leading doc separator glued to content by template whitespace trimming",
files: map[string]string{
"templates/service.yaml": "---apiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n",
},
expected: `apiVersion: v1
kind: Service
metadata:
name: test-svc
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/service.yaml'
`,
},
{
name: "leading doc separator on its own line",
files: map[string]string{
"templates/service.yaml": "---\napiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n",
},
expected: `apiVersion: v1
kind: Service
metadata:
name: test-svc
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/service.yaml'
`,
},
{
name: "multiple leading doc separators",
files: map[string]string{
"templates/service.yaml": "---\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n",
},
expected: `apiVersion: v1
kind: Service
metadata:
name: test-svc
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/service.yaml'
`,
},
{
name: "mid-content doc separator glued to content by template whitespace trimming",
files: map[string]string{
"templates/all.yaml": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test-cm\n---apiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n",
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/all.yaml'
---
apiVersion: v1
kind: Service
metadata:
name: test-svc
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/all.yaml'
`,
},
}
for _, tt := range tests {

Loading…
Cancel
Save