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:
parent
bb4e9b7cee
commit
eee91b06fa
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue