Skip to content

Commit 72a1842

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

27 files changed

Lines changed: 807 additions & 606 deletions

‎cli/exp_task.go‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
package cli
22

33
import (
4+
"context"
5+
"strings"
6+
7+
"github.com/google/uuid"
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/coder/v2/codersdk"
411
"github.com/coder/serpent"
512
)
613

@@ -23,3 +30,71 @@ func (r *RootCmd) tasksCommand() *serpent.Command {
2330
}
2431
return cmd
2532
}
33+
34+
func splitTaskIdentifier(identifier string) (owner string, taskName string, err error) {
35+
parts := strings.Split(identifier, "/")
36+
37+
switch len(parts) {
38+
case 1:
39+
owner = codersdk.Me
40+
taskName = parts[0]
41+
case 2:
42+
owner = parts[0]
43+
taskName = parts[1]
44+
default:
45+
return "", "", xerrors.Errorf("invalid task identifier: %q", identifier)
46+
}
47+
return owner, taskName, nil
48+
}
49+
50+
// resolveTask fetches and returns a task by an identifier, which may be either
51+
// a UUID, a bare name (for a task owned by the current user), or a "user/task"
52+
// combination, where user is either a username or UUID.
53+
//
54+
// Since there is no TaskByOwnerAndName endpoint yet, this function uses the
55+
// list endpoint with filtering when a name is provided.
56+
func resolveTask(ctx context.Context, client *codersdk.Client, identifier string) (codersdk.Task, error) {
57+
exp := codersdk.NewExperimentalClient(client)
58+
59+
identifier = strings.TrimSpace(identifier)
60+
61+
// Try parsing as UUID first.
62+
if taskID, err := uuid.Parse(identifier); err == nil {
63+
return exp.TaskByID(ctx, taskID)
64+
}
65+
66+
// Not a UUID, treat as identifier.
67+
owner, taskName, err := splitTaskIdentifier(identifier)
68+
if err != nil {
69+
return codersdk.Task{}, err
70+
}
71+
72+
tasks, err := exp.Tasks(ctx, &codersdk.TasksFilter{
73+
Owner: owner,
74+
})
75+
if err != nil {
76+
return codersdk.Task{}, xerrors.Errorf("list tasks for owner %q: %w", owner, err)
77+
}
78+
79+
if taskID, err := uuid.Parse(taskName); err == nil {
80+
// Find task by ID.
81+
for _, task := range tasks {
82+
if task.ID == taskID {
83+
return task, nil
84+
}
85+
}
86+
} else {
87+
// Find task by name.
88+
for _, task := range tasks {
89+
if task.Name == taskName {
90+
return task, nil
91+
}
92+
}
93+
}
94+
95+
// Mimic resource not found from API.
96+
var notFoundErr error = &codersdk.Error{
97+
Response: codersdk.Response{Message: "Resource not found or you do not have access to this resource"},
98+
}
99+
return codersdk.Task{}, xerrors.Errorf("task %q not found for owner %q: %w", taskName, owner, notFoundErr)
100+
}

‎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 := resolveTask(ctx, client, 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("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("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("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("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_internal_test.go‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/codersdk"
10+
)
11+
12+
func Test_splitTaskIdentifier(t *testing.T) {
13+
t.Parallel()
14+
15+
tests := []struct {
16+
name string
17+
identifier string
18+
expectedOwner string
19+
expectedTask string
20+
expectErr bool
21+
}{
22+
{
23+
name: "bare task name",
24+
identifier: "mytask",
25+
expectedOwner: codersdk.Me,
26+
expectedTask: "mytask",
27+
expectErr: false,
28+
},
29+
{
30+
name: "owner/task format",
31+
identifier: "alice/her-task",
32+
expectedOwner: "alice",
33+
expectedTask: "her-task",
34+
expectErr: false,
35+
},
36+
{
37+
name: "uuid/task format",
38+
identifier: "550e8400-e29b-41d4-a716-446655440000/task1",
39+
expectedOwner: "550e8400-e29b-41d4-a716-446655440000",
40+
expectedTask: "task1",
41+
expectErr: false,
42+
},
43+
{
44+
name: "owner/uuid format",
45+
identifier: "alice/3abe1dcf-cd87-4078-8b54-c0e2058ad2e2",
46+
expectedOwner: "alice",
47+
expectedTask: "3abe1dcf-cd87-4078-8b54-c0e2058ad2e2",
48+
expectErr: false,
49+
},
50+
{
51+
name: "too many slashes",
52+
identifier: "owner/task/extra",
53+
expectErr: true,
54+
},
55+
{
56+
name: "empty parts acceptable",
57+
identifier: "/task",
58+
expectedOwner: "",
59+
expectedTask: "task",
60+
expectErr: false,
61+
},
62+
}
63+
64+
for _, tt := range tests {
65+
t.Run(tt.name, func(t *testing.T) {
66+
t.Parallel()
67+
owner, taskName, err := splitTaskIdentifier(tt.identifier)
68+
if tt.expectErr {
69+
require.Error(t, err)
70+
} else {
71+
require.NoError(t, err)
72+
assert.Equal(t, tt.expectedOwner, owner)
73+
assert.Equal(t, tt.expectedTask, taskName)
74+
}
75+
})
76+
}
77+
}

‎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)

0 commit comments

Comments
 (0)