refactor(yaml): switch to goccy/go-yaml library

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero 2025-04-17 07:01:06 +08:00
parent 7624697b68
commit 2f0fd7a57c
10 changed files with 41 additions and 166 deletions

View File

@ -569,7 +569,6 @@ Helmfile uses some OS environment variables to override default behaviour:
* `HELMFILE_ENVIRONMENT` - specify [Helmfile environment](https://helmfile.readthedocs.io/en/latest/#environment), it has lower priority than CLI argument `--environment` * `HELMFILE_ENVIRONMENT` - specify [Helmfile environment](https://helmfile.readthedocs.io/en/latest/#environment), it has lower priority than CLI argument `--environment`
* `HELMFILE_TEMPDIR` - specify directory to store temporary files * `HELMFILE_TEMPDIR` - specify directory to store temporary files
* `HELMFILE_UPGRADE_NOTICE_DISABLED` - expecting any non-empty value to skip the check for the latest version of Helmfile in [helmfile version](https://helmfile.readthedocs.io/en/latest/#version) * `HELMFILE_UPGRADE_NOTICE_DISABLED` - expecting any non-empty value to skip the check for the latest version of Helmfile in [helmfile version](https://helmfile.readthedocs.io/en/latest/#version)
* `HELMFILE_GOCCY_GOYAML` - use *goccy/go-yaml* instead of *gopkg.in/yaml.v2*. It's `false` by default in Helmfile v0.x and `true` by default for Helmfile v1.x.
* `HELMFILE_CACHE_HOME` - specify directory to store cached files for remote operations * `HELMFILE_CACHE_HOME` - specify directory to store cached files for remote operations
* `HELMFILE_FILE_PATH` - specify the path to the helmfile.yaml file * `HELMFILE_FILE_PATH` - specify the path to the helmfile.yaml file
* `HELMFILE_INTERACTIVE` - enable interactive mode, expecting `true` lower case. The same as `--interactive` CLI flag * `HELMFILE_INTERACTIVE` - enable interactive mode, expecting `true` lower case. The same as `--interactive` CLI flag
@ -578,8 +577,8 @@ Helmfile uses some OS environment variables to override default behaviour:
``` ```
Declaratively deploy your Kubernetes manifests, Kustomize configs, and Charts as Helm releases in one shot Declaratively deploy your Kubernetes manifests, Kustomize configs, and Charts as Helm releases in one shot
V1 mode = false V1 mode = true
YAML library = gopkg.in/yaml.v2 YAML library = goccy/go-yaml
Usage: Usage:
helmfile [command] helmfile [command]

2
go.mod
View File

@ -28,7 +28,6 @@ require (
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/sync v0.13.0 golang.org/x/sync v0.13.0
golang.org/x/term v0.31.0 golang.org/x/term v0.31.0
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.17.3 helm.sh/helm/v3 v3.17.3
k8s.io/apimachinery v0.32.3 k8s.io/apimachinery v0.32.3
) )
@ -309,6 +308,7 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/gookit/color.v1 v1.1.6 // indirect gopkg.in/gookit/color.v1 v1.1.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.32.3 // indirect k8s.io/api v0.32.3 // indirect
k8s.io/apiextensions-apiserver v0.32.2 // indirect k8s.io/apiextensions-apiserver v0.32.2 // indirect

View File

@ -16,7 +16,6 @@ import (
"github.com/helmfile/helmfile/pkg/exectest" "github.com/helmfile/helmfile/pkg/exectest"
ffs "github.com/helmfile/helmfile/pkg/filesystem" ffs "github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec" "github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/runtime"
) )
func TestTemplate(t *testing.T) { func TestTemplate(t *testing.T) {
@ -338,20 +337,13 @@ releases:
func TestTemplate_StrictParsing(t *testing.T) { func TestTemplate_StrictParsing(t *testing.T) {
type testcase struct { type testcase struct {
goccyGoYaml bool ns string
ns string error string
error string
} }
check := func(t *testing.T, tc testcase) { check := func(t *testing.T, tc testcase) {
t.Helper() t.Helper()
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = tc.goccyGoYaml
t.Cleanup(func() {
runtime.GoccyGoYaml = v
})
var helm = &exectest.Helm{ var helm = &exectest.Helm{
FailOnUnexpectedList: true, FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true, FailOnUnexpectedDiff: true,
@ -413,7 +405,6 @@ releases:
t.Run("fail due to unknown field with goccy/go-yaml", func(t *testing.T) { t.Run("fail due to unknown field with goccy/go-yaml", func(t *testing.T) {
check(t, testcase{ check(t, testcase{
goccyGoYaml: true,
error: `in ./helmfile.yaml: failed to read helmfile.yaml: reading document at index 1. Started seeing this since Helmfile v1? Add the .gotmpl file extension: [4:3] unknown field "foobar" error: `in ./helmfile.yaml: failed to read helmfile.yaml: reading document at index 1. Started seeing this since Helmfile v1? Add the .gotmpl file extension: [4:3] unknown field "foobar"
2 | releases: 2 | releases:
3 | - name: app1 3 | - name: app1
@ -422,14 +413,6 @@ releases:
5 | chart: incubator/raw`, 5 | chart: incubator/raw`,
}) })
}) })
t.Run("fail due to unknown field with gopkg.in/yaml.v2", func(t *testing.T) {
check(t, testcase{
goccyGoYaml: false,
error: `in ./helmfile.yaml: failed to read helmfile.yaml: reading document at index 1. Started seeing this since Helmfile v1? Add the .gotmpl file extension: yaml: unmarshal errors:
line 4: field foobar not found in type state.ReleaseSpec`,
})
})
} }
func TestTemplate_CyclicInheritance(t *testing.T) { func TestTemplate_CyclicInheritance(t *testing.T) {

View File

@ -26,7 +26,6 @@ import (
ffs "github.com/helmfile/helmfile/pkg/filesystem" ffs "github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec" "github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/remote" "github.com/helmfile/helmfile/pkg/remote"
"github.com/helmfile/helmfile/pkg/runtime"
"github.com/helmfile/helmfile/pkg/state" "github.com/helmfile/helmfile/pkg/state"
"github.com/helmfile/helmfile/pkg/testhelper" "github.com/helmfile/helmfile/pkg/testhelper"
"github.com/helmfile/helmfile/pkg/testutil" "github.com/helmfile/helmfile/pkg/testutil"
@ -4020,15 +4019,9 @@ myrelease4 testNamespace true true chart:mychart1,id:myrelease1,name:myr
assert.Equal(t, expected, out) assert.Equal(t, expected, out)
} }
func testSetStringValuesTemplate(t *testing.T, goccyGoYaml bool) { func testSetStringValuesTemplate(t *testing.T) {
t.Helper() t.Helper()
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = goccyGoYaml
t.Cleanup(func() {
runtime.GoccyGoYaml = v
})
files := map[string]string{ files := map[string]string{
"/path/to/helmfile.yaml.gotmpl": ` "/path/to/helmfile.yaml.gotmpl": `
releases: releases:
@ -4088,15 +4081,9 @@ releases:
} }
} }
func testSetValuesTemplate(t *testing.T, goccyGoYaml bool) { func testSetValuesTemplate(t *testing.T) {
t.Helper() t.Helper()
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = goccyGoYaml
t.Cleanup(func() {
runtime.GoccyGoYaml = v
})
files := map[string]string{ files := map[string]string{
"/path/to/helmfile.yaml.gotmpl": ` "/path/to/helmfile.yaml.gotmpl": `
releases: releases:
@ -4162,21 +4149,13 @@ releases:
func TestSetValuesTemplate(t *testing.T) { func TestSetValuesTemplate(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) { t.Run("with goccy/go-yaml", func(t *testing.T) {
testSetValuesTemplate(t, true) testSetValuesTemplate(t)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
testSetValuesTemplate(t, false)
}) })
} }
func TestSetStringValuesTemplate(t *testing.T) { func TestSetStringValuesTemplate(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) { t.Run("with goccy/go-yaml", func(t *testing.T) {
testSetStringValuesTemplate(t, true) testSetStringValuesTemplate(t)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
testSetStringValuesTemplate(t, false)
}) })
} }

View File

@ -12,7 +12,6 @@ const (
FilePath = "HELMFILE_FILE_PATH" FilePath = "HELMFILE_FILE_PATH"
TempDir = "HELMFILE_TEMPDIR" TempDir = "HELMFILE_TEMPDIR"
UpgradeNoticeDisabled = "HELMFILE_UPGRADE_NOTICE_DISABLED" UpgradeNoticeDisabled = "HELMFILE_UPGRADE_NOTICE_DISABLED"
GoccyGoYaml = "HELMFILE_GOCCY_GOYAML"
CacheHome = "HELMFILE_CACHE_HOME" CacheHome = "HELMFILE_CACHE_HOME"
Interactive = "HELMFILE_INTERACTIVE" Interactive = "HELMFILE_INTERACTIVE"
) )

View File

@ -2,35 +2,19 @@ package runtime
import ( import (
"fmt" "fmt"
"os"
"github.com/helmfile/helmfile/pkg/envvar"
) )
var ( var (
// GoccyGoYaml is set to true in order to let Helmfile use // GoccyGoYaml is set to true in order to let Helmfile use
// goccy/go-yaml instead of gopkg.in/yaml.v2. // goccy/go-yaml instead of gopkg.in/yaml.v2.
// It's false by default in Helmfile v0.x and true by default for Helmfile v1.x.
GoccyGoYaml bool GoccyGoYaml bool
) )
func Info() string { func Info() string {
yamlLib := "gopkg.in/yaml.v2" yamlLib := "goccy/go-yaml"
if GoccyGoYaml {
yamlLib = "goccy/go-yaml"
}
return fmt.Sprintf("YAML library = %v", yamlLib) return fmt.Sprintf("YAML library = %v", yamlLib)
} }
func init() { func init() {
// You can switch the YAML library at runtime via an envvar: GoccyGoYaml = true
switch os.Getenv(envvar.GoccyGoYaml) {
case "true":
GoccyGoYaml = true
case "false":
GoccyGoYaml = false
default:
GoccyGoYaml = true
}
} }

View File

@ -200,15 +200,9 @@ func TestToYaml(t *testing.T) {
require.Equal(t, expected, actual) require.Equal(t, expected, actual)
} }
func testFromYaml(t *testing.T, goccyGoYaml bool, expected Values) { func testFromYaml(t *testing.T, expected Values) {
t.Helper() t.Helper()
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = goccyGoYaml
t.Cleanup(func() {
runtime.GoccyGoYaml = v
})
raw := `foo: raw := `foo:
bar: BAR bar: BAR
` `
@ -221,20 +215,6 @@ func TestFromYaml(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) { t.Run("with goccy/go-yaml", func(t *testing.T) {
testFromYaml( testFromYaml(
t, t,
true,
// nolint: unconvert
Values(map[string]any{
"foo": map[string]any{
"bar": "BAR",
},
}),
)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
testFromYaml(
t,
false,
// nolint: unconvert // nolint: unconvert
Values(map[string]any{ Values(map[string]any{
"foo": map[string]any{ "foo": map[string]any{

View File

@ -5,9 +5,6 @@ import (
"io" "io"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
v2 "gopkg.in/yaml.v2"
"github.com/helmfile/helmfile/pkg/runtime"
) )
type Encoder interface { type Encoder interface {
@ -17,19 +14,11 @@ type Encoder interface {
// NewEncoder creates and returns a function that is used to encode a Go object to a YAML document // NewEncoder creates and returns a function that is used to encode a Go object to a YAML document
func NewEncoder(w io.Writer) Encoder { func NewEncoder(w io.Writer) Encoder {
if runtime.GoccyGoYaml { return yaml.NewEncoder(w)
return yaml.NewEncoder(w)
}
return v2.NewEncoder(w)
} }
func Unmarshal(data []byte, v any) error { func Unmarshal(data []byte, v any) error {
if runtime.GoccyGoYaml { return yaml.Unmarshal(data, v)
return yaml.Unmarshal(data, v)
}
return v2.Unmarshal(data, v)
} }
// NewDecoder creates and returns a function that is used to decode a YAML document // NewDecoder creates and returns a function that is used to decode a YAML document
@ -37,26 +26,17 @@ func Unmarshal(data []byte, v any) error {
// When strict is true, this function ensures that every field found in the YAML document // When strict is true, this function ensures that every field found in the YAML document
// to have the corresponding field in the decoded Go struct. // to have the corresponding field in the decoded Go struct.
func NewDecoder(data []byte, strict bool) func(any) error { func NewDecoder(data []byte, strict bool) func(any) error {
if runtime.GoccyGoYaml { var opts []yaml.DecodeOption
var opts []yaml.DecodeOption if strict {
if strict { opts = append(opts, yaml.DisallowUnknownField())
opts = append(opts, yaml.DisallowUnknownField())
}
// allow duplicate keys
opts = append(opts, yaml.AllowDuplicateMapKey())
decoder := yaml.NewDecoder(
bytes.NewReader(data),
opts...,
)
return func(v any) error {
return decoder.Decode(v)
}
} }
// allow duplicate keys
opts = append(opts, yaml.AllowDuplicateMapKey())
decoder := v2.NewDecoder(bytes.NewReader(data)) decoder := yaml.NewDecoder(
decoder.SetStrict(strict) bytes.NewReader(data),
opts...,
)
return func(v any) error { return func(v any) error {
return decoder.Decode(v) return decoder.Decode(v)
@ -64,20 +44,16 @@ func NewDecoder(data []byte, strict bool) func(any) error {
} }
func Marshal(v any) ([]byte, error) { func Marshal(v any) ([]byte, error) {
if runtime.GoccyGoYaml { var b bytes.Buffer
var b bytes.Buffer yamlEncoder := yaml.NewEncoder(
yamlEncoder := yaml.NewEncoder( &b,
&b, yaml.Indent(2),
yaml.Indent(2), yaml.UseSingleQuote(true),
yaml.UseSingleQuote(true), yaml.UseLiteralStyleIfMultiline(true),
yaml.UseLiteralStyleIfMultiline(true), )
) err := yamlEncoder.Encode(v)
err := yamlEncoder.Encode(v) defer func() {
defer func() { _ = yamlEncoder.Close()
_ = yamlEncoder.Close() }()
}() return b.Bytes(), err
return b.Bytes(), err
}
return v2.Marshal(v)
} }

View File

@ -4,25 +4,12 @@ import (
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/runtime"
) )
func testYamlMarshal(t *testing.T, goccyGoYaml bool) { func testYamlMarshal(t *testing.T) {
t.Helper() t.Helper()
var yamlLibraryName string yamlLibraryName := "goccy/go-yaml"
if goccyGoYaml {
yamlLibraryName = "goccy/go-yaml"
} else {
yamlLibraryName = "gopkg.in/yaml.v2"
}
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = goccyGoYaml
t.Cleanup(func() {
runtime.GoccyGoYaml = v
})
tests := []struct { tests := []struct {
Name string `yaml:"name"` Name string `yaml:"name"`
@ -49,8 +36,7 @@ func testYamlMarshal(t *testing.T, goccyGoYaml bool) {
Annotation: "on", Annotation: "on",
}}, }},
expected: map[string]string{ expected: map[string]string{
"goccy/go-yaml": "name: John\ninfo:\n- age: 20\n address: New York\n annotation: 'on'\n", "goccy/go-yaml": "name: John\ninfo:\n- age: 20\n address: New York\n annotation: 'on'\n",
"gopkg.in/yaml.v2": "name: John\ninfo:\n- age: 20\n address: New York\n annotation: \"on\"\n",
}, },
}, },
} }
@ -64,10 +50,6 @@ func testYamlMarshal(t *testing.T, goccyGoYaml bool) {
func TestYamlMarshal(t *testing.T) { func TestYamlMarshal(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) { t.Run("with goccy/go-yaml", func(t *testing.T) {
testYamlMarshal(t, true) testYamlMarshal(t)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
testYamlMarshal(t, false)
}) })
} }

View File

@ -9,7 +9,6 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
goruntime "runtime" goruntime "runtime"
"strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -59,17 +58,11 @@ func (f fakeInit) Force() bool {
func TestHelmfileTemplateWithBuildCommand(t *testing.T) { func TestHelmfileTemplateWithBuildCommand(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) { t.Run("with goccy/go-yaml", func(t *testing.T) {
testHelmfileTemplateWithBuildCommand(t, true) testHelmfileTemplateWithBuildCommand(t)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
testHelmfileTemplateWithBuildCommand(t, false)
}) })
} }
func testHelmfileTemplateWithBuildCommand(t *testing.T, goccyGoYaml bool) { func testHelmfileTemplateWithBuildCommand(t *testing.T) {
t.Setenv(envvar.GoccyGoYaml, strconv.FormatBool(goccyGoYaml))
localChartPortSets := make(map[int]struct{}) localChartPortSets := make(map[int]struct{})
logger := helmexec.NewLogger(os.Stderr, "info") logger := helmexec.NewLogger(os.Stderr, "info")