cli: show lifecycle info in alloc status

Display task lifecycle info in `nomad alloc status <alloc_id>` 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
[...]
```
This commit is contained in:
Mahmood Ali 2020-03-22 08:20:42 -04:00
parent c7d07aa20c
commit 61c42034d5
2 changed files with 109 additions and 20 deletions

View File

@ -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) {

View File

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