From 69c483f6205a2b35bea970917aadd155b8b492b1 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Fri, 18 Jul 2014 18:51:28 -0700 Subject: [PATCH] Add APIObject for generic inclusion of API objects. Includes test and json/yaml getters and setters. --- pkg/api/apiobj.go | 90 +++++++++++++++++++++++++++++++++ pkg/api/apiobj_test.go | 72 ++++++++++++++++++++++++++ pkg/api/helper.go | 4 +- pkg/api/types.go | 18 +++++-- pkg/api/validation_test.go | 2 +- pkg/apiserver/apiserver.go | 22 +++----- pkg/apiserver/apiserver_test.go | 16 ++---- 7 files changed, 189 insertions(+), 35 deletions(-) create mode 100644 pkg/api/apiobj.go create mode 100644 pkg/api/apiobj_test.go diff --git a/pkg/api/apiobj.go b/pkg/api/apiobj.go new file mode 100644 index 0000000000..ccf9c9b93c --- /dev/null +++ b/pkg/api/apiobj.go @@ -0,0 +1,90 @@ +/* +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 api + +import ( + "gopkg.in/v1/yaml" +) + +// Encode()/Decode() are the canonical way of converting an API object to/from +// wire format. This file provides utility functions which permit doing so +// recursively, such that API objects of types known only at run time can be +// embedded within other API types. + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (a *APIObject) UnmarshalJSON(b []byte) error { + // Handle JSON's "null": Decode() doesn't expect it. + if len(b) == 4 && string(b) == "null" { + a.Object = nil + return nil + } + + obj, err := Decode(b) + if err != nil { + return err + } + a.Object = obj + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (a APIObject) MarshalJSON() ([]byte, error) { + if a.Object == nil { + // Encode unset/nil objects as JSON's "null". + return []byte("null"), nil + } + + return Encode(a.Object) +} + +// SetYAML implements the yaml.Setter interface. +func (a *APIObject) SetYAML(tag string, value interface{}) bool { + if value == nil { + a.Object = nil + return true + } + // Why does the yaml package send value as a map[interface{}]interface{}? + // It's especially frustrating because encoding/json does the right thing + // by giving a []byte. So here we do the embarrasing thing of re-encode and + // de-encode the right way. + // TODO: Write a version of Decode that uses reflect to turn this value + // into an API object. + b, err := yaml.Marshal(value) + if err != nil { + panic("yaml can't reverse it's own object") + } + obj, err := Decode(b) + if err != nil { + return false + } + a.Object = obj + return true +} + +// GetYAML implements the yaml.Getter interface. +func (a APIObject) GetYAML() (tag string, value interface{}) { + if a.Object == nil { + value = "null" + return + } + // Encode returns JSON, which is conveniently a subset of YAML. + v, err := Encode(a.Object) + if err != nil { + panic("impossible to encode API object!") + } + return tag, v +} diff --git a/pkg/api/apiobj_test.go b/pkg/api/apiobj_test.go new file mode 100644 index 0000000000..7ee243ecd5 --- /dev/null +++ b/pkg/api/apiobj_test.go @@ -0,0 +1,72 @@ +/* +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 api + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestAPIObject(t *testing.T) { + type EmbeddedTest struct { + JSONBase `yaml:",inline" json:",inline"` + Object APIObject `yaml:"object,omitempty" json:"object,omitempty"` + EmptyObject APIObject `yaml:"emptyObject,omitempty" json:"emptyObject,omitempty"` + } + + AddKnownTypes(EmbeddedTest{}) + + outer := &EmbeddedTest{ + JSONBase: JSONBase{ID: "outer"}, + Object: APIObject{ + &EmbeddedTest{ + JSONBase: JSONBase{ID: "inner"}, + }, + }, + } + + wire, err := Encode(outer) + if err != nil { + t.Fatalf("Unexpected encode error '%v'", err) + } + + t.Logf("Wire format is:\n%v\n", string(wire)) + + decoded, err := Decode(wire) + if err != nil { + t.Fatalf("Unexpected decode error %v", err) + } + + if e, a := outer, decoded; !reflect.DeepEqual(e, a) { + t.Errorf("Expected: %#v but got %#v", e, a) + } + + // test JSON decoding, too, since api.Decode uses yaml unmarshalling. + var decodedViaJSON EmbeddedTest + err = json.Unmarshal(wire, &decodedViaJSON) + if err != nil { + t.Fatalf("Unexpected decode error %v", err) + } + + // Things that Decode would have done for us: + decodedViaJSON.Kind = "" + + if e, a := outer, &decodedViaJSON; !reflect.DeepEqual(e, a) { + t.Errorf("Expected: %#v but got %#v", e, a) + } +} diff --git a/pkg/api/helper.go b/pkg/api/helper.go index df11cc39c4..fadd429e72 100644 --- a/pkg/api/helper.go +++ b/pkg/api/helper.go @@ -66,7 +66,7 @@ func FindJSONBaseRO(obj interface{}) (JSONBase, error) { v = v.Elem() } if v.Kind() != reflect.Struct { - return JSONBase{}, fmt.Errorf("expected struct, but got %v", v.Type().Name()) + return JSONBase{}, fmt.Errorf("expected struct, but got %v (%#v)", v.Type().Name(), v.Interface()) } jsonBase := v.FieldByName("JSONBase") if !jsonBase.IsValid() { @@ -125,7 +125,7 @@ func nameAndJSONBase(obj interface{}) (string, *JSONBase, error) { v = v.Elem() name := v.Type().Name() if v.Kind() != reflect.Struct { - return "", nil, fmt.Errorf("expected struct, but got %v", name) + return "", nil, fmt.Errorf("expected struct, but got %v: %v (%#v)", v.Kind(), v.Type().Name(), v.Interface()) } jsonBase := v.FieldByName("JSONBase") if !jsonBase.IsValid() { diff --git a/pkg/api/types.go b/pkg/api/types.go index ce4921cc29..4e2dfa1b01 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -66,7 +66,7 @@ type Volume struct { // Source represents the location and type of a volume to mount. // This is optional for now. If not specified, the Volume is implied to be an EmptyDir. // This implied behavior is deprecated and will be removed in a future version. - Source *VolumeSource `yaml:"source" json:"source"` + Source *VolumeSource `yaml:"source" json:"source"` } type VolumeSource struct { @@ -86,7 +86,7 @@ type HostDirectory struct { Path string `yaml:"path" json:"path"` } -type EmptyDirectory struct {} +type EmptyDirectory struct{} // Port represents a network port in a single container type Port struct { @@ -345,6 +345,16 @@ type WatchEvent struct { // The type of the watch event; added, modified, or deleted. Type watch.EventType - // An object which can be decoded via api.Decode - EmbeddedObject []byte + // For added or modified objects, this is the new object; for deleted objects, + // it's the state of the object immediately prior to its deletion. + Object APIObject +} + +// APIObject has appropriate encoder and decoder functions, such that on the wire, it's +// stored as a []byte, but in memory, the contained object is accessable as an interface{} +// via the Get() function. Only objects having a JSONBase may be stored via APIObject. +// The purpose of this is to allow an API object of type known only at runtime to be +// embedded within other API objects. +type APIObject struct { + Object interface{} } diff --git a/pkg/api/validation_test.go b/pkg/api/validation_test.go index 8414eaea60..5a82be6508 100644 --- a/pkg/api/validation_test.go +++ b/pkg/api/validation_test.go @@ -208,7 +208,7 @@ func TestValidateManifest(t *testing.T) { Version: "v1beta1", ID: "abc", Volumes: []Volume{{Name: "vol1", Source: &VolumeSource{HostDirectory: &HostDirectory{"/mnt/vol1"}}}, - {Name: "vol2", Source: &VolumeSource{HostDirectory: &HostDirectory{"/mnt/vol2"}}}}, + {Name: "vol2", Source: &VolumeSource{HostDirectory: &HostDirectory{"/mnt/vol2"}}}}, Containers: []Container{ { Name: "abc", diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 11c651acff..5cf26c7ea7 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -503,14 +503,9 @@ func (w *WatchServer) HandleWS(ws *websocket.Conn) { // End of results. return } - wireFormat, err := api.Encode(event.Object) - if err != nil { - glog.Errorf("error encoding %#v: %v", event.Object, err) - return - } - err = websocket.JSON.Send(ws, &api.WatchEvent{ - Type: event.Type, - EmbeddedObject: wireFormat, + err := websocket.JSON.Send(ws, &api.WatchEvent{ + Type: event.Type, + Object: api.APIObject{event.Object}, }) if err != nil { // Client disconnect. @@ -555,14 +550,9 @@ func (self *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { // End of results. return } - wireFormat, err := api.Encode(event.Object) - if err != nil { - glog.Errorf("error encoding %#v: %v", event.Object, err) - return - } - err = encoder.Encode(&api.WatchEvent{ - Type: event.Type, - EmbeddedObject: wireFormat, + err := encoder.Encode(&api.WatchEvent{ + Type: event.Type, + Object: api.APIObject{event.Object}, }) if err != nil { // Client disconnect. diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index b4402cdfa9..fddc8d0dbc 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -595,12 +595,8 @@ func TestWatchWebsocket(t *testing.T) { if got.Type != action { t.Errorf("Unexpected type: %v", got.Type) } - apiObj, err := api.Decode(got.EmbeddedObject) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if !reflect.DeepEqual(object, apiObj) { - t.Errorf("Expected %v, got %v", object, apiObj) + if e, a := object, got.Object.Object; !reflect.DeepEqual(e, a) { + t.Errorf("Expected %v, got %v", e, a) } } @@ -654,12 +650,8 @@ func TestWatchHTTP(t *testing.T) { if got.Type != action { t.Errorf("Unexpected type: %v", got.Type) } - apiObj, err := api.Decode(got.EmbeddedObject) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if !reflect.DeepEqual(object, apiObj) { - t.Errorf("Expected %v, got %v", object, apiObj) + if e, a := object, got.Object.Object; !reflect.DeepEqual(e, a) { + t.Errorf("Expected %v, got %v", e, a) } }