feat(coderd): use new data model for task delete (#20334)

Updates coder/internal#976
This commit is contained in:
Mathias Fredriksson
2025-10-23 19:45:18 +03:00
committed by GitHub
parent 79728c30fa
commit 9855460524
16 changed files with 355 additions and 102 deletions
-2
View File
@@ -22,8 +22,6 @@ import (
func TestExpTaskDelete(t *testing.T) {
t.Parallel()
t.Skip("TODO(mafredri): Remove, fixed down-stack!")
type testCounters struct {
deleteCalls atomic.Int64
nameResolves atomic.Int64
+56 -68
View File
@@ -593,9 +593,9 @@ func (api *API) tasksList(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Tags Experimental
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param id path string true "Task ID" format(uuid)
// @Param task path string true "Task ID" format(uuid)
// @Success 200 {object} codersdk.Task
// @Router /api/experimental/tasks/{user}/{id} [get]
// @Router /api/experimental/tasks/{user}/{task} [get]
//
// EXPERIMENTAL: This endpoint is experimental and not guaranteed to be stable.
// taskGet is an experimental endpoint to fetch a single AI task by ID
@@ -605,7 +605,7 @@ func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
idStr := chi.URLParam(r, "id")
idStr := chi.URLParam(r, "task")
taskID, err := uuid.Parse(idStr)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -710,83 +710,71 @@ func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Tags Experimental
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param id path string true "Task ID" format(uuid)
// @Param task path string true "Task ID" format(uuid)
// @Success 202 "Task deletion initiated"
// @Router /api/experimental/tasks/{user}/{id} [delete]
// @Router /api/experimental/tasks/{user}/{task} [delete]
//
// EXPERIMENTAL: This endpoint is experimental and not guaranteed to be stable.
// taskDelete is an experimental endpoint to delete a task by ID (workspace ID).
// taskDelete is an experimental endpoint to delete a task by ID.
// It creates a delete workspace build and returns 202 Accepted if the build was
// created.
func (api *API) taskDelete(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
task := httpmw.TaskParam(r)
idStr := chi.URLParam(r, "id")
taskID, err := uuid.Parse(idStr)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Invalid UUID %q for task ID.", idStr),
})
return
now := api.Clock.Now()
if task.WorkspaceID.Valid {
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 before deleting task.",
Detail: err.Error(),
})
return
}
// Construct a request to the workspace build creation handler to
// initiate deletion.
buildReq := codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionDelete,
Reason: "Deleted via tasks API",
}
_, 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
}
}
// For now, taskID = workspaceID, once we have a task data model in
// the DB, we can change this lookup.
workspaceID := taskID
workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceID)
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
_, err := api.Database.DeleteTask(ctx, database.DeleteTaskParams{
ID: task.ID,
DeletedAt: dbtime.Time(now),
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace.",
Message: "Failed to delete task",
Detail: err.Error(),
})
return
}
data, err := api.workspaceData(ctx, []database.Workspace{workspace})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace resources.",
Detail: err.Error(),
})
return
}
if len(data.builds) == 0 || len(data.templates) == 0 {
httpapi.ResourceNotFound(rw)
return
}
if data.builds[0].HasAITask == nil || !*data.builds[0].HasAITask {
httpapi.ResourceNotFound(rw)
return
}
// Construct a request to the workspace build creation handler to
// initiate deletion.
buildReq := codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionDelete,
Reason: "Deleted via tasks API",
}
_, 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
}
// Delete build created successfully.
// Task deleted and delete build created successfully.
rw.WriteHeader(http.StatusAccepted)
}
@@ -796,10 +784,10 @@ func (api *API) taskDelete(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Tags Experimental
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param id path string true "Task ID" format(uuid)
// @Param task path string true "Task ID" format(uuid)
// @Param request body codersdk.TaskSendRequest true "Task input request"
// @Success 204 "Input sent successfully"
// @Router /api/experimental/tasks/{user}/{id}/send [post]
// @Router /api/experimental/tasks/{user}/{task}/send [post]
//
// EXPERIMENTAL: This endpoint is experimental and not guaranteed to be stable.
// taskSend submits task input to the tasks sidebar app by dialing the agent
@@ -808,7 +796,7 @@ func (api *API) taskDelete(rw http.ResponseWriter, r *http.Request) {
func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idStr := chi.URLParam(r, "id")
idStr := chi.URLParam(r, "task")
taskID, err := uuid.Parse(idStr)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -878,9 +866,9 @@ func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Tags Experimental
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param id path string true "Task ID" format(uuid)
// @Param task path string true "Task ID" format(uuid)
// @Success 200 {object} codersdk.TaskLogsResponse
// @Router /api/experimental/tasks/{user}/{id}/logs [get]
// @Router /api/experimental/tasks/{user}/{task}/logs [get]
//
// EXPERIMENTAL: This endpoint is experimental and not guaranteed to be stable.
// taskLogs reads task output by dialing the agent directly over the tailnet.
@@ -888,7 +876,7 @@ func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) {
func (api *API) taskLogs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idStr := chi.URLParam(r, "id")
idStr := chi.URLParam(r, "task")
taskID, err := uuid.Parse(idStr)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
-2
View File
@@ -343,8 +343,6 @@ func TestTasks(t *testing.T) {
t.Run("Delete", func(t *testing.T) {
t.Parallel()
t.Skip("TODO(mafredri): Remove, fixed down-stack!")
t.Run("OK", func(t *testing.T) {
t.Parallel()
+7 -7
View File
@@ -229,7 +229,7 @@ const docTemplate = `{
}
}
},
"/api/experimental/tasks/{user}/{id}": {
"/api/experimental/tasks/{user}/{task}": {
"get": {
"security": [
{
@@ -253,7 +253,7 @@ const docTemplate = `{
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "id",
"name": "task",
"in": "path",
"required": true
}
@@ -290,7 +290,7 @@ const docTemplate = `{
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "id",
"name": "task",
"in": "path",
"required": true
}
@@ -302,7 +302,7 @@ const docTemplate = `{
}
}
},
"/api/experimental/tasks/{user}/{id}/logs": {
"/api/experimental/tasks/{user}/{task}/logs": {
"get": {
"security": [
{
@@ -326,7 +326,7 @@ const docTemplate = `{
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "id",
"name": "task",
"in": "path",
"required": true
}
@@ -341,7 +341,7 @@ const docTemplate = `{
}
}
},
"/api/experimental/tasks/{user}/{id}/send": {
"/api/experimental/tasks/{user}/{task}/send": {
"post": {
"security": [
{
@@ -365,7 +365,7 @@ const docTemplate = `{
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "id",
"name": "task",
"in": "path",
"required": true
},
+7 -7
View File
@@ -201,7 +201,7 @@
}
}
},
"/api/experimental/tasks/{user}/{id}": {
"/api/experimental/tasks/{user}/{task}": {
"get": {
"security": [
{
@@ -223,7 +223,7 @@
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "id",
"name": "task",
"in": "path",
"required": true
}
@@ -258,7 +258,7 @@
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "id",
"name": "task",
"in": "path",
"required": true
}
@@ -270,7 +270,7 @@
}
}
},
"/api/experimental/tasks/{user}/{id}/logs": {
"/api/experimental/tasks/{user}/{task}/logs": {
"get": {
"security": [
{
@@ -292,7 +292,7 @@
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "id",
"name": "task",
"in": "path",
"required": true
}
@@ -307,7 +307,7 @@
}
}
},
"/api/experimental/tasks/{user}/{id}/send": {
"/api/experimental/tasks/{user}/{task}/send": {
"post": {
"security": [
{
@@ -329,7 +329,7 @@
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "id",
"name": "task",
"in": "path",
"required": true
},
+8 -4
View File
@@ -1032,11 +1032,15 @@ func New(options *Options) *API {
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationMembersParam(options.Database, api.HTTPAuth.Authorize))
r.Get("/{id}", api.taskGet)
r.Delete("/{id}", api.taskDelete)
r.Post("/{id}/send", api.taskSend)
r.Get("/{id}/logs", api.taskLogs)
r.Post("/", api.tasksCreate)
r.Route("/{task}", func(r chi.Router) {
r.Use(httpmw.ExtractTaskParam(options.Database))
r.Get("/", api.taskGet)
r.Delete("/", api.taskDelete)
r.Post("/send", api.taskSend)
r.Get("/logs", api.taskLogs)
})
})
})
r.Route("/mcp", func(r chi.Router) {
+13
View File
@@ -1798,6 +1798,19 @@ func (q *querier) DeleteTailnetTunnel(ctx context.Context, arg database.DeleteTa
return q.db.DeleteTailnetTunnel(ctx, arg)
}
func (q *querier) DeleteTask(ctx context.Context, arg database.DeleteTaskParams) (database.TaskTable, error) {
task, err := q.db.GetTaskByID(ctx, arg.ID)
if err != nil {
return database.TaskTable{}, err
}
if err := q.authorizeContext(ctx, policy.ActionDelete, task.RBACObject()); err != nil {
return database.TaskTable{}, err
}
return q.db.DeleteTask(ctx, arg)
}
func (q *querier) DeleteUserSecret(ctx context.Context, id uuid.UUID) error {
// First get the secret to check ownership
secret, err := q.GetUserSecret(ctx, id)
+10
View File
@@ -2362,6 +2362,16 @@ func (s *MethodTestSuite) TestTasks() {
dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes()
check.Args(task.ID).Asserts(task, policy.ActionRead).Returns(task)
}))
s.Run("DeleteTask", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
task := testutil.Fake(s.T(), faker, database.Task{})
arg := database.DeleteTaskParams{
ID: task.ID,
DeletedAt: dbtime.Now(),
}
dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes()
dbm.EXPECT().DeleteTask(gomock.Any(), arg).Return(database.TaskTable{}, nil).AnyTimes()
check.Args(arg).Asserts(task, policy.ActionDelete).Returns(database.TaskTable{})
}))
s.Run("InsertTask", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
tpl := testutil.Fake(s.T(), faker, database.Template{})
tv := testutil.Fake(s.T(), faker, database.TemplateVersion{
@@ -474,6 +474,13 @@ func (m queryMetricsStore) DeleteTailnetTunnel(ctx context.Context, arg database
return r0, r1
}
func (m queryMetricsStore) DeleteTask(ctx context.Context, arg database.DeleteTaskParams) (database.TaskTable, error) {
start := time.Now()
r0, r1 := m.s.DeleteTask(ctx, arg)
m.queryLatencies.WithLabelValues("DeleteTask").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) DeleteUserSecret(ctx context.Context, id uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteUserSecret(ctx, id)
+15
View File
@@ -880,6 +880,21 @@ func (mr *MockStoreMockRecorder) DeleteTailnetTunnel(ctx, arg any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetTunnel", reflect.TypeOf((*MockStore)(nil).DeleteTailnetTunnel), ctx, arg)
}
// DeleteTask mocks base method.
func (m *MockStore) DeleteTask(ctx context.Context, arg database.DeleteTaskParams) (database.TaskTable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteTask", ctx, arg)
ret0, _ := ret[0].(database.TaskTable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteTask indicates an expected call of DeleteTask.
func (mr *MockStoreMockRecorder) DeleteTask(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTask", reflect.TypeOf((*MockStore)(nil).DeleteTask), ctx, arg)
}
// DeleteUserSecret mocks base method.
func (m *MockStore) DeleteUserSecret(ctx context.Context, id uuid.UUID) error {
m.ctrl.T.Helper()
+1
View File
@@ -120,6 +120,7 @@ type sqlcQuerier interface {
DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error
DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error)
DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error)
DeleteTask(ctx context.Context, arg DeleteTaskParams) (TaskTable, error)
DeleteUserSecret(ctx context.Context, id uuid.UUID) error
DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg DeleteWebpushSubscriptionByUserIDAndEndpointParams) error
DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error
+33
View File
@@ -12581,6 +12581,39 @@ func (q *sqlQuerier) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetT
return i, err
}
const deleteTask = `-- name: DeleteTask :one
UPDATE tasks
SET
deleted_at = $1::timestamptz
WHERE
id = $2::uuid
AND deleted_at IS NULL
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at
`
type DeleteTaskParams struct {
DeletedAt time.Time `db:"deleted_at" json:"deleted_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) DeleteTask(ctx context.Context, arg DeleteTaskParams) (TaskTable, error) {
row := q.db.QueryRowContext(ctx, deleteTask, arg.DeletedAt, arg.ID)
var i TaskTable
err := row.Scan(
&i.ID,
&i.OrganizationID,
&i.OwnerID,
&i.Name,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.TemplateParameters,
&i.Prompt,
&i.CreatedAt,
&i.DeletedAt,
)
return i, err
}
const getTaskByID = `-- name: GetTaskByID :one
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status, workspace_build_number, workspace_agent_id, workspace_app_id FROM tasks_with_status WHERE id = $1::uuid
`
+9
View File
@@ -47,3 +47,12 @@ WHERE tws.deleted_at IS NULL
AND CASE WHEN @owner_id::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.owner_id = @owner_id::UUID ELSE TRUE END
AND CASE WHEN @organization_id::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.organization_id = @organization_id::UUID ELSE TRUE END
ORDER BY tws.created_at DESC;
-- name: DeleteTask :one
UPDATE tasks
SET
deleted_at = @deleted_at::timestamptz
WHERE
id = @id::uuid
AND deleted_at IS NULL
RETURNING *;
+57
View File
@@ -0,0 +1,57 @@
package httpmw
import (
"context"
"net/http"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw/loggermw"
"github.com/coder/coder/v2/codersdk"
)
type taskParamContextKey struct{}
// TaskParam returns the task from the ExtractTaskParam handler.
func TaskParam(r *http.Request) database.Task {
task, ok := r.Context().Value(taskParamContextKey{}).(database.Task)
if !ok {
panic("developer error: task param middleware not provided")
}
return task
}
// ExtractTaskParam grabs a task from the "task" URL parameter by UUID.
func ExtractTaskParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
taskID, parsed := ParseUUIDParam(rw, r, "task")
if !parsed {
return
}
task, err := db.GetTaskByID(ctx, taskID)
if err != nil {
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching task.",
Detail: err.Error(),
})
return
}
ctx = context.WithValue(ctx, taskParamContextKey{}, task)
if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil {
rlogger.WithFields(slog.F("task_id", task.ID), slog.F("task_name", task.Name))
}
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
+120
View File
@@ -0,0 +1,120 @@
package httpmw_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/codersdk"
)
func TestTaskParam(t *testing.T) {
t.Parallel()
setup := func(db database.Store) (*http.Request, database.User) {
user := dbgen.User(t, db, database.User{})
_, token := dbgen.APIKey(t, db, database.APIKey{
UserID: user.ID,
})
r := httptest.NewRequest("GET", "/", nil)
r.Header.Set(codersdk.SessionTokenHeader, token)
ctx := chi.NewRouteContext()
ctx.URLParams.Add("user", "me")
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
return r, user
}
t.Run("None", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
rtr := chi.NewRouter()
rtr.Use(httpmw.ExtractTaskParam(db))
rtr.Get("/", nil)
r, _ := setup(db)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
rtr := chi.NewRouter()
rtr.Use(httpmw.ExtractTaskParam(db))
rtr.Get("/", nil)
r, _ := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("task", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
DB: db,
RedirectToLogin: false,
}),
httpmw.ExtractTaskParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.TaskParam(r)
rw.WriteHeader(http.StatusOK)
})
r, user := setup(db)
org := dbgen.Organization(t, db, database.Organization{})
tpl := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
CreatedBy: user.ID,
})
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
TemplateID: uuid.NullUUID{
UUID: tpl.ID,
Valid: true,
},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
Name: "test-workspace",
OrganizationID: org.ID,
TemplateID: tpl.ID,
})
task := dbgen.Task(t, db, database.TaskTable{
Name: "test-task",
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateVersionID: tv.ID,
WorkspaceID: uuid.NullUUID{UUID: workspace.ID, Valid: true},
Prompt: "test prompt",
})
chi.RouteContext(r.Context()).URLParams.Add("task", task.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
}
+12 -12
View File
@@ -84,19 +84,19 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{id} \
curl -X GET http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task} \
-H 'Accept: */*' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /api/experimental/tasks/{user}/{id}`
`GET /api/experimental/tasks/{user}/{task}`
### Parameters
| Name | In | Type | Required | Description |
|--------|------|--------------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `id` | path | string(uuid) | true | Task ID |
| `task` | path | string(uuid) | true | Task ID |
### Example responses
@@ -116,18 +116,18 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio
```shell
# Example request using curl
curl -X DELETE http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{id} \
curl -X DELETE http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task} \
-H 'Coder-Session-Token: API_KEY'
```
`DELETE /api/experimental/tasks/{user}/{id}`
`DELETE /api/experimental/tasks/{user}/{task}`
### Parameters
| Name | In | Type | Required | Description |
|--------|------|--------------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `id` | path | string(uuid) | true | Task ID |
| `task` | path | string(uuid) | true | Task ID |
### Responses
@@ -143,19 +143,19 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{id}/logs \
curl -X GET http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task}/logs \
-H 'Accept: */*' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /api/experimental/tasks/{user}/{id}/logs`
`GET /api/experimental/tasks/{user}/{task}/logs`
### Parameters
| Name | In | Type | Required | Description |
|--------|------|--------------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `id` | path | string(uuid) | true | Task ID |
| `task` | path | string(uuid) | true | Task ID |
### Example responses
@@ -175,12 +175,12 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio
```shell
# Example request using curl
curl -X POST http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{id}/send \
curl -X POST http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task}/send \
-H 'Content-Type: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`POST /api/experimental/tasks/{user}/{id}/send`
`POST /api/experimental/tasks/{user}/{task}/send`
> Body parameter
@@ -195,7 +195,7 @@ curl -X POST http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{id}/
| Name | In | Type | Required | Description |
|--------|------|----------------------------------------------------------------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `id` | path | string(uuid) | true | Task ID |
| `task` | path | string(uuid) | true | Task ID |
| `body` | body | [codersdk.TaskSendRequest](schemas.md#codersdktasksendrequest) | true | Task input request |
### Responses