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() var encodingjson = json.CaseSensitiveJsonIterator()
func GetObjectMeta(u *unstructured.Unstructured, dropMalformedFields bool) (*metav1.ObjectMeta, bool, error) { // GetObjectMeta does conversion of JSON to ObjectMeta. It first tries json.Unmarshal into a metav1.ObjectMeta
metadata, found := u.UnstructuredContent()["metadata"] // 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 { if !found {
return nil, false, nil 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{} objectMeta := &metav1.ObjectMeta{}
metadataBytes, err := encodingjson.Marshal(metadata) metadataBytes, err := encodingjson.Marshal(metadata)
if err != nil { if err != nil {
@ -72,9 +75,10 @@ func GetObjectMeta(u *unstructured.Unstructured, dropMalformedFields bool) (*met
return accumulatedObjectMeta, true, nil 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 { if objectMeta == nil {
unstructured.RemoveNestedField(u.UnstructuredContent(), "metadata") unstructured.RemoveNestedField(obj, "metadata")
return nil return nil
} }
@ -83,6 +87,6 @@ func SetObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta)
return err return err
} }
u.UnstructuredContent()["metadata"] = metadata obj["metadata"] = metadata
return nil return nil
} }

View File

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

View File

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