Skip to content

Commit 3237336

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. - 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 3237336

File tree

9 files changed

+944
-19
lines changed

9 files changed

+944
-19
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: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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) 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+
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+
a.serverErrorResponse(w, r, err)
78+
return
79+
}
80+
81+
// Get the workspace to check its paused state
82+
workspace, err := a.repositories.Workspace.GetWorkspace(r.Context(), namespace, workspaceName)
83+
if err != nil {
84+
a.serverErrorResponse(w, r, err)
85+
return
86+
}
87+
88+
// Return 200 OK with pause state
89+
err = a.WriteJSON(w, http.StatusOK, PauseStateEnvelope{
90+
Data: &workspaces.PauseState{
91+
Namespace: namespace,
92+
WorkspaceName: workspaceName,
93+
Paused: workspace.Paused,
94+
},
95+
}, nil)
96+
if err != nil {
97+
a.serverErrorResponse(w, r, err)
98+
return
99+
}
100+
}
101+
102+
// PauseWorkspaceHandler handles the pause workspace action
103+
//
104+
// @Summary Pause workspace
105+
// @Description Pauses a workspace, stopping all associated pods.
106+
// @Tags workspaces
107+
// @Accept json
108+
// @Produce json
109+
// @Param namespace path string true "Namespace of the workspace" example(default)
110+
// @Param workspaceName path string true "Name of the workspace" example(my-workspace)
111+
// @Success 200 {object} PauseStateEnvelope "Successful action. Returns the current pause state."
112+
// @Failure 400 {object} ErrorEnvelope "Bad Request. Invalid workspace kind name format."
113+
// @Failure 401 {object} ErrorEnvelope "Unauthorized. Authentication is required."
114+
// @Failure 403 {object} ErrorEnvelope "Forbidden. User does not have permission to access the workspace."
115+
// @Failure 404 {object} ErrorEnvelope "Not Found. Workspace does not exist."
116+
// @Failure 500 {object} ErrorEnvelope "Internal server error. An unexpected error occurred on the server."
117+
// @Router /workspaces/{namespace}/{workspaceName}/actions/pause [post]
118+
// @Security ApiKeyAuth
119+
func (a *App) PauseWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
120+
a.handleWorkspaceAction(w, r, ps, a.repositories.Workspace.PauseWorkspace)
121+
}
122+
123+
// StartWorkspaceHandler handles the start workspace action
124+
//
125+
// @Summary Start workspace
126+
// @Description Starts a workspace, resuming all associated pods.
127+
// @Tags workspaces
128+
// @Accept json
129+
// @Produce json
130+
// @Param namespace path string true "Namespace of the workspace" example(default)
131+
// @Param workspaceName path string true "Name of the workspace" example(my-workspace)
132+
// @Success 200 {object} PauseStateEnvelope "Successful action. Returns the current pause state."
133+
// @Failure 400 {object} ErrorEnvelope "Bad Request. Invalid workspace kind name format."
134+
// @Failure 401 {object} ErrorEnvelope "Unauthorized. Authentication is required."
135+
// @Failure 403 {object} ErrorEnvelope "Forbidden. User does not have permission to access the workspace."
136+
// @Failure 404 {object} ErrorEnvelope "Not Found. Workspace does not exist."
137+
// @Failure 500 {object} ErrorEnvelope "Internal server error. An unexpected error occurred on the server."
138+
// @Router /workspaces/{namespace}/{workspaceName}/actions/start [post]
139+
// @Security ApiKeyAuth
140+
func (a *App) StartWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
141+
a.handleWorkspaceAction(w, r, ps, a.repositories.Workspace.StartWorkspace)
142+
}

0 commit comments

Comments
 (0)