From 7d5bcac49b87362fb431a0956eccebbe4e74d367 Mon Sep 17 00:00:00 2001 From: "piotr.laczykowski" Date: Wed, 25 Mar 2026 15:14:04 +0100 Subject: [PATCH 1/4] fix(engine): override toPrettyJson to disable HTML escaping Sprig's toPrettyJson uses json.MarshalIndent which HTML-escapes &, <, > to \u0026, \u003c, \u003e. This breaks URLs and other strings containing these characters when used in Helm chart templates. Override toPrettyJson and mustToPrettyJson in the engine FuncMap with implementations that use json.Encoder with SetEscapeHTML(false), matching Sprig's two-space indentation for backward compatibility. Signed-off-by: piotr.laczykowski --- pkg/engine/funcs.go | 68 +++++++++++++++++++++++++++++++++------- pkg/engine/funcs_test.go | 20 ++++++++++++ 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index e03c13b38..b5fb62578 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -49,18 +49,20 @@ func funcMap() template.FuncMap { // Add some extra functionality extra := template.FuncMap{ - "toToml": toTOML, - "mustToToml": mustToTOML, - "fromToml": fromTOML, - "toYaml": toYAML, - "mustToYaml": mustToYAML, - "toYamlPretty": toYAMLPretty, - "fromYaml": fromYAML, - "fromYamlArray": fromYAMLArray, - "toJson": toJSON, - "mustToJson": mustToJSON, - "fromJson": fromJSON, - "fromJsonArray": fromJSONArray, + "toToml": toTOML, + "mustToToml": mustToTOML, + "fromToml": fromTOML, + "toYaml": toYAML, + "mustToYaml": mustToYAML, + "toYamlPretty": toYAMLPretty, + "fromYaml": fromYAML, + "fromYamlArray": fromYAMLArray, + "toJson": toJSON, + "mustToJson": mustToJSON, + "toPrettyJson": toPrettyJSON, + "mustToPrettyJson": mustToPrettyJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, // This is a placeholder for the "include" function, which is // late-bound to a template. By declaring it here, we preserve the @@ -220,6 +222,48 @@ func mustToJSON(v any) string { return string(data) } +// encodePrettyJSON encodes v as indented JSON without HTML-escaping special +// characters (&, <, >). It uses two-space indentation to match the behavior +// of Sprig's toPrettyJson. +func encodePrettyJSON(v any) (string, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + if err := enc.Encode(v); err != nil { + return "", err + } + return strings.TrimSuffix(buf.String(), "\n"), nil +} + +// toPrettyJSON takes an interface, marshals it to indented JSON without +// HTML-escaping special characters (&, <, >), and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toPrettyJSON(v any) string { + s, err := encodePrettyJSON(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return s +} + +// mustToPrettyJSON takes an interface, marshals it to indented JSON without +// HTML-escaping special characters (&, <, >), and returns a string. +// It will panic if there is an error. +// +// This is designed to be called from a template when you need to ensure that +// the output JSON is valid. +func mustToPrettyJSON(v any) string { + s, err := encodePrettyJSON(v) + if err != nil { + panic(err) + } + return s +} + // fromJSON converts a JSON document into a map[string]interface{}. // // This is not a general-purpose JSON parser, and will not parse all valid diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index be9d0153f..824f1033c 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -69,6 +69,15 @@ keyInElement1 = "valueInElement1"`, tpl: `{{ toJson . }}`, expect: `{"foo":"bar"}`, vars: map[string]any{"foo": "bar"}, + }, { + // toPrettyJson must not HTML-escape &, <, > and must use 2-space indent + tpl: "{{ toPrettyJson . }}", + expect: "{\n \"url\": \"https://example.com?a=1&b=2<>\"\n}", + vars: map[string]any{"url": "https://example.com?a=1&b=2<>"}, + }, { + tpl: "{{ toPrettyJson . }}", + expect: "{\n \"foo\": \"bar\"\n}", + vars: map[string]any{"foo": "bar"}, }, { tpl: `{{ fromYaml . }}`, expect: "map[hello:world]", @@ -151,6 +160,17 @@ keyInElement1 = "valueInElement1"`, }, { tpl: `{{ mustToJson . }}`, vars: loopMap, + }, { + tpl: `{{ mustToPrettyJson . }}`, + vars: loopMap, // circular reference must panic + }, { + tpl: `{{ mustToPrettyJson . }}`, + expect: "{\n \"foo\": \"bar\"\n}", + vars: map[string]any{"foo": "bar"}, + }, { + tpl: `{{ toPrettyJson . }}`, + expect: "", // circular reference must swallow error and return "" + vars: loopMap, }, { tpl: `{{ toYaml . }}`, expect: "", // should return empty string and swallow error From e98802f10e613ddd5c361c473011c8199f653305 Mon Sep 17 00:00:00 2001 From: "piotr.laczykowski" Date: Sat, 30 May 2026 21:13:00 +0200 Subject: [PATCH 2/4] fix(engine): introduce toPrettyRawJson instead of overriding toPrettyJson MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer review, do not override Sprig's toPrettyJson/mustToPrettyJson (which HTML-escape &, <, >), since existing charts may rely on the escaped output. Instead add new toPrettyRawJson/mustToPrettyRawJson functions that produce indented JSON with HTML characters unescaped — the indented counterpart to Sprig's toRawJson — preserving backwards compatibility. Add a regression test asserting toPrettyJson still escapes HTML. Co-Authored-By: claude-flow Signed-off-by: piotr.laczykowski --- pkg/engine/funcs.go | 40 ++++++++++++++++++++++++---------------- pkg/engine/funcs_test.go | 18 ++++++++++++------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 38576a3ed..9e88ee950 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -63,12 +63,12 @@ func funcMap() template.FuncMap { "toYamlPretty": toYAMLPretty, "fromYaml": fromYAML, "fromYamlArray": fromYAMLArray, - "toJson": toJSON, - "mustToJson": mustToJSON, - "toPrettyJson": toPrettyJSON, - "mustToPrettyJson": mustToPrettyJSON, - "fromJson": fromJSON, - "fromJsonArray": fromJSONArray, + "toJson": toJSON, + "mustToJson": mustToJSON, + "toPrettyRawJson": toPrettyRawJSON, + "mustToPrettyRawJson": mustToPrettyRawJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, // Duration helpers "mustToDuration": mustToDuration, @@ -241,10 +241,11 @@ func mustToJSON(v any) string { return string(data) } -// encodePrettyJSON encodes v as indented JSON without HTML-escaping special -// characters (&, <, >). It uses two-space indentation to match the behavior -// of Sprig's toPrettyJson. -func encodePrettyJSON(v any) (string, error) { +// encodePrettyRawJSON encodes v as indented JSON without HTML-escaping special +// characters (&, <, >). It uses two-space indentation to match the indentation +// of Sprig's toPrettyJson, while leaving HTML characters unescaped like Sprig's +// toRawJson. +func encodePrettyRawJSON(v any) (string, error) { var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) @@ -255,13 +256,17 @@ func encodePrettyJSON(v any) (string, error) { return strings.TrimSuffix(buf.String(), "\n"), nil } -// toPrettyJSON takes an interface, marshals it to indented JSON without +// toPrettyRawJSON takes an interface, marshals it to indented JSON without // HTML-escaping special characters (&, <, >), and returns a string. It will // always return a string, even on marshal error (empty string). // +// Unlike Sprig's toPrettyJson, HTML characters are not escaped. This is the +// indented counterpart to Sprig's toRawJson. The escaping behavior of +// toPrettyJson is intentionally left unchanged for backwards compatibility. +// // This is designed to be called from a template. -func toPrettyJSON(v any) string { - s, err := encodePrettyJSON(v) +func toPrettyRawJSON(v any) string { + s, err := encodePrettyRawJSON(v) if err != nil { // Swallow errors inside of a template. return "" @@ -269,14 +274,17 @@ func toPrettyJSON(v any) string { return s } -// mustToPrettyJSON takes an interface, marshals it to indented JSON without +// mustToPrettyRawJSON takes an interface, marshals it to indented JSON without // HTML-escaping special characters (&, <, >), and returns a string. // It will panic if there is an error. // +// Unlike Sprig's mustToPrettyJson, HTML characters are not escaped. This is the +// indented counterpart to Sprig's mustToRawJson. +// // This is designed to be called from a template when you need to ensure that // the output JSON is valid. -func mustToPrettyJSON(v any) string { - s, err := encodePrettyJSON(v) +func mustToPrettyRawJSON(v any) string { + s, err := encodePrettyRawJSON(v) if err != nil { panic(err) } diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index 38a1b7282..66effe841 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -73,14 +73,20 @@ keyInElement1 = "valueInElement1"`, expect: `{"foo":"bar"}`, vars: map[string]any{"foo": "bar"}, }, { - // toPrettyJson must not HTML-escape &, <, > and must use 2-space indent - tpl: "{{ toPrettyJson . }}", + // toPrettyRawJson must not HTML-escape &, <, > and must use 2-space indent + tpl: "{{ toPrettyRawJson . }}", expect: "{\n \"url\": \"https://example.com?a=1&b=2<>\"\n}", vars: map[string]any{"url": "https://example.com?a=1&b=2<>"}, }, { - tpl: "{{ toPrettyJson . }}", + tpl: "{{ toPrettyRawJson . }}", expect: "{\n \"foo\": \"bar\"\n}", vars: map[string]any{"foo": "bar"}, + }, { + // toPrettyJson (from Sprig) must keep HTML-escaping &, <, > for + // backwards compatibility. + tpl: "{{ toPrettyJson . }}", + expect: "{\n \"url\": \"https://example.com?a=1\\u0026b=2\\u003c\\u003e\"\n}", + vars: map[string]any{"url": "https://example.com?a=1&b=2<>"}, }, { tpl: `{{ fromYaml . }}`, expect: "map[hello:world]", @@ -164,14 +170,14 @@ keyInElement1 = "valueInElement1"`, tpl: `{{ mustToJson . }}`, vars: loopMap, }, { - tpl: `{{ mustToPrettyJson . }}`, + tpl: `{{ mustToPrettyRawJson . }}`, vars: loopMap, // circular reference must panic }, { - tpl: `{{ mustToPrettyJson . }}`, + tpl: `{{ mustToPrettyRawJson . }}`, expect: "{\n \"foo\": \"bar\"\n}", vars: map[string]any{"foo": "bar"}, }, { - tpl: `{{ toPrettyJson . }}`, + tpl: `{{ toPrettyRawJson . }}`, expect: "", // circular reference must swallow error and return "" vars: loopMap, }, { From eb9276703c0e606ddc1e6d0ec699611778ecf914 Mon Sep 17 00:00:00 2001 From: "piotr.laczykowski" Date: Fri, 14 Aug 2026 16:21:15 +0200 Subject: [PATCH 3/4] fix(engine): gofmt alignment in funcMap Co-Authored-By: claude-flow Signed-off-by: piotr.laczykowski --- pkg/engine/funcs.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 9e88ee950..9ae87e75c 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -55,14 +55,14 @@ func funcMap() template.FuncMap { // Add some extra functionality extra := template.FuncMap{ - "toToml": toTOML, - "mustToToml": mustToTOML, - "fromToml": fromTOML, - "toYaml": toYAML, - "mustToYaml": mustToYAML, - "toYamlPretty": toYAMLPretty, - "fromYaml": fromYAML, - "fromYamlArray": fromYAMLArray, + "toToml": toTOML, + "mustToToml": mustToTOML, + "fromToml": fromTOML, + "toYaml": toYAML, + "mustToYaml": mustToYAML, + "toYamlPretty": toYAMLPretty, + "fromYaml": fromYAML, + "fromYamlArray": fromYAMLArray, "toJson": toJSON, "mustToJson": mustToJSON, "toPrettyRawJson": toPrettyRawJSON, From 93e0d4007184a23c0e4b7588eb6ebf307e7d528a Mon Sep 17 00:00:00 2001 From: "piotr.laczykowski" Date: Fri, 14 Aug 2026 16:27:15 +0200 Subject: [PATCH 4/4] fix(engine): resolve leftover merge conflict in funcs_test.go The merge of main left unresolved conflict markers in the mustFuncsTests table. Keep main's brace and indentation style for the slice literal, and re-apply the toPrettyRawJson/mustToPrettyRawJson cases on top of it. Co-Authored-By: claude-flow Signed-off-by: piotr.laczykowski --- pkg/engine/funcs_test.go | 58 ++++++++-------------------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index 6d995bf7f..da18c3a23 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -162,52 +162,6 @@ keyInElement1 = "valueInElement1"`, tpl string expect any vars any -<<<<<<< HEAD - }{{ - tpl: `{{ mustToYaml . }}`, - vars: loopMap, - }, { - tpl: `{{ mustToJson . }}`, - vars: loopMap, - }, { - tpl: `{{ mustToPrettyRawJson . }}`, - vars: loopMap, // circular reference must panic - }, { - tpl: `{{ mustToPrettyRawJson . }}`, - expect: "{\n \"foo\": \"bar\"\n}", - vars: map[string]any{"foo": "bar"}, - }, { - tpl: `{{ toPrettyRawJson . }}`, - expect: "", // circular reference must swallow error and return "" - vars: loopMap, - }, { - tpl: `{{ mustToDuration 30 }}`, - expect: `30s`, - vars: nil, - }, { - tpl: `{{ mustToDuration "1m30s" }}`, - expect: `1m30s`, - vars: nil, - }, { - tpl: `{{ mustToDuration "foo" }}`, - vars: nil, - }, { - tpl: `{{ toYaml . }}`, - expect: "", // should return empty string and swallow error - vars: loopMap, - }, { - tpl: `{{ toJson . }}`, - expect: "", // should return empty string and swallow error - vars: loopMap, - }, { - tpl: `{{ mustToToml . }}`, - vars: map[int]string{1: "one"}, // non-string key is invalid in TOML - }, { - tpl: `{{ mustToToml . }}`, - expect: "foo = \"bar\"\n", // should succeed and return TOML string - vars: map[string]string{"foo": "bar"}, - }, -======= }{ { tpl: `{{ mustToYaml . }}`, @@ -215,6 +169,17 @@ keyInElement1 = "valueInElement1"`, }, { tpl: `{{ mustToJson . }}`, vars: loopMap, + }, { + tpl: `{{ mustToPrettyRawJson . }}`, + vars: loopMap, // circular reference must panic + }, { + tpl: `{{ mustToPrettyRawJson . }}`, + expect: "{\n \"foo\": \"bar\"\n}", + vars: map[string]any{"foo": "bar"}, + }, { + tpl: `{{ toPrettyRawJson . }}`, + expect: "", // circular reference must swallow error and return "" + vars: loopMap, }, { tpl: `{{ mustToDuration 30 }}`, expect: `30s`, @@ -242,7 +207,6 @@ keyInElement1 = "valueInElement1"`, expect: "foo = \"bar\"\n", // should succeed and return TOML string vars: map[string]string{"foo": "bar"}, }, ->>>>>>> f3d68cdbea3076a0283cbd1e3ec44a409ad48a43 } for _, tt := range mustFuncsTests {