Repository: helm/helm (go)

Issue:
 toToml filter converts int to float
Given the following data structure in values.yaml:
```
config:
  bar: 9
```

And this in my configmap template:

```
data:
  telegraf-local.conf: |-
{{ .Values.config | toToml | indent 4 }}
```

I get this:
```
data:
  telegraf-local.conf: |-
    foo = 9.0
```
Even casting the variable via bar: `!!int 9` doesn't change the behavior, can't reproduce using the toJson filter either.

I'm running Helm version.BuildInfo{Version:"v3.14.4", GitCommit:"81c902a123462fd4052bc5e9aa9c513c4c8fc142", GitTreeState:"clean", GoVersion:"go1.21.9"}


Pull request description:
fix: (toToml) renders int as float
This commit fixes the issue where the `yaml.Unmarshaller` converts all int values into float64; this passes an option to the decoder, enabling int conversion into.

closes #12987 #6539 #2474 

Signed-off-by: Althaf M <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?

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 67f09c2bb..7645ba96c 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 d41df6e7f..706c44d48 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..28e24ce63 100644
--- a/cmd/helm/template_test.go
+++ b/cmd/helm/template_test.go
@@ -22,6 +22,18 @@ import (
 	"testing"
 )
 
+func TestTemplateCmdWithToml(t *testing.T) {
+
+	tests := []cmdTestCase{
+		{
+			name:   "check toToml function rendering",
+			cmd:    fmt.Sprintf("template '%s'", "testdata/testcharts/issue-totoml"),
+			golden: "output/issue-totoml.txt",
+		},
+	}
+	runTestCmd(t, tests)
+}
+
 var chartPath = "testdata/testcharts/subchart"
 
 func TestTemplateCmd(t *testing.T) {
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..f4be7a213
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/issue-totoml/Chart.yaml
@@ -0,0 +1,3 @@
+apiVersion: v2
+name: issue-totoml
+version: 0.1.0
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..621e70d48
--- /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 }}
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 af34a3f6b..ac8e4d76e 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)