diff --git a/butane/config/openshift/v4_18/translate_test.go b/butane/config/openshift/v4_18/translate_test.go index 8295ea2d2..5b7dbe254 100644 --- a/butane/config/openshift/v4_18/translate_test.go +++ b/butane/config/openshift/v4_18/translate_test.go @@ -513,3 +513,39 @@ func TestValidateSupport(t *testing.T) { }) } } + +// TestMachineConfigYAMLLargeSizeMiB is the OCPBUGS-114878 / #2309 regression: +// MachineConfig YAML must encode sizeMiB >= 1e6 as a plain integer. +func TestMachineConfigYAMLLargeSizeMiB(t *testing.T) { + in := `variant: openshift +version: 4.18.0 +metadata: + name: 98-master-partition + labels: + machineconfiguration.openshift.io/role: master +storage: + disks: + - device: /dev/disk/by-id/virtio-targetdisk + partitions: + - number: 4 + label: root + size_mib: 8389000 + resize: true + - number: 5 + label: var + size_mib: 0 + - number: 6 + label: odf-1 + size_mib: 1231872 +` + out, r, err := ToConfigBytes([]byte(in), common.TranslateBytesOptions{}) + assert.NoError(t, err) + assert.False(t, r.IsFatal()) + got := string(out) + assert.Contains(t, got, "sizeMiB: 8389000") + assert.Contains(t, got, "sizeMiB: 1231872") + assert.Contains(t, got, "sizeMiB: 0") + assert.NotContains(t, got, "8.389e+06") + assert.NotContains(t, got, "1.231872e+06") + assert.NotContains(t, got, "e+") +} diff --git a/butane/config/util/util.go b/butane/config/util/util.go index fc6de99db..019dab082 100644 --- a/butane/config/util/util.go +++ b/butane/config/util/util.go @@ -155,8 +155,8 @@ func TranslateBytesYAML(input []byte, container interface{}, translateMethod str return jsonCfg, r, err } - var ifaceCfg interface{} - if err := json.Unmarshal(jsonCfg, &ifaceCfg); err != nil { + ifaceCfg, err := unmarshalJSONForYAML(jsonCfg) + if err != nil { return []byte{}, r, err } @@ -174,6 +174,49 @@ func TranslateBytesYAML(input []byte, container interface{}, translateMethod str return yamlCfg, r, err } +// unmarshalJSONForYAML decodes JSON into a generic structure suitable for +// YAML encoding. Integers are preserved as int64 so yaml.v3 does not emit +// scientific notation for values >= 1e6 (e.g. sizeMiB: 8.389e+06). +func unmarshalJSONForYAML(jsonCfg []byte) (interface{}, error) { + dec := json.NewDecoder(bytes.NewReader(jsonCfg)) + dec.UseNumber() + var ifaceCfg interface{} + if err := dec.Decode(&ifaceCfg); err != nil { + return nil, err + } + return convertJSONNumbers(ifaceCfg), nil +} + +// convertJSONNumbers walks a decoded JSON tree and replaces json.Number +// values with int64 when possible, otherwise float64. json.Unmarshal into +// interface{} otherwise uses float64, and yaml.v3 then formats values >= 1e6 +// with strconv.FormatFloat 'g' as scientific notation. +func convertJSONNumbers(v interface{}) interface{} { + switch x := v.(type) { + case map[string]interface{}: + for k, val := range x { + x[k] = convertJSONNumbers(val) + } + return x + case []interface{}: + for i, val := range x { + x[i] = convertJSONNumbers(val) + } + return x + case json.Number: + if i, err := x.Int64(); err == nil { + return i + } + f, err := x.Float64() + if err != nil { + return x.String() + } + return f + default: + return v + } +} + // Report an ErrFieldElided warning for any non-zero top-level fields in the // specified output struct. The caller will probably want to use // translate.PrefixReport() to reparent the report into the right place in diff --git a/butane/config/util/util_test.go b/butane/config/util/util_test.go index fc744d92c..9d138e540 100644 --- a/butane/config/util/util_test.go +++ b/butane/config/util/util_test.go @@ -15,7 +15,9 @@ package util import ( + "bytes" "fmt" + "strings" "testing" "github.com/coreos/ignition/v2/butane/config/common" @@ -24,6 +26,7 @@ import ( "github.com/coreos/vcontext/path" "github.com/coreos/vcontext/report" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestSnake(t *testing.T) { @@ -119,3 +122,22 @@ func TestTranslateReportPaths(t *testing.T) { assert.Equal(t, makeReport(false), r, "TranslateReportPaths changed original report") assert.Equal(t, makeReport(true), r2, "TranslateReportPaths returned incorrect report") } + +func TestUnmarshalJSONForYAMLIntegers(t *testing.T) { + jsonCfg := []byte(`{"sizeMiB":8389000,"startMiB":2048,"values":[1000000],"ratio":1.5}`) + v, err := unmarshalJSONForYAML(jsonCfg) + assert.NoError(t, err) + + var buf bytes.Buffer + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + assert.NoError(t, enc.Encode(v)) + assert.NoError(t, enc.Close()) + out := buf.String() + + assert.Contains(t, out, "sizeMiB: 8389000") + assert.Contains(t, out, "startMiB: 2048") + assert.Contains(t, out, "- 1000000") + assert.Contains(t, out, "ratio: 1.5") + assert.False(t, strings.Contains(out, "e+"), "YAML must not use scientific notation: %s", out) +} diff --git a/docs/release-notes.md b/docs/release-notes.md index e4015a63f..ed02b743f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,10 @@ nav_order: 9 ### Bug fixes +- Encode large integer fields such as `sizeMiB` as decimal numbers in + MachineConfig YAML, instead of scientific notation + ([#2309](https://github.com/coreos/ignition/issues/2309)) + ([OCPBUGS-114878](https://redhat.atlassian.net/browse/OCPBUGS-114878)) ## Ignition 2.27.0 (2026-08-26)