From 1d39e52b83278a73bc5b11e6481369ed3f21cd2a Mon Sep 17 00:00:00 2001 From: Andy Stoneberg Date: Mon, 12 May 2025 15:02:36 -0400 Subject: [PATCH] feat(ws): Implement pause workspace functionality as backend API related: #298 - Added PauseWorkspaceHandler to handle the pause operation for workspaces. - Introduced new route for pausing workspaces in the API. - Created a new EmptyResponse type for successful responses. - Added tests for the pause workspace functionality, including success and error cases. - Updated README/OpenAPI documentation to include the new pause workspace endpoint. Signed-off-by: Andy Stoneberg --- workspaces/backend/README.md | 46 +++-- workspaces/backend/api/app.go | 3 + workspaces/backend/api/helpers.go | 3 + .../backend/api/workspace_actions_handler.go | 96 ++++++++++ .../api/workspace_actions_handler_test.go | 166 ++++++++++++++++++ .../internal/repositories/workspaces/repo.go | 37 ++++ workspaces/backend/openapi/docs.go | 79 +++++++++ workspaces/backend/openapi/swagger.json | 79 +++++++++ workspaces/backend/openapi/swagger.yaml | 53 ++++++ 9 files changed, 543 insertions(+), 19 deletions(-) create mode 100644 workspaces/backend/api/workspace_actions_handler.go create mode 100644 workspaces/backend/api/workspace_actions_handler_test.go diff --git a/workspaces/backend/README.md b/workspaces/backend/README.md index f5cbd683f..20a6abb45 100644 --- a/workspaces/backend/README.md +++ b/workspaces/backend/README.md @@ -27,29 +27,30 @@ make run If you want to use a different port: ```shell -make run PORT=8000 +make run PORT=8000 ``` ### Endpoints -| URL Pattern | Handler | Action | -|----------------------------------------------|------------------------|-----------------------------------------| -| GET /api/v1/healthcheck | healthcheck_handler | Show application information | -| GET /api/v1/namespaces | namespaces_handler | Get all Namespaces | -| GET /api/v1/swagger/ | swagger_handler | Swagger API documentation | -| GET /api/v1/workspaces | workspaces_handler | Get all Workspaces | -| GET /api/v1/workspaces/{namespace} | workspaces_handler | Get all Workspaces from a namespace | -| POST /api/v1/workspaces/{namespace} | workspaces_handler | Create a Workspace in a given namespace | -| GET /api/v1/workspaces/{namespace}/{name} | workspaces_handler | Get a Workspace entity | -| PATCH /api/v1/workspaces/{namespace}/{name} | TBD | Patch a Workspace entity | -| PUT /api/v1/workspaces/{namespace}/{name} | TBD | Update a Workspace entity | -| DELETE /api/v1/workspaces/{namespace}/{name} | workspaces_handler | Delete a Workspace entity | -| GET /api/v1/workspacekinds | workspacekinds_handler | Get all WorkspaceKind | -| POST /api/v1/workspacekinds | TBD | Create a WorkspaceKind | -| GET /api/v1/workspacekinds/{name} | workspacekinds_handler | Get a WorkspaceKind entity | -| PATCH /api/v1/workspacekinds/{name} | TBD | Patch a WorkspaceKind entity | -| PUT /api/v1/workspacekinds/{name} | TBD | Update a WorkspaceKind entity | -| DELETE /api/v1/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity | +| URL Pattern | Handler | Action | +|-----------------------------------------------------------|---------------------------|-----------------------------------------| +| GET /api/v1/healthcheck | healthcheck_handler | Show application information | +| GET /api/v1/namespaces | namespaces_handler | Get all Namespaces | +| GET /api/v1/swagger/ | swagger_handler | Swagger API documentation | +| GET /api/v1/workspaces | workspaces_handler | Get all Workspaces | +| GET /api/v1/workspaces/{namespace} | workspaces_handler | Get all Workspaces from a namespace | +| POST /api/v1/workspaces/{namespace} | workspaces_handler | Create a Workspace in a given namespace | +| GET /api/v1/workspaces/{namespace}/{name} | workspaces_handler | Get a Workspace entity | +| PATCH /api/v1/workspaces/{namespace}/{name} | TBD | Patch a Workspace entity | +| PUT /api/v1/workspaces/{namespace}/{name} | TBD | Update a Workspace entity | +| DELETE /api/v1/workspaces/{namespace}/{name} | workspaces_handler | Delete a Workspace entity | +| POST /api/v1/workspaces/{namespace}/{name}/actions/pause | workspace_actions_handler | Pause a running workspace | +| GET /api/v1/workspacekinds | workspacekinds_handler | Get all WorkspaceKind | +| POST /api/v1/workspacekinds | TBD | Create a WorkspaceKind | +| GET /api/v1/workspacekinds/{name} | workspacekinds_handler | Get a WorkspaceKind entity | +| PATCH /api/v1/workspacekinds/{name} | TBD | Patch a WorkspaceKind entity | +| PUT /api/v1/workspacekinds/{name} | TBD | Update a WorkspaceKind entity | +| DELETE /api/v1/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity | ### Sample local calls @@ -128,6 +129,13 @@ Get a Workspace: curl -i localhost:4000/api/v1/workspaces/default/dora ``` +Pause a Workspace: + +```shell +# POST /api/v1/workspaces/{namespace}/{name}/actions/pause +curl -X POST localhost:4000/api/v1/workspaces/default/dora/actions/pause +``` + Delete a Workspace: ```shell diff --git a/workspaces/backend/api/app.go b/workspaces/backend/api/app.go index ef3fe7db1..e0c81816b 100644 --- a/workspaces/backend/api/app.go +++ b/workspaces/backend/api/app.go @@ -45,6 +45,8 @@ const ( AllWorkspacesPath = PathPrefix + "/workspaces" WorkspacesByNamespacePath = AllWorkspacesPath + "/:" + NamespacePathParam WorkspacesByNamePath = AllWorkspacesPath + "/:" + NamespacePathParam + "/:" + ResourceNamePathParam + WorkspaceActionsPath = WorkspacesByNamePath + "/actions" + PauseWorkspacePath = WorkspaceActionsPath + "/pause" // workspacekinds AllWorkspaceKindsPath = PathPrefix + "/workspacekinds" @@ -102,6 +104,7 @@ func (a *App) Routes() http.Handler { router.GET(WorkspacesByNamePath, a.GetWorkspaceHandler) router.POST(WorkspacesByNamespacePath, a.CreateWorkspaceHandler) router.DELETE(WorkspacesByNamePath, a.DeleteWorkspaceHandler) + router.POST(PauseWorkspacePath, a.PauseWorkspaceHandler) // workspacekinds router.GET(AllWorkspaceKindsPath, a.GetWorkspaceKindsHandler) diff --git a/workspaces/backend/api/helpers.go b/workspaces/backend/api/helpers.go index 0076a0604..99adfc44c 100644 --- a/workspaces/backend/api/helpers.go +++ b/workspaces/backend/api/helpers.go @@ -32,6 +32,9 @@ type Envelope[D any] struct { Data D `json:"data"` } +// EmptyResponse represents an empty JSON response +type EmptyResponse struct{} + // WriteJSON writes a JSON response with the given status code, data, and headers. func (a *App) WriteJSON(w http.ResponseWriter, status int, data any, headers http.Header) error { diff --git a/workspaces/backend/api/workspace_actions_handler.go b/workspaces/backend/api/workspace_actions_handler.go new file mode 100644 index 000000000..79a60997c --- /dev/null +++ b/workspaces/backend/api/workspace_actions_handler.go @@ -0,0 +1,96 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "errors" + "net/http" + + "github.com/julienschmidt/httprouter" + kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/kubeflow/notebooks/workspaces/backend/internal/auth" + "github.com/kubeflow/notebooks/workspaces/backend/internal/helper" + repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces" +) + +// PauseWorkspaceHandler handles the pause workspace action +// +// @Summary Pause workspace +// @Description Pauses a workspace, stopping all associated pods. +// @Tags workspaces +// @Accept json +// @Produce json +// @Param namespace path string true "Namespace of the workspace" example(default) +// @Param workspaceName path string true "Name of the workspace" example(my-workspace) +// @Success 200 {object} EmptyResponse "Successful action. Returns an empty JSON object." +// @Failure 400 {object} ErrorEnvelope "Bad Request. Invalid workspace kind name format." +// @Failure 401 {object} ErrorEnvelope "Unauthorized. Authentication is required." +// @Failure 403 {object} ErrorEnvelope "Forbidden. User does not have permission to access the workspace." +// @Failure 404 {object} ErrorEnvelope "Not Found. Workspace does not exist." +// @Failure 500 {object} ErrorEnvelope "Internal server error. An unexpected error occurred on the server." +// @Router /workspaces/{namespace}/{workspaceName}/actions/pause [post] +// @Security ApiKeyAuth +func (a *App) PauseWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + namespace := ps.ByName(NamespacePathParam) + workspaceName := ps.ByName(ResourceNamePathParam) + + // validate path parameters + var valErrs field.ErrorList + valErrs = append(valErrs, helper.ValidateFieldIsDNS1123Subdomain(field.NewPath(NamespacePathParam), namespace)...) + valErrs = append(valErrs, helper.ValidateFieldIsDNS1123Subdomain(field.NewPath(ResourceNamePathParam), workspaceName)...) + if len(valErrs) > 0 { + a.failedValidationResponse(w, r, errMsgPathParamsInvalid, valErrs, nil) + return + } + + // =========================== AUTH =========================== + authPolicies := []*auth.ResourcePolicy{ + auth.NewResourcePolicy( + auth.ResourceVerbUpdate, + &kubefloworgv1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: workspaceName, + }, + }, + ), + } + if success := a.requireAuth(w, r, authPolicies); !success { + return + } + // ============================================================ + + err := a.repositories.Workspace.PauseWorkspace(r.Context(), namespace, workspaceName) + if err != nil { + if errors.Is(err, repository.ErrWorkspaceNotFound) { + a.notFoundResponse(w, r) + return + } + a.serverErrorResponse(w, r, err) + return + } + + // Return 200 OK with empty JSON object + err = a.WriteJSON(w, http.StatusOK, EmptyResponse{}, nil) + if err != nil { + a.serverErrorResponse(w, r, err) + return + } +} diff --git a/workspaces/backend/api/workspace_actions_handler_test.go b/workspaces/backend/api/workspace_actions_handler_test.go new file mode 100644 index 000000000..a4be75053 --- /dev/null +++ b/workspaces/backend/api/workspace_actions_handler_test.go @@ -0,0 +1,166 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + + "github.com/julienschmidt/httprouter" + kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" +) + +var _ = Describe("Workspace Actions Handler", func() { + + // NOTE: the tests in this context work on the same resources, they must be run in order. + // also, they assume a specific state of the cluster, so cannot be run in parallel with other tests. + // therefore, we run them using the `Ordered` and `Serial` Ginkgo decorators. + Context("with existing Workspaces", Serial, Ordered, func() { + + const namespaceName1 = "ws-ops-ns1" + + var ( + workspaceName1 string + workspaceKey1 types.NamespacedName + workspaceKindName string + ) + + BeforeAll(func() { + uniqueName := "ws-ops-test" + workspaceName1 = fmt.Sprintf("workspace-1-%s", uniqueName) + workspaceKey1 = types.NamespacedName{Name: workspaceName1, Namespace: namespaceName1} + workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName) + + By("creating Namespace 1") + namespace1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName1, + }, + } + Expect(k8sClient.Create(ctx, namespace1)).To(Succeed()) + + By("creating a WorkspaceKind") + workspaceKind := NewExampleWorkspaceKind(workspaceKindName) + Expect(k8sClient.Create(ctx, workspaceKind)).To(Succeed()) + + By("creating Workspace 1 in Namespace 1") + workspace1 := NewExampleWorkspace(workspaceName1, namespaceName1, workspaceKindName) + Expect(k8sClient.Create(ctx, workspace1)).To(Succeed()) + }) + + AfterAll(func() { + By("deleting Workspace 1 from Namespace 1") + workspace1 := &kubefloworgv1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Name: workspaceName1, + Namespace: namespaceName1, + }, + } + Expect(k8sClient.Delete(ctx, workspace1)).To(Succeed()) + + By("deleting WorkspaceKind") + workspaceKind := &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: workspaceKindName, + }, + } + Expect(k8sClient.Delete(ctx, workspaceKind)).To(Succeed()) + + By("deleting Namespace 1") + namespace1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName1, + }, + } + Expect(k8sClient.Delete(ctx, namespace1)).To(Succeed()) + }) + + It("should pause a workspace successfully", func() { + By("creating the HTTP request") + path := strings.Replace(PauseWorkspacePath, ":"+NamespacePathParam, namespaceName1, 1) + path = strings.Replace(path, ":"+ResourceNamePathParam, workspaceName1, 1) + req, err := http.NewRequest(http.MethodPost, path, http.NoBody) + Expect(err).NotTo(HaveOccurred()) + + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + req.Header.Set("Content-Type", "application/merge-patch+json") + + By("executing PauseWorkspaceHandler") + ps := httprouter.Params{ + httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, + httprouter.Param{Key: ResourceNamePathParam, Value: workspaceName1}, + } + rr := httptest.NewRecorder() + a.PauseWorkspaceHandler(rr, req, ps) + rs := rr.Result() + defer rs.Body.Close() + + By("verifying the HTTP response status code") + Expect(rs.StatusCode).To(Equal(http.StatusOK), descUnexpectedHTTPStatus, rr.Body.String()) + + By("reading the HTTP response body") + body, err := io.ReadAll(rs.Body) + Expect(err).NotTo(HaveOccurred()) + + By("verifying the response is an empty JSON object") + Expect(string(body)).To(Equal("{}\n")) + + By("getting the Workspace from the Kubernetes API") + workspace := &kubefloworgv1beta1.Workspace{} + Expect(k8sClient.Get(ctx, workspaceKey1, workspace)).To(Succeed()) + + By("ensuring the workspace is paused") + Expect(workspace.Spec.Paused).To(Equal(ptr.To(true))) + }) + + It("should return 404 for a non-existent workspace", func() { + missingWorkspaceName := "non-existent-workspace" + + By("creating the HTTP request") + path := strings.Replace(PauseWorkspacePath, ":"+NamespacePathParam, namespaceName1, 1) + path = strings.Replace(path, ":"+ResourceNamePathParam, missingWorkspaceName, 1) + req, err := http.NewRequest(http.MethodPost, path, http.NoBody) + Expect(err).NotTo(HaveOccurred()) + + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + + By("executing PauseWorkspaceHandler") + ps := httprouter.Params{ + httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, + httprouter.Param{Key: ResourceNamePathParam, Value: missingWorkspaceName}, + } + rr := httptest.NewRecorder() + a.PauseWorkspaceHandler(rr, req, ps) + rs := rr.Result() + defer rs.Body.Close() + + By("verifying the HTTP response status code") + Expect(rs.StatusCode).To(Equal(http.StatusNotFound), descUnexpectedHTTPStatus, rr.Body.String()) + }) + }) +}) diff --git a/workspaces/backend/internal/repositories/workspaces/repo.go b/workspaces/backend/internal/repositories/workspaces/repo.go index 82f3eada7..303011b51 100644 --- a/workspaces/backend/internal/repositories/workspaces/repo.go +++ b/workspaces/backend/internal/repositories/workspaces/repo.go @@ -18,11 +18,13 @@ package workspaces import ( "context" + "encoding/json" "fmt" kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -214,3 +216,38 @@ func (r *WorkspaceRepository) DeleteWorkspace(ctx context.Context, namespace, wo return nil } + +// WorkspacePausePatch represents a patch to update a workspace's paused state +type WorkspacePausePatch struct { + Spec struct { + Paused bool `json:"paused"` + } `json:"spec"` +} + +// PauseWorkspace pauses a workspace by setting its paused field to true +func (r *WorkspaceRepository) PauseWorkspace(ctx context.Context, namespace, workspaceName string) error { + // Create a patch that only updates the paused field + patch := WorkspacePausePatch{} + patch.Spec.Paused = true + + // Convert patch to JSON + patchBytes, err := json.Marshal(patch) + if err != nil { + return fmt.Errorf("failed to marshal patch: %w", err) + } + + // Apply the patch + if err := r.client.Patch(ctx, &kubefloworgv1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: workspaceName, + }, + }, client.RawPatch(types.MergePatchType, patchBytes)); err != nil { + if apierrors.IsNotFound(err) { + return ErrWorkspaceNotFound + } + return fmt.Errorf("failed to patch workspace: %w", err) + } + + return nil +} diff --git a/workspaces/backend/openapi/docs.go b/workspaces/backend/openapi/docs.go index f255e86d3..0f8766313 100644 --- a/workspaces/backend/openapi/docs.go +++ b/workspaces/backend/openapi/docs.go @@ -385,6 +385,82 @@ const docTemplate = `{ } } }, + "/workspaces/{namespace}/{workspaceName}/actions/pause": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Pauses a workspace, stopping all associated pods.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspaces" + ], + "summary": "Pause workspace", + "parameters": [ + { + "type": "string", + "example": "default", + "description": "Namespace of the workspace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "example": "my-workspace", + "description": "Name of the workspace", + "name": "workspaceName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successful action. Returns an empty JSON object.", + "schema": { + "$ref": "#/definitions/api.EmptyResponse" + } + }, + "400": { + "description": "Bad Request. Invalid workspace kind name format.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized. Authentication is required.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden. User does not have permission to access the workspace.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found. Workspace does not exist.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "500": { + "description": "Internal server error. An unexpected error occurred on the server.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + } + } + } + }, "/workspaces/{namespace}/{workspace_name}": { "get": { "security": [ @@ -534,6 +610,9 @@ const docTemplate = `{ } }, "definitions": { + "api.EmptyResponse": { + "type": "object" + }, "api.ErrorCause": { "type": "object", "properties": { diff --git a/workspaces/backend/openapi/swagger.json b/workspaces/backend/openapi/swagger.json index ef67382c0..8b122609c 100644 --- a/workspaces/backend/openapi/swagger.json +++ b/workspaces/backend/openapi/swagger.json @@ -383,6 +383,82 @@ } } }, + "/workspaces/{namespace}/{workspaceName}/actions/pause": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Pauses a workspace, stopping all associated pods.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspaces" + ], + "summary": "Pause workspace", + "parameters": [ + { + "type": "string", + "example": "default", + "description": "Namespace of the workspace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "example": "my-workspace", + "description": "Name of the workspace", + "name": "workspaceName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successful action. Returns an empty JSON object.", + "schema": { + "$ref": "#/definitions/api.EmptyResponse" + } + }, + "400": { + "description": "Bad Request. Invalid workspace kind name format.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized. Authentication is required.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden. User does not have permission to access the workspace.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found. Workspace does not exist.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + }, + "500": { + "description": "Internal server error. An unexpected error occurred on the server.", + "schema": { + "$ref": "#/definitions/api.ErrorEnvelope" + } + } + } + } + }, "/workspaces/{namespace}/{workspace_name}": { "get": { "security": [ @@ -532,6 +608,9 @@ } }, "definitions": { + "api.EmptyResponse": { + "type": "object" + }, "api.ErrorCause": { "type": "object", "properties": { diff --git a/workspaces/backend/openapi/swagger.yaml b/workspaces/backend/openapi/swagger.yaml index 9cae76367..a7887ad82 100644 --- a/workspaces/backend/openapi/swagger.yaml +++ b/workspaces/backend/openapi/swagger.yaml @@ -1,5 +1,7 @@ basePath: /api/v1 definitions: + api.EmptyResponse: + type: object api.ErrorCause: properties: validation_errors: @@ -928,6 +930,57 @@ paths: summary: Get workspace tags: - workspaces + /workspaces/{namespace}/{workspaceName}/actions/pause: + post: + consumes: + - application/json + description: Pauses a workspace, stopping all associated pods. + parameters: + - description: Namespace of the workspace + example: default + in: path + name: namespace + required: true + type: string + - description: Name of the workspace + example: my-workspace + in: path + name: workspaceName + required: true + type: string + produces: + - application/json + responses: + "200": + description: Successful action. Returns an empty JSON object. + schema: + $ref: '#/definitions/api.EmptyResponse' + "400": + description: Bad Request. Invalid workspace kind name format. + schema: + $ref: '#/definitions/api.ErrorEnvelope' + "401": + description: Unauthorized. Authentication is required. + schema: + $ref: '#/definitions/api.ErrorEnvelope' + "403": + description: Forbidden. User does not have permission to access the workspace. + schema: + $ref: '#/definitions/api.ErrorEnvelope' + "404": + description: Not Found. Workspace does not exist. + schema: + $ref: '#/definitions/api.ErrorEnvelope' + "500": + description: Internal server error. An unexpected error occurred on the + server. + schema: + $ref: '#/definitions/api.ErrorEnvelope' + security: + - ApiKeyAuth: [] + summary: Pause workspace + tags: + - workspaces schemes: - http - https