From 61c42034d5e75b85be4e128cbddbbbd83d7ef4cb Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Sun, 22 Mar 2020 08:20:42 -0400 Subject: [PATCH] cli: show lifecycle info in alloc status Display task lifecycle info in `nomad alloc status ` output. I chose to embed it in the Task header and only add it for tasks with lifecycle info. Also, I chose to order the tasks in the following order: 1. prestart non-sidecar tasks 2. prestart sidecar tasks 3. main tasks The tasks are sorted lexicographically within each tier. Sample output: ``` $ nomad alloc status 6ec0eb52 ID = 6ec0eb52-e6c8-665c-169c-113d6081309b Eval ID = fb0caa98 Name = lifecycle.cache[0] [...] Task "init" (prestart) is "dead" Task Resources CPU Memory Disk Addresses 0/500 MHz 0 B/256 MiB 300 MiB [...] Task "some-sidecar" (prestart sidecar) is "running" Task Resources CPU Memory Disk Addresses 0/500 MHz 68 KiB/256 MiB 300 MiB [...] Task "redis" is "running" Task Resources CPU Memory Disk Addresses 10/500 MHz 984 KiB/256 MiB 300 MiB [...] ``` --- command/alloc_status.go | 66 +++++++++++++++++++++++++----------- command/alloc_status_test.go | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 20 deletions(-) diff --git a/command/alloc_status.go b/command/alloc_status.go index b9479aefd..cd1f918ab 100644 --- a/command/alloc_status.go +++ b/command/alloc_status.go @@ -364,9 +364,20 @@ func futureEvalTimePretty(evalID string, client *api.Client) string { // outputTaskDetails prints task details for each task in the allocation, // optionally printing verbose statistics if displayStats is set func (c *AllocStatusCommand) outputTaskDetails(alloc *api.Allocation, stats *api.AllocResourceUsage, displayStats bool, verbose bool) { - for task := range c.sortedTaskStateIterator(alloc.TaskStates) { + taskLifecycles := map[string]*api.TaskLifecycle{} + for _, t := range alloc.Job.LookupTaskGroup(alloc.TaskGroup).Tasks { + taskLifecycles[t.Name] = t.Lifecycle + } + + for _, task := range c.sortedTaskStateIterator(alloc.TaskStates, taskLifecycles) { state := alloc.TaskStates[task] - c.Ui.Output(c.Colorize().Color(fmt.Sprintf("\n[bold]Task %q is %q[reset]", task, state.State))) + + lcIndicator := "" + if lc := taskLifecycles[task]; !lc.Empty() { + lcIndicator = " (" + lifecycleDisplayName(lc) + ")" + } + + c.Ui.Output(c.Colorize().Color(fmt.Sprintf("\n[bold]Task %q%v is %q[reset]", task, lcIndicator, state.State))) c.outputTaskResources(alloc, task, stats, displayStats) c.Ui.Output("") c.outputTaskVolumes(alloc, task, verbose) @@ -671,20 +682,12 @@ func (c *AllocStatusCommand) shortTaskStatus(alloc *api.Allocation) { tasks := make([]string, 0, len(alloc.TaskStates)+1) tasks = append(tasks, "Name|State|Last Event|Time|Lifecycle") - taskLifecycles := map[string]string{} + taskLifecycles := map[string]*api.TaskLifecycle{} for _, t := range alloc.Job.LookupTaskGroup(alloc.TaskGroup).Tasks { - lc := "main" - if t.Lifecycle != nil { - sidecar := "" - if t.Lifecycle.Sidecar { - sidecar = "sidecar" - } - lc = fmt.Sprintf("%s %s", t.Lifecycle.Hook, sidecar) - } - taskLifecycles[t.Name] = lc + taskLifecycles[t.Name] = t.Lifecycle } - for task := range c.sortedTaskStateIterator(alloc.TaskStates) { + for _, task := range c.sortedTaskStateIterator(alloc.TaskStates, taskLifecycles) { state := alloc.TaskStates[task] lastState := state.State var lastEvent, lastTime string @@ -697,7 +700,7 @@ func (c *AllocStatusCommand) shortTaskStatus(alloc *api.Allocation) { } tasks = append(tasks, fmt.Sprintf("%s|%s|%s|%s|%s", - task, lastState, lastEvent, lastTime, taskLifecycles[task])) + task, lastState, lastEvent, lastTime, lifecycleDisplayName(taskLifecycles[task]))) } c.Ui.Output(c.Colorize().Color("\n[bold]Tasks[reset]")) @@ -706,8 +709,7 @@ func (c *AllocStatusCommand) shortTaskStatus(alloc *api.Allocation) { // sortedTaskStateIterator is a helper that takes the task state map and returns a // channel that returns the keys in a sorted order. -func (c *AllocStatusCommand) sortedTaskStateIterator(m map[string]*api.TaskState) <-chan string { - output := make(chan string, len(m)) +func (c *AllocStatusCommand) sortedTaskStateIterator(m map[string]*api.TaskState, lifecycles map[string]*api.TaskLifecycle) []string { keys := make([]string, len(m)) i := 0 for k := range m { @@ -716,12 +718,36 @@ func (c *AllocStatusCommand) sortedTaskStateIterator(m map[string]*api.TaskState } sort.Strings(keys) - for _, key := range keys { - output <- key + // display prestart then prestart sidecar then main + sort.SliceStable(keys, func(i, j int) bool { + lci := lifecycles[keys[i]] + lcj := lifecycles[keys[j]] + + switch { + case lci == nil: + return false + case lcj == nil: + return true + case !lci.Sidecar && lcj.Sidecar: + return true + default: + return false + } + }) + + return keys +} + +func lifecycleDisplayName(l *api.TaskLifecycle) string { + if l.Empty() { + return "main" } - close(output) - return output + sidecar := "" + if l.Sidecar { + sidecar = " sidecar" + } + return l.Hook + sidecar } func (c *AllocStatusCommand) outputTaskVolumes(alloc *api.Allocation, taskName string, verbose bool) { diff --git a/command/alloc_status_test.go b/command/alloc_status_test.go index 6f5b35c51..32006d0c1 100644 --- a/command/alloc_status_test.go +++ b/command/alloc_status_test.go @@ -87,6 +87,69 @@ func TestAllocStatusCommand_Fails(t *testing.T) { } } +func TestAllocStatusCommand_LifecycleInfo(t *testing.T) { + t.Parallel() + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + // Wait for a node to be ready + testutil.WaitForResult(func() (bool, error) { + nodes, _, err := client.Nodes().List(nil) + if err != nil { + return false, err + } + for _, node := range nodes { + if node.Status == structs.NodeStatusReady { + return true, nil + } + } + return false, fmt.Errorf("no ready nodes") + }, func(err error) { + require.NoError(t, err) + }) + + ui := new(cli.MockUi) + cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}} + state := srv.Agent.Server().State() + + a := mock.Alloc() + a.Metrics = &structs.AllocMetric{} + tg := a.Job.LookupTaskGroup(a.TaskGroup) + + initTask := tg.Tasks[0].Copy() + initTask.Name = "init_task" + initTask.Lifecycle = &structs.TaskLifecycleConfig{ + Hook: "prestart", + } + + prestartSidecarTask := tg.Tasks[0].Copy() + prestartSidecarTask.Name = "prestart_sidecar" + prestartSidecarTask.Lifecycle = &structs.TaskLifecycleConfig{ + Hook: "prestart", + Sidecar: true, + } + + tg.Tasks = append(tg.Tasks, initTask, prestartSidecarTask) + a.TaskResources["init_task"] = a.TaskResources["web"] + a.TaskResources["prestart_sidecar"] = a.TaskResources["web"] + a.TaskStates = map[string]*structs.TaskState{ + "web": &structs.TaskState{State: "pending"}, + "init_task": &structs.TaskState{State: "running"}, + "prestart_sidecar": &structs.TaskState{State: "running"}, + } + + require.Nil(t, state.UpsertAllocs(1000, []*structs.Allocation{a})) + + if code := cmd.Run([]string{"-address=" + url, a.ID}); code != 0 { + t.Fatalf("expected exit 0, got: %d", code) + } + out := ui.OutputWriter.String() + + require.Contains(t, out, `Task "init_task" (prestart) is "running"`) + require.Contains(t, out, `Task "prestart_sidecar" (prestart sidecar) is "running"`) + require.Contains(t, out, `Task "web" is "pending"`) +} + func TestAllocStatusCommand_Run(t *testing.T) { t.Parallel() srv, client, url := testServer(t, true, nil)