Repository: helm/helm (go)

Issue:
Dumping cyclic dictionaries has some serious problems
Hello folks, I don't really know where to start with all this, but the story starts in a point where I accidentally created a cyclic dictionary with the `merge` operation. For simplicity's sake we can try this little example:
```yaml
# let's say this is values.yaml
inner:
  item: 0
```
I was trying to merge some defaults over to the target dict like this (it was more convoluted),
```
{{- $outer := .Values }}{{/* possibly $outer := dict "inner" (dict "item" 0) */}}
{{- $inner := $outer.inner }}
{{- $merged := merge $inner $outer }}
{{- $merged | toYaml }}
```
and didn't realize that a deeply nested value within the `$outer` dict is in fact the `$inner` dict. My target was a result like this (which I actually achieved, but not only that :slightly_smiling_face:):
```yaml
inner:
  item: 0
item: 0
```
But because `$inner` didn't yet contain any `inner` key, it has been merged in from the `$outer` dict, making it cyclic with the reference to the `$inner` dict itself. Using YAML anchors, one could write it like this:
```yaml
&inner
inner: *inner
item: 0
```
This structure is not serializable (quite naturally) by anything I tried. The problem is how that manifests:
- `toString` - results in a stack overflow crash :boom:.
- `toJson` and `toYaml` - result in an empty string (this is the worst, tbh).
- `toRawJson` and `mustToJson` - result in an proper error.
  ```
  Error: template: bug-chart/templates/test.yaml:24:32: executing "bug-chart/templates/test.yaml" at <mustToJson>: error calling mustToJson: json: unsupported value: encountered a cycle via map[string]interface {}
  ```

I'm well aware that this gets fixed by preventing the creation of the cyclic dict, e.g. with `deepCopy`:
```
{{- $merged := merge (deepCopy $inner) $outer }}
```
but that's not the point, because once I know what I'm dealing with I can easily find ways to avoid the problem. To be honest, many beginner (or I dare to say intermediate) template devs won't take cyclic dicts into consideration, especially when nothing warns them while treading these murky waters.

I see part of this criticized functionality from `sprig` (`toJson`) [is documented](https://masterminds.github.io/sprig/defaults.html) to return an empty string (`toRawJson` errs, so that's rather inconsistent with its `toJson` sibling and makes me wonder if there's any difference with the `mustToRawJson` variant).

In general, I see I want to start using the (annoyingly long, but safer) `must*` variants, but what can I do with my BFF :heart_eyes:  `toYaml`, which doesn't have a `mustToYaml`? Could that be implemented?

I must say, I'd be glad if all these operations would behave like the `must` ones, while the others would be called something like `laxToJson`, for the use cases where this (IMHO niche and potentially dangerous) functionality is actually desired. But I understand that's hardly an option for an established library as `sprig` is.

### Conclusion
- [ ] `toString` should get fixed for sure.
- [ ] Getting a `mustToYaml` would be great (I know I can use `mustToJson` as JSON is a YAML subset), but that will make the resulting k8s manifests a big eyesore.
- [ ] Updating `merge` / `mergeOverwrite` description to warn folks about the risk of creating cyclic dicts would be a really simple thing to do and would hopefully raise awareness enough so other template wizards wouldn't need to spend a day on debugging this fun.

TIA for your help!

Output of `helm version`:
```
version.BuildInfo{Version:"3.16.2", GitCommit:"5a5449dc42be07001fd5771d56429132984ab3ab", GitTreeState:"clean", GoVersion:"go1.22.7"}
```


Pull request:
feat: Add mustToYaml and mustToJson template functions
Introduces two new template functions that marshal data to YAML and JSON, respectively, and panic on errors. This allows for strict validation of template output formats.

<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/main/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->

**What this PR does / why we need it**:
Add mustToYaml and mustToJson template functions

**Special notes for your reviewer**:
closes #13488

**If applicable**:

- [x] this PR contains user facing changes (the `docs needed` label should be applied if so)
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go
index d03a818c2..c1f590018 100644
--- a/pkg/engine/funcs.go
+++ b/pkg/engine/funcs.go
@@ -51,10 +51,12 @@ func funcMap() template.FuncMap {
 		"toToml":        toTOML,
 		"fromToml":      fromTOML,
 		"toYaml":        toYAML,
+		"mustToYaml":    mustToYAML,
 		"toYamlPretty":  toYAMLPretty,
 		"fromYaml":      fromYAML,
 		"fromYamlArray": fromYAMLArray,
 		"toJson":        toJSON,
+		"mustToJson":    mustToJSON,
 		"fromJson":      fromJSON,
 		"fromJsonArray": fromJSONArray,
 
@@ -91,6 +93,19 @@ func toYAML(v interface{}) string {
 	return strings.TrimSuffix(string(data), "\n")
 }
 
+// mustToYAML takes an interface, marshals it to yaml, and returns a string.
+// It will panic if there is an error.
+//
+// This is designed to be called from a template when need to ensure that the
+// output YAML is valid.
+func mustToYAML(v interface{}) string {
+	data, err := yaml.Marshal(v)
+	if err != nil {
+		panic(err)
+	}
+	return strings.TrimSuffix(string(data), "\n")
+}
+
 func toYAMLPretty(v interface{}) string {
 	var data bytes.Buffer
 	encoder := goYaml.NewEncoder(&data)
@@ -176,6 +191,19 @@ func toJSON(v interface{}) string {
 	return string(data)
 }
 
+// mustToJSON takes an interface, marshals it to json, and returns a string.
+// It will panic if there is an error.
+//
+// This is designed to be called from a template when need to ensure that the
+// output JSON is valid.
+func mustToJSON(v interface{}) string {
+	data, err := json.Marshal(v)
+	if err != nil {
+		panic(err)
+	}
+	return string(data)
+}
+
 // fromJSON converts a JSON document into a map[string]interface{}.
 //
 // This is not a general-purpose JSON parser, and will not parse all valid


Test patch:
diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go
index a4f4d604f..99edf5ae9 100644
--- a/pkg/engine/funcs_test.go
+++ b/pkg/engine/funcs_test.go
@@ -135,6 +135,43 @@ keyInElement1 = "valueInElement1"`,
 		assert.NoError(t, err)
 		assert.Equal(t, tt.expect, b.String(), tt.tpl)
 	}
+
+	loopMap := map[string]interface{}{
+		"foo": "bar",
+	}
+	loopMap["loop"] = []interface{}{loopMap}
+
+	mustFuncsTests := []struct {
+		tpl    string
+		expect interface{}
+		vars   interface{}
+	}{{
+		tpl:  `{{ mustToYaml . }}`,
+		vars: loopMap,
+	}, {
+		tpl:  `{{ mustToJson . }}`,
+		vars: loopMap,
+	}, {
+		tpl:    `{{ toYaml . }}`,
+		expect: "", // should return empty string and swallow error
+		vars:   loopMap,
+	}, {
+		tpl:    `{{ toJson . }}`,
+		expect: "", // should return empty string and swallow error
+		vars:   loopMap,
+	},
+	}
+
+	for _, tt := range mustFuncsTests {
+		var b strings.Builder
+		err := template.Must(template.New("test").Funcs(funcMap()).Parse(tt.tpl)).Execute(&b, tt.vars)
+		if tt.expect != nil {
+			assert.NoError(t, err)
+			assert.Equal(t, tt.expect, b.String(), tt.tpl)
+		} else {
+			assert.Error(t, err)
+		}
+	}
 }
 
 // This test to check a function provided by sprig is due to a change in a