mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add an endpoint to manually resume a coder task (#21948)
Closes https://github.com/coder/internal/issues/1262. This PR adds: * the `POST /api/experimental/tasks/{user}/{task}/resume` endpoint * follows conventions from https://github.com/coder/internal/issues/1261 * sets the build reason to `task_resume` * a task that is not paused (ie. is already running), cannot be resumed.
This commit is contained in:
@@ -1304,3 +1304,90 @@ func (api *API) pauseTask(rw http.ResponseWriter, r *http.Request) {
|
||||
WorkspaceBuild: &build,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Resume task
|
||||
// @ID resume-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.ResumeTaskResponse
|
||||
// @Router /tasks/{user}/{task}/resume [post]
|
||||
func (api *API) resumeTask(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
|
||||
}
|
||||
|
||||
latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching task workspace build.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(ctx, latestBuild.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching task workspace build job.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
workspaceStatus := codersdk.ConvertWorkspaceStatus(
|
||||
codersdk.ProvisionerJobStatus(job.JobStatus),
|
||||
codersdk.WorkspaceTransition(latestBuild.Transition),
|
||||
)
|
||||
if workspaceStatus == codersdk.WorkspaceStatusRunning {
|
||||
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||
Message: "Task workspace is already running.",
|
||||
Detail: fmt.Sprintf("Workspace status is %q.", workspaceStatus),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
buildReq := codersdk.CreateWorkspaceBuildRequest{
|
||||
Transition: codersdk.WorkspaceTransitionStart,
|
||||
Reason: codersdk.CreateWorkspaceBuildReasonTaskResume,
|
||||
}
|
||||
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.ResumeTaskResponse{
|
||||
WorkspaceBuild: &build,
|
||||
})
|
||||
}
|
||||
|
||||
+337
-1
@@ -2512,13 +2512,20 @@ func TestPauseTask(t *testing.T) {
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
resp, err := client.PauseTask(ctx, codersdk.Me, task.ID)
|
||||
|
||||
// Verify that the request was accepted correctly:
|
||||
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))
|
||||
|
||||
// Verify that the accepted request was processed correctly:
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
workspace, err = client.Workspace(ctx, task.WorkspaceID.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.WorkspaceStatusStopped, workspace.LatestBuild.Status)
|
||||
})
|
||||
|
||||
t.Run("Non-owner role access", func(t *testing.T) {
|
||||
@@ -2781,3 +2788,332 @@ func TestPauseTask(t *testing.T) {
|
||||
require.Equal(t, http.StatusInternalServerError, apiErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestResumeTask(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,
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
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: "resume 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: "resume me",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
pauseResp, err := client.PauseTask(ctx, codersdk.Me, task.ID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, pauseResp.WorkspaceBuild.ID)
|
||||
|
||||
resumeResp, err := client.ResumeTask(ctx, codersdk.Me, task.ID)
|
||||
require.NoError(t, err)
|
||||
build := *resumeResp.WorkspaceBuild
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, build.Transition)
|
||||
require.Equal(t, task.WorkspaceID.UUID, build.WorkspaceID)
|
||||
require.Equal(t, workspace.LatestBuild.BuildNumber+2, build.BuildNumber)
|
||||
require.Equal(t, string(codersdk.CreateWorkspaceBuildReasonTaskResume), string(build.Reason))
|
||||
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
workspace, err = client.Workspace(ctx, task.WorkspaceID.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, workspace.LatestBuild.Status)
|
||||
})
|
||||
|
||||
t.Run("Resume a task that is not paused", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
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).
|
||||
Succeeded().
|
||||
Do()
|
||||
|
||||
_, err := client.ResumeTask(ctx, codersdk.Me, workspaceBuild.Task.ID)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusConflict, 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.ResumeTask(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.ResumeTask(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.ResumeTask(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.ResumeTask(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.ResumeTask(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.ResumeTask(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.ActionWorkspaceStart && 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)
|
||||
|
||||
pauseResp, err := client.PauseTask(ctx, codersdk.Me, task.ID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, pauseResp.WorkspaceBuild.ID)
|
||||
|
||||
_, err = client.ResumeTask(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: "resume me",
|
||||
}, nil).
|
||||
Starting().
|
||||
Do()
|
||||
|
||||
_, err := client.ResumeTask(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,
|
||||
}
|
||||
|
||||
client := setupClient(t, &wrapped, ps, nil)
|
||||
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: "resume me",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
pauseResp, err := client.PauseTask(ctx, codersdk.Me, task.ID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, pauseResp.WorkspaceBuild.ID)
|
||||
|
||||
// Induce a transient failure in the database after the task has been paused.
|
||||
wrapped.insertWorkspaceBuild = func(ctx context.Context, arg database.InsertWorkspaceBuildParams) error {
|
||||
return xerrors.New("insert failed")
|
||||
}
|
||||
_, err = client.ResumeTask(ctx, codersdk.Me, task.ID)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusInternalServerError, apiErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
Generated
+54
-2
@@ -5866,6 +5866,48 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/{user}/{task}/resume": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Tasks"
|
||||
],
|
||||
"summary": "Resume task",
|
||||
"operationId": "resume-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.ResumeTaskResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/{user}/{task}/send": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -14145,7 +14187,8 @@ const docTemplate = `{
|
||||
"ssh_connection",
|
||||
"vscode_connection",
|
||||
"jetbrains_connection",
|
||||
"task_manual_pause"
|
||||
"task_manual_pause",
|
||||
"task_resume"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"CreateWorkspaceBuildReasonDashboard",
|
||||
@@ -14153,7 +14196,8 @@ const docTemplate = `{
|
||||
"CreateWorkspaceBuildReasonSSHConnection",
|
||||
"CreateWorkspaceBuildReasonVSCodeConnection",
|
||||
"CreateWorkspaceBuildReasonJetbrainsConnection",
|
||||
"CreateWorkspaceBuildReasonTaskManualPause"
|
||||
"CreateWorkspaceBuildReasonTaskManualPause",
|
||||
"CreateWorkspaceBuildReasonTaskResume"
|
||||
]
|
||||
},
|
||||
"codersdk.CreateWorkspaceBuildRequest": {
|
||||
@@ -18235,6 +18279,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ResumeTaskResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspace_build": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceBuild"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.RetentionConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+50
-2
@@ -5185,6 +5185,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/{user}/{task}/resume": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"tags": ["Tasks"],
|
||||
"summary": "Resume task",
|
||||
"operationId": "resume-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.ResumeTaskResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/{user}/{task}/send": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -12701,7 +12739,8 @@
|
||||
"ssh_connection",
|
||||
"vscode_connection",
|
||||
"jetbrains_connection",
|
||||
"task_manual_pause"
|
||||
"task_manual_pause",
|
||||
"task_resume"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"CreateWorkspaceBuildReasonDashboard",
|
||||
@@ -12709,7 +12748,8 @@
|
||||
"CreateWorkspaceBuildReasonSSHConnection",
|
||||
"CreateWorkspaceBuildReasonVSCodeConnection",
|
||||
"CreateWorkspaceBuildReasonJetbrainsConnection",
|
||||
"CreateWorkspaceBuildReasonTaskManualPause"
|
||||
"CreateWorkspaceBuildReasonTaskManualPause",
|
||||
"CreateWorkspaceBuildReasonTaskResume"
|
||||
]
|
||||
},
|
||||
"codersdk.CreateWorkspaceBuildRequest": {
|
||||
@@ -16647,6 +16687,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ResumeTaskResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspace_build": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceBuild"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.RetentionConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1079,6 +1079,7 @@ func New(options *Options) *API {
|
||||
r.Post("/send", api.taskSend)
|
||||
r.Get("/logs", api.taskLogs)
|
||||
r.Post("/pause", api.pauseTask)
|
||||
r.Post("/resume", api.resumeTask)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -354,6 +354,29 @@ func (c *Client) PauseTask(ctx context.Context, user string, id uuid.UUID) (Paus
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ResumeTaskResponse represents the response from resuming a task.
|
||||
type ResumeTaskResponse struct {
|
||||
WorkspaceBuild *WorkspaceBuild `json:"workspace_build"`
|
||||
}
|
||||
|
||||
func (c *Client) ResumeTask(ctx context.Context, user string, id uuid.UUID) (ResumeTaskResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/tasks/%s/%s/resume", user, id.String()), nil)
|
||||
if err != nil {
|
||||
return ResumeTaskResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusAccepted {
|
||||
return ResumeTaskResponse{}, ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
var resp ResumeTaskResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
return ResumeTaskResponse{}, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// TaskLogType indicates the source of a task log entry.
|
||||
type TaskLogType string
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ const (
|
||||
CreateWorkspaceBuildReasonVSCodeConnection CreateWorkspaceBuildReason = "vscode_connection"
|
||||
CreateWorkspaceBuildReasonJetbrainsConnection CreateWorkspaceBuildReason = "jetbrains_connection"
|
||||
CreateWorkspaceBuildReasonTaskManualPause CreateWorkspaceBuildReason = "task_manual_pause"
|
||||
CreateWorkspaceBuildReasonTaskResume CreateWorkspaceBuildReason = "task_resume"
|
||||
)
|
||||
|
||||
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
|
||||
|
||||
Generated
+222
-3
@@ -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`, `task_manual_pause`, `vscode_connection` |
|
||||
| Value(s) |
|
||||
|-----------------------------------------------------------------------------------------------------------------------|
|
||||
| `cli`, `dashboard`, `jetbrains_connection`, `ssh_connection`, `task_manual_pause`, `task_resume`, `vscode_connection` |
|
||||
|
||||
## codersdk.CreateWorkspaceBuildRequest
|
||||
|
||||
@@ -7522,6 +7522,225 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
| `message` | string | false | | Message is an actionable message that depicts actions the request took. These messages should be fully formed sentences with proper punctuation. Examples: - "A user has been created." - "Failed to create a user." |
|
||||
| `validations` | array of [codersdk.ValidationError](#codersdkvalidationerror) | false | | Validations are form field-specific friendly error messages. They will be shown on a form field in the UI. These can also be used to add additional context if there is a set of errors in the primary 'Message'. |
|
||||
|
||||
## codersdk.ResumeTaskResponse
|
||||
|
||||
```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.RetentionConfig
|
||||
|
||||
```json
|
||||
|
||||
Generated
+32
@@ -397,6 +397,38 @@ curl -X POST http://coder-server:8080/api/v2/tasks/{user}/{task}/pause \
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Resume task
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/tasks/{user}/{task}/resume \
|
||||
-H 'Accept: */*' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /tasks/{user}/{task}/resume`
|
||||
|
||||
### 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.ResumeTaskResponse](schemas.md#codersdkresumetaskresponse) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Send input to AI task
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+10
@@ -1426,6 +1426,7 @@ export type CreateWorkspaceBuildReason =
|
||||
| "jetbrains_connection"
|
||||
| "ssh_connection"
|
||||
| "task_manual_pause"
|
||||
| "task_resume"
|
||||
| "vscode_connection";
|
||||
|
||||
export const CreateWorkspaceBuildReasons: CreateWorkspaceBuildReason[] = [
|
||||
@@ -1434,6 +1435,7 @@ export const CreateWorkspaceBuildReasons: CreateWorkspaceBuildReason[] = [
|
||||
"jetbrains_connection",
|
||||
"ssh_connection",
|
||||
"task_manual_pause",
|
||||
"task_resume",
|
||||
"vscode_connection",
|
||||
];
|
||||
|
||||
@@ -4355,6 +4357,14 @@ export interface Response {
|
||||
readonly validations?: readonly ValidationError[];
|
||||
}
|
||||
|
||||
// From codersdk/aitasks.go
|
||||
/**
|
||||
* ResumeTaskResponse represents the response from resuming a task.
|
||||
*/
|
||||
export interface ResumeTaskResponse {
|
||||
readonly workspace_build: WorkspaceBuild | null;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
/**
|
||||
* RetentionConfig contains configuration for data retention policies.
|
||||
|
||||
Reference in New Issue
Block a user