mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat(cli): improve exp task status --watch (#19969)
* Improves logic for `exp task status --watch` so that it will also exit on task idle status. * Adds workspace agent health to `exp task status` output.
This commit is contained in:
+48
-25
@@ -21,6 +21,7 @@ func (r *RootCmd) taskStatus() *serpent.Command {
|
||||
[]string{
|
||||
"state changed",
|
||||
"status",
|
||||
"healthy",
|
||||
"state",
|
||||
"message",
|
||||
},
|
||||
@@ -92,44 +93,46 @@ func (r *RootCmd) taskStatus() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := formatter.Format(ctx, toStatusRow(task))
|
||||
tsr := toStatusRow(task)
|
||||
out, err := formatter.Format(ctx, []taskStatusRow{tsr})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("format task status: %w", err)
|
||||
}
|
||||
_, _ = fmt.Fprintln(i.Stdout, out)
|
||||
|
||||
if !watchArg {
|
||||
if !watchArg || taskWatchIsEnded(task) {
|
||||
return nil
|
||||
}
|
||||
|
||||
lastStatus := task.Status
|
||||
lastState := task.CurrentState
|
||||
t := time.NewTicker(watchIntervalArg)
|
||||
defer t.Stop()
|
||||
// TODO: implement streaming updates instead of polling
|
||||
lastStatusRow := tsr
|
||||
for range t.C {
|
||||
task, err := ec.TaskByID(ctx, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lastStatus == task.Status && taskStatusEqual(lastState, task.CurrentState) {
|
||||
continue
|
||||
}
|
||||
out, err := formatter.Format(ctx, toStatusRow(task))
|
||||
if err != nil {
|
||||
return xerrors.Errorf("format task status: %w", err)
|
||||
}
|
||||
// hack: skip the extra column header from formatter
|
||||
if formatter.FormatID() != cliui.JSONFormat().ID() {
|
||||
out = strings.SplitN(out, "\n", 2)[1]
|
||||
}
|
||||
_, _ = fmt.Fprintln(i.Stdout, out)
|
||||
|
||||
if task.Status == codersdk.WorkspaceStatusStopped {
|
||||
// Only print if something changed
|
||||
newStatusRow := toStatusRow(task)
|
||||
if !taskStatusRowEqual(lastStatusRow, newStatusRow) {
|
||||
out, err := formatter.Format(ctx, []taskStatusRow{newStatusRow})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("format task status: %w", err)
|
||||
}
|
||||
// hack: skip the extra column header from formatter
|
||||
if formatter.FormatID() != cliui.JSONFormat().ID() {
|
||||
out = strings.SplitN(out, "\n", 2)[1]
|
||||
}
|
||||
_, _ = fmt.Fprintln(i.Stdout, out)
|
||||
}
|
||||
|
||||
if taskWatchIsEnded(task) {
|
||||
return nil
|
||||
}
|
||||
lastStatus = task.Status
|
||||
lastState = task.CurrentState
|
||||
|
||||
lastStatusRow = newStatusRow
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -138,14 +141,20 @@ func (r *RootCmd) taskStatus() *serpent.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func taskStatusEqual(s1, s2 *codersdk.TaskStateEntry) bool {
|
||||
if s1 == nil && s2 == nil {
|
||||
func taskWatchIsEnded(task codersdk.Task) bool {
|
||||
if task.Status == codersdk.WorkspaceStatusStopped {
|
||||
return true
|
||||
}
|
||||
if s1 == nil || s2 == nil {
|
||||
if task.WorkspaceAgentHealth == nil || !task.WorkspaceAgentHealth.Healthy {
|
||||
return false
|
||||
}
|
||||
return s1.State == s2.State
|
||||
if task.WorkspaceAgentLifecycle == nil || task.WorkspaceAgentLifecycle.Starting() || task.WorkspaceAgentLifecycle.ShuttingDown() {
|
||||
return false
|
||||
}
|
||||
if task.CurrentState == nil || task.CurrentState.State == codersdk.TaskStateWorking {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type taskStatusRow struct {
|
||||
@@ -153,22 +162,36 @@ type taskStatusRow struct {
|
||||
ChangedAgo string `json:"-" table:"state changed,default_sort"`
|
||||
Timestamp time.Time `json:"-" table:"-"`
|
||||
TaskStatus string `json:"-" table:"status"`
|
||||
Healthy bool `json:"-" table:"healthy"`
|
||||
TaskState string `json:"-" table:"state"`
|
||||
Message string `json:"-" table:"message"`
|
||||
}
|
||||
|
||||
func toStatusRow(task codersdk.Task) []taskStatusRow {
|
||||
func taskStatusRowEqual(r1, r2 taskStatusRow) bool {
|
||||
return r1.TaskStatus == r2.TaskStatus &&
|
||||
r1.Healthy == r2.Healthy &&
|
||||
r1.TaskState == r2.TaskState &&
|
||||
r1.Message == r2.Message
|
||||
}
|
||||
|
||||
func toStatusRow(task codersdk.Task) taskStatusRow {
|
||||
tsr := taskStatusRow{
|
||||
Task: task,
|
||||
ChangedAgo: time.Since(task.UpdatedAt).Truncate(time.Second).String() + " ago",
|
||||
Timestamp: task.UpdatedAt,
|
||||
TaskStatus: string(task.Status),
|
||||
}
|
||||
tsr.Healthy = task.WorkspaceAgentHealth != nil &&
|
||||
task.WorkspaceAgentHealth.Healthy &&
|
||||
task.WorkspaceAgentLifecycle != nil &&
|
||||
!task.WorkspaceAgentLifecycle.Starting() &&
|
||||
!task.WorkspaceAgentLifecycle.ShuttingDown()
|
||||
|
||||
if task.CurrentState != nil {
|
||||
tsr.ChangedAgo = time.Since(task.CurrentState.Timestamp).Truncate(time.Second).String() + " ago"
|
||||
tsr.Timestamp = task.CurrentState.Timestamp
|
||||
tsr.TaskState = string(task.CurrentState.State)
|
||||
tsr.Message = task.CurrentState.Message
|
||||
}
|
||||
return []taskStatusRow{tsr}
|
||||
return tsr
|
||||
}
|
||||
|
||||
+27
-32
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
@@ -63,8 +64,8 @@ func Test_TaskStatus(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"exists"},
|
||||
expectOutput: `STATE CHANGED STATUS STATE MESSAGE
|
||||
0s ago running working Thinking furiously...`,
|
||||
expectOutput: `STATE CHANGED STATUS HEALTHY STATE MESSAGE
|
||||
0s ago running true working Thinking furiously...`,
|
||||
hf: func(ctx context.Context, now time.Time) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
@@ -83,6 +84,10 @@ func Test_TaskStatus(t *testing.T) {
|
||||
Timestamp: now,
|
||||
Message: "Thinking furiously...",
|
||||
},
|
||||
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
|
||||
Healthy: true,
|
||||
},
|
||||
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
|
||||
})
|
||||
default:
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
@@ -93,12 +98,10 @@ func Test_TaskStatus(t *testing.T) {
|
||||
{
|
||||
args: []string{"exists", "--watch"},
|
||||
expectOutput: `
|
||||
STATE CHANGED STATUS STATE MESSAGE
|
||||
4s ago running
|
||||
3s ago running working Reticulating splines...
|
||||
2s ago running complete Splines reticulated successfully!
|
||||
2s ago stopping complete Splines reticulated successfully!
|
||||
2s ago stopped complete Splines reticulated successfully!`,
|
||||
STATE CHANGED STATUS HEALTHY STATE MESSAGE
|
||||
4s ago running true
|
||||
3s ago running true working Reticulating splines...
|
||||
2s ago running true complete Splines reticulated successfully!`,
|
||||
hf: func(ctx context.Context, now time.Time) func(http.ResponseWriter, *http.Request) {
|
||||
var calls atomic.Int64
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -116,13 +119,21 @@ STATE CHANGED STATUS STATE MESSAGE
|
||||
Status: codersdk.WorkspaceStatusPending,
|
||||
CreatedAt: now.Add(-5 * time.Second),
|
||||
UpdatedAt: now.Add(-5 * time.Second),
|
||||
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
|
||||
Healthy: true,
|
||||
},
|
||||
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
|
||||
})
|
||||
case 1:
|
||||
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
||||
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
Status: codersdk.WorkspaceStatusRunning,
|
||||
CreatedAt: now.Add(-5 * time.Second),
|
||||
UpdatedAt: now.Add(-4 * time.Second),
|
||||
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
|
||||
Healthy: true,
|
||||
},
|
||||
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
|
||||
UpdatedAt: now.Add(-4 * time.Second),
|
||||
})
|
||||
case 2:
|
||||
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
||||
@@ -130,6 +141,10 @@ STATE CHANGED STATUS STATE MESSAGE
|
||||
Status: codersdk.WorkspaceStatusRunning,
|
||||
CreatedAt: now.Add(-5 * time.Second),
|
||||
UpdatedAt: now.Add(-4 * time.Second),
|
||||
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
|
||||
Healthy: true,
|
||||
},
|
||||
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
|
||||
CurrentState: &codersdk.TaskStateEntry{
|
||||
State: codersdk.TaskStateWorking,
|
||||
Timestamp: now.Add(-3 * time.Second),
|
||||
@@ -142,30 +157,10 @@ STATE CHANGED STATUS STATE MESSAGE
|
||||
Status: codersdk.WorkspaceStatusRunning,
|
||||
CreatedAt: now.Add(-5 * time.Second),
|
||||
UpdatedAt: now.Add(-4 * time.Second),
|
||||
CurrentState: &codersdk.TaskStateEntry{
|
||||
State: codersdk.TaskStateComplete,
|
||||
Timestamp: now.Add(-2 * time.Second),
|
||||
Message: "Splines reticulated successfully!",
|
||||
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
|
||||
Healthy: true,
|
||||
},
|
||||
})
|
||||
case 4:
|
||||
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
||||
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
Status: codersdk.WorkspaceStatusStopping,
|
||||
CreatedAt: now.Add(-5 * time.Second),
|
||||
UpdatedAt: now.Add(-1 * time.Second),
|
||||
CurrentState: &codersdk.TaskStateEntry{
|
||||
State: codersdk.TaskStateComplete,
|
||||
Timestamp: now.Add(-2 * time.Second),
|
||||
Message: "Splines reticulated successfully!",
|
||||
},
|
||||
})
|
||||
case 5:
|
||||
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
||||
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
Status: codersdk.WorkspaceStatusStopped,
|
||||
CreatedAt: now.Add(-5 * time.Second),
|
||||
UpdatedAt: now,
|
||||
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
|
||||
CurrentState: &codersdk.TaskStateEntry{
|
||||
State: codersdk.TaskStateComplete,
|
||||
Timestamp: now.Add(-2 * time.Second),
|
||||
|
||||
Reference in New Issue
Block a user