Introduce new `yaml` package with `Encode` func

Comparison versus `sigs.k8s.io/yaml#Marshal`:

```
BenchmarkEncode/EncodeWithSort-12         	    475	  2419063 ns/op	2235305 B/op	   5398 allocs/op
BenchmarkEncode/EncodeWithSort-12         	    498	  2406794 ns/op	2235300 B/op	   5398 allocs/op
BenchmarkEncode/EncodeWithSort-12         	    492	  2376460 ns/op	2235312 B/op	   5398 allocs/op
BenchmarkEncode/EncodeWithSort-12         	    496	  2406756 ns/op	2235323 B/op	   5398 allocs/op
BenchmarkEncode/EncodeWithSort-12         	    488	  2402969 ns/op	2235336 B/op	   5398 allocs/op
BenchmarkEncode/SigYAMLMarshal-12         	    202	  5791549 ns/op	3124841 B/op	  19324 allocs/op
BenchmarkEncode/SigYAMLMarshal-12         	    205	  5780248 ns/op	3123193 B/op	  19320 allocs/op
BenchmarkEncode/SigYAMLMarshal-12         	    207	  5762621 ns/op	3124537 B/op	  19324 allocs/op
BenchmarkEncode/SigYAMLMarshal-12         	    214	  5748899 ns/op	3121183 B/op	  19324 allocs/op
BenchmarkEncode/SigYAMLMarshal-12         	    211	  5682105 ns/op	3120592 B/op	  19325 allocs/op
```

Signed-off-by: Hidde Beydals <hidde@hhh.computer>
This commit is contained in:
Hidde Beydals 2023-07-17 22:57:31 +02:00
parent bb4e9b7cee
commit eee91b06fa
No known key found for this signature in database
GPG Key ID: 979F380FC2341744
12 changed files with 4637 additions and 221 deletions

View File

@ -8,6 +8,7 @@ require (
k8s.io/apiextensions-apiserver v0.27.4
k8s.io/apimachinery v0.27.4
sigs.k8s.io/controller-runtime v0.15.1
sigs.k8s.io/yaml v1.3.0
)
require (

View File

@ -103,3 +103,4 @@ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h6
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@ -17,7 +17,6 @@ limitations under the License.
package v2beta2
import (
"encoding/json"
"fmt"
"strings"
"time"
@ -25,6 +24,7 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/apis/kustomize"
"github.com/fluxcd/pkg/apis/meta"
@ -1118,7 +1118,7 @@ func (in HelmRelease) GetRequeueAfter() time.Duration {
func (in HelmRelease) GetValues() map[string]interface{} {
var values map[string]interface{}
if in.Spec.Values != nil {
_ = json.Unmarshal(in.Spec.Values.Raw, &values)
_ = yaml.Unmarshal(in.Spec.Values.Raw, &values)
}
return values
}

View File

@ -19,14 +19,18 @@ package chartutil
import (
"github.com/opencontainers/go-digest"
"helm.sh/helm/v3/pkg/chartutil"
intyaml "github.com/fluxcd/helm-controller/internal/yaml"
)
// DigestValues calculates the digest of the values using the provided algorithm.
// The caller is responsible for ensuring that the algorithm is supported.
func DigestValues(algo digest.Algorithm, values chartutil.Values) digest.Digest {
digester := algo.Digester()
if err := values.Encode(digester.Hash()); err != nil {
return ""
if values = valuesOrNil(values); values != nil {
if err := intyaml.Encode(digester.Hash(), values, intyaml.SortMapSlice); err != nil {
return ""
}
}
return digester.Digest()
}
@ -36,9 +40,22 @@ func VerifyValues(digest digest.Digest, values chartutil.Values) bool {
if digest.Validate() != nil {
return false
}
verifier := digest.Verifier()
if err := values.Encode(verifier); err != nil {
return false
if values = valuesOrNil(values); values != nil {
if err := intyaml.Encode(verifier, values, intyaml.SortMapSlice); err != nil {
return false
}
}
return verifier.Verified()
}
// valuesOrNil returns nil if the values are empty, otherwise the values are
// returned. This is used to ensure that the digest is calculated against nil
// opposed to an empty object.
func valuesOrNil(values chartutil.Values) chartutil.Values {
if values != nil && len(values) == 0 {
return nil
}
return values
}

View File

@ -23,45 +23,222 @@ import (
"helm.sh/helm/v3/pkg/chartutil"
)
const testDigestAlgo = digest.SHA256
func TestDigestValues(t *testing.T) {
tests := []struct {
name string
algo digest.Algorithm
values chartutil.Values
want digest.Digest
}{
{
name: "empty",
algo: digest.SHA256,
values: chartutil.Values{},
want: "sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
want: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
name: "nil",
algo: digest.SHA256,
values: nil,
want: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
name: "value map",
algo: digest.SHA256,
values: chartutil.Values{
"foo": "bar",
"baz": map[string]string{
"cool": "stuff",
"replicas": 3,
"image": map[string]interface{}{
"tag": "latest",
"repository": "nginx",
},
"ports": []interface{}{
map[string]interface{}{
"protocol": "TCP",
"port": 8080,
},
map[string]interface{}{
"port": 9090,
"protocol": "UDP",
},
},
},
want: "sha256:3f3641788a2d4abda3534eaa90c90b54916e4c6e3a5b2e1b24758b7bfa701ecd",
want: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
},
{
name: "value map in different order",
algo: digest.SHA256,
values: chartutil.Values{
"baz": map[string]string{
"image": map[string]interface{}{
"repository": "nginx",
"tag": "latest",
},
"ports": []interface{}{
map[string]interface{}{
"port": 8080,
"protocol": "TCP",
},
map[string]interface{}{
"port": 9090,
"protocol": "UDP",
},
},
"replicas": 3,
},
want: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
},
{
// Explicit test for something that does not work with sigs.k8s.io/yaml.
// See: https://go.dev/play/p/KRyfK9ZobZx
name: "values map with numeric keys",
algo: digest.SHA256,
values: chartutil.Values{
"replicas": 3,
"test": map[string]interface{}{
"632bd80235a05f4192aefade": "value1",
"632bd80ddf416cf32fd50679": "value2",
"632bd817c559818a52307da2": "value3",
"632bd82398e71231a98004b6": "value4",
},
},
want: "sha256:8a980fcbeadd6f05818f07e8aec14070c22250ca3d96af1fcd5f93b3e85b4d70",
},
{
name: "values map with numeric keys in different order",
algo: digest.SHA256,
values: chartutil.Values{
"test": map[string]interface{}{
"632bd82398e71231a98004b6": "value4",
"632bd817c559818a52307da2": "value3",
"632bd80ddf416cf32fd50679": "value2",
"632bd80235a05f4192aefade": "value1",
},
"replicas": 3,
},
want: "sha256:8a980fcbeadd6f05818f07e8aec14070c22250ca3d96af1fcd5f93b3e85b4d70",
},
{
name: "using different algorithm",
algo: digest.SHA512,
values: chartutil.Values{
"foo": "bar",
"baz": map[string]interface{}{
"cool": "stuff",
},
"foo": "bar",
},
want: "sha256:3f3641788a2d4abda3534eaa90c90b54916e4c6e3a5b2e1b24758b7bfa701ecd",
want: "sha512:b5f9cd4855ca3b08afd602557f373069b1732ce2e6d52341481b0d38f1938452e9d7759ab177c66699962b592f20ceded03eea3cd405d8670578c47842e2c550",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := DigestValues(testDigestAlgo, tt.values); got != tt.want {
if got := DigestValues(tt.algo, tt.values); got != tt.want {
t.Errorf("DigestValues() = %v, want %v", got, tt.want)
}
})
}
}
func TestVerifyValues(t *testing.T) {
tests := []struct {
name string
digest digest.Digest
values chartutil.Values
want bool
}{
{
name: "empty values",
digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
values: chartutil.Values{},
want: true,
},
{
name: "nil values",
digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
values: nil,
want: true,
},
{
name: "empty digest",
digest: "",
want: false,
},
{
name: "invalid digest",
digest: "sha512:invalid",
values: nil,
want: false,
},
{
name: "matching values",
digest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
values: chartutil.Values{
"image": map[string]interface{}{
"repository": "nginx",
"tag": "latest",
},
"ports": []interface{}{
map[string]interface{}{
"port": 8080,
"protocol": "TCP",
},
map[string]interface{}{
"port": 9090,
"protocol": "UDP",
},
},
"replicas": 3,
},
want: true,
},
{
name: "matching values in different order",
digest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
values: chartutil.Values{
"replicas": 3,
"image": map[string]interface{}{
"tag": "latest",
"repository": "nginx",
},
"ports": []interface{}{
map[string]interface{}{
"protocol": "TCP",
"port": 8080,
},
map[string]interface{}{
"port": 9090,
"protocol": "UDP",
},
},
},
want: true,
},
{
name: "matching values with numeric keys",
digest: "sha256:8a980fcbeadd6f05818f07e8aec14070c22250ca3d96af1fcd5f93b3e85b4d70",
values: chartutil.Values{
"replicas": 3,
"test": map[string]interface{}{
"632bd80235a05f4192aefade": "value1",
"632bd80ddf416cf32fd50679": "value2",
"632bd817c559818a52307da2": "value3",
"632bd82398e71231a98004b6": "value4",
},
},
want: true,
},
{
name: "mismatching values",
digest: "sha256:3f3641788a2d4abda3534eaa90c90b54916e4c6e3a5b2e1b24758b7bfa701ecd",
values: chartutil.Values{
"foo": "bar",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := VerifyValues(tt.digest, tt.values); got != tt.want {
t.Errorf("VerifyValues() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -17,14 +17,12 @@ limitations under the License.
package util
import (
"bytes"
"crypto/sha1"
"fmt"
"sort"
goyaml "gopkg.in/yaml.v2"
intyaml "github.com/fluxcd/helm-controller/internal/yaml"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
"sigs.k8s.io/yaml"
)
// ValuesChecksum calculates and returns the SHA1 checksum for the
@ -40,76 +38,11 @@ func ValuesChecksum(values chartutil.Values) string {
// OrderedValuesChecksum sort the chartutil.Values then calculates
// and returns the SHA1 checksum for the sorted values.
func OrderedValuesChecksum(values chartutil.Values) string {
var s []byte
var buf bytes.Buffer
if len(values) != 0 {
msValues := yaml.JSONObjectToYAMLObject(copyValues(values))
SortMapSlice(msValues)
s, _ = goyaml.Marshal(msValues)
_ = intyaml.Encode(&buf, values, intyaml.SortMapSlice)
}
return fmt.Sprintf("%x", sha1.Sum(s))
}
// SortMapSlice recursively sorts the given goyaml.MapSlice by key.
// This is used to ensure that the values checksum is the same regardless
// of the order of the keys in the values file.
func SortMapSlice(ms goyaml.MapSlice) {
sort.Slice(ms, func(i, j int) bool {
return fmt.Sprint(ms[i].Key) < fmt.Sprint(ms[j].Key)
})
for _, item := range ms {
if nestedMS, ok := item.Value.(goyaml.MapSlice); ok {
SortMapSlice(nestedMS)
} else if _, ok := item.Value.([]interface{}); ok {
for _, vItem := range item.Value.([]interface{}) {
if itemMS, ok := vItem.(goyaml.MapSlice); ok {
SortMapSlice(itemMS)
}
}
}
}
}
// cleanUpMapValue changes all instances of
// map[interface{}]interface{} to map[string]interface{}.
// This is for handling the mismatch when unmarshaling
// reference to the issue: https://github.com/go-yaml/yaml/issues/139
func cleanUpMapValue(v interface{}) interface{} {
switch v := v.(type) {
case []interface{}:
return cleanUpInterfaceArray(v)
case map[interface{}]interface{}:
return cleanUpInterfaceMap(v)
default:
return v
}
}
func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range in {
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
}
return result
}
func cleanUpInterfaceArray(in []interface{}) []interface{} {
result := make([]interface{}, len(in))
for i, v := range in {
result[i] = cleanUpMapValue(v)
}
return result
}
func copyValues(in map[string]interface{}) map[string]interface{} {
copiedValues, _ := goyaml.Marshal(in)
newValues := make(map[string]interface{})
_ = goyaml.Unmarshal(copiedValues, newValues)
for i, value := range newValues {
newValues[i] = cleanUpMapValue(value)
}
return newValues
return fmt.Sprintf("%x", sha1.Sum(buf.Bytes()))
}
// ReleaseRevision returns the revision of the given release.Release.

View File

@ -17,10 +17,8 @@ limitations under the License.
package util
import (
"reflect"
"testing"
goyaml "gopkg.in/yaml.v2"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
)
@ -98,133 +96,3 @@ func TestReleaseRevision(t *testing.T) {
t.Fatalf("ReleaseRevision() = %v, want %v", rev, 1)
}
}
func TestSortMapSlice(t *testing.T) {
tests := []struct {
name string
input goyaml.MapSlice
expected goyaml.MapSlice
}{
{
name: "Simple case",
input: goyaml.MapSlice{
{Key: "b", Value: 2},
{Key: "a", Value: 1},
},
expected: goyaml.MapSlice{
{Key: "a", Value: 1},
{Key: "b", Value: 2},
},
},
{
name: "Nested MapSlice",
input: goyaml.MapSlice{
{Key: "b", Value: 2},
{Key: "a", Value: 1},
{Key: "c", Value: goyaml.MapSlice{
{Key: "d", Value: 4},
{Key: "e", Value: 5},
}},
},
expected: goyaml.MapSlice{
{Key: "a", Value: 1},
{Key: "b", Value: 2},
{Key: "c", Value: goyaml.MapSlice{
{Key: "d", Value: 4},
{Key: "e", Value: 5},
}},
},
},
{
name: "Empty MapSlice",
input: goyaml.MapSlice{},
expected: goyaml.MapSlice{},
},
{
name: "Single element",
input: goyaml.MapSlice{
{Key: "a", Value: 1},
},
expected: goyaml.MapSlice{
{Key: "a", Value: 1},
},
},
{
name: "Already sorted",
input: goyaml.MapSlice{
{Key: "a", Value: 1},
{Key: "b", Value: 2},
{Key: "c", Value: 3},
},
expected: goyaml.MapSlice{
{Key: "a", Value: 1},
{Key: "b", Value: 2},
{Key: "c", Value: 3},
},
},
{
name: "Complex Case",
input: goyaml.MapSlice{
{Key: "b", Value: 2},
{Key: "a", Value: map[interface{}]interface{}{
"d": []interface{}{4, 5},
"c": 3,
}},
{Key: "c", Value: goyaml.MapSlice{
{Key: "f", Value: 6},
{Key: "e", Value: goyaml.MapSlice{
{Key: "h", Value: 8},
{Key: "g", Value: 7},
}},
}},
},
expected: goyaml.MapSlice{
{Key: "a", Value: map[interface{}]interface{}{
"c": 3,
"d": []interface{}{4, 5},
}},
{Key: "b", Value: 2},
{Key: "c", Value: goyaml.MapSlice{
{Key: "e", Value: goyaml.MapSlice{
{Key: "g", Value: 7},
{Key: "h", Value: 8},
}},
{Key: "f", Value: 6},
}},
},
},
{
name: "Map slice in slice",
input: goyaml.MapSlice{
{Key: "b", Value: 2},
{Key: "a", Value: []interface{}{
map[interface{}]interface{}{
"d": 4,
"c": 3,
},
1,
}},
},
expected: goyaml.MapSlice{
{Key: "a", Value: []interface{}{
map[interface{}]interface{}{
"c": 3,
"d": 4,
},
1,
}},
{Key: "b", Value: 2},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
SortMapSlice(test.input)
if !reflect.DeepEqual(test.input, test.expected) {
t.Errorf("Expected %v, got %v", test.expected, test.input)
}
})
}
}

43
internal/yaml/encode.go Normal file
View File

@ -0,0 +1,43 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package yaml
import (
"io"
goyaml "gopkg.in/yaml.v2"
"sigs.k8s.io/yaml"
)
// PreEncoder allows for pre-processing of the YAML data before encoding.
type PreEncoder func(goyaml.MapSlice)
// Encode encodes the given data to YAML format and writes it to the provided
// io.Write, without going through a byte representation (unlike
// sigs.k8s.io/yaml#Unmarshal).
//
// It optionally takes one or more PreEncoder functions that allow
// for pre-processing of the data before encoding, such as sorting the data.
//
// It returns an error if the data cannot be encoded.
func Encode(w io.Writer, data map[string]interface{}, pe ...PreEncoder) error {
ms := yaml.JSONObjectToYAMLObject(data)
for _, m := range pe {
m(ms)
}
return goyaml.NewEncoder(w).Encode(ms)
}

View File

@ -0,0 +1,106 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package yaml
import (
"bytes"
"os"
"testing"
"sigs.k8s.io/yaml"
)
func TestEncode(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
preEncoders []PreEncoder
want []byte
}{
{
name: "empty map",
input: map[string]interface{}{},
want: []byte(`{}
`),
},
{
name: "simple values",
input: map[string]interface{}{
"replicaCount": 3,
},
want: []byte(`replicaCount: 3
`),
},
{
name: "with pre-encoder",
input: map[string]interface{}{
"replicaCount": 3,
"image": map[string]interface{}{
"repository": "nginx",
"tag": "latest",
},
"port": 8080,
},
preEncoders: []PreEncoder{SortMapSlice},
want: []byte(`image:
repository: nginx
tag: latest
port: 8080
replicaCount: 3
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var actual bytes.Buffer
err := Encode(&actual, tt.input, tt.preEncoders...)
if err != nil {
t.Fatalf("error encoding: %v", err)
}
if !bytes.Equal(actual.Bytes(), tt.want) {
t.Errorf("Encode() = %v, want: %s", actual.String(), tt.want)
}
})
}
}
func BenchmarkEncode(b *testing.B) {
// Test against the values.yaml from the kube-prometheus-stack chart, which
// is a fairly large file.
v, err := os.ReadFile("testdata/values.yaml")
if err != nil {
b.Fatalf("error reading testdata: %v", err)
}
var data map[string]interface{}
if err = yaml.Unmarshal(v, &data); err != nil {
b.Fatalf("error unmarshalling testdata: %v", err)
}
b.Run("EncodeWithSort", func(b *testing.B) {
for i := 0; i < b.N; i++ {
Encode(bytes.NewBuffer(nil), data, SortMapSlice)
}
})
b.Run("SigYAMLMarshal", func(b *testing.B) {
for i := 0; i < b.N; i++ {
yaml.Marshal(data)
}
})
}

44
internal/yaml/sort.go Normal file
View File

@ -0,0 +1,44 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package yaml
import (
"sort"
goyaml "gopkg.in/yaml.v2"
)
// SortMapSlice recursively sorts the given goyaml.MapSlice by key.
// It can be used in combination with Encode to sort YAML by key
// before encoding it.
func SortMapSlice(ms goyaml.MapSlice) {
sort.Slice(ms, func(i, j int) bool {
return ms[i].Key.(string) < ms[j].Key.(string)
})
for _, item := range ms {
if nestedMS, ok := item.Value.(goyaml.MapSlice); ok {
SortMapSlice(nestedMS)
} else if nestedSlice, ok := item.Value.([]interface{}); ok {
for _, vItem := range nestedSlice {
if nestedMS, ok := vItem.(goyaml.MapSlice); ok {
SortMapSlice(nestedMS)
}
}
}
}
}

183
internal/yaml/sort_test.go Normal file
View File

@ -0,0 +1,183 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package yaml
import (
"bytes"
"testing"
goyaml "gopkg.in/yaml.v2"
"sigs.k8s.io/yaml"
)
func TestSortMapSlice(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
want map[string]interface{}
}{
{
name: "empty map",
input: map[string]interface{}{},
want: map[string]interface{}{},
},
{
name: "flat map",
input: map[string]interface{}{
"b": "value-b",
"a": "value-a",
"c": "value-c",
},
want: map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": "value-c",
},
},
{
name: "nested map",
input: map[string]interface{}{
"b": "value-b",
"a": "value-a",
"c": map[string]interface{}{
"z": "value-z",
"y": "value-y",
},
},
want: map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": map[string]interface{}{
"y": "value-y",
"z": "value-z",
},
},
},
{
name: "map with slices",
input: map[string]interface{}{
"b": []interface{}{"apple", "banana", "cherry"},
"a": []interface{}{"orange", "grape"},
"c": []interface{}{"strawberry"},
},
want: map[string]interface{}{
"a": []interface{}{"orange", "grape"},
"b": []interface{}{"apple", "banana", "cherry"},
"c": []interface{}{"strawberry"},
},
},
{
name: "map with mixed data types",
input: map[string]interface{}{
"b": 50,
"a": "value-a",
"c": []interface{}{"strawberry", "banana"},
"d": map[string]interface{}{
"x": true,
"y": 123,
},
},
want: map[string]interface{}{
"a": "value-a",
"b": 50,
"c": []interface{}{"strawberry", "banana"},
"d": map[string]interface{}{
"x": true,
"y": 123,
},
},
},
{
name: "map with complex structure",
input: map[string]interface{}{
"a": map[string]interface{}{
"c": "value-c",
"b": "value-b",
"a": "value-a",
},
"b": "value-b",
"c": map[string]interface{}{
"z": map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": "value-c",
},
"y": "value-y",
},
"d": map[string]interface{}{
"q": "value-q",
"p": "value-p",
"r": "value-r",
},
"e": []interface{}{"strawberry", "banana"},
},
want: map[string]interface{}{
"a": map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": "value-c",
},
"b": "value-b",
"c": map[string]interface{}{
"y": "value-y",
"z": map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": "value-c",
},
},
"d": map[string]interface{}{
"p": "value-p",
"q": "value-q",
"r": "value-r",
},
"e": []interface{}{"strawberry", "banana"},
},
},
{
name: "map with empty slices and maps",
input: map[string]interface{}{
"b": []interface{}{},
"a": map[string]interface{}{},
},
want: map[string]interface{}{
"a": map[string]interface{}{},
"b": []interface{}{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input := yaml.JSONObjectToYAMLObject(tt.input)
SortMapSlice(input)
expect, err := goyaml.Marshal(input)
if err != nil {
t.Fatalf("error marshalling output: %v", err)
}
actual, err := goyaml.Marshal(tt.want)
if err != nil {
t.Fatalf("error marshalling want: %v", err)
}
if !bytes.Equal(expect, actual) {
t.Errorf("SortMapSlice() = %s, want %s", expect, actual)
}
})
}
}

4043
internal/yaml/testdata/values.yaml vendored Normal file

File diff suppressed because it is too large Load Diff