1
0
mirror of https://github.com/getsops/sops.git synced 2026-02-05 12:45:21 +01:00

Use yaml.v3 instead of modified yaml.v2 for handling YAML files (#791)

* Add another test (that currently fails).

* First shot at using yaml.v3 for reading YAML files with comments.

* Allow parsing multi-document YAML files.

* Use Decoder to parse multi-part documents.

* Use yaml.v3 for config and audit.

* First step of serializing YAML using yaml.v3.

* Always serialize with yaml.v3.

* Remove debug prints.

* Remove traces of github.com/mozilla-services/yaml.

* Improve serialization of documents consisting only of comments.

* Improve handling of some empty documents.

* Adjust to latest changes in go-yaml/yaml#684.

* Bump yaml.v3 version, temporarily disable failing tests.

* Run go mod tidy.

* Fix CI.
This commit is contained in:
Felix Fontein
2021-02-21 18:48:23 +01:00
committed by GitHub
parent 24636e4f23
commit e2d6d0fdc3
6 changed files with 363 additions and 107 deletions

View File

@@ -12,7 +12,7 @@ import (
// empty import as per https://godoc.org/github.com/lib/pq
_ "github.com/lib/pq"
"github.com/mozilla-services/yaml"
"gopkg.in/yaml.v3"
"github.com/sirupsen/logrus"
"go.mozilla.org/sops/v3/logging"
)

View File

@@ -10,7 +10,7 @@ import (
"path"
"regexp"
"github.com/mozilla-services/yaml"
"gopkg.in/yaml.v3"
"github.com/sirupsen/logrus"
"go.mozilla.org/sops/v3"
"go.mozilla.org/sops/v3/age"

2
go.mod
View File

@@ -30,7 +30,6 @@ require (
github.com/lib/pq v1.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/go-wordwrap v1.0.0
github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v0.1.1 // indirect
@@ -49,5 +48,6 @@ require (
google.golang.org/protobuf v1.25.0
gopkg.in/ini.v1 v1.44.0
gopkg.in/urfave/cli.v1 v1.20.0
gopkg.in/yaml.v3 v3.0.0-20210107172259-749611fa9fcc
gotest.tools v2.2.0+incompatible // indirect
)

4
go.sum
View File

@@ -201,8 +201,6 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 h1:5IeGQzguDQ+EsTR5HE7tMYkZe09mqQ9cDypdKQEB5Kg=
github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625/go.mod h1:Is/Ucts/yU/mWyGR8yELRoO46mejouKsJfQLAIfTR18=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -410,6 +408,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20210107172259-749611fa9fcc h1:XANm4xAMEQhRdWKqaL0qmhGDv7RuobwCO97TIlktaQE=
gopkg.in/yaml.v3 v3.0.0-20210107172259-749611fa9fcc/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -1,9 +1,12 @@
package yaml //import "go.mozilla.org/sops/v3/stores/yaml"
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/mozilla-services/yaml"
"gopkg.in/yaml.v3"
"go.mozilla.org/sops/v3"
"go.mozilla.org/sops/v3/stores"
)
@@ -12,102 +15,232 @@ import (
type Store struct {
}
func (store Store) mapSliceToTreeBranch(in yaml.MapSlice) sops.TreeBranch {
branch := make(sops.TreeBranch, 0)
for _, item := range in {
if comment, ok := item.Key.(yaml.Comment); ok {
// Convert the yaml comment to a generic sops comment
branch = append(branch, sops.TreeItem{
Key: sops.Comment{
Value: comment.Value,
},
Value: nil,
})
} else {
branch = append(branch, sops.TreeItem{
Key: item.Key,
Value: store.yamlValueToTreeValue(item.Value),
})
func (store Store) appendCommentToList(comment string, list []interface{}) []interface{} {
if comment != "" {
for _, commentLine := range strings.Split(comment, "\n") {
if commentLine != "" {
list = append(list, sops.Comment{
Value: commentLine[1:],
})
}
}
}
return list
}
func (store Store) appendCommentToMap(comment string, branch sops.TreeBranch) sops.TreeBranch {
if comment != "" {
for _, commentLine := range strings.Split(comment, "\n") {
if commentLine != "" {
branch = append(branch, sops.TreeItem{
Key: sops.Comment{
Value: commentLine[1:],
},
Value: nil,
})
}
}
}
return branch
}
func (store Store) yamlValueToTreeValue(in interface{}) interface{} {
switch in := in.(type) {
case map[interface{}]interface{}:
return store.yamlMapToTreeBranch(in)
case yaml.MapSlice:
return store.mapSliceToTreeBranch(in)
case []interface{}:
return store.yamlSliceToTreeValue(in)
case yaml.Comment:
return sops.Comment{Value: in.Value}
default:
return in
func (store Store) nodeToTreeValue(node *yaml.Node, commentsWereHandled bool) (interface{}, error) {
switch node.Kind {
case yaml.DocumentNode:
panic("documents should never be passed here")
case yaml.SequenceNode:
var result []interface{}
if !commentsWereHandled {
result = store.appendCommentToList(node.HeadComment, result)
result = store.appendCommentToList(node.LineComment, result)
}
for _, item := range node.Content {
result = store.appendCommentToList(item.HeadComment, result)
result = store.appendCommentToList(item.LineComment, result)
val, err := store.nodeToTreeValue(item, true)
if err != nil {
return nil, err
}
result = append(result, val)
result = store.appendCommentToList(item.FootComment, result)
}
if !commentsWereHandled {
result = store.appendCommentToList(node.FootComment, result)
}
return result, nil
case yaml.MappingNode:
branch := make(sops.TreeBranch, 0)
return store.appendYamlNodeToTreeBranch(node, branch, false)
case yaml.ScalarNode:
var result interface{}
node.Decode(&result)
return result, nil
case yaml.AliasNode:
return store.nodeToTreeValue(node.Alias, false);
}
return nil, nil
}
func (store *Store) yamlSliceToTreeValue(in []interface{}) []interface{} {
for i, v := range in {
in[i] = store.yamlValueToTreeValue(v)
func (store Store) appendYamlNodeToTreeBranch(node *yaml.Node, branch sops.TreeBranch, commentsWereHandled bool) (sops.TreeBranch, error) {
var err error
if !commentsWereHandled {
branch = store.appendCommentToMap(node.HeadComment, branch)
branch = store.appendCommentToMap(node.LineComment, branch)
}
return in
switch node.Kind {
case yaml.DocumentNode:
for _, item := range node.Content {
branch, err = store.appendYamlNodeToTreeBranch(item, branch, false)
if err != nil {
return nil, err
}
}
case yaml.SequenceNode:
return nil, fmt.Errorf("YAML documents that are sequences are not supported")
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
key := node.Content[i]
value := node.Content[i + 1]
branch = store.appendCommentToMap(key.HeadComment, branch)
branch = store.appendCommentToMap(key.LineComment, branch)
handleValueComments := value.Kind == yaml.ScalarNode || value.Kind == yaml.AliasNode
if handleValueComments {
branch = store.appendCommentToMap(value.HeadComment, branch)
branch = store.appendCommentToMap(value.LineComment, branch)
}
var keyValue interface{}
key.Decode(&keyValue)
valueTV, err := store.nodeToTreeValue(value, handleValueComments)
if err != nil {
return nil, err
}
branch = append(branch, sops.TreeItem{
Key: keyValue,
Value: valueTV,
})
if handleValueComments {
branch = store.appendCommentToMap(value.FootComment, branch)
}
branch = store.appendCommentToMap(key.FootComment, branch)
}
case yaml.ScalarNode:
// A empty document with a document start marker without comments results in null
if node.ShortTag() == "!!null" {
return branch, nil
}
return nil, fmt.Errorf("YAML documents that are values are not supported")
case yaml.AliasNode:
branch, err = store.appendYamlNodeToTreeBranch(node.Alias, branch, false)
}
if !commentsWereHandled {
branch = store.appendCommentToMap(node.FootComment, branch)
}
return branch, nil
}
func (store *Store) yamlMapToTreeBranch(in map[interface{}]interface{}) sops.TreeBranch {
func (store Store) yamlDocumentNodeToTreeBranch(in yaml.Node) (sops.TreeBranch, error) {
branch := make(sops.TreeBranch, 0)
for k, v := range in {
branch = append(branch, sops.TreeItem{
Key: k.(string),
Value: store.yamlValueToTreeValue(v),
})
}
return branch
return store.appendYamlNodeToTreeBranch(&in, branch, false)
}
func (store Store) treeValueToYamlValue(in interface{}) interface{} {
func (store *Store) addCommentsHead(node *yaml.Node, comments []string) []string {
if len(comments) > 0 {
comment := "#" + strings.Join(comments, "\n#")
if len(node.HeadComment) > 0 {
node.HeadComment = comment + "\n" + node.HeadComment
} else {
node.HeadComment = comment
}
}
return nil
}
func (store *Store) addCommentsFoot(node *yaml.Node, comments []string) []string {
if len(comments) > 0 {
comment := "#" + strings.Join(comments, "\n#")
if len(node.FootComment) > 0 {
node.FootComment += "\n" + comment
} else {
node.FootComment = comment
}
}
return nil
}
func (store *Store) treeValueToNode(in interface{}) *yaml.Node {
switch in := in.(type) {
case sops.TreeBranch:
return store.treeBranchToYamlMap(in)
case sops.Comment:
return yaml.Comment{in.Value}
var mapping = &yaml.Node{}
mapping.Kind = yaml.MappingNode
store.appendTreeBranch(in, mapping)
return mapping
case []interface{}:
var out []interface{}
for _, v := range in {
out = append(out, store.treeValueToYamlValue(v))
}
return out
var sequence = &yaml.Node{}
sequence.Kind = yaml.SequenceNode
store.appendSequence(in, sequence)
return sequence
default:
return in
var valueNode = &yaml.Node{}
valueNode.Encode(in)
return valueNode
}
}
func (store Store) treeBranchToYamlMap(in sops.TreeBranch) yaml.MapSlice {
branch := make(yaml.MapSlice, 0)
func (store *Store) appendSequence(in []interface{}, sequence *yaml.Node) {
var comments []string
var beginning bool = true
for _, item := range in {
if comment, ok := item.Key.(sops.Comment); ok {
branch = append(branch, yaml.MapItem{
Key: store.treeValueToYamlValue(comment),
Value: nil,
})
if comment, ok := item.(sops.Comment); ok {
comments = append(comments, comment.Value)
} else {
branch = append(branch, yaml.MapItem{
Key: item.Key,
Value: store.treeValueToYamlValue(item.Value),
})
if beginning {
comments = store.addCommentsHead(sequence, comments)
beginning = false
}
itemNode := store.treeValueToNode(item)
comments = store.addCommentsHead(itemNode, comments)
sequence.Content = append(sequence.Content, itemNode)
}
}
if len(comments) > 0 {
if beginning {
comments = store.addCommentsHead(sequence, comments)
} else {
comments = store.addCommentsFoot(sequence.Content[len(sequence.Content) - 1], comments)
}
}
}
func (store *Store) appendTreeBranch(branch sops.TreeBranch, mapping *yaml.Node) {
var comments []string
var beginning bool = true
for _, item := range branch {
if comment, ok := item.Key.(sops.Comment); ok {
comments = append(comments, comment.Value)
} else {
if beginning {
comments = store.addCommentsHead(mapping, comments)
beginning = false
}
var keyNode = &yaml.Node{}
keyNode.Encode(item.Key)
comments = store.addCommentsHead(keyNode, comments)
valueNode := store.treeValueToNode(item.Value)
mapping.Content = append(mapping.Content, keyNode, valueNode)
}
}
if len(comments) > 0 {
if beginning {
comments = store.addCommentsHead(mapping, comments)
} else {
comments = store.addCommentsFoot(mapping.Content[len(mapping.Content) - 1], comments)
}
}
return branch
}
// LoadEncryptedFile loads the contents of an encrypted yaml file onto a
// sops.Tree runtime object
func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) {
var data []yaml.MapSlice
if err := (yaml.CommentUnmarshaler{}).UnmarshalDocuments(in, &data); err != nil {
return sops.Tree{}, fmt.Errorf("Error unmarshaling input YAML: %s", err)
}
// Because we don't know what fields the input file will have, we have to
// load the file in two steps.
// First, we load the file's metadata, the structure of which is known.
@@ -123,14 +256,33 @@ func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) {
if err != nil {
return sops.Tree{}, err
}
var data yaml.Node
if err := yaml.Unmarshal(in, &data); err != nil {
return sops.Tree{}, fmt.Errorf("Error unmarshaling input YAML: %s", err)
}
var branches sops.TreeBranches
for _, doc := range data {
for i, item := range doc {
if item.Key == "sops" { // Erase
doc = append(doc[:i], doc[i+1:]...)
d := yaml.NewDecoder(bytes.NewReader(in))
for true {
var data yaml.Node
err := d.Decode(&data)
if err == io.EOF {
break
}
if err != nil {
return sops.Tree{}, fmt.Errorf("Error unmarshaling input YAML: %s", err)
}
branch, err := store.yamlDocumentNodeToTreeBranch(data)
if err != nil {
return sops.Tree{}, fmt.Errorf("Error unmarshaling input YAML: %s", err)
}
for i, elt := range branch {
if elt.Key == "sops" { // Erase
branch = append(branch[:i], branch[i+1:]...)
}
}
branches = append(branches, store.mapSliceToTreeBranch(doc))
branches = append(branches, branch)
}
return sops.Tree{
Branches: branches,
@@ -139,16 +291,25 @@ func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) {
}
// LoadPlainFile loads the contents of a plaintext yaml file onto a
// sops.Tree runtime obejct
// sops.Tree runtime object
func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) {
var data []yaml.MapSlice
if err := (yaml.CommentUnmarshaler{}).UnmarshalDocuments(in, &data); err != nil {
return nil, fmt.Errorf("Error unmarshaling input YAML: %s", err)
}
var branches sops.TreeBranches
for _, doc := range data {
branches = append(branches, store.mapSliceToTreeBranch(doc))
d := yaml.NewDecoder(bytes.NewReader(in))
for true {
var data yaml.Node
err := d.Decode(&data)
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("Error unmarshaling input YAML: %s", err)
}
branch, err := store.yamlDocumentNodeToTreeBranch(data)
if err != nil {
return nil, fmt.Errorf("Error unmarshaling input YAML: %s", err)
}
branches = append(branches, branch)
}
return branches, nil
}
@@ -156,45 +317,70 @@ func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) {
// EmitEncryptedFile returns the encrypted bytes of the yaml file corresponding to a
// sops.Tree runtime object
func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) {
out := []byte{}
for i, branch := range in.Branches {
if i > 0 {
out = append(out, "---\n"...)
}
yamlMap := store.treeBranchToYamlMap(branch)
yamlMap = append(yamlMap, yaml.MapItem{Key: "sops", Value: stores.MetadataFromInternal(in.Metadata)})
tout, err := (&yaml.YAMLMarshaler{Indent: 4}).Marshal(yamlMap)
var b bytes.Buffer
e := yaml.NewEncoder(io.Writer(&b))
e.SetIndent(4)
for _, branch := range in.Branches {
// Document root
var doc = yaml.Node{}
doc.Kind = yaml.DocumentNode
// Add global mapping
var mapping = yaml.Node{}
mapping.Kind = yaml.MappingNode
doc.Content = append(doc.Content, &mapping)
// Create copy of branch with metadata appended
branch = append(sops.TreeBranch(nil), branch...)
branch = append(branch, sops.TreeItem{
Key: "sops",
Value: stores.MetadataFromInternal(in.Metadata),
})
// Marshal branch to global mapping node
store.appendTreeBranch(branch, &mapping)
// Encode YAML
err := e.Encode(&doc)
if err != nil {
return nil, fmt.Errorf("Error marshaling to yaml: %s", err)
}
out = append(out, tout...)
}
return out, nil
e.Close()
return b.Bytes(), nil
}
// EmitPlainFile returns the plaintext bytes of the yaml file corresponding to a
// sops.TreeBranches runtime object
func (store *Store) EmitPlainFile(branches sops.TreeBranches) ([]byte, error) {
var out []byte
for i, branch := range branches {
if i > 0 {
out = append(out, "---\n"...)
var b bytes.Buffer
e := yaml.NewEncoder(io.Writer(&b))
e.SetIndent(4)
for _, branch := range branches {
// Document root
var doc = yaml.Node{}
doc.Kind = yaml.DocumentNode
// Add global mapping
var mapping = yaml.Node{}
mapping.Kind = yaml.MappingNode
// Marshal branch to global mapping node
store.appendTreeBranch(branch, &mapping)
if len(mapping.Content) == 0 {
doc.HeadComment = mapping.HeadComment
} else {
doc.Content = append(doc.Content, &mapping)
}
yamlMap := store.treeBranchToYamlMap(branch)
tmpout, err := (&yaml.YAMLMarshaler{Indent: 4}).Marshal(yamlMap)
// Encode YAML
err := e.Encode(&doc)
if err != nil {
return nil, fmt.Errorf("Error marshaling to yaml: %s", err)
}
out = append(out[:], tmpout[:]...)
}
return out, nil
e.Close()
return b.Bytes(), nil
}
// EmitValue returns bytes corresponding to a single encoded value
// in a generic interface{} object
func (store *Store) EmitValue(v interface{}) ([]byte, error) {
v = store.treeValueToYamlValue(v)
return (&yaml.YAMLMarshaler{Indent: 4}).Marshal(v)
n := store.treeValueToNode(v)
return yaml.Marshal(n)
}
// EmitExample returns the bytes corresponding to an example complex tree

View File

@@ -15,6 +15,12 @@ key1_a: value
---
key2: value2`)
var PLAIN_0 = []byte(`# comment 0
key1: value
key1_a: value
# ^ comment 1
`)
var BRANCHES = sops.TreeBranches{
sops.TreeBranch{
sops.TreeItem{
@@ -73,8 +79,16 @@ prometheus-node-exporter:
##
jobLabel: node-exporter
extraArgs:
- --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+)($|/)
- --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$
- --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+)($|/)
- --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$
`)
var COMMENT_4 = []byte(`# foo
`)
var COMMENT_5 = []byte(`# foo
---
key: value
`)
func TestUnmarshalMetadataFromNonSOPSFile(t *testing.T) {
@@ -95,6 +109,7 @@ func TestComment1(t *testing.T) {
assert.Nil(t, err)
bytes, err := (&Store{}).EmitPlainFile(branches)
assert.Nil(t, err)
assert.Equal(t, string(COMMENT_1), string(bytes))
assert.Equal(t, COMMENT_1, bytes)
}
@@ -104,6 +119,7 @@ func TestComment2(t *testing.T) {
assert.Nil(t, err)
bytes, err := (&Store{}).EmitPlainFile(branches)
assert.Nil(t, err)
assert.Equal(t, string(COMMENT_2), string(bytes))
assert.Equal(t, COMMENT_2, bytes)
}
@@ -113,5 +129,59 @@ func TestComment3(t *testing.T) {
assert.Nil(t, err)
bytes, err := (&Store{}).EmitPlainFile(branches)
assert.Nil(t, err)
assert.Equal(t, string(COMMENT_3_OUT), string(bytes))
assert.Equal(t, COMMENT_3_OUT, bytes)
}
/* TODO: re-enable once https://github.com/go-yaml/yaml/pull/690 is merged
func TestComment4(t *testing.T) {
// First iteration: load and store
branches, err := (&Store{}).LoadPlainFile(COMMENT_4)
assert.Nil(t, err)
bytes, err := (&Store{}).EmitPlainFile(branches)
assert.Nil(t, err)
assert.Equal(t, string(COMMENT_4), string(bytes))
assert.Equal(t, COMMENT_4, bytes)
}
func TestComment5(t *testing.T) {
// First iteration: load and store
branches, err := (&Store{}).LoadPlainFile(COMMENT_5)
assert.Nil(t, err)
bytes, err := (&Store{}).EmitPlainFile(branches)
assert.Nil(t, err)
assert.Equal(t, string(COMMENT_5), string(bytes))
assert.Equal(t, COMMENT_5, bytes)
}
*/
func TestEmpty(t *testing.T) {
// First iteration: load and store
branches, err := (&Store{}).LoadPlainFile([]byte(``))
assert.Nil(t, err)
assert.Equal(t, len(branches), 0)
bytes, err := (&Store{}).EmitPlainFile(branches)
assert.Nil(t, err)
assert.Equal(t, ``, string(bytes))
}
/* TODO: re-enable once https://github.com/go-yaml/yaml/pull/690 is merged
func TestEmpty2(t *testing.T) {
// First iteration: load and store
branches, err := (&Store{}).LoadPlainFile([]byte(`---`))
assert.Nil(t, err)
assert.Equal(t, len(branches), 1)
assert.Equal(t, len(branches[0]), 0)
bytes, err := (&Store{}).EmitPlainFile(branches)
assert.Nil(t, err)
assert.Equal(t, ``, string(bytes))
}
*/
func TestEmitValue(t *testing.T) {
// First iteration: load and store
bytes, err := (&Store{}).EmitValue(BRANCHES[0])
assert.Nil(t, err)
assert.Equal(t, string(PLAIN_0), string(bytes))
assert.Equal(t, PLAIN_0, bytes)
}