Puneet Dixit 2 weeks ago committed by GitHub
commit 09eeea2a6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -19,6 +19,9 @@ package engine
import (
"bytes"
"encoding/json"
"fmt"
"math"
"reflect"
"strings"
"text/template"
@ -28,6 +31,8 @@ import (
goYaml "sigs.k8s.io/yaml/goyaml.v3"
)
const maxSafeYAMLInteger = (1 << 53) - 1
// funcMap returns a mapping of all of the functions that Engine has.
//
// Because some functions are late-bound (e.g. contain context-sensitive
@ -95,15 +100,69 @@ func toYAMLPretty(v interface{}) string {
var data bytes.Buffer
encoder := goYaml.NewEncoder(&data)
encoder.SetIndent(2)
err := encoder.Encode(v)
closeEncoder := func() error {
if encoder == nil {
return nil
}
err := encoder.Close()
encoder = nil
return err
}
defer func() {
_ = closeEncoder()
}()
if err != nil {
if err := encoder.Encode(normalizeYAMLScalars(v)); err != nil {
// Swallow errors inside of a template.
return ""
}
if err := closeEncoder(); err != nil {
// Swallow errors inside of a template.
return ""
}
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[normalizeYAMLMapKey(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) && 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 fmt.Sprint(normalized)
}
// fromYAML converts a YAML document into a map[string]interface{}.
//
// This is not a general-purpose YAML parser, and will not parse all valid

@ -17,6 +17,7 @@ limitations under the License.
package engine
import (
"math"
"strings"
"testing"
"text/template"
@ -37,6 +38,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",
@ -137,6 +142,79 @@ keyInElement1 = "valueInElement1"`,
}
}
func TestNormalizeYAMLScalars(t *testing.T) {
firstUnsafeInteger := float64(maxSafeYAMLInteger + 1)
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: "max safe integer float becomes integer",
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},
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{
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))
})
}
}
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

Loading…
Cancel
Save