diff --git a/coderd/aitasks.go b/coderd/aitasks.go index e0caa0121b..e71df52935 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -1244,3 +1244,63 @@ func (api *API) postWorkspaceAgentTaskLogSnapshot(rw http.ResponseWriter, r *htt rw.WriteHeader(http.StatusNoContent) } + +// @Summary Pause task +// @ID pause-task +// @Security CoderSessionToken +// @Accept json +// @Tags Tasks +// @Param user path string true "Username, user ID, or 'me' for the authenticated user" +// @Param task path string true "Task ID" format(uuid) +// @Success 202 {object} codersdk.PauseTaskResponse +// @Router /tasks/{user}/{task}/pause [post] +func (api *API) pauseTask(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + apiKey = httpmw.APIKey(r) + task = httpmw.TaskParam(r) + ) + + if !task.WorkspaceID.Valid { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Task does not have a workspace.", + }) + return + } + + workspace, err := api.Database.GetWorkspaceByID(ctx, task.WorkspaceID.UUID) + if err != nil { + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching task workspace.", + Detail: err.Error(), + }) + return + } + + buildReq := codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionStop, + Reason: codersdk.CreateWorkspaceBuildReasonTaskManualPause, + } + build, err := api.postWorkspaceBuildsInternal( + ctx, + apiKey, + workspace, + buildReq, + func(action policy.Action, object rbac.Objecter) bool { + return api.Authorize(r, action, object) + }, + audit.WorkspaceBuildBaggageFromRequest(r), + ) + if err != nil { + httperror.WriteWorkspaceBuildError(ctx, rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusAccepted, codersdk.PauseTaskResponse{ + WorkspaceBuild: &build, + }) +} diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index bd4ce26cfa..2bd684de21 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -16,6 +16,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/xerrors" agentapisdk "github.com/coder/agentapi-sdk-go" "github.com/coder/coder/v2/agent" @@ -26,11 +27,14 @@ import ( "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" @@ -100,6 +104,36 @@ func createTaskInState(db database.Store, ownerSubject rbac.Subject, ownerOrgID, } } +type aiTaskStoreWrapper struct { + database.Store + getWorkspaceByID func(ctx context.Context, id uuid.UUID) (database.Workspace, error) + insertWorkspaceBuild func(ctx context.Context, arg database.InsertWorkspaceBuildParams) error +} + +func (s aiTaskStoreWrapper) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (database.Workspace, error) { + if s.getWorkspaceByID != nil { + return s.getWorkspaceByID(ctx, id) + } + return s.Store.GetWorkspaceByID(ctx, id) +} + +func (s aiTaskStoreWrapper) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error { + if s.insertWorkspaceBuild != nil { + return s.insertWorkspaceBuild(ctx, arg) + } + return s.Store.InsertWorkspaceBuild(ctx, arg) +} + +func (s aiTaskStoreWrapper) InTx(fn func(database.Store) error, opts *database.TxOptions) error { + return s.Store.InTx(func(tx database.Store) error { + return fn(aiTaskStoreWrapper{ + Store: tx, + getWorkspaceByID: s.getWorkspaceByID, + insertWorkspaceBuild: s.insertWorkspaceBuild, + }) + }, opts) +} + func TestTasks(t *testing.T) { t.Parallel() @@ -2422,3 +2456,328 @@ func TestPostWorkspaceAgentTaskSnapshot(t *testing.T) { require.Equal(t, http.StatusUnauthorized, res.StatusCode) }) } + +func TestPauseTask(t *testing.T) { + t.Parallel() + + setupClient := func(t *testing.T, db database.Store, ps pubsub.Pubsub, authorizer rbac.Authorizer) *codersdk.Client { + t.Helper() + client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + Authorizer: authorizer, + }) + return client + } + + setupWorkspaceTask := func(t *testing.T, db database.Store, user codersdk.CreateFirstUserResponse) (database.Task, uuid.UUID) { + t.Helper() + workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithTask(database.TaskTable{ + Prompt: "pause me", + }, nil).Do() + return workspaceBuild.Task, workspaceBuild.Workspace.ID + } + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + ProvisionGraph: []*proto.Response{ + {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{ + HasAiTasks: true, + }}}, + }, + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + task, err := client.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{ + TemplateVersionID: template.ActiveVersionID, + Input: "pause me", + }) + require.NoError(t, err) + require.True(t, task.WorkspaceID.Valid) + + workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + resp, err := client.PauseTask(ctx, codersdk.Me, task.ID) + require.NoError(t, err) + build := *resp.WorkspaceBuild + require.NotNil(t, build) + require.Equal(t, codersdk.WorkspaceTransitionStop, build.Transition) + require.Equal(t, task.WorkspaceID.UUID, build.WorkspaceID) + require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber) + require.Equal(t, string(codersdk.CreateWorkspaceBuildReasonTaskManualPause), string(build.Reason)) + }) + + t.Run("Non-owner role access", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + db, ps := dbtestutil.NewDB(t) + client := setupClient(t, db, ps, nil) + owner := coderdtest.CreateFirstUser(t, client) + + cases := []struct { + name string + roles []rbac.RoleIdentifier + expectedStatus int + }{ + { + name: "org_member", + expectedStatus: http.StatusNotFound, + }, + { + name: "org_admin", + roles: []rbac.RoleIdentifier{rbac.ScopedRoleOrgAdmin(owner.OrganizationID)}, + expectedStatus: http.StatusAccepted, + }, + { + name: "sitewide_member", + roles: []rbac.RoleIdentifier{rbac.RoleMember()}, + expectedStatus: http.StatusNotFound, + }, + { + name: "sitewide_admin", + roles: []rbac.RoleIdentifier{rbac.RoleOwner()}, + expectedStatus: http.StatusAccepted, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + task, _ := setupWorkspaceTask(t, db, owner) + userClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, tc.roles...) + + resp, err := userClient.PauseTask(ctx, codersdk.Me, task.ID) + if tc.expectedStatus == http.StatusAccepted { + require.NoError(t, err) + require.NotNil(t, resp.WorkspaceBuild) + require.NotEqual(t, uuid.Nil, resp.WorkspaceBuild.ID) + return + } + + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, tc.expectedStatus, apiErr.StatusCode()) + }) + } + }) + + t.Run("Task not found", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + _ = coderdtest.CreateFirstUser(t, client) + + _, err := client.PauseTask(ctx, codersdk.Me, uuid.New()) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("Task lookup forbidden", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + db, ps := dbtestutil.NewDB(t) + auth := &coderdtest.FakeAuthorizer{ + ConditionalReturn: func(_ context.Context, _ rbac.Subject, action policy.Action, object rbac.Object) error { + if action == policy.ActionRead && object.Type == rbac.ResourceTask.Type { + return rbac.UnauthorizedError{} + } + return nil + }, + } + client := setupClient(t, db, ps, auth) + user := coderdtest.CreateFirstUser(t, client) + task, _ := setupWorkspaceTask(t, db, user) + + _, err := client.PauseTask(ctx, codersdk.Me, task.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("Workspace lookup forbidden", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + db, ps := dbtestutil.NewDB(t) + auth := &coderdtest.FakeAuthorizer{ + ConditionalReturn: func(_ context.Context, _ rbac.Subject, action policy.Action, object rbac.Object) error { + if action == policy.ActionRead && object.Type == rbac.ResourceWorkspace.Type { + return rbac.UnauthorizedError{} + } + return nil + }, + } + client := setupClient(t, db, ps, auth) + user := coderdtest.CreateFirstUser(t, client) + task, _ := setupWorkspaceTask(t, db, user) + + _, err := client.PauseTask(ctx, codersdk.Me, task.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("No Workspace for Task", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + db, ps := dbtestutil.NewDB(t) + client := setupClient(t, db, ps, nil) + user := coderdtest.CreateFirstUser(t, client) + + workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).Do() + task := dbgen.Task(t, db, database.TaskTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + TemplateVersionID: workspaceBuild.Build.TemplateVersionID, + Prompt: "no workspace", + }) + + _, err := client.PauseTask(ctx, codersdk.Me, task.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusInternalServerError, apiErr.StatusCode()) + require.Equal(t, "Task does not have a workspace.", apiErr.Message) + }) + + t.Run("Workspace not found", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + db, ps := dbtestutil.NewDB(t) + var workspaceID uuid.UUID + wrapped := aiTaskStoreWrapper{ + Store: db, + getWorkspaceByID: func(ctx context.Context, id uuid.UUID) (database.Workspace, error) { + if id == workspaceID && id != uuid.Nil { + return database.Workspace{}, sql.ErrNoRows + } + return db.GetWorkspaceByID(ctx, id) + }, + } + client := setupClient(t, wrapped, ps, nil) + user := coderdtest.CreateFirstUser(t, client) + task, workspaceIDValue := setupWorkspaceTask(t, db, user) + workspaceID = workspaceIDValue + + _, err := client.PauseTask(ctx, codersdk.Me, task.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("Workspace lookup internal error", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + db, ps := dbtestutil.NewDB(t) + var workspaceID uuid.UUID + wrapped := aiTaskStoreWrapper{ + Store: db, + getWorkspaceByID: func(ctx context.Context, id uuid.UUID) (database.Workspace, error) { + if id == workspaceID && id != uuid.Nil { + return database.Workspace{}, xerrors.New("boom") + } + return db.GetWorkspaceByID(ctx, id) + }, + } + client := setupClient(t, wrapped, ps, nil) + user := coderdtest.CreateFirstUser(t, client) + task, workspaceIDValue := setupWorkspaceTask(t, db, user) + workspaceID = workspaceIDValue + + _, err := client.PauseTask(ctx, codersdk.Me, task.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusInternalServerError, apiErr.StatusCode()) + require.Equal(t, "Internal error fetching task workspace.", apiErr.Message) + }) + + t.Run("Build Forbidden", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + db, ps := dbtestutil.NewDB(t) + auth := &coderdtest.FakeAuthorizer{ + ConditionalReturn: func(_ context.Context, _ rbac.Subject, action policy.Action, object rbac.Object) error { + if action == policy.ActionWorkspaceStop && object.Type == rbac.ResourceWorkspace.Type { + return rbac.UnauthorizedError{} + } + return nil + }, + } + client := setupClient(t, db, ps, auth) + user := coderdtest.CreateFirstUser(t, client) + task, _ := setupWorkspaceTask(t, db, user) + + _, err := client.PauseTask(ctx, codersdk.Me, task.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) + }) + + t.Run("Job already in progress", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + db, ps := dbtestutil.NewDB(t) + client := setupClient(t, db, ps, nil) + user := coderdtest.CreateFirstUser(t, client) + workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }). + WithTask(database.TaskTable{ + Prompt: "pause me", + }, nil). + Starting(). + Do() + + _, err := client.PauseTask(ctx, codersdk.Me, workspaceBuild.Task.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("Build Internal Error", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + db, ps := dbtestutil.NewDB(t) + wrapped := aiTaskStoreWrapper{ + Store: db, + insertWorkspaceBuild: func(ctx context.Context, arg database.InsertWorkspaceBuildParams) error { + return xerrors.New("insert failed") + }, + } + client := setupClient(t, wrapped, ps, nil) + user := coderdtest.CreateFirstUser(t, client) + task, _ := setupWorkspaceTask(t, db, user) + + _, err := client.PauseTask(ctx, codersdk.Me, task.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusInternalServerError, apiErr.StatusCode()) + }) +} diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6ba016c4a9..00f5078153 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5824,6 +5824,48 @@ const docTemplate = `{ } } }, + "/tasks/{user}/{task}/pause": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "tags": [ + "Tasks" + ], + "summary": "Pause task", + "operationId": "pause-task", + "parameters": [ + { + "type": "string", + "description": "Username, user ID, or 'me' for the authenticated user", + "name": "user", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Task ID", + "name": "task", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/codersdk.PauseTaskResponse" + } + } + } + } + }, "/tasks/{user}/{task}/send": { "post": { "security": [ @@ -14102,14 +14144,16 @@ const docTemplate = `{ "cli", "ssh_connection", "vscode_connection", - "jetbrains_connection" + "jetbrains_connection", + "task_manual_pause" ], "x-enum-varnames": [ "CreateWorkspaceBuildReasonDashboard", "CreateWorkspaceBuildReasonCLI", "CreateWorkspaceBuildReasonSSHConnection", "CreateWorkspaceBuildReasonVSCodeConnection", - "CreateWorkspaceBuildReasonJetbrainsConnection" + "CreateWorkspaceBuildReasonJetbrainsConnection", + "CreateWorkspaceBuildReasonTaskManualPause" ] }, "codersdk.CreateWorkspaceBuildRequest": { @@ -14143,7 +14187,8 @@ const docTemplate = `{ "cli", "ssh_connection", "vscode_connection", - "jetbrains_connection" + "jetbrains_connection", + "task_manual_pause" ], "allOf": [ { @@ -17014,6 +17059,14 @@ const docTemplate = `{ } } }, + "codersdk.PauseTaskResponse": { + "type": "object", + "properties": { + "workspace_build": { + "$ref": "#/definitions/codersdk.WorkspaceBuild" + } + } + }, "codersdk.Permission": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 24187a50ab..acabf1a22b 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5147,6 +5147,44 @@ } } }, + "/tasks/{user}/{task}/pause": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "tags": ["Tasks"], + "summary": "Pause task", + "operationId": "pause-task", + "parameters": [ + { + "type": "string", + "description": "Username, user ID, or 'me' for the authenticated user", + "name": "user", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Task ID", + "name": "task", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/codersdk.PauseTaskResponse" + } + } + } + } + }, "/tasks/{user}/{task}/send": { "post": { "security": [ @@ -12662,14 +12700,16 @@ "cli", "ssh_connection", "vscode_connection", - "jetbrains_connection" + "jetbrains_connection", + "task_manual_pause" ], "x-enum-varnames": [ "CreateWorkspaceBuildReasonDashboard", "CreateWorkspaceBuildReasonCLI", "CreateWorkspaceBuildReasonSSHConnection", "CreateWorkspaceBuildReasonVSCodeConnection", - "CreateWorkspaceBuildReasonJetbrainsConnection" + "CreateWorkspaceBuildReasonJetbrainsConnection", + "CreateWorkspaceBuildReasonTaskManualPause" ] }, "codersdk.CreateWorkspaceBuildRequest": { @@ -12699,7 +12739,8 @@ "cli", "ssh_connection", "vscode_connection", - "jetbrains_connection" + "jetbrains_connection", + "task_manual_pause" ], "allOf": [ { @@ -15477,6 +15518,14 @@ } } }, + "codersdk.PauseTaskResponse": { + "type": "object", + "properties": { + "workspace_build": { + "$ref": "#/definitions/codersdk.WorkspaceBuild" + } + } + }, "codersdk.Permission": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 942e785d36..ffd5d6699f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1078,6 +1078,7 @@ func New(options *Options) *API { r.Patch("/input", api.taskUpdateInput) r.Post("/send", api.taskSend) r.Get("/logs", api.taskLogs) + r.Post("/pause", api.pauseTask) }) }) }) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 78eaa303dd..482740f362 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -384,7 +384,7 @@ func (api *API) postWorkspaceBuildsInternal( Experiments(api.Experiments). TemplateVersionPresetID(createBuild.TemplateVersionPresetID) - if transition == database.WorkspaceTransitionStart && createBuild.Reason != "" { + if (transition == database.WorkspaceTransitionStart || transition == database.WorkspaceTransitionStop) && createBuild.Reason != "" { builder = builder.Reason(database.BuildReason(createBuild.Reason)) } diff --git a/codersdk/aitasks.go b/codersdk/aitasks.go index 46f8164d06..9dda558fcf 100644 --- a/codersdk/aitasks.go +++ b/codersdk/aitasks.go @@ -329,6 +329,31 @@ func (c *Client) UpdateTaskInput(ctx context.Context, user string, id uuid.UUID, return nil } +// PauseTaskResponse represents the response from pausing a task. +type PauseTaskResponse struct { + WorkspaceBuild *WorkspaceBuild `json:"workspace_build"` +} + +// PauseTask pauses a task by stopping its workspace. +// Experimental: uses the /api/experimental endpoint. +func (c *Client) PauseTask(ctx context.Context, user string, id uuid.UUID) (PauseTaskResponse, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/tasks/%s/%s/pause", user, id.String()), nil) + if err != nil { + return PauseTaskResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusAccepted { + return PauseTaskResponse{}, ReadBodyAsError(res) + } + + var resp PauseTaskResponse + if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { + return PauseTaskResponse{}, err + } + + return resp, nil +} + // TaskLogType indicates the source of a task log entry. type TaskLogType string diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index ad29c717a3..27a04dbfdf 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -109,6 +109,7 @@ const ( CreateWorkspaceBuildReasonSSHConnection CreateWorkspaceBuildReason = "ssh_connection" CreateWorkspaceBuildReasonVSCodeConnection CreateWorkspaceBuildReason = "vscode_connection" CreateWorkspaceBuildReasonJetbrainsConnection CreateWorkspaceBuildReason = "jetbrains_connection" + CreateWorkspaceBuildReasonTaskManualPause CreateWorkspaceBuildReason = "task_manual_pause" ) // CreateWorkspaceBuildRequest provides options to update the latest workspace build. @@ -129,7 +130,7 @@ type CreateWorkspaceBuildRequest struct { // TemplateVersionPresetID is the ID of the template version preset to use for the build. TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` // Reason sets the reason for the workspace build. - Reason CreateWorkspaceBuildReason `json:"reason,omitempty" validate:"omitempty,oneof=dashboard cli ssh_connection vscode_connection jetbrains_connection"` + Reason CreateWorkspaceBuildReason `json:"reason,omitempty" validate:"omitempty,oneof=dashboard cli ssh_connection vscode_connection jetbrains_connection task_manual_pause"` } type WorkspaceOptions struct { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 983f28987a..99af6ddb1a 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -2184,9 +2184,9 @@ This is required on creation to enable a user-flow of validating a template work #### Enumerated Values -| Value(s) | -|-----------------------------------------------------------------------------------| -| `cli`, `dashboard`, `jetbrains_connection`, `ssh_connection`, `vscode_connection` | +| Value(s) | +|--------------------------------------------------------------------------------------------------------| +| `cli`, `dashboard`, `jetbrains_connection`, `ssh_connection`, `task_manual_pause`, `vscode_connection` | ## codersdk.CreateWorkspaceBuildRequest @@ -2227,11 +2227,11 @@ This is required on creation to enable a user-flow of validating a template work #### Enumerated Values -| Property | Value(s) | -|--------------|-----------------------------------------------------------------------------------| -| `log_level` | `debug` | -| `reason` | `cli`, `dashboard`, `jetbrains_connection`, `ssh_connection`, `vscode_connection` | -| `transition` | `delete`, `start`, `stop` | +| Property | Value(s) | +|--------------|--------------------------------------------------------------------------------------------------------| +| `log_level` | `debug` | +| `reason` | `cli`, `dashboard`, `jetbrains_connection`, `ssh_connection`, `task_manual_pause`, `vscode_connection` | +| `transition` | `delete`, `start`, `stop` | ## codersdk.CreateWorkspaceProxyRequest @@ -6178,6 +6178,225 @@ Only certain features set these fields: - FeatureManagedAgentLimit| | `name` | string | true | | | | `regenerate_token` | boolean | false | | | +## codersdk.PauseTaskResponse + +```json +{ + "workspace_build": { + "build_number": 0, + "created_at": "2019-08-24T14:15:22Z", + "daily_cost": 0, + "deadline": "2019-08-24T14:15:22Z", + "has_ai_task": true, + "has_external_agent": true, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", + "initiator_name": "string", + "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], + "canceled_at": "2019-08-24T14:15:22Z", + "completed_at": "2019-08-24T14:15:22Z", + "created_at": "2019-08-24T14:15:22Z", + "error": "string", + "error_code": "REQUIRED_TEMPLATE_VARIABLES", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "logs_overflowed": true, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "queue_position": 0, + "queue_size": 0, + "started_at": "2019-08-24T14:15:22Z", + "status": "pending", + "tags": { + "property1": "string", + "property2": "string" + }, + "type": "template_version_import", + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" + }, + "matched_provisioners": { + "available": 0, + "count": 0, + "most_recently_seen": "2019-08-24T14:15:22Z" + }, + "max_deadline": "2019-08-24T14:15:22Z", + "reason": "initiator", + "resources": [ + { + "agents": [ + { + "api_version": "string", + "apps": [ + { + "command": "string", + "display_name": "string", + "external": true, + "group": "string", + "health": "disabled", + "healthcheck": { + "interval": 0, + "threshold": 0, + "url": "string" + }, + "hidden": true, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "open_in": "slim-window", + "sharing_level": "owner", + "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], + "subdomain": true, + "subdomain_name": "string", + "tooltip": "string", + "url": "string" + } + ], + "architecture": "string", + "connection_timeout_seconds": 0, + "created_at": "2019-08-24T14:15:22Z", + "directory": "string", + "disconnected_at": "2019-08-24T14:15:22Z", + "display_apps": [ + "vscode" + ], + "environment_variables": { + "property1": "string", + "property2": "string" + }, + "expanded_directory": "string", + "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": false, + "reason": "agent has lost connection" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "instance_id": "string", + "last_connected_at": "2019-08-24T14:15:22Z", + "latency": { + "property1": { + "latency_ms": 0, + "preferred": true + }, + "property2": { + "latency_ms": 0, + "preferred": true + } + }, + "lifecycle_state": "created", + "log_sources": [ + { + "created_at": "2019-08-24T14:15:22Z", + "display_name": "string", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1" + } + ], + "logs_length": 0, + "logs_overflowed": true, + "name": "string", + "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, + "ready_at": "2019-08-24T14:15:22Z", + "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "scripts": [ + { + "cron": "string", + "display_name": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "log_path": "string", + "log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a", + "run_on_start": true, + "run_on_stop": true, + "script": "string", + "start_blocks_login": true, + "timeout": 0 + } + ], + "started_at": "2019-08-24T14:15:22Z", + "startup_script_behavior": "blocking", + "status": "connecting", + "subsystems": [ + "envbox" + ], + "troubleshooting_url": "string", + "updated_at": "2019-08-24T14:15:22Z", + "version": "string" + } + ], + "created_at": "2019-08-24T14:15:22Z", + "daily_cost": 0, + "hide": true, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", + "metadata": [ + { + "key": "string", + "sensitive": true, + "value": "string" + } + ], + "name": "string", + "type": "string", + "workspace_transition": "start" + } + ], + "status": "pending", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", + "transition": "start", + "updated_at": "2019-08-24T14:15:22Z", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string", + "workspace_owner_avatar_url": "string", + "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", + "workspace_owner_name": "string" + } +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-------------------|----------------------------------------------------|----------|--------------|-------------| +| `workspace_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | + ## codersdk.Permission ```json diff --git a/docs/reference/api/tasks.md b/docs/reference/api/tasks.md index cbfb47c146..f2fb89df06 100644 --- a/docs/reference/api/tasks.md +++ b/docs/reference/api/tasks.md @@ -365,6 +365,38 @@ curl -X GET http://coder-server:8080/api/v2/tasks/{user}/{task}/logs \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Pause task + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/tasks/{user}/{task}/pause \ + -H 'Accept: */*' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /tasks/{user}/{task}/pause` + +### Parameters + +| Name | In | Type | Required | Description | +|--------|------|--------------|----------|-------------------------------------------------------| +| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user | +| `task` | path | string(uuid) | true | Task ID | + +### Example responses + +> 202 Response + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------------|-------------|--------------------------------------------------------------------| +| 202 | [Accepted](https://tools.ietf.org/html/rfc7231#section-6.3.3) | Accepted | [codersdk.PauseTaskResponse](schemas.md#codersdkpausetaskresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Send input to AI task ### Code samples diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index aa3548d444..e204b7eb5c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1425,6 +1425,7 @@ export type CreateWorkspaceBuildReason = | "dashboard" | "jetbrains_connection" | "ssh_connection" + | "task_manual_pause" | "vscode_connection"; export const CreateWorkspaceBuildReasons: CreateWorkspaceBuildReason[] = [ @@ -1432,6 +1433,7 @@ export const CreateWorkspaceBuildReasons: CreateWorkspaceBuildReason[] = [ "dashboard", "jetbrains_connection", "ssh_connection", + "task_manual_pause", "vscode_connection", ]; @@ -3583,6 +3585,14 @@ export interface PatchWorkspaceProxy { */ export const PathAppSessionTokenCookie = "coder_path_app_session_token"; +// From codersdk/aitasks.go +/** + * PauseTaskResponse represents the response from pausing a task. + */ +export interface PauseTaskResponse { + readonly workspace_build: WorkspaceBuild | null; +} + // From codersdk/roles.go /** * Permission is the format passed into the rego.