diff --git a/command/agent_info_test.go b/command/agent_info_test.go index d9d7a18c2..d16f04704 100644 --- a/command/agent_info_test.go +++ b/command/agent_info_test.go @@ -12,7 +12,7 @@ func TestAgentInfoCommand_Implements(t *testing.T) { } func TestAgentInfoCommand_Run(t *testing.T) { - srv, _, url := testServer(t) + srv, _, url := testServer(t, nil) defer srv.Stop() ui := new(cli.MockUi) diff --git a/command/agent_members_test.go b/command/agent_members_test.go index 0136e0dd5..d7a22df05 100644 --- a/command/agent_members_test.go +++ b/command/agent_members_test.go @@ -12,7 +12,7 @@ func TestAgentMembersCommand_Implements(t *testing.T) { } func TestAgentMembersCommand_Run(t *testing.T) { - srv, client, url := testServer(t) + srv, client, url := testServer(t, nil) defer srv.Stop() ui := new(cli.MockUi) diff --git a/command/helpers.go b/command/helpers.go new file mode 100644 index 000000000..489b3fb5e --- /dev/null +++ b/command/helpers.go @@ -0,0 +1,22 @@ +package command + +import ( + "github.com/ryanuber/columnize" +) + +// formatKV takes a set of strings and formats them into properly +// aligned k = v pairs using the columnize library. +func formatKV(in []string) string { + columnConf := columnize.DefaultConfig() + columnConf.Glue = " = " + return columnize.Format(in, columnConf) +} + +// formatList takes a set of strings and formats them into properly +// aligned output, replacing any blank fields with a placeholder +// for awk-ability. +func formatList(in []string) string { + columnConf := columnize.DefaultConfig() + columnConf.Empty = "" + return columnize.Format(in, columnConf) +} diff --git a/command/helpers_test.go b/command/helpers_test.go new file mode 100644 index 000000000..24f6fbee5 --- /dev/null +++ b/command/helpers_test.go @@ -0,0 +1,28 @@ +package command + +import ( + "testing" +) + +func TestHelpers_FormatKV(t *testing.T) { + in := []string{"alpha|beta", "charlie|delta"} + out := formatKV(in) + + expect := "alpha = beta\n" + expect += "charlie = delta" + + if out != expect { + t.Fatalf("expect: %s, got: %s", expect, out) + } +} + +func TestHelpers_FormatList(t *testing.T) { + in := []string{"alpha|beta||delta"} + out := formatList(in) + + expect := "alpha beta delta" + + if out != expect { + t.Fatalf("expect: %s, got: %s", expect, out) + } +} diff --git a/command/node_drain_test.go b/command/node_drain_test.go index 11bd04da8..fa437efb7 100644 --- a/command/node_drain_test.go +++ b/command/node_drain_test.go @@ -12,7 +12,7 @@ func TestNodeDrainCommand_Implements(t *testing.T) { } func TestNodeDrainCommand_Fails(t *testing.T) { - srv, _, url := testServer(t) + srv, _, url := testServer(t, nil) defer srv.Stop() ui := new(cli.MockUi) diff --git a/command/node_status.go b/command/node_status.go index 4c6f809f5..2a20c7a05 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -3,8 +3,6 @@ package command import ( "fmt" "strings" - - "github.com/ryanuber/columnize" ) type NodeStatusCommand struct { @@ -25,7 +23,14 @@ Usage: nomad node-status [options] [node] General Options: - ` + generalOptionsUsage() + ` + generalOptionsUsage() + ` + +Node Status Options: + + -short + Display short output. Used only when a single node is being + queried, and drops verbose output about node allocations. +` return strings.TrimSpace(helpText) } @@ -34,8 +39,12 @@ func (c *NodeStatusCommand) Synopsis() string { } func (c *NodeStatusCommand) Run(args []string) int { + var short bool + flags := c.Meta.FlagSet("node-status", FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.BoolVar(&short, "short", false, "") + if err := flags.Parse(args); err != nil { return 1 } @@ -82,7 +91,7 @@ func (c *NodeStatusCommand) Run(args []string) int { } // Dump the output - c.Ui.Output(columnize.SimpleFormat(out)) + c.Ui.Output(formatList(out)) return 0 } @@ -95,20 +104,44 @@ func (c *NodeStatusCommand) Run(args []string) int { } // Format the output - out := []string{ - fmt.Sprintf("ID | %s", node.ID), - fmt.Sprintf("Name | %s", node.Name), - fmt.Sprintf("Class | %s", node.NodeClass), - fmt.Sprintf("Datacenter | %s", node.Datacenter), - fmt.Sprintf("Drain | %v", node.Drain), - fmt.Sprintf("Status | %s", node.Status), + basic := []string{ + fmt.Sprintf("ID|%s", node.ID), + fmt.Sprintf("Name|%s", node.Name), + fmt.Sprintf("Class|%s", node.NodeClass), + fmt.Sprintf("Datacenter|%s", node.Datacenter), + fmt.Sprintf("Drain|%v", node.Drain), + fmt.Sprintf("Status|%s", node.Status), } - // Make the column config so we can dump k = v pairs - columnConf := columnize.DefaultConfig() - columnConf.Glue = " = " + var allocs []string + if !short { + // Query the node allocations + nodeAllocs, _, err := client.Nodes().Allocations(nodeID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying node allocations: %s", err)) + return 1 + } + + // Format the allocations + allocs = make([]string, len(nodeAllocs)+1) + allocs[0] = "ID|EvalID|JobID|TaskGroup|DesiredStatus|ClientStatus" + for i, alloc := range nodeAllocs { + allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%s|%s", + alloc.ID, + alloc.EvalID, + alloc.JobID, + alloc.NodeID, + alloc.TaskGroup, + alloc.DesiredStatus, + alloc.ClientStatus) + } + } // Dump the output - c.Ui.Output(columnize.Format(out, columnConf)) + c.Ui.Output(formatKV(basic)) + if !short { + c.Ui.Output("\n### Allocations") + c.Ui.Output(formatList(allocs)) + } return 0 } diff --git a/command/node_status_test.go b/command/node_status_test.go index 5b8965641..567a91f55 100644 --- a/command/node_status_test.go +++ b/command/node_status_test.go @@ -1,9 +1,11 @@ package command import ( + "fmt" "strings" "testing" + "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" ) @@ -12,25 +14,70 @@ func TestNodeStatusCommand_Implements(t *testing.T) { } func TestNodeStatusCommand_Run(t *testing.T) { - srv, _, url := testServer(t) + // Start in dev mode so we get a node registration + srv, client, url := testServer(t, func(c *testutil.TestServerConfig) { + c.DevMode = true + c.NodeName = "mynode" + }) defer srv.Stop() ui := new(cli.MockUi) cmd := &NodeStatusCommand{Meta: Meta{Ui: ui}} + // Wait for a node to appear + var nodeID string + testutil.WaitForResult(func() (bool, error) { + nodes, _, err := client.Nodes().List(nil) + if err != nil { + return false, err + } + if len(nodes) == 0 { + return false, fmt.Errorf("missing node") + } + nodeID = nodes[0].ID + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + // Query all node statuses if code := cmd.Run([]string{"-address=" + url}); code != 0 { t.Fatalf("expected exit 0, got: %d", code) } + out := ui.OutputWriter.String() + if !strings.Contains(out, "mynode") { + t.Fatalf("expect to find mynode, got: %s", out) + } + ui.OutputWriter.Reset() - // Expect empty output since we have no nodes - if out := ui.OutputWriter.String(); out != "" { - t.Fatalf("expected empty output, got: %s", out) + // Query a single node + if code := cmd.Run([]string{"-address=" + url, nodeID}); code != 0 { + t.Fatalf("expected exit 0, got: %d", code) + } + out = ui.OutputWriter.String() + if !strings.Contains(out, "mynode") { + t.Fatalf("expect to find mynode, got: %s", out) + } + if !strings.Contains(out, "Allocations") { + t.Fatalf("expected allocations, got: %s", out) + } + ui.OutputWriter.Reset() + + // Query single node in short view + if code := cmd.Run([]string{"-address=" + url, "-short", nodeID}); code != 0 { + t.Fatalf("expected exit 0, got: %d", code) + } + out = ui.OutputWriter.String() + if !strings.Contains(out, "mynode") { + t.Fatalf("expect to find mynode, got: %s", out) + } + if strings.Contains(out, "Allocations") { + t.Fatalf("should not dump allocations") } } func TestNodeStatusCommand_Fails(t *testing.T) { - srv, _, url := testServer(t) + srv, _, url := testServer(t, nil) defer srv.Stop() ui := new(cli.MockUi) diff --git a/command/status.go b/command/status.go index 3e445240e..b78700e57 100644 --- a/command/status.go +++ b/command/status.go @@ -3,8 +3,6 @@ package command import ( "fmt" "strings" - - "github.com/ryanuber/columnize" ) type StatusCommand struct { @@ -20,7 +18,15 @@ Usage: nomad status [options] [job] General Options: - ` + generalOptionsUsage() + ` + generalOptionsUsage() + ` + +Status Options: + + -short + Display short output. Used only when a single job is being + queried, and drops verbose information about allocations + and evaluations. +` return strings.TrimSpace(helpText) } @@ -29,8 +35,12 @@ func (c *StatusCommand) Synopsis() string { } func (c *StatusCommand) Run(args []string) int { + var short bool + flags := c.Meta.FlagSet("status", FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.BoolVar(&short, "short", false, "") + if err := flags.Parse(args); err != nil { return 1 } @@ -71,7 +81,7 @@ func (c *StatusCommand) Run(args []string) int { job.Priority, job.Status) } - c.Ui.Output(columnize.SimpleFormat(out)) + c.Ui.Output(formatList(out)) return 0 } @@ -85,20 +95,64 @@ func (c *StatusCommand) Run(args []string) int { // Format the job info basic := []string{ - fmt.Sprintf("ID | %s", job.ID), - fmt.Sprintf("Name | %s", job.Name), - fmt.Sprintf("Type | %s", job.Type), - fmt.Sprintf("Priority | %d", job.Priority), - fmt.Sprintf("Datacenters | %s", strings.Join(job.Datacenters, ",")), - fmt.Sprintf("Status | %s", job.Status), - fmt.Sprintf("StatusDescription | %s", job.StatusDescription), + fmt.Sprintf("ID|%s", job.ID), + fmt.Sprintf("Name|%s", job.Name), + fmt.Sprintf("Type|%s", job.Type), + fmt.Sprintf("Priority|%d", job.Priority), + fmt.Sprintf("Datacenters|%s", strings.Join(job.Datacenters, ",")), + fmt.Sprintf("Status|%s", job.Status), } - // Make the column config so we can dump k = v pairs - columnConf := columnize.DefaultConfig() - columnConf.Glue = " = " + var evals, allocs []string + if !short { + // Query the evaluations + jobEvals, _, err := client.Jobs().Evaluations(jobID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying job evaluations: %s", err)) + return 1 + } + + // Query the allocations + jobAllocs, _, err := client.Jobs().Allocations(jobID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying job allocations: %s", err)) + return 1 + } + + // Format the evals + evals = make([]string, len(jobEvals)+1) + evals[0] = "ID|Priority|Type|TriggeredBy|NodeID|Status" + for i, eval := range jobEvals { + evals[i+1] = fmt.Sprintf("%s|%d|%s|%s|%s|%s", + eval.ID, + eval.Priority, + eval.Type, + eval.TriggeredBy, + eval.NodeID, + eval.Status) + } + + // Format the allocs + allocs = make([]string, len(jobAllocs)+1) + allocs[0] = "ID|EvalID|NodeID|TaskGroup|DesiredStatus|ClientStatus" + for i, alloc := range jobAllocs { + allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%s|%s", + alloc.ID, + alloc.EvalID, + alloc.NodeID, + alloc.TaskGroup, + alloc.DesiredStatus, + alloc.ClientStatus) + } + } // Dump the output - c.Ui.Output(columnize.Format(basic, columnConf)) + c.Ui.Output(formatKV(basic)) + if !short { + c.Ui.Output("\n### Evaluations") + c.Ui.Output(formatList(evals)) + c.Ui.Output("\n### Allocations") + c.Ui.Output(formatList(allocs)) + } return 0 } diff --git a/command/status_test.go b/command/status_test.go index a50489c55..f58dc9b3a 100644 --- a/command/status_test.go +++ b/command/status_test.go @@ -13,7 +13,7 @@ func TestStatusCommand_Implements(t *testing.T) { } func TestStatusCommand_Run(t *testing.T) { - srv, client, url := testServer(t) + srv, client, url := testServer(t, nil) defer srv.Stop() ui := new(cli.MockUi) @@ -59,6 +59,28 @@ func TestStatusCommand_Run(t *testing.T) { if strings.Contains(out, "job1") || !strings.Contains(out, "job2") { t.Fatalf("expected only job2, got: %s", out) } + if !strings.Contains(out, "Evaluations") { + t.Fatalf("should dump evaluations") + } + if !strings.Contains(out, "Allocations") { + t.Fatalf("should dump allocations") + } + ui.OutputWriter.Reset() + + // Query in short view mode + if code := cmd.Run([]string{"-address=" + url, "-short", "job2"}); code != 0 { + t.Fatalf("expected exit 0, got: %d", code) + } + out = ui.OutputWriter.String() + if !strings.Contains(out, "job2") { + t.Fatalf("expected job2, got: %s", out) + } + if strings.Contains(out, "Evaluations") { + t.Fatalf("should not dump evaluations") + } + if strings.Contains(out, "Allocations") { + t.Fatalf("should not dump allocations") + } } func TestStatusCommand_Fails(t *testing.T) { diff --git a/command/util_test.go b/command/util_test.go index 82686fdb0..16fb62c91 100644 --- a/command/util_test.go +++ b/command/util_test.go @@ -15,7 +15,10 @@ func init() { seen = make(map[*testing.T]struct{}) } -func testServer(t *testing.T) (*testutil.TestServer, *api.Client, string) { +func testServer( + t *testing.T, + cb testutil.ServerConfigCallback) (*testutil.TestServer, *api.Client, string) { + // Always run these tests in parallel. if _, ok := seen[t]; !ok { seen[t] = struct{}{} @@ -23,7 +26,7 @@ func testServer(t *testing.T) (*testutil.TestServer, *api.Client, string) { } // Make a new test server - srv := testutil.NewTestServer(t, nil) + srv := testutil.NewTestServer(t, cb) // Make a client clientConf := api.DefaultConfig()