Skip to content

Commit a106d67

Browse files
authored
feat(coderd): use task data model for list (coder#20394)
Updates coder/internal#976
1 parent 2c6cbf1 commit a106d67

28 files changed

+976
-613
lines changed

cli/exp_task_delete.go

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"strings"
66
"time"
77

8-
"github.com/google/uuid"
98
"golang.org/x/xerrors"
109

1110
"github.com/coder/pretty"
@@ -47,43 +46,19 @@ func (r *RootCmd) taskDelete() *serpent.Command {
4746
}
4847
exp := codersdk.NewExperimentalClient(client)
4948

50-
type toDelete struct {
51-
ID uuid.UUID
52-
Owner string
53-
Display string
54-
}
55-
56-
var items []toDelete
49+
var tasks []codersdk.Task
5750
for _, identifier := range inv.Args {
58-
identifier = strings.TrimSpace(identifier)
59-
if identifier == "" {
60-
return xerrors.New("task identifier cannot be empty or whitespace")
61-
}
62-
63-
// Check task identifier, try UUID first.
64-
if id, err := uuid.Parse(identifier); err == nil {
65-
task, err := exp.TaskByID(ctx, id)
66-
if err != nil {
67-
return xerrors.Errorf("resolve task %q: %w", identifier, err)
68-
}
69-
display := fmt.Sprintf("%s/%s", task.OwnerName, task.Name)
70-
items = append(items, toDelete{ID: id, Display: display, Owner: task.OwnerName})
71-
continue
72-
}
73-
74-
// Non-UUID, treat as a workspace identifier (name or owner/name).
75-
ws, err := namedWorkspace(ctx, client, identifier)
51+
task, err := exp.TaskByIdentifier(ctx, identifier)
7652
if err != nil {
7753
return xerrors.Errorf("resolve task %q: %w", identifier, err)
7854
}
79-
display := ws.FullName()
80-
items = append(items, toDelete{ID: ws.ID, Display: display, Owner: ws.OwnerName})
55+
tasks = append(tasks, task)
8156
}
8257

8358
// Confirm deletion of the tasks.
8459
var displayList []string
85-
for _, it := range items {
86-
displayList = append(displayList, it.Display)
60+
for _, task := range tasks {
61+
displayList = append(displayList, fmt.Sprintf("%s/%s", task.OwnerName, task.Name))
8762
}
8863
_, err = cliui.Prompt(inv, cliui.PromptOptions{
8964
Text: fmt.Sprintf("Delete these tasks: %s?", pretty.Sprint(cliui.DefaultStyles.Code, strings.Join(displayList, ", "))),
@@ -94,12 +69,13 @@ func (r *RootCmd) taskDelete() *serpent.Command {
9469
return err
9570
}
9671

97-
for _, item := range items {
98-
if err := exp.DeleteTask(ctx, item.Owner, item.ID); err != nil {
99-
return xerrors.Errorf("delete task %q: %w", item.Display, err)
72+
for i, task := range tasks {
73+
display := displayList[i]
74+
if err := exp.DeleteTask(ctx, task.OwnerName, task.ID); err != nil {
75+
return xerrors.Errorf("delete task %q: %w", display, err)
10076
}
10177
_, _ = fmt.Fprintln(
102-
inv.Stdout, "Deleted task "+pretty.Sprint(cliui.DefaultStyles.Keyword, item.Display)+" at "+cliui.Timestamp(time.Now()),
78+
inv.Stdout, "Deleted task "+pretty.Sprint(cliui.DefaultStyles.Keyword, display)+" at "+cliui.Timestamp(time.Now()),
10379
)
10480
}
10581

cli/exp_task_delete_test.go

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,18 @@ func TestExpTaskDelete(t *testing.T) {
5656
taskID := uuid.MustParse(id1)
5757
return func(w http.ResponseWriter, r *http.Request) {
5858
switch {
59-
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/users/me/workspace/exists":
59+
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks" && r.URL.Query().Get("q") == "owner:\"me\"":
6060
c.nameResolves.Add(1)
61-
httpapi.Write(r.Context(), w, http.StatusOK, codersdk.Workspace{
62-
ID: taskID,
63-
Name: "exists",
64-
OwnerName: "me",
61+
httpapi.Write(r.Context(), w, http.StatusOK, struct {
62+
Tasks []codersdk.Task `json:"tasks"`
63+
Count int `json:"count"`
64+
}{
65+
Tasks: []codersdk.Task{{
66+
ID: taskID,
67+
Name: "exists",
68+
OwnerName: "me",
69+
}},
70+
Count: 1,
6571
})
6672
case r.Method == http.MethodDelete && r.URL.Path == "/api/experimental/tasks/me/"+id1:
6773
c.deleteCalls.Add(1)
@@ -104,12 +110,18 @@ func TestExpTaskDelete(t *testing.T) {
104110
firstID := uuid.MustParse(id3)
105111
return func(w http.ResponseWriter, r *http.Request) {
106112
switch {
107-
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/users/me/workspace/first":
113+
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks" && r.URL.Query().Get("q") == "owner:\"me\"":
108114
c.nameResolves.Add(1)
109-
httpapi.Write(r.Context(), w, http.StatusOK, codersdk.Workspace{
110-
ID: firstID,
111-
Name: "first",
112-
OwnerName: "me",
115+
httpapi.Write(r.Context(), w, http.StatusOK, struct {
116+
Tasks []codersdk.Task `json:"tasks"`
117+
Count int `json:"count"`
118+
}{
119+
Tasks: []codersdk.Task{{
120+
ID: firstID,
121+
Name: "first",
122+
OwnerName: "me",
123+
}},
124+
Count: 1,
113125
})
114126
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks/me/"+id4:
115127
httpapi.Write(r.Context(), w, http.StatusOK, codersdk.Task{
@@ -139,8 +151,14 @@ func TestExpTaskDelete(t *testing.T) {
139151
buildHandler: func(_ *testCounters) http.HandlerFunc {
140152
return func(w http.ResponseWriter, r *http.Request) {
141153
switch {
142-
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/users/me/workspace/doesnotexist":
143-
httpapi.ResourceNotFound(w)
154+
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks" && r.URL.Query().Get("q") == "owner:\"me\"":
155+
httpapi.Write(r.Context(), w, http.StatusOK, struct {
156+
Tasks []codersdk.Task `json:"tasks"`
157+
Count int `json:"count"`
158+
}{
159+
Tasks: []codersdk.Task{},
160+
Count: 0,
161+
})
144162
default:
145163
httpapi.InternalServerError(w, xerrors.New("unwanted path: "+r.Method+" "+r.URL.Path))
146164
}
@@ -156,12 +174,18 @@ func TestExpTaskDelete(t *testing.T) {
156174
taskID := uuid.MustParse(id5)
157175
return func(w http.ResponseWriter, r *http.Request) {
158176
switch {
159-
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/users/me/workspace/bad":
177+
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks" && r.URL.Query().Get("q") == "owner:\"me\"":
160178
c.nameResolves.Add(1)
161-
httpapi.Write(r.Context(), w, http.StatusOK, codersdk.Workspace{
162-
ID: taskID,
163-
Name: "bad",
164-
OwnerName: "me",
179+
httpapi.Write(r.Context(), w, http.StatusOK, struct {
180+
Tasks []codersdk.Task `json:"tasks"`
181+
Count int `json:"count"`
182+
}{
183+
Tasks: []codersdk.Task{{
184+
ID: taskID,
185+
Name: "bad",
186+
OwnerName: "me",
187+
}},
188+
Count: 1,
165189
})
166190
case r.Method == http.MethodDelete && r.URL.Path == "/api/experimental/tasks/me/"+id5:
167191
httpapi.InternalServerError(w, xerrors.New("boom"))

cli/exp_task_list.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"golang.org/x/xerrors"
99

1010
"github.com/coder/coder/v2/cli/cliui"
11+
"github.com/coder/coder/v2/coderd/util/slice"
1112
"github.com/coder/coder/v2/codersdk"
1213
"github.com/coder/serpent"
1314
)
@@ -98,10 +99,10 @@ func (r *RootCmd) taskList() *serpent.Command {
9899
Options: serpent.OptionSet{
99100
{
100101
Name: "status",
101-
Description: "Filter by task status (e.g. running, failed, etc).",
102+
Description: "Filter by task status.",
102103
Flag: "status",
103104
Default: "",
104-
Value: serpent.StringOf(&statusFilter),
105+
Value: serpent.EnumOf(&statusFilter, slice.ToStrings(codersdk.AllTaskStatuses())...),
105106
},
106107
{
107108
Name: "all",
@@ -142,8 +143,8 @@ func (r *RootCmd) taskList() *serpent.Command {
142143
}
143144

144145
tasks, err := exp.Tasks(ctx, &codersdk.TasksFilter{
145-
Owner: targetUser,
146-
WorkspaceStatus: statusFilter,
146+
Owner: targetUser,
147+
Status: codersdk.TaskStatus(statusFilter),
147148
})
148149
if err != nil {
149150
return xerrors.Errorf("list tasks: %w", err)

cli/exp_task_list_test.go

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ import (
2222
"github.com/coder/coder/v2/coderd/database/dbauthz"
2323
"github.com/coder/coder/v2/coderd/database/dbfake"
2424
"github.com/coder/coder/v2/coderd/database/dbgen"
25+
"github.com/coder/coder/v2/coderd/database/dbtime"
2526
"github.com/coder/coder/v2/coderd/util/slice"
2627
"github.com/coder/coder/v2/codersdk"
2728
"github.com/coder/coder/v2/pty/ptytest"
2829
"github.com/coder/coder/v2/testutil"
2930
)
3031

3132
// makeAITask creates an AI-task workspace.
32-
func makeAITask(t *testing.T, db database.Store, orgID, adminID, ownerID uuid.UUID, transition database.WorkspaceTransition, prompt string) (workspace database.WorkspaceTable) {
33+
func makeAITask(t *testing.T, db database.Store, orgID, adminID, ownerID uuid.UUID, transition database.WorkspaceTransition, prompt string) database.Task {
3334
t.Helper()
3435

3536
tv := dbfake.TemplateVersion(t, db).
@@ -91,14 +92,32 @@ func makeAITask(t *testing.T, db database.Store, orgID, adminID, ownerID uuid.UU
9192
)
9293
require.NoError(t, err)
9394

94-
return build.Workspace
95+
// Create a task record in the tasks table for the new data model.
96+
task := dbgen.Task(t, db, database.TaskTable{
97+
OrganizationID: orgID,
98+
OwnerID: ownerID,
99+
Name: build.Workspace.Name,
100+
WorkspaceID: uuid.NullUUID{UUID: build.Workspace.ID, Valid: true},
101+
TemplateVersionID: tv.TemplateVersion.ID,
102+
TemplateParameters: []byte("{}"),
103+
Prompt: prompt,
104+
CreatedAt: dbtime.Now(),
105+
})
106+
107+
// Link the task to the workspace app.
108+
dbgen.TaskWorkspaceApp(t, db, database.TaskWorkspaceApp{
109+
TaskID: task.ID,
110+
WorkspaceBuildNumber: build.Build.BuildNumber,
111+
WorkspaceAgentID: uuid.NullUUID{UUID: agentID, Valid: true},
112+
WorkspaceAppID: uuid.NullUUID{UUID: app.ID, Valid: true},
113+
})
114+
115+
return task
95116
}
96117

97118
func TestExpTaskList(t *testing.T) {
98119
t.Parallel()
99120

100-
t.Skip("TODO(mafredri): Remove, fixed down-stack!")
101-
102121
t.Run("NoTasks_Table", func(t *testing.T) {
103122
t.Parallel()
104123

@@ -130,7 +149,7 @@ func TestExpTaskList(t *testing.T) {
130149
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
131150

132151
wantPrompt := "build me a web app"
133-
ws := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, wantPrompt)
152+
task := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, wantPrompt)
134153

135154
inv, root := clitest.New(t, "exp", "task", "list", "--column", "id,name,status,initial prompt")
136155
clitest.SetupConfig(t, memberClient, root)
@@ -142,7 +161,7 @@ func TestExpTaskList(t *testing.T) {
142161
require.NoError(t, err)
143162

144163
// Validate the table includes the task and status.
145-
pty.ExpectMatch(ws.Name)
164+
pty.ExpectMatch(task.Name)
146165
pty.ExpectMatch("running")
147166
pty.ExpectMatch(wantPrompt)
148167
})
@@ -157,11 +176,11 @@ func TestExpTaskList(t *testing.T) {
157176
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
158177

159178
// Create two AI tasks: one running, one stopped.
160-
running := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, "keep me running")
161-
stopped := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStop, "stop me please")
179+
runningTask := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, "keep me running")
180+
stoppedTask := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStop, "stop me please")
162181

163182
// Use JSON output to reliably validate filtering.
164-
inv, root := clitest.New(t, "exp", "task", "list", "--status=stopped", "--output=json")
183+
inv, root := clitest.New(t, "exp", "task", "list", "--status=paused", "--output=json")
165184
clitest.SetupConfig(t, memberClient, root)
166185

167186
ctx := testutil.Context(t, testutil.WaitShort)
@@ -177,8 +196,8 @@ func TestExpTaskList(t *testing.T) {
177196

178197
// Only the stopped task is returned.
179198
require.Len(t, tasks, 1, "expected one task after filtering")
180-
require.Equal(t, stopped.ID, tasks[0].ID)
181-
require.NotEqual(t, running.ID, tasks[0].ID)
199+
require.Equal(t, stoppedTask.ID, tasks[0].ID)
200+
require.NotEqual(t, runningTask.ID, tasks[0].ID)
182201
})
183202

184203
t.Run("UserFlag_Me_Table", func(t *testing.T) {
@@ -190,7 +209,7 @@ func TestExpTaskList(t *testing.T) {
190209
_, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
191210

192211
_ = makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, "other-task")
193-
ws := makeAITask(t, db, owner.OrganizationID, owner.UserID, owner.UserID, database.WorkspaceTransitionStart, "me-task")
212+
task := makeAITask(t, db, owner.OrganizationID, owner.UserID, owner.UserID, database.WorkspaceTransitionStart, "me-task")
194213

195214
inv, root := clitest.New(t, "exp", "task", "list", "--user", "me")
196215
//nolint:gocritic // Owner client is intended here smoke test the member task not showing up.
@@ -202,7 +221,7 @@ func TestExpTaskList(t *testing.T) {
202221
err := inv.WithContext(ctx).Run()
203222
require.NoError(t, err)
204223

205-
pty.ExpectMatch(ws.Name)
224+
pty.ExpectMatch(task.Name)
206225
})
207226

208227
t.Run("Quiet", func(t *testing.T) {

cli/exp_task_logs.go

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cli
33
import (
44
"fmt"
55

6-
"github.com/google/uuid"
76
"golang.org/x/xerrors"
87

98
"github.com/coder/coder/v2/cli/cliui"
@@ -41,24 +40,17 @@ func (r *RootCmd) taskLogs() *serpent.Command {
4140
}
4241

4342
var (
44-
ctx = inv.Context()
45-
exp = codersdk.NewExperimentalClient(client)
46-
task = inv.Args[0]
47-
taskID uuid.UUID
43+
ctx = inv.Context()
44+
exp = codersdk.NewExperimentalClient(client)
45+
identifier = inv.Args[0]
4846
)
4947

50-
if id, err := uuid.Parse(task); err == nil {
51-
taskID = id
52-
} else {
53-
ws, err := namedWorkspace(ctx, client, task)
54-
if err != nil {
55-
return xerrors.Errorf("resolve task %q: %w", task, err)
56-
}
57-
58-
taskID = ws.ID
48+
task, err := exp.TaskByIdentifier(ctx, identifier)
49+
if err != nil {
50+
return xerrors.Errorf("resolve task %q: %w", identifier, err)
5951
}
6052

61-
logs, err := exp.TaskLogs(ctx, codersdk.Me, taskID)
53+
logs, err := exp.TaskLogs(ctx, codersdk.Me, task.ID)
6254
if err != nil {
6355
return xerrors.Errorf("get task logs: %w", err)
6456
}

0 commit comments

Comments
 (0)