From 3902385b3e5a67dd756c14fa57c2e24ddf691f08 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Tue, 29 Aug 2017 08:42:09 -0700 Subject: [PATCH 1/3] Status honors exact match and displays matches when more than one is available --- command/status.go | 20 ++++++++++++++++++-- command/status_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/command/status.go b/command/status.go index c9774f447ea0..6553c721eaa1 100644 --- a/command/status.go +++ b/command/status.go @@ -103,14 +103,18 @@ func (c *StatusCommand) Run(args []string) int { var match contexts.Context matchCount := 0 for ctx, vers := range res.Matches { - if len(vers) == 1 { + if l := len(vers); l == 1 { match = ctx matchCount++ + } else if l > 0 && vers[0] == id { + // Exact match + match = ctx + break } // 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 %q", id)) + c.outputMultipleMatches(id, res.Matches) return 1 } } @@ -134,3 +138,15 @@ func (c *StatusCommand) Run(args []string) int { return cmd.Run(argsCopy) } + +func (c *StatusCommand) outputMultipleMatches(id string, matches map[contexts.Context][]string) { + c.Ui.Error(fmt.Sprintf("Multiple matches found for id %q", id)) + for ctx, vers := range matches { + if len(vers) == 0 { + continue + } + + c.Ui.Error(fmt.Sprintf("\n%s:", strings.Title(string(ctx)))) + c.Ui.Error(fmt.Sprintf("%s", strings.Join(vers, ", "))) + } +} diff --git a/command/status_test.go b/command/status_test.go index a25f6abb4a9f..4b7f1c38fa48 100644 --- a/command/status_test.go +++ b/command/status_test.go @@ -39,6 +39,35 @@ func TestStatusCommand_Run_JobStatus(t *testing.T) { ui.OutputWriter.Reset() } +func TestStatusCommand_Run_JobStatus_MultiMatch(t *testing.T) { + assert := assert.New(t) + t.Parallel() + + srv, _, url := testServer(t, true, nil) + defer srv.Shutdown() + + ui := new(cli.MockUi) + cmd := &StatusCommand{Meta: Meta{Ui: ui, flagAddress: url}} + + // Create two fake jobs sharing a prefix + state := srv.Agent.Server().State() + j := mock.Job() + j2 := mock.Job() + j2.ID = fmt.Sprintf("%s-more", j.ID) + assert.Nil(state.UpsertJob(1000, j)) + assert.Nil(state.UpsertJob(1001, j2)) + + // Query to check the job status + 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, j.ID) + + ui.OutputWriter.Reset() +} + func TestStatusCommand_Run_EvalStatus(t *testing.T) { assert := assert.New(t) t.Parallel() From a50a3af82615e4648c38bf1b1bee43c4c380cf07 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Tue, 29 Aug 2017 09:21:18 -0700 Subject: [PATCH 2/3] Fix even length uuid post hyphen --- nomad/search_endpoint.go | 5 +- nomad/search_endpoint_test.go | 95 +++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/nomad/search_endpoint.go b/nomad/search_endpoint.go index daa40148dbc7..bf6d7c87529d 100644 --- a/nomad/search_endpoint.go +++ b/nomad/search_endpoint.go @@ -91,7 +91,10 @@ func roundUUIDDownIfOdd(prefix string, context structs.Context) string { return prefix } - l := len(prefix) + // We ignore the count of hyphens when calculating if the prefix is even: + // E.g "e3671fa4-21" + numHyphens := strings.Count(prefix, "-") + l := len(prefix) - numHyphens if l%2 == 0 { return prefix } diff --git a/nomad/search_endpoint_test.go b/nomad/search_endpoint_test.go index a72121e2cc1d..462255f12232 100644 --- a/nomad/search_endpoint_test.go +++ b/nomad/search_endpoint_test.go @@ -55,6 +55,48 @@ func TestSearch_PrefixSearch_Job(t *testing.T) { assert.Equal(uint64(jobIndex), resp.Index) } +func TestSearch_PrefixSearch_All_JobWithHyphen(t *testing.T) { + assert := assert.New(t) + prefix := "example-test" + + t.Parallel() + s := testServer(t, func(c *Config) { + c.NumSchedulers = 0 + }) + + defer s.Shutdown() + codec := rpcClient(t, s) + testutil.WaitForLeader(t, s.RPC) + + // Register a job and an allocation + jobID := registerAndVerifyJob(s, t, prefix, 0) + alloc := mock.Alloc() + alloc.JobID = jobID + summary := mock.JobSummary(alloc.JobID) + state := s.fsm.State() + + if err := state.UpsertJobSummary(999, summary); err != nil { + t.Fatalf("err: %v", err) + } + if err := state.UpsertAllocs(1000, []*structs.Allocation{alloc}); err != nil { + t.Fatalf("err: %v", err) + } + + req := &structs.SearchRequest{ + Prefix: "example-", + 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.EqualValues(jobIndex, resp.Index) +} + // truncate should limit results to 20 func TestSearch_PrefixSearch_Truncate(t *testing.T) { assert := assert.New(t) @@ -198,6 +240,59 @@ func TestSearch_PrefixSearch_Allocation(t *testing.T) { assert.Equal(uint64(90), resp.Index) } +func TestSearch_PrefixSearch_All_UUID_EvenPrefix(t *testing.T) { + assert := assert.New(t) + t.Parallel() + s := testServer(t, func(c *Config) { + c.NumSchedulers = 0 + }) + + defer s.Shutdown() + codec := rpcClient(t, s) + testutil.WaitForLeader(t, s.RPC) + + alloc := mock.Alloc() + summary := mock.JobSummary(alloc.JobID) + state := s.fsm.State() + + if err := state.UpsertJobSummary(999, summary); err != nil { + t.Fatalf("err: %v", err) + } + if err := state.UpsertAllocs(1000, []*structs.Allocation{alloc}); err != nil { + t.Fatalf("err: %v", err) + } + + node := mock.Node() + if err := state.UpsertNode(1001, node); err != nil { + t.Fatalf("err: %v", err) + } + + eval1 := mock.Eval() + eval1.ID = node.ID + if err := state.UpsertEvals(1002, []*structs.Evaluation{eval1}); err != nil { + t.Fatalf("err: %v", err) + } + + prefix := alloc.ID[:13] + t.Log(prefix) + + 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.Allocs])) + assert.Equal(alloc.ID, resp.Matches[structs.Allocs][0]) + assert.Equal(resp.Truncations[structs.Allocs], false) + + assert.EqualValues(1002, resp.Index) +} + func TestSearch_PrefixSearch_Node(t *testing.T) { assert := assert.New(t) t.Parallel() From cd5a54bc74f8d028ec24fe3e4a3aed9b9c1ad6f0 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Tue, 29 Aug 2017 09:46:14 -0700 Subject: [PATCH 3/3] respond to comments --- command/status.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/command/status.go b/command/status.go index 6553c721eaa1..1d4850ec596b 100644 --- a/command/status.go +++ b/command/status.go @@ -114,7 +114,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.outputMultipleMatches(id, res.Matches) + c.logMultiMatchError(id, res.Matches) return 1 } } @@ -139,7 +139,9 @@ func (c *StatusCommand) Run(args []string) int { return cmd.Run(argsCopy) } -func (c *StatusCommand) outputMultipleMatches(id string, matches map[contexts.Context][]string) { +// logMultiMatchError is used to log an error message when multiple matches are +// found. The error message logged displays the matched IDs per context. +func (c *StatusCommand) logMultiMatchError(id string, matches map[contexts.Context][]string) { c.Ui.Error(fmt.Sprintf("Multiple matches found for id %q", id)) for ctx, vers := range matches { if len(vers) == 0 {