diff --git a/pkg/api/rest/rest.go b/pkg/api/rest/rest.go index dd8acecd690..fbb80b47f5a 100644 --- a/pkg/api/rest/rest.go +++ b/pkg/api/rest/rest.go @@ -28,6 +28,25 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) +//TODO: +// Storage interfaces need to be separated into two groups; those that operate +// on collections and those that operate on individually named items. +// Collection interfaces: +// (Method: Current -> Proposed) +// GET: Lister -> CollectionGetter +// WATCH: Watcher -> CollectionWatcher +// CREATE: Creater -> CollectionCreater +// DELETE: (n/a) -> CollectionDeleter +// UPDATE: (n/a) -> CollectionUpdater +// +// Single item interfaces: +// (Method: Current -> Proposed) +// GET: Getter -> NamedGetter +// WATCH: (n/a) -> NamedWatcher +// CREATE: (n/a) -> NamedCreater +// DELETE: Deleter -> NamedDeleter +// UPDATE: Update -> NamedUpdater + // Storage is a generic interface for RESTful storage services. // 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 below interfaces. @@ -117,6 +136,18 @@ type Creater interface { Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) } +// NamedCreater is an object that can create an instance of a RESTful object using a name parameter. +type NamedCreater interface { + // New returns an empty object that can be used with Create 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 + + // Create creates a new version of a resource. It expects a name parameter from the path. + // This is needed for create operations on subresources which include the name of the parent + // resource in the path. + Create(ctx api.Context, name string, obj runtime.Object) (runtime.Object, error) +} + // Updater is an object that can update an instance of a RESTful object. type Updater interface { // New returns an empty object that can be used with Update after request data has been put into it. diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index b0c902c7ea2..f911abe0af1 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -130,6 +130,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag // what verbs are supported by the storage, used to know what verbs we support per path creater, isCreater := storage.(rest.Creater) + namedCreater, isNamedCreater := storage.(rest.NamedCreater) lister, isLister := storage.(rest.Lister) getter, isGetter := storage.(rest.Getter) getterWithOptions, isGetterWithOptions := storage.(rest.GetterWithOptions) @@ -145,6 +146,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag storageMeta = defaultStorageMetadata{} } + if isNamedCreater { + isCreater = true + } + var versionedList interface{} if isLister { list := lister.NewList() @@ -436,7 +441,13 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag addParams(route, action.Params) ws.Route(route) case "POST": // Create a resource. - route := ws.POST(action.Path).To(CreateResource(creater, reqScope, a.group.Typer, admit)). + var handler restful.RouteFunction + if isNamedCreater { + handler = CreateNamedResource(namedCreater, reqScope, a.group.Typer, admit) + } else { + handler = CreateResource(creater, reqScope, a.group.Typer, admit) + } + route := ws.POST(action.Path).To(handler). Filter(m). Doc("create a "+kind). Operation("create"+kind). diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index b2c4a2abbe5..1dd95c3f2cf 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -531,6 +531,25 @@ func (r *GetWithOptionsRESTStorage) NewGetOptions() (runtime.Object, bool, strin var _ rest.GetterWithOptions = &GetWithOptionsRESTStorage{} +type NamedCreaterRESTStorage struct { + *SimpleRESTStorage + createdName string +} + +func (storage *NamedCreaterRESTStorage) Create(ctx api.Context, name string, obj runtime.Object) (runtime.Object, error) { + storage.checkContext(ctx) + storage.created = obj.(*Simple) + storage.createdName = name + if err := storage.errors["create"]; err != nil { + return nil, err + } + var err error + if storage.injectedFunction != nil { + obj, err = storage.injectedFunction(obj) + } + return obj, err +} + func extractBody(response *http.Response, object runtime.Object) (string, error) { defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) @@ -1810,6 +1829,32 @@ func TestCreateChecksDecode(t *testing.T) { } } +func TestCreateWithName(t *testing.T) { + pathName := "helloworld" + storage := &NamedCreaterRESTStorage{SimpleRESTStorage: &SimpleRESTStorage{}} + handler := handle(map[string]rest.Storage{"simple/sub": storage}) + server := httptest.NewServer(handler) + defer server.Close() + client := http.Client{} + + simple := &Simple{Other: "foo"} + data, _ := codec.Encode(simple) + request, err := http.NewRequest("POST", server.URL+"/api/version/simple/"+pathName+"/sub", bytes.NewBuffer(data)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusCreated { + t.Errorf("Unexpected response %#v", response) + } + if storage.createdName != pathName { + t.Errorf("Did not get expected name in create context. Got: %s, Expected: %s", storage.createdName, pathName) + } +} + func TestUpdateChecksDecode(t *testing.T) { handler := handle(map[string]rest.Storage{"simple": &SimpleRESTStorage{}}) server := httptest.NewServer(handler) diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 4655d2e6a8f..7d0b4f2a473 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -256,19 +256,27 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch } } -// CreateResource returns a function that will handle a resource creation. -func CreateResource(r rest.Creater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction { +func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface, includeName bool) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter // TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer) timeout := parseTimeout(req.Request.URL.Query().Get("timeout")) - namespace, err := scope.Namer.Namespace(req) + var ( + namespace, name string + err error + ) + if includeName { + namespace, name, err = scope.Namer.Name(req) + } else { + namespace, err = scope.Namer.Namespace(req) + } if err != nil { errorJSON(err, scope.Codec, w) return } + ctx := scope.ContextFunc(req) ctx = api.WithNamespace(ctx, namespace) @@ -292,7 +300,7 @@ func CreateResource(r rest.Creater, scope RequestScope, typer runtime.ObjectType } result, err := finishRequest(timeout, func() (runtime.Object, error) { - out, err := r.Create(ctx, obj) + out, err := r.Create(ctx, name, obj) if status, ok := out.(*api.Status); ok && err == nil && status.Code == 0 { status.Code = http.StatusCreated } @@ -312,6 +320,24 @@ func CreateResource(r rest.Creater, scope RequestScope, typer runtime.ObjectType } } +// CreateNamedResource returns a function that will handle a resource creation with name. +func CreateNamedResource(r rest.NamedCreater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction { + return createHandler(r, scope, typer, admit, true) +} + +// CreateResource returns a function that will handle a resource creation. +func CreateResource(r rest.Creater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction { + return createHandler(&namedCreaterAdapter{r}, scope, typer, admit, false) +} + +type namedCreaterAdapter struct { + rest.Creater +} + +func (c *namedCreaterAdapter) Create(ctx api.Context, name string, obj runtime.Object) (runtime.Object, error) { + return c.Creater.Create(ctx, obj) +} + // PatchResource returns a function that will handle a resource patch // TODO: Eventually PatchResource should just use GuaranteedUpdate and this routine should be a bit cleaner func PatchResource(r rest.Patcher, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface, converter runtime.ObjectConvertor) restful.RouteFunction {