mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(coderd): use new data model for task delete (#20334)
Updates coder/internal#976
This commit is contained in:
committed by
GitHub
parent
79728c30fa
commit
9855460524
@@ -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
@@ -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{
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Generated
+7
-7
@@ -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
|
||||
},
|
||||
|
||||
Generated
+7
-7
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
`
|
||||
|
||||
@@ -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 *;
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Generated
+12
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user