Skip to content

Commit

Permalink
feat(ws): Notebooks 2.0 // Backend // CRUD Workspaces API
Browse files Browse the repository at this point in the history
In this PR:
- Created handlers and repositories for create, get and delete workspace
- Improved the type of our json response

Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com>
  • Loading branch information
ederign committed Oct 10, 2024
1 parent 3d2dac8 commit 8b2c1b2
Show file tree
Hide file tree
Showing 8 changed files with 487 additions and 57 deletions.
67 changes: 48 additions & 19 deletions workspaces/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,62 @@ make run PORT=8000
```
### Endpoints

| URL Pattern | Handler | Action |
|------------------------------------------|----------------------|-----------------------------------------|
| GET /v1/healthcheck | HealthcheckHandler | Show application information. |
| GET /v1/workspaces | GetWorkspacesHandler | Get all Workspaces |
| GET /v1/workspaces/{namespace} | GetWorkspacesHandler | Get all Workspaces from a namespace |
| POST /v1/workspaces/{namespace} | TBD | Create a Workspace in a given namespace |
| GET /v1/workspaces/{namespace}/{name} | TBD | Get a Workspace entity |
| PATCH /v1/workspaces/{namespace}/{name} | TBD | Patch a Workspace entity |
| PUT /v1/workspaces/{namespace}/{name} | TBD | Update a Workspace entity |
| DELETE /v1/workspaces/{namespace}/{name} | TBD | Delete a Workspace entity |
| GET /v1/workspacekinds | TBD | Get all WorkspaceKind |
| POST /v1/workspacekinds | TBD | Create a WorkspaceKind |
| GET /v1/workspacekinds/{name} | TBD | Get a WorkspaceKind entity |
| PATCH /v1/workspacekinds/{name} | TBD | Patch a WorkspaceKind entity |
| PUT /v1/workspacekinds/{name} | TBD | Update a WorkspaceKind entity |
| DELETE /v1/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity |
| URL Pattern | Handler | Action |
|----------------------------------------------|----------------------|-----------------------------------------|
| GET /api/v1/healthcheck | HealthcheckHandler | Show application information. |
| GET /api/v1/workspaces | GetWorkspacesHandler | Get all Workspaces |
| GET /api/v1/workspaces/{namespace} | GetWorkspacesHandler | Get all Workspaces from a namespace |
| POST /api/v1/workspaces/{namespace} | GetWorkspacesHandler | Create a Workspace in a given namespace |
| GET /api/v1/workspaces/{namespace}/{name} | GetWorkspacesHandler | Get a Workspace entity |
| PATCH /api/v1/workspaces/{namespace}/{name} | TBD | Patch a Workspace entity |
| PUT /api/v1/workspaces/{namespace}/{name} | TBD | Update a Workspace entity |
| DELETE /api/v1/workspaces/{namespace}/{name} | GetWorkspacesHandler | Delete a Workspace entity |
| GET /api/v1/workspacekinds | TBD | Get all WorkspaceKind |
| POST /api/v1/workspacekinds | TBD | Create a WorkspaceKind |
| GET /api/v1/workspacekinds/{name} | TBD | Get a WorkspaceKind entity |
| PATCH /api/v1/workspacekinds/{name} | TBD | Patch a WorkspaceKind entity |
| PUT /api/v1/workspacekinds/{name} | TBD | Update a WorkspaceKind entity |
| DELETE /api/v1/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity |

### Sample local calls
```
# GET /v1/healthcheck
# GET /api/v1/healthcheck
curl -i localhost:4000/api/v1/healthcheck
```
```
# GET /v1/workspaces/
# GET /api/v1/workspaces/
curl -i localhost:4000/api/v1/workspaces
```
```
# GET /v1/workspaces/{namespace}
# GET /api/v1/workspaces/{namespace}
curl -i localhost:4000/api/v1/workspaces/default
```
```
# POST /api/v1/workspaces/{namespace}
curl -X POST http://localhost:4000/api/v1/workspaces/default \
-H "Content-Type: application/json" \
-d '{
"name": "dora",
"paused": false,
"defer_updates": false,
"kind": "jupyterlab",
"image_config": "jupyterlab_scipy_190",
"pod_config": "tiny_cpu",
"home_volume": "workspace-home-bella",
"data_volumes": [
{
"pvc_name": "workspace-data-bella",
"mount_path": "/data/my-data",
"read_only": false
}
]
}'
```
```
# GET /api/v1/workspaces/{namespace}/{name}
curl -i localhost:4000/api/v1/workspaces/default/dora
```
```
# DELETE /api/v1/workspaces/{namespace}/{name}
curl -X DELETE localhost:4000/api/v1/workspaces/workspace-test/dora
```
8 changes: 8 additions & 0 deletions workspaces/backend/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const (
AllWorkspacesPath = PathPrefix + "/workspaces"
NamespacePathParam = "namespace"
WorkspacesByNamespacePath = AllWorkspacesPath + "/:" + NamespacePathParam

WorkspaceNamePathParam = "name"
WorkspacesByNamePath = AllWorkspacesPath + "/:" + NamespacePathParam + "/:" + WorkspaceNamePathParam
)

type App struct {
Expand Down Expand Up @@ -66,8 +69,13 @@ func (a *App) Routes() http.Handler {
router.MethodNotAllowed = http.HandlerFunc(a.methodNotAllowedResponse)

router.GET(HealthCheckPath, a.HealthcheckHandler)

router.GET(AllWorkspacesPath, a.GetWorkspacesHandler)
router.GET(WorkspacesByNamespacePath, a.GetWorkspacesHandler)

router.GET(WorkspacesByNamePath, a.GetWorkspaceHandler)
router.POST(WorkspacesByNamespacePath, a.CreateWorkspaceHandler)
router.DELETE(WorkspacesByNamePath, a.DeleteWorkspaceHandler)

return a.RecoverPanic(a.enableCORS(router))
}
6 changes: 5 additions & 1 deletion workspaces/backend/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type ErrorResponse struct {
Message string `json:"message"`
}

type ErrorEnvelope struct {
Error *HTTPError `json:"error"`
}

func (a *App) LogError(r *http.Request, err error) {
var (
method = r.Method
Expand All @@ -56,7 +60,7 @@ func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err err

func (a *App) errorResponse(w http.ResponseWriter, r *http.Request, error *HTTPError) {

env := Envelope{"error": error}
env := ErrorEnvelope{Error: error}

err := a.WriteJSON(w, error.StatusCode, env, nil)

Expand Down
4 changes: 3 additions & 1 deletion workspaces/backend/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import (
"strings"
)

type Envelope map[string]interface{}
type Envelope[D any] struct {
Data D `json:"data"`
}

func (a *App) WriteJSON(w http.ResponseWriter, status int, data any, headers http.Header) error {

Expand Down
111 changes: 109 additions & 2 deletions workspaces/backend/api/workspaces_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,58 @@ limitations under the License.
package api

import (
"encoding/json"
"errors"
"fmt"
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
"net/http"

"github.com/julienschmidt/httprouter"
)

type WorkspacesEnvelope Envelope[[]models.WorkspaceModel]
type WorkspaceEnvelope Envelope[models.WorkspaceModel]

func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {

namespace := ps.ByName(NamespacePathParam)
workspaceName := ps.ByName(WorkspaceNamePathParam)

var workspace models.WorkspaceModel
var err error
if namespace == "" {
a.serverErrorResponse(w, r, fmt.Errorf("namespace is nil"))
return
}
if workspaceName == "" {
a.serverErrorResponse(w, r, fmt.Errorf("workspaceName is nil"))
return
}

workspace, err = a.repositories.Workspace.GetWorkspace(r.Context(), namespace, workspaceName)

if err != nil {
if errors.Is(err, repositories.ErrWorkspaceNotFound) {
a.notFoundResponse(w, r)
return
}
a.serverErrorResponse(w, r, err)
return
}

modelRegistryRes := WorkspaceEnvelope{
Data: workspace,
}

err = a.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)

if err != nil {
a.serverErrorResponse(w, r, err)
}

}

func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {

namespace := ps.ByName(NamespacePathParam)
Expand All @@ -40,8 +86,8 @@ func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps ht
return
}

modelRegistryRes := Envelope{
"workspaces": workspaces,
modelRegistryRes := WorkspacesEnvelope{
Data: workspaces,
}

err = a.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
Expand All @@ -51,3 +97,64 @@ func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps ht
}

}

func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
namespace := ps.ByName("namespace")

if namespace == "" {
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
return
}

var workspaceModel models.WorkspaceModel
if err := json.NewDecoder(r.Body).Decode(&workspaceModel); err != nil {
a.serverErrorResponse(w, r, fmt.Errorf("error decoding JSON: %v", err))
return
}

workspaceModel.Namespace = namespace

createdWorkspace, err := a.repositories.Workspace.CreateWorkspace(r.Context(), workspaceModel)
if err != nil {
a.serverErrorResponse(w, r, fmt.Errorf("error creating workspace: %v", err))
return
}

// Return created workspace as JSON
workspaceEnvelope := WorkspaceEnvelope{
Data: createdWorkspace,
}

w.Header().Set("Location", r.URL.Path)
err = a.WriteJSON(w, http.StatusCreated, workspaceEnvelope, nil)
if err != nil {
a.serverErrorResponse(w, r, fmt.Errorf("error writing JSON: %v", err))
}
}

func (a *App) DeleteWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
namespace := ps.ByName("namespace")
workspaceName := ps.ByName("name")

if namespace == "" {
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
return
}

if workspaceName == "" {
a.serverErrorResponse(w, r, fmt.Errorf("workspace name is missing"))
return
}

err := a.repositories.Workspace.DeleteWorkspace(r.Context(), namespace, workspaceName)
if err != nil {
if errors.Is(err, repositories.ErrWorkspaceNotFound) {
a.notFoundResponse(w, r)
return
}
a.serverErrorResponse(w, r, err)
return
}

w.WriteHeader(http.StatusNoContent)
}
Loading

0 comments on commit 8b2c1b2

Please sign in to comment.