Skip to content

Commit e5d4e41

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

22 files changed

+789
-284
lines changed

workspaces/backend/README.md

Lines changed: 72 additions & 21 deletions
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

Lines changed: 6 additions & 6 deletions
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/healthcheck_handler_test.go

Lines changed: 1 addition & 1 deletion
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)

workspaces/backend/api/helpers.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,20 @@ package api
1818

1919
import (
2020
"encoding/json"
21+
"fmt"
22+
"mime"
2123
"net/http"
24+
"strings"
2225
)
2326

27+
// Envelope is the body of all requests and responses that contain data.
28+
// NOTE: error responses use the ErrorEnvelope type
2429
type Envelope[D any] struct {
30+
// TODO: make all declarations of Envelope use pointers for D
2531
Data D `json:"data"`
2632
}
2733

34+
// WriteJSON writes a JSON response with the given status code, data, and headers.
2835
func (a *App) WriteJSON(w http.ResponseWriter, status int, data any, headers http.Header) error {
2936

3037
js, err := json.MarshalIndent(data, "", "\t")
@@ -47,3 +54,42 @@ func (a *App) WriteJSON(w http.ResponseWriter, status int, data any, headers htt
4754

4855
return nil
4956
}
57+
58+
// DecodeJSON decodes the JSON request body into the given value.
59+
func (a *App) DecodeJSON(r *http.Request, v any) error {
60+
decoder := json.NewDecoder(r.Body)
61+
decoder.DisallowUnknownFields()
62+
if err := decoder.Decode(v); err != nil {
63+
return fmt.Errorf("error decoding JSON: %w", err)
64+
}
65+
return nil
66+
}
67+
68+
// ValidateContentType validates the Content-Type header of the request.
69+
// If this method returns false, the request has been handled and the caller should return immediately.
70+
// If this method returns true, the request has the correct Content-Type.
71+
func (a *App) ValidateContentType(w http.ResponseWriter, r *http.Request, expectedMediaType string) bool {
72+
contentType := r.Header.Get("Content-Type")
73+
if contentType == "" {
74+
a.unsupportedMediaTypeResponse(w, r, fmt.Errorf("Content-Type header is missing"))
75+
return false
76+
}
77+
mediaType, _, err := mime.ParseMediaType(contentType)
78+
if err != nil {
79+
a.badRequestResponse(w, r, fmt.Errorf("error parsing Content-Type header: %w", err))
80+
return false
81+
}
82+
if mediaType != expectedMediaType {
83+
a.unsupportedMediaTypeResponse(w, r, fmt.Errorf("unsupported media type: %s, expected: %s", mediaType, expectedMediaType))
84+
return false
85+
}
86+
87+
return true
88+
}
89+
90+
// LocationGetWorkspace returns the GET location (HTTP path) for a workspace resource.
91+
func (a *App) LocationGetWorkspace(namespace, name string) string {
92+
path := strings.Replace(WorkspacesByNamePath, ":"+NamespacePathParam, namespace, 1)
93+
path = strings.Replace(path, ":"+ResourceNamePathParam, name, 1)
94+
return path
95+
}

workspaces/backend/api/logging.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright 2024.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package api
18+
19+
import "net/http"
20+
21+
// LogError logs an error message with the request details.
22+
func (a *App) LogError(r *http.Request, err error) {
23+
var (
24+
method = r.Method
25+
uri = r.URL.RequestURI()
26+
)
27+
a.logger.Error(err.Error(), "method", method, "uri", uri)
28+
}
29+
30+
// LogWarn logs a warning message with the request details.
31+
func (a *App) LogWarn(r *http.Request, message string) {
32+
var (
33+
method = r.Method
34+
uri = r.URL.RequestURI()
35+
)
36+
a.logger.Warn(message, "method", method, "uri", uri)
37+
}
38+
39+
// LogInfo logs an info message with the request details.
40+
func (a *App) LogInfo(r *http.Request, message string) {
41+
var (
42+
method = r.Method
43+
uri = r.URL.RequestURI()
44+
)
45+
a.logger.Info(message, "method", method, "uri", uri)
46+
}

workspaces/backend/api/namespaces_handler.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces"
2727
)
2828

29-
type NamespacesEnvelope Envelope[[]models.Namespace]
29+
type NamespaceListEnvelope Envelope[[]models.Namespace]
3030

3131
func (a *App) GetNamespacesHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
3232

@@ -48,12 +48,6 @@ func (a *App) GetNamespacesHandler(w http.ResponseWriter, r *http.Request, _ htt
4848
return
4949
}
5050

51-
namespacesEnvelope := NamespacesEnvelope{
52-
Data: namespaces,
53-
}
54-
55-
err = a.WriteJSON(w, http.StatusOK, namespacesEnvelope, nil)
56-
if err != nil {
57-
a.serverErrorResponse(w, r, err)
58-
}
51+
responseEnvelope := &NamespaceListEnvelope{Data: namespaces}
52+
a.dataResponse(w, r, responseEnvelope)
5953
}

workspaces/backend/api/namespaces_handler_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,14 @@ var _ = Describe("Namespaces Handler", func() {
9393
defer rs.Body.Close()
9494

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

9898
By("reading the HTTP response body")
9999
body, err := io.ReadAll(rs.Body)
100100
Expect(err).NotTo(HaveOccurred())
101101

102-
By("unmarshalling the response JSON to NamespacesEnvelope")
103-
var response NamespacesEnvelope
102+
By("unmarshalling the response JSON to NamespaceListEnvelope")
103+
var response NamespaceListEnvelope
104104
err = json.Unmarshal(body, &response)
105105
Expect(err).NotTo(HaveOccurred())
106106

0 commit comments

Comments
 (0)