Move ks8deps to api for now.

This commit is contained in:
jregan
2019-10-20 16:30:52 -07:00
parent 22d07ed37d
commit fddde81f9c
32 changed files with 23 additions and 25 deletions

View File

@@ -0,0 +1,138 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kunstruct
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/k8sdeps/configmapandsecret"
"sigs.k8s.io/kustomize/v3/api/types"
)
// KunstructuredFactoryImpl hides construction using apimachinery types.
type KunstructuredFactoryImpl struct {
hasher *kustHash
}
var _ ifc.KunstructuredFactory = &KunstructuredFactoryImpl{}
// NewKunstructuredFactoryImpl returns a factory.
func NewKunstructuredFactoryImpl() ifc.KunstructuredFactory {
return &KunstructuredFactoryImpl{hasher: NewKustHash()}
}
// Hasher returns a kunstructured hasher
// input: kunstructured; output: string hash.
func (kf *KunstructuredFactoryImpl) Hasher() ifc.KunstructuredHasher {
return kf.hasher
}
// SliceFromBytes returns a slice of Kunstructured.
func (kf *KunstructuredFactoryImpl) SliceFromBytes(
in []byte) ([]ifc.Kunstructured, error) {
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(in), 1024)
var result []ifc.Kunstructured
var err error
for err == nil || isEmptyYamlError(err) {
var out unstructured.Unstructured
err = decoder.Decode(&out)
if err == nil {
if len(out.Object) == 0 {
continue
}
err = kf.validate(out)
if err != nil {
return nil, err
}
result = append(result, &UnstructAdapter{Unstructured: out})
}
}
if err != io.EOF {
return nil, err
}
return result, nil
}
func isEmptyYamlError(err error) bool {
return strings.Contains(err.Error(), "is missing in 'null'")
}
// FromMap returns an instance of Kunstructured.
func (kf *KunstructuredFactoryImpl) FromMap(
m map[string]interface{}) ifc.Kunstructured {
return &UnstructAdapter{Unstructured: unstructured.Unstructured{Object: m}}
}
// MakeConfigMap returns an instance of Kunstructured for ConfigMap
func (kf *KunstructuredFactoryImpl) MakeConfigMap(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
args *types.ConfigMapArgs) (ifc.Kunstructured, error) {
o, err := configmapandsecret.NewFactory(
kvLdr, options).MakeConfigMap(args)
if err != nil {
return nil, err
}
return NewKunstructuredFromObject(o)
}
// MakeSecret returns an instance of Kunstructured for Secret
func (kf *KunstructuredFactoryImpl) MakeSecret(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
args *types.SecretArgs) (ifc.Kunstructured, error) {
o, err := configmapandsecret.NewFactory(
kvLdr, options).MakeSecret(args)
if err != nil {
return nil, err
}
return NewKunstructuredFromObject(o)
}
// validate validates that u has kind and name
// except for kind `List`, which doesn't require a name
func (kf *KunstructuredFactoryImpl) validate(u unstructured.Unstructured) error {
kind := u.GetKind()
if kind == "" {
return fmt.Errorf("missing kind in object %v", u)
} else if strings.HasSuffix(kind, "List") {
return nil
}
if u.GetName() == "" {
return fmt.Errorf("missing metadata.name in object %v", u)
}
if result, path := checkListItemNil(u.Object); result {
return fmt.Errorf("empty item at %v in object %v", path, u)
}
return nil
}
func checkListItemNil(in interface{}) (bool, string) {
switch v := in.(type) {
case map[string]interface{}:
for key, s := range v {
if result, path := checkListItemNil(s); result {
return result, key + "/" + path
}
}
case []interface{}:
for index, s := range v {
if s == nil {
return true, ""
}
if result, path := checkListItemNil(s); result {
return result, "[" + strconv.Itoa(index) + "]/" + path
}
}
}
return false, ""
}

View File

@@ -0,0 +1,218 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kunstruct
import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/v3/api/ifc"
)
func TestSliceFromBytes(t *testing.T) {
factory := NewKunstructuredFactoryImpl()
testConfigMap := factory.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "winnie",
},
})
testList := factory.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "List",
"items": []interface{}{
testConfigMap.Map(),
testConfigMap.Map(),
},
})
testConfigMapList := factory.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMapList",
"items": []interface{}{
testConfigMap.Map(),
testConfigMap.Map(),
},
})
tests := []struct {
name string
input []byte
expectedOut []ifc.Kunstructured
expectedErr bool
}{
{
name: "garbage",
input: []byte("garbageIn: garbageOut"),
expectedOut: []ifc.Kunstructured{},
expectedErr: true,
},
{
name: "noBytes",
input: []byte{},
expectedOut: []ifc.Kunstructured{},
expectedErr: false,
},
{
name: "goodJson",
input: []byte(`
{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"winnie"}}
`),
expectedOut: []ifc.Kunstructured{testConfigMap},
expectedErr: false,
},
{
name: "goodYaml1",
input: []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`),
expectedOut: []ifc.Kunstructured{testConfigMap},
expectedErr: false,
},
{
name: "goodYaml2",
input: []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
---
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`),
expectedOut: []ifc.Kunstructured{testConfigMap, testConfigMap},
expectedErr: false,
},
{
name: "garbageInOneOfTwoObjects",
input: []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
---
WOOOOOOOOOOOOOOOOOOOOOOOOT: woot
`),
expectedOut: []ifc.Kunstructured{},
expectedErr: true,
},
{
name: "emptyObjects",
input: []byte(`
---
#a comment
---
`),
expectedOut: []ifc.Kunstructured{},
expectedErr: false,
},
{
name: "Missing .metadata.name in object",
input: []byte(`
apiVersion: v1
kind: Namespace
metadata:
annotations:
foo: bar
`),
expectedOut: nil,
expectedErr: true,
},
{
name: "nil value in list",
input: []byte(`
apiVersion: builtin
kind: ConfigMapGenerator
metadata:
name: kube100-site
labels:
app: web
testList:
- testA
-
`),
expectedOut: nil,
expectedErr: true,
},
{
name: "List",
input: []byte(`
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`),
expectedOut: []ifc.Kunstructured{testList},
expectedErr: false,
},
{
name: "ConfigMapList",
input: []byte(`
apiVersion: v1
kind: ConfigMapList
items:
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
`),
expectedOut: []ifc.Kunstructured{testConfigMapList},
expectedErr: false,
},
}
for _, test := range tests {
rs, err := factory.SliceFromBytes(test.input)
if test.expectedErr && err == nil {
t.Fatalf("%v: should return error", test.name)
}
if !test.expectedErr && err != nil {
t.Fatalf("%v: unexpected error: %s", test.name, err)
}
if len(rs) != len(test.expectedOut) {
t.Fatalf("%s: length mismatch %d != %d",
test.name, len(rs), len(test.expectedOut))
}
for i := range rs {
if !reflect.DeepEqual(test.expectedOut[i], rs[i]) {
t.Fatalf("%s: Got: %v\nexpected:%v",
test.name, test.expectedOut[i], rs[i])
}
}
}
}

View File

@@ -0,0 +1,123 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kunstruct
import (
"encoding/json"
"fmt"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/kustomize/v3/api/hasher"
"sigs.k8s.io/kustomize/v3/api/ifc"
)
// kustHash computes a hash of an unstructured object.
type kustHash struct{}
// NewKustHash returns a kustHash object
func NewKustHash() *kustHash {
return &kustHash{}
}
// Hash returns a hash of either a ConfigMap or a Secret
func (h *kustHash) Hash(m ifc.Kunstructured) (string, error) {
u := unstructured.Unstructured{
Object: m.Map(),
}
kind := u.GetKind()
switch kind {
case "ConfigMap":
cm, err := unstructuredToConfigmap(u)
if err != nil {
return "", err
}
return configMapHash(cm)
case "Secret":
sec, err := unstructuredToSecret(u)
if err != nil {
return "", err
}
return secretHash(sec)
default:
return "", fmt.Errorf(
"type %s is not supported for hashing in %v",
kind, m.Map())
}
}
// configMapHash returns a hash of the ConfigMap.
// The Data, Kind, and Name are taken into account.
func configMapHash(cm *v1.ConfigMap) (string, error) {
encoded, err := encodeConfigMap(cm)
if err != nil {
return "", err
}
h, err := hasher.Encode(hasher.Hash(encoded))
if err != nil {
return "", err
}
return h, nil
}
// SecretHash returns a hash of the Secret.
// The Data, Kind, Name, and Type are taken into account.
func secretHash(sec *v1.Secret) (string, error) {
encoded, err := encodeSecret(sec)
if err != nil {
return "", err
}
h, err := hasher.Encode(hasher.Hash(encoded))
if err != nil {
return "", err
}
return h, nil
}
// encodeConfigMap encodes a ConfigMap.
// Data, Kind, and Name are taken into account.
func encodeConfigMap(cm *v1.ConfigMap) (string, error) {
// json.Marshal sorts the keys in a stable order in the encoding
m := map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data}
if len(cm.BinaryData) > 0 {
m["binaryData"] = cm.BinaryData
}
data, err := json.Marshal(m)
if err != nil {
return "", err
}
return string(data), nil
}
// encodeSecret encodes a Secret.
// Data, Kind, Name, and Type are taken into account.
func encodeSecret(sec *v1.Secret) (string, error) {
// json.Marshal sorts the keys in a stable order in the encoding
data, err := json.Marshal(map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data})
if err != nil {
return "", err
}
return string(data), nil
}
func unstructuredToConfigmap(u unstructured.Unstructured) (*v1.ConfigMap, error) {
marshaled, err := json.Marshal(u.Object)
if err != nil {
return nil, err
}
var out v1.ConfigMap
err = json.Unmarshal(marshaled, &out)
return &out, err
}
func unstructuredToSecret(u unstructured.Unstructured) (*v1.Secret, error) {
marshaled, err := json.Marshal(u.Object)
if err != nil {
return nil, err
}
var out v1.Secret
err = json.Unmarshal(marshaled, &out)
return &out, err
}

View File

@@ -0,0 +1,180 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kunstruct
import (
"reflect"
"strings"
"testing"
"k8s.io/api/core/v1"
)
func TestConfigMapHash(t *testing.T) {
cases := []struct {
desc string
cm *v1.ConfigMap
hash string
err string
}{
// empty map
{"empty data", &v1.ConfigMap{Data: map[string]string{}, BinaryData: map[string][]byte{}}, "42745tchd9", ""},
// one key
{"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, "9g67k2htb6", ""},
// three keys (tests sorting order)
{"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, "f5h7t85m9b", ""},
// empty binary data map
{"empty binary data", &v1.ConfigMap{BinaryData: map[string][]byte{}}, "dk855m5d49", ""},
// one key with binary data
{"one key with binary data", &v1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}}, "mk79584b8c", ""},
// three keys with binary data (tests sorting order)
{"three keys with binary data", &v1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "t458mc6db2", ""},
// two keys, one with string and another with binary data
{"two keys with one each", &v1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}}, "698h7c7t9m", ""},
}
for _, c := range cases {
h, err := configMapHash(c.cm)
if SkipRest(t, c.desc, err, c.err) {
continue
}
if c.hash != h {
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
}
}
}
func TestSecretHash(t *testing.T) {
cases := []struct {
desc string
secret *v1.Secret
hash string
err string
}{
// empty map
{"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, "t75bgf6ctb", ""},
// one key
{"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, "74bd68bm66", ""},
// three keys (tests sorting order)
{"three keys", &v1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "dgcb6h9tmk", ""},
}
for _, c := range cases {
h, err := secretHash(c.secret)
if SkipRest(t, c.desc, err, c.err) {
continue
}
if c.hash != h {
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
}
}
}
func TestEncodeConfigMap(t *testing.T) {
cases := []struct {
desc string
cm *v1.ConfigMap
expect string
err string
}{
// empty map
{"empty data", &v1.ConfigMap{Data: map[string]string{}}, `{"data":{},"kind":"ConfigMap","name":""}`, ""},
// one key
{"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
// three keys (tests sorting order)
{"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}},
`{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}`, ""},
// empty binary map
{"empty data", &v1.ConfigMap{BinaryData: map[string][]byte{}}, `{"data":null,"kind":"ConfigMap","name":""}`, ""},
// one key with binary data
{"one key", &v1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}},
`{"binaryData":{"one":""},"data":null,"kind":"ConfigMap","name":""}`, ""},
// three keys with binary data (tests sorting order)
{"three keys", &v1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}},
`{"binaryData":{"one":"","three":"Mw==","two":"Mg=="},"data":null,"kind":"ConfigMap","name":""}`, ""},
// two keys, one string and one binary values
{"two keys with one each", &v1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}},
`{"binaryData":{"two":""},"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
}
for _, c := range cases {
s, err := encodeConfigMap(c.cm)
if SkipRest(t, c.desc, err, c.err) {
continue
}
if s != c.expect {
t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.cm)
}
}
}
func TestEncodeSecret(t *testing.T) {
cases := []struct {
desc string
secret *v1.Secret
expect string
err string
}{
// empty map
{"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, `{"data":{},"kind":"Secret","name":"","type":"my-type"}`, ""},
// one key
{"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""},
// three keys (tests sorting order) - note json.Marshal base64 encodes the values because they come in as []byte
{"three keys", &v1.Secret{
Type: "my-type",
Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")},
},
`{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}`, ""},
}
for _, c := range cases {
s, err := encodeSecret(c.secret)
if SkipRest(t, c.desc, err, c.err) {
continue
}
if s != c.expect {
t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.secret)
}
}
}
// warn devs who change types that they might have to update a hash function
// not perfect, as it only checks the number of top-level fields
func TestTypeStability(t *testing.T) {
errfmt := `case %q, expected %d fields but got %d
Depending on the field(s) you added, you may need to modify the hash function for this type.
To guide you: the hash function targets fields that comprise the contents of objects,
not their metadata (e.g. the Data of a ConfigMap, but nothing in ObjectMeta).
`
cases := []struct {
typeName string
obj interface{}
expect int
}{
{"ConfigMap", v1.ConfigMap{}, 4},
{"Secret", v1.Secret{}, 5},
}
for _, c := range cases {
val := reflect.ValueOf(c.obj)
if num := val.NumField(); c.expect != num {
t.Errorf(errfmt, c.typeName, c.expect, num)
}
}
}
// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen,
// and logs the appropriate error on the test object.
// The return value indicates whether we should skip the rest of the test case due to the error result.
func SkipRest(t *testing.T, desc string, err error, contains string) bool {
if err != nil {
if len(contains) == 0 {
t.Errorf("case %q, expect nil error but got %q", desc, err.Error())
} else if !strings.Contains(err.Error(), contains) {
t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error())
}
return true
} else if len(contains) > 0 {
t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains)
return true
}
return false
}

View File

@@ -0,0 +1,95 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package kunstruct provides unstructured from api machinery and factory for creating unstructured
package kunstruct
import (
"fmt"
"strconv"
"strings"
)
// A PathSection contains a list of nested fields, which may end with an
// indexable value. For instance, foo.bar resolves to a PathSection with 2
// fields and no index, while foo[0].bar resolves to two path sections, the
// first containing the field foo and the index 0, and the second containing
// the field bar, with no index. The latter PathSection references the bar
// field of the first item in the foo list
type PathSection struct {
fields []string
idx int
}
func newPathSection() PathSection {
return PathSection{idx: -1}
}
func appendNonEmpty(section *PathSection, field string) {
if len(field) != 0 {
section.fields = append(section.fields, field)
}
}
func parseFields(path string) (result []PathSection, err error) {
section := newPathSection()
if !strings.Contains(path, "[") {
section.fields = strings.Split(path, ".")
result = append(result, section)
return result, nil
}
start := 0
insideParentheses := false
for i, c := range path {
switch c {
case '.':
if !insideParentheses {
appendNonEmpty(&section, path[start:i])
start = i + 1
}
case '[':
if !insideParentheses {
appendNonEmpty(&section, path[start:i])
start = i + 1
insideParentheses = true
} else {
return nil, fmt.Errorf("nested parentheses are not allowed: %s", path)
}
case ']':
if insideParentheses {
// Assign this index to the current
// PathSection, save it to the result, then begin
// a new PathSection
tmpIdx, err := strconv.Atoi(path[start:i])
if err == nil {
// We have detected an integer so an array.
section.idx = tmpIdx
} else {
// We have detected the downwardapi syntax
appendNonEmpty(&section, path[start:i])
}
result = append(result, section)
section = newPathSection()
start = i + 1
insideParentheses = false
} else {
return nil, fmt.Errorf("invalid field path %s", path)
}
}
}
if start < len(path)-1 {
appendNonEmpty(&section, path[start:])
result = append(result, section)
}
for _, section := range result {
for i, f := range section.fields {
if strings.HasPrefix(f, "\"") || strings.HasPrefix(f, "'") {
section.fields[i] = strings.Trim(f, "\"'")
}
}
}
return result, nil
}

View File

@@ -0,0 +1,152 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kunstruct
import (
"reflect"
"testing"
)
type PathSectionSlice []PathSection
func buildPath(idx int, fields ...string) PathSectionSlice {
return PathSectionSlice{PathSection{fields: fields, idx: idx}}
}
func (a PathSectionSlice) addSection(idx int, fields ...string) PathSectionSlice {
return append(a, PathSection{fields: fields, idx: idx})
}
func TestParseField(t *testing.T) {
tests := []struct {
name string
pathToField string
expectedValue []PathSection
errorExpected bool
errorMsg string
}{
{
name: "oneField",
pathToField: "Kind",
expectedValue: buildPath(-1, "Kind"),
errorExpected: false,
},
{
name: "twoFields",
pathToField: "metadata.name",
expectedValue: buildPath(-1, "metadata", "name"),
errorExpected: false,
},
{
name: "threeFields",
pathToField: "spec.ports.port",
expectedValue: buildPath(-1, "spec", "ports", "port"),
errorExpected: false,
},
{
name: "empty",
pathToField: "",
expectedValue: buildPath(-1, ""),
errorExpected: false,
},
{
name: "validStringIndex",
pathToField: "that[1]",
expectedValue: buildPath(1, "that"),
errorExpected: false,
},
{
name: "sliceInSlice",
pathToField: "that[1][0]",
expectedValue: buildPath(1, "that").addSection(0),
errorExpected: false,
},
{
name: "validStructSubField",
pathToField: "those[1].field2",
expectedValue: buildPath(1, "those").addSection(-1, "field2"),
errorExpected: false,
},
{
name: "validStructSubFieldIndex",
pathToField: "these[1].field2[0]",
expectedValue: buildPath(1, "these").addSection(0, "field2"),
errorExpected: false,
},
{
name: "validStructSubFieldIndexSubfield",
pathToField: "complextree[1].field2[1].stringsubfield",
expectedValue: buildPath(1, "complextree").addSection(1, "field2").addSection(-1, "stringsubfield"),
errorExpected: false,
},
{
name: "validStructDownwardAPI",
pathToField: `metadata.labels["app.kubernetes.io/component"]`,
expectedValue: buildPath(-1, "metadata", "labels", "app.kubernetes.io/component"),
errorExpected: false,
},
{
name: "invalidIndexInIndex",
pathToField: "complextree[1[0]]",
errorExpected: true,
errorMsg: "nested parentheses are not allowed: complextree[1[0]]",
},
{
name: "invalidClosingBrackets",
pathToField: "complextree[1]]",
errorExpected: true,
errorMsg: "invalid field path complextree[1]]",
},
{
name: "validFieldsWithQuotes",
pathToField: "'complextree'[1].field2[1].'stringsubfield'",
expectedValue: buildPath(1, "complextree").addSection(1, "field2").addSection(-1, "stringsubfield"),
errorExpected: false,
},
}
for _, test := range tests {
s, err := parseFields(test.pathToField)
if test.errorExpected {
compareExpectedParserError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedParserError(t, test.name, test.pathToField, err)
}
compareParserValues(t, test.name, test.pathToField, test.expectedValue, s)
}
}
// unExpectedError function handles unexpected error
func unExpectedParserError(t *testing.T, name string, pathToField string, err error) {
t.Fatalf("%q; path %q - unexpected error %v", name, pathToField, err)
}
// compareExpectedError compares the expectedError and the actualError return by parseFields
func compareExpectedParserError(t *testing.T, name string, pathToField string, err error, errorMsg string) {
if err == nil {
t.Fatalf("%q; path %q - should return error, but no error returned",
name, pathToField)
}
if errorMsg != err.Error() {
t.Fatalf("%q; path %q - expected error: \"%s\", got error: \"%v\"",
name, pathToField, errorMsg, err.Error())
}
}
// compareValues compares the expectedValue and actualValue returned by parseFields
func compareParserValues(t *testing.T, name string, pathToField string, expectedValue PathSectionSlice, actualValue []PathSection) {
t.Helper()
if len(expectedValue) != len(actualValue) {
t.Fatalf("%q; Path: %s Got: %v Expected: %v", name, pathToField, actualValue, expectedValue)
}
for idx, expected := range expectedValue {
if !reflect.DeepEqual(expected, actualValue[idx]) {
t.Fatalf("%q; Path: %s idx: %v Fields Got: %v Expected: %v", name, pathToField, idx, actualValue[idx], expected)
}
}
}

View File

@@ -0,0 +1,350 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package kunstruct provides unstructured from api machinery and factory for creating unstructured
package kunstruct
import (
"encoding/json"
"fmt"
jsonpatch "github.com/evanphx/json-patch"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/resid"
)
var _ ifc.Kunstructured = &UnstructAdapter{}
// UnstructAdapter wraps unstructured.Unstructured from
// https://github.com/kubernetes/apimachinery/blob/master/
// pkg/apis/meta/v1/unstructured/unstructured.go
// to isolate dependence on apimachinery.
type UnstructAdapter struct {
unstructured.Unstructured
}
// NewKunstructuredFromObject returns a new instance of Kunstructured.
func NewKunstructuredFromObject(obj runtime.Object) (ifc.Kunstructured, error) {
// Convert obj to a byte stream, then convert that to JSON (Unstructured).
marshaled, err := json.Marshal(obj)
if err != nil {
return &UnstructAdapter{}, err
}
var u unstructured.Unstructured
err = u.UnmarshalJSON(marshaled)
// creationTimestamp always 'null', remove it
u.SetCreationTimestamp(metav1.Time{})
return &UnstructAdapter{Unstructured: u}, err
}
// GetGvk returns the Gvk name of the object.
func (fs *UnstructAdapter) GetGvk() resid.Gvk {
x := fs.GroupVersionKind()
return resid.Gvk{
Group: x.Group,
Version: x.Version,
Kind: x.Kind,
}
}
// SetGvk set the Gvk of the object to the input Gvk
func (fs *UnstructAdapter) SetGvk(g resid.Gvk) {
fs.SetGroupVersionKind(toSchemaGvk(g))
}
// Copy provides a copy behind an interface.
func (fs *UnstructAdapter) Copy() ifc.Kunstructured {
return &UnstructAdapter{*fs.DeepCopy()}
}
// Map returns the unstructured content map.
func (fs *UnstructAdapter) Map() map[string]interface{} {
return fs.Object
}
// SetMap overrides the unstructured content map.
func (fs *UnstructAdapter) SetMap(m map[string]interface{}) {
fs.Object = m
}
func (fs *UnstructAdapter) selectSubtree(path string) (map[string]interface{}, []string, bool, error) {
sections, err := parseFields(path)
if len(sections) == 0 || (err != nil) {
return nil, nil, false, err
}
content := fs.UnstructuredContent()
lastSectionIdx := len(sections)
// There are multiple sections to walk
for sectionIdx := 0; sectionIdx < lastSectionIdx; sectionIdx++ {
idx := sections[sectionIdx].idx
fields := sections[sectionIdx].fields
if idx == -1 {
// This section has no index
return content, fields, true, nil
}
// This section is terminated by an indexed field.
// Let's extract the slice first
indexedField, found, err := unstructured.NestedFieldNoCopy(content, fields...)
if !found || err != nil {
return content, fields, found, err
}
s, ok := indexedField.([]interface{})
if !ok {
return content, fields, false, fmt.Errorf("%v is of the type %T, expected []interface{}", indexedField, indexedField)
}
if idx >= len(s) {
return content, fields, false, fmt.Errorf("index %d is out of bounds", idx)
}
if sectionIdx == lastSectionIdx-1 {
// This is the last section. Let's build a fake map
// to let the rest of the field extraction to work.
idxstring := fmt.Sprintf("[%v]", idx)
newContent := map[string]interface{}{idxstring: s[idx]}
newFields := []string{idxstring}
return newContent, newFields, true, nil
}
newContent, ok := s[idx].(map[string]interface{})
if !ok {
// Only map are supported here
return content, fields, false,
fmt.Errorf("%#v is expected to be of type map[string]interface{}", s[idx])
}
content = newContent
}
// It seems to be an invalid path
return nil, []string{}, false, nil
}
// GetFieldValue returns the value at the given fieldpath.
func (fs *UnstructAdapter) GetFieldValue(path string) (interface{}, error) {
content, fields, found, err := fs.selectSubtree(path)
if !found || err != nil {
return nil, noFieldError{Field: path}
}
s, found, err := unstructured.NestedFieldNoCopy(
content, fields...)
if found || err != nil {
return s, err
}
return nil, noFieldError{Field: path}
}
// GetString returns value at the given fieldpath.
func (fs *UnstructAdapter) GetString(path string) (string, error) {
content, fields, found, err := fs.selectSubtree(path)
if !found || err != nil {
return "", noFieldError{Field: path}
}
s, found, err := unstructured.NestedString(
content, fields...)
if found || err != nil {
return s, err
}
return "", noFieldError{Field: path}
}
// GetStringSlice returns value at the given fieldpath.
func (fs *UnstructAdapter) GetStringSlice(path string) ([]string, error) {
content, fields, found, err := fs.selectSubtree(path)
if !found || err != nil {
return []string{}, noFieldError{Field: path}
}
s, found, err := unstructured.NestedStringSlice(
content, fields...)
if found || err != nil {
return s, err
}
return []string{}, noFieldError{Field: path}
}
// GetBool returns value at the given fieldpath.
func (fs *UnstructAdapter) GetBool(path string) (bool, error) {
content, fields, found, err := fs.selectSubtree(path)
if !found || err != nil {
return false, noFieldError{Field: path}
}
s, found, err := unstructured.NestedBool(
content, fields...)
if found || err != nil {
return s, err
}
return false, noFieldError{Field: path}
}
// GetFloat64 returns value at the given fieldpath.
func (fs *UnstructAdapter) GetFloat64(path string) (float64, error) {
content, fields, found, err := fs.selectSubtree(path)
if !found || err != nil {
return 0, err
}
s, found, err := unstructured.NestedFloat64(
content, fields...)
if found || err != nil {
return s, err
}
return 0, noFieldError{Field: path}
}
// GetInt64 returns value at the given fieldpath.
func (fs *UnstructAdapter) GetInt64(path string) (int64, error) {
content, fields, found, err := fs.selectSubtree(path)
if !found || err != nil {
return 0, noFieldError{Field: path}
}
s, found, err := unstructured.NestedInt64(
content, fields...)
if found || err != nil {
return s, err
}
return 0, noFieldError{Field: path}
}
// GetSlice returns value at the given fieldpath.
func (fs *UnstructAdapter) GetSlice(path string) ([]interface{}, error) {
content, fields, found, err := fs.selectSubtree(path)
if !found || err != nil {
return nil, noFieldError{Field: path}
}
s, found, err := unstructured.NestedSlice(
content, fields...)
if found || err != nil {
return s, err
}
return nil, noFieldError{Field: path}
}
// GetStringMap returns value at the given fieldpath.
func (fs *UnstructAdapter) GetStringMap(path string) (map[string]string, error) {
content, fields, found, err := fs.selectSubtree(path)
if !found || err != nil {
return nil, noFieldError{Field: path}
}
s, found, err := unstructured.NestedStringMap(
content, fields...)
if found || err != nil {
return s, err
}
return nil, noFieldError{Field: path}
}
// GetMap returns value at the given fieldpath.
func (fs *UnstructAdapter) GetMap(path string) (map[string]interface{}, error) {
content, fields, found, err := fs.selectSubtree(path)
if !found || err != nil {
return nil, noFieldError{Field: path}
}
s, found, err := unstructured.NestedMap(
content, fields...)
if found || err != nil {
return s, err
}
return nil, noFieldError{Field: path}
}
func (fs *UnstructAdapter) MatchesLabelSelector(selector string) (bool, error) {
s, err := labels.Parse(selector)
if err != nil {
return false, err
}
return s.Matches(labels.Set(fs.GetLabels())), nil
}
func (fs *UnstructAdapter) MatchesAnnotationSelector(selector string) (bool, error) {
s, err := labels.Parse(selector)
if err != nil {
return false, err
}
return s.Matches(labels.Set(fs.GetAnnotations())), nil
}
func (fs *UnstructAdapter) Patch(patch ifc.Kunstructured) error {
versionedObj, err := scheme.Scheme.New(
toSchemaGvk(patch.GetGvk()))
merged := map[string]interface{}{}
saveName := fs.GetName()
switch {
case runtime.IsNotRegisteredError(err):
baseBytes, err := json.Marshal(fs.Map())
if err != nil {
return err
}
patchBytes, err := json.Marshal(patch.Map())
if err != nil {
return err
}
mergedBytes, err := jsonpatch.MergePatch(baseBytes, patchBytes)
if err != nil {
return err
}
err = json.Unmarshal(mergedBytes, &merged)
if err != nil {
return err
}
case err != nil:
return err
default:
// Use Strategic-Merge-Patch to handle types w/ schema
// TODO: Change this to use the new Merge package.
// Store the name of the target object, because this name may have been munged.
// Apply this name to the patched object.
lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj)
if err != nil {
return err
}
merged, err = strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta(
fs.Map(),
patch.Map(),
lookupPatchMeta)
if err != nil {
return err
}
}
fs.SetMap(merged)
if len(fs.Map()) != 0 {
// if the patch deletes the object
// don't reset the name
fs.SetName(saveName)
}
return nil
}
// toSchemaGvk converts to a schema.GroupVersionKind.
func toSchemaGvk(x resid.Gvk) schema.GroupVersionKind {
return schema.GroupVersionKind{
Group: x.Group,
Version: x.Version,
Kind: x.Kind,
}
}
// noFieldError is returned when a field is expected, but missing.
type noFieldError struct {
Field string
}
func (e noFieldError) Error() string {
return fmt.Sprintf("no field named '%s'", e.Field)
}

View File

@@ -0,0 +1,831 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kunstruct
import (
"reflect"
"testing"
)
var kunstructured = NewKunstructuredFactoryImpl().FromMap(map[string]interface{}{
"Kind": "Service",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"app": "application-name",
},
"name": "service-name",
},
"spec": map[string]interface{}{
"ports": map[string]interface{}{
"port": int64(80),
},
},
"this": map[string]interface{}{
"is": map[string]interface{}{
"aNumber": int64(1000),
"aFloat": float64(1.001),
"aNilValue": nil,
"aBool": true,
"anEmptyMap": map[string]interface{}{},
"anEmptySlice": []interface{}{},
"unrecognizable": testing.InternalExample{
Name: "fooBar",
},
},
},
"that": []interface{}{
"idx0",
"idx1",
"idx2",
"idx3",
},
"those": []interface{}{
map[string]interface{}{
"field1": "idx0foo",
"field2": "idx0bar",
},
map[string]interface{}{
"field1": "idx1foo",
"field2": "idx1bar",
},
map[string]interface{}{
"field1": "idx2foo",
"field2": "idx2bar",
},
},
"these": []interface{}{
map[string]interface{}{
"field1": []interface{}{"idx010", "idx011"},
"field2": []interface{}{"idx020", "idx021"},
},
map[string]interface{}{
"field1": []interface{}{"idx110", "idx111"},
"field2": []interface{}{"idx120", "idx121"},
},
map[string]interface{}{
"field1": []interface{}{"idx210", "idx211"},
"field2": []interface{}{"idx220", "idx221"},
},
},
"complextree": []interface{}{
map[string]interface{}{
"field1": []interface{}{
map[string]interface{}{
"stringsubfield": "idx1010",
"intsubfield": int64(1010),
"floatsubfield": float64(1.010),
"boolfield": true,
},
map[string]interface{}{
"stringsubfield": "idx1011",
"intsubfield": int64(1011),
"floatsubfield": float64(1.011),
"boolfield": false,
},
},
"field2": []interface{}{
map[string]interface{}{
"stringsubfield": "idx1020",
"intsubfield": int64(1020),
"floatsubfield": float64(1.020),
"boolfield": true,
},
map[string]interface{}{
"stringsubfield": "idx1021",
"intsubfield": int64(1021),
"floatsubfield": float64(1.021),
"boolfield": false,
},
},
},
map[string]interface{}{
"field1": []interface{}{
map[string]interface{}{
"stringsubfield": "idx1110",
"intsubfield": int64(1110),
"floatsubfield": float64(1.110),
"boolfield": true,
},
map[string]interface{}{
"stringsubfield": "idx1111",
"intsubfield": int64(1111),
"floatsubfield": float64(1.111),
"boolfield": false,
},
},
"field2": []interface{}{
map[string]interface{}{
"stringsubfield": "idx1120",
"intsubfield": int64(1120),
"floatsubfield": float64(1.1120),
"boolfield": true,
},
map[string]interface{}{
"stringsubfield": "idx1121",
"intsubfield": int64(1121),
"floatsubfield": float64(1.1121),
"boolfield": false,
},
},
},
},
})
func TestGetFieldValue(t *testing.T) {
tests := []struct {
name string
pathToField string
expectedValue interface{}
errorExpected bool
errorMsg string
}{
{
name: "oneField",
pathToField: "Kind",
expectedValue: "Service",
errorExpected: false,
},
{
name: "twoFields",
pathToField: "metadata.name",
expectedValue: "service-name",
errorExpected: false,
},
{
name: "threeFields",
pathToField: "spec.ports.port",
expectedValue: int64(80),
errorExpected: false,
},
{
name: "empty",
pathToField: "",
errorExpected: true,
errorMsg: "no field named ''",
},
{
name: "emptyDotEmpty",
pathToField: ".",
errorExpected: true,
errorMsg: "no field named '.'",
},
{
name: "twoFieldsOneMissing",
pathToField: "metadata.banana",
errorExpected: true,
errorMsg: "no field named 'metadata.banana'",
},
{
name: "deeperMissingField",
pathToField: "this.is.aDeep.field.that.does.not.exist",
errorExpected: true,
errorMsg: "no field named 'this.is.aDeep.field.that.does.not.exist'",
},
{
name: "emptyMap",
pathToField: "this.is.anEmptyMap",
errorExpected: false,
expectedValue: map[string]interface{}{},
},
{
name: "emptySlice",
pathToField: "this.is.anEmptySlice",
errorExpected: false,
expectedValue: []interface{}{},
},
{
name: "numberAsValue",
pathToField: "this.is.aNumber",
errorExpected: false,
expectedValue: int64(1000),
},
{
name: "floatAsValue",
pathToField: "this.is.aFloat",
errorExpected: false,
expectedValue: float64(1.001),
},
{
name: "boolAsValue",
pathToField: "this.is.aBool",
errorExpected: false,
expectedValue: true,
},
{
name: "nilAsValue",
pathToField: "this.is.aNilValue",
errorExpected: false,
expectedValue: nil,
},
{
name: "unrecognizable",
pathToField: "this.is.unrecognizable.Name",
errorExpected: true,
errorMsg: ".this.is.unrecognizable.Name accessor error: {fooBar <nil> false} is of the type testing.InternalExample, expected map[string]interface{}",
},
{
name: "validStringIndex",
pathToField: "that[2]",
expectedValue: "idx2",
errorExpected: false,
},
{
name: "outOfBoundIndex",
pathToField: "that[99]",
errorMsg: "no field named 'that[99]'",
errorExpected: true,
},
{
name: "accessorError",
pathToField: "that[downwardapi]",
errorMsg: ".that.downwardapi accessor error: [idx0 idx1 idx2 idx3] is of the type []interface {}, expected map[string]interface{}",
errorExpected: true,
},
{
name: "unknownSlice",
pathToField: "unknown[0]",
errorMsg: "no field named 'unknown[0]'",
errorExpected: true,
},
{
name: "sliceInSlice",
pathToField: "that[2][0]",
errorExpected: true,
errorMsg: "no field named 'that[2][0]'",
},
{
name: "validStructIndex",
pathToField: "those[1]",
errorExpected: false,
expectedValue: map[string]interface{}{"field1": "idx1foo", "field2": "idx1bar"},
},
{
name: "validStructSubField",
pathToField: "those[1].field2",
errorExpected: false,
expectedValue: "idx1bar",
},
{
name: "validStructSubFieldIndex",
pathToField: "these[1].field2[1]",
errorExpected: false,
expectedValue: "idx121",
},
{
name: "validStructSubFieldOutOfBoundIndex",
pathToField: "these[1].field2[99]",
errorExpected: true,
errorMsg: "no field named 'these[1].field2[99]'",
},
{
name: "validStructSubFieldIndexSubfield",
pathToField: "complextree[1].field2[1].stringsubfield",
errorExpected: false,
expectedValue: "idx1121",
},
{
name: "validStructSubFieldIndexInvalidName",
pathToField: "complextree[1].field2[1].invalidsubfield",
errorExpected: true,
errorMsg: "no field named 'complextree[1].field2[1].invalidsubfield'",
},
{
name: "validDownwardAPILabels",
pathToField: `metadata.labels["app"]`,
errorExpected: false,
expectedValue: "application-name",
},
{
name: "validDownwardAPISpecs",
pathToField: `spec.ports['port']`,
errorExpected: false,
expectedValue: int64(80),
},
{
name: "validDownwardAPIThis",
pathToField: `this.is[aFloat]`,
errorExpected: false,
expectedValue: float64(1.001),
},
{
name: "downwardAPIInvalidLabel",
pathToField: `metadata.labels["theisnotanint"]`,
errorExpected: true,
errorMsg: `no field named 'metadata.labels["theisnotanint"]'`,
},
{
name: "downwardAPIInvalidLabel2",
pathToField: `invalidfield.labels["app"]`,
errorExpected: true,
errorMsg: `no field named 'invalidfield.labels["app"]'`,
},
{
name: "invalidIndexInIndex",
pathToField: "complextree[1[0]]",
errorExpected: true,
errorMsg: "no field named 'complextree[1[0]]'",
},
{
name: "invalidClosingBrackets",
pathToField: "complextree[1]]",
errorExpected: true,
errorMsg: "no field named 'complextree[1]]'",
},
{
name: "validFieldsWithQuotes",
pathToField: "'complextree'[1].field2[1].'stringsubfield'",
errorExpected: false,
expectedValue: "idx1121",
},
}
for _, test := range tests {
s, err := kunstructured.GetFieldValue(test.pathToField)
if test.errorExpected {
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedError(t, test.name, test.pathToField, err)
}
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
}
}
func TestGetString(t *testing.T) {
tests := []struct {
name string
pathToField string
expectedValue string
errorExpected bool
errorMsg string
}{
{
name: "oneField",
pathToField: "Kind",
expectedValue: "Service",
errorExpected: false,
},
{
name: "twoFields",
pathToField: "metadata.name",
expectedValue: "service-name",
errorExpected: false,
},
{
name: "emptyMap",
pathToField: "this.is.anEmptyMap",
errorExpected: true,
errorMsg: ".this.is.anEmptyMap accessor error: map[] is of the type map[string]interface {}, expected string",
},
{
name: "twoFieldsOneMissing",
pathToField: "metadata.banana",
errorExpected: true,
errorMsg: "no field named 'metadata.banana'",
},
{
name: "emptySlice",
pathToField: "this.is.anEmptySlice",
errorExpected: true,
errorMsg: ".this.is.anEmptySlice accessor error: [] is of the type []interface {}, expected string",
},
{
name: "numberAsValue",
pathToField: "this.is.aNumber",
errorExpected: true,
errorMsg: ".this.is.aNumber accessor error: 1000 is of the type int64, expected string",
},
{
name: "nilAsValue",
pathToField: "this.is.aNilValue",
errorExpected: true,
errorMsg: ".this.is.aNilValue accessor error: <nil> is of the type <nil>, expected string",
},
{
name: "validStringIndex",
pathToField: "that[2]",
expectedValue: "idx2",
errorExpected: false,
},
{
name: "validStructIndex",
pathToField: "those[1]",
errorExpected: true,
errorMsg: ".[1] accessor error: map[field1:idx1foo field2:idx1bar] is of the type map[string]interface {}, expected string",
},
{
name: "validStructSubField",
pathToField: "those[1].field2",
errorExpected: false,
expectedValue: "idx1bar",
},
{
name: "validStructSubFieldIndex",
pathToField: "these[1].field2[1]",
errorExpected: false,
expectedValue: "idx121",
},
{
name: "validStructSubFieldIndexSubfield",
pathToField: "complextree[1].field2[1].stringsubfield",
errorExpected: false,
expectedValue: "idx1121",
},
{
name: "invalidIndexInMap",
pathToField: "this.is[1]",
errorExpected: true,
errorMsg: "no field named 'this.is[1]'",
},
{
name: "anotherInvalidIndexInMap",
pathToField: "this.is[1].aString",
errorExpected: true,
errorMsg: "no field named 'this.is[1].aString'",
},
{
name: "validDownwardAPIField",
pathToField: `metadata.labels["app"]`,
errorExpected: false,
expectedValue: "application-name",
},
}
for _, test := range tests {
s, err := kunstructured.GetString(test.pathToField)
if test.errorExpected {
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedError(t, test.name, test.pathToField, err)
}
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
}
}
func TestGetInt64(t *testing.T) {
tests := []struct {
name string
pathToField string
expectedValue int64
errorExpected bool
errorMsg string
}{
{
name: "numberAsValue",
pathToField: "this.is.aNumber",
errorExpected: false,
expectedValue: int64(1000),
},
{
name: "validStructSubFieldIndexSubfield",
pathToField: "complextree[1].field2[1].intsubfield",
errorExpected: false,
expectedValue: int64(1121),
},
{
name: "twoFieldsOneMissing",
pathToField: "metadata.banana",
errorExpected: true,
errorMsg: "no field named 'metadata.banana'",
},
{
name: "validStructSubFieldOutOfBoundIndex",
pathToField: "these[1].field2[99]",
errorExpected: true,
errorMsg: "no field named 'these[1].field2[99]'",
},
{
name: "validDownwardAPISpecs",
pathToField: `spec.ports['port']`,
errorExpected: false,
expectedValue: int64(80),
},
}
for _, test := range tests {
s, err := kunstructured.GetInt64(test.pathToField)
if test.errorExpected {
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedError(t, test.name, test.pathToField, err)
}
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
}
}
func TestGetFloat64(t *testing.T) {
tests := []struct {
name string
pathToField string
expectedValue float64
errorExpected bool
errorMsg string
}{
{
name: "floatAsValue",
pathToField: "this.is.aFloat",
errorExpected: false,
expectedValue: float64(1.001),
},
{
name: "validStructSubFieldIndexSubfield",
pathToField: "complextree[1].field2[1].floatsubfield",
errorExpected: false,
expectedValue: float64(1.1121),
},
{
name: "validDownwardAPIThis",
pathToField: `this.is[aFloat]`,
errorExpected: false,
expectedValue: float64(1.001),
},
{
name: "twoFieldsOneMissing",
pathToField: "metadata.banana",
errorExpected: true,
errorMsg: "no field named 'metadata.banana'",
},
{
name: "validStructSubFieldOutOfBoundIndex",
pathToField: "these[1].field2[99]",
errorExpected: true,
errorMsg: "index 99 is out of bounds",
},
}
for _, test := range tests {
s, err := kunstructured.GetFloat64(test.pathToField)
if test.errorExpected {
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedError(t, test.name, test.pathToField, err)
}
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
}
}
func TestGetBool(t *testing.T) {
tests := []struct {
name string
pathToField string
expectedValue bool
errorExpected bool
errorMsg string
}{
{
name: "boolAsValue",
pathToField: "this.is.aBool",
errorExpected: false,
expectedValue: true,
},
{
name: "validStructSubFieldIndexSubfield",
pathToField: "complextree[1].field2[1].boolfield",
errorExpected: false,
expectedValue: false,
},
{
name: "twoFieldsOneMissing",
pathToField: "metadata.banana",
errorExpected: true,
errorMsg: "no field named 'metadata.banana'",
},
{
name: "validStructSubFieldOutOfBoundIndex",
pathToField: "these[1].field2[99]",
errorExpected: true,
errorMsg: "no field named 'these[1].field2[99]'",
},
}
for _, test := range tests {
s, err := kunstructured.GetBool(test.pathToField)
if test.errorExpected {
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedError(t, test.name, test.pathToField, err)
}
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
}
}
func TestGetStringMap(t *testing.T) {
tests := []struct {
name string
pathToField string
errorExpected bool
errorMsg string
}{
{
name: "validStringMap",
pathToField: "those[2]",
errorExpected: false,
},
{
name: "twoFieldsOneMissing",
pathToField: "metadata.banana",
errorExpected: true,
errorMsg: "no field named 'metadata.banana'",
},
{
name: "validStructSubFieldOutOfBoundIndex",
pathToField: "these[1].field2[99]",
errorExpected: true,
errorMsg: "no field named 'these[1].field2[99]'",
},
}
for _, test := range tests {
_, err := kunstructured.GetStringMap(test.pathToField)
if test.errorExpected {
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedError(t, test.name, test.pathToField, err)
}
}
}
func TestGetMap(t *testing.T) {
tests := []struct {
name string
pathToField string
errorExpected bool
errorMsg string
}{
{
name: "validMap",
pathToField: "those[2]",
errorExpected: false,
},
{
name: "validStructSubFieldIndexSubfield",
pathToField: "complextree[1].field2[1]",
errorExpected: false,
},
{
name: "twoFieldsOneMissing",
pathToField: "metadata.banana",
errorExpected: true,
errorMsg: "no field named 'metadata.banana'",
},
{
name: "validStructSubFieldOutOfBoundIndex",
pathToField: "these[1].field2[99]",
errorExpected: true,
errorMsg: "no field named 'these[1].field2[99]'",
},
}
for _, test := range tests {
_, err := kunstructured.GetMap(test.pathToField)
if test.errorExpected {
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedError(t, test.name, test.pathToField, err)
}
}
}
func TestGetStringSlice(t *testing.T) {
tests := []struct {
name string
pathToField string
errorExpected bool
errorMsg string
}{
{
name: "validStringSlice",
pathToField: "that",
errorExpected: false,
},
{
name: "twoFieldsOneMissing",
pathToField: "metadata.banana",
errorExpected: true,
errorMsg: "no field named 'metadata.banana'",
},
{
name: "validStructSubFieldOutOfBoundIndex",
pathToField: "these[1].field2[99]",
errorExpected: true,
errorMsg: "no field named 'these[1].field2[99]'",
},
}
for _, test := range tests {
_, err := kunstructured.GetStringSlice(test.pathToField)
if test.errorExpected {
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedError(t, test.name, test.pathToField, err)
}
}
}
func TestGetSlice(t *testing.T) {
tests := []struct {
name string
pathToField string
errorExpected bool
errorMsg string
}{
{
name: "validSlice1",
pathToField: "that",
errorExpected: false,
},
{
name: "validSlice2",
pathToField: "those",
errorExpected: false,
},
{
name: "validSlice3",
pathToField: "these",
errorExpected: false,
},
{
name: "validSlice4",
pathToField: "complextree",
errorExpected: false,
},
{
name: "twoFieldsOneMissing",
pathToField: "metadata.banana",
errorExpected: true,
errorMsg: "no field named 'metadata.banana'",
},
{
name: "validStructSubFieldOutOfBoundIndex",
pathToField: "these[1].field2[99]",
errorExpected: true,
errorMsg: "no field named 'these[1].field2[99]'",
},
}
for _, test := range tests {
_, err := kunstructured.GetSlice(test.pathToField)
if test.errorExpected {
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
continue
}
if err != nil {
unExpectedError(t, test.name, test.pathToField, err)
}
}
}
// unExpectedError function handles unexpected error
func unExpectedError(t *testing.T, name string, pathToField string, err error) {
t.Fatalf("%q; path %q - unexpected error %v", name, pathToField, err)
}
// compareExpectedError compares the expectedError and the actualError return by GetFieldValue
func compareExpectedError(t *testing.T, name string, pathToField string, err error, errorMsg string) {
if err == nil {
t.Fatalf("%q; path %q - should return error, but no error returned",
name, pathToField)
}
if errorMsg != err.Error() {
t.Fatalf("%q; path %q - expected error: \"%s\", got error: \"%v\"",
name, pathToField, errorMsg, err.Error())
}
}
// compareValues compares the expectedValue and actualValue returned by GetFieldValue
func compareValues(t *testing.T, name string, pathToField string, expectedValue interface{}, actualValue interface{}) {
t.Helper()
switch typedV := expectedValue.(type) {
case nil, string, bool, float64, int, int64:
if expectedValue != actualValue {
t.Fatalf("%q; Got: %v Expected: %v", name, actualValue, expectedValue)
}
case map[string]interface{}:
if !reflect.DeepEqual(expectedValue, actualValue) {
t.Fatalf("%q; Got: %v Expected: %v", name, actualValue, expectedValue)
}
case []interface{}:
if !reflect.DeepEqual(expectedValue, actualValue) {
t.Fatalf("%q; Got: %v Expected: %v", name, actualValue, expectedValue)
}
default:
t.Logf("%T value at `%s`", typedV, pathToField)
}
}