Skip to content

Commit 575e1a8

Browse files
committed
feat(ws): Implement workspace start + pause as backend APIs
related: #298 - Added StartWorkspaceHandler + PauseWorkspaceHandler to handle the respective workspace actions - Introduced new routes for starting and pausing workspaces in the API. - `api/v1/workspaces/{namespace}/{name}/actions/start` - `api/v1/workspaces/{namespace}/{name}/actions/pause` - Created a new PauseStateEnvelope type for successful responses. - Leveraging JSONPatch / client.RawPatch to ensure Workspace in "valid state" before attempting action - for `start`: `spec.paused` must be `true`, and `status.state` must be `Paused` - for `pause`: `spec.paused` must be `false` - note: I would love to have a `status.state` check here of `status.state != Paused`, but that type of comparison is not supported in [JSONPatch](https://datatracker.ietf.org/doc/html/rfc6902#section-4.6) - Added tests for the new APIs, including success and error cases. - Updated README/OpenAPI documentation to include the new endpoints. Signed-off-by: Andy Stoneberg <[email protected]>
1 parent 2c3e75e commit 575e1a8

File tree

9 files changed

+1084
-21
lines changed

9 files changed

+1084
-21
lines changed

workspaces/backend/README.md

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,31 @@ make run
2727
If you want to use a different port:
2828

2929
```shell
30-
make run PORT=8000
30+
make run PORT=8000
3131
```
3232

3333
### Endpoints
3434

35-
| URL Pattern | Handler | Action |
36-
|----------------------------------------------|------------------------|-----------------------------------------|
37-
| GET /api/v1/healthcheck | healthcheck_handler | Show application information |
38-
| GET /api/v1/namespaces | namespaces_handler | Get all Namespaces |
39-
| GET /api/v1/swagger/ | swagger_handler | Swagger API documentation |
40-
| GET /api/v1/workspaces | workspaces_handler | Get all Workspaces |
41-
| GET /api/v1/workspaces/{namespace} | workspaces_handler | Get all Workspaces from a namespace |
42-
| POST /api/v1/workspaces/{namespace} | workspaces_handler | Create a Workspace in a given namespace |
43-
| GET /api/v1/workspaces/{namespace}/{name} | workspaces_handler | Get a Workspace entity |
44-
| PATCH /api/v1/workspaces/{namespace}/{name} | TBD | Patch a Workspace entity |
45-
| PUT /api/v1/workspaces/{namespace}/{name} | TBD | Update a Workspace entity |
46-
| DELETE /api/v1/workspaces/{namespace}/{name} | workspaces_handler | Delete a Workspace entity |
47-
| GET /api/v1/workspacekinds | workspacekinds_handler | Get all WorkspaceKind |
48-
| POST /api/v1/workspacekinds | TBD | Create a WorkspaceKind |
49-
| GET /api/v1/workspacekinds/{name} | workspacekinds_handler | Get a WorkspaceKind entity |
50-
| PATCH /api/v1/workspacekinds/{name} | TBD | Patch a WorkspaceKind entity |
51-
| PUT /api/v1/workspacekinds/{name} | TBD | Update a WorkspaceKind entity |
52-
| DELETE /api/v1/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity |
35+
| URL Pattern | Handler | Action |
36+
|-----------------------------------------------------------|---------------------------|-----------------------------------------|
37+
| GET /api/v1/healthcheck | healthcheck_handler | Show application information |
38+
| GET /api/v1/namespaces | namespaces_handler | Get all Namespaces |
39+
| GET /api/v1/swagger/ | swagger_handler | Swagger API documentation |
40+
| GET /api/v1/workspaces | workspaces_handler | Get all Workspaces |
41+
| GET /api/v1/workspaces/{namespace} | workspaces_handler | Get all Workspaces from a namespace |
42+
| POST /api/v1/workspaces/{namespace} | workspaces_handler | Create a Workspace in a given namespace |
43+
| GET /api/v1/workspaces/{namespace}/{name} | workspaces_handler | Get a Workspace entity |
44+
| PATCH /api/v1/workspaces/{namespace}/{name} | TBD | Patch a Workspace entity |
45+
| PUT /api/v1/workspaces/{namespace}/{name} | TBD | Update a Workspace entity |
46+
| DELETE /api/v1/workspaces/{namespace}/{name} | workspaces_handler | Delete a Workspace entity |
47+
| POST /api/v1/workspaces/{namespace}/{name}/actions/start | workspace_actions_handler | Start a paused workspace |
48+
| POST /api/v1/workspaces/{namespace}/{name}/actions/pause | workspace_actions_handler | Pause a running workspace |
49+
| GET /api/v1/workspacekinds | workspacekinds_handler | Get all WorkspaceKind |
50+
| POST /api/v1/workspacekinds | TBD | Create a WorkspaceKind |
51+
| GET /api/v1/workspacekinds/{name} | workspacekinds_handler | Get a WorkspaceKind entity |
52+
| PATCH /api/v1/workspacekinds/{name} | TBD | Patch a WorkspaceKind entity |
53+
| PUT /api/v1/workspacekinds/{name} | TBD | Update a WorkspaceKind entity |
54+
| DELETE /api/v1/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity |
5355

5456
### Sample local calls
5557

@@ -128,6 +130,20 @@ Get a Workspace:
128130
curl -i localhost:4000/api/v1/workspaces/default/dora
129131
```
130132

133+
Pause a Workspace:
134+
135+
```shell
136+
# POST /api/v1/workspaces/{namespace}/{name}/actions/pause
137+
curl -X POST localhost:4000/api/v1/workspaces/default/dora/actions/pause
138+
```
139+
140+
Start a Workspace:
141+
142+
```shell
143+
# POST /api/v1/workspaces/{namespace}/{name}/actions/start
144+
curl -X POST localhost:4000/api/v1/workspaces/default/dora/actions/start
145+
```
146+
131147
Delete a Workspace:
132148

133149
```shell

workspaces/backend/api/app.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ const (
4545
AllWorkspacesPath = PathPrefix + "/workspaces"
4646
WorkspacesByNamespacePath = AllWorkspacesPath + "/:" + NamespacePathParam
4747
WorkspacesByNamePath = AllWorkspacesPath + "/:" + NamespacePathParam + "/:" + ResourceNamePathParam
48+
WorkspaceActionsPath = WorkspacesByNamePath + "/actions"
49+
PauseWorkspacePath = WorkspaceActionsPath + "/pause"
50+
StartWorkspacePath = WorkspaceActionsPath + "/start"
4851

4952
// workspacekinds
5053
AllWorkspaceKindsPath = PathPrefix + "/workspacekinds"
@@ -102,6 +105,8 @@ func (a *App) Routes() http.Handler {
102105
router.GET(WorkspacesByNamePath, a.GetWorkspaceHandler)
103106
router.POST(WorkspacesByNamespacePath, a.CreateWorkspaceHandler)
104107
router.DELETE(WorkspacesByNamePath, a.DeleteWorkspaceHandler)
108+
router.POST(PauseWorkspacePath, a.PauseWorkspaceHandler)
109+
router.POST(StartWorkspacePath, a.StartWorkspaceHandler)
105110

106111
// workspacekinds
107112
router.GET(AllWorkspaceKindsPath, a.GetWorkspaceKindsHandler)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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 (
20+
"context"
21+
"errors"
22+
"net/http"
23+
24+
"github.com/julienschmidt/httprouter"
25+
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/util/validation/field"
28+
29+
"github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
30+
"github.com/kubeflow/notebooks/workspaces/backend/internal/helper"
31+
"github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces"
32+
repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces"
33+
)
34+
35+
type PauseStateEnvelope Envelope[*workspaces.PauseState]
36+
37+
// workspaceActionFunc represents a function that performs an action on a workspace
38+
type workspaceActionFunc func(ctx context.Context, namespace, name string) (*kubefloworgv1beta1.Workspace, error)
39+
40+
// handleWorkspaceAction is a helper function that handles common logic for workspace actions
41+
func (a *App) handleWorkspaceAction(w http.ResponseWriter, r *http.Request, ps httprouter.Params, action workspaceActionFunc) {
42+
namespace := ps.ByName(NamespacePathParam)
43+
workspaceName := ps.ByName(ResourceNamePathParam)
44+
45+
// validate path parameters
46+
var valErrs field.ErrorList
47+
valErrs = append(valErrs, helper.ValidateFieldIsDNS1123Subdomain(field.NewPath(NamespacePathParam), namespace)...)
48+
valErrs = append(valErrs, helper.ValidateFieldIsDNS1123Subdomain(field.NewPath(ResourceNamePathParam), workspaceName)...)
49+
if len(valErrs) > 0 {
50+
a.failedValidationResponse(w, r, errMsgPathParamsInvalid, valErrs, nil)
51+
return
52+
}
53+
54+
// Authorization check
55+
authPolicies := []*auth.ResourcePolicy{
56+
auth.NewResourcePolicy(
57+
auth.ResourceVerbUpdate,
58+
&kubefloworgv1beta1.Workspace{
59+
ObjectMeta: metav1.ObjectMeta{
60+
Namespace: namespace,
61+
Name: workspaceName,
62+
},
63+
},
64+
),
65+
}
66+
if success := a.requireAuth(w, r, authPolicies); !success {
67+
return
68+
}
69+
70+
// Execute the workspace action
71+
workspace, err := action(r.Context(), namespace, workspaceName)
72+
if err != nil {
73+
if errors.Is(err, repository.ErrWorkspaceNotFound) {
74+
a.notFoundResponse(w, r)
75+
return
76+
}
77+
if errors.Is(err, repository.ErrWorkspaceInvalidState) {
78+
a.failedValidationResponse(w, r, err.Error(), nil, nil)
79+
return
80+
}
81+
a.serverErrorResponse(w, r, err)
82+
return
83+
}
84+
85+
// Return 200 OK with pause state
86+
err = a.WriteJSON(w, http.StatusOK, PauseStateEnvelope{
87+
Data: &workspaces.PauseState{
88+
Namespace: namespace,
89+
WorkspaceName: workspaceName,
90+
Paused: *workspace.Spec.Paused,
91+
},
92+
}, nil)
93+
if err != nil {
94+
a.serverErrorResponse(w, r, err)
95+
return
96+
}
97+
}
98+
99+
// PauseWorkspaceHandler handles the pause workspace action
100+
//
101+
// @Summary Pause workspace
102+
// @Description Pauses a workspace, stopping all associated pods.
103+
// @Tags workspaces
104+
// @Accept json
105+
// @Produce json
106+
// @Param namespace path string true "Namespace of the workspace" example(default)
107+
// @Param workspaceName path string true "Name of the workspace" example(my-workspace)
108+
// @Success 200 {object} PauseStateEnvelope "Successful action. Returns the current pause state."
109+
// @Failure 400 {object} ErrorEnvelope "Bad Request. Invalid workspace kind name format."
110+
// @Failure 401 {object} ErrorEnvelope "Unauthorized. Authentication is required."
111+
// @Failure 403 {object} ErrorEnvelope "Forbidden. User does not have permission to access the workspace."
112+
// @Failure 404 {object} ErrorEnvelope "Not Found. Workspace does not exist."
113+
// @Failure 422 {object} ErrorEnvelope "Unprocessable Entity. Workspace is not in running state."
114+
// @Failure 500 {object} ErrorEnvelope "Internal server error. An unexpected error occurred on the server."
115+
// @Router /workspaces/{namespace}/{workspaceName}/actions/pause [post]
116+
// @Security ApiKeyAuth
117+
func (a *App) PauseWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
118+
a.handleWorkspaceAction(w, r, ps, a.repositories.Workspace.PauseWorkspace)
119+
}
120+
121+
// StartWorkspaceHandler handles the start workspace action
122+
//
123+
// @Summary Start workspace
124+
// @Description Starts a workspace, resuming all associated pods.
125+
// @Tags workspaces
126+
// @Accept json
127+
// @Produce json
128+
// @Param namespace path string true "Namespace of the workspace" example(default)
129+
// @Param workspaceName path string true "Name of the workspace" example(my-workspace)
130+
// @Success 200 {object} PauseStateEnvelope "Successful action. Returns the current pause state."
131+
// @Failure 400 {object} ErrorEnvelope "Bad Request. Invalid workspace kind name format."
132+
// @Failure 401 {object} ErrorEnvelope "Unauthorized. Authentication is required."
133+
// @Failure 403 {object} ErrorEnvelope "Forbidden. User does not have permission to access the workspace."
134+
// @Failure 404 {object} ErrorEnvelope "Not Found. Workspace does not exist."
135+
// @Failure 422 {object} ErrorEnvelope "Unprocessable Entity. Workspace is not in paused state."
136+
// @Failure 500 {object} ErrorEnvelope "Internal server error. An unexpected error occurred on the server."
137+
// @Router /workspaces/{namespace}/{workspaceName}/actions/start [post]
138+
// @Security ApiKeyAuth
139+
func (a *App) StartWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
140+
a.handleWorkspaceAction(w, r, ps, a.repositories.Workspace.StartWorkspace)
141+
}

0 commit comments

Comments
 (0)