From 12a5eeea17ba75a84708eb9d47b69fc55970c65d Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sun, 22 May 2016 19:10:42 -0400 Subject: [PATCH 1/7] Introduce GroupVersioner for capturing desired target version Convert single GV and lists of GVs into an interface that can handle more complex scenarios (everything internal, nothing supported). Pass the interface down into conversion. --- pkg/api/meta/restmapper_test.go | 2 +- pkg/api/serialization_test.go | 13 +- pkg/api/testapi/testapi.go | 6 +- pkg/api/unversioned/group_version.go | 38 +++ pkg/apiserver/apiserver.go | 2 +- pkg/apiserver/negotiate_test.go | 4 +- .../clientcmd/api/latest/latest.go | 4 +- pkg/conversion/converter.go | 2 + pkg/genericapiserver/storage_factory.go | 30 +- pkg/registry/thirdpartyresourcedata/codec.go | 12 +- pkg/runtime/codec.go | 80 ++++++ pkg/runtime/helper.go | 2 +- pkg/runtime/interfaces.go | 27 +- pkg/runtime/scheme.go | 170 ++++------- pkg/runtime/scheme_test.go | 267 +++++++++++++++++- pkg/runtime/serializer/codec_factory.go | 40 ++- pkg/runtime/serializer/codec_test.go | 8 +- pkg/runtime/serializer/negotiated_codec.go | 5 +- .../serializer/versioning/versioning.go | 165 +++-------- .../serializer/versioning/versioning_test.go | 29 +- pkg/runtime/unstructured.go | 11 +- pkg/runtime/unversioned_test.go | 19 +- plugin/pkg/scheduler/api/latest/latest.go | 4 +- test/integration/framework/serializer.go | 9 +- 24 files changed, 617 insertions(+), 332 deletions(-) diff --git a/pkg/api/meta/restmapper_test.go b/pkg/api/meta/restmapper_test.go index 4240981300c..c4900d4aa44 100644 --- a/pkg/api/meta/restmapper_test.go +++ b/pkg/api/meta/restmapper_test.go @@ -32,7 +32,7 @@ func (fakeConvertor) Convert(in, out interface{}) error { return nil } -func (fakeConvertor) ConvertToVersion(in runtime.Object, _ unversioned.GroupVersion) (runtime.Object, error) { +func (fakeConvertor) ConvertToVersion(in runtime.Object, _ runtime.GroupVersioner) (runtime.Object, error) { return in, nil } diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index 4a44937a913..ec2894a8d03 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -43,7 +43,6 @@ import ( "k8s.io/kubernetes/pkg/conversion" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime/serializer/streaming" - "k8s.io/kubernetes/pkg/runtime/serializer/versioning" "k8s.io/kubernetes/pkg/util/diff" "k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/watch" @@ -181,10 +180,14 @@ func TestSetControllerConversion(t *testing.T) { t.Fatalf("unexpected encoding error: %v", err) } - decoder := api.Codecs.UniversalDecoder(*extGroup.GroupVersion(), *defaultGroup.GroupVersion()) - if err := versioning.EnableCrossGroupDecoding(decoder, extGroup.GroupVersion().Group, defaultGroup.GroupVersion().Group); err != nil { - t.Fatalf("unexpected error while enabling cross-group decoding: %v", err) - } + decoder := api.Codecs.DecoderToVersion( + api.Codecs.UniversalDeserializer(), + runtime.NewMultiGroupVersioner( + *defaultGroup.GroupVersion(), + unversioned.GroupKind{Group: defaultGroup.GroupVersion().Group}, + unversioned.GroupKind{Group: extGroup.GroupVersion().Group}, + ), + ) t.Logf("rs.v1beta1.extensions -> rc._internal") if err := runtime.DecodeInto(decoder, data, rc); err != nil { diff --git a/pkg/api/testapi/testapi.go b/pkg/api/testapi/testapi.go index a2b9c3b1f7b..0ad8b08c678 100644 --- a/pkg/api/testapi/testapi.go +++ b/pkg/api/testapi/testapi.go @@ -281,7 +281,7 @@ func (g TestGroup) Codec() runtime.Codec { if serializer.Serializer == nil { return api.Codecs.LegacyCodec(g.externalGroupVersion) } - return api.Codecs.CodecForVersions(serializer, api.Codecs.UniversalDeserializer(), []unversioned.GroupVersion{g.externalGroupVersion}, nil) + return api.Codecs.CodecForVersions(serializer, api.Codecs.UniversalDeserializer(), unversioned.GroupVersions{g.externalGroupVersion}, nil) } // NegotiatedSerializer returns the negotiated serializer for the server. @@ -309,7 +309,7 @@ func (g TestGroup) StorageCodec() runtime.Codec { } ds := recognizer.NewDecoder(s, api.Codecs.UniversalDeserializer()) - return api.Codecs.CodecForVersions(s, ds, []unversioned.GroupVersion{g.externalGroupVersion}, nil) + return api.Codecs.CodecForVersions(s, ds, unversioned.GroupVersions{g.externalGroupVersion}, nil) } // Converter returns the api.Scheme for the API version to test against, as set by the @@ -393,7 +393,7 @@ func (g TestGroup) RESTMapper() meta.RESTMapper { } // ExternalGroupVersions returns all external group versions allowed for the server. -func ExternalGroupVersions() []unversioned.GroupVersion { +func ExternalGroupVersions() unversioned.GroupVersions { versions := []unversioned.GroupVersion{} for _, g := range Groups { gv := g.GroupVersion() diff --git a/pkg/api/unversioned/group_version.go b/pkg/api/unversioned/group_version.go index 8f747892171..73cc1eb0556 100644 --- a/pkg/api/unversioned/group_version.go +++ b/pkg/api/unversioned/group_version.go @@ -179,6 +179,25 @@ func (gv GroupVersion) String() string { return gv.Version } +// KindForGroupVersionKinds identifies the preferred GroupVersionKind out of a list. It returns ok false +// if none of the options match the group. It prefers a match to group and version over just group. +// TODO: Move GroupVersion to a package under pkg/runtime, since it's used by scheme. +// TODO: Introduce an adapter type between GroupVersion and runtime.GroupVersioner, and use LegacyCodec(GroupVersion) +// in fewer places. +func (gv GroupVersion) KindForGroupVersionKinds(kinds []GroupVersionKind) (target GroupVersionKind, ok bool) { + for _, gvk := range kinds { + if gvk.Group == gv.Group && gvk.Version == gv.Version { + return gvk, true + } + } + for _, gvk := range kinds { + if gvk.Group == gv.Group { + return gv.WithKind(gvk.Kind), true + } + } + return GroupVersionKind{}, false +} + // ParseGroupVersion turns "group/version" string into a GroupVersion struct. It reports error // if it cannot parse the string. func ParseGroupVersion(gv string) (GroupVersion, error) { @@ -241,6 +260,25 @@ func (gv *GroupVersion) UnmarshalText(value []byte) error { return gv.unmarshal(value) } +// GroupVersions can be used to represent a set of desired group versions. +// TODO: Move GroupVersions to a package under pkg/runtime, since it's used by scheme. +// TODO: Introduce an adapter type between GroupVersions and runtime.GroupVersioner, and use LegacyCodec(GroupVersion) +// in fewer places. +type GroupVersions []GroupVersion + +// KindForGroupVersionKinds identifies the preferred GroupVersionKind out of a list. It returns ok false +// if none of the options match the group. +func (gvs GroupVersions) KindForGroupVersionKinds(kinds []GroupVersionKind) (target GroupVersionKind, ok bool) { + for _, gv := range gvs { + target, ok := gv.KindForGroupVersionKinds(kinds) + if !ok { + continue + } + return target, true + } + return GroupVersionKind{}, false +} + // ToAPIVersionAndKind is a convenience method for satisfying runtime.Object on types that // do not use TypeMeta. func (gvk *GroupVersionKind) ToAPIVersionAndKind() (string, string) { diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 1c4a0fcd3f9..1d4b07c1680 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -320,7 +320,7 @@ type StripVersionNegotiatedSerializer struct { runtime.NegotiatedSerializer } -func (n StripVersionNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv unversioned.GroupVersion) runtime.Encoder { +func (n StripVersionNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { serializer, ok := encoder.(runtime.Serializer) if !ok { // The stripVersionEncoder needs both an encoder and decoder, but is called from a context that doesn't have access to the diff --git a/pkg/apiserver/negotiate_test.go b/pkg/apiserver/negotiate_test.go index 1735f3eebde..8187a1761a2 100644 --- a/pkg/apiserver/negotiate_test.go +++ b/pkg/apiserver/negotiate_test.go @@ -64,11 +64,11 @@ func (n *fakeNegotiater) StreamingSerializerForMediaType(mediaType string, optio }, n.streamSerializer != nil } -func (n *fakeNegotiater) EncoderForVersion(serializer runtime.Encoder, gv unversioned.GroupVersion) runtime.Encoder { +func (n *fakeNegotiater) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { return n.serializer } -func (n *fakeNegotiater) DecoderToVersion(serializer runtime.Decoder, gv unversioned.GroupVersion) runtime.Decoder { +func (n *fakeNegotiater) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { return n.serializer } diff --git a/pkg/client/unversioned/clientcmd/api/latest/latest.go b/pkg/client/unversioned/clientcmd/api/latest/latest.go index b77509cb563..a32b251c77c 100644 --- a/pkg/client/unversioned/clientcmd/api/latest/latest.go +++ b/pkg/client/unversioned/clientcmd/api/latest/latest.go @@ -60,7 +60,7 @@ func init() { Scheme, yamlSerializer, yamlSerializer, - []unversioned.GroupVersion{{Version: Version}}, - []unversioned.GroupVersion{{Version: runtime.APIVersionInternal}}, + unversioned.GroupVersion{Version: Version}, + runtime.InternalGroupVersioner, ) } diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index 7a18d6360f5..8941b18aefd 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -213,6 +213,8 @@ type Meta struct { // KeyNameMapping is an optional function which may map the listed key (field name) // into a source and destination value. KeyNameMapping FieldMappingFunc + // Context is an optional field that callers may use to pass info to conversion functions. + Context interface{} } // scope contains information about an ongoing conversion. diff --git a/pkg/genericapiserver/storage_factory.go b/pkg/genericapiserver/storage_factory.go index 05c3b4ff5f1..dd2d55b52f5 100644 --- a/pkg/genericapiserver/storage_factory.go +++ b/pkg/genericapiserver/storage_factory.go @@ -24,7 +24,6 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime/serializer/recognizer" - "k8s.io/kubernetes/pkg/runtime/serializer/versioning" "k8s.io/kubernetes/pkg/storage/storagebackend" "k8s.io/kubernetes/pkg/util/sets" @@ -262,18 +261,25 @@ func NewStorageCodec(storageMediaType string, ns runtime.StorageSerializer, stor s = runtime.NewBase64Serializer(s) } + encoder := ns.EncoderForVersion( + s, + runtime.NewMultiGroupVersioner( + storageVersion, + unversioned.GroupKind{Group: storageVersion.Group}, + unversioned.GroupKind{Group: memoryVersion.Group}, + ), + ) + ds := recognizer.NewDecoder(s, ns.UniversalDeserializer()) - encoder := ns.EncoderForVersion(s, storageVersion) - decoder := ns.DecoderToVersion(ds, memoryVersion) - if memoryVersion.Group != storageVersion.Group { - // Allow this codec to translate between groups. - if err := versioning.EnableCrossGroupEncoding(encoder, memoryVersion.Group, storageVersion.Group); err != nil { - return nil, fmt.Errorf("error setting up encoder from %v to %v: %v", memoryVersion, storageVersion, err) - } - if err := versioning.EnableCrossGroupDecoding(decoder, storageVersion.Group, memoryVersion.Group); err != nil { - return nil, fmt.Errorf("error setting up decoder from %v to %v: %v", storageVersion, memoryVersion, err) - } - } + decoder := ns.DecoderToVersion( + ds, + runtime.NewMultiGroupVersioner( + memoryVersion, + unversioned.GroupKind{Group: memoryVersion.Group}, + unversioned.GroupKind{Group: storageVersion.Group}, + ), + ) + return runtime.NewCodec(encoder, decoder), nil } diff --git a/pkg/registry/thirdpartyresourcedata/codec.go b/pkg/registry/thirdpartyresourcedata/codec.go index b96d789ca57..323b640324d 100644 --- a/pkg/registry/thirdpartyresourcedata/codec.go +++ b/pkg/registry/thirdpartyresourcedata/codec.go @@ -42,7 +42,7 @@ type thirdPartyObjectConverter struct { converter runtime.ObjectConvertor } -func (t *thirdPartyObjectConverter) ConvertToVersion(in runtime.Object, outVersion unversioned.GroupVersion) (out runtime.Object, err error) { +func (t *thirdPartyObjectConverter) ConvertToVersion(in runtime.Object, outVersion runtime.GroupVersioner) (out runtime.Object, err error) { switch in.(type) { // This seems weird, but in this case the ThirdPartyResourceData is really just a wrapper on the raw 3rd party data. // The actual thing printed/sent to server is the actual raw third party resource data, which only has one version. @@ -234,11 +234,11 @@ func (t *thirdPartyResourceDataCodecFactory) StreamingSerializerForMediaType(med } } -func (t *thirdPartyResourceDataCodecFactory) EncoderForVersion(s runtime.Encoder, gv unversioned.GroupVersion) runtime.Encoder { - return &thirdPartyResourceDataEncoder{delegate: t.delegate.EncoderForVersion(s, gv), gvk: gv.WithKind(t.kind)} +func (t *thirdPartyResourceDataCodecFactory) EncoderForVersion(s runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { + return &thirdPartyResourceDataEncoder{delegate: t.delegate.EncoderForVersion(s, gv), gvk: t.encodeGV.WithKind(t.kind)} } -func (t *thirdPartyResourceDataCodecFactory) DecoderToVersion(s runtime.Decoder, gv unversioned.GroupVersion) runtime.Decoder { +func (t *thirdPartyResourceDataCodecFactory) DecoderToVersion(s runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { return NewDecoder(t.delegate.DecoderToVersion(s, gv), t.kind) } @@ -517,6 +517,10 @@ func (t *thirdPartyResourceDataEncoder) Encode(obj runtime.Object, stream io.Wri listItems[ix] = json.RawMessage(buff.Bytes()) } + if t.gvk.IsEmpty() { + return fmt.Errorf("thirdPartyResourceDataEncoder was not given a target version") + } + encMap := struct { Kind string `json:"kind,omitempty"` Items []json.RawMessage `json:"items"` diff --git a/pkg/runtime/codec.go b/pkg/runtime/codec.go index dc70ea55903..58e1d3b6784 100644 --- a/pkg/runtime/codec.go +++ b/pkg/runtime/codec.go @@ -198,3 +198,83 @@ func (s base64Serializer) Decode(data []byte, defaults *unversioned.GroupVersion } return s.Serializer.Decode(out[:n], defaults, into) } + +var ( + // InternalGroupVersioner will always prefer the internal version for a given group version kind. + InternalGroupVersioner GroupVersioner = internalGroupVersioner{} + // DisabledGroupVersioner will reject all kinds passed to it. + DisabledGroupVersioner GroupVersioner = disabledGroupVersioner{} +) + +type internalGroupVersioner struct{} + +// KindForGroupVersionKinds returns an internal Kind if one is found, or converts the first provided kind to the internal version. +func (internalGroupVersioner) KindForGroupVersionKinds(kinds []unversioned.GroupVersionKind) (unversioned.GroupVersionKind, bool) { + for _, kind := range kinds { + if kind.Version == APIVersionInternal { + return kind, true + } + } + for _, kind := range kinds { + return unversioned.GroupVersionKind{Group: kind.Group, Version: APIVersionInternal, Kind: kind.Kind}, true + } + return unversioned.GroupVersionKind{}, false +} + +type disabledGroupVersioner struct{} + +// KindForGroupVersionKinds returns false for any input. +func (disabledGroupVersioner) KindForGroupVersionKinds(kinds []unversioned.GroupVersionKind) (unversioned.GroupVersionKind, bool) { + return unversioned.GroupVersionKind{}, false +} + +// GroupVersioners implements GroupVersioner and resolves to the first exact match for any kind. +type GroupVersioners []GroupVersioner + +// KindForGroupVersionKinds returns the first match of any of the group versioners, or false if no match occured. +func (gvs GroupVersioners) KindForGroupVersionKinds(kinds []unversioned.GroupVersionKind) (unversioned.GroupVersionKind, bool) { + for _, gv := range gvs { + target, ok := gv.KindForGroupVersionKinds(kinds) + if !ok { + continue + } + return target, true + } + return unversioned.GroupVersionKind{}, false +} + +// Assert that unversioned.GroupVersion and GroupVersions implement GroupVersioner +var _ GroupVersioner = unversioned.GroupVersion{} +var _ GroupVersioner = unversioned.GroupVersions{} +var _ GroupVersioner = multiGroupVersioner{} + +type multiGroupVersioner struct { + target unversioned.GroupVersion + acceptedGroupKinds []unversioned.GroupKind +} + +// NewMultiGroupVersioner returns the provided group version for any kind that matches one of the provided group kinds. +// Kind may be empty in the provided group kind, in which case any kind will match. +func NewMultiGroupVersioner(gv unversioned.GroupVersion, groupKinds ...unversioned.GroupKind) GroupVersioner { + if len(groupKinds) == 0 || (len(groupKinds) == 1 && groupKinds[0].Group == gv.Group) { + return gv + } + return multiGroupVersioner{target: gv, acceptedGroupKinds: groupKinds} +} + +// KindForGroupVersionKinds returns the target group version if any kind matches any of the original group kinds. It will +// use the originating kind where possible. +func (v multiGroupVersioner) KindForGroupVersionKinds(kinds []unversioned.GroupVersionKind) (unversioned.GroupVersionKind, bool) { + for _, src := range kinds { + for _, kind := range v.acceptedGroupKinds { + if kind.Group != src.Group { + continue + } + if len(kind.Kind) > 0 && kind.Kind != src.Kind { + continue + } + return v.target.WithKind(src.Kind), true + } + } + return unversioned.GroupVersionKind{}, false +} diff --git a/pkg/runtime/helper.go b/pkg/runtime/helper.go index 827cff1b846..2f8f161dde9 100644 --- a/pkg/runtime/helper.go +++ b/pkg/runtime/helper.go @@ -35,7 +35,7 @@ var _ ObjectConvertor = unsafeObjectConvertor{} // ConvertToVersion converts in to the provided outVersion without copying the input first, which // is only safe if the output object is not mutated or reused. -func (c unsafeObjectConvertor) ConvertToVersion(in Object, outVersion unversioned.GroupVersion) (Object, error) { +func (c unsafeObjectConvertor) ConvertToVersion(in Object, outVersion GroupVersioner) (Object, error) { return c.Scheme.UnsafeConvertToVersion(in, outVersion) } diff --git a/pkg/runtime/interfaces.go b/pkg/runtime/interfaces.go index e46324608e5..4341f89b3fa 100644 --- a/pkg/runtime/interfaces.go +++ b/pkg/runtime/interfaces.go @@ -30,12 +30,23 @@ const ( APIVersionInternal = "__internal" ) +// GroupVersioner refines a set of possible conversion targets into a single option. +type GroupVersioner interface { + // KindForGroupVersionKinds returns a desired target group version kind for the given input, or returns ok false if no + // target is known. In general, if the return target is not in the input list, the caller is expected to invoke + // Scheme.New(target) and then perform a conversion between the current Go type and the destination Go type. + // Sophisticated implementations may use additional information about the input kinds to pick a destination kind. + KindForGroupVersionKinds(kinds []unversioned.GroupVersionKind) (target unversioned.GroupVersionKind, ok bool) +} + +// Encoders write objects to a serialized form type Encoder interface { // Encode writes an object to a stream. Implementations may return errors if the versions are // incompatible, or if no conversion is defined. Encode(obj Object, w io.Writer) error } +// Decoders attempt to load an object from data. 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 @@ -117,12 +128,10 @@ type NegotiatedSerializer interface { // EncoderForVersion returns an encoder that ensures objects being written to the provided // serializer are in the provided group version. - // TODO: take multiple group versions - EncoderForVersion(serializer Encoder, gv unversioned.GroupVersion) Encoder + EncoderForVersion(serializer Encoder, gv GroupVersioner) Encoder // DecoderForVersion returns a decoder that ensures objects being read by the provided // serializer are in the provided group version by default. - // TODO: take multiple group versions - DecoderToVersion(serializer Decoder, gv unversioned.GroupVersion) Decoder + DecoderToVersion(serializer Decoder, gv GroupVersioner) Decoder } // StorageSerializer is an interface used for obtaining encoders, decoders, and serializers @@ -139,19 +148,17 @@ type StorageSerializer interface { // EncoderForVersion returns an encoder that ensures objects being written to the provided // serializer are in the provided group version. - // TODO: take multiple group versions - EncoderForVersion(serializer Encoder, gv unversioned.GroupVersion) Encoder + EncoderForVersion(serializer Encoder, gv GroupVersioner) Encoder // DecoderForVersion returns a decoder that ensures objects being read by the provided // serializer are in the provided group version by default. - // TODO: take multiple group versions - DecoderToVersion(serializer Decoder, gv unversioned.GroupVersion) Decoder + DecoderToVersion(serializer Decoder, gv GroupVersioner) Decoder } /////////////////////////////////////////////////////////////////////////////// // Non-codec interfaces type ObjectVersioner interface { - ConvertToVersion(in Object, outVersion unversioned.GroupVersion) (out Object, err error) + ConvertToVersion(in Object, gv GroupVersioner) (out Object, err error) } // ObjectConvertor converts an object to a different version. @@ -161,7 +168,7 @@ type ObjectConvertor interface { 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 unversioned.GroupVersion) (out Object, err error) + ConvertToVersion(in Object, gv GroupVersioner) (out Object, err error) ConvertFieldLabel(version, kind, label, value string) (string, string, error) } diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 3da67978b97..f5d3bd08298 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -237,7 +237,7 @@ func (s *Scheme) ObjectKinds(obj Object) ([]unversioned.GroupVersionKind, bool, gvks, ok := s.typeToGVK[t] if !ok { - return nil, false, ¬RegisteredErr{t: t} + return nil, false, NewNotRegisteredErr(unversioned.GroupVersionKind{}, t) } _, unversionedType := s.unversionedTypes[t] @@ -275,7 +275,7 @@ func (s *Scheme) New(kind unversioned.GroupVersionKind) (Object, error) { if t, exists := s.unversionedKinds[kind.Kind]; exists { return reflect.New(t).Interface().(Object), nil } - return nil, ¬RegisteredErr{gvk: kind} + return nil, NewNotRegisteredErr(kind, nil) } // AddGenericConversionFunc adds a function that accepts the ConversionFunc call pattern @@ -438,23 +438,13 @@ func (s *Scheme) DeepCopy(src interface{}) (interface{}, error) { // 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. You can call this with types that haven't been registered (for example, -// a to test conversion of types that are nested within registered types), but in -// that case, the conversion.Scope object passed to your conversion functions won't -// have SrcVersion or DestVersion fields set correctly in Meta(). -func (s *Scheme) Convert(in, out interface{}) error { - inVersion := unversioned.GroupVersion{Group: "unknown", Version: "unknown"} - outVersion := unversioned.GroupVersion{Group: "unknown", Version: "unknown"} - if inObj, ok := in.(Object); ok { - if gvks, _, err := s.ObjectKinds(inObj); err == nil { - inVersion = gvks[0].GroupVersion() - } - } - if outObj, ok := out.(Object); ok { - if gvks, _, err := s.ObjectKinds(outObj); err == nil { - outVersion = gvks[0].GroupVersion() - } - } - flags, meta := s.generateConvertMeta(inVersion, outVersion, in) +// a to test conversion of types that are nested within registered types). The +// context interface is passed to the convertor. +// TODO: identify whether context should be hidden, or behind a formal context/scope +// interface +func (s *Scheme) Convert(in, out interface{}, context interface{}) error { + flags, meta := s.generateConvertMeta(in) + meta.Context = context if flags == 0 { flags = conversion.AllowDifferentFieldTypeNames } @@ -478,73 +468,20 @@ 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. The serializer handles loading/serializing nested objects. -func (s *Scheme) ConvertToVersion(in Object, outVersion unversioned.GroupVersion) (Object, error) { - switch in.(type) { - case *Unknown, *Unstructured, *UnstructuredList: - old := in.GetObjectKind().GroupVersionKind() - defer in.GetObjectKind().SetGroupVersionKind(old) - setTargetVersion(in, s, outVersion) - return in, nil - } - t := reflect.TypeOf(in) - if t.Kind() != reflect.Ptr { - return nil, fmt.Errorf("only pointer types may be converted: %v", t) - } - - t = t.Elem() - if t.Kind() != reflect.Struct { - return nil, fmt.Errorf("only pointers to struct types may be converted: %v", t) - } - - var kind unversioned.GroupVersionKind - if unversionedKind, ok := s.unversionedTypes[t]; ok { - kind = unversionedKind - } else { - kinds, ok := s.typeToGVK[t] - if !ok || len(kinds) == 0 { - return nil, fmt.Errorf("%v is not a registered type and cannot be converted into version %q", t, outVersion) - } - kind = kinds[0] - } - - outKind := outVersion.WithKind(kind.Kind) - - inKinds, _, err := s.ObjectKinds(in) - if err != nil { - return nil, err - } - - out, err := s.New(outKind) - if err != nil { - return nil, err - } - - flags, meta := s.generateConvertMeta(inKinds[0].GroupVersion(), outVersion, in) - if err := s.converter.Convert(in, out, flags, meta); err != nil { - return nil, err - } - - setTargetVersion(out, s, outVersion) - return out, nil +// returned. Passes target down to the conversion methods as the Context on the scope. +func (s *Scheme) ConvertToVersion(in Object, target GroupVersioner) (Object, error) { + return s.convertToVersion(true, in, target) } -// UnsafeConvertToVersion will convert in to the provided outVersion if such a conversion is possible, +// UnsafeConvertToVersion will convert in to the provided target if such a conversion is possible, // but does not guarantee the output object does not share fields with the input object. It attempts to be as // efficient as possible when doing conversion. -func (s *Scheme) UnsafeConvertToVersion(in Object, outVersion unversioned.GroupVersion) (Object, error) { - switch t := in.(type) { - case *Unknown: - t.APIVersion = outVersion.String() - return t, nil - case *Unstructured: - t.SetAPIVersion(outVersion.String()) - return t, nil - case *UnstructuredList: - t.SetAPIVersion(outVersion.String()) - return t, nil - } +func (s *Scheme) UnsafeConvertToVersion(in Object, target GroupVersioner) (Object, error) { + return s.convertToVersion(false, in, target) +} +// convertToVersion handles conversion with an optional copy. +func (s *Scheme) convertToVersion(copy bool, in Object, target GroupVersioner) (Object, error) { // determine the incoming kinds with as few allocations as possible. t := reflect.TypeOf(in) if t.Kind() != reflect.Ptr { @@ -556,64 +493,69 @@ func (s *Scheme) UnsafeConvertToVersion(in Object, outVersion unversioned.GroupV } kinds, ok := s.typeToGVK[t] if !ok || len(kinds) == 0 { - return nil, fmt.Errorf("%v is not a registered type and cannot be converted into version %q", t, outVersion) + return nil, NewNotRegisteredErr(unversioned.GroupVersionKind{}, t) } - // if the Go type is also registered to the destination kind, no conversion is necessary - for i := range kinds { - if kinds[i].Version == outVersion.Version && kinds[i].Group == outVersion.Group { - setTargetKind(in, kinds[i]) - return in, nil + gvk, ok := target.KindForGroupVersionKinds(kinds) + if !ok { + // TODO: should this be a typed error? + return nil, fmt.Errorf("%v is not suitable for converting to %q", t, target) + } + + // target wants to use the existing type, set kind and return (no conversion necessary) + for _, kind := range kinds { + if gvk == kind { + return copyAndSetTargetKind(copy, s, in, gvk) } } // type is unversioned, no conversion necessary - // it should be possible to avoid this allocation if unversionedKind, ok := s.unversionedTypes[t]; ok { - kind := unversionedKind - outKind := outVersion.WithKind(kind.Kind) - setTargetKind(in, outKind) - return in, nil + if gvk, ok := target.KindForGroupVersionKinds([]unversioned.GroupVersionKind{unversionedKind}); ok { + return copyAndSetTargetKind(copy, s, in, gvk) + } + return copyAndSetTargetKind(copy, s, in, unversionedKind) } - // allocate a new object as the target using the target kind - // TODO: this should look in the target group version and find the first kind that matches, rather than the - // first kind registered in typeToGVK - kind := kinds[0] - kind.Version = outVersion.Version - kind.Group = outVersion.Group - out, err := s.New(kind) + out, err := s.New(gvk) if err != nil { return nil, err } - // TODO: try to avoid the allocations here - in fast paths we are not likely to need these flags or meta - flags, meta := s.converter.DefaultMeta(t) + if copy { + copied, err := s.Copy(in) + if err != nil { + return nil, err + } + in = copied + } + + flags, meta := s.generateConvertMeta(in) + meta.Context = target if err := s.converter.Convert(in, out, flags, meta); err != nil { return nil, err } - setTargetKind(out, kind) + setTargetKind(out, gvk) return out, nil } // generateConvertMeta constructs the meta value we pass to Convert. -func (s *Scheme) generateConvertMeta(srcGroupVersion, destGroupVersion unversioned.GroupVersion, in interface{}) (conversion.FieldMatchingFlags, *conversion.Meta) { +func (s *Scheme) generateConvertMeta(in interface{}) (conversion.FieldMatchingFlags, *conversion.Meta) { return s.converter.DefaultMeta(reflect.TypeOf(in)) } -// setTargetVersion is deprecated and should be replaced by use of setTargetKind -func setTargetVersion(obj Object, raw *Scheme, gv unversioned.GroupVersion) { - if gv.Version == APIVersionInternal { - // internal is a special case - obj.GetObjectKind().SetGroupVersionKind(unversioned.GroupVersionKind{}) - return - } - if gvks, _, _ := raw.ObjectKinds(obj); len(gvks) > 0 { - obj.GetObjectKind().SetGroupVersionKind(unversioned.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: gvks[0].Kind}) - } else { - obj.GetObjectKind().SetGroupVersionKind(unversioned.GroupVersionKind{Group: gv.Group, Version: gv.Version}) +// copyAndSetTargetKind performs a conditional copy before returning the object, or an error if copy was not successful. +func copyAndSetTargetKind(copy bool, copier ObjectCopier, obj Object, kind unversioned.GroupVersionKind) (Object, error) { + if copy { + copied, err := copier.Copy(obj) + if err != nil { + return nil, err + } + obj = copied } + setTargetKind(obj, kind) + return obj, nil } // setTargetKind sets the kind on an object, taking into account whether the target kind is the internal version. diff --git a/pkg/runtime/scheme_test.go b/pkg/runtime/scheme_test.go index b81b9528f57..135d9c2f93c 100644 --- a/pkg/runtime/scheme_test.go +++ b/pkg/runtime/scheme_test.go @@ -18,6 +18,7 @@ package runtime_test import ( "reflect" + "strings" "testing" "github.com/google/gofuzz" @@ -460,6 +461,16 @@ type ExternalInternalSame struct { A TestType2 `json:"A,omitempty"` } +type UnversionedType struct { + MyWeirdCustomEmbeddedVersionKindField `json:",inline"` + A string `json:"A,omitempty"` +} + +type UnknownType struct { + MyWeirdCustomEmbeddedVersionKindField `json:",inline"` + A string `json:"A,omitempty"` +} + func (obj *MyWeirdCustomEmbeddedVersionKindField) GetObjectKind() unversioned.ObjectKind { return obj } func (obj *MyWeirdCustomEmbeddedVersionKindField) SetGroupVersionKind(gvk unversioned.GroupVersionKind) { obj.APIVersion, obj.ObjectKind = gvk.ToAPIVersionAndKind() @@ -500,6 +511,8 @@ var TestObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 100).Funcs( func GetTestScheme() *runtime.Scheme { internalGV := unversioned.GroupVersion{Version: "__internal"} externalGV := unversioned.GroupVersion{Version: "v1"} + alternateExternalGV := unversioned.GroupVersion{Group: "custom", Version: "v1"} + differentExternalGV := unversioned.GroupVersion{Group: "other", Version: "v2"} s := runtime.NewScheme() // Ordinarily, we wouldn't add TestType2, but because this is a test and @@ -511,6 +524,11 @@ func GetTestScheme() *runtime.Scheme { s.AddKnownTypeWithName(externalGV.WithKind("TestType2"), &ExternalTestType2{}) s.AddKnownTypeWithName(internalGV.WithKind("TestType3"), &TestType1{}) s.AddKnownTypeWithName(externalGV.WithKind("TestType3"), &ExternalTestType1{}) + s.AddKnownTypeWithName(externalGV.WithKind("TestType4"), &ExternalTestType1{}) + s.AddKnownTypeWithName(alternateExternalGV.WithKind("TestType3"), &ExternalTestType1{}) + s.AddKnownTypeWithName(alternateExternalGV.WithKind("TestType5"), &ExternalTestType1{}) + s.AddKnownTypeWithName(differentExternalGV.WithKind("TestType1"), &ExternalTestType1{}) + s.AddUnversionedTypes(externalGV, &UnversionedType{}) return s } @@ -528,7 +546,7 @@ func TestKnownTypes(t *testing.T) { } } -func TestConvertToVersion(t *testing.T) { +func TestConvertToVersionBasic(t *testing.T) { s := GetTestScheme() tt := &TestType1{A: "I'm not a pointer object"} other, err := s.ConvertToVersion(tt, unversioned.GroupVersion{Version: "v1"}) @@ -537,13 +555,258 @@ func TestConvertToVersion(t *testing.T) { } converted, ok := other.(*ExternalTestType1) if !ok { - t.Fatalf("Got wrong type") + t.Fatalf("Got wrong type: %T", other) } if tt.A != converted.A { t.Fatalf("Failed to convert object correctly: %#v", converted) } } +type testGroupVersioner struct { + target unversioned.GroupVersionKind + ok bool +} + +func (m testGroupVersioner) KindForGroupVersionKinds(kinds []unversioned.GroupVersionKind) (unversioned.GroupVersionKind, bool) { + return m.target, m.ok +} + +func TestConvertToVersion(t *testing.T) { + testCases := []struct { + scheme *runtime.Scheme + in runtime.Object + gv runtime.GroupVersioner + same bool + out runtime.Object + errFn func(error) bool + }{ + // errors if the type is not registered in the scheme + { + scheme: GetTestScheme(), + in: &UnknownType{}, + errFn: func(err error) bool { return err != nil && runtime.IsNotRegisteredError(err) }, + }, + // errors if the group versioner returns no target + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: testGroupVersioner{}, + errFn: func(err error) bool { + return err != nil && strings.Contains(err.Error(), "is not suitable for converting") + }, + }, + // converts to internal + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: unversioned.GroupVersion{Version: "__internal"}, + out: &TestType1{A: "test"}, + }, + // prefers the first group version in the list + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: unversioned.GroupVersions{{Version: "__internal"}, {Version: "v1"}}, + out: &TestType1{A: "test"}, + }, + // unversioned type returned as-is + { + scheme: GetTestScheme(), + in: &UnversionedType{A: "test"}, + gv: unversioned.GroupVersions{{Version: "v1"}}, + same: true, + out: &UnversionedType{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "v1", ObjectKind: "UnversionedType"}, + A: "test", + }, + }, + // detected as already being in the target version + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: unversioned.GroupVersions{{Version: "v1"}}, + same: true, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "v1", ObjectKind: "TestType1"}, + A: "test", + }, + }, + // detected as already being in the first target version + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: unversioned.GroupVersions{{Version: "v1"}, {Version: "__internal"}}, + same: true, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "v1", ObjectKind: "TestType1"}, + A: "test", + }, + }, + // detected as already being in the first target version + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: unversioned.GroupVersions{{Version: "v1"}, {Version: "__internal"}}, + same: true, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "v1", ObjectKind: "TestType1"}, + A: "test", + }, + }, + // the external type is registered in multiple groups, versions, and kinds, and can be targeted to all of them (1/3): different kind + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: testGroupVersioner{ok: true, target: unversioned.GroupVersionKind{Kind: "TestType3", Version: "v1"}}, + same: true, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "v1", ObjectKind: "TestType3"}, + A: "test", + }, + }, + // the external type is registered in multiple groups, versions, and kinds, and can be targeted to all of them (2/3): different gv + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: testGroupVersioner{ok: true, target: unversioned.GroupVersionKind{Kind: "TestType3", Group: "custom", Version: "v1"}}, + same: true, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "custom/v1", ObjectKind: "TestType3"}, + A: "test", + }, + }, + // the external type is registered in multiple groups, versions, and kinds, and can be targeted to all of them (3/3): different gvk + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: testGroupVersioner{ok: true, target: unversioned.GroupVersionKind{Group: "custom", Version: "v1", Kind: "TestType5"}}, + same: true, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "custom/v1", ObjectKind: "TestType5"}, + A: "test", + }, + }, + // multi group versioner recognizes multiple groups and forces the output to a particular version, copies because version differs + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: runtime.NewMultiGroupVersioner(unversioned.GroupVersion{Group: "other", Version: "v2"}, unversioned.GroupKind{Group: "custom", Kind: "TestType3"}, unversioned.GroupKind{Kind: "TestType1"}), + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "other/v2", ObjectKind: "TestType1"}, + A: "test", + }, + }, + // multi group versioner recognizes multiple groups and forces the output to a particular version, copies because version differs + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: runtime.NewMultiGroupVersioner(unversioned.GroupVersion{Group: "other", Version: "v2"}, unversioned.GroupKind{Kind: "TestType1"}, unversioned.GroupKind{Group: "custom", Kind: "TestType3"}), + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "other/v2", ObjectKind: "TestType1"}, + A: "test", + }, + }, + // multi group versioner is unable to find a match when kind AND group don't match (there is no TestType1 kind in group "other", and no kind "TestType5" in the default group) + { + scheme: GetTestScheme(), + in: &TestType1{A: "test"}, + gv: runtime.NewMultiGroupVersioner(unversioned.GroupVersion{Group: "custom", Version: "v1"}, unversioned.GroupKind{Group: "other"}, unversioned.GroupKind{Kind: "TestType5"}), + errFn: func(err error) bool { + return err != nil && strings.Contains(err.Error(), "is not suitable for converting") + }, + }, + // multi group versioner recognizes multiple groups and forces the output to a particular version, performs no copy + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: runtime.NewMultiGroupVersioner(unversioned.GroupVersion{Group: "", Version: "v1"}, unversioned.GroupKind{Group: "custom", Kind: "TestType3"}, unversioned.GroupKind{Kind: "TestType1"}), + same: true, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "v1", ObjectKind: "TestType1"}, + A: "test", + }, + }, + // multi group versioner recognizes multiple groups and forces the output to a particular version, performs no copy + { + scheme: GetTestScheme(), + in: &ExternalTestType1{A: "test"}, + gv: runtime.NewMultiGroupVersioner(unversioned.GroupVersion{Group: "", Version: "v1"}, unversioned.GroupKind{Kind: "TestType1"}, unversioned.GroupKind{Group: "custom", Kind: "TestType3"}), + same: true, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "v1", ObjectKind: "TestType1"}, + A: "test", + }, + }, + // group versioner can choose a particular target kind for a given input when kind is the same across group versions + { + scheme: GetTestScheme(), + in: &TestType1{A: "test"}, + gv: testGroupVersioner{ok: true, target: unversioned.GroupVersionKind{Version: "v1", Kind: "TestType3"}}, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "v1", ObjectKind: "TestType3"}, + A: "test", + }, + }, + // group versioner can choose a different kind + { + scheme: GetTestScheme(), + in: &TestType1{A: "test"}, + gv: testGroupVersioner{ok: true, target: unversioned.GroupVersionKind{Kind: "TestType5", Group: "custom", Version: "v1"}}, + out: &ExternalTestType1{ + MyWeirdCustomEmbeddedVersionKindField: MyWeirdCustomEmbeddedVersionKindField{APIVersion: "custom/v1", ObjectKind: "TestType5"}, + A: "test", + }, + }, + } + for i, test := range testCases { + original, _ := test.scheme.DeepCopy(test.in) + out, err := test.scheme.ConvertToVersion(test.in, test.gv) + switch { + case test.errFn != nil: + if !test.errFn(err) { + t.Errorf("%d: unexpected error: %v", i, err) + } + continue + case err != nil: + t.Errorf("%d: unexpected error: %v", i, err) + continue + } + if out == test.in { + t.Errorf("%d: ConvertToVersion should always copy out: %#v", i, out) + continue + } + + if test.same { + if !reflect.DeepEqual(original, test.in) { + t.Errorf("%d: unexpected mutation of input: %s", i, diff.ObjectReflectDiff(original, test.in)) + continue + } + if !reflect.DeepEqual(out, test.out) { + t.Errorf("%d: unexpected out: %s", i, diff.ObjectReflectDiff(out, test.out)) + continue + } + unsafe, err := test.scheme.UnsafeConvertToVersion(test.in, test.gv) + if err != nil { + t.Errorf("%d: unexpected error: %v", i, err) + continue + } + if !reflect.DeepEqual(unsafe, test.out) { + t.Errorf("%d: unexpected unsafe: %s", i, diff.ObjectReflectDiff(unsafe, test.out)) + continue + } + if unsafe != test.in { + t.Errorf("%d: UnsafeConvertToVersion should return same object: %#v", i, unsafe) + continue + } + continue + } + if !reflect.DeepEqual(out, test.out) { + t.Errorf("%d: unexpected out: %s", i, diff.ObjectReflectDiff(out, test.out)) + continue + } + } +} + func TestMetaValues(t *testing.T) { internalGV := unversioned.GroupVersion{Group: "test.group", Version: "__internal"} externalGV := unversioned.GroupVersion{Group: "test.group", Version: "externalVersion"} diff --git a/pkg/runtime/serializer/codec_factory.go b/pkg/runtime/serializer/codec_factory.go index 66a9ae5dbe2..ed5e8c471c3 100644 --- a/pkg/runtime/serializer/codec_factory.go +++ b/pkg/runtime/serializer/codec_factory.go @@ -188,13 +188,17 @@ func (f CodecFactory) SupportedStreamingMediaTypes() []string { return f.streamingAccepts } -// LegacyCodec encodes output to a given API version, and decodes output into the internal form from -// any recognized source. The returned codec will always encode output to JSON. +// LegacyCodec encodes output to a given API versions, and decodes output into the internal form from +// any recognized source. The returned codec will always encode output to JSON. If a type is not +// found in the list of versions an error will be returned. // // This method is deprecated - clients and servers should negotiate a serializer by mime-type and // invoke CodecForVersions. Callers that need only to read data should use UniversalDecoder(). +// +// TODO: make this call exist only in pkg/api, and initialize it with the set of default versions. +// All other callers will be forced to request a Codec directly. func (f CodecFactory) LegacyCodec(version ...unversioned.GroupVersion) runtime.Codec { - return versioning.NewCodecForScheme(f.scheme, f.legacySerializer, f.universal, version, nil) + return versioning.NewCodecForScheme(f.scheme, f.legacySerializer, f.universal, unversioned.GroupVersions(version), runtime.InternalGroupVersioner) } // UniversalDeserializer can convert any stored data recognized by this factory into a Go object that satisfies @@ -211,25 +215,39 @@ func (f CodecFactory) UniversalDeserializer() runtime.Decoder { // defaulting. // // TODO: the decoder will eventually be removed in favor of dealing with objects in their versioned form +// TODO: only accept a group versioner func (f CodecFactory) UniversalDecoder(versions ...unversioned.GroupVersion) runtime.Decoder { - return f.CodecForVersions(nil, f.universal, nil, versions) + var versioner runtime.GroupVersioner + if len(versions) == 0 { + versioner = runtime.InternalGroupVersioner + } else { + versioner = unversioned.GroupVersions(versions) + } + return f.CodecForVersions(nil, f.universal, nil, versioner) } // CodecForVersions creates a codec with the provided serializer. If an object is decoded and its group is not in the list, // it will default to runtime.APIVersionInternal. If encode is not specified for an object's group, the object is not // converted. If encode or decode are nil, no conversion is performed. -func (f CodecFactory) CodecForVersions(encoder runtime.Encoder, decoder runtime.Decoder, encode []unversioned.GroupVersion, decode []unversioned.GroupVersion) runtime.Codec { +func (f CodecFactory) CodecForVersions(encoder runtime.Encoder, decoder runtime.Decoder, encode runtime.GroupVersioner, decode runtime.GroupVersioner) runtime.Codec { + // TODO: these are for backcompat, remove them in the future + if encode == nil { + encode = runtime.DisabledGroupVersioner + } + if decode == nil { + decode = runtime.InternalGroupVersioner + } return versioning.NewCodecForScheme(f.scheme, encoder, decoder, encode, decode) } // DecoderToVersion returns a decoder that targets the provided group version. -func (f CodecFactory) DecoderToVersion(decoder runtime.Decoder, gv unversioned.GroupVersion) runtime.Decoder { - return f.CodecForVersions(nil, decoder, nil, []unversioned.GroupVersion{gv}) +func (f CodecFactory) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { + return f.CodecForVersions(nil, decoder, nil, gv) } // EncoderForVersion returns an encoder that targets the provided group version. -func (f CodecFactory) EncoderForVersion(encoder runtime.Encoder, gv unversioned.GroupVersion) runtime.Encoder { - return f.CodecForVersions(encoder, nil, []unversioned.GroupVersion{gv}, nil) +func (f CodecFactory) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { + return f.CodecForVersions(encoder, nil, gv, nil) } // SerializerForMediaType returns a serializer that matches the provided RFC2046 mediaType, or false if no such @@ -317,7 +335,7 @@ type DirectCodecFactory struct { } // EncoderForVersion returns an encoder that does not do conversion. gv is ignored. -func (f DirectCodecFactory) EncoderForVersion(serializer runtime.Encoder, gv unversioned.GroupVersion) runtime.Encoder { +func (f DirectCodecFactory) EncoderForVersion(serializer runtime.Encoder, _ runtime.GroupVersioner) runtime.Encoder { return DirectCodec{ runtime.NewCodec(serializer, nil), f.CodecFactory.scheme, @@ -325,7 +343,7 @@ func (f DirectCodecFactory) EncoderForVersion(serializer runtime.Encoder, gv unv } // DecoderToVersion returns an decoder that does not do conversion. gv is ignored. -func (f DirectCodecFactory) DecoderToVersion(serializer runtime.Decoder, gv unversioned.GroupVersion) runtime.Decoder { +func (f DirectCodecFactory) DecoderToVersion(serializer runtime.Decoder, _ runtime.GroupVersioner) runtime.Decoder { return DirectCodec{ runtime.NewCodec(nil, serializer), nil, diff --git a/pkg/runtime/serializer/codec_test.go b/pkg/runtime/serializer/codec_test.go index 0982fb3aae2..87158a0c23e 100644 --- a/pkg/runtime/serializer/codec_test.go +++ b/pkg/runtime/serializer/codec_test.go @@ -254,7 +254,7 @@ func TestVersionedEncoding(t *testing.T) { cf := newCodecFactory(s, newSerializersForScheme(s, testMetaFactory{})) encoder, _ := cf.SerializerForFileExtension("json") - codec := cf.CodecForVersions(encoder, nil, []unversioned.GroupVersion{{Version: "v2"}}, nil) + codec := cf.CodecForVersions(encoder, nil, unversioned.GroupVersion{Version: "v2"}, nil) out, err := runtime.Encode(codec, &TestType1{}) if err != nil { t.Fatal(err) @@ -263,19 +263,19 @@ func TestVersionedEncoding(t *testing.T) { t.Fatal(string(out)) } - codec = cf.CodecForVersions(encoder, nil, []unversioned.GroupVersion{{Version: "v3"}}, nil) + codec = cf.CodecForVersions(encoder, nil, unversioned.GroupVersion{Version: "v3"}, nil) _, err = runtime.Encode(codec, &TestType1{}) if err == nil { t.Fatal(err) } // unversioned encode with no versions is written directly to wire - codec = cf.CodecForVersions(encoder, nil, nil, nil) + codec = cf.CodecForVersions(encoder, nil, runtime.InternalGroupVersioner, nil) out, err = runtime.Encode(codec, &TestType1{}) if err != nil { t.Fatal(err) } - if string(out) != `{"myVersionKey":"__internal","myKindKey":"TestType1"}`+"\n" { + if string(out) != `{}`+"\n" { t.Fatal(string(out)) } } diff --git a/pkg/runtime/serializer/negotiated_codec.go b/pkg/runtime/serializer/negotiated_codec.go index 59b078ce8b0..d89177259ff 100644 --- a/pkg/runtime/serializer/negotiated_codec.go +++ b/pkg/runtime/serializer/negotiated_codec.go @@ -17,7 +17,6 @@ limitations under the License. package serializer import ( - "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/runtime" ) @@ -48,10 +47,10 @@ func (n *negotiatedSerializerWrapper) StreamingSerializerForMediaType(mediaType return n.streamInfo, true } -func (n *negotiatedSerializerWrapper) EncoderForVersion(e runtime.Encoder, _ unversioned.GroupVersion) runtime.Encoder { +func (n *negotiatedSerializerWrapper) EncoderForVersion(e runtime.Encoder, _ runtime.GroupVersioner) runtime.Encoder { return e } -func (n *negotiatedSerializerWrapper) DecoderToVersion(d runtime.Decoder, _gv unversioned.GroupVersion) runtime.Decoder { +func (n *negotiatedSerializerWrapper) DecoderToVersion(d runtime.Decoder, _gv runtime.GroupVersioner) runtime.Decoder { return d } diff --git a/pkg/runtime/serializer/versioning/versioning.go b/pkg/runtime/serializer/versioning/versioning.go index 6e67964b163..4d8e69ecf4a 100644 --- a/pkg/runtime/serializer/versioning/versioning.go +++ b/pkg/runtime/serializer/versioning/versioning.go @@ -17,59 +17,20 @@ limitations under the License. package versioning import ( - "fmt" "io" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/runtime" ) -// EnableCrossGroupDecoding modifies the given decoder in place, if it is a codec -// from this package. It allows objects from one group to be auto-decoded into -// another group. 'destGroup' must already exist in the codec. -// TODO: this is an encapsulation violation and should be refactored -func EnableCrossGroupDecoding(d runtime.Decoder, sourceGroup, destGroup string) error { - internal, ok := d.(*codec) - if !ok { - return fmt.Errorf("unsupported decoder type") - } - - dest, ok := internal.decodeVersion[destGroup] - if !ok { - return fmt.Errorf("group %q is not a possible destination group in the given codec", destGroup) - } - internal.decodeVersion[sourceGroup] = dest - - return nil -} - -// EnableCrossGroupEncoding modifies the given encoder in place, if it is a codec -// from this package. It allows objects from one group to be auto-decoded into -// another group. 'destGroup' must already exist in the codec. -// TODO: this is an encapsulation violation and should be refactored -func EnableCrossGroupEncoding(e runtime.Encoder, sourceGroup, destGroup string) error { - internal, ok := e.(*codec) - if !ok { - return fmt.Errorf("unsupported encoder type") - } - - dest, ok := internal.encodeVersion[destGroup] - if !ok { - return fmt.Errorf("group %q is not a possible destination group in the given codec", destGroup) - } - internal.encodeVersion[sourceGroup] = dest - - return nil -} - // NewCodecForScheme is a convenience method for callers that are using a scheme. func NewCodecForScheme( // TODO: I should be a scheme interface? scheme *runtime.Scheme, encoder runtime.Encoder, decoder runtime.Decoder, - encodeVersion []unversioned.GroupVersion, - decodeVersion []unversioned.GroupVersion, + encodeVersion runtime.GroupVersioner, + decodeVersion runtime.GroupVersioner, ) runtime.Codec { return NewCodec(encoder, decoder, runtime.UnsafeObjectConvertor(scheme), scheme, scheme, scheme, encodeVersion, decodeVersion) } @@ -84,8 +45,8 @@ func NewCodec( creater runtime.ObjectCreater, copier runtime.ObjectCopier, typer runtime.ObjectTyper, - encodeVersion []unversioned.GroupVersion, - decodeVersion []unversioned.GroupVersion, + encodeVersion runtime.GroupVersioner, + decodeVersion runtime.GroupVersioner, ) runtime.Codec { internal := &codec{ encoder: encoder, @@ -94,33 +55,10 @@ func NewCodec( creater: creater, copier: copier, typer: typer, - } - if encodeVersion != nil { - internal.encodeVersion = make(map[string]unversioned.GroupVersion) - for _, v := range encodeVersion { - // first one for a group wins. This is consistent with best to worst order throughout the codebase - if _, ok := internal.encodeVersion[v.Group]; ok { - continue - } - internal.encodeVersion[v.Group] = v - } - if len(internal.encodeVersion) == 1 { - for _, v := range internal.encodeVersion { - internal.preferredEncodeVersion = []unversioned.GroupVersion{v} - } - } - } - if decodeVersion != nil { - internal.decodeVersion = make(map[string]unversioned.GroupVersion) - for _, v := range decodeVersion { - // first one for a group wins. This is consistent with best to worst order throughout the codebase - if _, ok := internal.decodeVersion[v.Group]; ok { - continue - } - internal.decodeVersion[v.Group] = v - } - } + encodeVersion: encodeVersion, + decodeVersion: decodeVersion, + } return internal } @@ -132,10 +70,8 @@ type codec struct { copier runtime.ObjectCopier typer runtime.ObjectTyper - encodeVersion map[string]unversioned.GroupVersion - decodeVersion map[string]unversioned.GroupVersion - - preferredEncodeVersion []unversioned.GroupVersion + encodeVersion runtime.GroupVersioner + decodeVersion runtime.GroupVersioner } // Decode attempts a decode of the object, then tries to convert it to the internal version. If into is provided and the decoding is @@ -170,37 +106,7 @@ func (c *codec) Decode(data []byte, defaultGVK *unversioned.GroupVersionKind, in return into, gvk, nil } - // invoke a version conversion - group := gvk.Group - if defaultGVK != nil { - group = defaultGVK.Group - } - var targetGV unversioned.GroupVersion - if c.decodeVersion == nil { - // convert to internal by default - targetGV.Group = group - targetGV.Version = runtime.APIVersionInternal - } else { - gv, ok := c.decodeVersion[group] - if !ok { - // unknown objects are left in their original version - if isVersioned { - versioned.Objects = []runtime.Object{obj} - return versioned, gvk, nil - } - return obj, gvk, nil - } - targetGV = gv - } - - if gvk.GroupVersion() == targetGV { - if isVersioned { - versioned.Objects = []runtime.Object{obj} - return versioned, gvk, nil - } - return obj, gvk, nil - } - + // Convert if needed. if isVersioned { // create a copy, because ConvertToVersion does not guarantee non-mutation of objects copied, err := c.copier.Copy(obj) @@ -209,14 +115,14 @@ func (c *codec) Decode(data []byte, defaultGVK *unversioned.GroupVersionKind, in } versioned.Objects = []runtime.Object{copied} } - - // Convert if needed. - out, err := c.convertor.ConvertToVersion(obj, targetGV) + out, err := c.convertor.ConvertToVersion(obj, c.decodeVersion) if err != nil { return nil, gvk, err } if isVersioned { - versioned.Objects = append(versioned.Objects, out) + if versioned.Last() != out { + versioned.Objects = append(versioned.Objects, out) + } return versioned, gvk, nil } return out, gvk, nil @@ -225,50 +131,47 @@ func (c *codec) Decode(data []byte, defaultGVK *unversioned.GroupVersionKind, in // Encode ensures the provided object is output in the appropriate group and version, invoking // conversion if necessary. Unversioned objects (according to the ObjectTyper) are output as is. func (c *codec) Encode(obj runtime.Object, w io.Writer) error { - if _, ok := obj.(*runtime.Unknown); ok { + switch t := obj.(type) { + case *runtime.Unknown: + if gv, ok := runtime.PreferredGroupVersion(c.encodeVersion); ok { + t.APIVersion = gv.String() + } + return c.encoder.Encode(obj, w) + case *runtime.Unstructured: + if gv, ok := runtime.PreferredGroupVersion(c.encodeVersion); ok { + t.SetAPIVersion(gv.String()) + } + return c.encoder.Encode(obj, w) + case *runtime.UnstructuredList: + if gv, ok := runtime.PreferredGroupVersion(c.encodeVersion); ok { + t.SetAPIVersion(gv.String()) + } return c.encoder.Encode(obj, w) } + gvks, isUnversioned, err := c.typer.ObjectKinds(obj) if err != nil { return err } - gvk := gvks[0] if c.encodeVersion == nil || isUnversioned { objectKind := obj.GetObjectKind() old := objectKind.GroupVersionKind() - objectKind.SetGroupVersionKind(gvk) + objectKind.SetGroupVersionKind(gvks[0]) err = c.encoder.Encode(obj, w) objectKind.SetGroupVersionKind(old) return err } - targetGV, ok := c.encodeVersion[gvk.Group] - - // attempt a conversion to the sole encode version - if !ok && c.preferredEncodeVersion != nil { - ok = true - targetGV = c.preferredEncodeVersion[0] - } - - // if no fallback is available, error - if !ok { - return fmt.Errorf("the codec does not recognize group %q for kind %q and cannot encode it", gvk.Group, gvk.Kind) - } - // Perform a conversion if necessary objectKind := obj.GetObjectKind() old := objectKind.GroupVersionKind() - out, err := c.convertor.ConvertToVersion(obj, targetGV) + out, err := c.convertor.ConvertToVersion(obj, c.encodeVersion) if err != nil { - if ok { - return err - } - } else { - obj = out + return err } // Conversion is responsible for setting the proper group, version, and kind onto the outgoing object - err = c.encoder.Encode(obj, w) + err = c.encoder.Encode(out, w) // restore the old GVK, in case conversion returned the same object objectKind.SetGroupVersionKind(old) return err diff --git a/pkg/runtime/serializer/versioning/versioning_test.go b/pkg/runtime/serializer/versioning/versioning_test.go index 2dfae4921af..f2600909c6c 100644 --- a/pkg/runtime/serializer/versioning/versioning_test.go +++ b/pkg/runtime/serializer/versioning/versioning_test.go @@ -53,7 +53,7 @@ func TestDecode(t *testing.T) { yaml bool pretty bool - encodes, decodes []unversioned.GroupVersion + encodes, decodes runtime.GroupVersioner defaultGVK *unversioned.GroupVersionKind into runtime.Object @@ -67,12 +67,14 @@ func TestDecode(t *testing.T) { serializer: &mockSerializer{actual: gvk1}, convertor: &checkConvertor{groupVersion: unversioned.GroupVersion{Group: "other", Version: "__internal"}}, expectedGVK: gvk1, + decodes: unversioned.GroupVersion{Group: "other", Version: "__internal"}, }, { serializer: &mockSerializer{actual: gvk1, obj: decodable1}, convertor: &checkConvertor{in: decodable1, obj: decodable2, groupVersion: unversioned.GroupVersion{Group: "other", Version: "__internal"}}, expectedGVK: gvk1, sameObject: decodable2, + decodes: unversioned.GroupVersion{Group: "other", Version: "__internal"}, }, // defaultGVK.Group is allowed to force a conversion to the destination group { @@ -81,6 +83,7 @@ func TestDecode(t *testing.T) { convertor: &checkConvertor{in: decodable1, obj: decodable2, groupVersion: unversioned.GroupVersion{Group: "force", Version: "__internal"}}, expectedGVK: gvk1, sameObject: decodable2, + decodes: unversioned.GroupVersion{Group: "force", Version: "__internal"}, }, // uses direct conversion for into when objects differ { @@ -121,6 +124,7 @@ func TestDecode(t *testing.T) { convertor: &checkConvertor{in: decodable1, obj: decodable2, groupVersion: unversioned.GroupVersion{Group: "other", Version: "__internal"}}, expectedGVK: gvk1, expectedObject: &runtime.VersionedObjects{Objects: []runtime.Object{decodable1, decodable2}}, + decodes: unversioned.GroupVersion{Group: "other", Version: "__internal"}, }, { into: &runtime.VersionedObjects{Objects: []runtime.Object{}}, @@ -130,38 +134,45 @@ func TestDecode(t *testing.T) { convertor: &checkConvertor{in: decodable1, obj: decodable2, groupVersion: unversioned.GroupVersion{Group: "other", Version: "__internal"}}, expectedGVK: gvk1, expectedObject: &runtime.VersionedObjects{Objects: []runtime.Object{decodable1, decodable2}}, + decodes: unversioned.GroupVersion{Group: "other", Version: "__internal"}, }, // decode into the same version as the serialized object { - decodes: []unversioned.GroupVersion{gvk1.GroupVersion()}, + decodes: unversioned.GroupVersions{gvk1.GroupVersion()}, serializer: &mockSerializer{actual: gvk1, obj: decodable1}, + convertor: &checkConvertor{in: decodable1, obj: decodable1, groupVersion: unversioned.GroupVersions{{Group: "other", Version: "blah"}}}, expectedGVK: gvk1, expectedObject: decodable1, }, { into: &runtime.VersionedObjects{Objects: []runtime.Object{}}, - decodes: []unversioned.GroupVersion{gvk1.GroupVersion()}, + decodes: unversioned.GroupVersions{gvk1.GroupVersion()}, serializer: &mockSerializer{actual: gvk1, obj: decodable1}, + convertor: &checkConvertor{in: decodable1, obj: decodable1, groupVersion: unversioned.GroupVersions{{Group: "other", Version: "blah"}}}, + copier: &checkCopy{in: decodable1, obj: decodable1, err: nil}, expectedGVK: gvk1, expectedObject: &runtime.VersionedObjects{Objects: []runtime.Object{decodable1}}, }, // codec with non matching version skips conversion altogether { - decodes: []unversioned.GroupVersion{{Group: "something", Version: "else"}}, + decodes: unversioned.GroupVersions{{Group: "something", Version: "else"}}, serializer: &mockSerializer{actual: gvk1, obj: decodable1}, + convertor: &checkConvertor{in: decodable1, obj: decodable1, groupVersion: unversioned.GroupVersions{{Group: "something", Version: "else"}}}, expectedGVK: gvk1, expectedObject: decodable1, }, { into: &runtime.VersionedObjects{Objects: []runtime.Object{}}, - decodes: []unversioned.GroupVersion{{Group: "something", Version: "else"}}, + decodes: unversioned.GroupVersions{{Group: "something", Version: "else"}}, serializer: &mockSerializer{actual: gvk1, obj: decodable1}, + convertor: &checkConvertor{in: decodable1, obj: decodable1, groupVersion: unversioned.GroupVersions{{Group: "something", Version: "else"}}}, + copier: &checkCopy{in: decodable1, obj: decodable1, err: nil}, expectedGVK: gvk1, expectedObject: &runtime.VersionedObjects{Objects: []runtime.Object{decodable1}}, }, @@ -228,7 +239,7 @@ func (c *checkCopy) Copy(obj runtime.Object) (runtime.Object, error) { type checkConvertor struct { err error in, obj runtime.Object - groupVersion unversioned.GroupVersion + groupVersion runtime.GroupVersioner directConvert bool } @@ -244,15 +255,15 @@ func (c *checkConvertor) Convert(in, out interface{}) error { } return c.err } -func (c *checkConvertor) ConvertToVersion(in runtime.Object, outVersion unversioned.GroupVersion) (out runtime.Object, err error) { +func (c *checkConvertor) ConvertToVersion(in runtime.Object, outVersion runtime.GroupVersioner) (out runtime.Object, err error) { if c.directConvert { return nil, fmt.Errorf("unexpected call to ConvertToVersion") } if c.in != nil && c.in != in { return nil, fmt.Errorf("unexpected in: %s", in) } - if c.groupVersion != outVersion { - return nil, fmt.Errorf("unexpected outversion: %s", outVersion) + if !reflect.DeepEqual(c.groupVersion, outVersion) { + return nil, fmt.Errorf("unexpected outversion: %s (%s)", outVersion, c.groupVersion) } return c.obj, c.err } diff --git a/pkg/runtime/unstructured.go b/pkg/runtime/unstructured.go index 153dccda6c8..0e3488792d2 100644 --- a/pkg/runtime/unstructured.go +++ b/pkg/runtime/unstructured.go @@ -187,9 +187,14 @@ func (UnstructuredObjectConverter) Convert(in, out interface{}) error { return nil } -func (UnstructuredObjectConverter) ConvertToVersion(in Object, outVersion unversioned.GroupVersion) (Object, error) { - if gvk := in.GetObjectKind().GroupVersionKind(); gvk.GroupVersion() != outVersion { - return nil, errors.New("unstructured converter cannot convert versions") +func (UnstructuredObjectConverter) ConvertToVersion(in Object, target GroupVersioner) (Object, error) { + if kind := in.GetObjectKind().GroupVersionKind(); !kind.IsEmpty() { + gvk, ok := target.KindForGroupVersionKinds([]unversioned.GroupVersionKind{kind}) + if !ok { + // TODO: should this be a typed error? + return nil, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", kind, target) + } + in.GetObjectKind().SetGroupVersionKind(gvk) } return in, nil } diff --git a/pkg/runtime/unversioned_test.go b/pkg/runtime/unversioned_test.go index 5eb525dc944..7ffedfdb5a2 100644 --- a/pkg/runtime/unversioned_test.go +++ b/pkg/runtime/unversioned_test.go @@ -30,14 +30,13 @@ import ( "k8s.io/kubernetes/pkg/runtime" ) -var status = &unversioned.Status{ - Status: unversioned.StatusFailure, - Code: 200, - Reason: unversioned.StatusReasonUnknown, - Message: "", -} - func TestV1EncodeDecodeStatus(t *testing.T) { + status := &unversioned.Status{ + Status: unversioned.StatusFailure, + Code: 200, + Reason: unversioned.StatusReasonUnknown, + Message: "", + } v1Codec := testapi.Default.Codec() @@ -65,6 +64,12 @@ func TestV1EncodeDecodeStatus(t *testing.T) { } func TestExperimentalEncodeDecodeStatus(t *testing.T) { + status := &unversioned.Status{ + Status: unversioned.StatusFailure, + Code: 200, + Reason: unversioned.StatusReasonUnknown, + Message: "", + } // TODO: caesarxuchao: use the testapi.Extensions.Codec() once the PR that // moves experimental from v1 to v1beta1 got merged. expCodec := api.Codecs.LegacyCodec(extensions.SchemeGroupVersion) diff --git a/plugin/pkg/scheduler/api/latest/latest.go b/plugin/pkg/scheduler/api/latest/latest.go index dbea2a8af56..9eec37e2e7b 100644 --- a/plugin/pkg/scheduler/api/latest/latest.go +++ b/plugin/pkg/scheduler/api/latest/latest.go @@ -47,7 +47,7 @@ func init() { api.Scheme, jsonSerializer, jsonSerializer, - []unversioned.GroupVersion{{Version: Version}}, - []unversioned.GroupVersion{{Version: runtime.APIVersionInternal}}, + unversioned.GroupVersion{Version: Version}, + runtime.InternalGroupVersioner, ) } diff --git a/test/integration/framework/serializer.go b/test/integration/framework/serializer.go index dc5ac6ba446..3a72ca82d02 100644 --- a/test/integration/framework/serializer.go +++ b/test/integration/framework/serializer.go @@ -17,7 +17,6 @@ limitations under the License. package framework import ( - "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime/serializer/versioning" ) @@ -58,10 +57,10 @@ func (s *wrappedSerializer) UniversalDeserializer() runtime.Decoder { return s.serializer } -func (s *wrappedSerializer) EncoderForVersion(encoder runtime.Encoder, gv unversioned.GroupVersion) runtime.Encoder { - return versioning.NewCodec(encoder, nil, s.scheme, s.scheme, s.scheme, s.scheme, []unversioned.GroupVersion{gv}, nil) +func (s *wrappedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { + return versioning.NewCodec(encoder, nil, s.scheme, s.scheme, s.scheme, s.scheme, gv, nil) } -func (s *wrappedSerializer) DecoderToVersion(decoder runtime.Decoder, gv unversioned.GroupVersion) runtime.Decoder { - return versioning.NewCodec(nil, decoder, s.scheme, s.scheme, s.scheme, s.scheme, nil, []unversioned.GroupVersion{gv}) +func (s *wrappedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { + return versioning.NewCodec(nil, decoder, s.scheme, s.scheme, s.scheme, s.scheme, nil, gv) } From ce57455de6fddd77a933d11a63215d740f90adf9 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 25 May 2016 22:50:29 -0400 Subject: [PATCH 2/7] Allow objects to serialize their nested objects Introduce an optional interface for callers to encode themselves. --- pkg/runtime/interfaces.go | 12 ++++ pkg/runtime/serializer/codec_factory.go | 44 ++---------- .../serializer/versioning/versioning.go | 71 +++++++++++++++---- .../serializer/versioning/versioning_test.go | 68 ++++++++++++++++-- 4 files changed, 137 insertions(+), 58 deletions(-) diff --git a/pkg/runtime/interfaces.go b/pkg/runtime/interfaces.go index 4341f89b3fa..117f43a4a50 100644 --- a/pkg/runtime/interfaces.go +++ b/pkg/runtime/interfaces.go @@ -154,6 +154,18 @@ type StorageSerializer interface { DecoderToVersion(serializer Decoder, gv GroupVersioner) Decoder } +// NestedObjectEncoder is an optional interface that objects may implement to be given +// an opportunity to encode any nested Objects / RawExtensions during serialization. +type NestedObjectEncoder interface { + EncodeNestedObjects(e Encoder) error +} + +// NestedObjectDecoder is an optional interface that objects may implement to be given +// an opportunity to decode any nested Objects / RawExtensions during serialization. +type NestedObjectDecoder interface { + DecodeNestedObjects(d Decoder) error +} + /////////////////////////////////////////////////////////////////////////////// // Non-codec interfaces diff --git a/pkg/runtime/serializer/codec_factory.go b/pkg/runtime/serializer/codec_factory.go index ed5e8c471c3..afccae5a6ea 100644 --- a/pkg/runtime/serializer/codec_factory.go +++ b/pkg/runtime/serializer/codec_factory.go @@ -17,8 +17,6 @@ limitations under the License. package serializer import ( - "io" - "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime/serializer/json" @@ -336,47 +334,15 @@ type DirectCodecFactory struct { // EncoderForVersion returns an encoder that does not do conversion. gv is ignored. func (f DirectCodecFactory) EncoderForVersion(serializer runtime.Encoder, _ runtime.GroupVersioner) runtime.Encoder { - return DirectCodec{ - runtime.NewCodec(serializer, nil), - f.CodecFactory.scheme, + return versioning.DirectEncoder{ + Encoder: serializer, + ObjectTyper: f.CodecFactory.scheme, } } // DecoderToVersion returns an decoder that does not do conversion. gv is ignored. func (f DirectCodecFactory) DecoderToVersion(serializer runtime.Decoder, _ runtime.GroupVersioner) runtime.Decoder { - return DirectCodec{ - runtime.NewCodec(nil, serializer), - nil, + return versioning.DirectDecoder{ + Decoder: serializer, } } - -// DirectCodec is a codec that does not do conversion. It sets the gvk during serialization, and removes the gvk during deserialization. -type DirectCodec struct { - runtime.Serializer - runtime.ObjectTyper -} - -// EncodeToStream does not do conversion. It sets the gvk during serialization. overrides are ignored. -func (c DirectCodec) Encode(obj runtime.Object, stream io.Writer) error { - gvks, _, err := c.ObjectTyper.ObjectKinds(obj) - if err != nil { - return err - } - kind := obj.GetObjectKind() - oldGVK := kind.GroupVersionKind() - kind.SetGroupVersionKind(gvks[0]) - err = c.Serializer.Encode(obj, stream) - kind.SetGroupVersionKind(oldGVK) - return err -} - -// Decode does not do conversion. It removes the gvk during deserialization. -func (c DirectCodec) Decode(data []byte, defaults *unversioned.GroupVersionKind, into runtime.Object) (runtime.Object, *unversioned.GroupVersionKind, error) { - obj, gvk, err := c.Serializer.Decode(data, defaults, into) - if obj != nil { - kind := obj.GetObjectKind() - // clearing the gvk is just a convention of a codec - kind.SetGroupVersionKind(unversioned.GroupVersionKind{}) - } - return obj, gvk, err -} diff --git a/pkg/runtime/serializer/versioning/versioning.go b/pkg/runtime/serializer/versioning/versioning.go index 4d8e69ecf4a..4bd25e83156 100644 --- a/pkg/runtime/serializer/versioning/versioning.go +++ b/pkg/runtime/serializer/versioning/versioning.go @@ -88,6 +88,12 @@ func (c *codec) Decode(data []byte, defaultGVK *unversioned.GroupVersionKind, in return nil, gvk, err } + if d, ok := obj.(runtime.NestedObjectDecoder); ok { + if err := d.DecodeNestedObjects(DirectDecoder{c.decoder}); err != nil { + return nil, gvk, err + } + } + // if we specify a target, use generic conversion. if into != nil { if into == obj { @@ -131,21 +137,8 @@ func (c *codec) Decode(data []byte, defaultGVK *unversioned.GroupVersionKind, in // Encode ensures the provided object is output in the appropriate group and version, invoking // conversion if necessary. Unversioned objects (according to the ObjectTyper) are output as is. func (c *codec) Encode(obj runtime.Object, w io.Writer) error { - switch t := obj.(type) { - case *runtime.Unknown: - if gv, ok := runtime.PreferredGroupVersion(c.encodeVersion); ok { - t.APIVersion = gv.String() - } - return c.encoder.Encode(obj, w) - case *runtime.Unstructured: - if gv, ok := runtime.PreferredGroupVersion(c.encodeVersion); ok { - t.SetAPIVersion(gv.String()) - } - return c.encoder.Encode(obj, w) - case *runtime.UnstructuredList: - if gv, ok := runtime.PreferredGroupVersion(c.encodeVersion); ok { - t.SetAPIVersion(gv.String()) - } + switch obj.(type) { + case *runtime.Unknown, *runtime.Unstructured, *runtime.UnstructuredList: return c.encoder.Encode(obj, w) } @@ -155,6 +148,11 @@ func (c *codec) Encode(obj runtime.Object, w io.Writer) error { } if c.encodeVersion == nil || isUnversioned { + if e, ok := obj.(runtime.NestedObjectEncoder); ok { + if err := e.EncodeNestedObjects(DirectEncoder{Encoder: c.encoder, ObjectTyper: c.typer}); err != nil { + return err + } + } objectKind := obj.GetObjectKind() old := objectKind.GroupVersionKind() objectKind.SetGroupVersionKind(gvks[0]) @@ -170,9 +168,52 @@ func (c *codec) Encode(obj runtime.Object, w io.Writer) error { if err != nil { return err } + + if e, ok := out.(runtime.NestedObjectEncoder); ok { + if err := e.EncodeNestedObjects(DirectEncoder{Encoder: c.encoder, ObjectTyper: c.typer}); err != nil { + return err + } + } + // Conversion is responsible for setting the proper group, version, and kind onto the outgoing object err = c.encoder.Encode(out, w) // restore the old GVK, in case conversion returned the same object objectKind.SetGroupVersionKind(old) return err } + +// DirectEncoder serializes an object and ensures the GVK is set. +type DirectEncoder struct { + runtime.Encoder + runtime.ObjectTyper +} + +// Encode does not do conversion. It sets the gvk during serialization. +func (e DirectEncoder) Encode(obj runtime.Object, stream io.Writer) error { + gvks, _, err := e.ObjectTyper.ObjectKinds(obj) + if err != nil { + return err + } + kind := obj.GetObjectKind() + oldGVK := kind.GroupVersionKind() + kind.SetGroupVersionKind(gvks[0]) + err = e.Encoder.Encode(obj, stream) + kind.SetGroupVersionKind(oldGVK) + return err +} + +// DirectDecoder clears the group version kind of a deserialized object. +type DirectDecoder struct { + runtime.Decoder +} + +// Decode does not do conversion. It removes the gvk during deserialization. +func (d DirectDecoder) Decode(data []byte, defaults *unversioned.GroupVersionKind, into runtime.Object) (runtime.Object, *unversioned.GroupVersionKind, error) { + obj, gvk, err := d.Decoder.Decode(data, defaults, into) + if obj != nil { + kind := obj.GetObjectKind() + // clearing the gvk is just a convention of a codec + kind.SetGroupVersionKind(unversioned.GroupVersionKind{}) + } + return obj, gvk, err +} diff --git a/pkg/runtime/serializer/versioning/versioning_test.go b/pkg/runtime/serializer/versioning/versioning_test.go index f2600909c6c..0ae74b2c9ae 100644 --- a/pkg/runtime/serializer/versioning/versioning_test.go +++ b/pkg/runtime/serializer/versioning/versioning_test.go @@ -19,6 +19,7 @@ package versioning import ( "fmt" "io" + "io/ioutil" "reflect" "testing" @@ -37,6 +38,60 @@ func (d *testDecodable) GetObjectKind() unversioned.ObjectKind { func (d *testDecodable) SetGroupVersionKind(gvk unversioned.GroupVersionKind) { d.gvk = gvk } func (d *testDecodable) GroupVersionKind() unversioned.GroupVersionKind { return d.gvk } +type testNestedDecodable struct { + Other string + Value int `json:"value"` + + gvk unversioned.GroupVersionKind + nestedCalled bool + nestedErr error +} + +func (d *testNestedDecodable) GetObjectKind() unversioned.ObjectKind { return d } +func (d *testNestedDecodable) SetGroupVersionKind(gvk unversioned.GroupVersionKind) { d.gvk = gvk } +func (d *testNestedDecodable) GroupVersionKind() unversioned.GroupVersionKind { return d.gvk } + +func (d *testNestedDecodable) EncodeNestedObjects(e runtime.Encoder) error { + d.nestedCalled = true + return d.nestedErr +} + +func (d *testNestedDecodable) DecodeNestedObjects(_ runtime.Decoder) error { + d.nestedCalled = true + return d.nestedErr +} + +func TestNestedDecode(t *testing.T) { + n := &testNestedDecodable{nestedErr: fmt.Errorf("unable to decode")} + decoder := &mockSerializer{obj: n} + codec := NewCodec(nil, decoder, nil, nil, nil, nil, nil, nil) + if _, _, err := codec.Decode([]byte(`{}`), nil, n); err != n.nestedErr { + t.Errorf("unexpected error: %v", err) + } + if !n.nestedCalled { + t.Errorf("did not invoke nested decoder") + } +} + +func TestNestedEncode(t *testing.T) { + n := &testNestedDecodable{nestedErr: fmt.Errorf("unable to decode")} + n2 := &testNestedDecodable{nestedErr: fmt.Errorf("unable to decode 2")} + encoder := &mockSerializer{obj: n} + codec := NewCodec( + encoder, nil, + &checkConvertor{obj: n2, groupVersion: unversioned.GroupVersion{Group: "other"}}, + nil, nil, + &mockTyper{gvks: []unversioned.GroupVersionKind{{Kind: "test"}}}, + unversioned.GroupVersion{Group: "other"}, nil, + ) + if err := codec.Encode(n, ioutil.Discard); err != n2.nestedErr { + t.Errorf("unexpected error: %v", err) + } + if n.nestedCalled || !n2.nestedCalled { + t.Errorf("did not invoke correct nested decoder") + } +} + func TestDecode(t *testing.T) { gvk1 := &unversioned.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"} decodable1 := &testDecodable{} @@ -300,10 +355,15 @@ func (c *mockCreater) New(kind unversioned.GroupVersionKind) (runtime.Object, er } type mockTyper struct { - gvk *unversioned.GroupVersionKind - err error + gvks []unversioned.GroupVersionKind + unversioned bool + err error } -func (t *mockTyper) ObjectKind(obj runtime.Object) (*unversioned.GroupVersionKind, bool, error) { - return t.gvk, false, t.err +func (t *mockTyper) ObjectKinds(obj runtime.Object) ([]unversioned.GroupVersionKind, bool, error) { + return t.gvks, t.unversioned, t.err +} + +func (t *mockTyper) Recognizes(_ unversioned.GroupVersionKind) bool { + return true } From c2333f673fcfc3424bac283810d0d63148e9fa74 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 29 Jun 2016 00:51:08 -0700 Subject: [PATCH 3/7] Some internal types are not registered to all output versions Specifically JobTemplate in batch is not in extensions/v1beta1 --- pkg/api/serialization_test.go | 6 +++++- pkg/apis/extensions/register.go | 1 + pkg/apis/policy/register.go | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index ec2894a8d03..f3af252aec5 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -92,7 +92,11 @@ func roundTrip(t *testing.T, codec runtime.Codec, item runtime.Object) { name := reflect.TypeOf(item).Elem().Name() data, err := runtime.Encode(codec, item) if err != nil { - t.Errorf("%v: %v (%s)", name, err, printer.Sprintf("%#v", item)) + if runtime.IsNotRegisteredError(err) { + t.Logf("%v: not registered: %v (%s)", name, err, printer.Sprintf("%#v", item)) + } else { + t.Errorf("%v: %v (%s)", name, err, printer.Sprintf("%#v", item)) + } return } diff --git a/pkg/apis/extensions/register.go b/pkg/apis/extensions/register.go index f12335b97c2..d4168eefae8 100644 --- a/pkg/apis/extensions/register.go +++ b/pkg/apis/extensions/register.go @@ -68,6 +68,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &Ingress{}, &IngressList{}, &api.ListOptions{}, + &api.DeleteOptions{}, &ReplicaSet{}, &ReplicaSetList{}, &api.ExportOptions{}, diff --git a/pkg/apis/policy/register.go b/pkg/apis/policy/register.go index 0b219dff702..d9c26d2e82e 100644 --- a/pkg/apis/policy/register.go +++ b/pkg/apis/policy/register.go @@ -17,6 +17,7 @@ limitations under the License. package policy import ( + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/runtime" ) @@ -48,6 +49,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &PodDisruptionBudget{}, &PodDisruptionBudgetList{}, + &api.ListOptions{}, ) return nil } From a7a7fd463129afeb5e39df9c1318eed8703301b7 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sun, 3 Jul 2016 19:13:43 -0400 Subject: [PATCH 4/7] Patch was not requesting a version to encode to --- pkg/kubectl/cmd/patch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kubectl/cmd/patch.go b/pkg/kubectl/cmd/patch.go index ec3aba2aad4..0e830908bc5 100644 --- a/pkg/kubectl/cmd/patch.go +++ b/pkg/kubectl/cmd/patch.go @@ -197,7 +197,7 @@ func RunPatch(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri if err != nil { return err } - originalObjJS, err := runtime.Encode(api.Codecs.LegacyCodec(), info.VersionedObject.(runtime.Object)) + originalObjJS, err := runtime.Encode(api.Codecs.LegacyCodec(mapping.GroupVersionKind.GroupVersion()), info.VersionedObject.(runtime.Object)) if err != nil { return err } From 5f8366aac3094ee8dc5b560829130ecb1eb6b04d Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 4 Jul 2016 16:13:27 -0400 Subject: [PATCH 5/7] Convert() should accept the new conversion Context value Allows Convert() to reuse the same conversions as ConvertToVersion without being overly coupled to the version. --- cmd/kube-proxy/app/options/options.go | 2 +- cmd/kubelet/app/options/options.go | 2 +- pkg/api/meta/restmapper_test.go | 2 +- pkg/api/serialization_proto_test.go | 2 +- pkg/api/serialization_test.go | 2 +- pkg/api/v1/conversion_test.go | 6 +++--- pkg/api/v1/defaults_test.go | 2 +- pkg/apis/abac/v0/conversion_test.go | 2 +- pkg/apis/extensions/v1beta1/conversion_test.go | 4 ++-- pkg/apis/extensions/v1beta1/defaults_test.go | 2 +- pkg/auth/authorizer/abac/abac.go | 2 +- pkg/auth/authorizer/abac/abac_test.go | 4 ++-- pkg/controller/controller_utils.go | 2 +- pkg/controller/scheduledjob/utils.go | 2 +- pkg/kubelet/config/file_test.go | 2 +- pkg/kubelet/config/http_test.go | 2 +- pkg/registry/thirdpartyresourcedata/codec.go | 4 ++-- pkg/runtime/codec.go | 6 +++--- pkg/runtime/conversion_test.go | 2 +- pkg/runtime/interfaces.go | 8 +++++--- pkg/runtime/scheme_test.go | 4 ++-- pkg/runtime/serializer/versioning/versioning.go | 2 +- pkg/runtime/serializer/versioning/versioning_test.go | 2 +- pkg/runtime/unstructured.go | 2 +- plugin/cmd/kube-scheduler/app/options/options.go | 2 +- 25 files changed, 37 insertions(+), 35 deletions(-) diff --git a/cmd/kube-proxy/app/options/options.go b/cmd/kube-proxy/app/options/options.go index 0f492cd9f67..27382dd0054 100644 --- a/cmd/kube-proxy/app/options/options.go +++ b/cmd/kube-proxy/app/options/options.go @@ -50,7 +50,7 @@ type ProxyServerConfig struct { func NewProxyConfig() *ProxyServerConfig { config := componentconfig.KubeProxyConfiguration{} - api.Scheme.Convert(&v1alpha1.KubeProxyConfiguration{}, &config) + api.Scheme.Convert(&v1alpha1.KubeProxyConfiguration{}, &config, nil) return &ProxyServerConfig{ KubeProxyConfiguration: config, ContentType: "application/vnd.kubernetes.protobuf", diff --git a/cmd/kubelet/app/options/options.go b/cmd/kubelet/app/options/options.go index 45aad5afa6e..f175c177745 100644 --- a/cmd/kubelet/app/options/options.go +++ b/cmd/kubelet/app/options/options.go @@ -58,7 +58,7 @@ type KubeletServer struct { // NewKubeletServer will create a new KubeletServer with default values. func NewKubeletServer() *KubeletServer { config := componentconfig.KubeletConfiguration{} - api.Scheme.Convert(&v1alpha1.KubeletConfiguration{}, &config) + api.Scheme.Convert(&v1alpha1.KubeletConfiguration{}, &config, nil) return &KubeletServer{ AuthPath: util.NewStringFlag("/var/lib/kubelet/kubernetes_auth"), // deprecated KubeConfig: util.NewStringFlag("/var/lib/kubelet/kubeconfig"), diff --git a/pkg/api/meta/restmapper_test.go b/pkg/api/meta/restmapper_test.go index c4900d4aa44..11d7c11be0a 100644 --- a/pkg/api/meta/restmapper_test.go +++ b/pkg/api/meta/restmapper_test.go @@ -28,7 +28,7 @@ import ( type fakeConvertor struct{} -func (fakeConvertor) Convert(in, out interface{}) error { +func (fakeConvertor) Convert(in, out, context interface{}) error { return nil } diff --git a/pkg/api/serialization_proto_test.go b/pkg/api/serialization_proto_test.go index b899a71314b..5a75071e0dd 100644 --- a/pkg/api/serialization_proto_test.go +++ b/pkg/api/serialization_proto_test.go @@ -106,7 +106,7 @@ func BenchmarkEncodeCodecFromInternalProtobuf(b *testing.B) { width := len(items) encodable := make([]api.Pod, width) for i := range items { - if err := api.Scheme.Convert(&items[i], &encodable[i]); err != nil { + if err := api.Scheme.Convert(&items[i], &encodable[i], nil); err != nil { b.Fatal(err) } } diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index f3af252aec5..dcaf1880f3f 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -482,7 +482,7 @@ func BenchmarkEncodeCodecFromInternal(b *testing.B) { width := len(items) encodable := make([]api.Pod, width) for i := range items { - if err := api.Scheme.Convert(&items[i], &encodable[i]); err != nil { + if err := api.Scheme.Convert(&items[i], &encodable[i], nil); err != nil { b.Fatal(err) } } diff --git a/pkg/api/v1/conversion_test.go b/pkg/api/v1/conversion_test.go index 27c215f3cfe..1384de341f7 100644 --- a/pkg/api/v1/conversion_test.go +++ b/pkg/api/v1/conversion_test.go @@ -127,7 +127,7 @@ func TestPodSpecConversion(t *testing.T) { ServiceAccountName: name, } v := versioned.PodSpec{} - if err := api.Scheme.Convert(i, &v); err != nil { + if err := api.Scheme.Convert(i, &v, nil); err != nil { t.Fatalf("unexpected error: %v", err) } if v.ServiceAccountName != name { @@ -152,7 +152,7 @@ func TestPodSpecConversion(t *testing.T) { } for k, v := range testCases { got := api.PodSpec{} - err := api.Scheme.Convert(v, &got) + err := api.Scheme.Convert(v, &got, nil) if err != nil { t.Fatalf("unexpected error for case %d: %v", k, err) } @@ -206,7 +206,7 @@ func TestResourceListConversion(t *testing.T) { for i, test := range tests { output := api.ResourceList{} - err := api.Scheme.Convert(&test.input, &output) + err := api.Scheme.Convert(&test.input, &output, nil) if err != nil { t.Fatalf("unexpected error for case %d: %v", i, err) } diff --git a/pkg/api/v1/defaults_test.go b/pkg/api/v1/defaults_test.go index 6987d0f301c..e7c802f6166 100644 --- a/pkg/api/v1/defaults_test.go +++ b/pkg/api/v1/defaults_test.go @@ -40,7 +40,7 @@ func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { return nil } obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object) - err = api.Scheme.Convert(obj2, obj3) + err = api.Scheme.Convert(obj2, obj3, nil) if err != nil { t.Errorf("%v\nSource: %#v", err, obj2) return nil diff --git a/pkg/apis/abac/v0/conversion_test.go b/pkg/apis/abac/v0/conversion_test.go index 827e8ce3681..30a433b5e35 100644 --- a/pkg/apis/abac/v0/conversion_test.go +++ b/pkg/apis/abac/v0/conversion_test.go @@ -67,7 +67,7 @@ func TestConversion(t *testing.T) { } for k, tc := range testcases { internal := &api.Policy{} - if err := api.Scheme.Convert(tc.old, internal); err != nil { + if err := api.Scheme.Convert(tc.old, internal, nil); err != nil { t.Errorf("%s: unexpected error: %v", k, err) } if !reflect.DeepEqual(internal, tc.expected) { diff --git a/pkg/apis/extensions/v1beta1/conversion_test.go b/pkg/apis/extensions/v1beta1/conversion_test.go index 5fab6b2f463..82fd46009e6 100644 --- a/pkg/apis/extensions/v1beta1/conversion_test.go +++ b/pkg/apis/extensions/v1beta1/conversion_test.go @@ -59,7 +59,7 @@ func TestJobSpecConversion(t *testing.T) { ManualSelector: test.in, } v := versioned.JobSpec{} - if err := api.Scheme.Convert(i, &v); err != nil { + if err := api.Scheme.Convert(i, &v, nil); err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(test.expectOut, v.AutoSelector) { @@ -73,7 +73,7 @@ func TestJobSpecConversion(t *testing.T) { AutoSelector: test.in, } e := batch.JobSpec{} - if err := api.Scheme.Convert(i, &e); err != nil { + if err := api.Scheme.Convert(i, &e, nil); err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(test.expectOut, e.ManualSelector) { diff --git a/pkg/apis/extensions/v1beta1/defaults_test.go b/pkg/apis/extensions/v1beta1/defaults_test.go index 092cbe4b2e4..4a90f178215 100644 --- a/pkg/apis/extensions/v1beta1/defaults_test.go +++ b/pkg/apis/extensions/v1beta1/defaults_test.go @@ -728,7 +728,7 @@ func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { return nil } obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object) - err = api.Scheme.Convert(obj2, obj3) + err = api.Scheme.Convert(obj2, obj3, nil) if err != nil { t.Errorf("%v\nSource: %#v", err, obj2) return nil diff --git a/pkg/auth/authorizer/abac/abac.go b/pkg/auth/authorizer/abac/abac.go index 75db334e819..ef4b74d694a 100644 --- a/pkg/auth/authorizer/abac/abac.go +++ b/pkg/auth/authorizer/abac/abac.go @@ -89,7 +89,7 @@ func NewFromFile(path string) (policyList, error) { if err := runtime.DecodeInto(decoder, b, oldPolicy); err != nil { return nil, policyLoadError{path, i, b, err} } - if err := api.Scheme.Convert(oldPolicy, p); err != nil { + if err := api.Scheme.Convert(oldPolicy, p, nil); err != nil { return nil, policyLoadError{path, i, b, err} } pl = append(pl, p) diff --git a/pkg/auth/authorizer/abac/abac_test.go b/pkg/auth/authorizer/abac/abac_test.go index 0070fbdea32..05d09aa51c8 100644 --- a/pkg/auth/authorizer/abac/abac_test.go +++ b/pkg/auth/authorizer/abac/abac_test.go @@ -562,7 +562,7 @@ func TestSubjectMatches(t *testing.T) { for k, tc := range testCases { policy := &api.Policy{} - if err := api.Scheme.Convert(tc.Policy, policy); err != nil { + if err := api.Scheme.Convert(tc.Policy, policy, nil); err != nil { t.Errorf("%s: error converting: %v", k, err) continue } @@ -950,7 +950,7 @@ func TestPolicy(t *testing.T) { } for _, test := range tests { policy := &api.Policy{} - if err := api.Scheme.Convert(test.policy, policy); err != nil { + if err := api.Scheme.Convert(test.policy, policy, nil); err != nil { t.Errorf("%s: error converting: %v", test.name, err) continue } diff --git a/pkg/controller/controller_utils.go b/pkg/controller/controller_utils.go index 81930422eba..0a851c23f81 100644 --- a/pkg/controller/controller_utils.go +++ b/pkg/controller/controller_utils.go @@ -458,7 +458,7 @@ func GetPodFromTemplate(template *api.PodTemplateSpec, parentObject runtime.Obje if controllerRef != nil { pod.OwnerReferences = append(pod.OwnerReferences, *controllerRef) } - if err := api.Scheme.Convert(&template.Spec, &pod.Spec); err != nil { + if err := api.Scheme.Convert(&template.Spec, &pod.Spec, nil); err != nil { return nil, fmt.Errorf("unable to convert pod template: %v", err) } return pod, nil diff --git a/pkg/controller/scheduledjob/utils.go b/pkg/controller/scheduledjob/utils.go index a5d716baf0e..f65b2a2d136 100644 --- a/pkg/controller/scheduledjob/utils.go +++ b/pkg/controller/scheduledjob/utils.go @@ -209,7 +209,7 @@ func getJobFromTemplate(sj *batch.ScheduledJob, scheduledTime time.Time) (*batch Name: name, }, } - if err := api.Scheme.Convert(&sj.Spec.JobTemplate.Spec, &job.Spec); err != nil { + if err := api.Scheme.Convert(&sj.Spec.JobTemplate.Spec, &job.Spec, nil); err != nil { return nil, fmt.Errorf("unable to convert job template: %v", err) } return job, nil diff --git a/pkg/kubelet/config/file_test.go b/pkg/kubelet/config/file_test.go index ce79d88f4cf..f6bb67e81bb 100644 --- a/pkg/kubelet/config/file_test.go +++ b/pkg/kubelet/config/file_test.go @@ -129,7 +129,7 @@ func TestReadPodsFromFile(t *testing.T) { for _, testCase := range testCases { func() { var versionedPod runtime.Object - err := testapi.Default.Converter().Convert(&testCase.pod, &versionedPod) + err := testapi.Default.Converter().Convert(&testCase.pod, &versionedPod, nil) if err != nil { t.Fatalf("%s: error in versioning the pod: %v", testCase.desc, err) } diff --git a/pkg/kubelet/config/http_test.go b/pkg/kubelet/config/http_test.go index 20b4dc39e1c..9e70d9eecad 100644 --- a/pkg/kubelet/config/http_test.go +++ b/pkg/kubelet/config/http_test.go @@ -276,7 +276,7 @@ func TestExtractPodsFromHTTP(t *testing.T) { for _, testCase := range testCases { var versionedPods runtime.Object - err := testapi.Default.Converter().Convert(&testCase.pods, &versionedPods) + err := testapi.Default.Converter().Convert(&testCase.pods, &versionedPods, nil) if err != nil { t.Fatalf("%s: error in versioning the pods: %s", testCase.desc, err) } diff --git a/pkg/registry/thirdpartyresourcedata/codec.go b/pkg/registry/thirdpartyresourcedata/codec.go index 323b640324d..64308565e77 100644 --- a/pkg/registry/thirdpartyresourcedata/codec.go +++ b/pkg/registry/thirdpartyresourcedata/codec.go @@ -53,8 +53,8 @@ func (t *thirdPartyObjectConverter) ConvertToVersion(in runtime.Object, outVersi } } -func (t *thirdPartyObjectConverter) Convert(in, out interface{}) error { - return t.converter.Convert(in, out) +func (t *thirdPartyObjectConverter) Convert(in, out, context interface{}) error { + return t.converter.Convert(in, out, context) } func (t *thirdPartyObjectConverter) ConvertFieldLabel(version, kind, label, value string) (string, string, error) { diff --git a/pkg/runtime/codec.go b/pkg/runtime/codec.go index 58e1d3b6784..9077f0fcd73 100644 --- a/pkg/runtime/codec.go +++ b/pkg/runtime/codec.go @@ -145,16 +145,16 @@ func (c *parameterCodec) DecodeParameters(parameters url.Values, from unversione } targetGVK := targetGVKs[0] if targetGVK.GroupVersion() == from { - return c.convertor.Convert(¶meters, into) + return c.convertor.Convert(¶meters, into, nil) } input, err := c.creator.New(from.WithKind(targetGVK.Kind)) if err != nil { return err } - if err := c.convertor.Convert(¶meters, input); err != nil { + if err := c.convertor.Convert(¶meters, input, nil); err != nil { return err } - return c.convertor.Convert(input, into) + return c.convertor.Convert(input, into, nil) } // EncodeParameters converts the provided object into the to version, then converts that object to url.Values. diff --git a/pkg/runtime/conversion_test.go b/pkg/runtime/conversion_test.go index c364d8fd2d4..9c2e7f9f81a 100644 --- a/pkg/runtime/conversion_test.go +++ b/pkg/runtime/conversion_test.go @@ -122,7 +122,7 @@ func TestStringMapConversion(t *testing.T) { for k, tc := range testCases { out := &ExternalComplex{} - if err := scheme.Convert(&tc.input, out); (tc.errFn == nil && err != nil) || (tc.errFn != nil && !tc.errFn(err)) { + if err := scheme.Convert(&tc.input, out, nil); (tc.errFn == nil && err != nil) || (tc.errFn != nil && !tc.errFn(err)) { t.Errorf("%s: unexpected error: %v", k, err) continue } else if err != nil { diff --git a/pkg/runtime/interfaces.go b/pkg/runtime/interfaces.go index 117f43a4a50..07f77ca1b9f 100644 --- a/pkg/runtime/interfaces.go +++ b/pkg/runtime/interfaces.go @@ -176,10 +176,12 @@ type ObjectVersioner interface { // 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 + // not guarantee the in object is not mutated. The context argument will be passed to + // all nested conversions. + Convert(in, out, context 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. + // method does not guarantee that the in object is not mutated. This method is similar to + // Convert() but handles specific details of choosing the correct output version. ConvertToVersion(in Object, gv GroupVersioner) (out Object, err error) ConvertFieldLabel(version, kind, label, value string) (string, string, error) } diff --git a/pkg/runtime/scheme_test.go b/pkg/runtime/scheme_test.go index 135d9c2f93c..beeafe72176 100644 --- a/pkg/runtime/scheme_test.go +++ b/pkg/runtime/scheme_test.go @@ -129,7 +129,7 @@ func TestScheme(t *testing.T) { // Test Convert external := &ExternalSimple{} - err = scheme.Convert(simple, external) + err = scheme.Convert(simple, external, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -894,7 +894,7 @@ func TestMetaValuesUnregisteredConvert(t *testing.T) { simple := &InternalSimple{TestString: "foo"} external := &ExternalSimple{} - err = s.Convert(simple, external) + err = s.Convert(simple, external, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/pkg/runtime/serializer/versioning/versioning.go b/pkg/runtime/serializer/versioning/versioning.go index 4bd25e83156..9f47d00f1ab 100644 --- a/pkg/runtime/serializer/versioning/versioning.go +++ b/pkg/runtime/serializer/versioning/versioning.go @@ -102,7 +102,7 @@ func (c *codec) Decode(data []byte, defaultGVK *unversioned.GroupVersionKind, in } return into, gvk, nil } - if err := c.convertor.Convert(obj, into); err != nil { + if err := c.convertor.Convert(obj, into, c.decodeVersion); err != nil { return nil, gvk, err } if isVersioned { diff --git a/pkg/runtime/serializer/versioning/versioning_test.go b/pkg/runtime/serializer/versioning/versioning_test.go index 0ae74b2c9ae..ce5d944d1e8 100644 --- a/pkg/runtime/serializer/versioning/versioning_test.go +++ b/pkg/runtime/serializer/versioning/versioning_test.go @@ -298,7 +298,7 @@ type checkConvertor struct { directConvert bool } -func (c *checkConvertor) Convert(in, out interface{}) error { +func (c *checkConvertor) Convert(in, out, context interface{}) error { if !c.directConvert { return fmt.Errorf("unexpected call to Convert") } diff --git a/pkg/runtime/unstructured.go b/pkg/runtime/unstructured.go index 0e3488792d2..7f275ef1a9a 100644 --- a/pkg/runtime/unstructured.go +++ b/pkg/runtime/unstructured.go @@ -168,7 +168,7 @@ func (s unstructuredJSONScheme) decodeToList(data []byte, list *UnstructuredList // sane implementation for APIs that require an object converter. type UnstructuredObjectConverter struct{} -func (UnstructuredObjectConverter) Convert(in, out interface{}) error { +func (UnstructuredObjectConverter) Convert(in, out, context interface{}) error { unstructIn, ok := in.(*Unstructured) if !ok { return fmt.Errorf("input type %T in not valid for unstructured conversion", in) diff --git a/plugin/cmd/kube-scheduler/app/options/options.go b/plugin/cmd/kube-scheduler/app/options/options.go index 4d84dcd225f..69ec1db955f 100644 --- a/plugin/cmd/kube-scheduler/app/options/options.go +++ b/plugin/cmd/kube-scheduler/app/options/options.go @@ -41,7 +41,7 @@ type SchedulerServer struct { // NewSchedulerServer creates a new SchedulerServer with default parameters func NewSchedulerServer() *SchedulerServer { config := componentconfig.KubeSchedulerConfiguration{} - api.Scheme.Convert(&v1alpha1.KubeSchedulerConfiguration{}, &config) + api.Scheme.Convert(&v1alpha1.KubeSchedulerConfiguration{}, &config, nil) config.LeaderElection.LeaderElect = true s := SchedulerServer{ KubeSchedulerConfiguration: config, From e54d974a123c2b7e4452b4ba535873cacaeb81dc Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 5 Jul 2016 18:36:29 -0400 Subject: [PATCH 6/7] ResourcePrinter should pass all versions at once Rather than one at a time. --- pkg/kubectl/resource_printer.go | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index f4ee016daef..dcc958a33f2 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -195,20 +195,11 @@ func (p *VersionedPrinter) PrintObj(obj runtime.Object, w io.Writer) error { if len(p.versions) == 0 { return fmt.Errorf("no version specified, object cannot be converted") } - for _, version := range p.versions { - if version.IsEmpty() { - continue - } - converted, err := p.converter.ConvertToVersion(obj, version) - if runtime.IsNotRegisteredError(err) { - continue - } - if err != nil { - return err - } - return p.printer.PrintObj(converted, w) + converted, err := p.converter.ConvertToVersion(obj, unversioned.GroupVersions(p.versions)) + if err != nil { + return err } - return fmt.Errorf("the object cannot be converted to any of the versions: %v", p.versions) + return p.printer.PrintObj(converted, w) } // TODO: implement HandledResources() From 12d7032c3973e3401244970b240ceae995a8eac2 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 12 Jul 2016 12:52:07 -0400 Subject: [PATCH 7/7] Encoding nested objects must support unregistered runtime.Objects The nested object could be using a different scheme, or want to handle runtime.Unknown. --- pkg/runtime/serializer/versioning/versioning.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/runtime/serializer/versioning/versioning.go b/pkg/runtime/serializer/versioning/versioning.go index 9f47d00f1ab..b3a165a5379 100644 --- a/pkg/runtime/serializer/versioning/versioning.go +++ b/pkg/runtime/serializer/versioning/versioning.go @@ -192,6 +192,9 @@ type DirectEncoder struct { func (e DirectEncoder) Encode(obj runtime.Object, stream io.Writer) error { gvks, _, err := e.ObjectTyper.ObjectKinds(obj) if err != nil { + if runtime.IsNotRegisteredError(err) { + return e.Encoder.Encode(obj, stream) + } return err } kind := obj.GetObjectKind()