diff --git a/api/resmap/resmap.go b/api/resmap/resmap.go index 7c0c2ea80..960441b79 100644 --- a/api/resmap/resmap.go +++ b/api/resmap/resmap.go @@ -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 diff --git a/api/resmap/reswrangler.go b/api/resmap/reswrangler.go index 5bdc5c585..31bfe1fee 100644 --- a/api/resmap/reswrangler.go +++ b/api/resmap/reswrangler.go @@ -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 { diff --git a/api/resmap/reswrangler_test.go b/api/resmap/reswrangler_test.go index e3ed04142..a47dddab7 100644 --- a/api/resmap/reswrangler_test.go +++ b/api/resmap/reswrangler_test.go @@ -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" diff --git a/kyaml/kio/byteio_reader_test.go b/kyaml/kio/byteio_reader_test.go index dc6abb4b9..1ae73b545 100644 --- a/kyaml/kio/byteio_reader_test.go +++ b/kyaml/kio/byteio_reader_test.go @@ -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: diff --git a/kyaml/yaml/rnode.go b/kyaml/yaml/rnode.go index 9d9cbcaf9..151a2f4f4 100644 --- a/kyaml/yaml/rnode.go +++ b/kyaml/yaml/rnode.go @@ -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() diff --git a/kyaml/yaml/rnode_test.go b/kyaml/yaml/rnode_test.go index 3cce6f0ac..4ba1e87a4 100644 --- a/kyaml/yaml/rnode_test.go +++ b/kyaml/yaml/rnode_test.go @@ -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