From d93daec7a11cac0af2093fc96ca4ffa44f6fe249 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Tue, 15 Aug 2017 17:43:50 +0000 Subject: [PATCH 01/13] job names causes errors when searching other contexts, only log but not return this err --- nomad/search_endpoint.go | 8 ++++++-- nomad/search_endpoint_test.go | 38 ++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/nomad/search_endpoint.go b/nomad/search_endpoint.go index a85b0dbb7..130d968f2 100644 --- a/nomad/search_endpoint.go +++ b/nomad/search_endpoint.go @@ -120,10 +120,14 @@ func (s *Search) PrefixSearch(args *structs.SearchRequest, for _, ctx := range contexts { iter, err := getResourceIter(ctx, roundUUIDDownIfOdd(args.Prefix, args.Context), ws, state) + + // When searching all Contexts, Job ids will cause errors when searched + // in the context of allocs, nodes, and/or evals. if err != nil { - return err + s.srv.logger.Printf("[WARN] nomad.resources: error when searching context %s for id %s", ctx, args.Prefix) + } else { + iters[ctx] = iter } - iters[ctx] = iter } // Return matches for the given prefix diff --git a/nomad/search_endpoint_test.go b/nomad/search_endpoint_test.go index 41c03af02..a72121e2c 100644 --- a/nomad/search_endpoint_test.go +++ b/nomad/search_endpoint_test.go @@ -25,7 +25,7 @@ func registerAndVerifyJob(s *Server, t *testing.T, prefix string, counter int) s return job.ID } -func TestSearch_PrefixSearch(t *testing.T) { +func TestSearch_PrefixSearch_Job(t *testing.T) { assert := assert.New(t) prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970" @@ -88,6 +88,42 @@ func TestSearch_PrefixSearch_Truncate(t *testing.T) { assert.Equal(uint64(jobIndex), resp.Index) } +func TestSearch_PrefixSearch_AllWithJob(t *testing.T) { + assert := assert.New(t) + prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970" + + t.Parallel() + s := testServer(t, func(c *Config) { + c.NumSchedulers = 0 + }) + + defer s.Shutdown() + codec := rpcClient(t, s) + testutil.WaitForLeader(t, s.RPC) + + jobID := registerAndVerifyJob(s, t, prefix, 0) + + eval1 := mock.Eval() + eval1.ID = jobID + s.fsm.State().UpsertEvals(2000, []*structs.Evaluation{eval1}) + + req := &structs.SearchRequest{ + Prefix: prefix, + Context: structs.All, + } + + var resp structs.SearchResponse + if err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + assert.Equal(1, len(resp.Matches[structs.Jobs])) + assert.Equal(jobID, resp.Matches[structs.Jobs][0]) + + assert.Equal(1, len(resp.Matches[structs.Evals])) + assert.Equal(eval1.ID, resp.Matches[structs.Evals][0]) +} + func TestSearch_PrefixSearch_Evals(t *testing.T) { assert := assert.New(t) t.Parallel() From 9e3be518e33b0e7cd84b704f264b2e158297ca88 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Mon, 14 Aug 2017 17:35:13 +0000 Subject: [PATCH 02/13] 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, From d003af3125a172aece9f426ea0e976758ea3305d Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 17 Aug 2017 17:40:41 +0000 Subject: [PATCH 03/13] default to job status if no arguments are provided --- command/status.go | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/command/status.go b/command/status.go index 10b8247d3..6d7ef90bd 100644 --- a/command/status.go +++ b/command/status.go @@ -13,6 +13,20 @@ type StatusCommand struct { Meta } +// Check that the last argument provided is not setting a flag +func lastArgIsFlag(args []string) bool { + lastArg := args[len(args)-1] + + for _, flag := range flagOptions { + arg := strings.Replace(lastArg, "-", "", 1) // strip leading '-' from flag + + if strings.HasPrefix(arg, flag) { + return true + } + } + return false +} + func (c *StatusCommand) Run(args []string) int { // Get the HTTP client client, err := c.Meta.Client() @@ -21,21 +35,13 @@ func (c *StatusCommand) Run(args []string) int { return 1 } - id := "" + if len(args) == 0 || lastArgIsFlag(args) { + cmd := &JobStatusCommand{Meta: c.Meta} + return cmd.Run(args) + } + // 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) - } - } + id := args[len(args)-1] // Try querying for the context associated with the id res, err := client.Search().PrefixSearch(id, contexts.All) From c24caaaa18ebe760e8bf8af4ac920fc5708a35bd Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 17 Aug 2017 20:25:30 +0000 Subject: [PATCH 04/13] fix autocomplete to list all matches --- command/meta.go | 12 ------------ command/status.go | 15 ++++++++++++++- command/status_test.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/command/meta.go b/command/meta.go index e269ce346..cf32eeeb4 100644 --- a/command/meta.go +++ b/command/meta.go @@ -56,18 +56,6 @@ 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 diff --git a/command/status.go b/command/status.go index 6d7ef90bd..c44b8e973 100644 --- a/command/status.go +++ b/command/status.go @@ -109,7 +109,20 @@ func (s *StatusCommand) AutocompleteArgs() complete.Predictor { if err != nil { return []string{} } - return resp.Matches[contexts.All] + + final := make([]string, 0) + + for _, matches := range resp.Matches { + if len(matches) == 0 { + continue + } + + for _, id := range matches { + final = append(final, id) + } + } + + return final }) } diff --git a/command/status_test.go b/command/status_test.go index 48f3eea6d..17bae48e3 100644 --- a/command/status_test.go +++ b/command/status_test.go @@ -9,6 +9,8 @@ import ( "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" ) func TestStatusCommand_Run_JobStatus(t *testing.T) { @@ -212,3 +214,31 @@ func TestStatusCommand_Run_NoPrefix(t *testing.T) { ui.OutputWriter.Reset() } + +func TestStatusCommand_AutocompleteArgs(t *testing.T) { + assert := assert.New(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) + } + + prefix := jobID[:len(jobID)-5] + args := complete.Args{Last: prefix} + predictor := cmd.AutocompleteArgs() + + res := predictor.Predict(args) + assert.Contains(res, jobID) +} From d1ab4f431788f00419abd9eb10fa6a3a255e638c Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 17 Aug 2017 21:37:38 +0000 Subject: [PATCH 05/13] small fixups --- command/status.go | 16 ++++---- command/status_test.go | 89 +++++++++++++++--------------------------- 2 files changed, 38 insertions(+), 67 deletions(-) diff --git a/command/status.go b/command/status.go index c44b8e973..77d110a9b 100644 --- a/command/status.go +++ b/command/status.go @@ -15,12 +15,11 @@ type StatusCommand struct { // Check that the last argument provided is not setting a flag func lastArgIsFlag(args []string) bool { - lastArg := args[len(args)-1] + // strip leading '-' from what is potentially a flag + lastArg := strings.Replace(args[len(args)-1], "-", "", 1) for _, flag := range flagOptions { - arg := strings.Replace(lastArg, "-", "", 1) // strip leading '-' from flag - - if strings.HasPrefix(arg, flag) { + if strings.HasPrefix(lastArg, flag) { return true } } @@ -35,15 +34,15 @@ func (c *StatusCommand) Run(args []string) int { return 1 } + // If no identifier is provided, default to listing jobs if len(args) == 0 || lastArgIsFlag(args) { cmd := &JobStatusCommand{Meta: c.Meta} return cmd.Run(args) } - // Assume the last argument will be the id to search id := args[len(args)-1] - // Try querying for the context associated with the id + // Query 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)) @@ -63,7 +62,7 @@ func (c *StatusCommand) Run(args []string) int { matchCount++ } - // Only a single match should return, as this is a match against a full id + // Only a single result 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 @@ -89,8 +88,7 @@ func (c *StatusCommand) Run(args []string) int { } func (s *StatusCommand) Help() string { - helpText := ` -Usage: nomad status + helpText := `Usage: nomad status Display information about an existing resource. Job names, node ids, allocation ids, and evaluation ids are all valid identifiers. diff --git a/command/status_test.go b/command/status_test.go index 17bae48e3..af7f9a9fd 100644 --- a/command/status_test.go +++ b/command/status_test.go @@ -2,11 +2,9 @@ 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" "github.com/posener/complete" @@ -14,7 +12,9 @@ import ( ) func TestStatusCommand_Run_JobStatus(t *testing.T) { + assert := assert.New(t) t.Parallel() + srv, client, url := testServer(t, true, nil) defer srv.Shutdown() @@ -24,9 +24,8 @@ func TestStatusCommand_Run_JobStatus(t *testing.T) { // Register a job job1 := testJob("job1_sfx") resp, _, err := client.Jobs().Register(job1, nil) - if err != nil { - t.Fatalf("err: %s", err) - } + assert.Nil(err) + if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { t.Fatalf("status code non zero saw %d", code) } @@ -35,15 +34,15 @@ func TestStatusCommand_Run_JobStatus(t *testing.T) { 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) - } + out := ui.OutputWriter.String() + assert.Contains(out, "job1_sfx") + ui.OutputWriter.Reset() } func TestStatusCommand_Run_EvalStatus(t *testing.T) { + assert := assert.New(t) t.Parallel() srv, client, url := testServer(t, true, nil) @@ -55,9 +54,8 @@ func TestStatusCommand_Run_EvalStatus(t *testing.T) { jobID := "job1_sfx" job1 := testJob(jobID) resp, _, err := client.Jobs().Register(job1, nil) - if err != nil { - t.Fatalf("err: %s", err) - } + assert.Nil(err) + if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { t.Fatalf("status code non zero saw %d", code) } @@ -69,24 +67,22 @@ func TestStatusCommand_Run_EvalStatus(t *testing.T) { evalID = evals[0].ID } } - if evalID == "" { - t.Fatal("unable to find an evaluation") - } + + assert.NotEqual("", evalID) // 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) - } + out := ui.OutputWriter.String() + assert.Contains(out, evalID) ui.OutputWriter.Reset() } func TestStatusCommand_Run_NodeStatus(t *testing.T) { + assert := assert.New(t) t.Parallel() // Start in dev mode so we get a node registration @@ -118,46 +114,28 @@ func TestStatusCommand_Run_NodeStatus(t *testing.T) { 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) - } + out := ui.OutputWriter.String() + assert.Contains(out, "mynode") ui.OutputWriter.Reset() } func TestStatusCommand_Run_AllocStatus(t *testing.T) { + assert := assert.New(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) - } + assert.Nil(err) + if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { t.Fatalf("status code non zero saw %d", code) } @@ -169,23 +147,22 @@ func TestStatusCommand_Run_AllocStatus(t *testing.T) { allocId1 = allocs[0].ID } } - if allocId1 == "" { - t.Fatal("unable to find an allocation") - } + assert.NotEqual("", allocId1) 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") - } + assert.Contains(out, allocId1) ui.OutputWriter.Reset() } func TestStatusCommand_Run_NoPrefix(t *testing.T) { + assert := assert.New(t) t.Parallel() + srv, client, url := testServer(t, true, nil) defer srv.Shutdown() @@ -195,9 +172,8 @@ func TestStatusCommand_Run_NoPrefix(t *testing.T) { // Register a job job1 := testJob("job1_sfx") resp, _, err := client.Jobs().Register(job1, nil) - if err != nil { - t.Fatalf("err: %s", err) - } + assert.Nil(err) + if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { t.Fatalf("status code non zero saw %d", code) } @@ -206,11 +182,9 @@ func TestStatusCommand_Run_NoPrefix(t *testing.T) { 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) - } + out := ui.OutputWriter.String() + assert.Contains(out, "job1_sfx") ui.OutputWriter.Reset() } @@ -228,9 +202,8 @@ func TestStatusCommand_AutocompleteArgs(t *testing.T) { jobID := "job1_sfx" job1 := testJob(jobID) resp, _, err := client.Jobs().Register(job1, nil) - if err != nil { - t.Fatalf("err: %s", err) - } + assert.Nil(err) + if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { t.Fatalf("status code non zero saw %d", code) } From 3d8c3c59de3bcc77490a4a2fc39d81097bd91f49 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 18 Aug 2017 13:49:20 +0000 Subject: [PATCH 06/13] use existing arg parsing functionality --- command/meta.go | 5 ----- command/status.go | 33 ++++++++++++++++----------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/command/meta.go b/command/meta.go index cf32eeeb4..5f4a60c30 100644 --- a/command/meta.go +++ b/command/meta.go @@ -152,11 +152,6 @@ 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 index 77d110a9b..54e16636c 100644 --- a/command/status.go +++ b/command/status.go @@ -2,7 +2,6 @@ package command import ( "fmt" - "strings" "github.com/hashicorp/nomad/api/contexts" "github.com/mitchellh/cli" @@ -13,19 +12,6 @@ type StatusCommand struct { Meta } -// Check that the last argument provided is not setting a flag -func lastArgIsFlag(args []string) bool { - // strip leading '-' from what is potentially a flag - lastArg := strings.Replace(args[len(args)-1], "-", "", 1) - - for _, flag := range flagOptions { - if strings.HasPrefix(lastArg, flag) { - return true - } - } - return false -} - func (c *StatusCommand) Run(args []string) int { // Get the HTTP client client, err := c.Meta.Client() @@ -34,10 +20,23 @@ func (c *StatusCommand) Run(args []string) int { return 1 } + // Parsing args is not idempotent + argsCopy := args + + flags := c.Meta.FlagSet("status", FlagSetClient) + + if err := flags.Parse(args); err != nil { + c.Ui.Error(fmt.Sprintf("Error parsing arguments: %s", err)) + return 1 + } + + // Check that we got exactly one evaluation ID + args = flags.Args() + // If no identifier is provided, default to listing jobs - if len(args) == 0 || lastArgIsFlag(args) { + if len(args) == 0 { cmd := &JobStatusCommand{Meta: c.Meta} - return cmd.Run(args) + return cmd.Run(argsCopy) } id := args[len(args)-1] @@ -84,7 +83,7 @@ func (c *StatusCommand) Run(args []string) int { return 1 } - return cmd.Run(args) + return cmd.Run(argsCopy) } func (s *StatusCommand) Help() string { From a5238232aa2048156405d43a59949784fd52f064 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 18 Aug 2017 15:25:29 +0000 Subject: [PATCH 07/13] limit argument autocompletion to one --- command/status.go | 7 +++++++ command/status_test.go | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/command/status.go b/command/status.go index 54e16636c..2710c8ad4 100644 --- a/command/status.go +++ b/command/status.go @@ -102,6 +102,13 @@ func (s *StatusCommand) AutocompleteFlags() complete.Flags { func (s *StatusCommand) AutocompleteArgs() complete.Predictor { client, _ := s.Meta.Client() return complete.PredictFunc(func(a complete.Args) []string { + + for _, arg := range a.Completed { + if arg == a.Last { + return nil + } + } + resp, err := client.Search().PrefixSearch(a.Last, contexts.All) if err != nil { return []string{} diff --git a/command/status_test.go b/command/status_test.go index af7f9a9fd..51d701fe8 100644 --- a/command/status_test.go +++ b/command/status_test.go @@ -214,4 +214,9 @@ func TestStatusCommand_AutocompleteArgs(t *testing.T) { res := predictor.Predict(args) assert.Contains(res, jobID) + + args = complete.Args{Last: prefix, Completed: []string{prefix, "1", "2"}} + predictor = cmd.AutocompleteArgs() + res = predictor.Predict(args) + assert.Nil(res) } From d123a4a2b4ddc872c475f7b2bb95e7b163a77c3e Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 18 Aug 2017 20:27:53 +0000 Subject: [PATCH 08/13] fix up formatting of error message fixups from code review --- command/status.go | 48 ++++++++++++++++++---------------------- nomad/search_endpoint.go | 8 ++++--- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/command/status.go b/command/status.go index 2710c8ad4..8134a7f8a 100644 --- a/command/status.go +++ b/command/status.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "strings" "github.com/hashicorp/nomad/api/contexts" "github.com/mitchellh/cli" @@ -13,26 +14,27 @@ type StatusCommand struct { } 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 - } - - // Parsing args is not idempotent - argsCopy := args - flags := c.Meta.FlagSet("status", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } if err := flags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing arguments: %s", err)) + c.Ui.Error(fmt.Sprintf("Error parsing arguments: %q", err)) return 1 } + // Store the original arguments so we can pass them to the routed command + argsCopy := args + // Check that we got exactly one evaluation ID args = flags.Args() + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %q", err)) + return 1 + } + // If no identifier is provided, default to listing jobs if len(args) == 0 { cmd := &JobStatusCommand{Meta: c.Meta} @@ -44,12 +46,12 @@ func (c *StatusCommand) Run(args []string) int { // Query 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)) + c.Ui.Error(fmt.Sprintf("Error querying search with id: %q", err)) return 1 } if res.Matches == nil { - c.Ui.Error(fmt.Sprintf("No matches returned for query %s", err)) + c.Ui.Error(fmt.Sprintf("No matches returned for query: %q", err)) return 1 } @@ -63,7 +65,7 @@ func (c *StatusCommand) Run(args []string) int { // Only a single result 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)) + c.Ui.Error(fmt.Sprintf("Multiple matches found for id %s", id)) return 1 } } @@ -89,10 +91,9 @@ func (c *StatusCommand) Run(args []string) int { 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. + Display the status output for any given resource. The command will detect the type of resource being queried and display the appropriate status output. ` - return helpText + return strings.TrimSpace(helpText) } func (s *StatusCommand) AutocompleteFlags() complete.Flags { @@ -102,11 +103,8 @@ func (s *StatusCommand) AutocompleteFlags() complete.Flags { func (s *StatusCommand) AutocompleteArgs() complete.Predictor { client, _ := s.Meta.Client() return complete.PredictFunc(func(a complete.Args) []string { - - for _, arg := range a.Completed { - if arg == a.Last { - return nil - } + if len(a.Completed) > 1 { + return nil } resp, err := client.Search().PrefixSearch(a.Last, contexts.All) @@ -121,9 +119,7 @@ func (s *StatusCommand) AutocompleteArgs() complete.Predictor { continue } - for _, id := range matches { - final = append(final, id) - } + final = append(final, matches...) } return final @@ -131,5 +127,5 @@ func (s *StatusCommand) AutocompleteArgs() complete.Predictor { } func (c *StatusCommand) Synopsis() string { - return "Display status information and metadata" + return "Display the status output for a resource" } diff --git a/nomad/search_endpoint.go b/nomad/search_endpoint.go index 130d968f2..daa40148d 100644 --- a/nomad/search_endpoint.go +++ b/nomad/search_endpoint.go @@ -121,10 +121,12 @@ func (s *Search) PrefixSearch(args *structs.SearchRequest, for _, ctx := range contexts { iter, err := getResourceIter(ctx, roundUUIDDownIfOdd(args.Prefix, args.Context), ws, state) - // When searching all Contexts, Job ids will cause errors when searched - // in the context of allocs, nodes, and/or evals. if err != nil { - s.srv.logger.Printf("[WARN] nomad.resources: error when searching context %s for id %s", ctx, args.Prefix) + // Searching other contexts with job names raises an error, which in + // this case we want to ignore. + if !strings.Contains(err.Error(), "Invalid UUID: encoding/hex") { + return err + } } else { iters[ctx] = iter } From 497432bc713f699b8b4bc251c055bd1d350ee40a Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Mon, 21 Aug 2017 23:12:35 +0000 Subject: [PATCH 09/13] cli help and logging formatting --- command/status.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/command/status.go b/command/status.go index 8134a7f8a..6c78273b3 100644 --- a/command/status.go +++ b/command/status.go @@ -65,7 +65,7 @@ func (c *StatusCommand) Run(args []string) int { // Only a single result 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", id)) + c.Ui.Error(fmt.Sprintf("Multiple matches found for id %q", id)) return 1 } } @@ -81,7 +81,7 @@ func (c *StatusCommand) Run(args []string) int { case contexts.Jobs: cmd = &JobStatusCommand{Meta: c.Meta} default: - c.Ui.Error(fmt.Sprintf("Expected a specific context for id : %s", id)) + c.Ui.Error(fmt.Sprintf("Unable to resolve ID: %q", id)) return 1 } @@ -89,10 +89,17 @@ func (c *StatusCommand) Run(args []string) int { } func (s *StatusCommand) Help() string { - helpText := `Usage: nomad status + helpText := ` +Usage: nomad status [options] + + Display the status output for any given resource. The command will + detect the type of resource being queried and display the appropriate + status output. + +General Options: + + ` + generalOptionsUsage() - Display the status output for any given resource. The command will detect the type of resource being queried and display the appropriate status output. - ` return strings.TrimSpace(helpText) } From cf432b65287339b8bcc0f1062ba92c3c5aa69722 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 24 Aug 2017 21:04:38 +0000 Subject: [PATCH 10/13] vendor updated cli and autocomplete dependencies --- vendor/github.com/mitchellh/cli/Makefile | 20 ++++++++++ vendor/github.com/mitchellh/cli/cli.go | 45 +++++++++++----------- vendor/github.com/mitchellh/cli/ui_mock.go | 6 ++- vendor/vendor.json | 6 +-- 4 files changed, 51 insertions(+), 26 deletions(-) create mode 100644 vendor/github.com/mitchellh/cli/Makefile diff --git a/vendor/github.com/mitchellh/cli/Makefile b/vendor/github.com/mitchellh/cli/Makefile new file mode 100644 index 000000000..4874b0082 --- /dev/null +++ b/vendor/github.com/mitchellh/cli/Makefile @@ -0,0 +1,20 @@ +TEST?=./... + +default: test + +# test runs the test suite and vets the code +test: + go list $(TEST) | xargs -n1 go test -timeout=60s -parallel=10 $(TESTARGS) + +# testrace runs the race checker +testrace: + go list $(TEST) | xargs -n1 go test -race $(TESTARGS) + +# updatedeps installs all the dependencies to run and build +updatedeps: + go list ./... \ + | xargs go list -f '{{ join .Deps "\n" }}{{ printf "\n" }}{{ join .TestImports "\n" }}' \ + | grep -v github.com/mitchellh/cli \ + | xargs go get -f -u -v + +.PHONY: test testrace updatedeps diff --git a/vendor/github.com/mitchellh/cli/cli.go b/vendor/github.com/mitchellh/cli/cli.go index d4fe10c2f..273fbc3dc 100644 --- a/vendor/github.com/mitchellh/cli/cli.go +++ b/vendor/github.com/mitchellh/cli/cli.go @@ -85,13 +85,17 @@ type CLI struct { // for the flag name. These default to `autocomplete-install` and // `autocomplete-uninstall` respectively. // + // AutocompleteNoDefaultFlags is a boolean which controls if the default auto- + // complete flags like -help and -version are added to the output. + // // AutocompleteGlobalFlags are a mapping of global flags for // autocompletion. The help and version flags are automatically added. - Autocomplete bool - AutocompleteInstall string - AutocompleteUninstall string - AutocompleteGlobalFlags complete.Flags - autocompleteInstaller autocompleteInstaller // For tests + Autocomplete bool + AutocompleteInstall string + AutocompleteUninstall string + AutocompleteNoDefaultFlags bool + AutocompleteGlobalFlags complete.Flags + autocompleteInstaller autocompleteInstaller // For tests // HelpFunc and HelpWriter are used to output help information, if // requested. @@ -375,11 +379,13 @@ func (c *CLI) initAutocomplete() { // For the root, we add the global flags to the "Flags". This way // they don't show up on every command. - cmd.Flags = map[string]complete.Predictor{ - "-" + c.AutocompleteInstall: complete.PredictNothing, - "-" + c.AutocompleteUninstall: complete.PredictNothing, - "-help": complete.PredictNothing, - "-version": complete.PredictNothing, + if !c.AutocompleteNoDefaultFlags { + cmd.Flags = map[string]complete.Predictor{ + "-" + c.AutocompleteInstall: complete.PredictNothing, + "-" + c.AutocompleteUninstall: complete.PredictNothing, + "-help": complete.PredictNothing, + "-version": complete.PredictNothing, + } } cmd.GlobalFlags = c.AutocompleteGlobalFlags @@ -392,27 +398,22 @@ func (c *CLI) initAutocomplete() { func (c *CLI) initAutocompleteSub(prefix string) complete.Command { var cmd complete.Command walkFn := func(k string, raw interface{}) bool { + // Keep track of the full key so that we can nest further if necessary + fullKey := k + if len(prefix) > 0 { // If we have a prefix, trim the prefix + 1 (for the space) // Example: turns "sub one" to "one" with prefix "sub" k = k[len(prefix)+1:] } - // Keep track of the full key so that we can nest further if necessary - fullKey := k - - if idx := strings.LastIndex(k, " "); idx >= 0 { - // If there is a space, we trim up to the space + if idx := strings.Index(k, " "); idx >= 0 { + // If there is a space, we trim up to the space. This turns + // "sub sub2 sub3" into "sub". The prefix trim above will + // trim our current depth properly. k = k[:idx] } - if idx := strings.LastIndex(k, " "); idx >= 0 { - // This catches the scenario just in case where we see "sub one" - // before "sub". This will let us properly setup the subcommand - // regardless. - k = k[idx+1:] - } - if _, ok := cmd.Sub[k]; ok { // If we already tracked this subcommand then ignore return false diff --git a/vendor/github.com/mitchellh/cli/ui_mock.go b/vendor/github.com/mitchellh/cli/ui_mock.go index bdae2a664..0bfe0a191 100644 --- a/vendor/github.com/mitchellh/cli/ui_mock.go +++ b/vendor/github.com/mitchellh/cli/ui_mock.go @@ -100,8 +100,12 @@ func (b *syncBuffer) Reset() { } func (b *syncBuffer) String() string { + return string(b.Bytes()) +} + +func (b *syncBuffer) Bytes() []byte { b.RLock() data := b.b.Bytes() b.RUnlock() - return string(data) + return data } diff --git a/vendor/vendor.json b/vendor/vendor.json index cebfc6acd..4d960705a 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -996,10 +996,10 @@ "revision": "7e024ce8ce18b21b475ac6baf8fa3c42536bf2fa" }, { - "checksumSHA1": "cwT95naFga0RFGUZsCT1NeX5ncI=", + "checksumSHA1": "gPuHq0UytpuYPb2YWmFVb22Twcc=", "path": "github.com/mitchellh/cli", - "revision": "921cc83dadc195c0cd67f9df3a6ec822400a1df5", - "revisionTime": "2017-07-25T23:05:51Z" + "revision": "0ce7cd515f64496ee660ab19f6bbf373945d3af0", + "revisionTime": "2017-08-24T19:02:09Z" }, { "checksumSHA1": "ttEN1Aupb7xpPMkQLqb3tzLFdXs=", From e7f617476196f9336081cb76f5801781d1d01703 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 24 Aug 2017 22:55:40 +0000 Subject: [PATCH 11/13] add changelog --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 213f3faee..d8e98ca87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ IMPROVEMENTS: rolling update could remove an unnecessary amount of allocations [GH-3070] * api: Redact Vault.Token from AgentSelf response [GH-2988] * cli: node-status displays node version [GH-3002] + * cli: Add status command with autocomplete [GH-3047] * cli: Disable color output when STDOUT is not a TTY [GH-3057] * cli: Add autocomplete functionality for flags for all CLI command [GH 3087] * client: Unmount task directories when alloc is terminal [GH-3006] @@ -157,7 +158,7 @@ __BACKWARDS INCOMPATIBILITIES:__ prior to this release. A single image is expected by the driver so this behavior has been changed to take a single path as a string. Jobs using the `load` command should update the syntax to a single string. [GH-2361] - + IMPROVEMENTS: * core: Handle Serf Reap event [GH-2310] * core: Update Serf and Memberlist for more reliable gossip [GH-2255] @@ -203,7 +204,7 @@ BUG FIXES: * client: Fix remounting alloc dirs after reboots [GH-2391] [GH-2394] * client: Replace `-` with `_` in environment variable names [GH-2406] * client: Fix panic and deadlock during client restore state when prestart - fails [GH-2376] + fails [GH-2376] * config: Fix Consul Config Merging/Copying [GH-2278] * config: Fix Client reserved resource merging panic [GH-2281] * server: Fix panic when forwarding Vault derivation requests from non-leader @@ -218,7 +219,7 @@ IMPROVEMENTS: BUG FIXES: * client: Fix panic when upgrading to 0.5.3 [GH-2256] -## 0.5.3 (January 30, 2017) +## 0.5.3 (January 30, 2017) IMPROVEMENTS: * core: Introduce parameterized jobs and dispatch command/API [GH-2128] @@ -319,7 +320,7 @@ IMPROVEMENTS: * core: Scheduler version enforcement disallows different scheduler version from making decisions simultaneously [GH-1872] * core: Introduce node SecretID which can be used to minimize the available - surface area of RPCs to malicious Nomad Clients [GH-1597] + surface area of RPCs to malicious Nomad Clients [GH-1597] * core: Add `sticky` volumes which inform the scheduler to prefer placing updated allocations on the same node and to reuse the `local/` and `alloc/data` directory from previous allocation allowing semi-persistent @@ -383,7 +384,7 @@ BUG FIXES: logger [GH-1886] * client/fingerprint: Fix inconsistent CPU MHz fingerprinting [GH-1366] * env/aws: Fix an issue with reserved ports causing placement failures - [GH-1617] + [GH-1617] * discovery: Interpolate all service and check fields [GH-1966] * discovery: Fix old services not getting removed from Consul on update [GH-1668] From f6ca06ce431f32bb7091ccf95255d9820074b2cf Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 25 Aug 2017 14:24:36 +0000 Subject: [PATCH 12/13] refactor and fixups from code review --- CHANGELOG.md | 3 +- command/status.go | 30 ++++++------ command/status_test.go | 109 ++++++++++++++--------------------------- 3 files changed, 53 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e98ca87..e0504dbeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,10 @@ IMPROVEMENTS: rolling update could remove an unnecessary amount of allocations [GH-3070] * api: Redact Vault.Token from AgentSelf response [GH-2988] * cli: node-status displays node version [GH-3002] - * cli: Add status command with autocomplete [GH-3047] * cli: Disable color output when STDOUT is not a TTY [GH-3057] * cli: Add autocomplete functionality for flags for all CLI command [GH 3087] + * cli: Add status command which takes any identifier and routes to the + appropriate status command. * client: Unmount task directories when alloc is terminal [GH-3006] * client/template: Allow template to set Vault grace [GH-2947] * client/template: Template emits events explaining why it is blocked [GH-3001] diff --git a/command/status.go b/command/status.go index 6c78273b3..c86fc8af8 100644 --- a/command/status.go +++ b/command/status.go @@ -13,6 +13,21 @@ type StatusCommand struct { Meta } +func (s *StatusCommand) Help() string { + helpText := ` +Usage: nomad status [options] + + Display the status output for any given resource. The command will + detect the type of resource being queried and display the appropriate + status output. + +General Options: + + ` + generalOptionsUsage() + + return strings.TrimSpace(helpText) +} + func (c *StatusCommand) Run(args []string) int { flags := c.Meta.FlagSet("status", FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } @@ -88,21 +103,6 @@ func (c *StatusCommand) Run(args []string) int { return cmd.Run(argsCopy) } -func (s *StatusCommand) Help() string { - helpText := ` -Usage: nomad status [options] - - Display the status output for any given resource. The command will - detect the type of resource being queried and display the appropriate - status output. - -General Options: - - ` + generalOptionsUsage() - - return strings.TrimSpace(helpText) -} - func (s *StatusCommand) AutocompleteFlags() complete.Flags { return nil } diff --git a/command/status_test.go b/command/status_test.go index 51d701fe8..f28bc773f 100644 --- a/command/status_test.go +++ b/command/status_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/hashicorp/nomad/command/agent" + "github.com/hashicorp/nomad/nomad/mock" + "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" "github.com/posener/complete" @@ -15,28 +17,24 @@ func TestStatusCommand_Run_JobStatus(t *testing.T) { assert := assert.New(t) t.Parallel() - srv, client, url := testServer(t, true, nil) + srv, _, 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) - assert.Nil(err) - - if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { - t.Fatalf("status code non zero saw %d", code) - } + // Create a fake job + state := srv.Agent.Server().State() + j := mock.Job() + assert.Nil(state.UpsertJob(1000, j)) // Query to check the job status - if code := cmd.Run([]string{"-address=" + url, "job1_sfx"}); code != 0 { + if code := cmd.Run([]string{"-address=" + url, j.ID}); code != 0 { t.Fatalf("expected exit 0, got: %d", code) } out := ui.OutputWriter.String() - assert.Contains(out, "job1_sfx") + assert.Contains(out, j.ID) ui.OutputWriter.Reset() } @@ -45,38 +43,24 @@ func TestStatusCommand_Run_EvalStatus(t *testing.T) { assert := assert.New(t) t.Parallel() - srv, client, url := testServer(t, true, nil) + srv, _, 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) - assert.Nil(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 - } - } - - assert.NotEqual("", evalID) + // Create a fake eval + state := srv.Agent.Server().State() + eval := mock.Eval() + assert.Nil(state.UpsertEvals(1000, []*structs.Evaluation{eval})) // Query to check the eval status - if code := cmd.Run([]string{"-address=" + url, evalID}); code != 0 { + if code := cmd.Run([]string{"-address=" + url, eval.ID}); code != 0 { t.Fatalf("expected exit 0, got: %d", code) } out := ui.OutputWriter.String() - assert.Contains(out, evalID) + assert.Contains(out, eval.ID[:shortId]) ui.OutputWriter.Reset() } @@ -125,36 +109,23 @@ func TestStatusCommand_Run_AllocStatus(t *testing.T) { assert := assert.New(t) t.Parallel() - srv, client, url := testServer(t, true, nil) + srv, _, 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) - assert.Nil(err) + // Create a fake alloc + state := srv.Agent.Server().State() + alloc := mock.Alloc() + assert.Nil(state.UpsertAllocs(1000, []*structs.Allocation{alloc})) - 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 - } - } - assert.NotEqual("", allocId1) - - if code := cmd.Run([]string{"-address=" + url, allocId1}); code != 0 { + if code := cmd.Run([]string{"-address=" + url, alloc.ID}); code != 0 { t.Fatalf("expected exit 0, got: %d", code) } out := ui.OutputWriter.String() - assert.Contains(out, allocId1) + assert.Contains(out, alloc.ID[:shortId]) ui.OutputWriter.Reset() } @@ -163,20 +134,16 @@ func TestStatusCommand_Run_NoPrefix(t *testing.T) { assert := assert.New(t) t.Parallel() - srv, client, url := testServer(t, true, nil) + srv, _, 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) - assert.Nil(err) - - if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { - t.Fatalf("status code non zero saw %d", code) - } + // Create a fake job + state := srv.Agent.Server().State() + job := mock.Job() + assert.Nil(state.UpsertJob(1000, job)) // Query to check status if code := cmd.Run([]string{"-address=" + url}); code != 0 { @@ -184,7 +151,7 @@ func TestStatusCommand_Run_NoPrefix(t *testing.T) { } out := ui.OutputWriter.String() - assert.Contains(out, "job1_sfx") + assert.Contains(out, job.ID) ui.OutputWriter.Reset() } @@ -193,27 +160,23 @@ func TestStatusCommand_AutocompleteArgs(t *testing.T) { assert := assert.New(t) t.Parallel() - srv, client, url := testServer(t, true, nil) + srv, _, 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) - assert.Nil(err) + // Create a fake job + state := srv.Agent.Server().State() + job := mock.Job() + assert.Nil(state.UpsertJob(1000, job)) - if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { - t.Fatalf("status code non zero saw %d", code) - } - - prefix := jobID[:len(jobID)-5] + prefix := job.ID[:len(job.ID)-5] args := complete.Args{Last: prefix} predictor := cmd.AutocompleteArgs() res := predictor.Predict(args) - assert.Contains(res, jobID) + assert.Contains(res, job.ID) args = complete.Args{Last: prefix, Completed: []string{prefix, "1", "2"}} predictor = cmd.AutocompleteArgs() From 039c2070cc3a797b79cba0427159cc83d6f2635f Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 25 Aug 2017 19:42:22 +0000 Subject: [PATCH 13/13] add global flags to status; re-order functions --- command/status.go | 68 +++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/command/status.go b/command/status.go index c86fc8af8..c515c822d 100644 --- a/command/status.go +++ b/command/status.go @@ -28,6 +28,40 @@ General Options: return strings.TrimSpace(helpText) } +func (c *StatusCommand) Synopsis() string { + return "Display the status output for a resource" +} + +func (c *StatusCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), nil) +} + +func (c *StatusCommand) AutocompleteArgs() complete.Predictor { + client, _ := c.Meta.Client() + return complete.PredictFunc(func(a complete.Args) []string { + if len(a.Completed) > 1 { + return nil + } + + resp, err := client.Search().PrefixSearch(a.Last, contexts.All) + if err != nil { + return []string{} + } + + final := make([]string, 0) + + for _, matches := range resp.Matches { + if len(matches) == 0 { + continue + } + + final = append(final, matches...) + } + + return final + }) +} + func (c *StatusCommand) Run(args []string) int { flags := c.Meta.FlagSet("status", FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } @@ -102,37 +136,3 @@ func (c *StatusCommand) Run(args []string) int { return cmd.Run(argsCopy) } - -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 { - if len(a.Completed) > 1 { - return nil - } - - resp, err := client.Search().PrefixSearch(a.Last, contexts.All) - if err != nil { - return []string{} - } - - final := make([]string, 0) - - for _, matches := range resp.Matches { - if len(matches) == 0 { - continue - } - - final = append(final, matches...) - } - - return final - }) -} - -func (c *StatusCommand) Synopsis() string { - return "Display the status output for a resource" -}