Merge pull request #64267 from sttts/sttts-crd-objectmeta-pruning

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

apiextensions: add ObjectMeta validation and pruning

This is a critical pre-requisite for further multi-version support and especially for GA of CRDs: ObjectMeta must be schema-validated and pruned, like `json.Unmarshal` does this.

This PR adds this in the incoming request serializer and the storage decoder. The former errors when schema validation fails, the later just drops invalid typed fields.

Fixes #59451

```release-note
Meta data of CustomResources is now pruned and schema checked during deserialization of requests and when read from etcd. In the former case, invalid meta data is rejected, in the later it is dropped from the CustomResource objects.
```
pull/8/head
Kubernetes Submit Queue 2018-06-02 12:55:36 -07:00 committed by GitHub
commit 0ecfd343b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 916 additions and 22 deletions

View File

@ -299,6 +299,9 @@ func TestObjectWatchFraming(t *testing.T) {
f := fuzzer.FuzzerFor(FuzzerFuncs, rand.NewSource(benchmarkSeed), legacyscheme.Codecs)
secret := &api.Secret{}
f.Fuzz(secret)
if secret.Data == nil {
secret.Data = map[string][]byte{}
}
secret.Data["binary"] = []byte{0x00, 0x10, 0x30, 0x55, 0xff, 0x00}
secret.Data["utf8"] = []byte("a string with \u0345 characters")
secret.Data["long"] = bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x00}, 1000)

View File

@ -12,6 +12,7 @@ go_library(
deps = [
"//pkg/apis/apps:go_default_library",
"//vendor/github.com/google/gofuzz:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
],
)

View File

@ -19,6 +19,7 @@ package fuzzer
import (
fuzz "github.com/google/gofuzz"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/kubernetes/pkg/apis/apps"
)
@ -46,6 +47,12 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
if s.Status.CollisionCount == nil {
s.Status.CollisionCount = new(int32)
}
if s.Spec.Selector == nil {
s.Spec.Selector = &metav1.LabelSelector{MatchLabels: s.Spec.Template.Labels}
}
if len(s.Labels) == 0 {
s.Labels = s.Spec.Template.Labels
}
},
}
}

View File

@ -182,6 +182,7 @@ func Convert_v1_Deployment_To_extensions_Deployment(in *appsv1.Deployment, out *
out.Spec.RollbackTo = new(extensions.RollbackConfig)
out.Spec.RollbackTo.Revision = revision64
}
out.Annotations = deepCopyStringMap(out.Annotations)
delete(out.Annotations, appsv1.DeprecatedRollbackTo)
} else {
out.Spec.RollbackTo = nil
@ -195,6 +196,8 @@ func Convert_v1_Deployment_To_extensions_Deployment(in *appsv1.Deployment, out *
func Convert_extensions_Deployment_To_v1_Deployment(in *extensions.Deployment, out *appsv1.Deployment, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Annotations = deepCopyStringMap(out.Annotations) // deep copy because we modify it below
if err := Convert_extensions_DeploymentSpec_To_v1_DeploymentSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
@ -235,9 +238,8 @@ func Convert_v1_RollingUpdateDaemonSet_To_extensions_RollingUpdateDaemonSet(in *
func Convert_extensions_DaemonSet_To_v1_DaemonSet(in *extensions.DaemonSet, out *appsv1.DaemonSet, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if out.Annotations == nil {
out.Annotations = make(map[string]string)
}
out.Annotations = deepCopyStringMap(out.Annotations) // deep copy annotations because we change them below
out.Annotations[appsv1.DeprecatedTemplateGeneration] = strconv.FormatInt(in.Spec.TemplateGeneration, 10)
if err := Convert_extensions_DaemonSetSpec_To_v1_DaemonSetSpec(&in.Spec, &out.Spec, s); err != nil {
return err
@ -287,6 +289,7 @@ func Convert_v1_DaemonSet_To_extensions_DaemonSet(in *appsv1.DaemonSet, out *ext
return err
} else {
out.Spec.TemplateGeneration = value64
out.Annotations = deepCopyStringMap(out.Annotations)
delete(out.Annotations, appsv1.DeprecatedTemplateGeneration)
}
}
@ -496,3 +499,11 @@ func Convert_apps_StatefulSetStatus_To_v1_StatefulSetStatus(in *apps.StatefulSet
}
return nil
}
func deepCopyStringMap(m map[string]string) map[string]string {
ret := make(map[string]string, len(m))
for k, v := range m {
ret[k] = v
}
return ret
}

View File

@ -415,6 +415,7 @@ func Convert_v1beta2_Deployment_To_extensions_Deployment(in *appsv1beta2.Deploym
out.Spec.RollbackTo = new(extensions.RollbackConfig)
out.Spec.RollbackTo.Revision = revision64
}
out.Annotations = deepCopyStringMap(out.Annotations)
delete(out.Annotations, appsv1beta2.DeprecatedRollbackTo)
} else {
out.Spec.RollbackTo = nil
@ -440,6 +441,8 @@ func Convert_v1beta2_ReplicaSetSpec_To_extensions_ReplicaSetSpec(in *appsv1beta2
func Convert_extensions_Deployment_To_v1beta2_Deployment(in *extensions.Deployment, out *appsv1beta2.Deployment, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Annotations = deepCopyStringMap(out.Annotations) // deep copy because we modify annotations below
if err := Convert_extensions_DeploymentSpec_To_v1beta2_DeploymentSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
@ -463,9 +466,7 @@ func Convert_extensions_Deployment_To_v1beta2_Deployment(in *extensions.Deployme
func Convert_extensions_DaemonSet_To_v1beta2_DaemonSet(in *extensions.DaemonSet, out *appsv1beta2.DaemonSet, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if out.Annotations == nil {
out.Annotations = make(map[string]string)
}
out.Annotations = deepCopyStringMap(out.Annotations)
out.Annotations[appsv1beta2.DeprecatedTemplateGeneration] = strconv.FormatInt(in.Spec.TemplateGeneration, 10)
if err := Convert_extensions_DaemonSetSpec_To_v1beta2_DaemonSetSpec(&in.Spec, &out.Spec, s); err != nil {
return err
@ -515,6 +516,7 @@ func Convert_v1beta2_DaemonSet_To_extensions_DaemonSet(in *appsv1beta2.DaemonSet
return err
} else {
out.Spec.TemplateGeneration = value64
out.Annotations = deepCopyStringMap(out.Annotations)
delete(out.Annotations, appsv1beta2.DeprecatedTemplateGeneration)
}
}
@ -552,3 +554,11 @@ func Convert_v1beta2_DaemonSetUpdateStrategy_To_extensions_DaemonSetUpdateStrate
}
return nil
}
func deepCopyStringMap(m map[string]string) map[string]string {
ret := make(map[string]string, len(m))
for k, v := range m {
ret[k] = v
}
return ret
}

View File

@ -32,6 +32,14 @@ func newBool(val bool) *bool {
// Funcs returns the fuzzer functions for the batch api group.
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(j *batch.Job, c fuzz.Continue) {
c.FuzzNoCustom(j) // fuzz self without calling this function again
// match defaulting
if len(j.Labels) == 0 {
j.Labels = j.Spec.Template.Labels
}
},
func(j *batch.JobSpec, c fuzz.Continue) {
c.FuzzNoCustom(j) // fuzz self without calling this function again
completions := int32(c.Rand.Int31())

View File

@ -93,6 +93,19 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
c.Fuzz(&j.ObjectMeta)
j.Target.Name = c.RandString()
},
func(j *core.ReplicationController, c fuzz.Continue) {
c.FuzzNoCustom(j)
// match defaulting
if j.Spec.Template != nil {
if len(j.Labels) == 0 {
j.Labels = j.Spec.Template.Labels
}
if len(j.Spec.Selector) == 0 {
j.Spec.Selector = j.Spec.Template.Labels
}
}
},
func(j *core.ReplicationControllerSpec, c fuzz.Continue) {
c.FuzzNoCustom(j) // fuzz self without calling this function again
//j.TemplateRef = nil // this is required for round trip

View File

@ -12,6 +12,7 @@ go_library(
deps = [
"//pkg/apis/extensions:go_default_library",
"//vendor/github.com/google/gofuzz:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
],

View File

@ -21,6 +21,7 @@ import (
fuzz "github.com/google/gofuzz"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/kubernetes/pkg/apis/extensions"
@ -29,6 +30,17 @@ import (
// Funcs returns the fuzzer functions for the extensions api group.
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(j *extensions.Deployment, c fuzz.Continue) {
c.FuzzNoCustom(j)
// match defaulting
if j.Spec.Selector == nil {
j.Spec.Selector = &metav1.LabelSelector{MatchLabels: j.Spec.Template.Labels}
}
if len(j.Labels) == 0 {
j.Labels = j.Spec.Template.Labels
}
},
func(j *extensions.DeploymentSpec, c fuzz.Continue) {
c.FuzzNoCustom(j) // fuzz self without calling this function again
rhl := int32(c.Rand.Int31())
@ -54,6 +66,15 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
j.RollingUpdate = &rollingUpdate
}
},
func(j *extensions.DaemonSet, c fuzz.Continue) {
c.FuzzNoCustom(j)
// match defaulter
j.Spec.Template.Generation = 0
if len(j.ObjectMeta.Labels) == 0 {
j.ObjectMeta.Labels = j.Spec.Template.ObjectMeta.Labels
}
},
func(j *extensions.DaemonSetSpec, c fuzz.Continue) {
c.FuzzNoCustom(j) // fuzz self without calling this function again
rhl := int32(c.Rand.Int31())
@ -78,5 +99,16 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
j.RollingUpdate = &rollingUpdate
}
},
func(j *extensions.ReplicaSet, c fuzz.Continue) {
c.FuzzNoCustom(j)
// match defaulter
if j.Spec.Selector == nil {
j.Spec.Selector = &metav1.LabelSelector{MatchLabels: j.Spec.Template.Labels}
}
if len(j.Labels) == 0 {
j.Labels = j.Spec.Template.Labels
}
},
}
}

View File

@ -2082,6 +2082,10 @@
"ImportPath": "k8s.io/apimachinery/pkg/api/validation",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/fuzzer",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/internalversion",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -2270,6 +2274,10 @@
"ImportPath": "k8s.io/client-go/dynamic",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/kubernetes/scheme",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/rest",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

View File

@ -15,6 +15,7 @@ go_library(
"customresource_handler.go",
],
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/go-openapi/strfmt:go_default_library",
@ -75,6 +76,30 @@ go_library(
],
)
go_test(
name = "go_default_test",
srcs = [
"customresource_handler_test.go",
"jsonpath_test.go",
],
embed = [":go_default_library"],
deps = [
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/testing/fuzzer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/fuzzer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/scheme:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
@ -90,14 +115,5 @@ filegroup(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:all-srcs",
],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = ["customresource_handler_test.go"],
embed = [":go_default_library"],
deps = [
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
],
visibility = ["//visibility:public"],
)

View File

@ -17,6 +17,7 @@ limitations under the License.
package apiserver
import (
encodingjson "encoding/json"
"fmt"
"net/http"
"path"
@ -610,7 +611,8 @@ func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Enco
}
func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
return versioning.NewDefaultingCodecForScheme(Scheme, nil, decoder, nil, gv)
d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{}}
return versioning.NewDefaultingCodecForScheme(Scheme, nil, d, nil, gv)
}
type UnstructuredObjectTyper struct {
@ -704,7 +706,176 @@ type crdConversionRESTOptionsGetter struct {
func (t crdConversionRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
ret, err := t.RESTOptionsGetter.GetRESTOptions(resource)
if err == nil {
ret.StorageConfig.Codec = versioning.NewCodec(ret.StorageConfig.Codec, ret.StorageConfig.Codec, t.converter, &unstructuredCreator{}, discovery.NewUnstructuredObjectTyper(), &unstructuredDefaulter{delegate: Scheme}, t.encoderVersion, t.decoderVersion)
d := schemaCoercingDecoder{delegate: ret.StorageConfig.Codec, validator: unstructuredSchemaCoercer{
// drop invalid fields while decoding old CRs (before we had any ObjectMeta validation)
dropInvalidMetadata: true,
}}
c := schemaCoercingConverter{delegate: t.converter, validator: unstructuredSchemaCoercer{}}
ret.StorageConfig.Codec = versioning.NewCodec(ret.StorageConfig.Codec, d, c, &unstructuredCreator{}, discovery.NewUnstructuredObjectTyper(), &unstructuredDefaulter{delegate: Scheme}, t.encoderVersion, t.decoderVersion)
}
return ret, err
}
// schemaCoercingDecoder calls the delegate decoder, and then applies the Unstructured schema validator
// to coerce the schema.
type schemaCoercingDecoder struct {
delegate runtime.Decoder
validator unstructuredSchemaCoercer
}
var _ runtime.Decoder = schemaCoercingDecoder{}
func (d schemaCoercingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
obj, gvk, err := d.delegate.Decode(data, defaults, into)
if err != nil {
return nil, gvk, err
}
if u, ok := obj.(*unstructured.Unstructured); ok {
if err := d.validator.apply(u); err != nil {
return nil, gvk, err
}
}
return obj, gvk, nil
}
// schemaCoercingConverter calls the delegate converter and applies the Unstructured validator to
// coerce the schema.
type schemaCoercingConverter struct {
delegate runtime.ObjectConvertor
validator unstructuredSchemaCoercer
}
var _ runtime.ObjectConvertor = schemaCoercingConverter{}
func (v schemaCoercingConverter) Convert(in, out, context interface{}) error {
if err := v.delegate.Convert(in, out, context); err != nil {
return err
}
if u, ok := out.(*unstructured.Unstructured); ok {
if err := v.validator.apply(u); err != nil {
return err
}
}
return nil
}
func (v schemaCoercingConverter) ConvertToVersion(in runtime.Object, gv runtime.GroupVersioner) (runtime.Object, error) {
out, err := v.delegate.ConvertToVersion(in, gv)
if err != nil {
return nil, err
}
if u, ok := out.(*unstructured.Unstructured); ok {
if err := v.validator.apply(u); err != nil {
return nil, err
}
}
return out, nil
}
func (v schemaCoercingConverter) ConvertFieldLabel(version, kind, label, value string) (string, string, error) {
return v.ConvertFieldLabel(version, kind, label, value)
}
// unstructuredSchemaCoercer does the validation for Unstructured that json.Unmarshal
// does for native types. This includes:
// - validating and pruning ObjectMeta (here with optional error instead of pruning)
// - TODO: application of an OpenAPI validator (against the whole object or a top-level field of it).
// - TODO: optionally application of post-validation algorithms like defaulting and/or OpenAPI based pruning.
type unstructuredSchemaCoercer struct {
dropInvalidMetadata bool
}
func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
// save implicit meta fields that don't have to be specified in the validation spec
kind, foundKind, err := unstructured.NestedString(u.UnstructuredContent(), "kind")
if err != nil {
return err
}
apiVersion, foundApiVersion, err := unstructured.NestedString(u.UnstructuredContent(), "apiVersion")
if err != nil {
return err
}
objectMeta, foundObjectMeta, err := getObjectMeta(u, v.dropInvalidMetadata)
if err != nil {
return err
}
// restore meta fields, starting clean
if foundKind {
u.SetKind(kind)
}
if foundApiVersion {
u.SetAPIVersion(apiVersion)
}
if foundObjectMeta {
if err := setObjectMeta(u, objectMeta); err != nil {
return err
}
}
return nil
}
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,10 +17,24 @@ limitations under the License.
package apiserver
import (
encodingjson "encoding/json"
"math/rand"
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
conversion "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/testing/fuzzer"
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 TestConvertFieldLabel(t *testing.T) {
@ -94,3 +108,172 @@ 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 encoding/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)
}
}

View File

@ -0,0 +1,235 @@
/*
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 apiserver
import (
"bytes"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
)
type (
jsonPathNode struct {
index *int
field string
}
JsonPath []jsonPathNode
)
func (p JsonPath) String() string {
var buf bytes.Buffer
for _, n := range p {
if n.index == nil {
buf.WriteString("." + n.field)
} else {
buf.WriteString(fmt.Sprintf("[%d]", *n.index))
}
}
return buf.String()
}
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)
switch old := old.(type) {
case map[string]interface{}:
res = append(res, jsonPaths(kPth, old)...)
case []interface{}:
res = append(res, jsonIterSlice(kPth, old)...)
}
}
return res
}
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})
res = append(res, iPth)
switch old := old.(type) {
case map[string]interface{}:
res = append(res, jsonPaths(iPth, old)...)
case []interface{}:
res = append(res, jsonIterSlice(iPth, old)...)
}
}
return res
}
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")
}
if pth[base].index != nil {
return nil, fmt.Errorf("index json path is invalid for object")
}
field, ok := j[pth[base].field]
if !ok || len(pth) == base+1 {
if len(pth) > base+1 {
return nil, fmt.Errorf("invalid non-terminal json path %q for non-existing field", pth)
}
return j[pth[base].field], nil
}
switch field := field.(type) {
case map[string]interface{}:
return JsonPathValue(field, pth, base+1)
case []interface{}:
return jsonPathValueSlice(field, pth, base+1)
default:
return nil, fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
}
}
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)
}
if pth[base].index == nil {
return nil, fmt.Errorf("field json path %q is invalid for object", pth[:base+1])
}
if *pth[base].index >= len(j) {
return nil, fmt.Errorf("invalid index %q for array of size %d", pth[:base+1], len(j))
}
if len(pth) == base+1 {
return j[*pth[base].index], nil
}
switch item := j[*pth[base].index].(type) {
case map[string]interface{}:
return JsonPathValue(item, pth, base+1)
case []interface{}:
return jsonPathValueSlice(item, pth, base+1)
default:
return nil, fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
}
}
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")
}
if pth[base].index != nil {
return fmt.Errorf("index json path is invalid for object")
}
field, ok := j[pth[base].field]
if !ok || len(pth) == base+1 {
if len(pth) > base+1 {
return fmt.Errorf("invalid non-terminal json path %q for non-existing field", pth)
}
j[pth[base].field] = runtime.DeepCopyJSONValue(value)
return nil
}
switch field := field.(type) {
case map[string]interface{}:
return SetJsonPath(field, pth, base+1, value)
case []interface{}:
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 {
if len(pth) == base {
return fmt.Errorf("empty json path %q is invalid for object", pth)
}
if pth[base].index == nil {
return fmt.Errorf("field json path %q is invalid for object", pth[:base+1])
}
if *pth[base].index >= len(j) {
return fmt.Errorf("invalid index %q for array of size %d", pth[:base+1], len(j))
}
if len(pth) == base+1 {
j[*pth[base].index] = runtime.DeepCopyJSONValue(value)
return nil
}
switch item := j[*pth[base].index].(type) {
case map[string]interface{}:
return SetJsonPath(item, pth, base+1, value)
case []interface{}:
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 {
if len(pth) == base {
return fmt.Errorf("empty json path is invalid for object")
}
if pth[base].index != nil {
return fmt.Errorf("index json path is invalid for object")
}
field, ok := j[pth[base].field]
if !ok || len(pth) == base+1 {
if len(pth) > base+1 {
return fmt.Errorf("invalid non-terminal json path %q for non-existing field", pth)
}
delete(j, pth[base].field)
return nil
}
switch field := field.(type) {
case map[string]interface{}:
return DeleteJsonPath(field, pth, base+1)
case []interface{}:
if len(pth) == base+2 {
if pth[base+1].index == nil {
return fmt.Errorf("field json path %q is invalid for object", pth)
}
j[pth[base].field] = append(field[:*pth[base+1].index], field[*pth[base+1].index+1:]...)
return nil
}
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 {
if len(pth) == base {
return fmt.Errorf("empty json path %q is invalid for object", pth)
}
if pth[base].index == nil {
return fmt.Errorf("field json path %q is invalid for object", pth[:base+1])
}
if *pth[base].index >= len(j) {
return fmt.Errorf("invalid index %q for array of size %d", pth[:base+1], len(j))
}
if len(pth) == base+1 {
return fmt.Errorf("cannot delete item at index %q in-place", pth[:base])
}
switch item := j[*pth[base].index].(type) {
case map[string]interface{}:
return DeleteJsonPath(item, pth, base+1)
case []interface{}:
if len(pth) == base+2 {
if pth[base+1].index == nil {
return fmt.Errorf("field json path %q is invalid for object", pth)
}
j[*pth[base].index] = append(item[:*pth[base+1].index], item[*pth[base+1].index+1:])
return nil
}
return deleteJsonPathSlice(item, pth, base+1)
default:
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
}
}

View File

@ -11,6 +11,7 @@ go_test(
srcs = [
"basic_test.go",
"finalization_test.go",
"objectmeta_test.go",
"registration_test.go",
"subresources_test.go",
"table_test.go",
@ -22,6 +23,7 @@ go_test(
tags = ["integration"],
deps = [
"//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/github.com/coreos/etcd/pkg/transport:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
@ -38,8 +40,10 @@ go_test(
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],

View File

@ -0,0 +1,166 @@
/*
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 integration
import (
"path"
"reflect"
"strings"
"testing"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/pkg/transport"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/json"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/dynamic"
)
func TestPostInvalidObjectMeta(t *testing.T) {
stopCh, apiExtensionClient, dynamicClient, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
noxuResourceClient := newNamespacedCustomResourceClient("default", dynamicClient, noxuDefinition)
obj := testserver.NewNoxuInstance("default", "foo")
unstructured.SetNestedField(obj.UnstructuredContent(), int64(42), "metadata", "unknown")
unstructured.SetNestedField(obj.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "metadata", "labels")
_, err = instantiateCustomResource(t, obj, noxuResourceClient, noxuDefinition)
if err == nil {
t.Fatalf("unexpected non-error, expected invalid labels to be rejected: %v", err)
}
if status, ok := err.(errors.APIStatus); !ok {
t.Fatalf("expected APIStatus error, but got: %#v", err)
} else if !errors.IsBadRequest(err) {
t.Fatalf("expected BadRequst error, but got: %v", errors.ReasonForError(err))
} else if !strings.Contains(status.Status().Message, "json: cannot unmarshal") {
t.Fatalf("expected 'json: cannot unmarshal' error message, got: %v", status.Status().Message)
}
unstructured.SetNestedField(obj.UnstructuredContent(), map[string]interface{}{"bar": "abc"}, "metadata", "labels")
obj, err = instantiateCustomResource(t, obj, noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if unknown, found, err := unstructured.NestedInt64(obj.UnstructuredContent(), "metadata", "unknown"); err != nil {
t.Errorf("unexpected error getting metadata.unknown: %v", err)
} else if found {
t.Errorf("unexpected metadata.unknown=%#v: expected this to be pruned", unknown)
}
}
func TestInvalidObjectMetaInStorage(t *testing.T) {
serverConfig, err := testserver.DefaultServerConfig()
if err != nil {
t.Fatal(err)
}
stopCh, config, err := testserver.StartServer(serverConfig)
defer close(stopCh)
if err != nil {
t.Fatal(err)
}
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
restOptions, err := serverConfig.GenericConfig.RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: noxuDefinition.Spec.Group, Resource: noxuDefinition.Spec.Names.Plural})
if err != nil {
t.Fatal(err)
}
tlsInfo := transport.TLSInfo{
CertFile: restOptions.StorageConfig.CertFile,
KeyFile: restOptions.StorageConfig.KeyFile,
CAFile: restOptions.StorageConfig.CAFile,
}
tlsConfig, err := tlsInfo.ClientConfig()
if err != nil {
t.Fatal(err)
}
etcdConfig := clientv3.Config{
Endpoints: restOptions.StorageConfig.ServerList,
TLS: tlsConfig,
}
etcdclient, err := clientv3.New(etcdConfig)
if err != nil {
t.Fatal(err)
}
t.Logf("Creating object with invalid labels manually in etcd")
original := testserver.NewNoxuInstance("default", "foo")
unstructured.SetNestedField(original.UnstructuredContent(), int64(42), "metadata", "unknown")
unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "metadata", "labels")
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := path.Join("/", restOptions.StorageConfig.Prefix, noxuDefinition.Spec.Group, "noxus/default/foo")
val, _ := json.Marshal(original.UnstructuredContent())
if _, err := etcdclient.Put(ctx, key, string(val)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
t.Logf("Checking that ObjectMeta is pruned from unknown fields")
noxuResourceClient := newNamespacedCustomResourceClient("default", dynamicClient, noxuDefinition)
obj, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if unknown, found, err := unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "metadata", "unknown"); err != nil {
t.Errorf("unexpected error: %v", err)
} else if found {
t.Errorf("unexpected to find metadata.unknown=%#v", unknown)
}
t.Logf("Checking that ObjectMeta is pruned from invalid typed fields")
if labels, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "metadata", "labels"); err != nil {
t.Errorf("unexpected error: %v", err)
} else if found && !reflect.DeepEqual(labels, map[string]string{"bar": "abc"}) {
t.Errorf("unexpected to find metadata.lables=%#v", labels)
}
}

View File

@ -181,16 +181,41 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
j.Kind = ""
},
func(j *metav1.ObjectMeta, c fuzz.Continue) {
j.Name = c.RandString()
c.FuzzNoCustom(j)
j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10)
j.SelfLink = c.RandString()
j.UID = types.UID(c.RandString())
j.GenerateName = c.RandString()
var sec, nsec int64
c.Fuzz(&sec)
c.Fuzz(&nsec)
j.CreationTimestamp = metav1.Unix(sec, nsec).Rfc3339Copy()
if j.DeletionTimestamp != nil {
c.Fuzz(&sec)
c.Fuzz(&nsec)
t := metav1.Unix(sec, nsec).Rfc3339Copy()
j.DeletionTimestamp = &t
}
if len(j.Labels) == 0 {
j.Labels = nil
}
if len(j.Annotations) == 0 {
j.Annotations = nil
}
if len(j.OwnerReferences) == 0 {
j.OwnerReferences = nil
}
if len(j.Finalizers) == 0 {
j.Finalizers = nil
}
},
func(j *metav1.Initializers, c fuzz.Continue) {
c.FuzzNoCustom(j)
if len(j.Pending) == 0 {
j.Pending = nil
}
},
func(j *metav1.ListMeta, c fuzz.Continue) {
j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10)