Files
kustomize/kyaml/kio/byteio_reader_test.go
Rafael Leal 97de780feb 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
2022-03-23 09:36:17 -07:00

1105 lines
19 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kio_test
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
. "sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestByteReader(t *testing.T) {
type testCase struct {
name string
input string
err string
expectedItems []string
expectedFunctionConfig string
expectedResults string
wrappingAPIVersion string
wrappingAPIKind string
instance ByteReader
}
testCases := []testCase{
//
//
//
{
name: "wrapped_resource_list",
input: `apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
spec:
replicas: 1
- kind: Service
spec:
selectors:
foo: bar
`,
expectedItems: []string{
`kind: Deployment
spec:
replicas: 1
`,
`kind: Service
spec:
selectors:
foo: bar
`,
},
wrappingAPIVersion: ResourceListAPIVersion,
wrappingAPIKind: ResourceListKind,
},
//
//
//
{
name: "wrapped_resource_list_function_config",
input: `apiVersion: config.kubernetes.io/v1
kind: ResourceList
functionConfig:
foo: bar
elems:
- a
- b
- c
items:
- kind: Deployment
spec:
replicas: 1
- kind: Service
spec:
selectors:
foo: bar
`,
expectedItems: []string{
`kind: Deployment
spec:
replicas: 1
`,
`kind: Service
spec:
selectors:
foo: bar
`,
},
expectedFunctionConfig: `foo: bar
elems:
- a
- b
- c`,
wrappingAPIVersion: ResourceListAPIVersion,
wrappingAPIKind: ResourceListKind,
},
//
//
//
{
name: "wrapped_resource_list_function_config_without_items",
input: `apiVersion: config.kubernetes.io/v1
kind: ResourceList
functionConfig:
foo: bar
elems:
- a
- b
- c
`,
expectedItems: []string{},
expectedFunctionConfig: `foo: bar
elems:
- a
- b
- c`,
wrappingAPIVersion: ResourceListAPIVersion,
wrappingAPIKind: ResourceListKind,
},
//
//
//
{
name: "wrapped_list",
input: `
apiVersion: v1
kind: List
items:
- kind: Deployment
spec:
replicas: 1
- kind: Service
spec:
selectors:
foo: bar
`,
expectedItems: []string{
`
kind: Deployment
spec:
replicas: 1
`,
`
kind: Service
spec:
selectors:
foo: bar
`,
},
wrappingAPIKind: "List",
wrappingAPIVersion: "v1",
},
//
//
//
{
name: "unwrapped_items",
input: `
---
a: b # first resource
c: d
---
# second resource
e: f
g:
- h
---
---
i: j
`,
expectedItems: []string{
`a: b # first resource
c: d
metadata:
annotations:
config.kubernetes.io/index: '0'
internal.config.kubernetes.io/index: '0'
`,
`# second resource
e: f
g:
- h
metadata:
annotations:
config.kubernetes.io/index: '1'
internal.config.kubernetes.io/index: '1'
`,
`i: j
metadata:
annotations:
config.kubernetes.io/index: '2'
internal.config.kubernetes.io/index: '2'
`,
},
},
//
//
//
{
name: "omit_annotations",
input: `
---
a: b # first resource
c: d
---
# second resource
e: f
g:
- h
---
---
i: j
`,
expectedItems: []string{
`
a: b # first resource
c: d
`,
`
# second resource
e: f
g:
- h
`,
`
i: j
`,
},
instance: ByteReader{OmitReaderAnnotations: true},
},
//
//
//
{
name: "no_omit_annotations",
input: `
---
a: b # first resource
c: d
---
# second resource
e: f
g:
- h
---
---
i: j
`,
expectedItems: []string{
`
a: b # first resource
c: d
metadata:
annotations:
config.kubernetes.io/index: '0'
internal.config.kubernetes.io/index: '0'
`,
`
# second resource
e: f
g:
- h
metadata:
annotations:
config.kubernetes.io/index: '1'
internal.config.kubernetes.io/index: '1'
`,
`
i: j
metadata:
annotations:
config.kubernetes.io/index: '2'
internal.config.kubernetes.io/index: '2'
`,
},
instance: ByteReader{},
},
//
//
//
{
name: "set_annotation",
input: `
---
a: b # first resource
c: d
---
# second resource
e: f
g:
- h
---
---
i: j
`,
expectedItems: []string{
`a: b # first resource
c: d
metadata:
annotations:
foo: 'bar'
`,
`# second resource
e: f
g:
- h
metadata:
annotations:
foo: 'bar'
`,
`i: j
metadata:
annotations:
foo: 'bar'
`,
},
instance: ByteReader{
OmitReaderAnnotations: true,
SetAnnotations: map[string]string{"foo": "bar"}},
},
//
//
//
{
name: "windows_line_ending",
input: "\r\n---\r\na: b # first resource\r\nc: d\r\n---\r\n# second resource\r\ne: f\r\ng:\r\n- h\r\n---\r\n\r\n---\r\n i: j",
expectedItems: []string{
`a: b # first resource
c: d
metadata:
annotations:
foo: 'bar'
`,
`# second resource
e: f
g:
- h
metadata:
annotations:
foo: 'bar'
`,
`i: j
metadata:
annotations:
foo: 'bar'
`,
},
instance: ByteReader{
OmitReaderAnnotations: true,
SetAnnotations: map[string]string{"foo": "bar"}},
},
//
//
//
{
name: "json",
input: `
{
"a": "b",
"c": [1, 2]
}
`,
expectedItems: []string{
`
{"a": "b", "c": [1, 2], metadata: {annotations: {config.kubernetes.io/index: '0', internal.config.kubernetes.io/index: '0'}}}
`,
},
instance: ByteReader{},
},
//
//
//
{
name: "white_space_after_document_separator_should_be_ignored",
input: `
a: b
---
c: d
`,
expectedItems: []string{
`
a: b
`,
`
c: d
`,
},
instance: ByteReader{OmitReaderAnnotations: true},
},
//
//
//
{
name: "comment_after_document_separator_should_be_ignored",
input: `
a: b
--- #foo
c: d
`,
expectedItems: []string{
`
a: b
`,
`
c: d
`,
},
instance: ByteReader{OmitReaderAnnotations: true},
},
//
//
//
{
name: "anything_after_document_separator_other_than_white_space_or_comment_is_an_error",
input: `
a: b
--- foo
c: d
`,
err: "invalid document separator: --- foo",
instance: ByteReader{OmitReaderAnnotations: true},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
r := tc.instance
r.Reader = bytes.NewBufferString(tc.input)
nodes, err := r.Read()
if tc.err != "" {
if !assert.EqualError(t, err, tc.err) {
t.FailNow()
}
return
}
if !assert.NoError(t, err) {
t.FailNow()
}
// verify the contents
if !assert.Len(t, nodes, len(tc.expectedItems)) {
t.FailNow()
}
for i := range nodes {
actual, err := nodes[i].String()
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t,
strings.TrimSpace(tc.expectedItems[i]),
strings.TrimSpace(actual)) {
t.FailNow()
}
}
// verify the function config
if tc.expectedFunctionConfig != "" {
actual, err := r.FunctionConfig.String()
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t,
strings.TrimSpace(tc.expectedFunctionConfig),
strings.TrimSpace(actual)) {
t.FailNow()
}
} else if !assert.Nil(t, r.FunctionConfig) {
t.FailNow()
}
if tc.expectedResults != "" {
actual, err := r.Results.String()
actual = strings.TrimSpace(actual)
if !assert.NoError(t, err) {
t.FailNow()
}
tc.expectedResults = strings.TrimSpace(tc.expectedResults)
if !assert.Equal(t, tc.expectedResults, actual) {
t.FailNow()
}
} else if !assert.Nil(t, r.Results) {
t.FailNow()
}
if !assert.Equal(t, tc.wrappingAPIKind, r.WrappingKind) {
t.FailNow()
}
if !assert.Equal(t, tc.wrappingAPIVersion, r.WrappingAPIVersion) {
t.FailNow()
}
})
}
}
func TestFromBytes(t *testing.T) {
type expected struct {
isErr bool
sOut []string
}
testCases := map[string]struct {
input []byte
exp expected
}{
"garbage": {
input: []byte("garbageIn: garbageOut"),
exp: expected{
sOut: []string{"garbageIn: garbageOut"},
},
},
"noBytes": {
input: []byte{},
exp: expected{
sOut: []string{},
},
},
"goodJson": {
input: []byte(`
{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"winnie"}}
`),
exp: expected{
sOut: []string{`
{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "winnie"}}
`},
},
},
"goodYaml1": {
input: []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`),
exp: expected{
sOut: []string{`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`},
},
},
"goodYaml2": {
input: []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
---
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`),
exp: expected{
sOut: []string{`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`, `
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`},
},
},
"localConfigYaml": {
input: []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie-skip
annotations:
# this annotation causes the Resource to be ignored by kustomize
config.kubernetes.io/local-config: ""
---
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`),
exp: expected{
sOut: []string{`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie-skip
annotations:
# this annotation causes the Resource to be ignored by kustomize
config.kubernetes.io/local-config: ""
`,
`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`},
},
},
"garbageInOneOfTwoObjects": {
input: []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
---
WOOOOOOOOOOOOOOOOOOOOOOOOT: woot
`),
exp: expected{
sOut: []string{`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`,
`
WOOOOOOOOOOOOOOOOOOOOOOOOT: woot
`},
},
},
"emptyObjects": {
input: []byte(`
---
#a comment
---
`),
exp: expected{
sOut: []string{},
},
},
"Missing .metadata.name in object": {
input: []byte(`
apiVersion: v1
kind: Namespace
metadata:
annotations:
foo: bar
`),
exp: expected{
sOut: []string{`
apiVersion: v1
kind: Namespace
metadata:
annotations:
foo: bar
`},
},
},
"nil value in list": {
input: []byte(`
apiVersion: builtin
kind: ConfigMapGenerator
metadata:
name: kube100-site
labels:
app: web
testList:
- testA
-
`),
exp: expected{
isErr: true,
},
},
"List": {
input: []byte(`
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`),
exp: expected{
sOut: []string{`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`, `
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`},
},
},
"ConfigMapList": {
input: []byte(`
apiVersion: v1
kind: ConfigMapList
items:
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`),
exp: expected{
sOut: []string{`
apiVersion: v1
kind: ConfigMapList
items:
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`},
},
},
"mergeAnchors": {
input: []byte(`
apiVersion: v1
kind: DeploymentList
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-a
spec: &hostAliases
template:
spec:
hostAliases:
- hostnames:
- a.example.com
ip: 8.8.8.8
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-b
spec:
<<: *hostAliases
`),
exp: expected{
sOut: []string{`
apiVersion: v1
kind: DeploymentList
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-a
spec:
template:
spec:
hostAliases:
- hostnames:
- a.example.com
ip: 8.8.8.8
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-b
spec:
template:
spec:
hostAliases:
- hostnames:
- a.example.com
ip: 8.8.8.8
`},
},
},
"listWithAnchors": {
input: []byte(`
apiVersion: v1
kind: DeploymentList
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-a
spec: &hostAliases
template:
spec:
hostAliases:
- hostnames:
- a.example.com
ip: 8.8.8.8
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-b
spec: *hostAliases
`),
exp: expected{
sOut: []string{`
apiVersion: v1
kind: DeploymentList
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-a
spec:
template:
spec:
hostAliases:
- hostnames:
- a.example.com
ip: 8.8.8.8
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-b
spec:
template:
spec:
hostAliases:
- hostnames:
- a.example.com
ip: 8.8.8.8
`},
},
},
}
for n := range testCases {
tc := testCases[n]
t.Run(n, func(t *testing.T) {
rNodes, err := FromBytes(tc.input)
if err != nil {
assert.True(t, tc.exp.isErr)
return
}
assert.False(t, tc.exp.isErr)
assert.Equal(t, len(tc.exp.sOut), len(rNodes))
for i, n := range rNodes {
json, err := n.String()
assert.NoError(t, err)
assert.Equal(
t, strings.TrimSpace(tc.exp.sOut[i]),
strings.TrimSpace(json), n)
}
})
}
}
// Show the low level (go-yaml) representation of a small doc with a
// YAML anchor and alias after reading it with anchor expansion on or off.
func TestByteReader_AnchorsAweigh(t *testing.T) {
const input = `
data:
color: &color-used blue
feeling: *color-used
`
var rNode *yaml.RNode
{
rNodes, err := (&ByteReader{
OmitReaderAnnotations: true,
AnchorsAweigh: false,
Reader: bytes.NewBuffer([]byte(input)),
}).Read()
assert.NoError(t, err)
assert.Equal(t, 1, len(rNodes))
rNode = rNodes[0]
}
// Confirm internal representation.
{
yNode := rNode.YNode()
// The high level object is a map of "data" to some value.
assert.Equal(t, yaml.NodeTagMap, yNode.Tag)
yNodes := yNode.Content
assert.Equal(t, 2, len(yNodes))
// Confirm that the key is "data".
assert.Equal(t, yaml.NodeTagString, yNodes[0].Tag)
assert.Equal(t, "data", yNodes[0].Value)
assert.Equal(t, yaml.NodeTagMap, yNodes[1].Tag)
// The value of the "data" key.
yNodes = yNodes[1].Content
// Expect two name-value pairs.
assert.Equal(t, 4, len(yNodes))
assert.Equal(t, yaml.ScalarNode, yNodes[0].Kind)
assert.Equal(t, yaml.NodeTagString, yNodes[0].Tag)
assert.Equal(t, "color", yNodes[0].Value)
assert.Empty(t, yNodes[0].Anchor)
assert.Nil(t, yNodes[0].Alias)
assert.Equal(t, yaml.ScalarNode, yNodes[1].Kind)
assert.Equal(t, yaml.NodeTagString, yNodes[1].Tag)
assert.Equal(t, "blue", yNodes[1].Value)
assert.Equal(t, "color-used", yNodes[1].Anchor)
assert.Nil(t, yNodes[1].Alias)
assert.Equal(t, yaml.ScalarNode, yNodes[2].Kind)
assert.Equal(t, yaml.NodeTagString, yNodes[2].Tag)
assert.Equal(t, "feeling", yNodes[2].Value)
assert.Empty(t, yNodes[2].Anchor)
assert.Nil(t, yNodes[2].Alias)
assert.Equal(t, yaml.AliasNode, yNodes[3].Kind)
assert.Empty(t, yNodes[3].Tag)
assert.Equal(t, "color-used", yNodes[3].Value)
assert.Empty(t, yNodes[3].Anchor)
assert.NotNil(t, yNodes[3].Alias)
}
str, err := rNode.String()
assert.NoError(t, err)
// The string version matches the input (it still has anchors and aliases).
assert.Equal(t, strings.TrimSpace(input), strings.TrimSpace(str))
// Now do same thing again, but this time set AnchorsAweigh = true.
{
rNodes, err := (&ByteReader{
OmitReaderAnnotations: true,
AnchorsAweigh: true,
Reader: bytes.NewBuffer([]byte(input)),
}).Read()
assert.NoError(t, err)
assert.Equal(t, 1, len(rNodes))
rNode = rNodes[0]
}
// Again make assertions on the internals.
{
yNode := rNode.YNode()
assert.Equal(t, yaml.NodeTagMap, yNode.Tag)
yNodes := yNode.Content
assert.Equal(t, 2, len(yNodes))
assert.Equal(t, yaml.NodeTagString, yNodes[0].Tag)
assert.Equal(t, "data", yNodes[0].Value)
assert.Equal(t, yaml.NodeTagMap, yNodes[1].Tag)
yNodes = yNodes[1].Content
assert.Equal(t, 4, len(yNodes))
assert.Equal(t, yaml.ScalarNode, yNodes[0].Kind)
assert.Equal(t, yaml.NodeTagString, yNodes[0].Tag)
assert.Equal(t, "color", yNodes[0].Value)
assert.Empty(t, yNodes[0].Anchor)
assert.Nil(t, yNodes[0].Alias)
assert.Equal(t, yaml.ScalarNode, yNodes[1].Kind)
assert.Equal(t, yaml.NodeTagString, yNodes[1].Tag)
assert.Equal(t, "blue", yNodes[1].Value)
assert.Empty(t, yNodes[1].Anchor)
assert.Nil(t, yNodes[1].Alias)
assert.Equal(t, yaml.ScalarNode, yNodes[2].Kind)
assert.Equal(t, yaml.NodeTagString, yNodes[2].Tag)
assert.Equal(t, "feeling", yNodes[2].Value)
assert.Empty(t, yNodes[2].Anchor)
assert.Nil(t, yNodes[2].Alias)
assert.Equal(t, yaml.ScalarNode, yNodes[3].Kind)
assert.Equal(t, yaml.NodeTagString, yNodes[3].Tag)
assert.Equal(t, "blue", yNodes[3].Value)
assert.Empty(t, yNodes[3].Anchor)
assert.Nil(t, yNodes[3].Alias)
}
str, err = rNode.String()
assert.NoError(t, err)
// This time, the alias has been replaced with the anchor definition.
assert.Equal(t, strings.TrimSpace(`
data:
color: blue
feeling: blue
`), strings.TrimSpace(str))
}
// TestByteReader_AddSeqIndentAnnotation tests if the internal.config.kubernetes.io/seqindent
// annotation is added to resources appropriately
func TestByteReader_AddSeqIndentAnnotation(t *testing.T) {
type testCase struct {
name string
err string
input string
expectedAnnoValue string
OmitReaderAnnotations bool
}
testCases := []testCase{
{
name: "read with wide indentation",
input: `apiVersion: apps/v1
kind: Deployment
spec:
- foo
- bar
- baz
`,
expectedAnnoValue: "wide",
},
{
name: "read with compact indentation",
input: `apiVersion: apps/v1
kind: Deployment
spec:
- foo
- bar
- baz
`,
expectedAnnoValue: "compact",
},
{
name: "read with mixed indentation, wide wins",
input: `apiVersion: apps/v1
kind: Deployment
spec:
- foo
- bar
- baz
env:
- foo
- bar
`,
expectedAnnoValue: "wide",
},
{
name: "read with mixed indentation, compact wins",
input: `apiVersion: apps/v1
kind: Deployment
spec:
- foo
- bar
- baz
env:
- foo
- bar
`,
expectedAnnoValue: "compact",
},
{
name: "error if conflicting options",
input: `apiVersion: apps/v1
kind: Deployment
spec:
- foo
- bar
- baz
env:
- foo
- bar
`,
OmitReaderAnnotations: true,
err: `"PreserveSeqIndent" option adds a reader annotation, please set "OmitReaderAnnotations" to false`,
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
rNodes, err := (&ByteReader{
OmitReaderAnnotations: tc.OmitReaderAnnotations,
PreserveSeqIndent: true,
Reader: bytes.NewBuffer([]byte(tc.input)),
}).Read()
if tc.err != "" {
assert.Error(t, err)
assert.Equal(t, tc.err, err.Error())
return
}
assert.NoError(t, err)
actual := rNodes[0].GetAnnotations()[kioutil.SeqIndentAnnotation]
assert.Equal(t, tc.expectedAnnoValue, actual)
})
}
}