Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add YAML anchor/alias expansion. #4114

Merged
merged 1 commit into from
Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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