Fix error during expansion of !!merge <<: anchor tags (#4383)

* WIP

* Fix merge corner cases

* Add test for explicit !!merge tag

* Fix tests

* Cleanup

* Cleanup

* Fix deanchoring lists

* Add test case for keeping comments

* Add MapEntrySetter and fix json marshalling after deanchoring

* Keep duplicated keys

* Move MergeTag definition to yaml alias

* Remove go-spew from api

* Add support for sequence nodes on merge tags

* Add docstring to MapEntrySetter.Key

* Add docstring to MapEntrySetter struct

* Add tests to MapEntrySetter

* Fix duplicate merge key

* Revert whitespace changes on forked go-yaml

* Remove AssocMapEntry function

* Refactoring merge order

* Return errors on VisitFields and PipeE

* Add tests for each non-conforming map merges
This commit is contained in:
Rafael Leal
2022-03-23 13:36:17 -03:00
committed by GitHub
parent 3490fb8716
commit 97de780feb
8 changed files with 787 additions and 10 deletions

View File

@@ -932,7 +932,17 @@ func deAnchor(yn *yaml.Node) (res *yaml.Node, err error) {
return yn, nil
case yaml.AliasNode:
return deAnchor(yn.Alias)
case yaml.DocumentNode, yaml.MappingNode, yaml.SequenceNode:
case yaml.MappingNode:
toMerge, err := removeMergeTags(yn)
if err != nil {
return nil, err
}
err = mergeAll(yn, toMerge)
if err != nil {
return nil, err
}
fallthrough
case yaml.DocumentNode, yaml.SequenceNode:
for i := range yn.Content {
yn.Content[i], err = deAnchor(yn.Content[i])
if err != nil {
@@ -945,6 +955,103 @@ func deAnchor(yn *yaml.Node) (res *yaml.Node, err error) {
}
}
// isMerge returns if the node is tagged with !!merge
func isMerge(yn *yaml.Node) bool {
return yn.Tag == MergeTag
}
// findMergeValues receives either a MappingNode, a AliasNode or a potentially
// mixed list of MappingNodes and AliasNodes. It returns a list of MappingNodes.
func findMergeValues(yn *yaml.Node) ([]*yaml.Node, error) {
if yn == nil {
return []*yaml.Node{}, nil
}
switch yn.Kind {
case MappingNode:
return []*yaml.Node{yn}, nil
case AliasNode:
if yn.Alias != nil && yn.Alias.Kind != MappingNode {
return nil, errors.Errorf("invalid map merge: received alias for a non-map value")
}
return []*yaml.Node{yn.Alias}, nil
case SequenceNode:
mergeValues := []*yaml.Node{}
for i := 0; i < len(yn.Content); i++ {
if yn.Content[i].Kind == SequenceNode {
return nil, errors.Errorf("invalid map merge: received a nested sequence")
}
newMergeValues, err := findMergeValues(yn.Content[i])
if err != nil {
return nil, err
}
mergeValues = append(newMergeValues, mergeValues...)
}
return mergeValues, nil
default:
return nil, errors.Errorf("map merge requires map or sequence of maps as the value")
}
}
// getMergeTagValue receives a MappingNode yaml node, and it searches for
// merge tagged keys and return its value yaml node. If the key is duplicated,
// it fails.
func getMergeTagValue(yn *yaml.Node) (*yaml.Node, error) {
var result *yaml.Node
for i := 0; i < len(yn.Content); i += 2 {
key := yn.Content[i]
value := yn.Content[i+1]
if isMerge(key) {
if result != nil {
return nil, fmt.Errorf("duplicate merge key")
}
result = value
}
}
return result, nil
}
// removeMergeTags removes all merge tags and returns a ordered list of yaml
// nodes to merge and a error
func removeMergeTags(yn *yaml.Node) ([]*yaml.Node, error) {
if yn == nil || yn.Content == nil {
return nil, nil
}
if yn.Kind != yaml.MappingNode {
return nil, nil
}
value, err := getMergeTagValue(yn)
if err != nil {
return nil, err
}
toMerge, err := findMergeValues(value)
if err != nil {
return nil, err
}
err = NewRNode(yn).PipeE(Clear("<<"))
if err != nil {
return nil, err
}
return toMerge, nil
}
func mergeAll(yn *yaml.Node, toMerge []*yaml.Node) error {
// We only need to start with a copy of the existing node because we need to
// maintain duplicated keys and style
rn := NewRNode(yn).Copy()
toMerge = append(toMerge, yn)
for i := range toMerge {
rnToMerge := NewRNode(toMerge[i]).Copy()
err := rnToMerge.VisitFields(func(node *MapNode) error {
return rn.PipeE(MapEntrySetter{Key: node.Key, Value: node.Value})
})
if err != nil {
return err
}
}
*yn = *rn.value
return nil
}
// GetValidatedMetadata returns metadata after subjecting it to some tests.
func (rn *RNode) GetValidatedMetadata() (ResourceMeta, error) {
m, err := rn.GetMeta()