apiextensions: move ObjectMeta coercion into own package

This commit is contained in:
Dr. Stefan Schimanski 2019-06-07 10:52:53 +02:00
parent 78220fe380
commit b807def8d9
5 changed files with 327 additions and 277 deletions

View File

@ -28,10 +28,12 @@ import (
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
@ -1022,7 +1024,7 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
if err != nil {
return err
}
objectMeta, foundObjectMeta, err := getObjectMeta(u, v.dropInvalidMetadata)
objectMeta, foundObjectMeta, err := schemaobjectmeta.GetObjectMeta(u, v.dropInvalidMetadata)
if err != nil {
return err
}
@ -1044,72 +1046,10 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
u.SetAPIVersion(apiVersion)
}
if foundObjectMeta {
if err := setObjectMeta(u, objectMeta); err != nil {
if err := schemaobjectmeta.SetObjectMeta(u, objectMeta); err != nil {
return err
}
}
return nil
}
var encodingjson = json.CaseSensitiveJsonIterator()
func getObjectMeta(u *unstructured.Unstructured, dropMalformedFields bool) (*metav1.ObjectMeta, bool, error) {
metadata, found := u.UnstructuredContent()["metadata"]
if !found {
return nil, false, nil
}
// round-trip through JSON first, hoping that unmarshaling just works
objectMeta := &metav1.ObjectMeta{}
metadataBytes, err := encodingjson.Marshal(metadata)
if err != nil {
return nil, false, err
}
if err = encodingjson.Unmarshal(metadataBytes, objectMeta); err == nil {
// if successful, return
return objectMeta, true, nil
}
if !dropMalformedFields {
// if we're not trying to drop malformed fields, return the error
return nil, true, err
}
metadataMap, ok := metadata.(map[string]interface{})
if !ok {
return nil, false, fmt.Errorf("invalid metadata: expected object, got %T", metadata)
}
// Go field by field accumulating into the metadata object.
// This takes advantage of the fact that you can repeatedly unmarshal individual fields into a single struct,
// each iteration preserving the old key-values.
accumulatedObjectMeta := &metav1.ObjectMeta{}
testObjectMeta := &metav1.ObjectMeta{}
for k, v := range metadataMap {
// serialize a single field
if singleFieldBytes, err := encodingjson.Marshal(map[string]interface{}{k: v}); err == nil {
// do a test unmarshal
if encodingjson.Unmarshal(singleFieldBytes, testObjectMeta) == nil {
// if that succeeds, unmarshal for real
encodingjson.Unmarshal(singleFieldBytes, accumulatedObjectMeta)
}
}
}
return accumulatedObjectMeta, true, nil
}
func setObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta) error {
if objectMeta == nil {
unstructured.RemoveNestedField(u.UnstructuredContent(), "metadata")
return nil
}
metadata, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objectMeta)
if err != nil {
return err
}
u.UnstructuredContent()["metadata"] = metadata
return nil
}

View File

@ -17,24 +17,11 @@ limitations under the License.
package apiserver
import (
"math/rand"
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
"k8s.io/apimachinery/pkg/api/equality"
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/util/diff"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)
func TestConvertFieldLabel(t *testing.T) {
@ -118,202 +105,3 @@ func TestConvertFieldLabel(t *testing.T) {
})
}
}
func TestRoundtripObjectMeta(t *testing.T) {
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)
codec := json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
seed := rand.Int63()
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(seed), codecs)
N := 1000
for i := 0; i < N; i++ {
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
original := &metav1.ObjectMeta{}
fuzzer.Fuzz(original)
if err := setObjectMeta(u, original); err != nil {
t.Fatalf("unexpected error setting ObjectMeta: %v", err)
}
o, _, err := getObjectMeta(u, false)
if err != nil {
t.Fatalf("unexpected error getting the Objectmeta: %v", err)
}
if !equality.Semantic.DeepEqual(original, o) {
t.Errorf("diff: %v\nCodec: %#v", diff.ObjectReflectDiff(original, o), codec)
}
}
}
// TestMalformedObjectMetaFields sets a number of different random values and types for all
// metadata fields. If json.Unmarshal accepts them, compare that getObjectMeta
// gives the same result. Otherwise, drop malformed fields.
func TestMalformedObjectMetaFields(t *testing.T) {
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(rand.Int63()), serializer.NewCodecFactory(runtime.NewScheme()))
spuriousValues := func() []interface{} {
return []interface{}{
// primitives
nil,
int64(1),
float64(1.5),
true,
"a",
// well-formed complex values
[]interface{}{"a", "b"},
map[string]interface{}{"a": "1", "b": "2"},
[]interface{}{int64(1), int64(2)},
[]interface{}{float64(1.5), float64(2.5)},
// known things json decoding tolerates
map[string]interface{}{"a": "1", "b": nil},
// malformed things
map[string]interface{}{"a": "1", "b": []interface{}{"nested"}},
[]interface{}{"a", int64(1), float64(1.5), true, []interface{}{"nested"}},
}
}
N := 100
for i := 0; i < N; i++ {
fuzzedObjectMeta := &metav1.ObjectMeta{}
fuzzer.Fuzz(fuzzedObjectMeta)
goodMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
if err != nil {
t.Fatal(err)
}
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)
if err != nil {
t.Fatalf("unexpected to not find something at %v: %v", pth, err)
}
if reflect.TypeOf(v) == reflect.TypeOf(orig) {
continue
}
// make a spurious map
spuriousMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
if err != nil {
t.Fatal(err)
}
if err := SetJsonPath(spuriousMetaMap, pth, 0, v); err != nil {
t.Fatal(err)
}
// See if it can unmarshal to object meta
spuriousJSON, err := encodingjson.Marshal(spuriousMetaMap)
if err != nil {
t.Fatalf("error on %v=%#v: %v", pth, v, err)
}
expectedObjectMeta := &metav1.ObjectMeta{}
if err := encodingjson.Unmarshal(spuriousJSON, expectedObjectMeta); err != nil {
// if standard json unmarshal would fail decoding this field, drop the field entirely
truncatedMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
if err != nil {
t.Fatal(err)
}
// we expect this logic for the different fields:
switch {
default:
// delete complete top-level field by default
DeleteJsonPath(truncatedMetaMap, pth[:1], 0)
}
truncatedJSON, err := encodingjson.Marshal(truncatedMetaMap)
if err != nil {
t.Fatalf("error on %v=%#v: %v", pth, v, err)
}
expectedObjectMeta = &metav1.ObjectMeta{}
if err := encodingjson.Unmarshal(truncatedJSON, expectedObjectMeta); err != nil {
t.Fatalf("error on %v=%#v: %v", pth, v, err)
}
}
// make sure dropInvalidTypedFields+getObjectMeta matches what we expect
u := &unstructured.Unstructured{Object: map[string]interface{}{"metadata": spuriousMetaMap}}
actualObjectMeta, _, err := getObjectMeta(u, true)
if err != nil {
t.Errorf("got unexpected error after dropping invalid typed fields on %v=%#v: %v", pth, v, err)
continue
}
if !equality.Semantic.DeepEqual(expectedObjectMeta, actualObjectMeta) {
t.Errorf("%v=%#v, diff: %v\n", pth, v, diff.ObjectReflectDiff(expectedObjectMeta, actualObjectMeta))
t.Errorf("expectedObjectMeta %#v", expectedObjectMeta)
}
}
}
}
}
func TestGetObjectMetaNils(t *testing.T) {
u := &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "Pod",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"generateName": nil,
"labels": map[string]interface{}{
"foo": nil,
},
},
},
}
o, _, err := getObjectMeta(u, true)
if err != nil {
t.Fatal(err)
}
if o.GenerateName != "" {
t.Errorf("expected null json value to be read as \"\" string, but got: %q", o.GenerateName)
}
if got, expected := o.Labels, map[string]string{"foo": ""}; !reflect.DeepEqual(got, expected) {
t.Errorf("unexpected labels, expected=%#v, got=%#v", expected, got)
}
// double check this what the kube JSON decode is doing
bs, _ := encodingjson.Marshal(u.UnstructuredContent())
kubeObj, _, err := clientgoscheme.Codecs.UniversalDecoder(corev1.SchemeGroupVersion).Decode(bs, nil, nil)
if err != nil {
t.Fatal(err)
}
pod, ok := kubeObj.(*corev1.Pod)
if !ok {
t.Fatalf("expected v1 Pod, got: %T", pod)
}
if got, expected := o.GenerateName, pod.ObjectMeta.GenerateName; got != expected {
t.Errorf("expected generatedName to be %q, got %q", expected, got)
}
if got, expected := o.Labels, pod.ObjectMeta.Labels; !reflect.DeepEqual(got, expected) {
t.Errorf("expected labels to be %v, got %v", expected, got)
}
}
func TestGetObjectMeta(t *testing.T) {
for i := 0; i < 100; i++ {
u := &unstructured.Unstructured{Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "good",
"Name": "bad1",
"nAme": "bad2",
"naMe": "bad3",
"namE": "bad4",
"namespace": "good",
"Namespace": "bad1",
"nAmespace": "bad2",
"naMespace": "bad3",
"namEspace": "bad4",
"creationTimestamp": "a",
},
}}
meta, _, err := getObjectMeta(u, true)
if err != nil {
t.Fatal(err)
}
if meta.Name != "good" || meta.Namespace != "good" {
t.Fatalf("got %#v", meta)
}
}
}

View File

@ -0,0 +1,88 @@
/*
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 (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
)
var encodingjson = json.CaseSensitiveJsonIterator()
func GetObjectMeta(u *unstructured.Unstructured, dropMalformedFields bool) (*metav1.ObjectMeta, bool, error) {
metadata, found := u.UnstructuredContent()["metadata"]
if !found {
return nil, false, nil
}
// round-trip through JSON first, hoping that unmarshaling just works
objectMeta := &metav1.ObjectMeta{}
metadataBytes, err := encodingjson.Marshal(metadata)
if err != nil {
return nil, false, err
}
if err = encodingjson.Unmarshal(metadataBytes, objectMeta); err == nil {
// if successful, return
return objectMeta, true, nil
}
if !dropMalformedFields {
// if we're not trying to drop malformed fields, return the error
return nil, true, err
}
metadataMap, ok := metadata.(map[string]interface{})
if !ok {
return nil, false, fmt.Errorf("invalid metadata: expected object, got %T", metadata)
}
// Go field by field accumulating into the metadata object.
// This takes advantage of the fact that you can repeatedly unmarshal individual fields into a single struct,
// each iteration preserving the old key-values.
accumulatedObjectMeta := &metav1.ObjectMeta{}
testObjectMeta := &metav1.ObjectMeta{}
for k, v := range metadataMap {
// serialize a single field
if singleFieldBytes, err := encodingjson.Marshal(map[string]interface{}{k: v}); err == nil {
// do a test unmarshal
if encodingjson.Unmarshal(singleFieldBytes, testObjectMeta) == nil {
// if that succeeds, unmarshal for real
encodingjson.Unmarshal(singleFieldBytes, accumulatedObjectMeta)
}
}
}
return accumulatedObjectMeta, true, nil
}
func SetObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta) error {
if objectMeta == nil {
unstructured.RemoveNestedField(u.UnstructuredContent(), "metadata")
return nil
}
metadata, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objectMeta)
if err != nil {
return err
}
u.UnstructuredContent()["metadata"] = metadata
return nil
}

View File

@ -0,0 +1,234 @@
/*
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 (
"math/rand"
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
"k8s.io/apimachinery/pkg/api/equality"
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/util/diff"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)
func TestRoundtripObjectMeta(t *testing.T) {
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)
codec := json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
seed := rand.Int63()
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(seed), codecs)
N := 1000
for i := 0; i < N; i++ {
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
original := &metav1.ObjectMeta{}
fuzzer.Fuzz(original)
if err := SetObjectMeta(u, original); err != nil {
t.Fatalf("unexpected error setting ObjectMeta: %v", err)
}
o, _, err := GetObjectMeta(u, false)
if err != nil {
t.Fatalf("unexpected error getting the Objectmeta: %v", err)
}
if !equality.Semantic.DeepEqual(original, o) {
t.Errorf("diff: %v\nCodec: %#v", diff.ObjectReflectDiff(original, o), codec)
}
}
}
// TestMalformedObjectMetaFields sets a number of different random values and types for all
// metadata fields. If json.Unmarshal accepts them, compare that getObjectMeta
// gives the same result. Otherwise, drop malformed fields.
func TestMalformedObjectMetaFields(t *testing.T) {
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(rand.Int63()), serializer.NewCodecFactory(runtime.NewScheme()))
spuriousValues := func() []interface{} {
return []interface{}{
// primitives
nil,
int64(1),
float64(1.5),
true,
"a",
// well-formed complex values
[]interface{}{"a", "b"},
map[string]interface{}{"a": "1", "b": "2"},
[]interface{}{int64(1), int64(2)},
[]interface{}{float64(1.5), float64(2.5)},
// known things json decoding tolerates
map[string]interface{}{"a": "1", "b": nil},
// malformed things
map[string]interface{}{"a": "1", "b": []interface{}{"nested"}},
[]interface{}{"a", int64(1), float64(1.5), true, []interface{}{"nested"}},
}
}
N := 100
for i := 0; i < N; i++ {
fuzzedObjectMeta := &metav1.ObjectMeta{}
fuzzer.Fuzz(fuzzedObjectMeta)
goodMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
if err != nil {
t.Fatal(err)
}
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)
if err != nil {
t.Fatalf("unexpected to not find something at %v: %v", pth, err)
}
if reflect.TypeOf(v) == reflect.TypeOf(orig) {
continue
}
// make a spurious map
spuriousMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
if err != nil {
t.Fatal(err)
}
if err := SetJsonPath(spuriousMetaMap, pth, 0, v); err != nil {
t.Fatal(err)
}
// See if it can unmarshal to object meta
spuriousJSON, err := encodingjson.Marshal(spuriousMetaMap)
if err != nil {
t.Fatalf("error on %v=%#v: %v", pth, v, err)
}
expectedObjectMeta := &metav1.ObjectMeta{}
if err := encodingjson.Unmarshal(spuriousJSON, expectedObjectMeta); err != nil {
// if standard json unmarshal would fail decoding this field, drop the field entirely
truncatedMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
if err != nil {
t.Fatal(err)
}
// we expect this logic for the different fields:
switch {
default:
// delete complete top-level field by default
DeleteJsonPath(truncatedMetaMap, pth[:1], 0)
}
truncatedJSON, err := encodingjson.Marshal(truncatedMetaMap)
if err != nil {
t.Fatalf("error on %v=%#v: %v", pth, v, err)
}
expectedObjectMeta = &metav1.ObjectMeta{}
if err := encodingjson.Unmarshal(truncatedJSON, expectedObjectMeta); err != nil {
t.Fatalf("error on %v=%#v: %v", pth, v, err)
}
}
// make sure dropInvalidTypedFields+getObjectMeta matches what we expect
u := &unstructured.Unstructured{Object: map[string]interface{}{"metadata": spuriousMetaMap}}
actualObjectMeta, _, err := GetObjectMeta(u, true)
if err != nil {
t.Errorf("got unexpected error after dropping invalid typed fields on %v=%#v: %v", pth, v, err)
continue
}
if !equality.Semantic.DeepEqual(expectedObjectMeta, actualObjectMeta) {
t.Errorf("%v=%#v, diff: %v\n", pth, v, diff.ObjectReflectDiff(expectedObjectMeta, actualObjectMeta))
t.Errorf("expectedObjectMeta %#v", expectedObjectMeta)
}
}
}
}
}
func TestGetObjectMetaNils(t *testing.T) {
u := &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "Pod",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"generateName": nil,
"labels": map[string]interface{}{
"foo": nil,
},
},
},
}
o, _, err := GetObjectMeta(u, true)
if err != nil {
t.Fatal(err)
}
if o.GenerateName != "" {
t.Errorf("expected null json value to be read as \"\" string, but got: %q", o.GenerateName)
}
if got, expected := o.Labels, map[string]string{"foo": ""}; !reflect.DeepEqual(got, expected) {
t.Errorf("unexpected labels, expected=%#v, got=%#v", expected, got)
}
// double check this what the kube JSON decode is doing
bs, _ := encodingjson.Marshal(u.UnstructuredContent())
kubeObj, _, err := clientgoscheme.Codecs.UniversalDecoder(corev1.SchemeGroupVersion).Decode(bs, nil, nil)
if err != nil {
t.Fatal(err)
}
pod, ok := kubeObj.(*corev1.Pod)
if !ok {
t.Fatalf("expected v1 Pod, got: %T", pod)
}
if got, expected := o.GenerateName, pod.ObjectMeta.GenerateName; got != expected {
t.Errorf("expected generatedName to be %q, got %q", expected, got)
}
if got, expected := o.Labels, pod.ObjectMeta.Labels; !reflect.DeepEqual(got, expected) {
t.Errorf("expected labels to be %v, got %v", expected, got)
}
}
func TestGetObjectMeta(t *testing.T) {
for i := 0; i < 100; i++ {
u := &unstructured.Unstructured{Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "good",
"Name": "bad1",
"nAme": "bad2",
"naMe": "bad3",
"namE": "bad4",
"namespace": "good",
"Namespace": "bad1",
"nAmespace": "bad2",
"naMespace": "bad3",
"namEspace": "bad4",
"creationTimestamp": "a",
},
}}
meta, _, err := GetObjectMeta(u, true)
if err != nil {
t.Fatal(err)
}
if meta.Name != "good" || meta.Namespace != "good" {
t.Fatalf("got %#v", meta)
}
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package apiserver
package objectmeta
import (
"bytes"