Split RESTStorage into separate interfaces

Omit unimplemented interfaces from Swagger
This commit is contained in:
Clayton Coleman 2015-01-12 00:33:25 -05:00
parent a52b216324
commit 22c99c98e2
15 changed files with 239 additions and 92 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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=<duration> Timeout for synchronous requests, only applies if sync=true
// labels=<label-selector> 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

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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{}
}

View File

@ -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")

View File

@ -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)

View File

@ -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) {

View File

@ -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) {