diff --git a/pkg/runtime/embedded_test.go b/pkg/runtime/embedded_test.go index c9ad19725f5..90aae83f1fe 100644 --- a/pkg/runtime/embedded_test.go +++ b/pkg/runtime/embedded_test.go @@ -22,16 +22,14 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) -var scheme = runtime.NewScheme() -var Codec = runtime.CodecFor(scheme, "v1test") - type EmbeddedTest struct { - runtime.TypeMeta `json:",inline"` - ID string `json:"id,omitempty"` - Object runtime.EmbeddedObject `json:"object,omitempty"` - EmptyObject runtime.EmbeddedObject `json:"emptyObject,omitempty"` + runtime.TypeMeta + ID string + Object runtime.EmbeddedObject + EmptyObject runtime.EmbeddedObject } type EmbeddedTestExternal struct { @@ -41,21 +39,91 @@ type EmbeddedTestExternal struct { EmptyObject runtime.RawExtension `json:"emptyObject,omitempty"` } +type ObjectTest struct { + runtime.TypeMeta + + ID string + Items []runtime.Object +} + +type ObjectTestExternal struct { + runtime.TypeMeta `yaml:",inline" json:",inline"` + + ID string `json:"id,omitempty"` + Items []runtime.RawExtension `json:"items,omitempty"` +} + +func (*ObjectTest) IsAnAPIObject() {} +func (*ObjectTestExternal) IsAnAPIObject() {} func (*EmbeddedTest) IsAnAPIObject() {} func (*EmbeddedTestExternal) IsAnAPIObject() {} +func TestDecodeEmptyRawExtensionAsObject(t *testing.T) { + s := runtime.NewScheme() + s.AddKnownTypes("", &ObjectTest{}) + s.AddKnownTypeWithName("v1test", "ObjectTest", &ObjectTestExternal{}) + + _, err := s.Decode([]byte(`{"kind":"ObjectTest","apiVersion":"v1test","items":[{}]}`)) + if err == nil { + t.Fatalf("unexpected non-error") + } +} + +func TestArrayOfRuntimeObject(t *testing.T) { + s := runtime.NewScheme() + s.AddKnownTypes("", &EmbeddedTest{}) + s.AddKnownTypeWithName("v1test", "EmbeddedTest", &EmbeddedTestExternal{}) + s.AddKnownTypes("", &ObjectTest{}) + s.AddKnownTypeWithName("v1test", "ObjectTest", &ObjectTestExternal{}) + + internal := &ObjectTest{ + Items: []runtime.Object{ + &EmbeddedTest{ID: "foo"}, + &EmbeddedTest{ID: "bar"}, + // TODO: until YAML is removed, this JSON must be in ascending key order to ensure consistent roundtrip serialization + &runtime.Unknown{RawJSON: []byte(`{"apiVersion":"unknown","foo":"bar","kind":"OtherTest"}`)}, + &ObjectTest{ + Items: []runtime.Object{ + &EmbeddedTest{ID: "baz"}, + }, + }, + }, + } + wire, err := s.EncodeToVersion(internal, "v1test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + t.Logf("Wire format is:\n%s\n", string(wire)) + + obj := &ObjectTestExternal{} + if err := json.Unmarshal(wire, obj); err != nil { + t.Fatalf("unexpected error: %v", err) + } + t.Logf("exact wire is: %#v", string(obj.Items[0].RawJSON)) + + decoded, err := s.Decode(wire) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + internal.Items[2].(*runtime.Unknown).Kind = "OtherTest" + internal.Items[2].(*runtime.Unknown).APIVersion = "unknown" + if e, a := internal, decoded; !reflect.DeepEqual(e, a) { + t.Log(string(decoded.(*ObjectTest).Items[2].(*runtime.Unknown).RawJSON)) + t.Errorf("mismatched decoded: %s", util.ObjectDiff(e, a)) + } +} + func TestEmbeddedObject(t *testing.T) { - s := scheme + s := runtime.NewScheme() s.AddKnownTypes("", &EmbeddedTest{}) s.AddKnownTypeWithName("v1test", "EmbeddedTest", &EmbeddedTestExternal{}) outer := &EmbeddedTest{ - TypeMeta: runtime.TypeMeta{}, - ID: "outer", + ID: "outer", Object: runtime.EmbeddedObject{ &EmbeddedTest{ - TypeMeta: runtime.TypeMeta{}, - ID: "inner", + ID: "inner", }, }, } diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 3984eabd2d5..5227607555a 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -140,15 +140,77 @@ func (self *Scheme) rawExtensionToEmbeddedObject(in *RawExtension, out *Embedded return nil } +// runtimeObjectToRawExtensionArray takes a list of objects and encodes them as RawExtension in the output version +// defined by the conversion.Scope. If objects must be encoded to different schema versions you should set them as +// runtime.Unknown in the internal version instead. +func (self *Scheme) runtimeObjectToRawExtensionArray(in *[]Object, out *[]RawExtension, s conversion.Scope) error { + src := *in + dest := make([]RawExtension, len(src)) + + _, outVersion, scheme := self.fromScope(s) + + for i := range src { + switch t := src[i].(type) { + case *Unknown: + dest[i].RawJSON = t.RawJSON + default: + data, err := scheme.EncodeToVersion(src[i], outVersion) + if err != nil { + return err + } + dest[i].RawJSON = data + } + } + *out = dest + return nil +} + +// rawExtensionToRuntimeObjectArray attempts to decode objects from the array - if they are unrecognized objects, +// they are added as Unknown. +func (self *Scheme) rawExtensionToRuntimeObjectArray(in *[]RawExtension, out *[]Object, s conversion.Scope) error { + src := *in + dest := make([]Object, len(src)) + + _, _, scheme := self.fromScope(s) + + for i := range src { + data := src[i].RawJSON + obj, err := scheme.Decode(data) + if err != nil { + if !IsNotRegisteredError(err) { + return err + } + version, kind, err := scheme.raw.DataVersionAndKind(data) + if err != nil { + return err + } + obj = &Unknown{ + TypeMeta: TypeMeta{ + APIVersion: version, + Kind: kind, + }, + RawJSON: data, + } + } + dest[i] = obj + } + *out = dest + return nil +} + // NewScheme creates a new Scheme. This scheme is pluggable by default. func NewScheme() *Scheme { s := &Scheme{conversion.NewScheme()} s.raw.InternalVersion = "" s.raw.MetaFactory = conversion.SimpleMetaFactory{BaseFields: []string{"TypeMeta"}, VersionField: "APIVersion", KindField: "Kind"} - s.raw.AddConversionFuncs( + if err := s.raw.AddConversionFuncs( s.embeddedObjectToRawExtension, s.rawExtensionToEmbeddedObject, - ) + s.runtimeObjectToRawExtensionArray, + s.rawExtensionToRuntimeObjectArray, + ); err != nil { + panic(err) + } return s }