Introduce some RNode validation methods.

This commit is contained in:
jregan
2020-11-16 11:27:46 -08:00
parent b2ba82a0bd
commit e1c3caeba6
2 changed files with 271 additions and 14 deletions

View File

@@ -7,6 +7,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strconv"
"strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/errors"
@@ -456,8 +458,8 @@ func (rn *RNode) Elements() ([]*RNode, error) {
return elements, nil return elements, nil
} }
// ElementValues returns a list of all observed values for a given field name in a // ElementValues returns a list of all observed values for a given field name
// list of elements. // in a list of elements.
// Returns error for non-SequenceNodes. // Returns error for non-SequenceNodes.
func (rn *RNode) ElementValues(key string) ([]string, error) { func (rn *RNode) ElementValues(key string) ([]string, error) {
if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil { if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil {
@@ -600,6 +602,55 @@ func (rn *RNode) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// GetValidatedMetadata returns metadata after subjecting it to some tests.
func (rn *RNode) GetValidatedMetadata() (ResourceMeta, error) {
m, err := rn.GetMeta()
if err != nil {
return m, err
}
if m.Kind == "" {
return m, fmt.Errorf("missing kind in object %v", m)
}
if strings.HasSuffix(m.Kind, "List") {
// A list doesn't require a name.
return m, nil
}
if m.NameMeta.Name == "" {
return m, fmt.Errorf("missing metadata.name in object %v", m)
}
return m, nil
}
// HasNilEntryInList returns true if the RNode contains a list which has
// a nil item, along with the path to the missing item.
// TODO(broken): This was copied from
// api/k8sdeps/kunstruct/factory.go//checkListItemNil
// and doesn't do what it claims to do (see TODO in unit test and pr 1513).
func (rn *RNode) HasNilEntryInList() (bool, string) {
return hasNilEntryInList(rn.value)
}
func hasNilEntryInList(in interface{}) (bool, string) {
switch v := in.(type) {
case map[string]interface{}:
for key, s := range v {
if result, path := hasNilEntryInList(s); result {
return result, key + "/" + path
}
}
case []interface{}:
for index, s := range v {
if s == nil {
return true, ""
}
if result, path := hasNilEntryInList(s); result {
return result, "[" + strconv.Itoa(index) + "]/" + path
}
}
}
return false, ""
}
func FromMap(m map[string]interface{}) (*RNode, error) { func FromMap(m map[string]interface{}) (*RNode, error) {
c, err := Marshal(m) c, err := Marshal(m)
if err != nil { if err != nil {

View File

@@ -11,6 +11,212 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestRNodeHasNilEntryInList(t *testing.T) {
testConfigMap := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "winnie",
},
}
type resultExpected struct {
hasNil bool
path string
}
testCases := map[string]struct {
theMap map[string]interface{}
rsExp resultExpected
}{
"actuallyNil": {
theMap: nil,
rsExp: resultExpected{},
},
"empty": {
theMap: map[string]interface{}{},
rsExp: resultExpected{},
},
"list": {
theMap: map[string]interface{}{
"apiVersion": "v1",
"kind": "List",
"items": []interface{}{
testConfigMap,
testConfigMap,
},
},
rsExp: resultExpected{},
},
"listWithNil": {
theMap: map[string]interface{}{
"apiVersion": "v1",
"kind": "List",
"items": []interface{}{
testConfigMap,
nil,
},
},
rsExp: resultExpected{
hasNil: false, // TODO: This should be true.
path: "this/should/be/non-empty",
},
},
}
for n := range testCases {
tc := testCases[n]
t.Run(n, func(t *testing.T) {
rn, err := FromMap(tc.theMap)
if !assert.NoError(t, err) {
t.FailNow()
}
hasNil, path := rn.HasNilEntryInList()
if tc.rsExp.hasNil {
if !assert.True(t, hasNil) {
t.FailNow()
}
if !assert.Equal(t, tc.rsExp.path, path) {
t.FailNow()
}
} else {
if !assert.False(t, hasNil) {
t.FailNow()
}
if !assert.Empty(t, path) {
t.FailNow()
}
}
})
}
}
func TestRNodeGetValidatedMetadata(t *testing.T) {
testConfigMap := map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "winnie",
},
}
type resultExpected struct {
out ResourceMeta
errMsg string
}
testCases := map[string]struct {
theMap map[string]interface{}
rsExp resultExpected
}{
"actuallyNil": {
theMap: nil,
rsExp: resultExpected{
errMsg: "missing Resource metadata",
},
},
"empty": {
theMap: map[string]interface{}{},
rsExp: resultExpected{
errMsg: "missing Resource metadata",
},
},
"mostlyEmpty": {
theMap: map[string]interface{}{
"hey": "there",
},
rsExp: resultExpected{
errMsg: "missing Resource metadata",
},
},
"noNameConfigMap": {
theMap: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
},
rsExp: resultExpected{
errMsg: "missing metadata.name",
},
},
"configmap": {
theMap: testConfigMap,
rsExp: resultExpected{
out: ResourceMeta{
TypeMeta: TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: ObjectMeta{
NameMeta: NameMeta{
Name: "winnie",
},
},
},
},
},
"list": {
theMap: map[string]interface{}{
"apiVersion": "v1",
"kind": "List",
"items": []interface{}{
testConfigMap,
testConfigMap,
},
},
rsExp: resultExpected{
out: ResourceMeta{
TypeMeta: TypeMeta{
APIVersion: "v1",
Kind: "List",
},
},
},
},
"configmaplist": {
theMap: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMapList",
"items": []interface{}{
testConfigMap,
testConfigMap,
},
},
rsExp: resultExpected{
out: ResourceMeta{
TypeMeta: TypeMeta{
APIVersion: "v1",
Kind: "ConfigMapList",
},
},
},
},
}
for n := range testCases {
tc := testCases[n]
t.Run(n, func(t *testing.T) {
rn, err := FromMap(tc.theMap)
if !assert.NoError(t, err) {
t.FailNow()
}
m, err := rn.GetValidatedMetadata()
if tc.rsExp.errMsg == "" {
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t, tc.rsExp.out, m) {
t.FailNow()
}
} else {
if !assert.Error(t, err) {
t.FailNow()
}
if !assert.Contains(t, err.Error(), tc.rsExp.errMsg) {
t.FailNow()
}
}
})
}
}
func TestRNodeFromMap(t *testing.T) { func TestRNodeFromMap(t *testing.T) {
testConfigMap := map[string]interface{}{ testConfigMap := map[string]interface{}{
"apiVersion": "v1", "apiVersion": "v1",
@@ -19,25 +225,25 @@ func TestRNodeFromMap(t *testing.T) {
"name": "winnie", "name": "winnie",
}, },
} }
type thingExpected struct { type resultExpected struct {
out string out string
err error err error
} }
testCases := map[string]struct { testCases := map[string]struct {
theMap map[string]interface{} theMap map[string]interface{}
expected thingExpected rsExp resultExpected
}{ }{
"actuallyNil": { "actuallyNil": {
theMap: nil, theMap: nil,
expected: thingExpected{ rsExp: resultExpected{
out: `{}`, out: `{}`,
err: nil, err: nil,
}, },
}, },
"empty": { "empty": {
theMap: map[string]interface{}{}, theMap: map[string]interface{}{},
expected: thingExpected{ rsExp: resultExpected{
out: `{}`, out: `{}`,
err: nil, err: nil,
}, },
@@ -46,14 +252,14 @@ func TestRNodeFromMap(t *testing.T) {
theMap: map[string]interface{}{ theMap: map[string]interface{}{
"hey": "there", "hey": "there",
}, },
expected: thingExpected{ rsExp: resultExpected{
out: `hey: there`, out: `hey: there`,
err: nil, err: nil,
}, },
}, },
"configmap": { "configmap": {
theMap: testConfigMap, theMap: testConfigMap,
expected: thingExpected{ rsExp: resultExpected{
out: ` out: `
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
@@ -72,7 +278,7 @@ metadata:
testConfigMap, testConfigMap,
}, },
}, },
expected: thingExpected{ rsExp: resultExpected{
out: ` out: `
apiVersion: v1 apiVersion: v1
items: items:
@@ -98,7 +304,7 @@ kind: List
testConfigMap, testConfigMap,
}, },
}, },
expected: thingExpected{ rsExp: resultExpected{
out: ` out: `
apiVersion: v1 apiVersion: v1
items: items:
@@ -121,12 +327,12 @@ kind: ConfigMapList
tc := testCases[n] tc := testCases[n]
t.Run(n, func(t *testing.T) { t.Run(n, func(t *testing.T) {
rn, err := FromMap(tc.theMap) rn, err := FromMap(tc.theMap)
if tc.expected.err == nil { if tc.rsExp.err == nil {
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.FailNow() t.FailNow()
} }
if !assert.Equal(t, if !assert.Equal(t,
strings.TrimSpace(tc.expected.out), strings.TrimSpace(tc.rsExp.out),
strings.TrimSpace(rn.MustString())) { strings.TrimSpace(rn.MustString())) {
t.FailNow() t.FailNow()
} }
@@ -134,7 +340,7 @@ kind: ConfigMapList
if !assert.Error(t, err) { if !assert.Error(t, err) {
t.FailNow() t.FailNow()
} }
if !assert.Equal(t, tc.expected.err, err) { if !assert.Equal(t, tc.rsExp.err, err) {
t.FailNow() t.FailNow()
} }
} }