mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
9d2aed88c4
The pause/resume endpoints were only registered under /api/experimental but the frontend and Go SDK were calling /api/v2, resulting in 404s. Register the routes in the v2 group, update the SDK client paths, and fix swagger annotations (Accept → Produce) since these POST endpoints have no request body.
424 lines
15 KiB
Go
424 lines
15 KiB
Go
package codersdk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
// CreateTaskRequest represents the request to create a new task.
|
|
type CreateTaskRequest struct {
|
|
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"`
|
|
TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"`
|
|
Input string `json:"input"`
|
|
Name string `json:"name,omitempty"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
}
|
|
|
|
// CreateTask creates a new task.
|
|
func (c *Client) CreateTask(ctx context.Context, user string, request CreateTaskRequest) (Task, error) {
|
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/tasks/%s", user), request)
|
|
if err != nil {
|
|
return Task{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusCreated {
|
|
return Task{}, ReadBodyAsError(res)
|
|
}
|
|
|
|
var task Task
|
|
if err := json.NewDecoder(res.Body).Decode(&task); err != nil {
|
|
return Task{}, err
|
|
}
|
|
|
|
return task, nil
|
|
}
|
|
|
|
// TaskStatus represents the status of a task.
|
|
type TaskStatus string
|
|
|
|
const (
|
|
// TaskStatusPending indicates the task has been created but no workspace
|
|
// has been provisioned yet, or the workspace build job status is unknown.
|
|
TaskStatusPending TaskStatus = "pending"
|
|
// TaskStatusInitializing indicates the workspace build is pending/running,
|
|
// the agent is connecting, or apps are initializing.
|
|
TaskStatusInitializing TaskStatus = "initializing"
|
|
// TaskStatusActive indicates the task's workspace is running with a
|
|
// successful start transition, the agent is connected, and all workspace
|
|
// apps are either healthy or disabled.
|
|
TaskStatusActive TaskStatus = "active"
|
|
// TaskStatusPaused indicates the task's workspace has been stopped or
|
|
// deleted (stop/delete transition with successful job status).
|
|
TaskStatusPaused TaskStatus = "paused"
|
|
// TaskStatusUnknown indicates the task's status cannot be determined
|
|
// based on the workspace build, agent lifecycle, or app health states.
|
|
TaskStatusUnknown TaskStatus = "unknown"
|
|
// TaskStatusError indicates the task's workspace build job has failed,
|
|
// or the workspace apps are reporting unhealthy status.
|
|
TaskStatusError TaskStatus = "error"
|
|
)
|
|
|
|
func AllTaskStatuses() []TaskStatus {
|
|
return []TaskStatus{
|
|
TaskStatusPending,
|
|
TaskStatusInitializing,
|
|
TaskStatusActive,
|
|
TaskStatusPaused,
|
|
TaskStatusError,
|
|
TaskStatusUnknown,
|
|
}
|
|
}
|
|
|
|
// TaskState represents the high-level lifecycle of a task.
|
|
type TaskState string
|
|
|
|
// TaskState enums.
|
|
const (
|
|
// TaskStateWorking indicates the AI agent is actively processing work.
|
|
// Reported when the agent is performing actions or the screen is changing.
|
|
TaskStateWorking TaskState = "working"
|
|
// TaskStateIdle indicates the AI agent's screen is stable and no work
|
|
// is being performed. Reported automatically by the screen watcher.
|
|
TaskStateIdle TaskState = "idle"
|
|
// TaskStateComplete indicates the AI agent has successfully completed
|
|
// the task. Reported via the workspace app status.
|
|
TaskStateComplete TaskState = "complete"
|
|
// TaskStateFailed indicates the AI agent reported a failure state.
|
|
// Reported via the workspace app status.
|
|
TaskStateFailed TaskState = "failed"
|
|
)
|
|
|
|
// Task represents a task.
|
|
type Task struct {
|
|
ID uuid.UUID `json:"id" format:"uuid" table:"id"`
|
|
OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"`
|
|
OwnerID uuid.UUID `json:"owner_id" format:"uuid" table:"owner id"`
|
|
OwnerName string `json:"owner_name" table:"owner name"`
|
|
OwnerAvatarURL string `json:"owner_avatar_url,omitempty" table:"owner avatar url"`
|
|
Name string `json:"name" table:"name,default_sort"`
|
|
DisplayName string `json:"display_name" table:"display_name"`
|
|
TemplateID uuid.UUID `json:"template_id" format:"uuid" table:"template id"`
|
|
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid" table:"template version id"`
|
|
TemplateName string `json:"template_name" table:"template name"`
|
|
TemplateDisplayName string `json:"template_display_name" table:"template display name"`
|
|
TemplateIcon string `json:"template_icon" table:"template icon"`
|
|
WorkspaceID uuid.NullUUID `json:"workspace_id" format:"uuid" table:"workspace id"`
|
|
WorkspaceName string `json:"workspace_name" table:"workspace name"`
|
|
WorkspaceStatus WorkspaceStatus `json:"workspace_status,omitempty" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted" table:"workspace status"`
|
|
WorkspaceBuildNumber int32 `json:"workspace_build_number,omitempty" table:"workspace build number"`
|
|
WorkspaceAgentID uuid.NullUUID `json:"workspace_agent_id" format:"uuid" table:"workspace agent id"`
|
|
WorkspaceAgentLifecycle *WorkspaceAgentLifecycle `json:"workspace_agent_lifecycle" table:"workspace agent lifecycle"`
|
|
WorkspaceAgentHealth *WorkspaceAgentHealth `json:"workspace_agent_health" table:"workspace agent health"`
|
|
WorkspaceAppID uuid.NullUUID `json:"workspace_app_id" format:"uuid" table:"workspace app id"`
|
|
InitialPrompt string `json:"initial_prompt" table:"initial prompt"`
|
|
Status TaskStatus `json:"status" enums:"pending,initializing,active,paused,unknown,error" table:"status"`
|
|
CurrentState *TaskStateEntry `json:"current_state" table:"cs,recursive_inline,empty_nil"`
|
|
CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at"`
|
|
UpdatedAt time.Time `json:"updated_at" format:"date-time" table:"updated at"`
|
|
}
|
|
|
|
// TaskStateEntry represents a single entry in the task's state history.
|
|
type TaskStateEntry struct {
|
|
Timestamp time.Time `json:"timestamp" format:"date-time" table:"-"`
|
|
State TaskState `json:"state" enum:"working,idle,completed,failed" table:"state"`
|
|
Message string `json:"message" table:"message"`
|
|
URI string `json:"uri" table:"-"`
|
|
}
|
|
|
|
// TasksFilter filters the list of tasks.
|
|
type TasksFilter struct {
|
|
// Owner can be a username, UUID, or "me".
|
|
Owner string `json:"owner,omitempty"`
|
|
// Organization can be an organization name or UUID.
|
|
Organization string `json:"organization,omitempty"`
|
|
// Status filters the tasks by their task status.
|
|
Status TaskStatus `json:"status,omitempty"`
|
|
// FilterQuery allows specifying a raw filter query.
|
|
FilterQuery string `json:"filter_query,omitempty"`
|
|
}
|
|
|
|
// TaskListResponse is the response shape for tasks list.
|
|
type TasksListResponse struct {
|
|
Tasks []Task `json:"tasks"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
func (f TasksFilter) asRequestOption() RequestOption {
|
|
return func(r *http.Request) {
|
|
var params []string
|
|
// Make sure all user input is quoted to ensure it's parsed as a single
|
|
// string.
|
|
if f.Owner != "" {
|
|
params = append(params, fmt.Sprintf("owner:%q", f.Owner))
|
|
}
|
|
if f.Organization != "" {
|
|
params = append(params, fmt.Sprintf("organization:%q", f.Organization))
|
|
}
|
|
if f.Status != "" {
|
|
params = append(params, fmt.Sprintf("status:%q", string(f.Status)))
|
|
}
|
|
if f.FilterQuery != "" {
|
|
// If custom stuff is added, just add it on here.
|
|
params = append(params, f.FilterQuery)
|
|
}
|
|
|
|
q := r.URL.Query()
|
|
q.Set("q", strings.Join(params, " "))
|
|
r.URL.RawQuery = q.Encode()
|
|
}
|
|
}
|
|
|
|
// Tasks lists all tasks belonging to the user or specified owner.
|
|
func (c *Client) Tasks(ctx context.Context, filter *TasksFilter) ([]Task, error) {
|
|
if filter == nil {
|
|
filter = &TasksFilter{}
|
|
}
|
|
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/tasks", nil, filter.asRequestOption())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, ReadBodyAsError(res)
|
|
}
|
|
|
|
var tres TasksListResponse
|
|
if err := json.NewDecoder(res.Body).Decode(&tres); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tres.Tasks, nil
|
|
}
|
|
|
|
// TaskByID fetches a single task by its ID.
|
|
// Only tasks owned by codersdk.Me are supported.
|
|
func (c *Client) TaskByID(ctx context.Context, id uuid.UUID) (Task, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/tasks/%s/%s", "me", id.String()), nil)
|
|
if err != nil {
|
|
return Task{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return Task{}, ReadBodyAsError(res)
|
|
}
|
|
|
|
var task Task
|
|
if err := json.NewDecoder(res.Body).Decode(&task); err != nil {
|
|
return Task{}, err
|
|
}
|
|
|
|
return task, nil
|
|
}
|
|
|
|
// TaskByOwnerAndName fetches a single task by its owner and name.
|
|
func (c *Client) TaskByOwnerAndName(ctx context.Context, owner, ident string) (Task, error) {
|
|
if owner == "" {
|
|
owner = Me
|
|
}
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/tasks/%s/%s", owner, ident), nil)
|
|
if err != nil {
|
|
return Task{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return Task{}, ReadBodyAsError(res)
|
|
}
|
|
|
|
var task Task
|
|
if err := json.NewDecoder(res.Body).Decode(&task); err != nil {
|
|
return Task{}, err
|
|
}
|
|
|
|
return task, nil
|
|
}
|
|
|
|
func splitTaskIdentifier(identifier string) (owner string, taskName string, err error) {
|
|
parts := strings.Split(identifier, "/")
|
|
|
|
switch len(parts) {
|
|
case 1:
|
|
owner = Me
|
|
taskName = parts[0]
|
|
case 2:
|
|
owner = parts[0]
|
|
taskName = parts[1]
|
|
default:
|
|
return "", "", xerrors.Errorf("invalid task identifier: %q", identifier)
|
|
}
|
|
return owner, taskName, nil
|
|
}
|
|
|
|
// TaskByIdentifier fetches and returns a task by an identifier, which may be
|
|
// either a UUID, a name (for a task owned by the current user), or a
|
|
// "user/task" combination, where user is either a username or UUID.
|
|
//
|
|
// Since there is no TaskByOwnerAndName endpoint yet, this function uses the
|
|
// list endpoint with filtering when a name is provided.
|
|
func (c *Client) TaskByIdentifier(ctx context.Context, identifier string) (Task, error) {
|
|
identifier = strings.TrimSpace(identifier)
|
|
|
|
// Try parsing as UUID first.
|
|
if taskID, err := uuid.Parse(identifier); err == nil {
|
|
return c.TaskByID(ctx, taskID)
|
|
}
|
|
|
|
// Not a UUID, treat as identifier.
|
|
owner, taskName, err := splitTaskIdentifier(identifier)
|
|
if err != nil {
|
|
return Task{}, err
|
|
}
|
|
|
|
return c.TaskByOwnerAndName(ctx, owner, taskName)
|
|
}
|
|
|
|
// DeleteTask deletes a task by its ID.
|
|
func (c *Client) DeleteTask(ctx context.Context, user string, id uuid.UUID) error {
|
|
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/tasks/%s/%s", user, id.String()), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusAccepted {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TaskSendRequest is used to send task input to the tasks sidebar app.
|
|
type TaskSendRequest struct {
|
|
Input string `json:"input"`
|
|
}
|
|
|
|
// TaskSend submits task input to the tasks sidebar app.
|
|
func (c *Client) TaskSend(ctx context.Context, user string, id uuid.UUID, req TaskSendRequest) error {
|
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/tasks/%s/%s/send", user, id.String()), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateTaskInputRequest is used to update a task's input.
|
|
type UpdateTaskInputRequest struct {
|
|
Input string `json:"input"`
|
|
}
|
|
|
|
// UpdateTaskInput updates the task's input.
|
|
func (c *Client) UpdateTaskInput(ctx context.Context, user string, id uuid.UUID, req UpdateTaskInputRequest) error {
|
|
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/tasks/%s/%s/input", user, id.String()), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
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.
|
|
func (c *Client) PauseTask(ctx context.Context, user string, id uuid.UUID) (PauseTaskResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/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
|
|
}
|
|
|
|
// 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/v2/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
|
|
|
|
// TaskLogType enums.
|
|
const (
|
|
TaskLogTypeInput TaskLogType = "input"
|
|
TaskLogTypeOutput TaskLogType = "output"
|
|
)
|
|
|
|
// TaskLogEntry represents a single log entry for a task.
|
|
type TaskLogEntry struct {
|
|
ID int `json:"id" table:"id"`
|
|
Content string `json:"content" table:"content"`
|
|
Type TaskLogType `json:"type" enum:"input,output" table:"type"`
|
|
Time time.Time `json:"time" format:"date-time" table:"time,default_sort"`
|
|
}
|
|
|
|
// TaskLogsResponse contains task logs and metadata. When snapshot is false,
|
|
// logs are fetched live from the task app. When snapshot is true, logs are
|
|
// fetched from a stored snapshot captured during pause.
|
|
type TaskLogsResponse struct {
|
|
Logs []TaskLogEntry `json:"logs"`
|
|
Snapshot bool `json:"snapshot,omitempty"`
|
|
SnapshotAt *time.Time `json:"snapshot_at,omitempty"`
|
|
}
|
|
|
|
// TaskLogs retrieves logs from the task app.
|
|
func (c *Client) TaskLogs(ctx context.Context, user string, id uuid.UUID) (TaskLogsResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/tasks/%s/%s/logs", user, id.String()), nil)
|
|
if err != nil {
|
|
return TaskLogsResponse{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return TaskLogsResponse{}, ReadBodyAsError(res)
|
|
}
|
|
|
|
var logs TaskLogsResponse
|
|
if err := json.NewDecoder(res.Body).Decode(&logs); err != nil {
|
|
return TaskLogsResponse{}, xerrors.Errorf("decoding task logs response: %w", err)
|
|
}
|
|
|
|
return logs, nil
|
|
}
|