Repository: helm/helm (go)

Issue:
Is it possible to have a newline inside `fail` function?
Is it possible to have a newline inside `fail` function?

`{{- fail "fail" }}` gives:
```
Error: execution error at (panel/templates/_helpers.tpl:17:4): fail
```

whereas `{{- fail "\nfail" }}` gives:
```
Error: template: panel/templates/_helpers.tpl:17:4: executing "panel.api.image" at <fail "\nfail">: error calling fail: HELM_ERR_START
failHELM_ERR_END
```
So adding a newline (`\n`) inside `fail` function causes `fail` function to fail.

Is it another way to add a newline to the fail function or is it impossible?


Pull request description:
fix(engine): parse fail messages with newlines
**What this PR does / why we need it**:

Fix the parsing of `required` and `fail` error messages so that messages containing newlines are cleaned up the same as messages without newlines.

Before:
```console
$ cat issue9981/templates/fail.yaml
{{ fail "message\nwith newlines" }}
$ helm version
version.BuildInfo{Version:"v3.6.3", GitCommit:"d506314abfb5d21419df8c7e7e68012379db2354", GitTreeState:"dirty", GoVersion:"go1.16.5"}
$ helm template issue9981
Error: template: issue9981/templates/fail.yaml:1:3: executing "issue9981/templates/fail.yaml" at <fail "message\nwith newlines">: error calling fail: HELM_ERR_STARTmessage
with newlinesHELM_ERR_END

Use --debug flag to render out invalid YAML
```

After:
```console
$ ./bin/helm template issue9981
Error: execution error at (issue9981/templates/fail.yaml:1:3): message
with newlines

Use --debug flag to render out invalid YAML
```

Closes #9981

**Special notes for your reviewer**:

The templating engine handles errors originating from the `required` and `fail` template functions specially, cleaning up the error messages to be more presentable to users. Go's `text/template` package unfortunately does not make this straightforward to implement. Despite `template.ExecError` implementing `Unwrap`, the error value returned from the template function cannot be retrieved using `errors.As`. The wrapped error in `ExecError` is a pre-formatted error string with the template function's error string interpolated in with the original error value erased. Helm works around this limitation by delimiting the template-supplied message and extracting the message out of the `ExecError` string with a regex.

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go
index 4da64f5cc..8ff42941a 100644
--- a/pkg/engine/engine.go
+++ b/pkg/engine/engine.go
@@ -97,7 +97,7 @@ const warnStartDelim = "HELM_ERR_START"
 const warnEndDelim = "HELM_ERR_END"
 const recursionMaxNums = 1000
 
-var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim)
+var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim)
 
 func warnWrap(warn string) string {
 	return warnStartDelim + warn + warnEndDelim


Test patch:
diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go
index 72ee02626..429511003 100644
--- a/pkg/engine/engine_test.go
+++ b/pkg/engine/engine_test.go
@@ -245,44 +245,65 @@ func TestParseErrors(t *testing.T) {
 
 func TestExecErrors(t *testing.T) {
 	vals := chartutil.Values{"Values": map[string]interface{}{}}
-
-	tplsMissingRequired := map[string]renderable{
-		"missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals},
-	}
-	_, err := new(Engine).render(tplsMissingRequired)
-	if err == nil {
-		t.Fatalf("Expected failures while rendering: %s", err)
-	}
-	expected := `execution error at (missing_required:1:2): foo is required`
-	if err.Error() != expected {
-		t.Errorf("Expected '%s', got %q", expected, err.Error())
-	}
-
-	tplsMissingRequired = map[string]renderable{
-		"missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals},
-	}
-	_, err = new(Engine).render(tplsMissingRequired)
-	if err == nil {
-		t.Fatalf("Expected failures while rendering: %s", err)
-	}
-	expected = `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`
-	if err.Error() != expected {
-		t.Errorf("Expected '%s', got %q", expected, err.Error())
-	}
-
-	issue6044tpl := `{{ $someEmptyValue := "" }}
+	cases := []struct {
+		name     string
+		tpls     map[string]renderable
+		expected string
+	}{
+		{
+			name: "MissingRequired",
+			tpls: map[string]renderable{
+				"missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals},
+			},
+			expected: `execution error at (missing_required:1:2): foo is required`,
+		},
+		{
+			name: "MissingRequiredWithColons",
+			tpls: map[string]renderable{
+				"missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals},
+			},
+			expected: `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`,
+		},
+		{
+			name: "Issue6044",
+			tpls: map[string]renderable{
+				"issue6044": {
+					vals: vals,
+					tpl: `{{ $someEmptyValue := "" }}
 {{ $myvar := "abc" }}
-{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`
-	tplsMissingRequired = map[string]renderable{
-		"issue6044": {tpl: issue6044tpl, vals: vals},
-	}
-	_, err = new(Engine).render(tplsMissingRequired)
-	if err == nil {
-		t.Fatalf("Expected failures while rendering: %s", err)
+{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`,
+				},
+			},
+			expected: `execution error at (issue6044:3:4): abc: something is missing`,
+		},
+		{
+			name: "MissingRequiredWithNewlines",
+			tpls: map[string]renderable{
+				"issue9981": {tpl: `{{required "foo is required\nmore info after the break" .Values.foo}}`, vals: vals},
+			},
+			expected: `execution error at (issue9981:1:2): foo is required
+more info after the break`,
+		},
+		{
+			name: "FailWithNewlines",
+			tpls: map[string]renderable{
+				"issue9981": {tpl: `{{fail "something is wrong\nlinebreak"}}`, vals: vals},
+			},
+			expected: `execution error at (issue9981:1:2): something is wrong
+linebreak`,
+		},
 	}
-	expected = `execution error at (issue6044:3:4): abc: something is missing`
-	if err.Error() != expected {
-		t.Errorf("Expected '%s', got %q", expected, err.Error())
+
+	for _, tt := range cases {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := new(Engine).render(tt.tpls)
+			if err == nil {
+				t.Fatalf("Expected failures while rendering: %s", err)
+			}
+			if err.Error() != tt.expected {
+				t.Errorf("Expected %q, got %q", tt.expected, err.Error())
+			}
+		})
 	}
 }