Repository: helm/helm
Pull request #13534 (2024-12-14 23:31:17)

fix (helm) : toToml` renders int as float [ backport to v3 ]
This commit fixes the issue where the `yaml.Unmarshaller` converts all int values read from yaml docs into float64; this passes an option to the decoder, enabling int conversion into.

closes https://github.com/helm/helm/issues/12987 https://github.com/helm/helm/issues/6539 https://github.com/helm/helm/issues/2474

Signed-off-by: Althaf M [althafm@outlook.com](mailto:althafm@outlook.com)

<!--  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**:

This is required so that helm function `toToml` prints out the integer values as integers rather than floats. Without this, it prints it as float strings, appending a `.0` to the end.

Currently, 
Assuming values.yaml 
```
global:
  key: 13
```

if we have a template that renders using `toToml` function, like 

```
apiVersion: v1
kind: ConfigMap
metadata:
  name: issue-totoml
data: |
  {{ .Values.global | toToml }}
```

Will get rendered as 

```
# Source: issue-totoml/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: issue-totoml
data: |
  key = 13.0
```

With this PR, it will be rendered as integer as expected : 

```
# Source: issue-totoml/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: issue-totoml
data: |
  key = 13
```

**Special notes for your reviewer**:

Why does this happen only in the `toToml` function?

I have ensured all tests pass with the `make test-unit`  command and fixed the failing tests due to the type change.


The [library](https://github.com/BurntSushi/toml) we use to convert to the TOML uses the reflection to identify the type of elements or values and prints them out based on the type. Other libraries, like YAML and JSON, ignore the element type. This causes the output to be printed out as a float rather than as an integer. The root cause is that when using the `yaml.Unmarshal` library, we are parsing all the integers as float64 values.

**If applicable**:
- [ ] this PR contains user facing changes (the `docs needed` label should be applied if so)
- [X] this PR contains unit tests
- [X] this PR has been tested for backwards compatibility



Patch:
diff --git a/pkg/chart/loader/load.go b/pkg/chart/loader/load.go
index a68a05aa9..622dac4b8 100644
--- a/pkg/chart/loader/load.go
+++ b/pkg/chart/loader/load.go
@@ -18,6 +18,7 @@ package loader
 
 import (
 	"bytes"
+	"encoding/json"
 	"log"
 	"os"
 	"path/filepath"
@@ -104,7 +105,10 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
 			}
 		case f.Name == "values.yaml":
 			c.Values = make(map[string]interface{})
-			if err := yaml.Unmarshal(f.Data, &c.Values); err != nil {
+			if err := yaml.Unmarshal(f.Data, &c.Values, func(d *json.Decoder) *json.Decoder {
+				d.UseNumber()
+				return d
+			}); err != nil {
 				return c, errors.Wrap(err, "cannot load values.yaml")
 			}
 		case f.Name == "values.schema.json":
diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go
index 61c633a6d..566951ec5 100644
--- a/pkg/chartutil/values.go
+++ b/pkg/chartutil/values.go
@@ -17,6 +17,7 @@ limitations under the License.
 package chartutil
 
 import (
+	"encoding/json"
 	"fmt"
 	"io"
 	"os"
@@ -105,7 +106,10 @@ func tableLookup(v Values, simple string) (Values, error) {
 
 // ReadValues will parse YAML byte data into a Values.
 func ReadValues(data []byte) (vals Values, err error) {
-	err = yaml.Unmarshal(data, &vals)
+	err = yaml.Unmarshal(data, &vals, func(d *json.Decoder) *json.Decoder {
+		d.UseNumber()
+		return d
+	})
 	if len(vals) == 0 {
 		vals = Values{}
 	}


Test patch:
diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go
index e5b939879..d13f96057 100644
--- a/cmd/helm/template_test.go
+++ b/cmd/helm/template_test.go
@@ -161,6 +161,11 @@ func TestTemplateCmd(t *testing.T) {
 			cmd:    fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath),
 			golden: "output/template-subchart-cm-set-file.txt",
 		},
+		{
+			name:   "check toToml function rendering",
+			cmd:    fmt.Sprintf("template '%s'", "testdata/testcharts/issue-totoml"),
+			golden: "output/issue-totoml.txt",
+		},
 	}
 	runTestCmd(t, tests)
 }
diff --git a/cmd/helm/testdata/output/issue-totoml.txt b/cmd/helm/testdata/output/issue-totoml.txt
new file mode 100644
index 000000000..06cf4bb8d
--- /dev/null
+++ b/cmd/helm/testdata/output/issue-totoml.txt
@@ -0,0 +1,8 @@
+---
+# Source: issue-totoml/templates/configmap.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: issue-totoml
+data: |
+  key = 13
diff --git a/cmd/helm/testdata/testcharts/issue-totoml/Chart.yaml b/cmd/helm/testdata/testcharts/issue-totoml/Chart.yaml
new file mode 100644
index 000000000..2815d0d21
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/issue-totoml/Chart.yaml
@@ -0,0 +1,3 @@
+apiVersion: v2
+name: issue-totoml
+version: 0.1.0
\ No newline at end of file
diff --git a/cmd/helm/testdata/testcharts/issue-totoml/templates/configmap.yaml b/cmd/helm/testdata/testcharts/issue-totoml/templates/configmap.yaml
new file mode 100644
index 000000000..2db705b63
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/issue-totoml/templates/configmap.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: issue-totoml
+data: |
+  {{ .Values.global | toToml }}
\ No newline at end of file
diff --git a/cmd/helm/testdata/testcharts/issue-totoml/values.yaml b/cmd/helm/testdata/testcharts/issue-totoml/values.yaml
new file mode 100644
index 000000000..dd0140449
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/issue-totoml/values.yaml
@@ -0,0 +1,2 @@
+global:
+  key: 13
\ No newline at end of file
diff --git a/pkg/chartutil/dependencies_test.go b/pkg/chartutil/dependencies_test.go
index 7e035be5f..bf8b1a9b5 100644
--- a/pkg/chartutil/dependencies_test.go
+++ b/pkg/chartutil/dependencies_test.go
@@ -15,6 +15,7 @@ limitations under the License.
 package chartutil
 
 import (
+	"encoding/json"
 	"os"
 	"path/filepath"
 	"sort"
@@ -237,6 +238,20 @@ func TestProcessDependencyImportValues(t *testing.T) {
 			if b := strconv.FormatBool(pv); b != vv {
 				t.Errorf("failed to match imported bool value %v with expected %v for key %q", b, vv, kk)
 			}
+		case json.Number:
+			if fv, err := pv.Float64(); err == nil {
+				if sfv := strconv.FormatFloat(fv, 'f', -1, 64); sfv != vv {
+					t.Errorf("failed to match imported float value %v with expected %v for key %q", sfv, vv, kk)
+				}
+			}
+			if iv, err := pv.Int64(); err == nil {
+				if siv := strconv.FormatInt(iv, 10); siv != vv {
+					t.Errorf("failed to match imported int value %v with expected %v for key %q", siv, vv, kk)
+				}
+			}
+			if pv.String() != vv {
+				t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk)
+			}
 		default:
 			if pv != vv {
 				t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk)
@@ -309,6 +324,10 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) {
 			if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv {
 				t.Errorf("failed to match imported float value %v with expected %v", s, vv)
 			}
+		case json.Number:
+			if pv.String() != vv {
+				t.Errorf("failed to match imported string value %q with expected %q", pv, vv)
+			}
 		default:
 			if pv != vv {
 				t.Errorf("failed to match imported string value %q with expected %q", pv, vv)