refactor(yaml): upgrade from gopkg.in/yaml.v2 to v3 (#2039)

* refactor(yaml): upgrade from gopkg.in/yaml.v2 to v3

Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor(yaml): enhance yaml encoding with consistent formatting and quotes

Signed-off-by: yxxhero <aiopsclub@163.com>

* optimize code

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix tests

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix more issues

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix tests

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero 2025-05-15 22:21:37 +08:00 committed by GitHub
parent 867bef0f03
commit b52ca9ae04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 140 additions and 147 deletions

View File

@ -570,18 +570,17 @@ 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_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_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_GO_YAML_V3` - use *gopkg.in/yaml.v3* instead of *gopkg.in/yaml.v2*. It's `false` by default in Helmfile v0.x, and `true` in Helmfile v1.x.
* `HELMFILE_CACHE_HOME` - specify directory to store cached files for remote operations
* `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_ENABLE_GOCCY_GOYAML_JSON_STYLE`: - enable JSON style for *goccy/go-yaml* instead of *gopkg.in/yaml.v2*. It's `false` by default in Helmfile. it will add quotes to string values.
## CLI Reference
```
Declaratively deploy your Kubernetes manifests, Kustomize configs, and Charts as Helm releases in one shot
V1 mode = false
YAML library = gopkg.in/yaml.v2
YAML library = gopkg.in/yaml.v3
Usage:
helmfile [command]

4
go.mod
View File

@ -8,7 +8,6 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/go-test/deep v1.1.1
github.com/goccy/go-yaml v1.17.1
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.7.0
github.com/gosuri/uitable v0.0.4
@ -29,6 +28,7 @@ require (
golang.org/x/sync v0.14.0
golang.org/x/term v0.32.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.17.3
k8s.io/apimachinery v0.33.0
)
@ -217,6 +217,7 @@ require (
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-yaml v1.17.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
@ -307,7 +308,6 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/gookit/color.v1 v1.1.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.33.0 // indirect
k8s.io/apiextensions-apiserver v0.32.2 // indirect
k8s.io/cli-runtime v0.32.2 // indirect

View File

@ -338,18 +338,18 @@ releases:
func TestTemplate_StrictParsing(t *testing.T) {
type testcase struct {
goccyGoYaml bool
ns string
error string
GoYamlV3 bool
ns string
error string
}
check := func(t *testing.T, tc testcase) {
t.Helper()
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = tc.goccyGoYaml
v := runtime.GoYamlV3
runtime.GoYamlV3 = tc.GoYamlV3
t.Cleanup(func() {
runtime.GoccyGoYaml = v
runtime.GoYamlV3 = v
})
var helm = &exectest.Helm{
@ -411,21 +411,17 @@ releases:
})
}
t.Run("fail due to unknown field with goccy/go-yaml", func(t *testing.T) {
t.Run("fail due to unknown field with gopkg.in/yaml.v3", func(t *testing.T) {
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"
2 | releases:
3 | - name: app1
> 4 | foobar: FOOBAR
^
5 | chart: incubator/raw`,
GoYamlV3: 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: yaml: unmarshal errors:
line 4: field foobar not found in type state.ReleaseSpec`,
})
})
t.Run("fail due to unknown field with gopkg.in/yaml.v2", func(t *testing.T) {
check(t, testcase{
goccyGoYaml: false,
GoYamlV3: 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`,
})

View File

@ -4020,13 +4020,13 @@ myrelease4 testNamespace true true chart:mychart1,id:myrelease1,name:myr
assert.Equal(t, expected, out)
}
func testSetStringValuesTemplate(t *testing.T, goccyGoYaml bool) {
func testSetStringValuesTemplate(t *testing.T, GoYamlV3 bool) {
t.Helper()
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = goccyGoYaml
v := runtime.GoYamlV3
runtime.GoYamlV3 = GoYamlV3
t.Cleanup(func() {
runtime.GoccyGoYaml = v
runtime.GoYamlV3 = v
})
files := map[string]string{
@ -4088,13 +4088,13 @@ releases:
}
}
func testSetValuesTemplate(t *testing.T, goccyGoYaml bool) {
func testSetValuesTemplate(t *testing.T, GoYamlV3 bool) {
t.Helper()
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = goccyGoYaml
v := runtime.GoYamlV3
runtime.GoYamlV3 = GoYamlV3
t.Cleanup(func() {
runtime.GoccyGoYaml = v
runtime.GoYamlV3 = v
})
files := map[string]string{
@ -4161,7 +4161,7 @@ releases:
}
func TestSetValuesTemplate(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) {
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
testSetValuesTemplate(t, true)
})
@ -4171,7 +4171,7 @@ func TestSetValuesTemplate(t *testing.T) {
}
func TestSetStringValuesTemplate(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) {
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
testSetStringValuesTemplate(t, true)
})

View File

@ -6,14 +6,13 @@ const (
// use helm status to check if a release exists before installing it
UseHelmStatusToCheckReleaseExistence = "HELMFILE_USE_HELM_STATUS_TO_CHECK_RELEASE_EXISTENCE"
DisableRunnerUniqueID = "HELMFILE_DISABLE_RUNNER_UNIQUE_ID"
Experimental = "HELMFILE_EXPERIMENTAL" // environment variable for experimental features, expecting "true" lower case
Environment = "HELMFILE_ENVIRONMENT"
FilePath = "HELMFILE_FILE_PATH"
TempDir = "HELMFILE_TEMPDIR"
UpgradeNoticeDisabled = "HELMFILE_UPGRADE_NOTICE_DISABLED"
GoccyGoYaml = "HELMFILE_GOCCY_GOYAML"
CacheHome = "HELMFILE_CACHE_HOME"
Interactive = "HELMFILE_INTERACTIVE"
EnableGoccyGoYamlJSONStyle = "HELMFILE_ENABLE_GOCCY_GOYAML_JSON_STYLE"
DisableRunnerUniqueID = "HELMFILE_DISABLE_RUNNER_UNIQUE_ID"
Experimental = "HELMFILE_EXPERIMENTAL" // environment variable for experimental features, expecting "true" lower case
Environment = "HELMFILE_ENVIRONMENT"
FilePath = "HELMFILE_FILE_PATH"
TempDir = "HELMFILE_TEMPDIR"
UpgradeNoticeDisabled = "HELMFILE_UPGRADE_NOTICE_DISABLED"
GoYamlV3 = "HELMFILE_GO_YAML_V3"
CacheHome = "HELMFILE_CACHE_HOME"
Interactive = "HELMFILE_INTERACTIVE"
)

View File

@ -8,16 +8,16 @@ import (
)
var (
// GoccyGoYaml is set to true in order to let Helmfile 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.
GoccyGoYaml bool
// GoYamlV3 is set to true in order to let Helmfile use
// gopkg.in/yaml.v3 instead of gopkg.in/yaml.v2.
// It's false by default in Helmfile v0.x and true in Helmfile v1.x.
GoYamlV3 bool
)
func Info() string {
yamlLib := "gopkg.in/yaml.v2"
if GoccyGoYaml {
yamlLib = "goccy/go-yaml"
if GoYamlV3 {
yamlLib = "gopkg.in/yaml.v3"
}
return fmt.Sprintf("YAML library = %v", yamlLib)
@ -25,12 +25,12 @@ func Info() string {
func init() {
// You can switch the YAML library at runtime via an envvar:
switch os.Getenv(envvar.GoccyGoYaml) {
switch os.Getenv(envvar.GoYamlV3) {
case "true":
GoccyGoYaml = true
GoYamlV3 = true
case "false":
GoccyGoYaml = false
GoYamlV3 = false
default:
GoccyGoYaml = true
GoYamlV3 = true
}
}

View File

@ -22,10 +22,10 @@ func TestExecuteTemplateExpressions(t *testing.T) {
},
})
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = true
v := runtime.GoYamlV3
runtime.GoYamlV3 = true
t.Cleanup(func() {
runtime.GoccyGoYaml = v
runtime.GoYamlV3 = v
})
rs := ReleaseSpec{

View File

@ -4,14 +4,12 @@ import (
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
goruntime "runtime"
"testing"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/envvar"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/runtime"
)
@ -162,9 +160,9 @@ func TestReadFile_PassAbsPath(t *testing.T) {
}
func TestToYaml_NestedMapInterfaceKey(t *testing.T) {
v := runtime.GoccyGoYaml
v := runtime.GoYamlV3
t.Cleanup(func() {
runtime.GoccyGoYaml = v
runtime.GoYamlV3 = v
})
// nolint: unconvert
@ -174,13 +172,13 @@ func TestToYaml_NestedMapInterfaceKey(t *testing.T) {
},
})
runtime.GoccyGoYaml = true
runtime.GoYamlV3 = true
actual, err := ToYaml(vals)
require.Equal(t, "foo:\n bar: BAR\n", actual)
require.NoError(t, err, "expected nil, but got: %v, when type: map[interface {}]interface {}", err)
runtime.GoccyGoYaml = false
runtime.GoYamlV3 = false
actual, err = ToYaml(vals)
require.Equal(t, "foo:\n bar: BAR\n", actual)
@ -189,18 +187,16 @@ func TestToYaml_NestedMapInterfaceKey(t *testing.T) {
func TestToYaml(t *testing.T) {
tests := []struct {
name string
input any
expected string
wantErr bool
enableJsonStyle bool
name string
input any
expected string
wantErr bool
}{
{
// https://github.com/helmfile/helmfile/issues/2024
name: "test unmarshalling issue 2024",
enableJsonStyle: true,
input: map[string]any{"thisShouldBeString": "01234567890123456789"},
expected: `'thisShouldBeString': '01234567890123456789'
name: "test unmarshalling issue 2024",
input: map[string]any{"thisShouldBeString": "01234567890123456789"},
expected: `thisShouldBeString: "01234567890123456789"
`,
},
{
@ -247,12 +243,6 @@ func TestToYaml(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.enableJsonStyle {
_ = os.Setenv(envvar.EnableGoccyGoYamlJSONStyle, "true")
}
defer func() {
_ = os.Unsetenv(envvar.EnableGoccyGoYamlJSONStyle)
}()
actual, err := ToYaml(tt.input)
if tt.wantErr {
require.Error(t, err)
@ -345,13 +335,13 @@ func testFromYamlNull(t *testing.T) {
require.Equal(t, nil, actual)
}
func testFromYaml(t *testing.T, goccyGoYaml bool) {
func testFromYaml(t *testing.T, GoYamlV3 bool) {
t.Helper()
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = goccyGoYaml
v := runtime.GoYamlV3
runtime.GoYamlV3 = GoYamlV3
t.Cleanup(func() {
runtime.GoccyGoYaml = v
runtime.GoYamlV3 = v
})
t.Run("test unmarshalling object", testFromYamlObject)
@ -368,11 +358,11 @@ func testFromYaml(t *testing.T, goccyGoYaml bool) {
}
func TestFromYaml(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) {
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
testFromYaml(t, true)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
testFromYaml(t, false)
})
}

View File

@ -3,12 +3,10 @@ package yaml
import (
"bytes"
"io"
"os"
"github.com/goccy/go-yaml"
v2 "gopkg.in/yaml.v2"
v3 "gopkg.in/yaml.v3"
"github.com/helmfile/helmfile/pkg/envvar"
"github.com/helmfile/helmfile/pkg/runtime"
)
@ -19,24 +17,22 @@ type Encoder interface {
// NewEncoder creates and returns a function that is used to encode a Go object to a YAML document
func NewEncoder(w io.Writer) Encoder {
if runtime.GoccyGoYaml {
yamlEncoderOpts := []yaml.EncodeOption{}
// enable JSON style if the envvar is set
if os.Getenv(envvar.EnableGoccyGoYamlJSONStyle) == "true" {
yamlEncoderOpts = append(yamlEncoderOpts, yaml.JSON(), yaml.Flow(false))
}
return yaml.NewEncoder(w, yamlEncoderOpts...)
if runtime.GoYamlV3 {
v3Encoder := v3.NewEncoder(w)
v3Encoder.SetIndent(2)
return v3Encoder
}
return v2.NewEncoder(w)
}
func Unmarshal(data []byte, v any) error {
if runtime.GoccyGoYaml {
return yaml.Unmarshal(data, v)
}
return v2.Unmarshal(data, v)
func Marshal(v any) ([]byte, error) {
var b bytes.Buffer
yamlEncoder := NewEncoder(&b)
err := yamlEncoder.Encode(v)
defer func() {
_ = yamlEncoder.Close()
}()
return b.Bytes(), err
}
// NewDecoder creates and returns a function that is used to decode a YAML document
@ -44,19 +40,9 @@ func Unmarshal(data []byte, v any) error {
// 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.
func NewDecoder(data []byte, strict bool) func(any) error {
if runtime.GoccyGoYaml {
var opts []yaml.DecodeOption
if strict {
opts = append(opts, yaml.DisallowUnknownField())
}
// allow duplicate keys
opts = append(opts, yaml.AllowDuplicateMapKey())
decoder := yaml.NewDecoder(
bytes.NewReader(data),
opts...,
)
if runtime.GoYamlV3 {
decoder := v3.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(strict)
return func(v any) error {
return decoder.Decode(v)
}
@ -70,29 +56,10 @@ func NewDecoder(data []byte, strict bool) func(any) error {
}
}
func Marshal(v any) ([]byte, error) {
if runtime.GoccyGoYaml {
var b bytes.Buffer
yamlEncoderOpts := []yaml.EncodeOption{
yaml.Indent(2),
yaml.UseSingleQuote(true),
yaml.UseLiteralStyleIfMultiline(true),
}
// enable JSON style if the envvar is set
if os.Getenv(envvar.EnableGoccyGoYamlJSONStyle) == "true" {
yamlEncoderOpts = append(yamlEncoderOpts, yaml.JSON(), yaml.Flow(false))
}
yamlEncoder := yaml.NewEncoder(
&b,
yamlEncoderOpts...,
)
err := yamlEncoder.Encode(v)
defer func() {
_ = yamlEncoder.Close()
}()
return b.Bytes(), err
func Unmarshal(data []byte, v any) error {
if runtime.GoYamlV3 {
return v3.Unmarshal(data, v)
}
return v2.Marshal(v)
return v2.Unmarshal(data, v)
}

View File

@ -8,20 +8,20 @@ import (
"github.com/helmfile/helmfile/pkg/runtime"
)
func testYamlMarshal(t *testing.T, goccyGoYaml bool) {
func testYamlMarshal(t *testing.T, GoYamlV3 bool) {
t.Helper()
var yamlLibraryName string
if goccyGoYaml {
yamlLibraryName = "goccy/go-yaml"
if GoYamlV3 {
yamlLibraryName = "gopkg.in/yaml.v3"
} else {
yamlLibraryName = "gopkg.in/yaml.v2"
}
v := runtime.GoccyGoYaml
runtime.GoccyGoYaml = goccyGoYaml
v := runtime.GoYamlV3
runtime.GoYamlV3 = GoYamlV3
t.Cleanup(func() {
runtime.GoccyGoYaml = v
runtime.GoYamlV3 = v
})
tests := []struct {
@ -49,8 +49,8 @@ func testYamlMarshal(t *testing.T, goccyGoYaml bool) {
Annotation: "on",
}},
expected: map[string]string{
"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",
"gopkg.in/yaml.v3": "name: John\ninfo:\n - age: 20\n address: New York\n annotation: \"on\"\n",
},
},
}
@ -63,11 +63,11 @@ func testYamlMarshal(t *testing.T, goccyGoYaml bool) {
}
func TestYamlMarshal(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) {
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
testYamlMarshal(t, true)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
testYamlMarshal(t, false)
})
}

View File

@ -58,17 +58,17 @@ func (f fakeInit) Force() bool {
}
func TestHelmfileTemplateWithBuildCommand(t *testing.T) {
t.Run("with goccy/go-yaml", func(t *testing.T) {
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
testHelmfileTemplateWithBuildCommand(t, true)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
testHelmfileTemplateWithBuildCommand(t, false)
})
}
func testHelmfileTemplateWithBuildCommand(t *testing.T, goccyGoYaml bool) {
t.Setenv(envvar.GoccyGoYaml, strconv.FormatBool(goccyGoYaml))
func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
t.Setenv(envvar.GoYamlV3, strconv.FormatBool(GoYamlV3))
localChartPortSets := make(map[int]struct{})
@ -224,7 +224,12 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, goccyGoYaml bool) {
t.Logf("Using HELM_CACHE_HOME=%s, HELMFILE_CACHE_HOME=%s, HELM_CONFIG_HOME=%s", helmCacheHome, helmfileCacheHome, helmConfigHome)
inputFile := filepath.Join(testdataDir, name, "input.yaml.gotmpl")
outputFile := filepath.Join(testdataDir, name, "output.yaml")
outputFile := ""
if GoYamlV3 {
outputFile = filepath.Join(testdataDir, name, "gopkg.in-yaml.v3-output.yaml")
} else {
outputFile = filepath.Join(testdataDir, name, "gopkg.in-yaml.v2-output.yaml")
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

View File

@ -0,0 +1,37 @@
---
# Source: __workingdir__/testdata/snapshot/issue_2098_release_template_needs/input.yaml.gotmpl
filepath: input.yaml.gotmpl
helmBinary: helm
kustomizeBinary: kustomize
environments:
default: {}
repositories:
- name: aservo
url: https://aservo.github.io/charts
releases:
- chart: aservo/util
version: 0.0.1
name: default-shared-resources
namespace: default
labels:
chart: util
name: default-shared-resources
namespace: default
service: shared-resources
- chart: aservo/util
version: 0.0.1
needs:
- default/default-shared-resources
name: default-release-resources
namespace: default
labels:
chart: util
name: default-release-resources
namespace: default
service: release-resources
templates:
defaults:
name: default-{{ .Release.Labels.service }}
namespace: default
renderedvalues: {}