apiextensions: implement x-kubernetes-embedded-resource algorithm

This commit is contained in:
Dr. Stefan Schimanski 2019-06-07 15:39:42 +02:00
parent b807def8d9
commit 8fc42ed116
7 changed files with 988 additions and 36 deletions

View File

@ -0,0 +1,99 @@
/*
Copyright 2019 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 objectmeta
import (
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// Coerce checks types of embedded ObjectMeta and TypeMeta and prunes unknown fields inside the former.
// It does coerce ObjectMeta and TypeMeta at the root if includeRoot is true.
// If dropInvalidFields is true, fields of wrong type will be dropped.
func Coerce(pth *field.Path, obj interface{}, s *structuralschema.Structural, includeRoot, dropInvalidFields bool) *field.Error {
if includeRoot {
if s == nil {
s = &structuralschema.Structural{}
}
clone := *s
clone.XEmbeddedResource = true
s = &clone
}
c := coercer{dropInvalidFields: dropInvalidFields}
return c.coerce(pth, obj, s)
}
type coercer struct {
dropInvalidFields bool
}
func (c *coercer) coerce(pth *field.Path, x interface{}, s *structuralschema.Structural) *field.Error {
if s == nil {
return nil
}
switch x := x.(type) {
case map[string]interface{}:
for k, v := range x {
if s.XEmbeddedResource {
switch k {
case "apiVersion", "kind":
if _, ok := v.(string); !ok && c.dropInvalidFields {
delete(x, k)
} else if !ok {
return field.Invalid(pth, v, "must be a string")
}
case "metadata":
meta, found, err := GetObjectMeta(x, c.dropInvalidFields)
if err != nil {
if !c.dropInvalidFields {
return field.Invalid(pth.Child("metadata"), v, err.Error())
}
// pass through on error if dropInvalidFields is true
} else if found {
if err := SetObjectMeta(x, meta); err != nil {
return field.Invalid(pth.Child("metadata"), v, err.Error())
}
if meta.CreationTimestamp.IsZero() {
unstructured.RemoveNestedField(x, "metadata", "creationTimestamp")
}
}
}
}
prop, ok := s.Properties[k]
if ok {
if err := c.coerce(pth.Child(k), v, &prop); err != nil {
return err
}
} else if s.AdditionalProperties != nil {
if err := c.coerce(pth.Key(k), v, s.AdditionalProperties.Structural); err != nil {
return err
}
}
}
case []interface{}:
for i, v := range x {
if err := c.coerce(pth.Index(i), v, s.Items); err != nil {
return err
}
}
default:
// scalars, do nothing
}
return nil
}

View File

@ -0,0 +1,515 @@
/*
Copyright 2019 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 objectmeta
import (
"bytes"
"reflect"
"testing"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apimachinery/pkg/util/json"
)
func TestCoerce(t *testing.T) {
tests := []struct {
name string
json string
includeRoot bool
dropInvalidFields bool
schema *structuralschema.Structural
expected string
expectedError bool
}{
{name: "empty", json: "null", schema: nil, expected: "null"},
{name: "scalar", json: "4", schema: &structuralschema.Structural{}, expected: "4"},
{name: "scalar array", json: "[1,2]", schema: &structuralschema.Structural{
Items: &structuralschema.Structural{},
}, expected: "[1,2]"},
{name: "x-kubernetes-embedded-resource", json: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"unspecified":"bar",
"pruned": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"spec": {
"unspecified": "bar"
}
},
"preserving": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"spec": {
"unspecified": "bar"
}
},
"nested": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"spec": {
"unspecified": "bar",
"embedded": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"spec": {
"unspecified": "bar"
}
}
}
}
}
`, schema: &structuralschema.Structural{
Generic: structuralschema.Generic{Type: "object"},
Properties: map[string]structuralschema.Structural{
"pruned": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
},
Properties: map[string]structuralschema.Structural{
"spec": {
Generic: structuralschema.Generic{Type: "object"},
},
},
},
"preserving": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
XPreserveUnknownFields: true,
},
},
"nested": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
},
Properties: map[string]structuralschema.Structural{
"spec": {
Generic: structuralschema.Generic{Type: "object"},
Properties: map[string]structuralschema.Structural{
"embedded": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
},
Properties: map[string]structuralschema.Structural{
"spec": {
Generic: structuralschema.Generic{Type: "object"},
},
},
},
},
},
},
},
},
}, expected: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"unspecified":"bar",
"pruned": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance"
},
"spec": {
"unspecified": "bar"
}
},
"preserving": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance"
},
"spec": {
"unspecified": "bar"
}
},
"nested": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance"
},
"spec": {
"unspecified": "bar",
"embedded": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance"
},
"spec": {
"unspecified": "bar"
}
}
}
}
}
`},
{name: "x-kubernetes-embedded-resource, with includeRoot=true", json: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"unspecified":"bar",
"pruned": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"spec": {
"unspecified": "bar"
}
},
"preserving": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"spec": {
"unspecified": "bar"
}
},
"nested": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"spec": {
"unspecified": "bar",
"embedded": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance",
"unspecified": "bar"
},
"spec": {
"unspecified": "bar"
}
}
}
}
}
`, includeRoot: true, schema: &structuralschema.Structural{
Generic: structuralschema.Generic{Type: "object"},
Properties: map[string]structuralschema.Structural{
"pruned": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
},
Properties: map[string]structuralschema.Structural{
"spec": {
Generic: structuralschema.Generic{Type: "object"},
},
},
},
"preserving": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
XPreserveUnknownFields: true,
},
},
"nested": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
},
Properties: map[string]structuralschema.Structural{
"spec": {
Generic: structuralschema.Generic{Type: "object"},
Properties: map[string]structuralschema.Structural{
"embedded": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
},
Properties: map[string]structuralschema.Structural{
"spec": {
Generic: structuralschema.Generic{Type: "object"},
},
},
},
},
},
},
},
},
}, expected: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance"
},
"unspecified":"bar",
"pruned": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance"
},
"spec": {
"unspecified": "bar"
}
},
"preserving": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance"
},
"spec": {
"unspecified": "bar"
}
},
"nested": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance"
},
"spec": {
"unspecified": "bar",
"embedded": {
"apiVersion": "foo/v1",
"kind": "Foo",
"unspecified": "bar",
"metadata": {
"name": "instance"
},
"spec": {
"unspecified": "bar"
}
}
}
}
}
`},
{name: "without name", json: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance"
},
"pruned": {
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"namespace": "kube-system"
}
}
}
`, schema: &structuralschema.Structural{
Generic: structuralschema.Generic{Type: "object"},
Properties: map[string]structuralschema.Structural{
"pruned": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
},
},
},
}, expected: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance"
},
"pruned": {
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"namespace": "kube-system"
}
}
}
`},
{name: "x-kubernetes-embedded-resource, with dropInvalidFields=true", json: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance"
},
"pruned": {
"apiVersion": 42,
"kind": 42,
"metadata": {
"name": "instance",
"namespace": ["abc"],
"labels": {
"foo": 42
}
}
}
}
`, dropInvalidFields: true, schema: &structuralschema.Structural{
Generic: structuralschema.Generic{Type: "object"},
Properties: map[string]structuralschema.Structural{
"pruned": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
},
Properties: map[string]structuralschema.Structural{
"spec": {
Generic: structuralschema.Generic{Type: "object"},
},
},
},
},
}, expected: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance"
},
"pruned": {
"metadata": {
"name": "instance"
}
}
}
`},
{name: "invalid metadata type, with dropInvalidFields=true", json: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance"
},
"pruned": {
"apiVersion": 42,
"kind": 42,
"metadata": [42]
}
}
`, dropInvalidFields: true, schema: &structuralschema.Structural{
Generic: structuralschema.Generic{Type: "object"},
Properties: map[string]structuralschema.Structural{
"pruned": {
Generic: structuralschema.Generic{Type: "object"},
Extensions: structuralschema.Extensions{
XEmbeddedResource: true,
},
},
},
}, expected: `
{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": {
"name": "instance"
},
"pruned": {
"metadata": [42]
}
}
`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var in interface{}
if err := json.Unmarshal([]byte(tt.json), &in); err != nil {
t.Fatal(err)
}
var expected interface{}
if err := json.Unmarshal([]byte(tt.expected), &expected); err != nil {
t.Fatal(err)
}
err := Coerce(nil, in, tt.schema, tt.includeRoot, tt.dropInvalidFields)
if tt.expectedError && err == nil {
t.Error("expected error, but did not get any")
} else if !tt.expectedError && err != nil {
t.Errorf("expected no error, but got: %v", err)
} else if !reflect.DeepEqual(in, expected) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
err := enc.Encode(in)
if err != nil {
t.Fatalf("unexpected result mashalling error: %v", err)
}
t.Errorf("expected: %s\ngot: %s\ndiff: %s", tt.expected, buf.String(), diff.ObjectDiff(expected, in))
}
})
}
}

View File

@ -27,13 +27,16 @@ import (
var encodingjson = json.CaseSensitiveJsonIterator()
func GetObjectMeta(u *unstructured.Unstructured, dropMalformedFields bool) (*metav1.ObjectMeta, bool, error) {
metadata, found := u.UnstructuredContent()["metadata"]
// GetObjectMeta does conversion of JSON to ObjectMeta. It first tries json.Unmarshal into a metav1.ObjectMeta
// type. If that does not work and dropMalformedFields is true, it does field-by-field best-effort conversion
// throwing away fields which lead to errors.
func GetObjectMeta(obj map[string]interface{}, dropMalformedFields bool) (*metav1.ObjectMeta, bool, error) {
metadata, found := obj["metadata"]
if !found {
return nil, false, nil
}
// round-trip through JSON first, hoping that unmarshaling just works
// round-trip through JSON first, hoping that unmarshalling just works
objectMeta := &metav1.ObjectMeta{}
metadataBytes, err := encodingjson.Marshal(metadata)
if err != nil {
@ -72,9 +75,10 @@ func GetObjectMeta(u *unstructured.Unstructured, dropMalformedFields bool) (*met
return accumulatedObjectMeta, true, nil
}
func SetObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta) error {
// SetObjectMeta writes back ObjectMeta into a JSON data structure.
func SetObjectMeta(obj map[string]interface{}, objectMeta *metav1.ObjectMeta) error {
if objectMeta == nil {
unstructured.RemoveNestedField(u.UnstructuredContent(), "metadata")
unstructured.RemoveNestedField(obj, "metadata")
return nil
}
@ -83,6 +87,6 @@ func SetObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta)
return err
}
u.UnstructuredContent()["metadata"] = metadata
obj["metadata"] = metadata
return nil
}

View File

@ -46,10 +46,10 @@ func TestRoundtripObjectMeta(t *testing.T) {
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
original := &metav1.ObjectMeta{}
fuzzer.Fuzz(original)
if err := SetObjectMeta(u, original); err != nil {
if err := SetObjectMeta(u.Object, original); err != nil {
t.Fatalf("unexpected error setting ObjectMeta: %v", err)
}
o, _, err := GetObjectMeta(u, false)
o, _, err := GetObjectMeta(u.Object, false)
if err != nil {
t.Fatalf("unexpected error getting the Objectmeta: %v", err)
}
@ -96,7 +96,7 @@ func TestMalformedObjectMetaFields(t *testing.T) {
for _, pth := range jsonPaths(nil, goodMetaMap) {
for _, v := range spuriousValues() {
// skip values of same type, because they can only cause decoding errors further insides
orig, err := JsonPathValue(goodMetaMap, pth, 0)
orig, err := JSONPathValue(goodMetaMap, pth, 0)
if err != nil {
t.Fatalf("unexpected to not find something at %v: %v", pth, err)
}
@ -109,7 +109,7 @@ func TestMalformedObjectMetaFields(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := SetJsonPath(spuriousMetaMap, pth, 0, v); err != nil {
if err := SetJSONPath(spuriousMetaMap, pth, 0, v); err != nil {
t.Fatal(err)
}
@ -130,7 +130,7 @@ func TestMalformedObjectMetaFields(t *testing.T) {
switch {
default:
// delete complete top-level field by default
DeleteJsonPath(truncatedMetaMap, pth[:1], 0)
DeleteJSONPath(truncatedMetaMap, pth[:1], 0)
}
truncatedJSON, err := encodingjson.Marshal(truncatedMetaMap)
@ -145,7 +145,7 @@ func TestMalformedObjectMetaFields(t *testing.T) {
// make sure dropInvalidTypedFields+getObjectMeta matches what we expect
u := &unstructured.Unstructured{Object: map[string]interface{}{"metadata": spuriousMetaMap}}
actualObjectMeta, _, err := GetObjectMeta(u, true)
actualObjectMeta, _, err := GetObjectMeta(u.Object, true)
if err != nil {
t.Errorf("got unexpected error after dropping invalid typed fields on %v=%#v: %v", pth, v, err)
continue
@ -174,7 +174,7 @@ func TestGetObjectMetaNils(t *testing.T) {
},
}
o, _, err := GetObjectMeta(u, true)
o, _, err := GetObjectMeta(u.Object, true)
if err != nil {
t.Fatal(err)
}
@ -223,7 +223,7 @@ func TestGetObjectMeta(t *testing.T) {
},
}}
meta, _, err := GetObjectMeta(u, true)
meta, _, err := GetObjectMeta(u.Object, true)
if err != nil {
t.Fatal(err)
}

View File

@ -28,10 +28,10 @@ type (
index *int
field string
}
JsonPath []jsonPathNode
JSONPath []jsonPathNode
)
func (p JsonPath) String() string {
func (p JSONPath) String() string {
var buf bytes.Buffer
for _, n := range p {
if n.index == nil {
@ -43,8 +43,8 @@ func (p JsonPath) String() string {
return buf.String()
}
func jsonPaths(base JsonPath, j map[string]interface{}) []JsonPath {
res := make([]JsonPath, 0, len(j))
func jsonPaths(base JSONPath, j map[string]interface{}) []JSONPath {
res := make([]JSONPath, 0, len(j))
for k, old := range j {
kPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{field: k})
res = append(res, kPth)
@ -59,8 +59,8 @@ func jsonPaths(base JsonPath, j map[string]interface{}) []JsonPath {
return res
}
func jsonIterSlice(base JsonPath, j []interface{}) []JsonPath {
res := make([]JsonPath, 0, len(j))
func jsonIterSlice(base JSONPath, j []interface{}) []JSONPath {
res := make([]JSONPath, 0, len(j))
for i, old := range j {
index := i
iPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{index: &index})
@ -76,7 +76,7 @@ func jsonIterSlice(base JsonPath, j []interface{}) []JsonPath {
return res
}
func JsonPathValue(j map[string]interface{}, pth JsonPath, base int) (interface{}, error) {
func JSONPathValue(j map[string]interface{}, pth JSONPath, base int) (interface{}, error) {
if len(pth) == base {
return nil, fmt.Errorf("empty json path is invalid for object")
}
@ -92,7 +92,7 @@ func JsonPathValue(j map[string]interface{}, pth JsonPath, base int) (interface{
}
switch field := field.(type) {
case map[string]interface{}:
return JsonPathValue(field, pth, base+1)
return JSONPathValue(field, pth, base+1)
case []interface{}:
return jsonPathValueSlice(field, pth, base+1)
default:
@ -100,7 +100,7 @@ func JsonPathValue(j map[string]interface{}, pth JsonPath, base int) (interface{
}
}
func jsonPathValueSlice(j []interface{}, pth JsonPath, base int) (interface{}, error) {
func jsonPathValueSlice(j []interface{}, pth JSONPath, base int) (interface{}, error) {
if len(pth) == base {
return nil, fmt.Errorf("empty json path %q is invalid for object", pth)
}
@ -115,7 +115,7 @@ func jsonPathValueSlice(j []interface{}, pth JsonPath, base int) (interface{}, e
}
switch item := j[*pth[base].index].(type) {
case map[string]interface{}:
return JsonPathValue(item, pth, base+1)
return JSONPathValue(item, pth, base+1)
case []interface{}:
return jsonPathValueSlice(item, pth, base+1)
default:
@ -123,7 +123,7 @@ func jsonPathValueSlice(j []interface{}, pth JsonPath, base int) (interface{}, e
}
}
func SetJsonPath(j map[string]interface{}, pth JsonPath, base int, value interface{}) error {
func SetJSONPath(j map[string]interface{}, pth JSONPath, base int, value interface{}) error {
if len(pth) == base {
return fmt.Errorf("empty json path is invalid for object")
}
@ -140,15 +140,15 @@ func SetJsonPath(j map[string]interface{}, pth JsonPath, base int, value interfa
}
switch field := field.(type) {
case map[string]interface{}:
return SetJsonPath(field, pth, base+1, value)
return SetJSONPath(field, pth, base+1, value)
case []interface{}:
return setJsonPathSlice(field, pth, base+1, value)
return setJSONPathSlice(field, pth, base+1, value)
default:
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
}
}
func setJsonPathSlice(j []interface{}, pth JsonPath, base int, value interface{}) error {
func setJSONPathSlice(j []interface{}, pth JSONPath, base int, value interface{}) error {
if len(pth) == base {
return fmt.Errorf("empty json path %q is invalid for object", pth)
}
@ -164,15 +164,15 @@ func setJsonPathSlice(j []interface{}, pth JsonPath, base int, value interface{}
}
switch item := j[*pth[base].index].(type) {
case map[string]interface{}:
return SetJsonPath(item, pth, base+1, value)
return SetJSONPath(item, pth, base+1, value)
case []interface{}:
return setJsonPathSlice(item, pth, base+1, value)
return setJSONPathSlice(item, pth, base+1, value)
default:
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
}
}
func DeleteJsonPath(j map[string]interface{}, pth JsonPath, base int) error {
func DeleteJSONPath(j map[string]interface{}, pth JSONPath, base int) error {
if len(pth) == base {
return fmt.Errorf("empty json path is invalid for object")
}
@ -189,7 +189,7 @@ func DeleteJsonPath(j map[string]interface{}, pth JsonPath, base int) error {
}
switch field := field.(type) {
case map[string]interface{}:
return DeleteJsonPath(field, pth, base+1)
return DeleteJSONPath(field, pth, base+1)
case []interface{}:
if len(pth) == base+2 {
if pth[base+1].index == nil {
@ -198,13 +198,13 @@ func DeleteJsonPath(j map[string]interface{}, pth JsonPath, base int) error {
j[pth[base].field] = append(field[:*pth[base+1].index], field[*pth[base+1].index+1:]...)
return nil
}
return deleteJsonPathSlice(field, pth, base+1)
return deleteJSONPathSlice(field, pth, base+1)
default:
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
}
}
func deleteJsonPathSlice(j []interface{}, pth JsonPath, base int) error {
func deleteJSONPathSlice(j []interface{}, pth JSONPath, base int) error {
if len(pth) == base {
return fmt.Errorf("empty json path %q is invalid for object", pth)
}
@ -219,7 +219,7 @@ func deleteJsonPathSlice(j []interface{}, pth JsonPath, base int) error {
}
switch item := j[*pth[base].index].(type) {
case map[string]interface{}:
return DeleteJsonPath(item, pth, base+1)
return DeleteJSONPath(item, pth, base+1)
case []interface{}:
if len(pth) == base+2 {
if pth[base+1].index == nil {
@ -228,7 +228,7 @@ func deleteJsonPathSlice(j []interface{}, pth JsonPath, base int) error {
j[*pth[base].index] = append(item[:*pth[base+1].index], item[*pth[base+1].index+1:])
return nil
}
return deleteJsonPathSlice(item, pth, base+1)
return deleteJSONPathSlice(item, pth, base+1)
default:
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
}

View File

@ -0,0 +1,119 @@
/*
Copyright 2019 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 objectmeta
import (
"strings"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
metavalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/api/validation/path"
"k8s.io/apimachinery/pkg/runtime/schema"
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// Validate validates embedded ObjectMeta and TypeMeta.
// It also validate those at the root if includeRoot is true.
func Validate(pth *field.Path, obj interface{}, s *structuralschema.Structural, includeRoot bool) field.ErrorList {
if includeRoot {
if s == nil {
s = &structuralschema.Structural{}
}
clone := *s
clone.XEmbeddedResource = true
s = &clone
}
return validate(pth, obj, s)
}
func validate(pth *field.Path, x interface{}, s *structuralschema.Structural) field.ErrorList {
if s == nil {
return nil
}
var allErrs field.ErrorList
switch x := x.(type) {
case map[string]interface{}:
if s.XEmbeddedResource {
allErrs = append(allErrs, validateEmbeddedResource(pth, x, s)...)
}
for k, v := range x {
prop, ok := s.Properties[k]
if ok {
allErrs = append(allErrs, validate(pth.Child(k), v, &prop)...)
} else if s.AdditionalProperties != nil {
allErrs = append(allErrs, validate(pth.Key(k), v, s.AdditionalProperties.Structural)...)
}
}
case []interface{}:
for i, v := range x {
allErrs = append(allErrs, validate(pth.Index(i), v, s.Items)...)
}
default:
// scalars, do nothing
}
return allErrs
}
func validateEmbeddedResource(pth *field.Path, x map[string]interface{}, s *structuralschema.Structural) field.ErrorList {
var allErrs field.ErrorList
// require apiVersion and kind, but not metadata
if _, found := x["apiVersion"]; !found {
allErrs = append(allErrs, field.Required(pth.Child("apiVersion"), "must not be empty"))
}
if _, found := x["kind"]; !found {
allErrs = append(allErrs, field.Required(pth.Child("kind"), "must not be empty"))
}
for k, v := range x {
switch k {
case "apiVersion":
if apiVersion, ok := v.(string); !ok {
allErrs = append(allErrs, field.Invalid(pth.Child("apiVersion"), v, "must be a string"))
} else if len(apiVersion) == 0 {
allErrs = append(allErrs, field.Invalid(pth.Child("apiVersion"), apiVersion, "must not be empty"))
} else if _, err := schema.ParseGroupVersion(apiVersion); err != nil {
allErrs = append(allErrs, field.Invalid(pth.Child("apiVersion"), apiVersion, err.Error()))
}
case "kind":
if kind, ok := v.(string); !ok {
allErrs = append(allErrs, field.Invalid(pth.Child("kind"), v, "must be a string"))
} else if len(kind) == 0 {
allErrs = append(allErrs, field.Invalid(pth.Child("kind"), kind, "must not be empty"))
} else if errs := utilvalidation.IsDNS1035Label(strings.ToLower(kind)); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(pth.Child("kind"), kind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ",")))
}
case "metadata":
meta, _, err := GetObjectMeta(x, false)
if err != nil {
allErrs = append(allErrs, field.Invalid(pth.Child("metadata"), v, err.Error()))
} else {
if len(meta.Name) == 0 {
meta.Name = "fakename" // we have to set something to avoid an error
}
allErrs = append(allErrs, metavalidation.ValidateObjectMeta(meta, len(meta.Namespace) > 0, path.ValidatePathSegmentName, pth.Child("metadata"))...)
}
}
}
return allErrs
}

View File

@ -0,0 +1,215 @@
/*
Copyright 2019 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 objectmeta
import (
"testing"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestValidateEmbeddedResource(t *testing.T) {
tests := []struct {
name string
object map[string]interface{}
errors []validationMatch
}{
{name: "empty", object: map[string]interface{}{}, errors: []validationMatch{
required("apiVersion"),
required("kind"),
}},
{name: "version and kind", object: map[string]interface{}{
"apiVersion": "foo/v1",
"kind": "Foo",
}},
{name: "invalid kind", object: map[string]interface{}{
"apiVersion": "foo/v1",
"kind": "foo.bar-com",
}, errors: []validationMatch{
invalid("kind"),
}},
{name: "no name", object: map[string]interface{}{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": map[string]interface{}{
"namespace": "kube-system",
},
}},
{name: "no namespace", object: map[string]interface{}{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": map[string]interface{}{
"name": "foo",
},
}},
{name: "invalid", object: map[string]interface{}{
"apiVersion": "foo/v1",
"kind": "Foo",
"metadata": map[string]interface{}{
"name": "..",
"namespace": "$$$",
"labels": map[string]interface{}{
"#": "#",
},
"annotations": map[string]interface{}{
"#": "#",
},
},
}, errors: []validationMatch{
invalid("metadata", "name"),
invalid("metadata", "namespace"),
invalid("metadata", "labels"), // key
invalid("metadata", "labels"), // value
invalid("metadata", "annotations"), // key
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
schema := &structuralschema.Structural{Extensions: structuralschema.Extensions{XEmbeddedResource: true}}
errs := validateEmbeddedResource(nil, tt.object, schema)
seenErrs := make([]bool, len(errs))
for _, expectedError := range tt.errors {
found := false
for i, err := range errs {
if expectedError.matches(err) && !seenErrs[i] {
found = true
seenErrs[i] = true
break
}
}
if !found {
t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs)
}
}
for i, seen := range seenErrs {
if !seen {
t.Errorf("unexpected error: %v", errs[i])
}
}
})
}
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
object string
includeRoot bool
errors []validationMatch
}{
{name: "empty", object: `{}`, errors: []validationMatch{}},
{name: "include root", object: `{}`, includeRoot: true, errors: []validationMatch{
required("apiVersion"),
required("kind"),
}},
{name: "embedded", object: `
{
"embedded": {}
}`, errors: []validationMatch{
required("embedded", "apiVersion"),
required("embedded", "kind"),
}},
{name: "nested", object: `
{
"nested": {
"embedded": {}
}
}`, errors: []validationMatch{
required("nested", "apiVersion"),
required("nested", "kind"),
required("nested", "embedded", "apiVersion"),
required("nested", "embedded", "kind"),
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
schema := &structuralschema.Structural{
Properties: map[string]structuralschema.Structural{
"embedded": {Extensions: structuralschema.Extensions{XEmbeddedResource: true}},
"nested": {
Extensions: structuralschema.Extensions{XEmbeddedResource: true},
Properties: map[string]structuralschema.Structural{
"embedded": {Extensions: structuralschema.Extensions{XEmbeddedResource: true}},
},
},
},
}
var obj map[string]interface{}
if err := json.Unmarshal([]byte(tt.object), &obj); err != nil {
t.Fatal(err)
}
errs := Validate(nil, obj, schema, tt.includeRoot)
seenErrs := make([]bool, len(errs))
for _, expectedError := range tt.errors {
found := false
for i, err := range errs {
if expectedError.matches(err) && !seenErrs[i] {
found = true
seenErrs[i] = true
break
}
}
if !found {
t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs)
}
}
for i, seen := range seenErrs {
if !seen {
t.Errorf("unexpected error: %v", errs[i])
}
}
})
}
}
type validationMatch struct {
path *field.Path
errorType field.ErrorType
}
func required(path ...string) validationMatch {
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeRequired}
}
func invalid(path ...string) validationMatch {
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
}
func invalidIndex(index int, path ...string) validationMatch {
return validationMatch{path: field.NewPath(path[0], path[1:]...).Index(index), errorType: field.ErrorTypeInvalid}
}
func unsupported(path ...string) validationMatch {
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeNotSupported}
}
func immutable(path ...string) validationMatch {
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
}
func forbidden(path ...string) validationMatch {
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeForbidden}
}
func (v validationMatch) matches(err *field.Error) bool {
return err.Type == v.errorType && err.Field == v.path.String()
}