Skip to content

Commit

Permalink
Add YAML anchor/alias expansion.
Browse files Browse the repository at this point in the history
  • Loading branch information
monopole committed Aug 19, 2021
1 parent 6c4e801 commit 360585d
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 9 deletions.
29 changes: 29 additions & 0 deletions api/resmap/resmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,35 @@ type ResMap interface {
// namespaces. Cluster wide objects are never excluded.
SubsetThatCouldBeReferencedByResource(*resource.Resource) ResMap

// DeAnchor replaces YAML aliases with structured data copied from anchors.
// This cannot be undone; if desired, call DeepCopy first.
// Subsequent marshalling to YAML will no longer have anchor
// definitions ('&') or aliases ('*').
//
// Anchors are not expected to work across YAML 'documents'.
// If three resources are loaded from one file containing three YAML docs:
//
// {resourceA}
// ---
// {resourceB}
// ---
// {resourceC}
//
// then anchors defined in A cannot be seen from B and C and vice versa.
// OTOH, cross-resource links (a field in B referencing fields in A) will
// work if the resources are gathered in a ResourceList:
//
// apiVersion: config.kubernetes.io/v1
// kind: ResourceList
// metadata:
// name: someList
// items:
// - {resourceA}
// - {resourceB}
// - {resourceC}
//
DeAnchor() error

// DeepCopy copies the ResMap and underlying resources.
DeepCopy() ResMap

Expand Down
10 changes: 10 additions & 0 deletions api/resmap/reswrangler.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,16 @@ func (m *resWrangler) ToRNodeSlice() []*kyaml.RNode {
return result
}

// DeAnchor implements ResMap.
func (m *resWrangler) DeAnchor() (err error) {
for i := range m.rList {
if err = m.rList[i].DeAnchor(); err != nil {
return err
}
}
return nil
}

// ApplySmPatch applies the patch, and errors on Id collisions.
func (m *resWrangler) ApplySmPatch(
selectedSet *resource.IdSet, patch *resource.Resource) error {
Expand Down
94 changes: 94 additions & 0 deletions api/resmap/reswrangler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,100 @@ rules:
}
}

func TestDeAnchorSingleDoc(t *testing.T) {
input := `apiVersion: v1
kind: ConfigMap
metadata:
name: wildcard
data:
color: &color-used blue
feeling: *color-used
`
rm, err := rmF.NewResMapFromBytes([]byte(input))
assert.NoError(t, err)
assert.NoError(t, rm.DeAnchor())
yaml, err := rm.AsYaml()
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(`
apiVersion: v1
data:
color: blue
feeling: blue
kind: ConfigMap
metadata:
name: wildcard
`), strings.TrimSpace(string(yaml)))
}

// Anchor references don't cross YAML document boundaries.
func TestDeAnchorMultiDoc(t *testing.T) {
input := `apiVersion: v1
kind: ConfigMap
metadata:
name: betty
data:
color: &color-used blue
feeling: *color-used
---
apiVersion: v1
kind: ConfigMap
metadata:
name: bob
data:
color: red
feeling: *color-used
`
_, err := rmF.NewResMapFromBytes([]byte(input))
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown anchor 'color-used' referenced")
}

// Anchor references cross list elements in a ResourceList.
func TestDeAnchorResourceList(t *testing.T) {
input := `apiVersion: config.kubernetes.io/v1
kind: ResourceList
metadata:
name: aShortList
items:
- apiVersion: v1
kind: ConfigMap
metadata:
name: betty
data:
color: &color-used blue
feeling: *color-used
- apiVersion: v1
kind: ConfigMap
metadata:
name: bob
data:
color: red
feeling: *color-used
`
rm, err := rmF.NewResMapFromBytes([]byte(input))
assert.NoError(t, err)
assert.NoError(t, rm.DeAnchor())
yaml, err := rm.AsYaml()
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(`
apiVersion: v1
data:
color: blue
feeling: blue
kind: ConfigMap
metadata:
name: betty
---
apiVersion: v1
data:
color: red
feeling: blue
kind: ConfigMap
metadata:
name: bob
`), strings.TrimSpace(string(yaml)))
}

func TestApplySmPatch_General(t *testing.T) {
const (
myDeployment = "Deployment"
Expand Down
16 changes: 7 additions & 9 deletions kyaml/kio/byteio_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -808,15 +808,13 @@ items:
}
}

// This test is just an exploration of the low level (go-yaml)
// representation of a small doc with an anchor. The anchor
// structure is there, in the sense that an alias pointer is
// readily available when a node's kind is an AliasNode.
// That is, the anchor mapping has already been recognized.
// However, the github.com/go-yaml/yaml/encoder.go code doesn't
// appear to have an option to perform anchor replacements when
// encoding (instead it emits the anchor definitions and
// references, which is not a bad thing but not desired here).
// This test shows the lower level (go-yaml) representation of a small doc
// with an anchor. The anchor structure is there, in the sense that an
// alias pointer is readily available when a node's kind is an AliasNode.
// I.e. the anchor mapping name -> object was noted during unmarshalling.
// However, at the time of writing github.com/go-yaml/yaml/encoder.go
// doesn't appear to have an option to perform anchor replacements when
// encoding. It emits anchor definitions and references (aliases) intact.
func TestByteReader_AnchorBehavior(t *testing.T) {
const input = `
data:
Expand Down
42 changes: 42 additions & 0 deletions kyaml/yaml/rnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,48 @@ func (rn *RNode) UnmarshalJSON(b []byte) error {
return nil
}

// DeAnchor inflates all YAML aliases with their anchor values.
// All YAML anchor data is permanently removed (feel free to call Copy first).
func (rn *RNode) DeAnchor() (err error) {
rn.value, err = deAnchor(rn.value)
return
}

// deAnchor removes all AliasNodes from the yaml.Node's tree, replacing
// them with what they point to. All Anchor fields (these are used to mark
// anchor definitions) are cleared.
func deAnchor(yn *yaml.Node) (res *yaml.Node, err error) {
if yn == nil {
return nil, nil
}
if yn.Anchor != "" {
// This node defines an anchor. Clear the field so that it
// doesn't show up when marshalling.
if yn.Kind == yaml.AliasNode {
// Maybe this is OK, but for now treating it as a bug.
return nil, fmt.Errorf(
"anchor %q defined using alias %v", yn.Anchor, yn.Alias)
}
yn.Anchor = ""
}
switch yn.Kind {
case yaml.ScalarNode:
return yn, nil
case yaml.AliasNode:
return deAnchor(yn.Alias)
case yaml.DocumentNode, yaml.MappingNode, yaml.SequenceNode:
for i := range yn.Content {
yn.Content[i], err = deAnchor(yn.Content[i])
if err != nil {
return nil, err
}
}
return yn, nil
default:
return nil, fmt.Errorf("cannot deAnchor kind %q", yn.Kind)
}
}

// GetValidatedMetadata returns metadata after subjecting it to some tests.
func (rn *RNode) GetValidatedMetadata() (ResourceMeta, error) {
m, err := rn.GetMeta()
Expand Down
25 changes: 25 additions & 0 deletions kyaml/yaml/rnode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,31 @@ spec:
}
}

func TestDeAnchor(t *testing.T) {
rn, err := Parse(`
apiVersion: v1
kind: ConfigMap
metadata:
name: wildcard
data:
color: &color-used blue
feeling: *color-used
`)
assert.NoError(t, err)
assert.NoError(t, rn.DeAnchor())
actual, err := rn.String()
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(`
apiVersion: v1
kind: ConfigMap
metadata:
name: wildcard
data:
color: blue
feeling: blue
`), strings.TrimSpace(actual))
}

func TestRNode_UnmarshalJSON(t *testing.T) {
testCases := []struct {
testName string
Expand Down

0 comments on commit 360585d

Please sign in to comment.