Skip to content

Commit f56a468

Browse files
committed
Merge branch 'main' of https://github.com/coder/coder into bq/handle-agent-being-ready-for-tasks
2 parents 2f0999a + a1546b5 commit f56a468

File tree

11 files changed

+974
-43
lines changed

11 files changed

+974
-43
lines changed

cli/exp_task.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ func (r *RootCmd) tasksCommand() *serpent.Command {
1414
},
1515
Children: []*serpent.Command{
1616
r.taskList(),
17+
r.taskCreate(),
18+
r.taskStatus(),
1719
},
1820
}
1921
return cmd

cli/exp_task_status.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/google/uuid"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/v2/cli/cliui"
12+
"github.com/coder/coder/v2/codersdk"
13+
"github.com/coder/serpent"
14+
)
15+
16+
func (r *RootCmd) taskStatus() *serpent.Command {
17+
var (
18+
client = new(codersdk.Client)
19+
formatter = cliui.NewOutputFormatter(
20+
cliui.TableFormat(
21+
[]taskStatusRow{},
22+
[]string{
23+
"state changed",
24+
"status",
25+
"state",
26+
"message",
27+
},
28+
),
29+
cliui.ChangeFormatterData(
30+
cliui.JSONFormat(),
31+
func(data any) (any, error) {
32+
rows, ok := data.([]taskStatusRow)
33+
if !ok {
34+
return nil, xerrors.Errorf("expected []taskStatusRow, got %T", data)
35+
}
36+
if len(rows) != 1 {
37+
return nil, xerrors.Errorf("expected exactly 1 row, got %d", len(rows))
38+
}
39+
return rows[0], nil
40+
},
41+
),
42+
)
43+
watchArg bool
44+
watchIntervalArg time.Duration
45+
)
46+
cmd := &serpent.Command{
47+
Short: "Show the status of a task.",
48+
Use: "status",
49+
Aliases: []string{"stat"},
50+
Options: serpent.OptionSet{
51+
{
52+
Default: "false",
53+
Description: "Watch the task status output. This will stream updates to the terminal until the underlying workspace is stopped.",
54+
Flag: "watch",
55+
Name: "watch",
56+
Value: serpent.BoolOf(&watchArg),
57+
},
58+
{
59+
Default: "1s",
60+
Description: "Interval to poll the task for updates. Only used in tests.",
61+
Hidden: true,
62+
Flag: "watch-interval",
63+
Name: "watch-interval",
64+
Value: serpent.DurationOf(&watchIntervalArg),
65+
},
66+
},
67+
Middleware: serpent.Chain(
68+
serpent.RequireNArgs(1),
69+
r.InitClient(client),
70+
),
71+
Handler: func(i *serpent.Invocation) error {
72+
ctx := i.Context()
73+
ec := codersdk.NewExperimentalClient(client)
74+
identifier := i.Args[0]
75+
76+
taskID, err := uuid.Parse(identifier)
77+
if err != nil {
78+
// Try to resolve the task as a named workspace
79+
// TODO: right now tasks are still "workspaces" under the hood.
80+
// We should update this once we have a proper task model.
81+
ws, err := namedWorkspace(ctx, client, identifier)
82+
if err != nil {
83+
return err
84+
}
85+
taskID = ws.ID
86+
}
87+
task, err := ec.TaskByID(ctx, taskID)
88+
if err != nil {
89+
return err
90+
}
91+
92+
out, err := formatter.Format(ctx, toStatusRow(task))
93+
if err != nil {
94+
return xerrors.Errorf("format task status: %w", err)
95+
}
96+
_, _ = fmt.Fprintln(i.Stdout, out)
97+
98+
if !watchArg {
99+
return nil
100+
}
101+
102+
lastStatus := task.Status
103+
lastState := task.CurrentState
104+
t := time.NewTicker(watchIntervalArg)
105+
defer t.Stop()
106+
// TODO: implement streaming updates instead of polling
107+
for range t.C {
108+
task, err := ec.TaskByID(ctx, taskID)
109+
if err != nil {
110+
return err
111+
}
112+
if lastStatus == task.Status && taskStatusEqual(lastState, task.CurrentState) {
113+
continue
114+
}
115+
out, err := formatter.Format(ctx, toStatusRow(task))
116+
if err != nil {
117+
return xerrors.Errorf("format task status: %w", err)
118+
}
119+
// hack: skip the extra column header from formatter
120+
if formatter.FormatID() != cliui.JSONFormat().ID() {
121+
out = strings.SplitN(out, "\n", 2)[1]
122+
}
123+
_, _ = fmt.Fprintln(i.Stdout, out)
124+
125+
if task.Status == codersdk.WorkspaceStatusStopped {
126+
return nil
127+
}
128+
lastStatus = task.Status
129+
lastState = task.CurrentState
130+
}
131+
return nil
132+
},
133+
}
134+
formatter.AttachOptions(&cmd.Options)
135+
return cmd
136+
}
137+
138+
func taskStatusEqual(s1, s2 *codersdk.TaskStateEntry) bool {
139+
if s1 == nil && s2 == nil {
140+
return true
141+
}
142+
if s1 == nil || s2 == nil {
143+
return false
144+
}
145+
return s1.State == s2.State
146+
}
147+
148+
type taskStatusRow struct {
149+
codersdk.Task `table:"-"`
150+
ChangedAgo string `json:"-" table:"state changed,default_sort"`
151+
Timestamp time.Time `json:"-" table:"-"`
152+
TaskStatus string `json:"-" table:"status"`
153+
TaskState string `json:"-" table:"state"`
154+
Message string `json:"-" table:"message"`
155+
}
156+
157+
func toStatusRow(task codersdk.Task) []taskStatusRow {
158+
tsr := taskStatusRow{
159+
Task: task,
160+
ChangedAgo: time.Since(task.UpdatedAt).Truncate(time.Second).String() + " ago",
161+
Timestamp: task.UpdatedAt,
162+
TaskStatus: string(task.Status),
163+
}
164+
if task.CurrentState != nil {
165+
tsr.ChangedAgo = time.Since(task.CurrentState.Timestamp).Truncate(time.Second).String() + " ago"
166+
tsr.Timestamp = task.CurrentState.Timestamp
167+
tsr.TaskState = string(task.CurrentState.State)
168+
tsr.Message = task.CurrentState.Message
169+
}
170+
return []taskStatusRow{tsr}
171+
}

0 commit comments

Comments
 (0)