Skip to content

Commit da20e77

Browse files
feat(ws): add WorkspaceCreate model to backend
Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>
1 parent 6d04cff commit da20e77

16 files changed

+430
-205
lines changed

workspaces/backend/README.md

+72-21
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,29 @@ The Kubeflow Workspaces Backend is the _backend for frontend_ (BFF) used by the
77
> We greatly appreciate any contributions.
88
99
# Building and Deploying
10+
1011
TBD
1112

1213
# Development
14+
1315
Run the following command to build the BFF:
16+
1417
```shell
1518
make build
1619
```
20+
1721
After building it, you can run our app with:
22+
1823
```shell
1924
make run
2025
```
26+
2127
If you want to use a different port:
28+
2229
```shell
2330
make run PORT=8000
2431
```
32+
2533
### Endpoints
2634

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

4553
### Sample local calls
46-
```
54+
55+
Healthcheck:
56+
57+
```shell
4758
# GET /api/v1/healthcheck
4859
curl -i localhost:4000/api/v1/healthcheck
4960
```
50-
```
61+
62+
List all Namespaces:
63+
64+
```shell
5165
# GET /api/v1/namespaces
5266
curl -i localhost:4000/api/v1/namespaces
5367
```
54-
```
68+
69+
List all Workspaces:
70+
71+
```shell
5572
# GET /api/v1/workspaces/
5673
curl -i localhost:4000/api/v1/workspaces
5774
```
58-
```
75+
76+
List all Workspaces in a Namespace:
77+
78+
```shell
5979
# GET /api/v1/workspaces/{namespace}
6080
curl -i localhost:4000/api/v1/workspaces/default
6181
```
62-
```
82+
83+
Create a Workspace:
84+
85+
```shell
6386
# POST /api/v1/workspaces/{namespace}
6487
curl -X POST http://localhost:4000/api/v1/workspaces/default \
6588
-H "Content-Type: application/json" \
6689
-d '{
90+
"data": {
6791
"name": "dora",
92+
"kind": "jupyterlab",
6893
"paused": false,
6994
"defer_updates": false,
70-
"kind": "jupyterlab",
71-
"image_config": "jupyterlab_scipy_190",
72-
"pod_config": "tiny_cpu",
73-
"home_volume": "workspace-home-bella",
74-
"data_volumes": [
75-
{
76-
"pvc_name": "workspace-data-bella",
77-
"mount_path": "/data/my-data",
78-
"read_only": false
95+
"pod_template": {
96+
"pod_metadata": {
97+
"labels": {
98+
"app": "dora"
99+
},
100+
"annotations": {
101+
"app": "dora"
102+
}
103+
},
104+
"volumes": {
105+
"home": "workspace-home-bella",
106+
"data": [
107+
{
108+
"pvc_name": "workspace-data-bella",
109+
"mount_path": "/data/my-data",
110+
"read_only": false
111+
}
112+
]
113+
},
114+
"options": {
115+
"image_config": "jupyterlab_scipy_190",
116+
"pod_config": "tiny_cpu"
79117
}
80-
]
81-
}'
82-
```
118+
}
119+
}
120+
}'
83121
```
122+
123+
Get a Workspace:
124+
125+
```shell
84126
# GET /api/v1/workspaces/{namespace}/{name}
85127
curl -i localhost:4000/api/v1/workspaces/default/dora
86128
```
87-
```
129+
130+
Delete a Workspace:
131+
132+
```shell
88133
# DELETE /api/v1/workspaces/{namespace}/{name}
89-
curl -X DELETE localhost:4000/api/v1/workspaces/workspace-test/dora
90-
```
134+
curl -X DELETE localhost:4000/api/v1/workspaces/default/dora
91135
```
136+
137+
List all WorkspaceKinds:
138+
139+
```shell
92140
# GET /api/v1/workspacekinds
93141
curl -i localhost:4000/api/v1/workspacekinds
94142
```
95-
```
143+
144+
Get a WorkspaceKind:
145+
146+
```shell
96147
# GET /api/v1/workspacekinds/{name}
97148
curl -i localhost:4000/api/v1/workspacekinds/jupyterlab
98149
```

workspaces/backend/api/app.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,20 @@ const (
3434
Version = "1.0.0"
3535
PathPrefix = "/api/v1"
3636

37+
NamespacePathParam = "namespace"
38+
ResourceNamePathParam = "name"
39+
3740
// healthcheck
3841
HealthCheckPath = PathPrefix + "/healthcheck"
3942

4043
// workspaces
4144
AllWorkspacesPath = PathPrefix + "/workspaces"
42-
NamespacePathParam = "namespace"
43-
WorkspaceNamePathParam = "name"
4445
WorkspacesByNamespacePath = AllWorkspacesPath + "/:" + NamespacePathParam
45-
WorkspacesByNamePath = AllWorkspacesPath + "/:" + NamespacePathParam + "/:" + WorkspaceNamePathParam
46+
WorkspacesByNamePath = AllWorkspacesPath + "/:" + NamespacePathParam + "/:" + ResourceNamePathParam
4647

4748
// workspacekinds
48-
AllWorkspaceKindsPath = PathPrefix + "/workspacekinds"
49-
WorkspaceKindNamePathParam = "name"
50-
WorkspaceKindsByNamePath = AllWorkspaceKindsPath + "/:" + WorkspaceNamePathParam
49+
AllWorkspaceKindsPath = PathPrefix + "/workspacekinds"
50+
WorkspaceKindsByNamePath = AllWorkspaceKindsPath + "/:" + ResourceNamePathParam
5151

5252
// namespaces
5353
AllNamespacesPath = PathPrefix + "/namespaces"

workspaces/backend/api/errors.go

+54-38
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ limitations under the License.
1717
package api
1818

1919
import (
20-
"encoding/json"
2120
"fmt"
2221
"net/http"
2322
"strconv"
@@ -29,23 +28,25 @@ type HTTPError struct {
2928
}
3029

3130
type ErrorResponse struct {
32-
Code string `json:"code"`
33-
Message string `json:"message"`
31+
Code string `json:"code"`
32+
Message string `json:"message"`
33+
ValidationErrors map[string]string `json:"validation_errors,omitempty"`
3434
}
3535

3636
type ErrorEnvelope struct {
3737
Error *HTTPError `json:"error"`
3838
}
3939

40+
// LogError logs an error message with the request details.
4041
func (a *App) LogError(r *http.Request, err error) {
4142
var (
4243
method = r.Method
4344
uri = r.URL.RequestURI()
4445
)
45-
4646
a.logger.Error(err.Error(), "method", method, "uri", uri)
4747
}
4848

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

58-
//nolint:unused
59-
func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
60-
httpError := &HTTPError{
61-
StatusCode: http.StatusBadRequest,
62-
ErrorResponse: ErrorResponse{
63-
Code: strconv.Itoa(http.StatusBadRequest),
64-
Message: err.Error(),
65-
},
66-
}
67-
a.errorResponse(w, r, httpError)
68-
}
69-
59+
// errorResponse writes an error response to the client.
7060
func (a *App) errorResponse(w http.ResponseWriter, r *http.Request, httpError *HTTPError) {
7161
env := ErrorEnvelope{Error: httpError}
7262

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

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

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

93-
func (a *App) notFoundResponse(w http.ResponseWriter, r *http.Request) {
94-
httpError := &HTTPError{
95-
StatusCode: http.StatusNotFound,
96-
ErrorResponse: ErrorResponse{
97-
Code: strconv.Itoa(http.StatusNotFound),
98-
Message: "the requested resource could not be found",
99-
},
100-
}
101-
a.errorResponse(w, r, httpError)
102-
}
103-
104-
func (a *App) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
84+
// HTTP: 400
85+
func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
10586
httpError := &HTTPError{
106-
StatusCode: http.StatusMethodNotAllowed,
87+
StatusCode: http.StatusBadRequest,
10788
ErrorResponse: ErrorResponse{
108-
Code: strconv.Itoa(http.StatusMethodNotAllowed),
109-
Message: fmt.Sprintf("the %s method is not supported for this resource", r.Method),
89+
Code: strconv.Itoa(http.StatusBadRequest),
90+
Message: err.Error(),
11091
},
11192
}
11293
a.errorResponse(w, r, httpError)
11394
}
11495

96+
// HTTP: 401
11597
func (a *App) unauthorizedResponse(w http.ResponseWriter, r *http.Request) {
11698
httpError := &HTTPError{
11799
StatusCode: http.StatusUnauthorized,
@@ -123,6 +105,7 @@ func (a *App) unauthorizedResponse(w http.ResponseWriter, r *http.Request) {
123105
a.errorResponse(w, r, httpError)
124106
}
125107

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

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

139-
//nolint:unused
140-
func (a *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
141-
message, err := json.Marshal(errors)
142-
if err != nil {
143-
message = []byte("{}")
122+
// HTTP: 404
123+
func (a *App) notFoundResponse(w http.ResponseWriter, r *http.Request) {
124+
httpError := &HTTPError{
125+
StatusCode: http.StatusNotFound,
126+
ErrorResponse: ErrorResponse{
127+
Code: strconv.Itoa(http.StatusNotFound),
128+
Message: "the requested resource could not be found",
129+
},
130+
}
131+
a.errorResponse(w, r, httpError)
132+
}
133+
134+
// HTTP: 405
135+
func (a *App) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
136+
httpError := &HTTPError{
137+
StatusCode: http.StatusMethodNotAllowed,
138+
ErrorResponse: ErrorResponse{
139+
Code: strconv.Itoa(http.StatusMethodNotAllowed),
140+
Message: fmt.Sprintf("the %s method is not supported for this resource", r.Method),
141+
},
144142
}
143+
a.errorResponse(w, r, httpError)
144+
}
145+
146+
// HTTP:415
147+
func (a *App) unsupportedMediaTypeResponse(w http.ResponseWriter, r *http.Request, err error) {
148+
httpError := &HTTPError{
149+
StatusCode: http.StatusUnsupportedMediaType,
150+
ErrorResponse: ErrorResponse{
151+
Code: strconv.Itoa(http.StatusUnsupportedMediaType),
152+
Message: err.Error(),
153+
},
154+
}
155+
a.errorResponse(w, r, httpError)
156+
}
145157

158+
// HTTP: 422
159+
// validationErrors is a map of field reference to error message.
160+
func (a *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, validationErrors map[string]string) {
146161
httpError := &HTTPError{
147162
StatusCode: http.StatusUnprocessableEntity,
148163
ErrorResponse: ErrorResponse{
149-
Code: strconv.Itoa(http.StatusUnprocessableEntity),
150-
Message: string(message),
164+
Code: strconv.Itoa(http.StatusUnprocessableEntity),
165+
Message: "request body was not valid",
166+
ValidationErrors: validationErrors,
151167
},
152168
}
153169
a.errorResponse(w, r, httpError)

workspaces/backend/api/healthcheck_handler_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ var _ = Describe("HealthCheck Handler", func() {
4646
defer rs.Body.Close()
4747

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

5151
By("reading the HTTP response body")
5252
body, err := io.ReadAll(rs.Body)

0 commit comments

Comments
 (0)