Skip to content

Commit

Permalink
feat(ws): add WorkspaceCreate model to backend
Browse files Browse the repository at this point in the history
Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>
  • Loading branch information
thesuperzapper committed Feb 12, 2025
1 parent 6d04cff commit da20e77
Show file tree
Hide file tree
Showing 16 changed files with 430 additions and 205 deletions.
93 changes: 72 additions & 21 deletions workspaces/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,29 @@ The Kubeflow Workspaces Backend is the _backend for frontend_ (BFF) used by the
> We greatly appreciate any contributions.
# Building and Deploying

TBD

# Development

Run the following command to build the BFF:

```shell
make build
```

After building it, you can run our app with:

```shell
make run
```

If you want to use a different port:

```shell
make run PORT=8000
```

### Endpoints

| URL Pattern | Handler | Action |
Expand All @@ -43,56 +51,99 @@ make run PORT=8000
| DELETE /api/v1/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity |

### Sample local calls
```

Healthcheck:

```shell
# GET /api/v1/healthcheck
curl -i localhost:4000/api/v1/healthcheck
```
```

List all Namespaces:

```shell
# GET /api/v1/namespaces
curl -i localhost:4000/api/v1/namespaces
```
```

List all Workspaces:

```shell
# GET /api/v1/workspaces/
curl -i localhost:4000/api/v1/workspaces
```
```

List all Workspaces in a Namespace:

```shell
# GET /api/v1/workspaces/{namespace}
curl -i localhost:4000/api/v1/workspaces/default
```
```

Create a Workspace:

```shell
# POST /api/v1/workspaces/{namespace}
curl -X POST http://localhost:4000/api/v1/workspaces/default \
-H "Content-Type: application/json" \
-d '{
"data": {
"name": "dora",
"kind": "jupyterlab",
"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
"pod_template": {
"pod_metadata": {
"labels": {
"app": "dora"
},
"annotations": {
"app": "dora"
}
},
"volumes": {
"home": "workspace-home-bella",
"data": [
{
"pvc_name": "workspace-data-bella",
"mount_path": "/data/my-data",
"read_only": false
}
]
},
"options": {
"image_config": "jupyterlab_scipy_190",
"pod_config": "tiny_cpu"
}
]
}'
```
}
}
}'
```

Get a Workspace:

```shell
# GET /api/v1/workspaces/{namespace}/{name}
curl -i localhost:4000/api/v1/workspaces/default/dora
```
```

Delete a Workspace:

```shell
# DELETE /api/v1/workspaces/{namespace}/{name}
curl -X DELETE localhost:4000/api/v1/workspaces/workspace-test/dora
```
curl -X DELETE localhost:4000/api/v1/workspaces/default/dora
```

List all WorkspaceKinds:

```shell
# GET /api/v1/workspacekinds
curl -i localhost:4000/api/v1/workspacekinds
```
```

Get a WorkspaceKind:

```shell
# GET /api/v1/workspacekinds/{name}
curl -i localhost:4000/api/v1/workspacekinds/jupyterlab
```
12 changes: 6 additions & 6 deletions workspaces/backend/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,20 @@ const (
Version = "1.0.0"
PathPrefix = "/api/v1"

NamespacePathParam = "namespace"
ResourceNamePathParam = "name"

// healthcheck
HealthCheckPath = PathPrefix + "/healthcheck"

// workspaces
AllWorkspacesPath = PathPrefix + "/workspaces"
NamespacePathParam = "namespace"
WorkspaceNamePathParam = "name"
WorkspacesByNamespacePath = AllWorkspacesPath + "/:" + NamespacePathParam
WorkspacesByNamePath = AllWorkspacesPath + "/:" + NamespacePathParam + "/:" + WorkspaceNamePathParam
WorkspacesByNamePath = AllWorkspacesPath + "/:" + NamespacePathParam + "/:" + ResourceNamePathParam

// workspacekinds
AllWorkspaceKindsPath = PathPrefix + "/workspacekinds"
WorkspaceKindNamePathParam = "name"
WorkspaceKindsByNamePath = AllWorkspaceKindsPath + "/:" + WorkspaceNamePathParam
AllWorkspaceKindsPath = PathPrefix + "/workspacekinds"
WorkspaceKindsByNamePath = AllWorkspaceKindsPath + "/:" + ResourceNamePathParam

// namespaces
AllNamespacesPath = PathPrefix + "/namespaces"
Expand Down
92 changes: 54 additions & 38 deletions workspaces/backend/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package api

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
Expand All @@ -29,23 +28,25 @@ type HTTPError struct {
}

type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
Code string `json:"code"`
Message string `json:"message"`
ValidationErrors map[string]string `json:"validation_errors,omitempty"`
}

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

// LogError logs an error message with the request details.
func (a *App) LogError(r *http.Request, err error) {
var (
method = r.Method
uri = r.URL.RequestURI()
)

a.logger.Error(err.Error(), "method", method, "uri", uri)
}

// LogWarn logs a warning message with the request details.
func (a *App) LogWarn(r *http.Request, message string) {
var (
method = r.Method
Expand All @@ -55,18 +56,7 @@ func (a *App) LogWarn(r *http.Request, message string) {
a.logger.Warn(message, "method", method, "uri", uri)
}

//nolint:unused
func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
httpError := &HTTPError{
StatusCode: http.StatusBadRequest,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusBadRequest),
Message: err.Error(),
},
}
a.errorResponse(w, r, httpError)
}

// errorResponse writes an error response to the client.
func (a *App) errorResponse(w http.ResponseWriter, r *http.Request, httpError *HTTPError) {
env := ErrorEnvelope{Error: httpError}

Expand All @@ -77,6 +67,7 @@ func (a *App) errorResponse(w http.ResponseWriter, r *http.Request, httpError *H
}
}

// HTTP: 500
func (a *App) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
a.LogError(r, err)

Expand All @@ -90,28 +81,19 @@ func (a *App) serverErrorResponse(w http.ResponseWriter, r *http.Request, err er
a.errorResponse(w, r, httpError)
}

func (a *App) notFoundResponse(w http.ResponseWriter, r *http.Request) {
httpError := &HTTPError{
StatusCode: http.StatusNotFound,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusNotFound),
Message: "the requested resource could not be found",
},
}
a.errorResponse(w, r, httpError)
}

func (a *App) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
// HTTP: 400
func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
httpError := &HTTPError{
StatusCode: http.StatusMethodNotAllowed,
StatusCode: http.StatusBadRequest,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusMethodNotAllowed),
Message: fmt.Sprintf("the %s method is not supported for this resource", r.Method),
Code: strconv.Itoa(http.StatusBadRequest),
Message: err.Error(),
},
}
a.errorResponse(w, r, httpError)
}

// HTTP: 401
func (a *App) unauthorizedResponse(w http.ResponseWriter, r *http.Request) {
httpError := &HTTPError{
StatusCode: http.StatusUnauthorized,
Expand All @@ -123,6 +105,7 @@ func (a *App) unauthorizedResponse(w http.ResponseWriter, r *http.Request) {
a.errorResponse(w, r, httpError)
}

// HTTP: 403
func (a *App) forbiddenResponse(w http.ResponseWriter, r *http.Request, msg string) {
a.LogWarn(r, msg)

Expand All @@ -136,18 +119,51 @@ func (a *App) forbiddenResponse(w http.ResponseWriter, r *http.Request, msg stri
a.errorResponse(w, r, httpError)
}

//nolint:unused
func (a *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
message, err := json.Marshal(errors)
if err != nil {
message = []byte("{}")
// HTTP: 404
func (a *App) notFoundResponse(w http.ResponseWriter, r *http.Request) {
httpError := &HTTPError{
StatusCode: http.StatusNotFound,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusNotFound),
Message: "the requested resource could not be found",
},
}
a.errorResponse(w, r, httpError)
}

// HTTP: 405
func (a *App) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
httpError := &HTTPError{
StatusCode: http.StatusMethodNotAllowed,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusMethodNotAllowed),
Message: fmt.Sprintf("the %s method is not supported for this resource", r.Method),
},
}
a.errorResponse(w, r, httpError)
}

// HTTP:415
func (a *App) unsupportedMediaTypeResponse(w http.ResponseWriter, r *http.Request, err error) {
httpError := &HTTPError{
StatusCode: http.StatusUnsupportedMediaType,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusUnsupportedMediaType),
Message: err.Error(),
},
}
a.errorResponse(w, r, httpError)
}

// HTTP: 422
// validationErrors is a map of field reference to error message.
func (a *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, validationErrors map[string]string) {
httpError := &HTTPError{
StatusCode: http.StatusUnprocessableEntity,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusUnprocessableEntity),
Message: string(message),
Code: strconv.Itoa(http.StatusUnprocessableEntity),
Message: "request body was not valid",
ValidationErrors: validationErrors,
},
}
a.errorResponse(w, r, httpError)
Expand Down
2 changes: 1 addition & 1 deletion workspaces/backend/api/healthcheck_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var _ = Describe("HealthCheck Handler", func() {
defer rs.Body.Close()

By("verifying the HTTP response status code")
Expect(rs.StatusCode).To(Equal(http.StatusOK))
Expect(rs.StatusCode).To(Equal(http.StatusOK), descUnexpectedHTTPStatus, rr.Body.String())

By("reading the HTTP response body")
body, err := io.ReadAll(rs.Body)
Expand Down
Loading

0 comments on commit da20e77

Please # to comment.