From 22c99c98e29b29de07037fd1cb43a75186b5cc40 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 12 Jan 2015 00:33:25 -0500 Subject: [PATCH] Split RESTStorage into separate interfaces Omit unimplemented interfaces from Swagger --- pkg/apiserver/apiserver.go | 108 +++++++++++++++++--------------- pkg/apiserver/apiserver_test.go | 66 +++++++++++++++++++ pkg/apiserver/interfaces.go | 17 ++++- pkg/apiserver/proxy.go | 3 +- pkg/apiserver/redirect.go | 3 +- pkg/apiserver/resthandler.go | 48 ++++++++++---- pkg/apiserver/watch.go | 52 +++++++-------- pkg/master/publish.go | 3 +- pkg/master/rest_to_nodes.go | 7 ++- pkg/registry/controller/rest.go | 4 ++ pkg/registry/endpoint/rest.go | 4 ++ pkg/registry/event/rest.go | 4 ++ pkg/registry/minion/rest.go | 4 ++ pkg/registry/pod/rest.go | 4 ++ pkg/registry/service/rest.go | 4 ++ 15 files changed, 239 insertions(+), 92 deletions(-) diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f119a1352f3..cf97c7664ff 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -129,62 +129,70 @@ func registerResourceHandlers(ws *restful.WebService, version string, path strin nameParam := ws.PathParameter("name", "name of the "+kind).DataType("string") namespaceParam := ws.PathParameter("namespace", "object name and auth scope, such as for teams and projects").DataType("string") - ws.Route( - addParamIf( - ws.POST(path).To(h). - Doc("create a "+kind). - Operation("create"+kind). - Reads(versionedObject), // from the request - namespaceParam, namespaceScope)) - - // TODO: This seems like a hack. Add NewList() to storage? - listKind := kind + "List" - if _, ok := kinds[listKind]; !ok { - glog.V(1).Infof("no list type: %v\n", listKind) + createRoute := ws.POST(path).To(h). + Doc("create a " + kind). + Operation("create" + kind). + Reads(versionedObject) // from the request + addParamIf(createRoute, namespaceParam, namespaceScope) + if _, ok := storage.(RESTCreater); ok { + ws.Route(createRoute.Reads(versionedObject)) // from the request } else { - versionedListPtr, err := api.Scheme.New(version, listKind) - if err != nil { - glog.Errorf("error making list: %v\n", err) - } else { - versionedList := indirectArbitraryPointer(versionedListPtr) - glog.V(3).Infoln("type: ", reflect.TypeOf(versionedList)) - ws.Route( - addParamIf( - ws.GET(path).To(h). - Doc("list objects of kind "+kind). - Operation("list"+kind). - Returns(http.StatusOK, "OK", versionedList), - namespaceParam, namespaceScope)) - } + ws.Route(createRoute.Returns(http.StatusMethodNotAllowed, "creating objects is not supported", nil)) } - ws.Route( - addParamIf( - ws.GET(path+"/{name}").To(h). - Doc("read the specified "+kind). - Operation("read"+kind). - Param(nameParam). - Writes(versionedObject), // on the response - namespaceParam, namespaceScope)) + listRoute := ws.GET(path).To(h). + Doc("list objects of kind " + kind). + Operation("list" + kind) + addParamIf(listRoute, namespaceParam, namespaceScope) + if lister, ok := storage.(RESTLister); ok { + list := lister.NewList() + _, listKind, err := api.Scheme.ObjectVersionAndKind(list) + versionedListPtr, err := api.Scheme.New(version, listKind) + if err != nil { + glog.Errorf("error making list object: %v\n", err) + return + } + versionedList := indirectArbitraryPointer(versionedListPtr) + glog.V(3).Infoln("type: ", reflect.TypeOf(versionedList)) + ws.Route(listRoute.Returns(http.StatusOK, "OK", versionedList)) + } else { + ws.Route(listRoute.Returns(http.StatusMethodNotAllowed, "listing objects is not supported", nil)) + } - ws.Route( - addParamIf( - ws.PUT(path+"/{name}").To(h). - Doc("update the specified "+kind). - Operation("update"+kind). - Param(nameParam). - Reads(versionedObject), // from the request - namespaceParam, namespaceScope)) + getRoute := ws.GET(path + "/{name}").To(h). + Doc("read the specified " + kind). + Operation("read" + kind). + Param(nameParam) + addParamIf(getRoute, namespaceParam, namespaceScope) + if _, ok := storage.(RESTGetter); ok { + ws.Route(getRoute.Writes(versionedObject)) // on the response + } else { + ws.Route(ws.GET(path+"/{name}").To(h). + Returns(http.StatusMethodNotAllowed, "reading individual objects is not supported", nil)) + } + + updateRoute := ws.PUT(path + "/{name}").To(h). + Doc("update the specified " + kind). + Operation("update" + kind). + Param(nameParam) + addParamIf(updateRoute, namespaceParam, namespaceScope) + if _, ok := storage.(RESTUpdater); ok { + ws.Route(updateRoute.Reads(versionedObject)) // from the request + } else { + ws.Route(updateRoute.Returns(http.StatusMethodNotAllowed, "updating objects is not supported", nil)) + } // TODO: Support PATCH - - ws.Route( - addParamIf( - ws.DELETE(path+"/{name}").To(h). - Doc("delete the specified "+kind). - Operation("delete"+kind). - Param(nameParam), - namespaceParam, namespaceScope)) + deleteRoute := ws.DELETE(path + "/{name}").To(h). + Doc("delete the specified " + kind). + Operation("delete" + kind). + Param(nameParam) + addParamIf(deleteRoute, namespaceParam, namespaceScope) + if _, ok := storage.(RESTDeleter); ok { + ws.Route(deleteRoute) + } else { + ws.Route(deleteRoute.Returns(http.StatusMethodNotAllowed, "deleting objects is not supported", nil)) + } } // Adds the given param to the given route builder if shouldAdd is true. Does nothing if shouldAdd is false. diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index dd4803e79ec..81b4193c96f 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -183,6 +183,10 @@ func (storage *SimpleRESTStorage) New() runtime.Object { return &Simple{} } +func (storage *SimpleRESTStorage) NewList() runtime.Object { + return &SimpleList{} +} + func (storage *SimpleRESTStorage) Create(ctx api.Context, obj runtime.Object) (<-chan RESTResult, error) { storage.created = obj.(*Simple) if err := storage.errors["create"]; err != nil { @@ -288,6 +292,68 @@ func TestNotFound(t *testing.T) { } } +type UnimplementedRESTStorage struct{} + +func (UnimplementedRESTStorage) New() runtime.Object { + return &Simple{} +} + +func TestMethodNotAllowed(t *testing.T) { + type T struct { + Method string + Path string + } + cases := map[string]T{ + "GET object": {"GET", "/prefix/version/foo/bar"}, + "GET list": {"GET", "/prefix/version/foo"}, + "POST list": {"POST", "/prefix/version/foo"}, + "PUT object": {"PUT", "/prefix/version/foo/bar"}, + "DELETE object": {"DELETE", "/prefix/version/foo/bar"}, + //"watch list": {"GET", "/prefix/version/watch/foo"}, + //"watch object": {"GET", "/prefix/version/watch/foo/bar"}, + "proxy object": {"GET", "/prefix/version/proxy/foo/bar"}, + "redirect object": {"GET", "/prefix/version/redirect/foo/bar"}, + } + handler := Handle(map[string]RESTStorage{ + "foo": UnimplementedRESTStorage{}, + }, codec, "/prefix", testVersion, selfLinker, admissionControl) + server := httptest.NewServer(handler) + defer server.Close() + client := http.Client{} + for k, v := range cases { + request, err := http.NewRequest(v.Method, server.URL+v.Path, bytes.NewReader([]byte(`{"kind":"Simple","apiVersion":"version"}`))) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + response, err := client.Do(request) + if err != nil { + t.Fatalf("unexpected error: %v", err) + continue + } + defer response.Body.Close() + data, _ := ioutil.ReadAll(response.Body) + t.Logf("resp: %s", string(data)) + if response.StatusCode != http.StatusMethodNotAllowed { + t.Errorf("%s: expected %d for %s, Got %s", k, http.StatusMethodNotAllowed, v.Method, string(data)) + continue + } + obj, err := codec.Decode(data) + if err != nil { + t.Errorf("%s: unexpected decode error: %v", k, err) + continue + } + status, ok := obj.(*api.Status) + if !ok { + t.Errorf("%s: unexpected object: %#v", k, obj) + continue + } + if status.Reason != api.StatusReasonMethodNotAllowed { + t.Errorf("%s: unexpected status: %#v", k, status) + } + } +} + func TestVersion(t *testing.T) { handler := Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker, admissionControl) server := httptest.NewServer(handler) diff --git a/pkg/apiserver/interfaces.go b/pkg/apiserver/interfaces.go index 72d5ebb1021..94f58a6747e 100644 --- a/pkg/apiserver/interfaces.go +++ b/pkg/apiserver/interfaces.go @@ -24,28 +24,43 @@ import ( ) // RESTStorage is a generic interface for RESTful storage services. -// Resources which are exported to the RESTful API of apiserver need to implement this interface. +// Resources which are exported to the RESTful API of apiserver need to implement this interface. It is expected +// that objects may implement any of the REST* interfaces. +// TODO: implement dynamic introspection (so GenericREST objects can indicate what they implement) type RESTStorage interface { // New returns an empty object that can be used with Create and Update after request data has been put into it. // This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object) New() runtime.Object +} +type RESTLister interface { + // NewList returns an empty object that can be used with the List call. + // This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object) + NewList() runtime.Object // List selects resources in the storage which match to the selector. List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) +} +type RESTGetter interface { // Get finds a resource in the storage by id and returns it. // Although it can return an arbitrary error value, IsNotFound(err) is true for the // returned error value err when the specified resource is not found. Get(ctx api.Context, id string) (runtime.Object, error) +} +type RESTDeleter interface { // Delete finds a resource in the storage and deletes it. // Although it can return an arbitrary error value, IsNotFound(err) is true for the // returned error value err when the specified resource is not found. Delete(ctx api.Context, id string) (<-chan RESTResult, error) +} +type RESTCreater interface { // Create creates a new version of a resource. Create(ctx api.Context, obj runtime.Object) (<-chan RESTResult, error) +} +type RESTUpdater interface { // Update finds a resource in the storage and updates it. Some implementations // may allow updates creates the object - they should set the Created flag of // the returned RESTResultto true. In the event of an asynchronous error returned diff --git a/pkg/apiserver/proxy.go b/pkg/apiserver/proxy.go index 1acad7c386b..edaed7423b9 100644 --- a/pkg/apiserver/proxy.go +++ b/pkg/apiserver/proxy.go @@ -30,6 +30,7 @@ import ( "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -106,7 +107,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { redirector, ok := storage.(Redirector) if !ok { httplog.LogOf(req, w).Addf("'%v' is not a redirector", kind) - notFound(w, req) + errorJSON(errors.NewMethodNotSupported(kind, "proxy"), r.codec, w) return } diff --git a/pkg/apiserver/redirect.go b/pkg/apiserver/redirect.go index bdc9aa44fbf..bd6f5b5c47d 100644 --- a/pkg/apiserver/redirect.go +++ b/pkg/apiserver/redirect.go @@ -20,6 +20,7 @@ import ( "net/http" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) @@ -53,7 +54,7 @@ func (r *RedirectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { redirector, ok := storage.(Redirector) if !ok { httplog.LogOf(req, w).Addf("'%v' is not a redirector", kind) - notFound(w, req) + errorJSON(errors.NewMethodNotSupported(kind, "redirect"), r.codec, w) return } diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 978a5149212..61a5447f9e8 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -23,7 +23,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -48,14 +48,13 @@ func (h *RESTHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { notFound(w, req) return } - storage := h.storage[kind] - if storage == nil { - httplog.LogOf(req, w).Addf("'%v' has no storage object", kind) + storage, ok := h.storage[kind] + if !ok { notFound(w, req) return } - h.handleRESTStorage(parts, req, w, storage, namespace) + h.handleRESTStorage(parts, req, w, storage, namespace, kind) } // Sets the SelfLink field of the object. @@ -148,7 +147,7 @@ func curry(f func(runtime.Object, *http.Request) error, req *http.Request) func( // sync=[false|true] Synchronous request (only applies to create, update, delete operations) // timeout= Timeout for synchronous requests, only applies if sync=true // labels= Used for filtering list operations -func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w http.ResponseWriter, storage RESTStorage, namespace string) { +func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w http.ResponseWriter, storage RESTStorage, namespace, kind string) { ctx := api.WithNamespace(api.NewContext(), namespace) sync := req.URL.Query().Get("sync") == "true" timeout := parseTimeout(req.URL.Query().Get("timeout")) @@ -166,7 +165,12 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt errorJSON(err, h.codec, w) return } - list, err := storage.List(ctx, label, field) + lister, ok := storage.(RESTLister) + if !ok { + errorJSON(errors.NewMethodNotSupported(kind, "list"), h.codec, w) + return + } + list, err := lister.List(ctx, label, field) if err != nil { errorJSON(err, h.codec, w) return @@ -177,7 +181,12 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt } writeJSON(http.StatusOK, h.codec, list, w) case 2: - item, err := storage.Get(ctx, parts[1]) + getter, ok := storage.(RESTGetter) + if !ok { + errorJSON(errors.NewMethodNotSupported(kind, "get"), h.codec, w) + return + } + item, err := getter.Get(ctx, parts[1]) if err != nil { errorJSON(err, h.codec, w) return @@ -196,6 +205,12 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt notFound(w, req) return } + creater, ok := storage.(RESTCreater) + if !ok { + errorJSON(errors.NewMethodNotSupported(kind, "create"), h.codec, w) + return + } + body, err := readBody(req) if err != nil { errorJSON(err, h.codec, w) @@ -215,7 +230,7 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt return } - out, err := storage.Create(ctx, obj) + out, err := creater.Create(ctx, obj) if err != nil { errorJSON(err, h.codec, w) return @@ -228,6 +243,11 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt notFound(w, req) return } + deleter, ok := storage.(RESTDeleter) + if !ok { + errorJSON(errors.NewMethodNotSupported(kind, "delete"), h.codec, w) + return + } // invoke admission control err := h.admissionControl.Admit(admission.NewAttributesRecord(nil, namespace, parts[0], "DELETE")) @@ -236,7 +256,7 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt return } - out, err := storage.Delete(ctx, parts[1]) + out, err := deleter.Delete(ctx, parts[1]) if err != nil { errorJSON(err, h.codec, w) return @@ -249,6 +269,12 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt notFound(w, req) return } + updater, ok := storage.(RESTUpdater) + if !ok { + errorJSON(errors.NewMethodNotSupported(kind, "create"), h.codec, w) + return + } + body, err := readBody(req) if err != nil { errorJSON(err, h.codec, w) @@ -268,7 +294,7 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt return } - out, err := storage.Update(ctx, obj) + out, err := updater.Update(ctx, obj) if err != nil { errorJSON(err, h.codec, w) return diff --git a/pkg/apiserver/watch.go b/pkg/apiserver/watch.go index d43826f6301..867259a7366 100644 --- a/pkg/apiserver/watch.go +++ b/pkg/apiserver/watch.go @@ -24,6 +24,7 @@ import ( "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -98,34 +99,35 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { notFound(w, req) return } - if watcher, ok := storage.(ResourceWatcher); ok { - label, field, resourceVersion, err := getWatchParams(req.URL.Query()) - if err != nil { - errorJSON(err, h.codec, w) - return - } - watching, err := watcher.Watch(ctx, label, field, resourceVersion) - if err != nil { - errorJSON(err, h.codec, w) - return - } - - // TODO: This is one watch per connection. We want to multiplex, so that - // multiple watches of the same thing don't create two watches downstream. - watchServer := &WatchServer{watching, h.codec, func(obj runtime.Object) { - if err := h.setSelfLinkAddName(obj, req); err != nil { - glog.Errorf("Failed to set self link for object %#v", obj) - } - }} - if isWebsocketRequest(req) { - websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(w), req) - } else { - watchServer.ServeHTTP(w, req) - } + watcher, ok := storage.(ResourceWatcher) + if !ok { + errorJSON(errors.NewMethodNotSupported(kind, "watch"), h.codec, w) return } - notFound(w, req) + label, field, resourceVersion, err := getWatchParams(req.URL.Query()) + if err != nil { + errorJSON(err, h.codec, w) + return + } + watching, err := watcher.Watch(ctx, label, field, resourceVersion) + if err != nil { + errorJSON(err, h.codec, w) + return + } + + // TODO: This is one watch per connection. We want to multiplex, so that + // multiple watches of the same thing don't create two watches downstream. + watchServer := &WatchServer{watching, h.codec, func(obj runtime.Object) { + if err := h.setSelfLinkAddName(obj, req); err != nil { + glog.Errorf("Failed to set self link for object %#v", obj) + } + }} + if isWebsocketRequest(req) { + websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(w), req) + } else { + watchServer.ServeHTTP(w, req) + } } // WatchServer serves a watch.Interface over a websocket or vanilla HTTP. diff --git a/pkg/master/publish.go b/pkg/master/publish.go index 5e4041b3c7b..41b91b6af2a 100644 --- a/pkg/master/publish.go +++ b/pkg/master/publish.go @@ -21,6 +21,7 @@ import ( "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/golang/glog" ) @@ -94,7 +95,7 @@ func (m *Master) createMasterServiceIfNeeded(serviceName string, port int) error } // Kids, don't do this at home: this is a hack. There's no good way to call the business // logic which lives in the REST object from here. - c, err := m.storage["services"].Create(ctx, svc) + c, err := m.storage["services"].(apiserver.RESTCreater).Create(ctx, svc) if err != nil { return err } diff --git a/pkg/master/rest_to_nodes.go b/pkg/master/rest_to_nodes.go index a86a7eea90b..e25fc2aba1e 100644 --- a/pkg/master/rest_to_nodes.go +++ b/pkg/master/rest_to_nodes.go @@ -33,6 +33,9 @@ import ( // TODO: considering that the only difference between the various client types // and RESTStorage type is the type of the arguments, maybe use "go generate" to // write a specialized adaptor for every client type? +// +// TODO: this also means that pod and node API endpoints have to be colocated in the same +// process func RESTStorageToNodes(storage apiserver.RESTStorage) client.NodesInterface { return &nodeAdaptor{storage} } @@ -61,7 +64,7 @@ func (n *nodeAdaptor) Create(minion *api.Node) (*api.Node, error) { // List lists all the nodes in the cluster. func (n *nodeAdaptor) List() (*api.NodeList, error) { ctx := api.NewContext() - obj, err := n.storage.List(ctx, labels.Everything(), labels.Everything()) + obj, err := n.storage.(apiserver.RESTLister).List(ctx, labels.Everything(), labels.Everything()) if err != nil { return nil, err } @@ -71,7 +74,7 @@ func (n *nodeAdaptor) List() (*api.NodeList, error) { // Get gets an existing minion func (n *nodeAdaptor) Get(name string) (*api.Node, error) { ctx := api.NewContext() - obj, err := n.storage.Get(ctx, name) + obj, err := n.storage.(apiserver.RESTGetter).Get(ctx, name) if err != nil { return nil, err } diff --git a/pkg/registry/controller/rest.go b/pkg/registry/controller/rest.go index 1f298ee06f8..0a38fef53d0 100644 --- a/pkg/registry/controller/rest.go +++ b/pkg/registry/controller/rest.go @@ -122,6 +122,10 @@ func (*REST) New() runtime.Object { return &api.ReplicationController{} } +func (*REST) NewList() runtime.Object { + return &api.ReplicationControllerList{} +} + // Update replaces a given ReplicationController instance with an existing // instance in storage.registry. func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { diff --git a/pkg/registry/endpoint/rest.go b/pkg/registry/endpoint/rest.go index e409bf6d104..3a9d2771c70 100644 --- a/pkg/registry/endpoint/rest.go +++ b/pkg/registry/endpoint/rest.go @@ -105,3 +105,7 @@ func (rs *REST) Delete(ctx api.Context, id string) (<-chan apiserver.RESTResult, func (rs REST) New() runtime.Object { return &api.Endpoints{} } + +func (*REST) NewList() runtime.Object { + return &api.EndpointsList{} +} diff --git a/pkg/registry/event/rest.go b/pkg/registry/event/rest.go index 511ee5e40ee..5048a53a49d 100644 --- a/pkg/registry/event/rest.go +++ b/pkg/registry/event/rest.go @@ -128,6 +128,10 @@ func (*REST) New() runtime.Object { return &api.Event{} } +func (*REST) NewList() runtime.Object { + return &api.EventList{} +} + // Update returns an error: Events are not mutable. func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { return nil, fmt.Errorf("not allowed: 'Event' objects are not mutable") diff --git a/pkg/registry/minion/rest.go b/pkg/registry/minion/rest.go index 56202cc0351..8415a5968ff 100644 --- a/pkg/registry/minion/rest.go +++ b/pkg/registry/minion/rest.go @@ -104,6 +104,10 @@ func (rs *REST) New() runtime.Object { return &api.Node{} } +func (*REST) NewList() runtime.Object { + return &api.NodeList{} +} + // Update satisfies the RESTStorage interface. func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { minion, ok := obj.(*api.Node) diff --git a/pkg/registry/pod/rest.go b/pkg/registry/pod/rest.go index 3b78a068608..99bc58ff4b0 100644 --- a/pkg/registry/pod/rest.go +++ b/pkg/registry/pod/rest.go @@ -161,6 +161,10 @@ func (*REST) New() runtime.Object { return &api.Pod{} } +func (*REST) NewList() runtime.Object { + return &api.PodList{} +} + func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { pod := obj.(*api.Pod) if !api.ValidNamespace(ctx, &pod.ObjectMeta) { diff --git a/pkg/registry/service/rest.go b/pkg/registry/service/rest.go index 1165aeeb01e..b62e8c03edc 100644 --- a/pkg/registry/service/rest.go +++ b/pkg/registry/service/rest.go @@ -217,6 +217,10 @@ func (*REST) New() runtime.Object { return &api.Service{} } +func (*REST) NewList() runtime.Object { + return &api.Service{} +} + func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { service := obj.(*api.Service) if !api.ValidNamespace(ctx, &service.ObjectMeta) {