diff --git a/cli/exp_task_status.go b/cli/exp_task_status.go index 328499d9d3..f0e1e7b865 100644 --- a/cli/exp_task_status.go +++ b/cli/exp_task_status.go @@ -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 } diff --git a/cli/exp_task_status_test.go b/cli/exp_task_status_test.go index 89935940d0..be62a76476 100644 --- a/cli/exp_task_status_test.go +++ b/cli/exp_task_status_test.go @@ -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),