diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 72343d7b6..ce41b98b9 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -19,6 +19,7 @@ package loader import ( "bufio" "bytes" + "encoding/json" "errors" "fmt" "io" @@ -206,7 +207,12 @@ func LoadValues(data io.Reader) (map[string]any, error) { } return nil, fmt.Errorf("error reading yaml document: %w", err) } - if err := yaml.Unmarshal(raw, ¤tMap); err != nil { + // Decode numbers as json.Number so integers stay integers through + // toYaml/toToml. v2 charts keep float64 for compatibility; see #30884. + if err := yaml.Unmarshal(raw, ¤tMap, func(d *json.Decoder) *json.Decoder { + d.UseNumber() + return d + }); err != nil { return nil, fmt.Errorf("cannot unmarshal yaml document: %w", err) } values = MergeMaps(values, currentMap) diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index 2163cf4e2..35896bdea 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -20,6 +20,7 @@ import ( "archive/tar" "bytes" "compress/gzip" + "encoding/json" "errors" "fmt" "io" @@ -31,8 +32,10 @@ import ( "testing" "time" + "github.com/BurntSushi/toml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/pkg/chart/common" @@ -420,6 +423,43 @@ foo: }, }, }, + "It should load integers as json.Number": { + data: []byte(` +key: 13 +`), + expctedValues: map[string]any{ + "key": json.Number("13"), + }, + }, + "It should load floats as json.Number": { + data: []byte(` +pi: 3.14 +`), + expctedValues: map[string]any{ + "pi": json.Number("3.14"), + }, + }, + "It should keep integers larger than float64 mantissa as json.Number": { + data: []byte(` +key: 9007199254740993 +`), + expctedValues: map[string]any{ + "key": json.Number("9007199254740993"), + }, + }, + "It should load nested numbers as json.Number": { + data: []byte(` +global: + key: 13 + ratio: 1.5 +`), + expctedValues: map[string]any{ + "global": map[string]any{ + "key": json.Number("13"), + "ratio": json.Number("1.5"), + }, + }, + }, } for testName, testCase := range testCases { t.Run(testName, func(tt *testing.T) { @@ -430,6 +470,33 @@ foo: } } +func TestLoadValuesNumberEncoding(t *testing.T) { + values, err := LoadValues(bytes.NewReader([]byte(` +global: + key: 13 + large: 9007199254740993 + pi: 3.14 +`))) + require.NoError(t, err) + + yamlOut, err := yaml.Marshal(values) + require.NoError(t, err) + assert.Contains(t, string(yamlOut), "key: 13") + assert.Contains(t, string(yamlOut), "large: 9007199254740993") + assert.Contains(t, string(yamlOut), "pi: 3.14") + assert.NotContains(t, string(yamlOut), "13.0") + assert.NotContains(t, string(yamlOut), "1.048576e+") + + var tomlBuf bytes.Buffer + require.NoError(t, toml.NewEncoder(&tomlBuf).Encode(values)) + tomlOut := tomlBuf.String() + assert.Contains(t, tomlOut, "key = 13") + assert.Contains(t, tomlOut, "large = 9007199254740993") + assert.Contains(t, tomlOut, "pi = 3.14") + assert.NotContains(t, tomlOut, "13.0") + assert.NotContains(t, tomlOut, "key = \"13\"") +} + func TestLoadValuesEOFBoundary(t *testing.T) { // Reproduces #32506: a single logical line whose length is a multiple of // bufio's default buffer (4096) and has no trailing newline used to be diff --git a/internal/chart/v3/util/dependencies_test.go b/internal/chart/v3/util/dependencies_test.go index 45693ef46..02af85b77 100644 --- a/internal/chart/v3/util/dependencies_test.go +++ b/internal/chart/v3/util/dependencies_test.go @@ -15,6 +15,7 @@ limitations under the License. package util import ( + "encoding/json" "os" "path/filepath" "sort" @@ -219,6 +220,8 @@ func TestProcessDependencyImportValues(t *testing.T) { case bool: b := strconv.FormatBool(pv) assert.Equalf(t, vv, b, "failed to match imported bool value %v with expected %v for key %q", b, vv, kk) + case json.Number: + assert.Equalf(t, vv, pv.String(), "failed to match imported number value %q with expected %q for key %q", pv, vv, kk) default: assert.Equalf(t, vv, pv, "failed to match imported string value %q with expected %q for key %q", pv, vv, kk) } @@ -296,6 +299,8 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { case float64: s := strconv.FormatFloat(pv, 'f', -1, 64) assert.Equalf(t, vv, s, "failed to match imported float value %v with expected %v", s, vv) + case json.Number: + assert.Equalf(t, vv, pv.String(), "failed to match imported number value %q with expected %q", pv, vv) default: assert.Equalf(t, vv, pv, "failed to match imported string value %q with expected %q", pv, vv) } diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index 03ed64153..256729e8b 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -17,6 +17,7 @@ limitations under the License. package engine import ( + "encoding/json" "math" "strings" "testing" @@ -36,6 +37,22 @@ func TestFuncs(t *testing.T) { tpl: `{{ toYaml . }}`, expect: `foo: bar`, vars: map[string]any{"foo": "bar"}, + }, { + tpl: `{{ toYaml . }}`, + expect: "key: 13", + vars: map[string]any{"key": json.Number("13")}, + }, { + tpl: `{{ toYaml . }}`, + expect: "large: 9007199254740993", + vars: map[string]any{"large": json.Number("9007199254740993")}, + }, { + tpl: `{{ toYaml . }}`, + expect: "pi: 3.14", + vars: map[string]any{"pi": json.Number("3.14")}, + }, { + tpl: `{{ toToml . }}`, + expect: "key = 13\n", + vars: map[string]any{"key": json.Number("13")}, }, { tpl: `{{ toYamlPretty . }}`, expect: "baz:\n - 1\n - 2\n - 3",