Files
kustomize/kyaml/kio/byteio_reader_test.go
Karl Isenberg 43868688d5 Use require for Error and NoError
Assert keeps going after failure, but require immediately fails
the tests, making it easier to find the output related to the test
failure, rather than having to comb through a bunch of subsequent
assertion failures. For equality tests, we may or may not want to
continue, but for error checks we almost always want to immediately
fail the test. Exceptions can be changed as-needed.
2024-03-20 13:19:18 -07:00

1106 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"
"github.com/stretchr/testify/require"
. "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()
require.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()
require.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()
require.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()
require.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()
require.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 != "" {
require.Error(t, err)
assert.Equal(t, tc.err, err.Error())
return
}
require.NoError(t, err)
actual := rNodes[0].GetAnnotations()[kioutil.SeqIndentAnnotation]
assert.Equal(t, tc.expectedAnnoValue, actual)
})
}
}