From 51104a8d0021eb080e627cf374463bbed89aef15 Mon Sep 17 00:00:00 2001 From: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Date: Fri, 22 May 2026 12:28:49 +0530 Subject: [PATCH 1/4] Fix toYamlPretty integer output Signed-off-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> (cherry picked from commit fc77c0e61aac40c4336f54d121909cd9547ceef5) --- pkg/engine/funcs.go | 33 ++++++++++++++++++++++++++++++++- pkg/engine/funcs_test.go | 4 ++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index d03a818c2..5eef2a2b8 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -95,7 +95,7 @@ func toYAMLPretty(v interface{}) string { var data bytes.Buffer encoder := goYaml.NewEncoder(&data) encoder.SetIndent(2) - err := encoder.Encode(v) + err := encoder.Encode(normalizeYAMLScalars(v)) if err != nil { // Swallow errors inside of a template. @@ -104,6 +104,37 @@ func toYAMLPretty(v interface{}) string { return strings.TrimSuffix(data.String(), "\n") } +func normalizeYAMLScalars(v any) any { + switch typedValue := v.(type) { + case map[string]any: + normalized := make(map[string]any, len(typedValue)) + for key, value := range typedValue { + normalized[key] = normalizeYAMLScalars(value) + } + return normalized + case map[any]any: + normalized := make(map[any]any, len(typedValue)) + for key, value := range typedValue { + normalized[key] = normalizeYAMLScalars(value) + } + return normalized + case []any: + normalized := make([]any, len(typedValue)) + for index, value := range typedValue { + normalized[index] = normalizeYAMLScalars(value) + } + return normalized + case float64: + // sigs.k8s.io/yaml may unmarshal integer YAML values as float64. + if typedValue == math.Trunc(typedValue) && + typedValue > float64(math.MinInt64) && + typedValue < float64(math.MaxInt64) { + return int64(typedValue) + } + } + return v +} + // fromYAML converts a YAML document into a map[string]interface{}. // // This is not a general-purpose YAML parser, and will not parse all valid diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index a7e2506a3..c876dd318 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -37,6 +37,10 @@ func TestFuncs(t *testing.T) { tpl: `{{ toYamlPretty . }}`, expect: "baz:\n - 1\n - 2\n - 3", vars: map[string]interface{}{"baz": []int{1, 2, 3}}, + }, { + tpl: `{{ toYamlPretty (fromYaml .) }}`, + expect: "foo: 1000000", + vars: "foo: !!int 1000000", }, { tpl: `{{ toToml . }}`, expect: "foo = \"bar\"\n", From d77f523794a3c4a92d38a479c72c9470536b08a9 Mon Sep 17 00:00:00 2001 From: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Date: Sat, 23 May 2026 10:47:30 +0530 Subject: [PATCH 2/4] fix(engine): harden YAML scalar normalization Signed-off-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> (cherry picked from commit faddb58b9bca771e011280c40d6b9f9264fe90b9) --- pkg/engine/funcs.go | 28 ++++++++++++++++++----- pkg/engine/funcs_test.go | 48 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 5eef2a2b8..e1a12ea76 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -19,6 +19,8 @@ package engine import ( "bytes" "encoding/json" + "math" + "reflect" "strings" "text/template" @@ -28,6 +30,8 @@ import ( goYaml "sigs.k8s.io/yaml/goyaml.v3" ) +const maxSafeYAMLInteger = 1 << 53 + // funcMap returns a mapping of all of the functions that Engine has. // // Because some functions are late-bound (e.g. contain context-sensitive @@ -95,9 +99,12 @@ func toYAMLPretty(v interface{}) string { var data bytes.Buffer encoder := goYaml.NewEncoder(&data) encoder.SetIndent(2) - err := encoder.Encode(normalizeYAMLScalars(v)) - if err != nil { + if err := encoder.Encode(normalizeYAMLScalars(v)); err != nil { + // Swallow errors inside of a template. + return "" + } + if err := encoder.Close(); err != nil { // Swallow errors inside of a template. return "" } @@ -115,7 +122,7 @@ func normalizeYAMLScalars(v any) any { case map[any]any: normalized := make(map[any]any, len(typedValue)) for key, value := range typedValue { - normalized[key] = normalizeYAMLScalars(value) + normalized[normalizeYAMLMapKey(key)] = normalizeYAMLScalars(value) } return normalized case []any: @@ -126,15 +133,24 @@ func normalizeYAMLScalars(v any) any { return normalized case float64: // sigs.k8s.io/yaml may unmarshal integer YAML values as float64. - if typedValue == math.Trunc(typedValue) && - typedValue > float64(math.MinInt64) && - typedValue < float64(math.MaxInt64) { + if typedValue == math.Trunc(typedValue) && math.Abs(typedValue) <= maxSafeYAMLInteger { return int64(typedValue) } } return v } +func normalizeYAMLMapKey(key any) any { + normalized := normalizeYAMLScalars(key) + if normalized == nil { + return normalized + } + if reflect.TypeOf(normalized).Comparable() { + return normalized + } + return key +} + // fromYAML converts a YAML document into a map[string]interface{}. // // This is not a general-purpose YAML parser, and will not parse all valid diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index c876dd318..8958c2352 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -17,6 +17,7 @@ limitations under the License. package engine import ( + "math" "strings" "testing" "text/template" @@ -141,6 +142,53 @@ keyInElement1 = "valueInElement1"`, } } +func TestNormalizeYAMLScalars(t *testing.T) { + aboveSafeInteger := math.Nextafter(maxSafeYAMLInteger, math.Inf(1)) + + tests := []struct { + name string + input any + expect any + }{ + { + name: "non-integer floats stay floats", + input: map[string]any{"value": 1.5}, + expect: map[string]any{"value": 1.5}, + }, + { + name: "safe integer floats become integers", + input: map[string]any{"value": 1.0}, + expect: map[string]any{"value": int64(1)}, + }, + { + name: "unsafe integer floats stay floats", + input: map[string]any{"value": aboveSafeInteger}, + expect: map[string]any{"value": aboveSafeInteger}, + }, + { + name: "map keys and nested values are normalized", + input: map[any]any{ + float64(2): float64(3), + "nested": map[any]any{ + float64(4): []any{float64(5)}, + }, + }, + expect: map[any]any{ + int64(2): int64(3), + "nested": map[any]any{ + int64(4): []any{int64(5)}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expect, normalizeYAMLScalars(tt.input)) + }) + } +} + // This test to check a function provided by sprig is due to a change in a // dependency of sprig. mergo in v0.3.9 changed the way it merges and only does // public fields (i.e. those starting with a capital letter). This test, from From cabc387810759bb4ee1e82aac0bf00fe97396890 Mon Sep 17 00:00:00 2001 From: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Date: Sat, 23 May 2026 23:57:03 +0530 Subject: [PATCH 3/4] fix(engine): harden YAML scalar edge cases Signed-off-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> --- pkg/engine/funcs.go | 21 ++++++++++++++++++--- pkg/engine/funcs_test.go | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index e1a12ea76..49c7b15ff 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -19,6 +19,7 @@ package engine import ( "bytes" "encoding/json" + "fmt" "math" "reflect" "strings" @@ -30,7 +31,7 @@ import ( goYaml "sigs.k8s.io/yaml/goyaml.v3" ) -const maxSafeYAMLInteger = 1 << 53 +const maxSafeYAMLInteger = (1 << 53) - 1 // funcMap returns a mapping of all of the functions that Engine has. // @@ -99,12 +100,23 @@ func toYAMLPretty(v interface{}) string { var data bytes.Buffer encoder := goYaml.NewEncoder(&data) encoder.SetIndent(2) + closeEncoder := func() error { + if encoder == nil { + return nil + } + err := encoder.Close() + encoder = nil + return err + } + defer func() { + _ = closeEncoder() + }() if err := encoder.Encode(normalizeYAMLScalars(v)); err != nil { // Swallow errors inside of a template. return "" } - if err := encoder.Close(); err != nil { + if err := closeEncoder(); err != nil { // Swallow errors inside of a template. return "" } @@ -148,7 +160,10 @@ func normalizeYAMLMapKey(key any) any { if reflect.TypeOf(normalized).Comparable() { return normalized } - return key + if reflect.TypeOf(key).Comparable() { + return key + } + return fmt.Sprint(normalized) } // fromYAML converts a YAML document into a map[string]interface{}. diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index 8958c2352..17b1de929 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -160,11 +160,26 @@ func TestNormalizeYAMLScalars(t *testing.T) { input: map[string]any{"value": 1.0}, expect: map[string]any{"value": int64(1)}, }, + { + name: "max safe integer float becomes integer", + input: map[string]any{"value": float64(maxSafeYAMLInteger)}, + expect: map[string]any{"value": int64(maxSafeYAMLInteger)}, + }, { name: "unsafe integer floats stay floats", input: map[string]any{"value": aboveSafeInteger}, expect: map[string]any{"value": aboveSafeInteger}, }, + { + name: "safe negative integer floats become integers", + input: map[string]any{"value": -float64(maxSafeYAMLInteger)}, + expect: map[string]any{"value": -int64(maxSafeYAMLInteger)}, + }, + { + name: "unsafe negative integer floats stay floats", + input: map[string]any{"value": -aboveSafeInteger}, + expect: map[string]any{"value": -aboveSafeInteger}, + }, { name: "map keys and nested values are normalized", input: map[any]any{ @@ -189,6 +204,11 @@ func TestNormalizeYAMLScalars(t *testing.T) { } } +func TestNormalizeYAMLMapKey(t *testing.T) { + assert.Equal(t, int64(1), normalizeYAMLMapKey(float64(1))) + assert.Equal(t, "[1 key]", normalizeYAMLMapKey([]any{float64(1), "key"})) +} + // This test to check a function provided by sprig is due to a change in a // dependency of sprig. mergo in v0.3.9 changed the way it merges and only does // public fields (i.e. those starting with a capital letter). This test, from From 91933f110790a4b77de30827cbf87d4f8992864d Mon Sep 17 00:00:00 2001 From: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Date: Sun, 24 May 2026 01:18:34 +0530 Subject: [PATCH 4/4] fix(engine): tighten YAML scalar edge cases Assisted-by: OpenAI GPT-5 Signed-off-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> --- pkg/engine/funcs.go | 3 --- pkg/engine/funcs_test.go | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 49c7b15ff..7f2b353d9 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -160,9 +160,6 @@ func normalizeYAMLMapKey(key any) any { if reflect.TypeOf(normalized).Comparable() { return normalized } - if reflect.TypeOf(key).Comparable() { - return key - } return fmt.Sprint(normalized) } diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index 17b1de929..835ee4c19 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -143,6 +143,7 @@ keyInElement1 = "valueInElement1"`, } func TestNormalizeYAMLScalars(t *testing.T) { + firstUnsafeInteger := float64(maxSafeYAMLInteger + 1) aboveSafeInteger := math.Nextafter(maxSafeYAMLInteger, math.Inf(1)) tests := []struct { @@ -165,6 +166,11 @@ func TestNormalizeYAMLScalars(t *testing.T) { input: map[string]any{"value": float64(maxSafeYAMLInteger)}, expect: map[string]any{"value": int64(maxSafeYAMLInteger)}, }, + { + name: "first unsafe integer float stays float", + input: map[string]any{"value": firstUnsafeInteger}, + expect: map[string]any{"value": firstUnsafeInteger}, + }, { name: "unsafe integer floats stay floats", input: map[string]any{"value": aboveSafeInteger},