diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index ec68affceb0..68effe0c96b 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -115,12 +115,14 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { }, func(j *api.List, c fuzz.Continue) { c.FuzzNoCustom(j) // fuzz self without calling this function again - if j.Items == nil { + // TODO: uncomment when round trip starts from a versioned object + if false { //j.Items == nil { j.Items = []runtime.Object{} } }, func(j *runtime.Object, c fuzz.Continue) { - if c.RandBool() { + // TODO: uncomment when round trip starts from a versioned object + if true { //c.RandBool() { *j = &runtime.Unknown{ TypeMeta: runtime.TypeMeta{Kind: "Something", APIVersion: "unknown"}, RawJSON: []byte(`{"apiVersion":"unknown","kind":"Something","someKey":"someValue"}`), diff --git a/pkg/client/testclient/fixture.go b/pkg/client/testclient/fixture.go index 00c294d8a38..00158f57311 100644 --- a/pkg/client/testclient/fixture.go +++ b/pkg/client/testclient/fixture.go @@ -81,7 +81,7 @@ func ObjectReaction(o ObjectRetriever, mapper meta.RESTMapper) ReactionFunc { // AddObjectsFromPath loads the JSON or YAML file containing Kubernetes API resources // and adds them to the provided ObjectRetriever. -func AddObjectsFromPath(path string, o ObjectRetriever) error { +func AddObjectsFromPath(path string, o ObjectRetriever, decoder runtime.Decoder) error { data, err := ioutil.ReadFile(path) if err != nil { return err @@ -90,7 +90,7 @@ func AddObjectsFromPath(path string, o ObjectRetriever) error { if err != nil { return err } - obj, err := api.Codec.Decode(data) + obj, err := decoder.Decode(data) if err != nil { return err } @@ -103,17 +103,12 @@ func AddObjectsFromPath(path string, o ObjectRetriever) error { type objects struct { types map[string][]runtime.Object last map[string]int - typer runtime.ObjectTyper - creater runtime.ObjectCreater - copier copier + scheme runtime.ObjectScheme + decoder runtime.ObjectDecoder } var _ ObjectRetriever = &objects{} -type copier interface { - Copy(obj runtime.Object) (runtime.Object, error) -} - // NewObjects implements the ObjectRetriever interface by introspecting the // objects provided to Add() and returning them when the Kind method is invoked. // If an api.List object is provided to Add(), each child item is added. If an @@ -124,18 +119,17 @@ type copier interface { // as a runtime.Object if Status == Success). If multiple PodLists are provided, they // will be returned in order by the Kind call, and the last PodList will be reused for // subsequent calls. -func NewObjects(scheme *runtime.Scheme) ObjectRetriever { +func NewObjects(scheme runtime.ObjectScheme, decoder runtime.ObjectDecoder) ObjectRetriever { return objects{ types: make(map[string][]runtime.Object), last: make(map[string]int), - typer: scheme, - creater: scheme, - copier: scheme, + scheme: scheme, + decoder: decoder, } } func (o objects) Kind(kind, name string) (runtime.Object, error) { - empty, _ := o.creater.New("", kind) + empty, _ := o.scheme.New("", kind) nilValue := reflect.Zero(reflect.TypeOf(empty)).Interface().(runtime.Object) arr, ok := o.types[kind] @@ -146,14 +140,14 @@ func (o objects) Kind(kind, name string) (runtime.Object, error) { if !ok { return empty, nil } - out, err := o.creater.New("", kind) + out, err := o.scheme.New("", kind) if err != nil { return nilValue, err } if err := runtime.SetList(out, arr); err != nil { return nilValue, err } - if out, err = o.copier.Copy(out); err != nil { + if out, err = o.scheme.Copy(out); err != nil { return nilValue, err } return out, nil @@ -168,7 +162,7 @@ func (o objects) Kind(kind, name string) (runtime.Object, error) { if index < 0 { return nilValue, errors.NewNotFound(kind, name) } - out, err := o.copier.Copy(arr[index]) + out, err := o.scheme.Copy(arr[index]) if err != nil { return nilValue, err } @@ -187,7 +181,7 @@ func (o objects) Kind(kind, name string) (runtime.Object, error) { } func (o objects) Add(obj runtime.Object) error { - _, kind, err := o.typer.ObjectVersionAndKind(obj) + _, kind, err := o.scheme.ObjectVersionAndKind(obj) if err != nil { return err } @@ -202,6 +196,9 @@ func (o objects) Add(obj runtime.Object) error { if err != nil { return err } + if errs := runtime.DecodeList(list, o.decoder); len(errs) > 0 { + return errs[0] + } for _, obj := range list { if err := o.Add(obj); err != nil { return err diff --git a/pkg/client/testclient/testclient.go b/pkg/client/testclient/testclient.go index 41ec014b153..63a24642605 100644 --- a/pkg/client/testclient/testclient.go +++ b/pkg/client/testclient/testclient.go @@ -27,7 +27,7 @@ import ( // NewSimpleFake returns a client that will respond with the provided objects func NewSimpleFake(objects ...runtime.Object) *Fake { - o := NewObjects(api.Scheme) + o := NewObjects(api.Scheme, api.Scheme) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) diff --git a/pkg/client/testclient/testclient_test.go b/pkg/client/testclient/testclient_test.go index 3ea953bdefa..c2ca86ebbba 100644 --- a/pkg/client/testclient/testclient_test.go +++ b/pkg/client/testclient/testclient_test.go @@ -27,8 +27,8 @@ import ( ) func TestNewClient(t *testing.T) { - o := NewObjects(api.Scheme) - if err := AddObjectsFromPath("../../../examples/guestbook/frontend-service.json", o); err != nil { + o := NewObjects(api.Scheme, api.Scheme) + if err := AddObjectsFromPath("../../../examples/guestbook/frontend-service.json", o, api.Scheme); err != nil { t.Fatal(err) } client := &Fake{ReactFn: ObjectReaction(o, latest.RESTMapper)} @@ -52,7 +52,7 @@ func TestNewClient(t *testing.T) { } func TestErrors(t *testing.T) { - o := NewObjects(api.Scheme) + o := NewObjects(api.Scheme, api.Scheme) o.Add(&api.List{ Items: []runtime.Object{ // This first call to List will return this error diff --git a/pkg/conversion/error.go b/pkg/conversion/error.go index 1c053c3eaae..e6a12ee7847 100644 --- a/pkg/conversion/error.go +++ b/pkg/conversion/error.go @@ -54,6 +54,10 @@ type missingKindErr struct { data string } +func NewMissingKindErr(data string) error { + return &missingKindErr{data} +} + func (k *missingKindErr) Error() string { return fmt.Sprintf("Object 'Kind' is missing in '%s'", k.data) } @@ -70,6 +74,10 @@ type missingVersionErr struct { data string } +func NewMissingVersionErr(data string) error { + return &missingVersionErr{data} +} + func (k *missingVersionErr) Error() string { return fmt.Sprintf("Object 'apiVersion' is missing in '%s'", k.data) } diff --git a/pkg/conversion/scheme.go b/pkg/conversion/scheme.go index 7f9f8aa4014..594b23124b4 100644 --- a/pkg/conversion/scheme.go +++ b/pkg/conversion/scheme.go @@ -238,6 +238,17 @@ func (s *Scheme) AddDefaultingFuncs(defaultingFuncs ...interface{}) error { return nil } +// Recognizes returns true if the scheme is able to handle the provided version and kind +// of an object. +func (s *Scheme) Recognizes(version, kind string) bool { + m, ok := s.versionMap[version] + if !ok { + return false + } + _, ok = m[kind] + return ok +} + // RegisterInputDefaults sets the provided field mapping function and field matching // as the defaults for the provided input type. The fn may be nil, in which case no // mapping will happen by default. Use this method to register a mechanism for handling diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index 452ffd9ff8d..e435cba9f73 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -393,6 +393,17 @@ func TestGetMultipleTypeObjectsAsList(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } + list, err := runtime.ExtractList(out) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if errs := runtime.DecodeList(list, api.Scheme); len(errs) > 0 { + t.Fatalf("unexpected error: %v", errs) + } + if err := runtime.SetList(out, list); err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := &api.List{ Items: []runtime.Object{ &pods.Items[0], diff --git a/pkg/kubectl/resource/visitor.go b/pkg/kubectl/resource/visitor.go index a364bae5ed8..b8bd30ec3df 100644 --- a/pkg/kubectl/resource/visitor.go +++ b/pkg/kubectl/resource/visitor.go @@ -347,6 +347,12 @@ func (v FlattenListVisitor) Visit(fn VisitorFunc) error { if err != nil { return fn(info) } + if errs := runtime.DecodeList(items, struct { + runtime.ObjectTyper + runtime.Decoder + }{v.Mapper, info.Mapping.Codec}); len(errs) > 0 { + return errors.NewAggregate(errs) + } for i := range items { item, err := v.InfoForObject(items[i]) if err != nil { diff --git a/pkg/namespace/namespace_controller_test.go b/pkg/namespace/namespace_controller_test.go index 53fb4daa02b..f7fd7433c7b 100644 --- a/pkg/namespace/namespace_controller_test.go +++ b/pkg/namespace/namespace_controller_test.go @@ -135,7 +135,7 @@ func TestSyncNamespaceThatIsActive(t *testing.T) { } func TestRunStop(t *testing.T) { - o := testclient.NewObjects(api.Scheme) + o := testclient.NewObjects(api.Scheme, api.Scheme) client := &testclient.Fake{ReactFn: testclient.ObjectReaction(o, latest.RESTMapper)} nsMgr := NewNamespaceManager(client, 1*time.Second) diff --git a/pkg/runtime/codec.go b/pkg/runtime/codec.go index 640d6c3eb3d..937dd6ec8d3 100644 --- a/pkg/runtime/codec.go +++ b/pkg/runtime/codec.go @@ -21,8 +21,8 @@ import ( ) // CodecFor returns a Codec that invokes Encode with the provided version. -func CodecFor(scheme *Scheme, version string) Codec { - return &codecWrapper{scheme, version} +func CodecFor(codec ObjectCodec, version string) Codec { + return &codecWrapper{codec, version} } // yamlCodec converts YAML passed to the Decoder methods to JSON. @@ -69,11 +69,11 @@ func EncodeOrDie(codec Codec, obj Object) string { // codecWrapper implements encoding to an alternative // default version for a scheme. type codecWrapper struct { - *Scheme + ObjectCodec version string } // Encode implements Codec func (c *codecWrapper) Encode(obj Object) ([]byte, error) { - return c.Scheme.EncodeToVersion(obj, c.version) + return c.EncodeToVersion(obj, c.version) } diff --git a/pkg/runtime/embedded_test.go b/pkg/runtime/embedded_test.go index 90aae83f1fe..c37d9e04c36 100644 --- a/pkg/runtime/embedded_test.go +++ b/pkg/runtime/embedded_test.go @@ -63,9 +63,22 @@ func TestDecodeEmptyRawExtensionAsObject(t *testing.T) { s.AddKnownTypes("", &ObjectTest{}) s.AddKnownTypeWithName("v1test", "ObjectTest", &ObjectTestExternal{}) - _, err := s.Decode([]byte(`{"kind":"ObjectTest","apiVersion":"v1test","items":[{}]}`)) - if err == nil { - t.Fatalf("unexpected non-error") + obj, err := s.Decode([]byte(`{"kind":"ObjectTest","apiVersion":"v1test","items":[{}]}`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test := obj.(*ObjectTest) + if unk, ok := test.Items[0].(*runtime.Unknown); !ok || unk.Kind != "" || unk.APIVersion != "" || string(unk.RawJSON) != "{}" { + t.Fatalf("unexpected object: %#v", test.Items[0]) + } + + obj, err = s.Decode([]byte(`{"kind":"ObjectTest","apiVersion":"v1test","items":[{"kind":"Other","apiVersion":"v1"}]}`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test = obj.(*ObjectTest) + if unk, ok := test.Items[0].(*runtime.Unknown); !ok || unk.Kind != "Other" || unk.APIVersion != "v1" || string(unk.RawJSON) != `{"kind":"Other","apiVersion":"v1"}` { + t.Fatalf("unexpected object: %#v", test.Items[0]) } } @@ -99,17 +112,34 @@ func TestArrayOfRuntimeObject(t *testing.T) { if err := json.Unmarshal(wire, obj); err != nil { t.Fatalf("unexpected error: %v", err) } - t.Logf("exact wire is: %#v", string(obj.Items[0].RawJSON)) + t.Logf("exact wire is: %s", string(obj.Items[0].RawJSON)) decoded, err := s.Decode(wire) if err != nil { t.Fatalf("unexpected error: %v", err) } + list, err := runtime.ExtractList(decoded) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if errs := runtime.DecodeList(list, s); len(errs) > 0 { + t.Fatalf("unexpected error: %v", errs) + } + + list2, err := runtime.ExtractList(list[3]) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if errs := runtime.DecodeList(list2, s); len(errs) > 0 { + t.Fatalf("unexpected error: %v", errs) + } + if err := runtime.SetList(list[3], list2); err != nil { + t.Fatalf("unexpected error: %v", err) + } internal.Items[2].(*runtime.Unknown).Kind = "OtherTest" internal.Items[2].(*runtime.Unknown).APIVersion = "unknown" - if e, a := internal, decoded; !reflect.DeepEqual(e, a) { - t.Log(string(decoded.(*ObjectTest).Items[2].(*runtime.Unknown).RawJSON)) + if e, a := internal.Items, list; !reflect.DeepEqual(e, a) { t.Errorf("mismatched decoded: %s", util.ObjectDiff(e, a)) } } diff --git a/pkg/runtime/helper.go b/pkg/runtime/helper.go index beff579144a..1624e598469 100644 --- a/pkg/runtime/helper.go +++ b/pkg/runtime/helper.go @@ -23,7 +23,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" ) -// TODO: move me to pkg/api/meta +// IsListType returns true if the provided Object has a slice called Items func IsListType(obj Object) bool { _, err := GetItemsPtr(obj) return err == nil @@ -33,7 +33,6 @@ func IsListType(obj Object) bool { // If 'list' doesn't have an Items member, it's not really a list type // and an error will be returned. // This function will either return a pointer to a slice, or an error, but not both. -// TODO: move me to pkg/api/meta func GetItemsPtr(list Object) (interface{}, error) { v, err := conversion.EnforcePtr(list) if err != nil { @@ -150,9 +149,36 @@ func FieldPtr(v reflect.Value, fieldName string, dest interface{}) error { return fmt.Errorf("couldn't assign/convert %v to %v", field.Type(), v.Type()) } +// DecodeList alters the list in place, attempting to decode any objects found in +// the list that have the runtime.Unknown type. Any errors that occur are returned +// after the entire list is processed. Decoders are tried in order. +func DecodeList(objects []Object, decoders ...ObjectDecoder) []error { + errs := []error(nil) + for i, obj := range objects { + switch t := obj.(type) { + case *Unknown: + for _, decoder := range decoders { + if !decoder.Recognizes(t.APIVersion, t.Kind) { + continue + } + obj, err := decoder.Decode(t.RawJSON) + if err != nil { + errs = append(errs, err) + break + } + objects[i] = obj + break + } + } + } + return errs +} + // MultiObjectTyper returns the types of objects across multiple schemes in order. type MultiObjectTyper []ObjectTyper +var _ ObjectTyper = MultiObjectTyper{} + func (m MultiObjectTyper) DataVersionAndKind(data []byte) (version, kind string, err error) { for _, t := range m { version, kind, err = t.DataVersionAndKind(data) @@ -162,6 +188,7 @@ func (m MultiObjectTyper) DataVersionAndKind(data []byte) (version, kind string, } return } + func (m MultiObjectTyper) ObjectVersionAndKind(obj Object) (version, kind string, err error) { for _, t := range m { version, kind, err = t.ObjectVersionAndKind(obj) @@ -171,3 +198,12 @@ func (m MultiObjectTyper) ObjectVersionAndKind(obj Object) (version, kind string } return } + +func (m MultiObjectTyper) Recognizes(version, kind string) bool { + for _, t := range m { + if t.Recognizes(version, kind) { + return true + } + } + return false +} diff --git a/pkg/runtime/helper_test.go b/pkg/runtime/helper_test.go index d0238a9ab5e..9b17dd7e17f 100644 --- a/pkg/runtime/helper_test.go +++ b/pkg/runtime/helper_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -132,6 +133,22 @@ func TestExtractListOfValuePtrs(t *testing.T) { } } +func TestDecodeList(t *testing.T) { + pl := &api.List{ + Items: []runtime.Object{ + &api.Pod{ObjectMeta: api.ObjectMeta{Name: "1"}}, + &runtime.Unknown{TypeMeta: runtime.TypeMeta{Kind: "Pod", APIVersion: "v1beta3"}, RawJSON: []byte(`{"kind":"Pod","apiVersion":"v1beta3","metadata":{"name":"test"}}`)}, + &runtime.Unstructured{TypeMeta: runtime.TypeMeta{Kind: "Foo", APIVersion: "Bar"}, Object: map[string]interface{}{"test": "value"}}, + }, + } + if errs := runtime.DecodeList(pl.Items, api.Scheme); len(errs) != 0 { + t.Fatalf("unexpected error %v", errs) + } + if pod, ok := pl.Items[1].(*api.Pod); !ok || pod.Name != "test" { + t.Errorf("object not converted: %#v", pl.Items[1]) + } +} + func TestSetList(t *testing.T) { pl := &api.PodList{} list := []runtime.Object{ diff --git a/pkg/runtime/interfaces.go b/pkg/runtime/interfaces.go index da38b6640b5..3c7accb3021 100644 --- a/pkg/runtime/interfaces.go +++ b/pkg/runtime/interfaces.go @@ -16,6 +16,24 @@ limitations under the License. package runtime +// ObjectScheme represents common conversions between formal external API versions +// and the internal Go structs. ObjectScheme is typically used with ObjectCodec to +// transform internal Go structs into serialized versions. There may be many valid +// ObjectCodecs for each ObjectScheme. +type ObjectScheme interface { + ObjectConvertor + ObjectTyper + ObjectCreater + ObjectCopier +} + +// ObjectCodec represents the common mechanisms for converting to and from a particular +// binary representation of an object. +type ObjectCodec interface { + ObjectEncoder + Decoder +} + // Decoder defines methods for deserializing API objects into a given type type Decoder interface { Decode(data []byte) (Object, error) @@ -33,6 +51,22 @@ type Codec interface { Encoder } +// ObjectCopier duplicates an object. +type ObjectCopier interface { + // Copy returns an exact copy of the provided Object, or an error if the + // copy could not be completed. + Copy(Object) (Object, error) +} + +// ObjectEncoder turns an object into a byte array. This interface is a +// general form of the Encoder interface +type ObjectEncoder interface { + // EncodeToVersion convert and serializes an object in the internal format + // to a specified output version. An error is returned if the object + // cannot be converted for any reason. + EncodeToVersion(obj Object, outVersion string) ([]byte, error) +} + // ObjectConvertor converts an object to a different version. type ObjectConvertor interface { Convert(in, out interface{}) error @@ -43,8 +77,17 @@ type ObjectConvertor interface { // ObjectTyper contains methods for extracting the APIVersion and Kind // of objects. type ObjectTyper interface { + // DataVersionAndKind returns the version and kind of the provided data, or an error + // if another problem is detected. In many cases this method can be as expensive to + // invoke as the Decode method. DataVersionAndKind([]byte) (version, kind string, err error) + // ObjectVersionAndKind returns the version and kind of the provided object, or an + // error if the object is not recognized (IsNotRegisteredError will return true). ObjectVersionAndKind(Object) (version, kind string, err error) + // Recognizes returns true if the scheme is able to handle the provided version and kind, + // or more precisely that the provided version is a possible conversion or decoding + // target. + Recognizes(version, kind string) bool } // ObjectCreater contains methods for instantiating an object by kind and version. @@ -52,6 +95,20 @@ type ObjectCreater interface { New(version, kind string) (out Object, err error) } +// ObjectDecoder is a convenience interface for identifying serialized versions of objects +// and transforming them into Objects. It intentionally overlaps with ObjectTyper and +// Decoder for use in decode only paths. +type ObjectDecoder interface { + Decoder + // DataVersionAndKind returns the version and kind of the provided data, or an error + // if another problem is detected. In many cases this method can be as expensive to + // invoke as the Decode method. + DataVersionAndKind([]byte) (version, kind string, err error) + // Recognizes returns true if the scheme is able to handle the provided version and kind + // of an object. + Recognizes(version, kind string) bool +} + // ResourceVersioner provides methods for setting and retrieving // the resource version from an API object. type ResourceVersioner interface { diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 376313de9fd..da4fd28dc44 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -17,6 +17,7 @@ limitations under the License. package runtime import ( + "encoding/json" "fmt" "net/url" "reflect" @@ -164,7 +165,15 @@ func (self *Scheme) runtimeObjectToRawExtensionArray(in *[]Object, out *[]RawExt for i := range src { switch t := src[i].(type) { case *Unknown: + // TODO: this should be decoupled from the scheme (since it is JSON specific) dest[i].RawJSON = t.RawJSON + case *Unstructured: + // TODO: this should be decoupled from the scheme (since it is JSON specific) + data, err := json.Marshal(t.Object) + if err != nil { + return err + } + dest[i].RawJSON = data default: version := outVersion // if the object exists @@ -192,24 +201,17 @@ func (self *Scheme) rawExtensionToRuntimeObjectArray(in *[]RawExtension, out *[] for i := range src { data := src[i].RawJSON - obj, err := scheme.Decode(data) + version, kind, err := scheme.raw.DataVersionAndKind(data) if err != nil { - if !IsNotRegisteredError(err) { - return err - } - version, kind, err := scheme.raw.DataVersionAndKind(data) - if err != nil { - return err - } - obj = &Unknown{ - TypeMeta: TypeMeta{ - APIVersion: version, - Kind: kind, - }, - RawJSON: data, - } + return err + } + dest[i] = &Unknown{ + TypeMeta: TypeMeta{ + APIVersion: version, + Kind: kind, + }, + RawJSON: data, } - dest[i] = obj } *out = dest return nil @@ -275,6 +277,12 @@ func (s *Scheme) ObjectVersionAndKind(obj Object) (version, kind string, err err return s.raw.ObjectVersionAndKind(obj) } +// Recognizes returns true if the scheme is able to handle the provided version and kind +// of an object. +func (s *Scheme) Recognizes(version, kind string) bool { + return s.raw.Recognizes(version, kind) +} + // New returns a new API object of the given version ("" for internal // representation) and name, or an error if it hasn't been registered. func (s *Scheme) New(versionName, typeName string) (Object, error) { diff --git a/pkg/runtime/types.go b/pkg/runtime/types.go index 8f8a857c1a2..55ba7582ce7 100644 --- a/pkg/runtime/types.go +++ b/pkg/runtime/types.go @@ -116,3 +116,17 @@ type Unknown struct { } func (*Unknown) IsAnAPIObject() {} + +// Unstructured allows objects that do not have Golang structs registered to be manipulated +// generically. This can be used to deal with the API objects from a plug-in. Unstructured +// objects still have functioning TypeMeta features-- kind, version, etc. +// TODO: Make this object have easy access to field based accessors and settors for +// metadata and field mutatation. +type Unstructured struct { + TypeMeta `json:",inline"` + // Object is a JSON compatible map with string, float, int, []interface{}, or map[string]interface{} + // children. + Object map[string]interface{} +} + +func (*Unstructured) IsAnAPIObject() {} diff --git a/pkg/runtime/unstructured.go b/pkg/runtime/unstructured.go new file mode 100644 index 00000000000..fe2d9e63375 --- /dev/null +++ b/pkg/runtime/unstructured.go @@ -0,0 +1,89 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package runtime + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" +) + +// UnstructuredJSONScheme is capable of converting JSON data into the Unstructured +// type, which can be used for generic access to objects without a predefined scheme. +var UnstructuredJSONScheme ObjectDecoder = unstructuredJSONScheme{} + +type unstructuredJSONScheme struct{} + +// Recognizes returns true for any version or kind that is specified (internal +// versions are specifically excluded). +func (unstructuredJSONScheme) Recognizes(version, kind string) bool { + return len(version) > 0 && len(kind) > 0 +} + +func (s unstructuredJSONScheme) Decode(data []byte) (Object, error) { + unstruct := &Unstructured{} + if err := s.DecodeInto(data, unstruct); err != nil { + return nil, err + } + return unstruct, nil +} + +func (unstructuredJSONScheme) DecodeInto(data []byte, obj Object) error { + unstruct, ok := obj.(*Unstructured) + if !ok { + return fmt.Errorf("the unstructured JSON scheme does not recognize %v", reflect.TypeOf(obj)) + } + + m := make(map[string]interface{}) + if err := json.Unmarshal(data, &m); err != nil { + return err + } + if v, ok := m["kind"]; ok { + if s, ok := v.(string); ok { + unstruct.Kind = s + } + } + if v, ok := m["apiVersion"]; ok { + if s, ok := v.(string); ok { + unstruct.APIVersion = s + } + } + if len(unstruct.APIVersion) == 0 { + return conversion.NewMissingVersionErr(string(data)) + } + if len(unstruct.Kind) == 0 { + return conversion.NewMissingKindErr(string(data)) + } + unstruct.Object = m + return nil +} + +func (unstructuredJSONScheme) DataVersionAndKind(data []byte) (version, kind string, err error) { + obj := TypeMeta{} + if err := json.Unmarshal(data, &obj); err != nil { + return "", "", err + } + if len(obj.APIVersion) == 0 { + return "", "", conversion.NewMissingVersionErr(string(data)) + } + if len(obj.Kind) == 0 { + return "", "", conversion.NewMissingKindErr(string(data)) + } + return obj.APIVersion, obj.Kind, nil +} diff --git a/pkg/runtime/unstructured_test.go b/pkg/runtime/unstructured_test.go new file mode 100644 index 00000000000..2d7e6dc3dd2 --- /dev/null +++ b/pkg/runtime/unstructured_test.go @@ -0,0 +1,44 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package runtime_test + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func TestDecodeUnstructured(t *testing.T) { + pl := &api.List{ + Items: []runtime.Object{ + &api.Pod{ObjectMeta: api.ObjectMeta{Name: "1"}}, + &runtime.Unknown{TypeMeta: runtime.TypeMeta{Kind: "Pod", APIVersion: "v1beta3"}, RawJSON: []byte(`{"kind":"Pod","apiVersion":"v1beta3","metadata":{"name":"test"}}`)}, + &runtime.Unknown{TypeMeta: runtime.TypeMeta{Kind: "", APIVersion: "v1beta3"}, RawJSON: []byte(`{"kind":"Pod","apiVersion":"v1beta3","metadata":{"name":"test"}}`)}, + &runtime.Unstructured{TypeMeta: runtime.TypeMeta{Kind: "Foo", APIVersion: "Bar"}, Object: map[string]interface{}{"test": "value"}}, + }, + } + if errs := runtime.DecodeList(pl.Items, runtime.UnstructuredJSONScheme); len(errs) == 1 { + t.Fatalf("unexpected error %v", errs) + } + if pod, ok := pl.Items[1].(*runtime.Unstructured); !ok || pod.Object["kind"] != "Pod" || pod.Object["metadata"].(map[string]interface{})["name"] != "test" { + t.Errorf("object not converted: %#v", pl.Items[1]) + } + if _, ok := pl.Items[2].(*runtime.Unknown); !ok { + t.Errorf("object should not have been converted: %#v", pl.Items[2]) + } +} diff --git a/pkg/volume/persistent_claim/persistent_claim_test.go b/pkg/volume/persistent_claim/persistent_claim_test.go index bef5bd4f7e6..c43870e304a 100644 --- a/pkg/volume/persistent_claim/persistent_claim_test.go +++ b/pkg/volume/persistent_claim/persistent_claim_test.go @@ -153,7 +153,7 @@ func TestNewBuilder(t *testing.T) { } for _, item := range tests { - o := testclient.NewObjects(api.Scheme) + o := testclient.NewObjects(api.Scheme, api.Scheme) o.Add(item.pv) o.Add(item.claim) client := &testclient.Fake{ReactFn: testclient.ObjectReaction(o, latest.RESTMapper)} @@ -213,7 +213,7 @@ func TestNewBuilderClaimNotBound(t *testing.T) { ClaimName: "claimC", }, } - o := testclient.NewObjects(api.Scheme) + o := testclient.NewObjects(api.Scheme, api.Scheme) o.Add(pv) o.Add(claim) client := &testclient.Fake{ReactFn: testclient.ObjectReaction(o, latest.RESTMapper)} diff --git a/pkg/volumeclaimbinder/persistent_volume_claim_binder_test.go b/pkg/volumeclaimbinder/persistent_volume_claim_binder_test.go index 985ec257db0..c487b0fe3c2 100644 --- a/pkg/volumeclaimbinder/persistent_volume_claim_binder_test.go +++ b/pkg/volumeclaimbinder/persistent_volume_claim_binder_test.go @@ -29,7 +29,7 @@ import ( ) func TestRunStop(t *testing.T) { - o := testclient.NewObjects(api.Scheme) + o := testclient.NewObjects(api.Scheme, api.Scheme) client := &testclient.Fake{ReactFn: testclient.ObjectReaction(o, latest.RESTMapper)} binder := NewPersistentVolumeClaimBinder(client, 1*time.Second) @@ -111,8 +111,8 @@ func TestExampleObjects(t *testing.T) { } for name, scenario := range scenarios { - o := testclient.NewObjects(api.Scheme) - if err := testclient.AddObjectsFromPath("../../examples/persistent-volumes/"+name, o); err != nil { + o := testclient.NewObjects(api.Scheme, api.Scheme) + if err := testclient.AddObjectsFromPath("../../examples/persistent-volumes/"+name, o, api.Scheme); err != nil { t.Fatal(err) } @@ -168,11 +168,11 @@ func TestExampleObjects(t *testing.T) { func TestBindingWithExamples(t *testing.T) { api.ForTesting_ReferencesAllowBlankSelfLinks = true - o := testclient.NewObjects(api.Scheme) - if err := testclient.AddObjectsFromPath("../../examples/persistent-volumes/claims/claim-01.yaml", o); err != nil { + o := testclient.NewObjects(api.Scheme, api.Scheme) + if err := testclient.AddObjectsFromPath("../../examples/persistent-volumes/claims/claim-01.yaml", o, api.Scheme); err != nil { t.Fatal(err) } - if err := testclient.AddObjectsFromPath("../../examples/persistent-volumes/volumes/local-01.yaml", o); err != nil { + if err := testclient.AddObjectsFromPath("../../examples/persistent-volumes/volumes/local-01.yaml", o, api.Scheme); err != nil { t.Fatal(err) }