From 9e3be518e33b0e7cd84b704f264b2e158297ca88 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Mon, 14 Aug 2017 17:35:13 +0000 Subject: [PATCH] adds any resource autocomplete defaults to listing jobs if no id is provided --- command/agent/http.go | 4 +- command/job_status.go | 1 + command/meta.go | 17 ++++ command/status.go | 112 +++++++++++++++++++++ command/status_test.go | 214 +++++++++++++++++++++++++++++++++++++++++ commands.go | 5 + 6 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 command/status.go create mode 100644 command/status_test.go diff --git a/command/agent/http.go b/command/agent/http.go index a75feb4fb..ebee73c30 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -145,8 +145,6 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/evaluations", s.wrap(s.EvalsRequest)) s.mux.HandleFunc("/v1/evaluation/", s.wrap(s.EvalSpecificRequest)) - s.mux.HandleFunc("/v1/search", s.wrap(s.SearchRequest)) - s.mux.HandleFunc("/v1/deployments", s.wrap(s.DeploymentsRequest)) s.mux.HandleFunc("/v1/deployment/", s.wrap(s.DeploymentSpecificRequest)) @@ -169,6 +167,8 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/status/leader", s.wrap(s.StatusLeaderRequest)) s.mux.HandleFunc("/v1/status/peers", s.wrap(s.StatusPeersRequest)) + s.mux.HandleFunc("/v1/search", s.wrap(s.SearchRequest)) + s.mux.HandleFunc("/v1/operator/", s.wrap(s.OperatorRequest)) s.mux.HandleFunc("/v1/system/gc", s.wrap(s.GarbageCollectRequest)) diff --git a/command/job_status.go b/command/job_status.go index 7d1ba5303..fd1c40f66 100644 --- a/command/job_status.go +++ b/command/job_status.go @@ -138,6 +138,7 @@ func (c *JobStatusCommand) Run(args []string) int { // Try querying the job jobID := args[0] + jobs, _, err := client.Jobs().PrefixList(jobID) if err != nil { c.Ui.Error(fmt.Sprintf("Error querying job: %s", err)) diff --git a/command/meta.go b/command/meta.go index 5f4a60c30..e269ce346 100644 --- a/command/meta.go +++ b/command/meta.go @@ -56,6 +56,18 @@ type Meta struct { insecure bool } +func (m *Meta) Copy(dest *Meta) { + dest.Ui = m.Ui + dest.flagAddress = m.flagAddress + dest.noColor = m.noColor + dest.region = m.region + dest.caCert = m.caCert + dest.caPath = m.caPath + dest.clientCert = m.clientCert + dest.clientKey = m.clientKey + dest.insecure = m.insecure +} + // FlagSet returns a FlagSet with the common flags that every // command implements. The exact behavior of FlagSet can be configured // using the flags as the second parameter, for example to disable @@ -152,6 +164,11 @@ func (m *Meta) Colorize() *colorstring.Colorize { } } +var ( + // flagOptions is a list of all available flags that can be used via the cli + flagOptions = []string{"address", "region", "no-color", "ca-cert", "ca-path", "client-cert", "client-key", "tls-skip-verify"} +) + // generalOptionsUsage returns the help string for the global options. func generalOptionsUsage() string { helpText := ` diff --git a/command/status.go b/command/status.go new file mode 100644 index 000000000..10b8247d3 --- /dev/null +++ b/command/status.go @@ -0,0 +1,112 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/nomad/api/contexts" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +type StatusCommand struct { + Meta +} + +func (c *StatusCommand) Run(args []string) int { + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + id := "" + // Assume the last argument will be the id to search + if len(args) > 0 { + id = args[len(args)-1] + } + + // Check that the last argument provided is not setting a flag + for _, flag := range flagOptions { + arg := strings.Replace(id, "-", "", 1) // strip leading '-' from flag + + if strings.HasPrefix(arg, flag) { + cmd := &JobStatusCommand{Meta: c.Meta} + return cmd.Run(args) + } + } + + // Try querying for the context associated with the id + res, err := client.Search().PrefixSearch(id, contexts.All) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying search with id: %s", err)) + return 1 + } + + if res.Matches == nil { + c.Ui.Error(fmt.Sprintf("No matches returned for query %s", err)) + return 1 + } + + var match contexts.Context + matchCount := 0 + for ctx, vers := range res.Matches { + if len(vers) == 1 { + match = ctx + matchCount++ + } + + // Only a single match should return, as this is a match against a full id + if matchCount > 1 || len(vers) > 1 { + c.Ui.Error(fmt.Sprintf("Multiple matches found for id %s", err)) + return 1 + } + } + + var cmd cli.Command + switch match { + case contexts.Evals: + cmd = &EvalStatusCommand{Meta: c.Meta} + case contexts.Nodes: + cmd = &NodeStatusCommand{Meta: c.Meta} + case contexts.Allocs: + cmd = &AllocStatusCommand{Meta: c.Meta} + case contexts.Jobs: + cmd = &JobStatusCommand{Meta: c.Meta} + default: + c.Ui.Error(fmt.Sprintf("Expected a specific context for id : %s", id)) + return 1 + } + + return cmd.Run(args) +} + +func (s *StatusCommand) Help() string { + helpText := ` +Usage: nomad status + + Display information about an existing resource. Job names, node ids, + allocation ids, and evaluation ids are all valid identifiers. + ` + return helpText +} + +func (s *StatusCommand) AutocompleteFlags() complete.Flags { + return nil +} + +func (s *StatusCommand) AutocompleteArgs() complete.Predictor { + client, _ := s.Meta.Client() + return complete.PredictFunc(func(a complete.Args) []string { + resp, err := client.Search().PrefixSearch(a.Last, contexts.All) + if err != nil { + return []string{} + } + return resp.Matches[contexts.All] + }) +} + +func (c *StatusCommand) Synopsis() string { + return "Display status information and metadata" +} diff --git a/command/status_test.go b/command/status_test.go new file mode 100644 index 000000000..48f3eea6d --- /dev/null +++ b/command/status_test.go @@ -0,0 +1,214 @@ +package command + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/nomad/command/agent" + "github.com/hashicorp/nomad/nomad/structs" + "github.com/hashicorp/nomad/testutil" + "github.com/mitchellh/cli" +) + +func TestStatusCommand_Run_JobStatus(t *testing.T) { + t.Parallel() + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + ui := new(cli.MockUi) + cmd := &StatusCommand{Meta: Meta{Ui: ui, flagAddress: url}} + + // Register a job + job1 := testJob("job1_sfx") + resp, _, err := client.Jobs().Register(job1, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { + t.Fatalf("status code non zero saw %d", code) + } + + // Query to check the job status + if code := cmd.Run([]string{"-address=" + url, "job1_sfx"}); code != 0 { + t.Fatalf("expected exit 0, got: %d", code) + } + out := ui.OutputWriter.String() + + if !strings.Contains(out, "job1_sfx") { + t.Fatalf("expected job1_sfx, got: %s", out) + } + ui.OutputWriter.Reset() +} + +func TestStatusCommand_Run_EvalStatus(t *testing.T) { + t.Parallel() + + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + ui := new(cli.MockUi) + cmd := &StatusCommand{Meta: Meta{Ui: ui, flagAddress: url}} + + jobID := "job1_sfx" + job1 := testJob(jobID) + resp, _, err := client.Jobs().Register(job1, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { + t.Fatalf("status code non zero saw %d", code) + } + + // get an eval id + evalID := "" + if evals, _, err := client.Jobs().Evaluations(jobID, nil); err == nil { + if len(evals) > 0 { + evalID = evals[0].ID + } + } + if evalID == "" { + t.Fatal("unable to find an evaluation") + } + + // Query to check the eval status + if code := cmd.Run([]string{"-address=" + url, evalID}); code != 0 { + t.Fatalf("expected exit 0, got: %d", code) + } + out := ui.OutputWriter.String() + + if !strings.Contains(out, evalID) { + t.Fatalf("expected eval id, got: %s", out) + } + + ui.OutputWriter.Reset() +} + +func TestStatusCommand_Run_NodeStatus(t *testing.T) { + t.Parallel() + + // Start in dev mode so we get a node registration + srv, client, url := testServer(t, true, func(c *agent.Config) { + c.NodeName = "mynode" + }) + defer srv.Shutdown() + + ui := new(cli.MockUi) + cmd := &StatusCommand{Meta: Meta{Ui: ui, flagAddress: url}} + + // 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 to check the node status + 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("expected node id (mynode), got: %s", out) + } + + ui.OutputWriter.Reset() +} + +func TestStatusCommand_Run_AllocStatus(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) { + t.Fatalf("err: %v", err) + }) + + ui := new(cli.MockUi) + cmd := &StatusCommand{Meta: Meta{Ui: ui, flagAddress: url}} + + jobID := "job1_sfx" + job1 := testJob(jobID) + resp, _, err := client.Jobs().Register(job1, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { + t.Fatalf("status code non zero saw %d", code) + } + + // get an alloc id + allocId1 := "" + if allocs, _, err := client.Jobs().Allocations(jobID, false, nil); err == nil { + if len(allocs) > 0 { + allocId1 = allocs[0].ID + } + } + if allocId1 == "" { + t.Fatal("unable to find an allocation") + } + + if code := cmd.Run([]string{"-address=" + url, allocId1}); code != 0 { + t.Fatalf("expected exit 0, got: %d", code) + } + out := ui.OutputWriter.String() + if !strings.Contains(out, allocId1) { + t.Fatal("expected to find alloc id in output") + } + + ui.OutputWriter.Reset() +} + +func TestStatusCommand_Run_NoPrefix(t *testing.T) { + t.Parallel() + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + ui := new(cli.MockUi) + cmd := &StatusCommand{Meta: Meta{Ui: ui, flagAddress: url}} + + // Register a job + job1 := testJob("job1_sfx") + resp, _, err := client.Jobs().Register(job1, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { + t.Fatalf("status code non zero saw %d", code) + } + + // Query to check status + 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, "job1_sfx") { + t.Fatalf("expected job1_sfx, got: %s", out) + } + + ui.OutputWriter.Reset() +} diff --git a/commands.go b/commands.go index cdf92b84d..cf5d14f35 100644 --- a/commands.go +++ b/commands.go @@ -224,6 +224,11 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "status": func() (cli.Command, error) { + return &command.StatusCommand{ + Meta: meta, + }, nil + }, "stop": func() (cli.Command, error) { return &command.StopCommand{ Meta: meta,