diff --git a/cmd/gendeepcopy/deep_copy.go b/cmd/gendeepcopy/deep_copy.go index a10bfe3a011..56fa74b03dc 100644 --- a/cmd/gendeepcopy/deep_copy.go +++ b/cmd/gendeepcopy/deep_copy.go @@ -104,7 +104,7 @@ func main() { } else { pkgname = gv.Group } - if len(gv.Version) != 0 { + if len(gv.Version) != 0 && gv.Version != kruntime.APIVersionInternal { pkgname = gv.Version } diff --git a/pkg/runtime/codec.go b/pkg/runtime/codec.go index 3539388a4c3..bde0ae9755d 100644 --- a/pkg/runtime/codec.go +++ b/pkg/runtime/codec.go @@ -17,99 +17,155 @@ limitations under the License. package runtime import ( + "bytes" + "fmt" "io" + "net/url" + "reflect" "k8s.io/kubernetes/pkg/api/unversioned" - "k8s.io/kubernetes/pkg/util/yaml" + "k8s.io/kubernetes/pkg/conversion/queryparams" ) +// codec binds an encoder and decoder. +type codec struct { + Encoder + Decoder +} + +// NewCodec creates a Codec from an Encoder and Decoder. +func NewCodec(e Encoder, d Decoder) Codec { + return codec{e, d} +} + // Encode is a convenience wrapper for encoding to a []byte from an Encoder -// TODO: these are transitional interfaces to reduce refactor cost as Codec is altered. -func Encode(e Encoder, obj Object) ([]byte, error) { - return e.Encode(obj) +func Encode(e Encoder, obj Object, overrides ...unversioned.GroupVersion) ([]byte, error) { + // TODO: reuse buffer + buf := &bytes.Buffer{} + if err := e.EncodeToStream(obj, buf, overrides...); err != nil { + return nil, err + } + return buf.Bytes(), nil } // Decode is a convenience wrapper for decoding data into an Object. -// TODO: these are transitional interfaces to reduce refactor cost as Codec is altered. func Decode(d Decoder, data []byte) (Object, error) { - return d.Decode(data) + obj, _, err := d.Decode(data, nil, nil) + return obj, err } // DecodeInto performs a Decode into the provided object. -// TODO: these are transitional interfaces to reduce refactor cost as Codec is altered. func DecodeInto(d Decoder, data []byte, into Object) error { - return d.DecodeInto(data, into) -} - -// CodecFor returns a Codec that invokes Encode with the provided version. -func CodecFor(codec ObjectCodec, version unversioned.GroupVersion) Codec { - return &codecWrapper{codec, version} -} - -// yamlCodec converts YAML passed to the Decoder methods to JSON. -type yamlCodec struct { - // a Codec for JSON - Codec -} - -// yamlCodec implements Codec -var _ Codec = yamlCodec{} -var _ Decoder = yamlCodec{} - -// YAMLDecoder adds YAML decoding support to a codec that supports JSON. -func YAMLDecoder(codec Codec) Codec { - return &yamlCodec{codec} -} - -func (c yamlCodec) Decode(data []byte) (Object, error) { - out, err := yaml.ToJSON(data) - if err != nil { - return nil, err - } - data = out - return c.Codec.Decode(data) -} - -func (c yamlCodec) DecodeInto(data []byte, obj Object) error { - out, err := yaml.ToJSON(data) + out, gvk, err := d.Decode(data, nil, into) if err != nil { return err } - data = out - return c.Codec.DecodeInto(data, obj) + if out != into { + return fmt.Errorf("unable to decode %s into %v", gvk, reflect.TypeOf(into)) + } + return nil } // EncodeOrDie is a version of Encode which will panic instead of returning an error. For tests. -func EncodeOrDie(codec Codec, obj Object) string { - bytes, err := Encode(codec, obj) +func EncodeOrDie(e Encoder, obj Object) string { + bytes, err := Encode(e, obj) if err != nil { panic(err) } return string(bytes) } -// codecWrapper implements encoding to an alternative -// default version for a scheme. -type codecWrapper struct { - ObjectCodec - version unversioned.GroupVersion +// UseOrCreateObject returns obj if the canonical ObjectKind returned by the provided typer matches gvk, or +// invokes the ObjectCreator to instantiate a new gvk. Returns an error if the typer cannot find the object. +func UseOrCreateObject(t Typer, c ObjectCreater, gvk unversioned.GroupVersionKind, obj Object) (Object, error) { + if obj != nil { + into, _, err := t.ObjectKind(obj) + if err != nil { + return nil, err + } + if gvk == *into { + return obj, nil + } + } + return c.New(gvk) } -// codecWrapper implements Decoder -var _ Decoder = &codecWrapper{} - -// Encode implements Codec -func (c *codecWrapper) Encode(obj Object) ([]byte, error) { - return c.EncodeToVersion(obj, c.version.String()) +// NoopEncoder converts an Decoder to a Serializer or Codec for code that expects them but only uses decoding. +type NoopEncoder struct { + Decoder } -func (c *codecWrapper) EncodeToStream(obj Object, stream io.Writer) error { - return c.EncodeToVersionStream(obj, c.version.String(), stream) +var _ Serializer = NoopEncoder{} + +func (n NoopEncoder) EncodeToStream(obj Object, w io.Writer, overrides ...unversioned.GroupVersion) error { + return fmt.Errorf("encoding is not allowed for this codec: %v", reflect.TypeOf(n.Decoder)) } -// TODO: Make this behaviour default when we move everyone away from -// the unversioned types. -// -// func (c *codecWrapper) Decode(data []byte) (Object, error) { -// return c.DecodeToVersion(data, c.version) -// } +// NoopDecoder converts an Encoder to a Serializer or Codec for code that expects them but only uses encoding. +type NoopDecoder struct { + Encoder +} + +var _ Serializer = NoopDecoder{} + +func (n NoopDecoder) Decode(data []byte, gvk *unversioned.GroupVersionKind, into Object) (Object, *unversioned.GroupVersionKind, error) { + return nil, nil, fmt.Errorf("decoding is not allowed for this codec: %v", reflect.TypeOf(n.Encoder)) +} + +// NewParameterCodec creates a ParameterCodec capable of transforming url values into versioned objects and back. +func NewParameterCodec(scheme *Scheme) ParameterCodec { + return ¶meterCodec{ + typer: ObjectTyperToTyper(scheme), + convertor: scheme, + creator: scheme, + } +} + +// parameterCodec implements conversion to and from query parameters and objects. +type parameterCodec struct { + typer Typer + convertor ObjectConvertor + creator ObjectCreater +} + +var _ ParameterCodec = ¶meterCodec{} + +// DecodeParameters converts the provided url.Values into an object of type From with the kind of into, and then +// converts that object to into (if necessary). Returns an error if the operation cannot be completed. +func (c *parameterCodec) DecodeParameters(parameters url.Values, from unversioned.GroupVersion, into Object) error { + if len(parameters) == 0 { + return nil + } + targetGVK, _, err := c.typer.ObjectKind(into) + if err != nil { + return err + } + if targetGVK.GroupVersion() == from { + return c.convertor.Convert(¶meters, into) + } + input, err := c.creator.New(from.WithKind(targetGVK.Kind)) + if err != nil { + return err + } + if err := c.convertor.Convert(¶meters, input); err != nil { + return err + } + return c.convertor.Convert(input, into) +} + +// EncodeParameters converts the provided object into the to version, then converts that object to url.Values. +// Returns an error if conversion is not possible. +func (c *parameterCodec) EncodeParameters(obj Object, to unversioned.GroupVersion) (url.Values, error) { + gvk, _, err := c.typer.ObjectKind(obj) + if err != nil { + return nil, err + } + if to != gvk.GroupVersion() { + out, err := c.convertor.ConvertToVersion(obj, to.String()) + if err != nil { + return nil, err + } + obj = out + } + return queryparams.Convert(obj) +} diff --git a/pkg/runtime/conversion_generator.go b/pkg/runtime/conversion_generator.go index 54e2f0a9cc8..6d70224e9be 100644 --- a/pkg/runtime/conversion_generator.go +++ b/pkg/runtime/conversion_generator.go @@ -102,10 +102,8 @@ func (g *conversionGenerator) AddImport(pkg string) string { func (g *conversionGenerator) GenerateConversionsForType(gv unversioned.GroupVersion, reflection reflect.Type) error { kind := reflection.Name() // TODO this is equivalent to what it did before, but it needs to be fixed for the proper group - internalVersion, exists := g.scheme.InternalVersions[gv.Group] - if !exists { - return fmt.Errorf("no internal version for %v", gv) - } + internalVersion := gv + internalVersion.Version = APIVersionInternal internalObj, err := g.scheme.NewObject(internalVersion.WithKind(kind)) if err != nil { @@ -775,6 +773,10 @@ func (g *conversionGenerator) writeConversionForStruct(b *buffer, inType, outTyp continue } + if g.scheme.Converter().IsConversionIgnored(inField.Type, outField.Type) { + continue + } + existsConversion := g.scheme.Converter().HasConversionFunc(inField.Type, outField.Type) _, hasPublicConversion := g.publicFuncs[typePair{inField.Type, outField.Type}] // TODO: This allows a private conversion for a slice to take precedence over a public @@ -895,12 +897,7 @@ type typePair struct { outType reflect.Type } -var defaultConversions []typePair = []typePair{ - {reflect.TypeOf([]RawExtension{}), reflect.TypeOf([]Object{})}, - {reflect.TypeOf([]Object{}), reflect.TypeOf([]RawExtension{})}, - {reflect.TypeOf(RawExtension{}), reflect.TypeOf(EmbeddedObject{})}, - {reflect.TypeOf(EmbeddedObject{}), reflect.TypeOf(RawExtension{})}, -} +var defaultConversions []typePair = []typePair{} func (g *conversionGenerator) OverwritePackage(pkg, overwrite string) { g.pkgOverwrites[pkg] = overwrite diff --git a/pkg/runtime/conversion_test.go b/pkg/runtime/conversion_test.go index cf31550839a..6105e5aad5a 100644 --- a/pkg/runtime/conversion_test.go +++ b/pkg/runtime/conversion_test.go @@ -46,12 +46,11 @@ func (obj *InternalComplex) GetObjectKind() unversioned.ObjectKind { return &obj func (obj *ExternalComplex) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func TestStringMapConversion(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "external"} scheme := runtime.NewScheme() scheme.Log(t) - scheme.AddInternalGroupVersion(internalGV) scheme.AddKnownTypeWithName(internalGV.WithKind("Complex"), &InternalComplex{}) scheme.AddKnownTypeWithName(externalGV.WithKind("Complex"), &ExternalComplex{}) diff --git a/pkg/runtime/embedded.go b/pkg/runtime/embedded.go new file mode 100644 index 00000000000..0934d6837c2 --- /dev/null +++ b/pkg/runtime/embedded.go @@ -0,0 +1,124 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 runtime + +import ( + "errors" + + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/conversion" +) + +type encodable struct { + e Encoder `json:"-"` + obj Object + versions []unversioned.GroupVersion `json:"-"` +} + +func (e encodable) GetObjectKind() unversioned.ObjectKind { return e.obj.GetObjectKind() } + +// NewEncodable creates an object that will be encoded with the provided codec on demand. +// Provided as a convenience for test cases dealing with internal objects. +func NewEncodable(e Encoder, obj Object, versions ...unversioned.GroupVersion) Object { + if _, ok := obj.(*Unknown); ok { + return obj + } + return encodable{e, obj, versions} +} + +func (re encodable) UnmarshalJSON(in []byte) error { + return errors.New("runtime.encodable cannot be unmarshalled from JSON") +} + +// Marshal may get called on pointers or values, so implement MarshalJSON on value. +// http://stackoverflow.com/questions/21390979/custom-marshaljson-never-gets-called-in-go +func (re encodable) MarshalJSON() ([]byte, error) { + return Encode(re.e, re.obj) +} + +// NewEncodableList creates an object that will be encoded with the provided codec on demand. +// Provided as a convenience for test cases dealing with internal objects. +func NewEncodableList(e Encoder, objects []Object, versions ...unversioned.GroupVersion) []Object { + out := make([]Object, len(objects)) + for i := range objects { + if _, ok := objects[i].(*Unknown); ok { + out[i] = objects[i] + continue + } + out[i] = NewEncodable(e, objects[i], versions...) + } + return out +} + +func (re *Unknown) UnmarshalJSON(in []byte) error { + if re == nil { + return errors.New("runtime.Unknown: UnmarshalJSON on nil pointer") + } + re.TypeMeta = TypeMeta{} + re.RawJSON = append(re.RawJSON[0:0], in...) + return nil +} + +// Marshal may get called on pointers or values, so implement MarshalJSON on value. +// http://stackoverflow.com/questions/21390979/custom-marshaljson-never-gets-called-in-go +func (re Unknown) MarshalJSON() ([]byte, error) { + if re.RawJSON == nil { + return []byte("null"), nil + } + return re.RawJSON, nil +} + +func DefaultEmbeddedConversions() []interface{} { + return []interface{}{ + func(in *Object, out *RawExtension, s conversion.Scope) error { + if in == nil { + out.RawJSON = []byte("null") + return nil + } + obj := *in + if unk, ok := obj.(*Unknown); ok { + if unk.RawJSON != nil { + out.RawJSON = unk.RawJSON + return nil + } + obj = out.Object + } + if obj == nil { + out.RawJSON = nil + return nil + } + out.Object = obj + return nil + }, + + func(in *RawExtension, out *Object, s conversion.Scope) error { + if in.Object != nil { + *out = in.Object + return nil + } + data := in.RawJSON + if len(data) == 0 || (len(data) == 4 && string(data) == "null") { + *out = nil + return nil + } + *out = &Unknown{ + RawJSON: data, + } + return nil + }, + } +} diff --git a/pkg/runtime/embedded_test.go b/pkg/runtime/embedded_test.go index c0b9c867f99..64d6d74fb94 100644 --- a/pkg/runtime/embedded_test.go +++ b/pkg/runtime/embedded_test.go @@ -19,20 +19,21 @@ package runtime_test import ( "encoding/json" "reflect" + "strings" "testing" - "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/runtime/serializer" "k8s.io/kubernetes/pkg/util" ) type EmbeddedTest struct { runtime.TypeMeta ID string - Object runtime.EmbeddedObject - EmptyObject runtime.EmbeddedObject + Object runtime.Object + EmptyObject runtime.Object } type EmbeddedTestExternal struct { @@ -62,16 +63,17 @@ func (obj *EmbeddedTest) GetObjectKind() unversioned.ObjectKind { return func (obj *EmbeddedTestExternal) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func TestDecodeEmptyRawExtensionAsObject(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "v1test"} externalGVK := externalGV.WithKind("ObjectTest") s := runtime.NewScheme() - s.AddInternalGroupVersion(internalGV) s.AddKnownTypes(internalGV, &ObjectTest{}) s.AddKnownTypeWithName(externalGVK, &ObjectTestExternal{}) - obj, err := s.Decode([]byte(`{"kind":"` + externalGVK.Kind + `","apiVersion":"` + externalGV.String() + `","items":[{}]}`)) + codec := serializer.NewCodecFactory(s).LegacyCodec(externalGV) + + obj, gvk, err := codec.Decode([]byte(`{"kind":"`+externalGVK.Kind+`","apiVersion":"`+externalGV.String()+`","items":[{}]}`), nil, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -79,42 +81,51 @@ func TestDecodeEmptyRawExtensionAsObject(t *testing.T) { if unk, ok := test.Items[0].(*runtime.Unknown); !ok || unk.Kind != "" || unk.APIVersion != "" || string(unk.RawJSON) != "{}" { t.Fatalf("unexpected object: %#v", test.Items[0]) } + if *gvk != externalGVK { + t.Fatalf("unexpected kind: %#v", gvk) + } - obj, err = s.Decode([]byte(`{"kind":"` + externalGVK.Kind + `","apiVersion":"` + externalGV.String() + `","items":[{"kind":"Other","apiVersion":"v1"}]}`)) + obj, gvk, err = codec.Decode([]byte(`{"kind":"`+externalGVK.Kind+`","apiVersion":"`+externalGV.String()+`","items":[{"kind":"Other","apiVersion":"v1"}]}`), nil, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } test = obj.(*ObjectTest) - if unk, ok := test.Items[0].(*runtime.Unknown); !ok || unk.Kind != "Other" || unk.APIVersion != "v1" || string(unk.RawJSON) != `{"kind":"Other","apiVersion":"v1"}` { + if unk, ok := test.Items[0].(*runtime.Unknown); !ok || unk.Kind != "" || unk.APIVersion != "" || string(unk.RawJSON) != `{"kind":"Other","apiVersion":"v1"}` { t.Fatalf("unexpected object: %#v", test.Items[0]) } + if *gvk != externalGVK { + t.Fatalf("unexpected kind: %#v", gvk) + } } func TestArrayOfRuntimeObject(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "v1test"} s := runtime.NewScheme() - s.AddInternalGroupVersion(internalGV) s.AddKnownTypes(internalGV, &EmbeddedTest{}) s.AddKnownTypeWithName(externalGV.WithKind("EmbeddedTest"), &EmbeddedTestExternal{}) s.AddKnownTypes(internalGV, &ObjectTest{}) s.AddKnownTypeWithName(externalGV.WithKind("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.group/unknown","foo":"bar","kind":"OtherTest"}`)}, - &ObjectTest{ - Items: []runtime.Object{ - &EmbeddedTest{ID: "baz"}, - }, - }, + codec := serializer.NewCodecFactory(s).LegacyCodec(externalGV) + + innerItems := []runtime.Object{ + &EmbeddedTest{ID: "baz"}, + } + 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.group/unknown","foo":"bar","kind":"OtherTest"}`)}, + &ObjectTest{ + Items: runtime.NewEncodableList(codec, innerItems), }, } - wire, err := s.EncodeToVersion(internal, externalGV.String()) + internal := &ObjectTest{ + Items: runtime.NewEncodableList(codec, items), + } + wire, err := runtime.Encode(codec, internal) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -126,7 +137,10 @@ func TestArrayOfRuntimeObject(t *testing.T) { } t.Logf("exact wire is: %s", string(obj.Items[0].RawJSON)) - decoded, err := runtime.Decode(s, wire) + items[3] = &ObjectTest{Items: innerItems} + internal.Items = items + + decoded, err := runtime.Decode(codec, wire) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -134,7 +148,7 @@ func TestArrayOfRuntimeObject(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if errs := runtime.DecodeList(list, s); len(errs) > 0 { + if errs := runtime.DecodeList(list, codec); len(errs) > 0 { t.Fatalf("unexpected error: %v", errs) } @@ -142,53 +156,65 @@ func TestArrayOfRuntimeObject(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if errs := runtime.DecodeList(list2, s); len(errs) > 0 { + if errs := runtime.DecodeList(list2, codec); len(errs) > 0 { t.Fatalf("unexpected error: %v", errs) } if err := meta.SetList(list[3], list2); err != nil { t.Fatalf("unexpected error: %v", err) } - internal.Items[2].(*runtime.Unknown).Kind = "OtherTest" - internal.Items[2].(*runtime.Unknown).APIVersion = "unknown.group/unknown" + // we want DecodeList to set type meta if possible, even on runtime.Unknown objects + internal.Items[2].(*runtime.Unknown).TypeMeta = runtime.TypeMeta{Kind: "OtherTest", APIVersion: "unknown.group/unknown"} if e, a := internal.Items, list; !reflect.DeepEqual(e, a) { - t.Errorf("mismatched decoded: %s", util.ObjectDiff(e, a)) + t.Errorf("mismatched decoded: %s", util.ObjectGoPrintSideBySide(e, a)) } } -func TestEmbeddedObject(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} +func TestNestedObject(t *testing.T) { + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "v1test"} embeddedTestExternalGVK := externalGV.WithKind("EmbeddedTest") s := runtime.NewScheme() - s.AddInternalGroupVersion(internalGV) s.AddKnownTypes(internalGV, &EmbeddedTest{}) s.AddKnownTypeWithName(embeddedTestExternalGVK, &EmbeddedTestExternal{}) + codec := serializer.NewCodecFactory(s).LegacyCodec(externalGV) + + inner := &EmbeddedTest{ + ID: "inner", + } outer := &EmbeddedTest{ - ID: "outer", - Object: runtime.EmbeddedObject{ - Object: &EmbeddedTest{ - ID: "inner", - }, - }, + ID: "outer", + Object: runtime.NewEncodable(codec, inner), } - wire, err := s.EncodeToVersion(outer, externalGV.String()) + wire, err := runtime.Encode(codec, outer) if err != nil { t.Fatalf("Unexpected encode error '%v'", err) } t.Logf("Wire format is:\n%v\n", string(wire)) - decoded, err := runtime.Decode(s, wire) + decoded, err := runtime.Decode(codec, wire) if err != nil { t.Fatalf("Unexpected decode error %v", err) } + // for later tests + outer.Object = inner + + if e, a := outer, decoded; reflect.DeepEqual(e, a) { + t.Errorf("Expected unequal %#v %#v", e, a) + } + + obj, err := runtime.Decode(codec, decoded.(*EmbeddedTest).Object.(*runtime.Unknown).RawJSON) + if err != nil { + t.Fatal(err) + } + decoded.(*EmbeddedTest).Object = obj if e, a := outer, decoded; !reflect.DeepEqual(e, a) { - t.Errorf("Expected: %#v but got %#v", e, a) + t.Errorf("Expected equal %#v %#v", e, a) } // test JSON decoding of the external object, which should preserve @@ -211,46 +237,45 @@ func TestEmbeddedObject(t *testing.T) { // the external representation var decodedViaJSON EmbeddedTest err = json.Unmarshal(wire, &decodedViaJSON) - if err != nil { + if err == nil || !strings.Contains(err.Error(), "unmarshal object into Go value of type runtime.Object") { t.Fatalf("Unexpected decode error %v", err) } - if a := decodedViaJSON; a.Object.Object != nil || a.EmptyObject.Object != nil { + if a := decodedViaJSON; a.Object != nil || a.EmptyObject != nil { t.Errorf("Expected embedded objects to be nil: %#v", a) } } -// TestDeepCopyOfEmbeddedObject checks to make sure that EmbeddedObject's can be passed through DeepCopy with fidelity -func TestDeepCopyOfEmbeddedObject(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} +// TestDeepCopyOfRuntimeObject checks to make sure that runtime.Objects's can be passed through DeepCopy with fidelity +func TestDeepCopyOfRuntimeObject(t *testing.T) { + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "v1test"} embeddedTestExternalGVK := externalGV.WithKind("EmbeddedTest") s := runtime.NewScheme() - s.AddInternalGroupVersion(internalGV) s.AddKnownTypes(internalGV, &EmbeddedTest{}) s.AddKnownTypeWithName(embeddedTestExternalGVK, &EmbeddedTestExternal{}) original := &EmbeddedTest{ ID: "outer", - Object: runtime.EmbeddedObject{ - Object: &EmbeddedTest{ - ID: "inner", - }, + Object: &EmbeddedTest{ + ID: "inner", }, } - originalData, err := s.EncodeToVersion(original, externalGV.String()) + codec := serializer.NewCodecFactory(s).LegacyCodec(externalGV) + + originalData, err := runtime.Encode(codec, original) if err != nil { t.Errorf("unexpected error: %v", err) } t.Logf("originalRole = %v\n", string(originalData)) - copyOfOriginal, err := api.Scheme.DeepCopy(original) + copyOfOriginal, err := s.DeepCopy(original) if err != nil { t.Fatalf("unexpected error: %v", err) } - copiedData, err := s.EncodeToVersion(copyOfOriginal.(runtime.Object), externalGV.String()) + copiedData, err := runtime.Encode(codec, copyOfOriginal.(runtime.Object)) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/pkg/runtime/error.go b/pkg/runtime/error.go index 40fa2437a6d..cbdc9b66105 100644 --- a/pkg/runtime/error.go +++ b/pkg/runtime/error.go @@ -37,3 +37,11 @@ func IsMissingKind(err error) bool { func IsMissingVersion(err error) bool { return conversion.IsMissingVersion(err) } + +func NewMissingKindErr(data string) error { + return conversion.NewMissingKindErr(data) +} + +func NewMissingVersionErr(data string) error { + return conversion.NewMissingVersionErr(data) +} diff --git a/pkg/runtime/extension.go b/pkg/runtime/extension.go index 2194ea3a9e9..629f675b69e 100644 --- a/pkg/runtime/extension.go +++ b/pkg/runtime/extension.go @@ -16,7 +16,10 @@ limitations under the License. package runtime -import "errors" +import ( + "encoding/json" + "errors" +) func (re *RawExtension) UnmarshalJSON(in []byte) error { if re == nil { @@ -29,5 +32,16 @@ func (re *RawExtension) UnmarshalJSON(in []byte) error { // Marshal may get called on pointers or values, so implement MarshalJSON on value. // http://stackoverflow.com/questions/21390979/custom-marshaljson-never-gets-called-in-go func (re RawExtension) MarshalJSON() ([]byte, error) { + if re.RawJSON == nil { + // TODO: this is to support legacy behavior of JSONPrinter and YAMLPrinter, which + // expect to call json.Marshal on arbitrary versioned objects (even those not in + // the scheme). pkg/kubectl/resource#AsVersionedObjects and its interaction with + // kubectl get on objects not in the scheme needs to be updated to ensure that the + // objects that are not part of the scheme are correctly put into the right form. + if re.Object != nil { + return json.Marshal(re.Object) + } + return []byte("null"), nil + } return re.RawJSON, nil } diff --git a/pkg/runtime/helper.go b/pkg/runtime/helper.go index 9b6c57cd659..4a76e81dc9f 100644 --- a/pkg/runtime/helper.go +++ b/pkg/runtime/helper.go @@ -22,8 +22,30 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/conversion" + "k8s.io/kubernetes/pkg/util/errors" ) +type objectTyperToTyper struct { + typer ObjectTyper +} + +func (t objectTyperToTyper) ObjectKind(obj Object) (*unversioned.GroupVersionKind, bool, error) { + gvk, err := t.typer.ObjectKind(obj) + if err != nil { + return nil, false, err + } + unversionedType, ok := t.typer.IsUnversioned(obj) + if !ok { + // ObjectTyper violates its contract + return nil, false, fmt.Errorf("typer returned a kind for %v, but then reported it was not in the scheme with IsUnversioned", reflect.TypeOf(obj)) + } + return &gvk, unversionedType, nil +} + +func ObjectTyperToTyper(typer ObjectTyper) Typer { + return objectTyperToTyper{typer: typer} +} + // fieldPtr puts the address of fieldName, which must be a member of v, // into dest, which must be an address of a variable to which this field's // address can be assigned. @@ -48,32 +70,56 @@ func FieldPtr(v reflect.Value, fieldName string, dest interface{}) error { return fmt.Errorf("couldn't assign/convert %v to %v", field.Type(), v.Type()) } +// EncodeList ensures that each object in an array is converted to a Unknown{} in serialized form. +// TODO: accept a content type. +func EncodeList(e Encoder, objects []Object, overrides ...unversioned.GroupVersion) error { + var errs []error + for i := range objects { + data, err := Encode(e, objects[i], overrides...) + if err != nil { + errs = append(errs, err) + continue + } + objects[i] = &Unknown{RawJSON: data} + } + return errors.NewAggregate(errs) +} + +func decodeListItem(obj *Unknown, decoders []Decoder) (Object, error) { + for _, decoder := range decoders { + obj, err := Decode(decoder, obj.RawJSON) + if err != nil { + if IsNotRegisteredError(err) { + continue + } + return nil, err + } + return obj, nil + } + // could not decode, so leave the object as Unknown, but give the decoders the + // chance to set Unknown.TypeMeta if it is available. + for _, decoder := range decoders { + if err := DecodeInto(decoder, obj.RawJSON, obj); err == nil { + return obj, nil + } + } + return obj, nil +} + // DecodeList alters the list in place, attempting to decode any objects found in -// the list that have the runtime.Unknown type. Any errors that occur are returned +// the list that have the Unknown type. Any errors that occur are returned // after the entire list is processed. Decoders are tried in order. -func DecodeList(objects []Object, decoders ...ObjectDecoder) []error { +func DecodeList(objects []Object, decoders ...Decoder) []error { errs := []error(nil) for i, obj := range objects { switch t := obj.(type) { case *Unknown: - for _, decoder := range decoders { - gv, err := unversioned.ParseGroupVersion(t.APIVersion) - if err != nil { - errs = append(errs, err) - break - } - - if !decoder.Recognizes(gv.WithKind(t.Kind)) { - continue - } - obj, err := Decode(decoder, t.RawJSON) - if err != nil { - errs = append(errs, err) - break - } - objects[i] = obj + decoded, err := decodeListItem(t, decoders) + if err != nil { + errs = append(errs, err) break } + objects[i] = decoded } } return errs @@ -84,16 +130,6 @@ type MultiObjectTyper []ObjectTyper var _ ObjectTyper = MultiObjectTyper{} -func (m MultiObjectTyper) DataKind(data []byte) (gvk unversioned.GroupVersionKind, err error) { - for _, t := range m { - gvk, err = t.DataKind(data) - if err == nil { - return - } - } - return -} - func (m MultiObjectTyper) ObjectKind(obj Object) (gvk unversioned.GroupVersionKind, err error) { for _, t := range m { gvk, err = t.ObjectKind(obj) @@ -122,3 +158,12 @@ func (m MultiObjectTyper) Recognizes(gvk unversioned.GroupVersionKind) bool { } return false } + +func (m MultiObjectTyper) IsUnversioned(obj Object) (bool, bool) { + for _, t := range m { + if unversioned, ok := t.IsUnversioned(obj); ok { + return unversioned, true + } + } + return false, false +} diff --git a/pkg/runtime/helper_test.go b/pkg/runtime/helper_test.go index 36434054cde..be7f0dedda1 100644 --- a/pkg/runtime/helper_test.go +++ b/pkg/runtime/helper_test.go @@ -32,7 +32,7 @@ func TestDecodeList(t *testing.T) { &runtime.Unstructured{TypeMeta: runtime.TypeMeta{Kind: "Foo", APIVersion: "Bar"}, Object: map[string]interface{}{"test": "value"}}, }, } - if errs := runtime.DecodeList(pl.Items, api.Scheme); len(errs) != 0 { + if errs := runtime.DecodeList(pl.Items, testapi.Default.Codec()); len(errs) != 0 { t.Fatalf("unexpected error %v", errs) } if pod, ok := pl.Items[1].(*api.Pod); !ok || pod.Name != "test" { diff --git a/pkg/runtime/interfaces.go b/pkg/runtime/interfaces.go index 7e6eb647686..6a51ee05ec8 100644 --- a/pkg/runtime/interfaces.go +++ b/pkg/runtime/interfaces.go @@ -23,70 +23,84 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" ) -// Codec defines methods for serializing and deserializing API objects. -type Codec interface { - Decoder - Encoder +const ( + APIVersionInternal = "__internal" + APIVersionUnversioned = "__unversioned" +) + +// Typer retrieves information about an object's group, version, and kind. +type Typer interface { + // ObjectKind returns the version and kind of the provided object, or an + // error if the object is not recognized (IsNotRegisteredError will return true). + // It returns whether the object is considered unversioned at the same time. + // TODO: align the signature of ObjectTyper with this interface + ObjectKind(Object) (*unversioned.GroupVersionKind, bool, error) } -// Decoder defines methods for deserializing API objects into a given type -type Decoder interface { - // TODO: change the signature of this method - Decode(data []byte) (Object, error) - // DEPRECATED: This method is being removed - DecodeToVersion(data []byte, groupVersion unversioned.GroupVersion) (Object, error) - // DEPRECATED: This method is being removed - DecodeInto(data []byte, obj Object) error - // DEPRECATED: This method is being removed - DecodeIntoWithSpecifiedVersionKind(data []byte, obj Object, groupVersionKind unversioned.GroupVersionKind) error - - DecodeParametersInto(parameters url.Values, obj Object) error -} - -// Encoder defines methods for serializing API objects into bytes type Encoder interface { - // DEPRECATED: This method is being removed - Encode(obj Object) (data []byte, err error) - EncodeToStream(obj Object, stream io.Writer) error - - // TODO: Add method for processing url parameters. - // EncodeParameters(obj Object) (url.Values, error) + // EncodeToStream writes an object to a stream. Override versions may be provided for each group + // that enforce a certain versioning. Implementations may return errors if the versions are incompatible, + // or if no conversion is defined. + EncodeToStream(obj Object, stream io.Writer, overrides ...unversioned.GroupVersion) error } -// ObjectCodec represents the common mechanisms for converting to and from a particular -// binary representation of an object. -// TODO: Remove this interface - it is used only in CodecFor() method. -type ObjectCodec interface { - Decoder - - // EncodeToVersion convert and serializes an object in the internal format - // to a specified output version. An error is returned if the object - // cannot be converted for any reason. - EncodeToVersion(obj Object, outVersion string) ([]byte, error) - EncodeToVersionStream(obj Object, outVersion string, stream io.Writer) error +type Decoder interface { + // Decode attempts to deserialize the provided data using either the innate typing of the scheme or the + // default kind, group, and version provided. It returns a decoded object as well as the kind, group, and + // version from the serialized data, or an error. If into is non-nil, it will be used as the target type + // and implementations may choose to use it rather than reallocating an object. However, the object is not + // guaranteed to be populated. The returned object is not guaranteed to match into. If defaults are + // provided, they are applied to the data by default. If no defaults or partial defaults are provided, the + // type of the into may be used to guide conversion decisions. + Decode(data []byte, defaults *unversioned.GroupVersionKind, into Object) (Object, *unversioned.GroupVersionKind, error) } -// ObjectDecoder is a convenience interface for identifying serialized versions of objects -// and transforming them into Objects. It intentionally overlaps with ObjectTyper and -// Decoder for use in decode only paths. -// TODO: Consider removing this interface? -type ObjectDecoder interface { +// Serializer is the core interface for transforming objects into a serialized format and back. +// Implementations may choose to perform conversion of the object, but no assumptions should be made. +type Serializer interface { + Encoder Decoder - // DataVersionAndKind returns the group,version,kind of the provided data, or an error - // if another problem is detected. In many cases this method can be as expensive to - // invoke as the Decode method. - DataKind([]byte) (unversioned.GroupVersionKind, error) - // Recognizes returns true if the scheme is able to handle the provided group,version,kind - // of an object. - Recognizes(unversioned.GroupVersionKind) bool +} + +// Codec is a Serializer that deals with the details of versioning objects. It offers the same +// interface as Serializer, so this is a marker to consumers that care about the version of the objects +// they receive. +type Codec Serializer + +// ParameterCodec defines methods for serializing and deserializing API objects to url.Values and +// performing any necessary conversion. Unlike the normal Codec, query parameters are not self describing +// and the desired version must be specified. +type ParameterCodec interface { + // DecodeParameters takes the given url.Values in the specified group version and decodes them + // into the provided object, or returns an error. + DecodeParameters(parameters url.Values, from unversioned.GroupVersion, into Object) error + // EncodeParameters encodes the provided object as query parameters or returns an error. + EncodeParameters(obj Object, to unversioned.GroupVersion) (url.Values, error) +} + +// NegotiatedSerializer is an interface used for obtaining encoders, decoders, and serializers +// for multiple supported media types. +type NegotiatedSerializer interface { + SupportedMediaTypes() []string + SerializerForMediaType(mediaType string, options map[string]string) (Serializer, bool) + EncoderForVersion(serializer Serializer, gv unversioned.GroupVersion) Encoder + DecoderToVersion(serializer Serializer, gv unversioned.GroupVersion) Decoder } /////////////////////////////////////////////////////////////////////////////// // Non-codec interfaces +type ObjectVersioner interface { + ConvertToVersion(in Object, outVersion string) (out Object, err error) +} + // ObjectConvertor converts an object to a different version. type ObjectConvertor interface { + // Convert attempts to convert one object into another, or returns an error. This method does + // not guarantee the in object is not mutated. Convert(in, out interface{}) error + // ConvertToVersion takes the provided object and converts it the provided version. This + // method does not guarantee that the in object is not mutated. ConvertToVersion(in Object, outVersion string) (out Object, err error) ConvertFieldLabel(version, kind, label, value string) (string, string, error) } @@ -94,10 +108,6 @@ type ObjectConvertor interface { // ObjectTyper contains methods for extracting the APIVersion and Kind // of objects. type ObjectTyper interface { - // DataKind returns the group,version,kind of the provided data, or an error - // if another problem is detected. In many cases this method can be as expensive to - // invoke as the Decode method. - DataKind([]byte) (unversioned.GroupVersionKind, error) // ObjectKind returns the default group,version,kind of the provided object, or an // error if the object is not recognized (IsNotRegisteredError will return true). ObjectKind(Object) (unversioned.GroupVersionKind, error) @@ -108,6 +118,10 @@ type ObjectTyper interface { // or more precisely that the provided version is a possible conversion or decoding // target. Recognizes(gvk unversioned.GroupVersionKind) bool + // IsUnversioned returns true if the provided object is considered unversioned and thus + // should have Version and Group suppressed in the output. If the object is not recognized + // in the scheme, ok is false. + IsUnversioned(Object) (unversioned bool, ok bool) } // ObjectCreater contains methods for instantiating an object by kind and version. diff --git a/pkg/runtime/register.go b/pkg/runtime/register.go index 53186957ac0..95244913c10 100644 --- a/pkg/runtime/register.go +++ b/pkg/runtime/register.go @@ -20,16 +20,6 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" ) -// SetGroupVersionKind satisfies the ObjectKind interface for all objects that embed PluginBase -func (obj *PluginBase) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { - _, obj.Kind = gvk.ToAPIVersionAndKind() -} - -// GroupVersionKind satisfies the ObjectKind interface for all objects that embed PluginBase -func (obj *PluginBase) GroupVersionKind() *unversioned.GroupVersionKind { - return unversioned.FromAPIVersionAndKind("", obj.Kind) -} - // SetGroupVersionKind satisfies the ObjectKind interface for all objects that embed TypeMeta func (obj *TypeMeta) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind() @@ -42,3 +32,33 @@ func (obj *TypeMeta) GroupVersionKind() *unversioned.GroupVersionKind { func (obj *Unknown) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *Unstructured) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } + +// GetObjectKind implements Object for VersionedObjects, returning an empty ObjectKind +// interface if no objects are provided, or the ObjectKind interface of the object in the +// highest array position. +func (obj *VersionedObjects) GetObjectKind() unversioned.ObjectKind { + last := obj.Last() + if last == nil { + return unversioned.EmptyObjectKind + } + return last.GetObjectKind() +} + +// First returns the leftmost object in the VersionedObjects array, which is usually the +// object as serialized on the wire. +func (obj *VersionedObjects) First() Object { + if len(obj.Objects) == 0 { + return nil + } + return obj.Objects[0] +} + +// Last is the rightmost object in the VersionedObjects array, which is the object after +// all transformations have been applied. This is the same object that would be returned +// by Decode in a normal invocation (without VersionedObjects in the into argument). +func (obj *VersionedObjects) Last() Object { + if len(obj.Objects) == 0 { + return nil + } + return obj.Objects[len(obj.Objects)-1] +} diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 8b28d11a630..474a6eb307f 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -17,9 +17,7 @@ limitations under the License. package runtime import ( - "encoding/json" "fmt" - "io" "net/url" "reflect" @@ -36,9 +34,6 @@ type Scheme struct { fieldLabelConversionFuncs map[string]map[string]FieldLabelConversionFunc } -var _ Decoder = &Scheme{} -var _ ObjectTyper = &Scheme{} - // Function to convert a field selector to internal representation. type FieldLabelConversionFunc func(label, value string) (internalLabel, internalValue string, err error) @@ -55,205 +50,11 @@ func (self *Scheme) fromScope(s conversion.Scope) (inVersion, outVersion string, return inVersion, outVersion, scheme } -// emptyPlugin is used to copy the Kind field to and from plugin objects. -type emptyPlugin struct { - PluginBase `json:",inline"` -} - -// embeddedObjectToRawExtension does the conversion you would expect from the name, using the information -// given in conversion.Scope. It's placed in the DefaultScheme as a ConversionFunc to enable plugins; -// see the comment for RawExtension. -func (self *Scheme) embeddedObjectToRawExtension(in *EmbeddedObject, out *RawExtension, s conversion.Scope) error { - if in.Object == nil { - out.RawJSON = []byte("null") - return nil - } - - // Figure out the type and kind of the output object. - _, outGroupVersionString, scheme := self.fromScope(s) - objKind, err := scheme.raw.ObjectKind(in.Object) - if err != nil { - return err - } - outVersion, err := unversioned.ParseGroupVersion(outGroupVersionString) - if err != nil { - return err - } - - // Manufacture an object of this type and kind. - outObj, err := scheme.New(outVersion.WithKind(objKind.Kind)) - if err != nil { - return err - } - - // Manually do the conversion. - err = s.Convert(in.Object, outObj, 0) - if err != nil { - return err - } - - // Copy the kind field into the output object. - err = s.Convert( - &emptyPlugin{PluginBase: PluginBase{Kind: objKind.Kind}}, - outObj, - conversion.SourceToDest|conversion.IgnoreMissingFields|conversion.AllowDifferentFieldTypeNames, - ) - if err != nil { - return err - } - // Because we provide the correct version, EncodeToVersion will not attempt a conversion. - raw, err := scheme.EncodeToVersion(outObj, outVersion.String()) - if err != nil { - // TODO: if this fails, create an Unknown-- maybe some other - // component will understand it. - return err - } - out.RawJSON = raw - return nil -} - -// rawExtensionToEmbeddedObject does the conversion you would expect from the name, using the information -// given in conversion.Scope. It's placed in all schemes as a ConversionFunc to enable plugins; -// see the comment for RawExtension. -func (self *Scheme) rawExtensionToEmbeddedObject(in *RawExtension, out *EmbeddedObject, s conversion.Scope) error { - if len(in.RawJSON) == 0 || (len(in.RawJSON) == 4 && string(in.RawJSON) == "null") { - out.Object = nil - return nil - } - // Figure out the type and kind of the output object. - inGroupVersionString, outGroupVersionString, scheme := self.fromScope(s) - dataKind, err := scheme.raw.DataKind(in.RawJSON) - if err != nil { - return err - } - inVersion, err := unversioned.ParseGroupVersion(inGroupVersionString) - if err != nil { - return err - } - outVersion, err := unversioned.ParseGroupVersion(outGroupVersionString) - if err != nil { - return err - } - - // We have to make this object ourselves because we don't store the version field for - // plugin objects. - inObj, err := scheme.New(inVersion.WithKind(dataKind.Kind)) - if err != nil { - return err - } - - err = DecodeInto(scheme, in.RawJSON, inObj) - if err != nil { - return err - } - - // Make the desired internal version, and do the conversion. - outObj, err := scheme.New(outVersion.WithKind(dataKind.Kind)) - if err != nil { - return err - } - err = scheme.Convert(inObj, outObj) - if err != nil { - return err - } - // Last step, clear the Kind field; that should always be blank in memory. - err = s.Convert( - &emptyPlugin{PluginBase: PluginBase{Kind: ""}}, - outObj, - conversion.SourceToDest|conversion.IgnoreMissingFields|conversion.AllowDifferentFieldTypeNames, - ) - if err != nil { - return err - } - out.Object = outObj - 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 than the default, you -// should encode them yourself with runtime.Unknown, or convert the object prior to invoking conversion. Objects -// outside of the current scheme must be added as runtime.Unknown. -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: - // TODO: this should be decoupled from the scheme (since it is JSON specific) - dest[i].RawJSON = t.RawJSON - case *Unstructured: - // TODO: this should be decoupled from the scheme (since it is JSON specific) - data, err := json.Marshal(t.Object) - if err != nil { - return err - } - dest[i].RawJSON = data - default: - version := outVersion - // if the object exists - // this code is try to set the outputVersion, but only if the object has a non-internal group version - if inGVK, err := scheme.ObjectKind(src[i]); err == nil && !inGVK.GroupVersion().IsEmpty() { - if self.raw.InternalVersions[inGVK.Group] != inGVK.GroupVersion() { - version = inGVK.GroupVersion().String() - } - } - data, err := scheme.EncodeToVersion(src[i], version) - 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 - dataKind, err := scheme.raw.DataKind(data) - if err != nil { - return err - } - dest[i] = &Unknown{ - TypeMeta: TypeMeta{ - APIVersion: dataKind.GroupVersion().String(), - Kind: dataKind.Kind, - }, - RawJSON: data, - } - } - *out = dest - return nil -} - // NewScheme creates a new Scheme. This scheme is pluggable by default. -func NewScheme(internalGroupVersions ...unversioned.GroupVersion) *Scheme { +func NewScheme() *Scheme { s := &Scheme{conversion.NewScheme(), map[string]map[string]FieldLabelConversionFunc{}} + s.AddConversionFuncs(DefaultEmbeddedConversions()...) - for _, internalGV := range internalGroupVersions { - s.raw.InternalVersions[internalGV.Group] = internalGV - } - - s.raw.MetaFactory = conversion.SimpleMetaFactory{BaseFields: []string{"TypeMeta"}, VersionField: "APIVersion", KindField: "Kind"} - if err := s.raw.AddConversionFuncs( - s.embeddedObjectToRawExtension, - s.rawExtensionToEmbeddedObject, - s.runtimeObjectToRawExtensionArray, - s.rawExtensionToRuntimeObjectArray, - ); err != nil { - panic(err) - } // Enable map[string][]string conversions by default if err := s.raw.AddConversionFuncs(DefaultStringConversions...); err != nil { panic(err) @@ -267,14 +68,22 @@ func NewScheme(internalGroupVersions ...unversioned.GroupVersion) *Scheme { return s } -// AddInternalGroupVersion registers an internal GroupVersion with the scheme. This can later be -// used to lookup the internal GroupVersion for a given Group -func (s *Scheme) AddInternalGroupVersion(gv unversioned.GroupVersion) { - s.raw.InternalVersions[gv.Group] = gv +// AddUnversionedTypes registers the provided types as "unversioned", which means that they follow special rules. +// Whenever an object of this type is serialized, it is serialized with the provided group version and is not +// converted. Thus unversioned objects are expected to remain backwards compatible forever, as if they were in an +// API group and version that would never be updated. +// +// TODO: there is discussion about removing unversioned and replacing it with objects that are manifest into +// every version with particular schemas. Resolve tihs method at that point. +func (s *Scheme) AddUnversionedTypes(gv unversioned.GroupVersion, types ...Object) { + interfaces := make([]interface{}, len(types)) + for i := range types { + interfaces[i] = types[i] + } + s.raw.AddUnversionedTypes(gv, interfaces...) } // AddKnownTypes registers the types of the arguments to the marshaller of the package api. -// Encode() refuses the object unless its type is registered with AddKnownTypes. func (s *Scheme) AddKnownTypes(gv unversioned.GroupVersion, types ...Object) { interfaces := make([]interface{}, len(types)) for i := range types { @@ -283,6 +92,12 @@ func (s *Scheme) AddKnownTypes(gv unversioned.GroupVersion, types ...Object) { s.raw.AddKnownTypes(gv, interfaces...) } +// AddIgnoredConversionType declares a particular conversion that should be ignored - during conversion +// this method is not invoked. +func (s *Scheme) AddIgnoredConversionType(from, to interface{}) error { + return s.raw.AddIgnoredConversionType(from, to) +} + // AddKnownTypeWithName is like AddKnownTypes, but it lets you specify what this type should // be encoded as. Useful for testing when you don't want to make multiple packages to define // your structs. @@ -296,12 +111,6 @@ func (s *Scheme) KnownTypes(gv unversioned.GroupVersion) map[string]reflect.Type return s.raw.KnownTypes(gv) } -// DataKind will return the group,version,kind of the given wire-format -// encoding of an API Object, or an error. -func (s *Scheme) DataKind(data []byte) (unversioned.GroupVersionKind, error) { - return s.raw.DataKind(data) -} - // ObjectKind returns the default group,version,kind of the given Object. func (s *Scheme) ObjectKind(obj Object) (unversioned.GroupVersionKind, error) { return s.raw.ObjectKind(obj) @@ -318,7 +127,12 @@ func (s *Scheme) Recognizes(gvk unversioned.GroupVersionKind) bool { return s.raw.Recognizes(gvk) } -// New returns a new API object of the given kind, or an error if it hasn't been registered. +func (s *Scheme) IsUnversioned(obj Object) (bool, bool) { + return s.raw.IsUnversioned(obj) +} + +// New returns a new API object of the given version ("" for internal +// representation) and name, or an error if it hasn't been registered. func (s *Scheme) New(kind unversioned.GroupVersionKind) (Object, error) { obj, err := s.raw.NewObject(kind) if err != nil { @@ -394,11 +208,31 @@ func (s *Scheme) AddDefaultingFuncs(defaultingFuncs ...interface{}) error { return s.raw.AddDefaultingFuncs(defaultingFuncs...) } +// Copy does a deep copy of an API object. +func (s *Scheme) Copy(src Object) (Object, error) { + dst, err := s.raw.DeepCopy(src) + if err != nil { + return nil, err + } + return dst.(Object), nil +} + // Performs a deep copy of the given object. func (s *Scheme) DeepCopy(src interface{}) (interface{}, error) { return s.raw.DeepCopy(src) } +// WithConversions returns an ObjectConvertor that has the additional conversion functions +// defined in fns. The current scheme is not altered. +func (s *Scheme) WithConversions(fns *conversion.ConversionFuncs) ObjectConvertor { + if fns == nil { + return s + } + copied := *s + copied.raw = s.raw.WithConversions(*fns) + return &copied +} + // Convert will attempt to convert in into out. Both must be pointers. // For easy testing of conversion functions. Returns an error if the conversion isn't // possible. @@ -423,8 +257,19 @@ func (s *Scheme) ConvertFieldLabel(version, kind, label, value string) (string, // version within this scheme. Will return an error if the provided version does not // contain the inKind (or a mapping by name defined with AddKnownTypeWithName). Will also // return an error if the conversion does not result in a valid Object being -// returned. +// returned. The serializer handles loading/serializing nested objects. func (s *Scheme) ConvertToVersion(in Object, outVersion string) (Object, error) { + gv, err := unversioned.ParseGroupVersion(outVersion) + if err != nil { + return nil, err + } + switch in.(type) { + case *Unknown, *Unstructured: + old := in.GetObjectKind().GroupVersionKind() + defer in.GetObjectKind().SetGroupVersionKind(old) + setTargetVersion(in, s.raw, gv) + return in, nil + } unknown, err := s.raw.ConvertToVersion(in, outVersion) if err != nil { return nil, err @@ -433,105 +278,16 @@ func (s *Scheme) ConvertToVersion(in Object, outVersion string) (Object, error) if !ok { return nil, fmt.Errorf("the provided object cannot be converted to a runtime.Object: %#v", unknown) } + setTargetVersion(obj, s.raw, gv) return obj, nil } -// EncodeToVersion turns the given api object into an appropriate JSON string. -// Will return an error if the object doesn't have an embedded TypeMeta. -// Obj may be a pointer to a struct, or a struct. If a struct, a copy -// must be made. If a pointer, the object may be modified before encoding, -// but will be put back into its original state before returning. -// -// Memory/wire format differences: -// * Having to keep track of the Kind and APIVersion fields makes tests -// very annoying, so the rule is that they are set only in wire format -// (json), not when in native (memory) format. This is possible because -// both pieces of information are implicit in the go typed object. -// * An exception: note that, if there are embedded API objects of known -// type, for example, PodList{... Items []Pod ...}, these embedded -// objects must be of the same version of the object they are embedded -// within, and their APIVersion and Kind must both be empty. -// * Note that the exception does not apply to the APIObject type, which -// recursively does Encode()/Decode(), and is capable of expressing any -// API object. -// * Only versioned objects should be encoded. This means that, if you pass -// a native object, Encode will convert it to a versioned object. For -// example, an api.Pod will get converted to a v1.Pod. However, if -// you pass in an object that's already versioned (v1.Pod), Encode -// will not modify it. -// -// The purpose of the above complex conversion behavior is to allow us to -// change the memory format yet not break compatibility with any stored -// objects, whether they be in our storage layer (e.g., etcd), or in user's -// config files. -func (s *Scheme) EncodeToVersion(obj Object, destVersion string) (data []byte, err error) { - return s.raw.EncodeToVersion(obj, destVersion) -} - -func (s *Scheme) EncodeToVersionStream(obj Object, destVersion string, stream io.Writer) error { - return s.raw.EncodeToVersionStream(obj, destVersion, stream) -} - -// Decode converts a YAML or JSON string back into a pointer to an api object. -// Deduces the type based upon the APIVersion and Kind fields, which are set -// by Encode. Only versioned objects (APIVersion != "") are accepted. The object -// will be converted into the in-memory unversioned type before being returned. -func (s *Scheme) Decode(data []byte) (Object, error) { - obj, err := s.raw.Decode(data) - if err != nil { - return nil, err +func setTargetVersion(obj Object, raw *conversion.Scheme, gv unversioned.GroupVersion) { + if gv.Version == APIVersionInternal { + // internal is a special case + obj.GetObjectKind().SetGroupVersionKind(nil) + } else { + gvk, _ := raw.ObjectKind(obj) + obj.GetObjectKind().SetGroupVersionKind(&unversioned.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: gvk.Kind}) } - return obj.(Object), nil -} - -// DecodeToVersion converts a YAML or JSON string back into a pointer to an api -// object. Deduces the type based upon the APIVersion and Kind fields, which -// are set by Encode. Only versioned objects (APIVersion != "") are -// accepted. The object will be converted into the in-memory versioned type -// requested before being returned. -func (s *Scheme) DecodeToVersion(data []byte, gv unversioned.GroupVersion) (Object, error) { - obj, err := s.raw.DecodeToVersion(data, gv) - if err != nil { - return nil, err - } - return obj.(Object), nil -} - -// DecodeInto parses a YAML or JSON string and stores it in obj. Returns an error -// if data.Kind is set and doesn't match the type of obj. Obj should be a -// pointer to an api type. -// If obj's APIVersion doesn't match that in data, an attempt will be made to convert -// data into obj's version. -// TODO: allow Decode/DecodeInto to take a default apiVersion and a default kind, to -// be applied if the provided object does not have either field (integrate external -// apis into the decoding scheme). -func (s *Scheme) DecodeInto(data []byte, obj Object) error { - return s.raw.DecodeInto(data, obj) -} - -// DecodeIntoWithSpecifiedVersionKind coerces the data into the obj, assuming that the data is -// of type GroupVersionKind -func (s *Scheme) DecodeIntoWithSpecifiedVersionKind(data []byte, obj Object, gvk unversioned.GroupVersionKind) error { - return s.raw.DecodeIntoWithSpecifiedVersionKind(data, obj, gvk) -} - -func (s *Scheme) DecodeParametersInto(parameters url.Values, obj Object) error { - return s.raw.DecodeParametersInto(parameters, obj) -} - -// Copy does a deep copy of an API object. Useful mostly for tests. -func (s *Scheme) Copy(src Object) (Object, error) { - dst, err := s.raw.DeepCopy(src) - if err != nil { - return nil, err - } - return dst.(Object), nil -} - -func (s *Scheme) CopyOrDie(obj Object) Object { - newObj, err := s.Copy(obj) - if err != nil { - panic(err) - } - return newObj } diff --git a/pkg/runtime/scheme_test.go b/pkg/runtime/scheme_test.go index f406585f970..d0f9f2d08bf 100644 --- a/pkg/runtime/scheme_test.go +++ b/pkg/runtime/scheme_test.go @@ -23,6 +23,8 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/conversion" "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/runtime/serializer" + "k8s.io/kubernetes/pkg/util" ) type TypeMeta struct { @@ -51,23 +53,19 @@ type ExternalSimple struct { } func (obj *InternalSimple) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *InternalSimple) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { - obj.TypeMeta.APIVersion, obj.TypeMeta.Kind = gvk.ToAPIVersionAndKind() -} func (obj *ExternalSimple) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *ExternalSimple) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { - obj.TypeMeta.APIVersion, obj.TypeMeta.Kind = gvk.ToAPIVersionAndKind() -} func TestScheme(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "testExternal"} scheme := runtime.NewScheme() - scheme.AddInternalGroupVersion(internalGV) scheme.AddKnownTypeWithName(internalGV.WithKind("Simple"), &InternalSimple{}) scheme.AddKnownTypeWithName(externalGV.WithKind("Simple"), &ExternalSimple{}) + // If set, would clear TypeMeta during conversion. + //scheme.AddIgnoredConversionType(&TypeMeta{}, &TypeMeta{}) + // test that scheme is an ObjectTyper var _ runtime.ObjectTyper = scheme @@ -102,21 +100,27 @@ func TestScheme(t *testing.T) { }, ) if err != nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("unexpected error: %v", err) } + + codecs := serializer.NewCodecFactory(scheme) + codec := codecs.LegacyCodec(externalGV) + jsonserializer, _ := codecs.SerializerForFileExtension("json") + simple := &InternalSimple{ TestString: "foo", } // Test Encode, Decode, DecodeInto, and DecodeToVersion obj := runtime.Object(simple) - data, err := scheme.EncodeToVersion(obj, externalGV.String()) - obj2, err2 := runtime.Decode(scheme, data) - obj3 := &InternalSimple{} - err3 := runtime.DecodeInto(scheme, data, obj3) - obj4, err4 := scheme.DecodeToVersion(data, externalGV) - if err != nil || err2 != nil || err3 != nil || err4 != nil { - t.Fatalf("Failure: '%v' '%v' '%v' '%v'", err, err2, err3, err4) + data, err := runtime.Encode(codec, obj) + if err != nil { + t.Fatal(err) + } + + obj2, err := runtime.Decode(codec, data) + if err != nil { + t.Fatal(err) } if _, ok := obj2.(*InternalSimple); !ok { t.Fatalf("Got wrong type") @@ -124,9 +128,22 @@ func TestScheme(t *testing.T) { if e, a := simple, obj2; !reflect.DeepEqual(e, a) { t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a) } + + obj3 := &InternalSimple{} + if err := runtime.DecodeInto(codec, data, obj3); err != nil { + t.Fatal(err) + } + // clearing TypeMeta is a function of the scheme, which we do not test here (ConvertToVersion + // does not automatically clear TypeMeta anymore). + simple.TypeMeta = TypeMeta{Kind: "Simple", APIVersion: externalGV.String()} if e, a := simple, obj3; !reflect.DeepEqual(e, a) { t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a) } + + obj4, err := runtime.Decode(jsonserializer, data) + if err != nil { + t.Fatal(err) + } if _, ok := obj4.(*ExternalSimple); !ok { t.Fatalf("Got wrong type") } @@ -135,7 +152,7 @@ func TestScheme(t *testing.T) { external := &ExternalSimple{} err = scheme.Convert(simple, external) if err != nil { - t.Errorf("Unexpected error: %v", err) + t.Fatalf("Unexpected error: %v", err) } if e, a := simple.TestString, external.TestString; e != a { t.Errorf("Expected %v, got %v", e, a) @@ -145,36 +162,23 @@ func TestScheme(t *testing.T) { if e, a := 2, internalToExternalCalls; e != a { t.Errorf("Expected %v, got %v", e, a) } - // Decode and DecodeInto should each have caused an increment. + // DecodeInto and Decode should each have caused an increment because of a conversion if e, a := 2, externalToInternalCalls; e != a { t.Errorf("Expected %v, got %v", e, a) } } -func TestInvalidObjectValueKind(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "", Version: ""} - - scheme := runtime.NewScheme() - scheme.AddKnownTypeWithName(internalGV.WithKind("Simple"), &InternalSimple{}) - - embedded := &runtime.EmbeddedObject{} - switch obj := embedded.Object.(type) { - default: - _, err := scheme.ObjectKind(obj) - if err == nil { - t.Errorf("Expected error on invalid kind") - } - } -} - func TestBadJSONRejection(t *testing.T) { scheme := runtime.NewScheme() + codecs := serializer.NewCodecFactory(scheme) + jsonserializer, _ := codecs.SerializerForFileExtension("json") + badJSONMissingKind := []byte(`{ }`) - if _, err := runtime.Decode(scheme, badJSONMissingKind); err == nil { + if _, err := runtime.Decode(jsonserializer, badJSONMissingKind); err == nil { t.Errorf("Did not reject despite lack of kind field: %s", badJSONMissingKind) } badJSONUnknownType := []byte(`{"kind": "bar"}`) - if _, err1 := runtime.Decode(scheme, badJSONUnknownType); err1 == nil { + if _, err1 := runtime.Decode(jsonserializer, badJSONUnknownType); err1 == nil { t.Errorf("Did not reject despite use of unknown type: %s", badJSONUnknownType) } /*badJSONKindMismatch := []byte(`{"kind": "Pod"}`) @@ -184,13 +188,13 @@ func TestBadJSONRejection(t *testing.T) { } type ExtensionA struct { - runtime.PluginBase `json:",inline"` - TestString string `json:"testString"` + TypeMeta `json:",inline"` + TestString string `json:"testString"` } type ExtensionB struct { - runtime.PluginBase `json:",inline"` - TestString string `json:"testString"` + TypeMeta `json:",inline"` + TestString string `json:"testString"` } type ExternalExtensionType struct { @@ -200,7 +204,7 @@ type ExternalExtensionType struct { type InternalExtensionType struct { TypeMeta `json:",inline"` - Extension runtime.EmbeddedObject `json:"extension"` + Extension runtime.Object `json:"extension"` } type ExternalOptionalExtensionType struct { @@ -210,143 +214,135 @@ type ExternalOptionalExtensionType struct { type InternalOptionalExtensionType struct { TypeMeta `json:",inline"` - Extension runtime.EmbeddedObject `json:"extension,omitempty"` + Extension runtime.Object `json:"extension,omitempty"` } -func (obj *ExtensionA) GetObjectKind() unversioned.ObjectKind { return &obj.PluginBase } -func (obj *ExtensionA) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { - _, obj.PluginBase.Kind = gvk.ToAPIVersionAndKind() -} -func (obj *ExtensionB) GetObjectKind() unversioned.ObjectKind { return &obj.PluginBase } -func (obj *ExtensionB) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { - _, obj.PluginBase.Kind = gvk.ToAPIVersionAndKind() -} -func (obj *ExternalExtensionType) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *ExternalExtensionType) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { - obj.TypeMeta.APIVersion, obj.TypeMeta.Kind = gvk.ToAPIVersionAndKind() -} -func (obj *InternalExtensionType) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *InternalExtensionType) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { - obj.TypeMeta.APIVersion, obj.TypeMeta.Kind = gvk.ToAPIVersionAndKind() -} +func (obj *ExtensionA) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ExtensionB) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ExternalExtensionType) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *InternalExtensionType) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *ExternalOptionalExtensionType) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *ExternalOptionalExtensionType) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { - obj.TypeMeta.APIVersion, obj.TypeMeta.Kind = gvk.ToAPIVersionAndKind() -} func (obj *InternalOptionalExtensionType) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *InternalOptionalExtensionType) SetGroupVersionKind(gvk *unversioned.GroupVersionKind) { - obj.TypeMeta.APIVersion, obj.TypeMeta.Kind = gvk.ToAPIVersionAndKind() -} func TestExternalToInternalMapping(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "testExternal"} scheme := runtime.NewScheme() - scheme.AddInternalGroupVersion(internalGV) scheme.AddKnownTypeWithName(internalGV.WithKind("OptionalExtensionType"), &InternalOptionalExtensionType{}) scheme.AddKnownTypeWithName(externalGV.WithKind("OptionalExtensionType"), &ExternalOptionalExtensionType{}) + codec := serializer.NewCodecFactory(scheme).LegacyCodec(externalGV) + table := []struct { obj runtime.Object encoded string }{ { - &InternalOptionalExtensionType{Extension: runtime.EmbeddedObject{Object: nil}}, + &InternalOptionalExtensionType{Extension: nil}, `{"kind":"OptionalExtensionType","apiVersion":"` + externalGV.String() + `"}`, }, } - for _, item := range table { - gotDecoded, err := runtime.Decode(scheme, []byte(item.encoded)) + for i, item := range table { + gotDecoded, err := runtime.Decode(codec, []byte(item.encoded)) if err != nil { t.Errorf("unexpected error '%v' (%v)", err, item.encoded) } else if e, a := item.obj, gotDecoded; !reflect.DeepEqual(e, a) { - var eEx, aEx runtime.Object - if obj, ok := e.(*InternalOptionalExtensionType); ok { - eEx = obj.Extension.Object - } - if obj, ok := a.(*InternalOptionalExtensionType); ok { - aEx = obj.Extension.Object - } - t.Errorf("expected %#v, got %#v (%#v, %#v)", e, a, eEx, aEx) + t.Errorf("%d: unexpected objects:\n%s", i, util.ObjectGoPrintSideBySide(e, a)) } } } func TestExtensionMapping(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "testExternal"} scheme := runtime.NewScheme() - scheme.AddInternalGroupVersion(internalGV) scheme.AddKnownTypeWithName(internalGV.WithKind("ExtensionType"), &InternalExtensionType{}) scheme.AddKnownTypeWithName(internalGV.WithKind("OptionalExtensionType"), &InternalOptionalExtensionType{}) - scheme.AddKnownTypeWithName(internalGV.WithKind("A"), &ExtensionA{}) - scheme.AddKnownTypeWithName(internalGV.WithKind("B"), &ExtensionB{}) scheme.AddKnownTypeWithName(externalGV.WithKind("ExtensionType"), &ExternalExtensionType{}) scheme.AddKnownTypeWithName(externalGV.WithKind("OptionalExtensionType"), &ExternalOptionalExtensionType{}) + + // register external first when the object is the same in both schemes, so ObjectVersionAndKind reports the + // external version. scheme.AddKnownTypeWithName(externalGV.WithKind("A"), &ExtensionA{}) scheme.AddKnownTypeWithName(externalGV.WithKind("B"), &ExtensionB{}) + scheme.AddKnownTypeWithName(internalGV.WithKind("A"), &ExtensionA{}) + scheme.AddKnownTypeWithName(internalGV.WithKind("B"), &ExtensionB{}) + + codec := serializer.NewCodecFactory(scheme).LegacyCodec(externalGV) table := []struct { - obj runtime.Object - encoded string + obj runtime.Object + expected runtime.Object + encoded string }{ { - &InternalExtensionType{Extension: runtime.EmbeddedObject{Object: &ExtensionA{TestString: "foo"}}}, - `{"kind":"ExtensionType","apiVersion":"` + externalGV.String() + `","extension":{"kind":"A","testString":"foo"}} + &InternalExtensionType{ + Extension: runtime.NewEncodable(codec, &ExtensionA{TestString: "foo"}), + }, + &InternalExtensionType{ + Extension: &runtime.Unknown{ + RawJSON: []byte(`{"kind":"A","apiVersion":"test.group/testExternal","testString":"foo"}`), + }, + }, + // apiVersion is set in the serialized object for easier consumption by clients + `{"kind":"ExtensionType","apiVersion":"` + externalGV.String() + `","extension":{"kind":"A","apiVersion":"test.group/testExternal","testString":"foo"}} `, }, { - &InternalExtensionType{Extension: runtime.EmbeddedObject{Object: &ExtensionB{TestString: "bar"}}}, - `{"kind":"ExtensionType","apiVersion":"` + externalGV.String() + `","extension":{"kind":"B","testString":"bar"}} + &InternalExtensionType{Extension: runtime.NewEncodable(codec, &ExtensionB{TestString: "bar"})}, + &InternalExtensionType{ + Extension: &runtime.Unknown{ + RawJSON: []byte(`{"kind":"B","apiVersion":"test.group/testExternal","testString":"bar"}`), + }, + }, + // apiVersion is set in the serialized object for easier consumption by clients + `{"kind":"ExtensionType","apiVersion":"` + externalGV.String() + `","extension":{"kind":"B","apiVersion":"test.group/testExternal","testString":"bar"}} `, }, { - &InternalExtensionType{Extension: runtime.EmbeddedObject{Object: nil}}, + &InternalExtensionType{Extension: nil}, + &InternalExtensionType{ + Extension: nil, + }, `{"kind":"ExtensionType","apiVersion":"` + externalGV.String() + `","extension":null} `, }, } - for _, item := range table { - gotEncoded, err := scheme.EncodeToVersion(item.obj, externalGV.String()) + for i, item := range table { + gotEncoded, err := runtime.Encode(codec, item.obj) if err != nil { t.Errorf("unexpected error '%v' (%#v)", err, item.obj) } else if e, a := item.encoded, string(gotEncoded); e != a { t.Errorf("expected\n%#v\ngot\n%#v\n", e, a) } - gotDecoded, err := runtime.Decode(scheme, []byte(item.encoded)) + gotDecoded, err := runtime.Decode(codec, []byte(item.encoded)) if err != nil { t.Errorf("unexpected error '%v' (%v)", err, item.encoded) - } else if e, a := item.obj, gotDecoded; !reflect.DeepEqual(e, a) { - var eEx, aEx runtime.Object - if obj, ok := e.(*InternalExtensionType); ok { - eEx = obj.Extension.Object - } - if obj, ok := a.(*InternalExtensionType); ok { - aEx = obj.Extension.Object - } - t.Errorf("expected %#v, got %#v (%#v, %#v)", e, a, eEx, aEx) + } else if e, a := item.expected, gotDecoded; !reflect.DeepEqual(e, a) { + t.Errorf("%d: unexpected objects:\n%s", i, util.ObjectGoPrintSideBySide(e, a)) } } } func TestEncode(t *testing.T) { - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "testExternal"} scheme := runtime.NewScheme() - scheme.AddInternalGroupVersion(internalGV) scheme.AddKnownTypeWithName(internalGV.WithKind("Simple"), &InternalSimple{}) scheme.AddKnownTypeWithName(externalGV.WithKind("Simple"), &ExternalSimple{}) - codec := runtime.CodecFor(scheme, externalGV) + + codec := serializer.NewCodecFactory(scheme).LegacyCodec(externalGV) + test := &InternalSimple{ TestString: "I'm the same", } obj := runtime.Object(test) data, err := runtime.Encode(codec, obj) - obj2, err2 := runtime.Decode(codec, data) + obj2, gvk, err2 := codec.Decode(data, nil, nil) if err != nil || err2 != nil { t.Fatalf("Failure: '%v' '%v'", err, err2) } @@ -354,6 +350,68 @@ func TestEncode(t *testing.T) { t.Fatalf("Got wrong type") } if !reflect.DeepEqual(obj2, test) { - t.Errorf("Expected:\n %#v,\n Got:\n %#v", &test, obj2) + t.Errorf("Expected:\n %#v,\n Got:\n %#v", test, obj2) + } + if !reflect.DeepEqual(gvk, &unversioned.GroupVersionKind{Group: "test.group", Version: "testExternal", Kind: "Simple"}) { + t.Errorf("unexpected gvk returned by decode: %#v", gvk) + } +} + +func TestUnversionedTypes(t *testing.T) { + internalGV := unversioned.GroupVersion{Group: "test.group", Version: runtime.APIVersionInternal} + externalGV := unversioned.GroupVersion{Group: "test.group", Version: "testExternal"} + otherGV := unversioned.GroupVersion{Group: "group", Version: "other"} + + scheme := runtime.NewScheme() + scheme.AddUnversionedTypes(externalGV, &InternalSimple{}) + scheme.AddKnownTypeWithName(internalGV.WithKind("Simple"), &InternalSimple{}) + scheme.AddKnownTypeWithName(externalGV.WithKind("Simple"), &ExternalSimple{}) + scheme.AddKnownTypeWithName(otherGV.WithKind("Simple"), &ExternalSimple{}) + + codec := serializer.NewCodecFactory(scheme).LegacyCodec(externalGV) + + if unv, ok := scheme.IsUnversioned(&InternalSimple{}); !unv || !ok { + t.Fatal("type not unversioned and in scheme: %t %t", unv, ok) + } + + kind, err := scheme.ObjectKind(&InternalSimple{}) + if err != nil { + t.Fatal(err) + } + if kind != externalGV.WithKind("InternalSimple") { + t.Fatalf("unexpected: %#v", kind) + } + + test := &InternalSimple{ + TestString: "I'm the same", + } + obj := runtime.Object(test) + data, err := runtime.Encode(codec, obj) + if err != nil { + t.Fatal(err) + } + obj2, gvk, err := codec.Decode(data, nil, nil) + if err != nil { + t.Fatal(err) + } + if _, ok := obj2.(*InternalSimple); !ok { + t.Fatalf("Got wrong type") + } + if !reflect.DeepEqual(obj2, test) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", test, obj2) + } + // object is serialized as an unversioned object (in the group and version it was defined in) + if !reflect.DeepEqual(gvk, &unversioned.GroupVersionKind{Group: "test.group", Version: "testExternal", Kind: "InternalSimple"}) { + t.Errorf("unexpected gvk returned by decode: %#v", gvk) + } + + // when serialized to a different group, the object is kept in its preferred name + codec = serializer.NewCodecFactory(scheme).LegacyCodec(otherGV) + data, err = runtime.Encode(codec, obj) + if err != nil { + t.Fatal(err) + } + if string(data) != `{"kind":"InternalSimple","apiVersion":"test.group/testExternal","testString":"I'm the same"}`+"\n" { + t.Errorf("unexpected data: %s", data) } } diff --git a/pkg/runtime/types.go b/pkg/runtime/types.go index a09af5e14f8..3b8cede446a 100644 --- a/pkg/runtime/types.go +++ b/pkg/runtime/types.go @@ -36,39 +36,18 @@ type TypeMeta struct { Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` } -// PluginBase is like TypeMeta, but it's intended for plugin objects that won't ever be encoded -// except while embedded in other objects. -type PluginBase struct { - Kind string `json:"kind,omitempty"` -} - -// EmbeddedObject has appropriate encoder and decoder functions, such that on the wire, it's -// stored as a []byte, but in memory, the contained object is accessible as an Object -// via the Get() function. Only valid API objects may be stored via EmbeddedObject. -// The purpose of this is to allow an API object of type known only at runtime to be -// embedded within other API objects. -// -// Note that object assumes that you've registered all of your api types with the api package. -// -// EmbeddedObject and RawExtension can be used together to allow for API object extensions: -// see the comment for RawExtension. -type EmbeddedObject struct { - Object -} - -// RawExtension is used with EmbeddedObject to do a two-phase encoding of extension objects. +// RawExtension is used to hold extensions in external versions. // // To use this, make a field which has RawExtension as its type in your external, versioned -// struct, and EmbeddedObject in your internal struct. You also need to register your +// struct, and Object in your internal struct. You also need to register your // various plugin types. // // // Internal package: // type MyAPIObject struct { // runtime.TypeMeta `json:",inline"` -// MyPlugin runtime.EmbeddedObject `json:"myPlugin"` +// MyPlugin runtime.Object `json:"myPlugin"` // } // type PluginA struct { -// runtime.PluginBase `json:",inline"` // AOption string `json:"aOption"` // } // @@ -78,7 +57,6 @@ type EmbeddedObject struct { // MyPlugin runtime.RawExtension `json:"myPlugin"` // } // type PluginA struct { -// runtime.PluginBase `json:",inline"` // AOption string `json:"aOption"` // } // @@ -97,12 +75,16 @@ type EmbeddedObject struct { // The next step is to copy (using pkg/conversion) into the internal struct. The runtime // package's DefaultScheme has conversion functions installed which will unpack the // JSON stored in RawExtension, turning it into the correct object type, and storing it -// in the EmbeddedObject. (TODO: In the case where the object is of an unknown type, a +// in the Object. (TODO: In the case where the object is of an unknown type, a // runtime.Unknown object will be created and stored.) // // +protobuf=true type RawExtension struct { + // RawJSON is the underlying serialization of this object. RawJSON []byte + // Object can hold a representation of this extension - useful for working with versioned + // structs. + Object Object `json:"-"` } // Unknown allows api objects with unknown types to be passed-through. This can be used @@ -131,3 +113,13 @@ type Unstructured struct { // children. Object map[string]interface{} } + +// VersionedObjects is used by Decoders to give callers a way to access all versions +// of an object during the decoding process. +type VersionedObjects struct { + // Objects is the set of objects retrieved during decoding, in order of conversion. + // The 0 index is the object as serialized on the wire. If conversion has occured, + // other objects may be present. The right most object is the same as would be returned + // by a normal Decode call. + Objects []Object +} diff --git a/pkg/runtime/unstructured.go b/pkg/runtime/unstructured.go index 158961fd8ef..a236e9adb6b 100644 --- a/pkg/runtime/unstructured.go +++ b/pkg/runtime/unstructured.go @@ -18,9 +18,7 @@ package runtime import ( "encoding/json" - "fmt" - "net/url" - "reflect" + "io" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/conversion" @@ -28,36 +26,19 @@ import ( // UnstructuredJSONScheme is capable of converting JSON data into the Unstructured // type, which can be used for generic access to objects without a predefined scheme. -var UnstructuredJSONScheme ObjectDecoder = unstructuredJSONScheme{} +// TODO: move into serializer/json. +var UnstructuredJSONScheme Decoder = unstructuredJSONScheme{} type unstructuredJSONScheme struct{} -var _ Decoder = unstructuredJSONScheme{} -var _ ObjectDecoder = unstructuredJSONScheme{} +var _ Codec = unstructuredJSONScheme{} -// Recognizes returns true for any version or kind that is specified (internal -// versions are specifically excluded). -func (unstructuredJSONScheme) Recognizes(gvk unversioned.GroupVersionKind) bool { - return !gvk.GroupVersion().IsEmpty() && len(gvk.Kind) > 0 -} - -func (s unstructuredJSONScheme) Decode(data []byte) (Object, error) { +func (s unstructuredJSONScheme) Decode(data []byte, _ *unversioned.GroupVersionKind, _ Object) (Object, *unversioned.GroupVersionKind, error) { unstruct := &Unstructured{} - if err := DecodeInto(s, data, unstruct); err != nil { - return nil, err - } - return unstruct, nil -} - -func (unstructuredJSONScheme) DecodeInto(data []byte, obj Object) error { - unstruct, ok := obj.(*Unstructured) - if !ok { - return fmt.Errorf("the unstructured JSON scheme does not recognize %v", reflect.TypeOf(obj)) - } m := make(map[string]interface{}) if err := json.Unmarshal(data, &m); err != nil { - return err + return nil, nil, err } if v, ok := m["kind"]; ok { if s, ok := v.(string); ok { @@ -69,44 +50,30 @@ func (unstructuredJSONScheme) DecodeInto(data []byte, obj Object) error { unstruct.APIVersion = s } } + if len(unstruct.APIVersion) == 0 { - return conversion.NewMissingVersionErr(string(data)) + return nil, nil, conversion.NewMissingVersionErr(string(data)) } + gv, err := unversioned.ParseGroupVersion(unstruct.APIVersion) + if err != nil { + return nil, nil, err + } + gvk := gv.WithKind(unstruct.Kind) if len(unstruct.Kind) == 0 { - return conversion.NewMissingKindErr(string(data)) + return nil, &gvk, conversion.NewMissingKindErr(string(data)) } unstruct.Object = m - return nil + return unstruct, &gvk, nil } -func (unstructuredJSONScheme) DecodeIntoWithSpecifiedVersionKind(data []byte, obj Object, gvk unversioned.GroupVersionKind) error { - return nil -} - -func (unstructuredJSONScheme) DecodeToVersion(data []byte, gv unversioned.GroupVersion) (Object, error) { - return nil, nil -} - -func (unstructuredJSONScheme) DecodeParametersInto(paramaters url.Values, obj Object) error { - return nil -} - -func (unstructuredJSONScheme) DataKind(data []byte) (unversioned.GroupVersionKind, error) { - obj := TypeMeta{} - if err := json.Unmarshal(data, &obj); err != nil { - return unversioned.GroupVersionKind{}, err +func (s unstructuredJSONScheme) EncodeToStream(obj Object, w io.Writer, overrides ...unversioned.GroupVersion) error { + switch t := obj.(type) { + case *Unstructured: + return json.NewEncoder(w).Encode(t.Object) + case *Unknown: + _, err := w.Write(t.RawJSON) + return err + default: + return json.NewEncoder(w).Encode(t) } - if len(obj.APIVersion) == 0 { - return unversioned.GroupVersionKind{}, conversion.NewMissingVersionErr(string(data)) - } - if len(obj.Kind) == 0 { - return unversioned.GroupVersionKind{}, conversion.NewMissingKindErr(string(data)) - } - - gv, err := unversioned.ParseGroupVersion(obj.APIVersion) - if err != nil { - return unversioned.GroupVersionKind{}, err - } - - return gv.WithKind(obj.Kind), nil } diff --git a/pkg/runtime/unstructured_test.go b/pkg/runtime/unstructured_test.go index 120e3ff80a5..cca0fe25119 100644 --- a/pkg/runtime/unstructured_test.go +++ b/pkg/runtime/unstructured_test.go @@ -42,7 +42,7 @@ func TestDecodeUnstructured(t *testing.T) { if pod, ok := pl.Items[1].(*runtime.Unstructured); !ok || pod.Object["kind"] != "Pod" || pod.Object["metadata"].(map[string]interface{})["name"] != "test" { t.Errorf("object not converted: %#v", pl.Items[1]) } - if _, ok := pl.Items[2].(*runtime.Unknown); !ok { - t.Errorf("object should not have been converted: %#v", pl.Items[2]) + if pod, ok := pl.Items[2].(*runtime.Unstructured); !ok || pod.Object["kind"] != "Pod" || pod.Object["metadata"].(map[string]interface{})["name"] != "test" { + t.Errorf("object not converted: %#v", pl.Items[2]) } }