Skip to content

Commit

Permalink
Merge pull request #3120 from hashicorp/b-multi-match
Browse files Browse the repository at this point in the history
Fix exact job match when prefix of other job
  • Loading branch information
dadgar committed Aug 29, 2017
2 parents 4692d1f + cd5a54b commit 8fb23c3
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 3 deletions.
22 changes: 20 additions & 2 deletions command/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.logMultiMatchError(id, res.Matches)
return 1
}
}
Expand All @@ -134,3 +138,17 @@ func (c *StatusCommand) Run(args []string) int {

return cmd.Run(argsCopy)
}

// 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 {
continue
}

c.Ui.Error(fmt.Sprintf("\n%s:", strings.Title(string(ctx))))
c.Ui.Error(fmt.Sprintf("%s", strings.Join(vers, ", ")))
}
}
29 changes: 29 additions & 0 deletions command/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 4 additions & 1 deletion nomad/search_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
95 changes: 95 additions & 0 deletions nomad/search_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 8fb23c3

Please sign in to comment.