diff --git a/api/allocations.go b/api/allocations.go index e2885507f9b8..9a87bbaf8f7b 100644 --- a/api/allocations.go +++ b/api/allocations.go @@ -26,6 +26,10 @@ func (a *Allocations) List(q *QueryOptions) ([]*AllocationListStub, *QueryMeta, return resp, qm, nil } +func (a *Allocations) PrefixList(prefix string) ([]*AllocationListStub, *QueryMeta, error) { + return a.List(&QueryOptions{Prefix: prefix}) +} + // Info is used to retrieve a single allocation. func (a *Allocations) Info(allocID string, q *QueryOptions) (*Allocation, *QueryMeta, error) { var resp Allocation diff --git a/api/allocations_test.go b/api/allocations_test.go index c8fde832f79d..597000472fc2 100644 --- a/api/allocations_test.go +++ b/api/allocations_test.go @@ -52,6 +52,52 @@ func TestAllocations_List(t *testing.T) { } } +func TestAllocations_PrefixList(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + a := c.Allocations() + + // Querying when no allocs exist returns nothing + allocs, qm, err := a.PrefixList("") + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex != 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if n := len(allocs); n != 0 { + t.Fatalf("expected 0 allocs, got: %d", n) + } + + // TODO: do something that causes an allocation to actually happen + // so we can query for them. + return + + job := &Job{ + ID: "job1", + Name: "Job #1", + Type: JobTypeService, + } + eval, _, err := c.Jobs().Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + // List the allocations by prefix + allocs, qm, err = a.PrefixList("foobar") + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex == 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + + // Check that we got the allocation back + if len(allocs) == 0 || allocs[0].EvalID != eval { + t.Fatalf("bad: %#v", allocs) + } +} + func TestAllocations_CreateIndexSort(t *testing.T) { allocs := []*AllocationListStub{ &AllocationListStub{CreateIndex: 2}, diff --git a/api/api.go b/api/api.go index 5ca63aad78d8..32d9b87b0f26 100644 --- a/api/api.go +++ b/api/api.go @@ -31,6 +31,9 @@ type QueryOptions struct { // WaitTime is used to bound the duration of a wait. // Defaults to that of the Config, but can be overriden. WaitTime time.Duration + + // If set, used as prefix for resource list searches + Prefix string } // WriteOptions are used to parameterize a write @@ -150,6 +153,9 @@ func (r *request) setQueryOptions(q *QueryOptions) { if q.WaitTime != 0 { r.params.Set("wait", durToMsec(q.WaitTime)) } + if q.Prefix != "" { + r.params.Set("prefix", q.Prefix) + } } // durToMsec converts a duration to a millisecond specified string diff --git a/api/evaluations.go b/api/evaluations.go index 9fbb83b5ca63..304f5ae72add 100644 --- a/api/evaluations.go +++ b/api/evaluations.go @@ -26,6 +26,10 @@ func (e *Evaluations) List(q *QueryOptions) ([]*Evaluation, *QueryMeta, error) { return resp, qm, nil } +func (e *Evaluations) PrefixList(prefix string) ([]*Evaluation, *QueryMeta, error) { + return e.List(&QueryOptions{Prefix: prefix}) +} + // Info is used to query a single evaluation by its ID. func (e *Evaluations) Info(evalID string, q *QueryOptions) (*Evaluation, *QueryMeta, error) { var resp Evaluation diff --git a/api/evaluations_test.go b/api/evaluations_test.go index c7772dc96e61..2a66534e8b26 100644 --- a/api/evaluations_test.go +++ b/api/evaluations_test.go @@ -46,6 +46,45 @@ func TestEvaluations_List(t *testing.T) { } } +func TestEvaluations_PrefixList(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + e := c.Evaluations() + + // Listing when nothing exists returns empty + result, qm, err := e.PrefixList("abcdef") + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex != 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if n := len(result); n != 0 { + t.Fatalf("expected 0 evaluations, got: %d", n) + } + + // Register a job. This will create an evaluation. + jobs := c.Jobs() + job := testJob() + evalID, wm, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Check the evaluations again + result, qm, err = e.PrefixList(evalID[:4]) + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + + // Check if we have the right list + if len(result) != 1 || result[0].ID != evalID { + t.Fatalf("bad: %#v", result) + } +} + func TestEvaluations_Info(t *testing.T) { c, s := makeClient(t, nil, nil) defer s.Stop() diff --git a/api/jobs.go b/api/jobs.go index 3eea39daf099..185539277fc9 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -47,6 +47,11 @@ func (j *Jobs) List(q *QueryOptions) ([]*JobListStub, *QueryMeta, error) { return resp, qm, nil } +// PrefixList is used to list all existing jobs that match the prefix. +func (j *Jobs) PrefixList(prefix string) ([]*JobListStub, *QueryMeta, error) { + return j.List(&QueryOptions{Prefix: prefix}) +} + // Info is used to retrieve information about a particular // job given its unique ID. func (j *Jobs) Info(jobID string, q *QueryOptions) (*Job, *QueryMeta, error) { diff --git a/api/jobs_test.go b/api/jobs_test.go index 51018e159a91..3ab49a185551 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -81,6 +81,82 @@ func TestJobs_Info(t *testing.T) { } } +func TestJobs_PrefixList(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Listing when nothing exists returns empty + results, qm, err := jobs.PrefixList("dummy") + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex != 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if n := len(results); n != 0 { + t.Fatalf("expected 0 jobs, got: %d", n) + } + + // Register the job + job := testJob() + _, wm, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Query the job again and ensure it exists + // Listing when nothing exists returns empty + results, qm, err = jobs.PrefixList(job.ID[:1]) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Check if we have the right list + if len(results) != 1 || results[0].ID != job.ID { + t.Fatalf("bad: %#v", results) + } +} + +func TestJobs_List(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Listing when nothing exists returns empty + results, qm, err := jobs.List(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex != 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if n := len(results); n != 0 { + t.Fatalf("expected 0 jobs, got: %d", n) + } + + // Register the job + job := testJob() + _, wm, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Query the job again and ensure it exists + // Listing when nothing exists returns empty + results, qm, err = jobs.List(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Check if we have the right list + if len(results) != 1 || results[0].ID != job.ID { + t.Fatalf("bad: %#v", results) + } +} + func TestJobs_Allocations(t *testing.T) { c, s := makeClient(t, nil, nil) defer s.Stop() diff --git a/api/nodes.go b/api/nodes.go index 5c8b01d47d1d..4f3d664896f1 100644 --- a/api/nodes.go +++ b/api/nodes.go @@ -26,6 +26,10 @@ func (n *Nodes) List(q *QueryOptions) ([]*NodeListStub, *QueryMeta, error) { return resp, qm, nil } +func (n *Nodes) PrefixList(prefix string) ([]*NodeListStub, *QueryMeta, error) { + return n.List(&QueryOptions{Prefix: prefix}) +} + // Info is used to query a specific node by its ID. func (n *Nodes) Info(nodeID string, q *QueryOptions) (*Node, *QueryMeta, error) { var resp Node diff --git a/api/nodes_test.go b/api/nodes_test.go index 23bd1af9cb43..2e0ed5a9246a 100644 --- a/api/nodes_test.go +++ b/api/nodes_test.go @@ -38,6 +38,47 @@ func TestNodes_List(t *testing.T) { assertQueryMeta(t, qm) } +func TestNodes_PrefixList(t *testing.T) { + c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) { + c.DevMode = true + }) + defer s.Stop() + nodes := c.Nodes() + + var qm *QueryMeta + var out []*NodeListStub + var err error + + // Get the node ID + var nodeID, dc string + testutil.WaitForResult(func() (bool, error) { + out, _, err := nodes.List(nil) + if err != nil { + return false, err + } + if n := len(out); n != 1 { + return false, fmt.Errorf("expected 1 node, got: %d", n) + } + nodeID = out[0].ID + dc = out[0].Datacenter + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + + // Find node based on four character prefix + out, qm, err = nodes.PrefixList(nodeID[:4]) + if err != nil { + t.Fatalf("err: %s", err) + } + if n := len(out); n != 1 { + t.Fatalf("expected 1 node, got: %d ", n) + } + + // Check that we got valid QueryMeta. + assertQueryMeta(t, qm) +} + func TestNodes_Info(t *testing.T) { c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) { c.DevMode = true diff --git a/command/agent/alloc_endpoint_test.go b/command/agent/alloc_endpoint_test.go index 3f071b0b070b..7bb80a3053d5 100644 --- a/command/agent/alloc_endpoint_test.go +++ b/command/agent/alloc_endpoint_test.go @@ -45,7 +45,7 @@ func TestHTTP_AllocsList(t *testing.T) { t.Fatalf("missing last contact") } - // Check the job + // Check the alloc n := obj.([]*structs.AllocListStub) if len(n) != 2 { t.Fatalf("bad: %#v", n) @@ -53,6 +53,57 @@ func TestHTTP_AllocsList(t *testing.T) { }) } +func TestHTTP_AllocsPrefixList(t *testing.T) { + httpTest(t, nil, func(s *TestServer) { + // Directly manipulate the state + state := s.Agent.server.State() + alloc1 := mock.Alloc() + alloc1.ID = "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706" + alloc2 := mock.Alloc() + alloc2.ID = "aaabbbbb-e8f7-fd38-c855-ab94ceb89706" + err := state.UpsertAllocs(1000, + []*structs.Allocation{alloc1, alloc2}) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/allocations?prefix=aaab", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.AllocsRequest(respW, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check for the index + if respW.HeaderMap.Get("X-Nomad-Index") == "" { + t.Fatalf("missing index") + } + if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { + t.Fatalf("missing known leader") + } + if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { + t.Fatalf("missing last contact") + } + + // Check the alloc + n := obj.([]*structs.AllocListStub) + if len(n) != 1 { + t.Fatalf("bad: %#v", n) + } + + // Check the identifier + if n[0].ID != alloc2.ID { + t.Fatalf("expected alloc ID: %v, Actual: %v", alloc2.ID, n[0].ID) + } + }) +} + func TestHTTP_AllocQuery(t *testing.T) { httpTest(t, nil, func(s *TestServer) { // Directly manipulate the state diff --git a/command/agent/eval_endpoint_test.go b/command/agent/eval_endpoint_test.go index 69a322ca5f3a..102c3d7c2666 100644 --- a/command/agent/eval_endpoint_test.go +++ b/command/agent/eval_endpoint_test.go @@ -45,7 +45,7 @@ func TestHTTP_EvalList(t *testing.T) { t.Fatalf("missing last contact") } - // Check the job + // Check the eval e := obj.([]*structs.Evaluation) if len(e) != 2 { t.Fatalf("bad: %#v", e) @@ -53,6 +53,57 @@ func TestHTTP_EvalList(t *testing.T) { }) } +func TestHTTP_EvalPrefixList(t *testing.T) { + httpTest(t, nil, func(s *TestServer) { + // Directly manipulate the state + state := s.Agent.server.State() + eval1 := mock.Eval() + eval1.ID = "aaabbbbb-e8f7-fd38-c855-ab94ceb89706" + eval2 := mock.Eval() + eval2.ID = "aaabbbbb-e8f7-fd38-c855-ab94ceb89706" + err := state.UpsertEvals(1000, + []*structs.Evaluation{eval1, eval2}) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/evaluations?prefix=aaab", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.EvalsRequest(respW, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check for the index + if respW.HeaderMap.Get("X-Nomad-Index") == "" { + t.Fatalf("missing index") + } + if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { + t.Fatalf("missing known leader") + } + if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { + t.Fatalf("missing last contact") + } + + // Check the eval + e := obj.([]*structs.Evaluation) + if len(e) != 1 { + t.Fatalf("bad: %#v", e) + } + + // Check the identifier + if e[0].ID != eval2.ID { + t.Fatalf("expected eval ID: %v, Actual: %v", eval2.ID, e[0].ID) + } + }) +} + func TestHTTP_EvalAllocations(t *testing.T) { httpTest(t, nil, func(s *TestServer) { // Directly manipulate the state diff --git a/command/agent/http.go b/command/agent/http.go index 1478c9d5a5b0..4c46ee8b4ea1 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -258,6 +258,14 @@ func parseConsistency(req *http.Request, b *structs.QueryOptions) { } } +// parsePrefix is used to parse the ?prefix query param +func parsePrefix(req *http.Request, b *structs.QueryOptions) { + query := req.URL.Query() + if prefix := query.Get("prefix"); prefix != "" { + b.Prefix = prefix + } +} + // parseRegion is used to parse the ?region query param func (s *HTTPServer) parseRegion(req *http.Request, r *string) { if other := req.URL.Query().Get("region"); other != "" { @@ -271,5 +279,6 @@ func (s *HTTPServer) parseRegion(req *http.Request, r *string) { func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, r *string, b *structs.QueryOptions) bool { s.parseRegion(req, r) parseConsistency(req, b) + parsePrefix(req, b) return parseWait(resp, req, b) } diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go index 3c960d12b022..cdb1bc88ffac 100644 --- a/command/agent/job_endpoint_test.go +++ b/command/agent/job_endpoint_test.go @@ -56,6 +56,59 @@ func TestHTTP_JobsList(t *testing.T) { }) } +func TestHTTP_PrefixJobsList(t *testing.T) { + ids := []string{ + "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706", + "aabbbbbb-e8f7-fd38-c855-ab94ceb89706", + "aabbcccc-e8f7-fd38-c855-ab94ceb89706", + } + httpTest(t, nil, func(s *TestServer) { + for i := 0; i < 3; i++ { + // Create the job + job := mock.Job() + job.ID = ids[i] + args := structs.JobRegisterRequest{ + Job: job, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.JobRegisterResponse + if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/jobs?prefix=aabb", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.JobsRequest(respW, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check for the index + if respW.HeaderMap.Get("X-Nomad-Index") == "" { + t.Fatalf("missing index") + } + if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { + t.Fatalf("missing known leader") + } + if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { + t.Fatalf("missing last contact") + } + + // Check the job + j := obj.([]*structs.JobListStub) + if len(j) != 2 { + t.Fatalf("bad: %#v", j) + } + }) +} + func TestHTTP_JobsRegister(t *testing.T) { httpTest(t, nil, func(s *TestServer) { // Create the job diff --git a/command/agent/node_endpoint_test.go b/command/agent/node_endpoint_test.go index a745a62784b4..ec52d8149096 100644 --- a/command/agent/node_endpoint_test.go +++ b/command/agent/node_endpoint_test.go @@ -48,7 +48,7 @@ func TestHTTP_NodesList(t *testing.T) { t.Fatalf("missing last contact") } - // Check the job + // Check the nodes n := obj.([]*structs.NodeListStub) if len(n) < 3 { // Maybe 4 including client t.Fatalf("bad: %#v", n) @@ -56,6 +56,55 @@ func TestHTTP_NodesList(t *testing.T) { }) } +func TestHTTP_NodesPrefixList(t *testing.T) { + httpTest(t, nil, func(s *TestServer) { + ids := []string{"aaaaa", "aaaab", "aaabb", "aabbb", "abbbb", "bbbbb"} + for i := 0; i < 5; i++ { + // Create the node + node := mock.Node() + node.ID = ids[i] + args := structs.NodeRegisterRequest{ + Node: node, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.NodeUpdateResponse + if err := s.Agent.RPC("Node.Register", &args, &resp); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/nodes?prefix=aaa", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.NodesRequest(respW, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check for the index + if respW.HeaderMap.Get("X-Nomad-Index") == "" { + t.Fatalf("missing index") + } + if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { + t.Fatalf("missing known leader") + } + if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { + t.Fatalf("missing last contact") + } + + // Check the nodes + n := obj.([]*structs.NodeListStub) + if len(n) != 3 { + t.Fatalf("bad: %#v", n) + } + }) +} + func TestHTTP_NodeForceEval(t *testing.T) { httpTest(t, nil, func(s *TestServer) { // Create the node diff --git a/command/alloc_status.go b/command/alloc_status.go index 07467597650c..3616817686af 100644 --- a/command/alloc_status.go +++ b/command/alloc_status.go @@ -68,8 +68,37 @@ func (c *AllocStatusCommand) Run(args []string) int { // Query the allocation info alloc, _, err := client.Allocations().Info(allocID, nil) if err != nil { - c.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err)) - return 1 + allocs, _, err := client.Allocations().PrefixList(allocID) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying allocation: %v", err)) + return 1 + } + if len(allocs) == 0 { + c.Ui.Error(fmt.Sprintf("No allocation(s) with prefix or id %q found", allocID)) + return 1 + } + if len(allocs) > 1 { + // Format the allocs + out := make([]string, len(allocs)+1) + out[0] = "ID|EvalID|JobID|TaskGroup|DesiredStatus|ClientStatus" + for i, alloc := range allocs { + out[i+1] = fmt.Sprintf("%s|%s|%s|%s|%s|%s", + alloc.ID, + alloc.EvalID, + alloc.JobID, + alloc.TaskGroup, + alloc.DesiredStatus, + alloc.ClientStatus) + } + c.Ui.Output(fmt.Sprintf("Please disambiguate the desired allocation\n\n%s", formatList(out))) + return 0 + } + // Prefix lookup matched a single allocation + alloc, _, err = client.Allocations().Info(allocs[0].ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err)) + return 1 + } } // Format the allocation data diff --git a/command/alloc_status_test.go b/command/alloc_status_test.go index 090ebb0ebc16..208cca88fb08 100644 --- a/command/alloc_status_test.go +++ b/command/alloc_status_test.go @@ -34,12 +34,13 @@ func TestAllocStatusCommand_Fails(t *testing.T) { if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying allocation") { t.Fatalf("expected failed query error, got: %s", out) } + ui.ErrorWriter.Reset() // Fails on missing alloc if code := cmd.Run([]string{"-address=" + url, "26470238-5CF2-438F-8772-DC67CFB0705C"}); code != 1 { t.Fatalf("expected exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "not found") { + if out := ui.ErrorWriter.String(); !strings.Contains(out, "No allocation(s) with prefix or id") { t.Fatalf("expected not found error, got: %s", out) } } diff --git a/command/eval_monitor_test.go b/command/eval_monitor_test.go index a7c341252412..f805872f3252 100644 --- a/command/eval_monitor_test.go +++ b/command/eval_monitor_test.go @@ -31,7 +31,7 @@ func TestEvalMonitorCommand_Fails(t *testing.T) { if code := cmd.Run([]string{"-address=" + url, "3E55C771-76FC-423B-BCED-3E5314F433B1"}); code != 1 { t.Fatalf("expect exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "not found") { + if out := ui.ErrorWriter.String(); !strings.Contains(out, "No evaluation(s) with prefix or id") { t.Fatalf("expect not found error, got: %s", out) } ui.ErrorWriter.Reset() diff --git a/command/monitor.go b/command/monitor.go index 5fd6050b1823..cb1de5e5a683 100644 --- a/command/monitor.go +++ b/command/monitor.go @@ -182,8 +182,35 @@ func (m *monitor) monitor(evalID string) int { // Query the evaluation eval, _, err := m.client.Evaluations().Info(evalID, nil) if err != nil { - m.ui.Error(fmt.Sprintf("Error reading evaluation: %s", err)) - return 1 + evals, _, err := m.client.Evaluations().PrefixList(evalID) + if err != nil { + m.ui.Error(fmt.Sprintf("Error reading evaluation: %s", err)) + return 1 + } + if len(evals) == 0 { + m.ui.Error(fmt.Sprintf("No evaluation(s) with prefix or id %q found", evalID)) + return 1 + } + if len(evals) > 1 { + // Format the evaluations + out := make([]string, len(evals)+1) + out[0] = "ID|Priority|Type|TriggeredBy|Status" + for i, eval := range evals { + out[i+1] = fmt.Sprintf("%s|%d|%s|%s|%s", + eval.ID, + eval.Priority, + eval.Type, + eval.TriggeredBy, + eval.Status) + } + m.ui.Output(fmt.Sprintf("Please disambiguate the desired evaluation\n\n%s", formatList(out))) + return 0 + } + // Prefix lookup matched a single evaluation + eval, _, err = m.client.Evaluations().Info(evals[0].ID, nil) + if err != nil { + m.ui.Error(fmt.Sprintf("Error reading evaluation: %s", err)) + } } // Create the new eval state. @@ -196,7 +223,7 @@ func (m *monitor) monitor(evalID string) int { state.index = eval.CreateIndex // Query the allocations associated with the evaluation - allocs, _, err := m.client.Evaluations().Allocations(evalID, nil) + allocs, _, err := m.client.Evaluations().Allocations(eval.ID, nil) if err != nil { m.ui.Error(fmt.Sprintf("Error reading allocations: %s", err)) return 1 diff --git a/command/monitor_test.go b/command/monitor_test.go index fd0be835afd8..59bfb4599092 100644 --- a/command/monitor_test.go +++ b/command/monitor_test.go @@ -276,6 +276,52 @@ func TestMonitor_Monitor(t *testing.T) { } } +func TestMonitor_MonitorWithPrefix(t *testing.T) { + srv, client, _ := testServer(t, nil) + defer srv.Stop() + + // Create the monitor + ui := new(cli.MockUi) + mon := newMonitor(ui, client) + + // Submit a job - this creates a new evaluation we can monitor + job := testJob("job1") + evalID, _, err := client.Jobs().Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Start monitoring the eval + var code int + doneCh := make(chan struct{}) + go func() { + defer close(doneCh) + code = mon.monitor(evalID[:4]) + }() + + // Wait for completion + select { + case <-doneCh: + case <-time.After(5 * time.Second): + t.Fatalf("eval monitor took too long") + } + + // Check the return code. We should get exit code 2 as there + // would be a scheduling problem on the test server (no clients). + if code != 2 { + t.Fatalf("expect exit 2, got: %d", code) + } + + // Check the output + out := ui.OutputWriter.String() + if !strings.Contains(out, evalID) { + t.Fatalf("missing eval\n\n%s", out) + } + if !strings.Contains(out, "finished with status") { + t.Fatalf("missing final status\n\n%s", out) + } +} + func TestMonitor_DumpAllocStatus(t *testing.T) { ui := new(cli.MockUi) diff --git a/command/node_drain.go b/command/node_drain.go index efb0d13fe0b0..4a0a696ecaf6 100644 --- a/command/node_drain.go +++ b/command/node_drain.go @@ -68,8 +68,48 @@ func (c *NodeDrainCommand) Run(args []string) int { return 1 } + // Check if node exists + node, _, err := client.Nodes().Info(nodeID, nil) + if err != nil { + // Exact lookup failed, try with prefix based search + nodes, _, err := client.Nodes().PrefixList(nodeID) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error toggling drain mode: %s", err)) + return 1 + } + // Return error if no nodes are found + if len(nodes) == 0 { + c.Ui.Error(fmt.Sprintf("No node(s) with prefix or id %q found", nodeID)) + return 1 + } + if len(nodes) > 1 { + // Format the nodes list that matches the prefix so that the user + // can create a more specific request + out := make([]string, len(nodes)+1) + out[0] = "ID|DC|Name|Class|Drain|Status" + for i, node := range nodes { + out[i+1] = fmt.Sprintf("%s|%s|%s|%s|%v|%s", + node.ID, + node.Datacenter, + node.Name, + node.NodeClass, + node.Drain, + node.Status) + } + // Dump the output + c.Ui.Output(fmt.Sprintf("Please disambiguate the desired node\n\n%s", formatList(out))) + return 0 + } + // Prefix lookup matched a single node + node, _, err = client.Nodes().Info(nodes[0].ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error toggling drain mode: %s", err)) + return 1 + } + } + // Toggle node draining - if _, err := client.Nodes().ToggleDrain(nodeID, enable, nil); err != nil { + if _, err := client.Nodes().ToggleDrain(node.ID, enable, nil); err != nil { c.Ui.Error(fmt.Sprintf("Error toggling drain mode: %s", err)) return 1 } diff --git a/command/node_drain_test.go b/command/node_drain_test.go index fa437efb7f12..af22d5be00c0 100644 --- a/command/node_drain_test.go +++ b/command/node_drain_test.go @@ -40,7 +40,7 @@ func TestNodeDrainCommand_Fails(t *testing.T) { if code := cmd.Run([]string{"-address=" + url, "-enable", "nope"}); code != 1 { t.Fatalf("expected exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "not found") { + if out := ui.ErrorWriter.String(); !strings.Contains(out, "No node(s) with prefix or id") { t.Fatalf("expected not exist error, got: %s", out) } ui.ErrorWriter.Reset() diff --git a/command/node_status.go b/command/node_status.go index b3cba519cbea..fd29f2335ce6 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -100,8 +100,41 @@ func (c *NodeStatusCommand) Run(args []string) int { nodeID := args[0] node, _, err := client.Nodes().Info(nodeID, nil) if err != nil { - c.Ui.Error(fmt.Sprintf("Error querying node info: %s", err)) - return 1 + // Exact lookup failed, try with prefix based search + nodes, _, err := client.Nodes().PrefixList(nodeID) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying node info: %s", err)) + return 1 + } + // Return error if no nodes are found + if len(nodes) == 0 { + c.Ui.Error(fmt.Sprintf("No node(s) with prefix %q found", nodeID)) + return 1 + } + if len(nodes) > 1 { + // Format the nodes list that matches the prefix so that the user + // can create a more specific request + out := make([]string, len(nodes)+1) + out[0] = "ID|DC|Name|Class|Drain|Status" + for i, node := range nodes { + out[i+1] = fmt.Sprintf("%s|%s|%s|%s|%v|%s", + node.ID, + node.Datacenter, + node.Name, + node.NodeClass, + node.Drain, + node.Status) + } + // Dump the output + c.Ui.Output(fmt.Sprintf("Please disambiguate the desired node\n\n%s", formatList(out))) + return 0 + } + // Prefix lookup matched a single node + node, _, err = client.Nodes().Info(nodes[0].ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying node info: %s", err)) + return 1 + } } m := node.Attributes @@ -132,7 +165,7 @@ func (c *NodeStatusCommand) Run(args []string) int { var allocs []string if !short { // Query the node allocations - nodeAllocs, _, err := client.Nodes().Allocations(nodeID, nil) + nodeAllocs, _, err := client.Nodes().Allocations(node.ID, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error querying node allocations: %s", err)) return 1 diff --git a/command/node_status_test.go b/command/node_status_test.go index 567a91f55639..fe0590177f3d 100644 --- a/command/node_status_test.go +++ b/command/node_status_test.go @@ -74,6 +74,19 @@ func TestNodeStatusCommand_Run(t *testing.T) { if strings.Contains(out, "Allocations") { t.Fatalf("should not dump allocations") } + + // Query a single node based on prefix + if code := cmd.Run([]string{"-address=" + url, nodeID[:4]}); 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() } func TestNodeStatusCommand_Fails(t *testing.T) { @@ -99,12 +112,13 @@ func TestNodeStatusCommand_Fails(t *testing.T) { if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying node status") { t.Fatalf("expected failed query error, got: %s", out) } + ui.ErrorWriter.Reset() // Fails on non-existent node if code := cmd.Run([]string{"-address=" + url, "nope"}); code != 1 { t.Fatalf("expected exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "not found") { + if out := ui.ErrorWriter.String(); !strings.Contains(out, "No node(s) with prefix") { t.Fatalf("expected not found error, got: %s", out) } } diff --git a/command/status.go b/command/status.go index de5128a64c60..cbfdefc6dcff 100644 --- a/command/status.go +++ b/command/status.go @@ -95,8 +95,34 @@ func (c *StatusCommand) Run(args []string) int { jobID := args[0] job, _, err := client.Jobs().Info(jobID, nil) if err != nil { - c.Ui.Error(fmt.Sprintf("Error querying job: %s", err)) - return 1 + jobs, _, err := client.Jobs().PrefixList(jobID) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying job: %s", err)) + return 1 + } + if len(jobs) == 0 { + c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID)) + return 1 + } + if len(jobs) > 1 { + out := make([]string, len(jobs)+1) + out[0] = "ID|Type|Priority|Status" + for i, job := range jobs { + out[i+1] = fmt.Sprintf("%s|%s|%d|%s", + job.ID, + job.Type, + job.Priority, + job.Status) + } + c.Ui.Output(fmt.Sprintf("Please disambiguate the desired job\n\n%s", formatList(out))) + return 0 + } + // Prefix lookup matched a single job + job, _, err = client.Jobs().Info(jobs[0].ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying job: %s", err)) + return 1 + } } // Check if it is periodic @@ -129,14 +155,14 @@ func (c *StatusCommand) Run(args []string) int { var evals, allocs []string // Query the evaluations - jobEvals, _, err := client.Jobs().Evaluations(jobID, nil) + jobEvals, _, err := client.Jobs().Evaluations(job.ID, 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) + jobAllocs, _, err := client.Jobs().Allocations(job.ID, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error querying job allocations: %s", err)) return 1 diff --git a/command/status_test.go b/command/status_test.go index e5561e0d8add..766bf305a47a 100644 --- a/command/status_test.go +++ b/command/status_test.go @@ -31,11 +31,11 @@ func TestStatusCommand_Run(t *testing.T) { } // Register two jobs - job1 := testJob("job1") + job1 := testJob("job1_sfx") if _, _, err := client.Jobs().Register(job1, nil); err != nil { t.Fatalf("err: %s", err) } - job2 := testJob("job2") + job2 := testJob("job2_sfx") if _, _, err := client.Jobs().Register(job2, nil); err != nil { t.Fatalf("err: %s", err) } @@ -45,18 +45,18 @@ func TestStatusCommand_Run(t *testing.T) { t.Fatalf("expected exit 0, got: %d", code) } out := ui.OutputWriter.String() - if !strings.Contains(out, "job1") || !strings.Contains(out, "job2") { - t.Fatalf("expected job1 and job2, got: %s", out) + if !strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") { + t.Fatalf("expected job1_sfx and job2_sfx, got: %s", out) } ui.OutputWriter.Reset() // Query a single job - if code := cmd.Run([]string{"-address=" + url, "job2"}); code != 0 { + if code := cmd.Run([]string{"-address=" + url, "job2_sfx"}); code != 0 { t.Fatalf("expected exit 0, got: %d", code) } out = ui.OutputWriter.String() - if strings.Contains(out, "job1") || !strings.Contains(out, "job2") { - t.Fatalf("expected only job2, got: %s", out) + if strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") { + t.Fatalf("expected only job2_sfx, got: %s", out) } if !strings.Contains(out, "Evaluations") { t.Fatalf("should dump evaluations") @@ -66,6 +66,26 @@ func TestStatusCommand_Run(t *testing.T) { } ui.OutputWriter.Reset() + // Query jobs with prefix match + if code := cmd.Run([]string{"-address=" + url, "job"}); code != 0 { + t.Fatalf("expected exit 0, got: %d", code) + } + out = ui.OutputWriter.String() + if !strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") { + t.Fatalf("expected job1_sfx and job2_sfx, got: %s", out) + } + ui.OutputWriter.Reset() + + // Query a single job with prefix match + if code := cmd.Run([]string{"-address=" + url, "job1"}); code != 0 { + t.Fatalf("expected exit 0, got: %d", code) + } + out = ui.OutputWriter.String() + if !strings.Contains(out, "job1_sfx") || strings.Contains(out, "job2_sfx") { + t.Fatalf("expected only job1_sfx, got: %s", out) + } + 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) diff --git a/command/stop.go b/command/stop.go index c7705f9fa57d..a309dd26a01c 100644 --- a/command/stop.go +++ b/command/stop.go @@ -65,13 +65,40 @@ func (c *StopCommand) Run(args []string) int { } // Check if the job exists - if _, _, err := client.Jobs().Info(jobID, nil); err != nil { - c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err)) - return 1 + job, _, err := client.Jobs().Info(jobID, nil) + if err != nil { + jobs, _, err := client.Jobs().PrefixList(jobID) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err)) + return 1 + } + if len(jobs) == 0 { + c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID)) + return 1 + } + if len(jobs) > 1 { + out := make([]string, len(jobs)+1) + out[0] = "ID|Type|Priority|Status" + for i, job := range jobs { + out[i+1] = fmt.Sprintf("%s|%s|%d|%s", + job.ID, + job.Type, + job.Priority, + job.Status) + } + c.Ui.Output(fmt.Sprintf("Please disambiguate the desired job\n\n%s", formatList(out))) + return 0 + } + // Prefix lookup matched a single job + job, _, err = client.Jobs().Info(jobs[0].ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err)) + return 1 + } } // Invoke the stop - evalID, _, err := client.Jobs().Deregister(jobID, nil) + evalID, _, err := client.Jobs().Deregister(job.ID, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err)) return 1 diff --git a/command/stop_test.go b/command/stop_test.go index 2195cbf8a407..9abe0548fc91 100644 --- a/command/stop_test.go +++ b/command/stop_test.go @@ -31,7 +31,7 @@ func TestStopCommand_Fails(t *testing.T) { if code := cmd.Run([]string{"-address=" + url, "nope"}); code != 1 { t.Fatalf("expect exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "not found") { + if out := ui.ErrorWriter.String(); !strings.Contains(out, "No job(s) with prefix or id") { t.Fatalf("expect not found error, got: %s", out) } ui.ErrorWriter.Reset() diff --git a/nomad/alloc_endpoint.go b/nomad/alloc_endpoint.go index c07d5549d739..ca65621b18b2 100644 --- a/nomad/alloc_endpoint.go +++ b/nomad/alloc_endpoint.go @@ -4,6 +4,7 @@ import ( "time" "github.com/armon/go-metrics" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/watch" ) @@ -31,7 +32,12 @@ func (a *Alloc) List(args *structs.AllocListRequest, reply *structs.AllocListRes if err != nil { return err } - iter, err := snap.Allocs() + var iter memdb.ResultIterator + if prefix := args.QueryOptions.Prefix; prefix != "" { + iter, err = snap.AllocsByIDPrefix(prefix) + } else { + iter, err = snap.Allocs() + } if err != nil { return err } diff --git a/nomad/alloc_endpoint_test.go b/nomad/alloc_endpoint_test.go index bcab0a3876bd..2f82ad6de356 100644 --- a/nomad/alloc_endpoint_test.go +++ b/nomad/alloc_endpoint_test.go @@ -25,7 +25,7 @@ func TestAllocEndpoint_List(t *testing.T) { t.Fatalf("err: %v", err) } - // Lookup the jobs + // Lookup the allocations get := &structs.AllocListRequest{ QueryOptions: structs.QueryOptions{Region: "global"}, } @@ -43,6 +43,26 @@ func TestAllocEndpoint_List(t *testing.T) { if resp.Allocations[0].ID != alloc.ID { t.Fatalf("bad: %#v", resp.Allocations[0]) } + + // Lookup the allocations by prefix + get = &structs.AllocListRequest{ + QueryOptions: structs.QueryOptions{Region: "global", Prefix: alloc.ID[:4]}, + } + + var resp2 structs.AllocListResponse + if err := msgpackrpc.CallWithCodec(codec, "Alloc.List", get, &resp2); err != nil { + t.Fatalf("err: %v", err) + } + if resp2.Index != 1000 { + t.Fatalf("Bad index: %d %d", resp2.Index, 1000) + } + + if len(resp2.Allocations) != 1 { + t.Fatalf("bad: %#v", resp2.Allocations) + } + if resp2.Allocations[0].ID != alloc.ID { + t.Fatalf("bad: %#v", resp2.Allocations[0]) + } } func TestAllocEndpoint_List_Blocking(t *testing.T) { diff --git a/nomad/eval_endpoint.go b/nomad/eval_endpoint.go index bc74e85f362c..71211169779e 100644 --- a/nomad/eval_endpoint.go +++ b/nomad/eval_endpoint.go @@ -5,6 +5,7 @@ import ( "time" "github.com/armon/go-metrics" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/watch" ) @@ -239,7 +240,12 @@ func (e *Eval) List(args *structs.EvalListRequest, if err != nil { return err } - iter, err := snap.Evals() + var iter memdb.ResultIterator + if prefix := args.QueryOptions.Prefix; prefix != "" { + iter, err = snap.EvalsByIDPrefix(prefix) + } else { + iter, err = snap.Evals() + } if err != nil { return err } diff --git a/nomad/eval_endpoint_test.go b/nomad/eval_endpoint_test.go index 55782a031da3..3d59f33ed10a 100644 --- a/nomad/eval_endpoint_test.go +++ b/nomad/eval_endpoint_test.go @@ -391,7 +391,9 @@ func TestEvalEndpoint_List(t *testing.T) { // Create the register request eval1 := mock.Eval() + eval1.ID = "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9" eval2 := mock.Eval() + eval2.ID = "aaaabbbb-3350-4b4b-d185-0e1992ed43e9" s1.fsm.State().UpsertEvals(1000, []*structs.Evaluation{eval1, eval2}) // Lookup the eval @@ -409,6 +411,23 @@ func TestEvalEndpoint_List(t *testing.T) { if len(resp.Evaluations) != 2 { t.Fatalf("bad: %#v", resp.Evaluations) } + + // Lookup the eval by prefix + get = &structs.EvalListRequest{ + QueryOptions: structs.QueryOptions{Region: "global", Prefix: "aaaabb"}, + } + var resp2 structs.EvalListResponse + if err := msgpackrpc.CallWithCodec(codec, "Eval.List", get, &resp2); err != nil { + t.Fatalf("err: %v", err) + } + if resp2.Index != 1000 { + t.Fatalf("Bad index: %d %d", resp2.Index, 1000) + } + + if len(resp2.Evaluations) != 1 { + t.Fatalf("bad: %#v", resp2.Evaluations) + } + } func TestEvalEndpoint_List_Blocking(t *testing.T) { diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index e17cd78fb7ab..eb6205e06e02 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -6,6 +6,7 @@ import ( "time" "github.com/armon/go-metrics" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/watch" ) @@ -293,7 +294,12 @@ func (j *Job) List(args *structs.JobListRequest, if err != nil { return err } - iter, err := snap.Jobs() + var iter memdb.ResultIterator + if prefix := args.QueryOptions.Prefix; prefix != "" { + iter, err = snap.JobsByIDPrefix(prefix) + } else { + iter, err = snap.Jobs() + } if err != nil { return err } diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index b9bb9a80ec86..236028c4b9af 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -683,6 +683,25 @@ func TestJobEndpoint_ListJobs(t *testing.T) { if resp2.Jobs[0].ID != job.ID { t.Fatalf("bad: %#v", resp2.Jobs[0]) } + + // Lookup the jobs by prefix + get = &structs.JobListRequest{ + QueryOptions: structs.QueryOptions{Region: "global", Prefix: resp2.Jobs[0].ID[:4]}, + } + var resp3 structs.JobListResponse + if err := msgpackrpc.CallWithCodec(codec, "Job.List", get, &resp3); err != nil { + t.Fatalf("err: %v", err) + } + if resp3.Index != 1000 { + t.Fatalf("Bad index: %d %d", resp3.Index, 1000) + } + + if len(resp3.Jobs) != 1 { + t.Fatalf("bad: %#v", resp3.Jobs) + } + if resp3.Jobs[0].ID != job.ID { + t.Fatalf("bad: %#v", resp3.Jobs[0]) + } } func TestJobEndpoint_ListJobs_Blocking(t *testing.T) { diff --git a/nomad/node_endpoint.go b/nomad/node_endpoint.go index 5bd600380f1a..f004110b4d89 100644 --- a/nomad/node_endpoint.go +++ b/nomad/node_endpoint.go @@ -5,6 +5,7 @@ import ( "time" "github.com/armon/go-metrics" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/watch" ) @@ -424,7 +425,12 @@ func (n *Node) List(args *structs.NodeListRequest, if err != nil { return err } - iter, err := snap.Nodes() + var iter memdb.ResultIterator + if prefix := args.QueryOptions.Prefix; prefix != "" { + iter, err = snap.NodesByIDPrefix(prefix) + } else { + iter, err = snap.Nodes() + } if err != nil { return err } diff --git a/nomad/node_endpoint_test.go b/nomad/node_endpoint_test.go index 74b154655e7a..b7464c7e9aef 100644 --- a/nomad/node_endpoint_test.go +++ b/nomad/node_endpoint_test.go @@ -879,6 +879,25 @@ func TestClientEndpoint_ListNodes(t *testing.T) { if resp2.Nodes[0].ID != node.ID { t.Fatalf("bad: %#v", resp2.Nodes[0]) } + + // Lookup the node with prefix + get = &structs.NodeListRequest{ + QueryOptions: structs.QueryOptions{Region: "global", Prefix: node.ID[:4]}, + } + var resp3 structs.NodeListResponse + if err := msgpackrpc.CallWithCodec(codec, "Node.List", get, &resp3); err != nil { + t.Fatalf("err: %v", err) + } + if resp3.Index != resp.Index { + t.Fatalf("Bad index: %d %d", resp3.Index, resp2.Index) + } + + if len(resp3.Nodes) != 1 { + t.Fatalf("bad: %#v", resp3.Nodes) + } + if resp3.Nodes[0].ID != node.ID { + t.Fatalf("bad: %#v", resp3.Nodes[0]) + } } func TestClientEndpoint_ListNodes_Blocking(t *testing.T) { diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index fb31a2971d02..651e152ddf20 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -252,6 +252,18 @@ func (s *StateStore) NodeByID(nodeID string) (*structs.Node, error) { return nil, nil } +// NodesByIDPrefix is used to lookup nodes by prefix +func (s *StateStore) NodesByIDPrefix(nodeID string) (memdb.ResultIterator, error) { + txn := s.db.Txn(false) + + iter, err := txn.Get("nodes", "id_prefix", nodeID) + if err != nil { + return nil, fmt.Errorf("node lookup failed: %v", err) + } + + return iter, nil +} + // Nodes returns an iterator over all the nodes func (s *StateStore) Nodes() (memdb.ResultIterator, error) { txn := s.db.Txn(false) @@ -347,6 +359,18 @@ func (s *StateStore) JobByID(id string) (*structs.Job, error) { return nil, nil } +// JobsByIDPrefix is used to lookup a job by prefix +func (s *StateStore) JobsByIDPrefix(id string) (memdb.ResultIterator, error) { + txn := s.db.Txn(false) + + iter, err := txn.Get("jobs", "id_prefix", id) + if err != nil { + return nil, fmt.Errorf("job lookup failed: %v", err) + } + + return iter, nil +} + // Jobs returns an iterator over all the jobs func (s *StateStore) Jobs() (memdb.ResultIterator, error) { txn := s.db.Txn(false) @@ -607,6 +631,18 @@ func (s *StateStore) EvalByID(id string) (*structs.Evaluation, error) { return nil, nil } +// EvalsByIDPrefix is used to lookup evaluations by prefix +func (s *StateStore) EvalsByIDPrefix(id string) (memdb.ResultIterator, error) { + txn := s.db.Txn(false) + + iter, err := txn.Get("evals", "id_prefix", id) + if err != nil { + return nil, fmt.Errorf("eval lookup failed: %v", err) + } + + return iter, nil +} + // EvalsByJob returns all the evaluations by job id func (s *StateStore) EvalsByJob(jobID string) ([]*structs.Evaluation, error) { txn := s.db.Txn(false) @@ -756,6 +792,18 @@ func (s *StateStore) AllocByID(id string) (*structs.Allocation, error) { return nil, nil } +// AllocsByIDPrefix is used to lookup allocs by prefix +func (s *StateStore) AllocsByIDPrefix(id string) (memdb.ResultIterator, error) { + txn := s.db.Txn(false) + + iter, err := txn.Get("allocs", "id_prefix", id) + if err != nil { + return nil, fmt.Errorf("alloc lookup failed: %v", err) + } + + return iter, nil +} + // AllocsByNode returns all the allocations by node func (s *StateStore) AllocsByNode(node string) ([]*structs.Allocation, error) { txn := s.db.Txn(false) diff --git a/nomad/state/state_store_test.go b/nomad/state/state_store_test.go index ee3a50a53d51..5f10d7173ee0 100644 --- a/nomad/state/state_store_test.go +++ b/nomad/state/state_store_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/watch" @@ -216,6 +217,77 @@ func TestStateStore_Nodes(t *testing.T) { } } +func TestStateStore_NodesByIDPrefix(t *testing.T) { + state := testStateStore(t) + node := mock.Node() + + node.ID = "11111111-662e-d0ab-d1c9-3e434af7bdb4" + err := state.UpsertNode(1000, node) + if err != nil { + t.Fatalf("err: %v", err) + } + + iter, err := state.NodesByIDPrefix(node.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + + gatherNodes := func(iter memdb.ResultIterator) []*structs.Node { + var nodes []*structs.Node + for { + raw := iter.Next() + if raw == nil { + break + } + node := raw.(*structs.Node) + nodes = append(nodes, node) + } + return nodes + } + + nodes := gatherNodes(iter) + if len(nodes) != 1 { + t.Fatalf("err: %v", err) + } + + iter, err = state.NodesByIDPrefix("11") + if err != nil { + t.Fatalf("err: %v", err) + } + + nodes = gatherNodes(iter) + if len(nodes) != 1 { + t.Fatalf("err: %v", err) + } + + node = mock.Node() + node.ID = "11222222-662e-d0ab-d1c9-3e434af7bdb4" + err = state.UpsertNode(1001, node) + if err != nil { + t.Fatalf("err: %v", err) + } + + iter, err = state.NodesByIDPrefix("11") + if err != nil { + t.Fatalf("err: %v", err) + } + + nodes = gatherNodes(iter) + if len(nodes) != 2 { + t.Fatalf("err: %v", err) + } + + iter, err = state.NodesByIDPrefix("111") + if err != nil { + t.Fatalf("err: %v", err) + } + + nodes = gatherNodes(iter) + if len(nodes) != 1 { + t.Fatalf("err: %v", err) + } +} + func TestStateStore_RestoreNode(t *testing.T) { state := testStateStore(t) node := mock.Node() @@ -405,6 +477,76 @@ func TestStateStore_Jobs(t *testing.T) { } } +func TestStateStore_JobsByIDPrefix(t *testing.T) { + state := testStateStore(t) + job := mock.Job() + + job.ID = "redis" + err := state.UpsertJob(1000, job) + if err != nil { + t.Fatalf("err: %v", err) + } + + iter, err := state.JobsByIDPrefix(job.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + + gatherJobs := func(iter memdb.ResultIterator) []*structs.Job { + var jobs []*structs.Job + for { + raw := iter.Next() + if raw == nil { + break + } + jobs = append(jobs, raw.(*structs.Job)) + } + return jobs + } + + jobs := gatherJobs(iter) + if len(jobs) != 1 { + t.Fatalf("err: %v", err) + } + + iter, err = state.JobsByIDPrefix("re") + if err != nil { + t.Fatalf("err: %v", err) + } + + jobs = gatherJobs(iter) + if len(jobs) != 1 { + t.Fatalf("err: %v", err) + } + + job = mock.Job() + job.ID = "riak" + err = state.UpsertJob(1001, job) + if err != nil { + t.Fatalf("err: %v", err) + } + + iter, err = state.JobsByIDPrefix("r") + if err != nil { + t.Fatalf("err: %v", err) + } + + jobs = gatherJobs(iter) + if len(jobs) != 2 { + t.Fatalf("err: %v", err) + } + + iter, err = state.JobsByIDPrefix("ri") + if err != nil { + t.Fatalf("err: %v", err) + } + + jobs = gatherJobs(iter) + if len(jobs) != 1 { + t.Fatalf("err: %v", err) + } +} + func TestStateStore_JobsByPeriodic(t *testing.T) { state := testStateStore(t) var periodic, nonPeriodic []*structs.Job @@ -444,9 +586,6 @@ func TestStateStore_JobsByPeriodic(t *testing.T) { } iter, err = state.JobsByPeriodic(false) - if err != nil { - t.Fatalf("err: %v", err) - } var outNonPeriodic []*structs.Job for { @@ -1142,6 +1281,74 @@ func TestStateStore_Evals(t *testing.T) { } } +func TestStateStore_EvalsByIDPrefix(t *testing.T) { + state := testStateStore(t) + var evals []*structs.Evaluation + + ids := []string{ + "aaaaaaaa-7bfb-395d-eb95-0685af2176b2", + "aaaaaaab-7bfb-395d-eb95-0685af2176b2", + "aaaaaabb-7bfb-395d-eb95-0685af2176b2", + "aaaaabbb-7bfb-395d-eb95-0685af2176b2", + "aaaabbbb-7bfb-395d-eb95-0685af2176b2", + "aaabbbbb-7bfb-395d-eb95-0685af2176b2", + "aabbbbbb-7bfb-395d-eb95-0685af2176b2", + "abbbbbbb-7bfb-395d-eb95-0685af2176b2", + "bbbbbbbb-7bfb-395d-eb95-0685af2176b2", + } + for i := 0; i < 9; i++ { + eval := mock.Eval() + eval.ID = ids[i] + evals = append(evals, eval) + } + + err := state.UpsertEvals(1000, evals) + if err != nil { + t.Fatalf("err: %v", err) + } + + iter, err := state.EvalsByIDPrefix("aaaa") + if err != nil { + t.Fatalf("err: %v", err) + } + + gatherEvals := func(iter memdb.ResultIterator) []*structs.Evaluation { + var evals []*structs.Evaluation + for { + raw := iter.Next() + if raw == nil { + break + } + evals = append(evals, raw.(*structs.Evaluation)) + } + return evals + } + + out := gatherEvals(iter) + if len(out) != 5 { + t.Fatalf("bad: expected five evaluations, got: %#v", out) + } + + sort.Sort(EvalIDSort(evals)) + + for index, eval := range out { + if ids[index] != eval.ID { + t.Fatalf("bad: got unexpected id: %s", eval.ID) + } + } + + iter, err = state.EvalsByIDPrefix("b-a7bfb") + if err != nil { + t.Fatalf("err: %v", err) + } + + out = gatherEvals(iter) + if len(out) != 0 { + t.Fatalf("bad: unexpected zero evaluations, got: %#v", out) + } + +} + func TestStateStore_RestoreEval(t *testing.T) { state := testStateStore(t) eval := mock.Eval() @@ -1402,6 +1609,73 @@ func TestStateStore_AllocsByJob(t *testing.T) { } } +func TestStateStore_AllocsByIDPrefix(t *testing.T) { + state := testStateStore(t) + var allocs []*structs.Allocation + + ids := []string{ + "aaaaaaaa-7bfb-395d-eb95-0685af2176b2", + "aaaaaaab-7bfb-395d-eb95-0685af2176b2", + "aaaaaabb-7bfb-395d-eb95-0685af2176b2", + "aaaaabbb-7bfb-395d-eb95-0685af2176b2", + "aaaabbbb-7bfb-395d-eb95-0685af2176b2", + "aaabbbbb-7bfb-395d-eb95-0685af2176b2", + "aabbbbbb-7bfb-395d-eb95-0685af2176b2", + "abbbbbbb-7bfb-395d-eb95-0685af2176b2", + "bbbbbbbb-7bfb-395d-eb95-0685af2176b2", + } + for i := 0; i < 9; i++ { + alloc := mock.Alloc() + alloc.ID = ids[i] + allocs = append(allocs, alloc) + } + + err := state.UpsertAllocs(1000, allocs) + if err != nil { + t.Fatalf("err: %v", err) + } + + iter, err := state.AllocsByIDPrefix("aaaa") + if err != nil { + t.Fatalf("err: %v", err) + } + + gatherAllocs := func(iter memdb.ResultIterator) []*structs.Allocation { + var allocs []*structs.Allocation + for { + raw := iter.Next() + if raw == nil { + break + } + allocs = append(allocs, raw.(*structs.Allocation)) + } + return allocs + } + + out := gatherAllocs(iter) + if len(out) != 5 { + t.Fatalf("bad: expected five allocations, got: %#v", out) + } + + sort.Sort(AllocIDSort(allocs)) + + for index, alloc := range out { + if ids[index] != alloc.ID { + t.Fatalf("bad: got unexpected id: %s", alloc.ID) + } + } + + iter, err = state.AllocsByIDPrefix("b-a7bfb") + if err != nil { + t.Fatalf("err: %v", err) + } + + out = gatherAllocs(iter) + if len(out) != 0 { + t.Fatalf("bad: unexpected zero allocations, got: %#v", out) + } +} + func TestStateStore_Allocs(t *testing.T) { state := testStateStore(t) var allocs []*structs.Allocation diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index df75d62781ca..1680f83255b9 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -71,6 +71,9 @@ type QueryOptions struct { // If set, any follower can service the request. Results // may be arbitrarily stale. AllowStale bool + + // If set, used as prefix for resource list searches + Prefix string } func (q QueryOptions) RequestRegion() string { diff --git a/website/source/docs/commands/alloc-status.html.md.erb b/website/source/docs/commands/alloc-status.html.md.erb index f84a217d0e20..b0dcc2ad114f 100644 --- a/website/source/docs/commands/alloc-status.html.md.erb +++ b/website/source/docs/commands/alloc-status.html.md.erb @@ -19,8 +19,9 @@ current state of its tasks. nomad alloc-status [options] ``` -An allocation ID must be provided. This specific allocation will be queried -and detailed information for it will be dumped. +An allocation ID or prefix must be provided. If there is an exact match, the +full details of the allocation will be displayed. Otherwise, a list of matching +allocations and information will be displayed. ## General Options diff --git a/website/source/docs/commands/eval-monitor.html.md.erb b/website/source/docs/commands/eval-monitor.html.md.erb index 7fa6a136531c..0159debe84b8 100644 --- a/website/source/docs/commands/eval-monitor.html.md.erb +++ b/website/source/docs/commands/eval-monitor.html.md.erb @@ -20,10 +20,12 @@ reaches a terminal state. nomad eval-monitor [options] ``` -The eval-monitor command requires a single argument, specifying the -evaluation ID to monitor. An interactive monitoring session will be -started in the terminal. It is safe to exit the monitor at any time -using ctrl+c. +An evaluation ID or prefix must be provided. If there is an exact match, the +the evaluation will be monitored. Otherwise, a list of matching evaluations and +information will be displayed. + +An interactive monitoring session will be started in the terminal. It is safe +to exit the monitor at any time using ctrl+c. The command will exit when the given evaluation reaches a terminal state (completed or failed). Exit code 0 is returned on successful diff --git a/website/source/docs/commands/node-drain.html.md.erb b/website/source/docs/commands/node-drain.html.md.erb index ac9624e0cce0..919989f4261b 100644 --- a/website/source/docs/commands/node-drain.html.md.erb +++ b/website/source/docs/commands/node-drain.html.md.erb @@ -21,9 +21,12 @@ nicely by providing the current drain status of a given node. nomad node-drain [options] ``` -This command expects exactly one argument to specify the node ID to enable or -disable drain mode for. It is also required to pass one of `-enable` or -`-disable`, depending on which operation is desired. +A node ID or prefix must be provided. If there is an exact match, the +drain mode will be adjusted for that node. Otherwise, a list of matching +nodes and information will be displayed. + +It is also required to pass one of `-enable` or `-disable`, depending on which +operation is desired. ## General Options diff --git a/website/source/docs/commands/node-status.html.md.erb b/website/source/docs/commands/node-status.html.md.erb index 5d514caebcbf..066f99ab1859 100644 --- a/website/source/docs/commands/node-status.html.md.erb +++ b/website/source/docs/commands/node-status.html.md.erb @@ -20,9 +20,11 @@ nomad node-status [options] [node] If no node ID is passed, then the command will enter "list mode" and dump a high-level list of all known nodes. This list output contains less information -but is a good way to get a bird's-eye view of things. If a node ID is specified, -then that particular node will be queried, and detailed information will be -displayed. +but is a good way to get a bird's-eye view of things. + +If there is an exact match based on the provided node ID or prefix, then that +particular node will be queried, and detailed information will be displayed. +Otherwise, a list of matching nodes and information will be displayed. ## General Options @@ -50,7 +52,7 @@ Single-node view in short mode: $ nomad node-status -short 1f3f03ea-a420-b64b-c73b-51290ed7f481 ID = 1f3f03ea-a420-b64b-c73b-51290ed7f481 Name = node2 -Class = +Class = Datacenter = dc1 Drain = false Status = ready @@ -62,7 +64,7 @@ Full output for a single node: $ nomad node-status 1f3f03ea-a420-b64b-c73b-51290ed7f481 ID = 1f3f03ea-a420-b64b-c73b-51290ed7f481 Name = node2 -Class = +Class = Datacenter = dc1 Drain = false Status = ready diff --git a/website/source/docs/commands/status.html.md.erb b/website/source/docs/commands/status.html.md.erb index 7a5995e8e1a5..e1b10f8f5f00 100644 --- a/website/source/docs/commands/status.html.md.erb +++ b/website/source/docs/commands/status.html.md.erb @@ -16,10 +16,13 @@ The `status` command displays status information for jobs. nomad status [options] [job] ``` -This command accepts an optional job ID as the sole argument. If the job ID is -provided, information about the specific job is queried and displayed. If the ID -is omitted, the command lists out all of the existing jobs and a few of the most -useful status fields for each. +This command accepts an optional job ID or prefix as the sole argument. If there +is an exact match based on the provided job ID or prefix, then information about +the specific job is queried and displayed. Otherwise, a list of matching jobs and +information will be displayed. + +If the ID is omitted, the command lists out all of the existing jobs and a few of +the most useful status fields for each. ## General Options diff --git a/website/source/docs/commands/stop.html.md.erb b/website/source/docs/commands/stop.html.md.erb index b9794282978d..08fcec3eecc0 100644 --- a/website/source/docs/commands/stop.html.md.erb +++ b/website/source/docs/commands/stop.html.md.erb @@ -17,8 +17,10 @@ to cancel all of the running allocations. nomad stop [options] ``` -The stop command requires a single argument, specifying the job ID to -cancel. +The stop command requires a single argument, specifying the job ID or prefix to +cancel. If there is an exact match based on the provided job ID or prefix, then +the job will be cancelled. Otherwise, a list of matching jobs and information +will be displayed. Upon successful deregistration, an interactive monitor session will start to display log lines as the job unwinds its allocations and completes shutting diff --git a/website/source/docs/http/allocs.html.md b/website/source/docs/http/allocs.html.md index 7cb38ab66369..dea7b63f0a46 100644 --- a/website/source/docs/http/allocs.html.md +++ b/website/source/docs/http/allocs.html.md @@ -28,7 +28,14 @@ be specified using the `?region=` query parameter.
Parameters
- None +
    +
  • + prefix + optional + even-length + Filter allocations based on an identifier prefix. +
  • +
Blocking Queries
diff --git a/website/source/docs/http/evals.html.md b/website/source/docs/http/evals.html.md index 23d98cc95160..b843d307a7d7 100644 --- a/website/source/docs/http/evals.html.md +++ b/website/source/docs/http/evals.html.md @@ -28,7 +28,14 @@ be specified using the `?region=` query parameter.
Parameters
- None +
    +
  • + prefix + optional + even-length + Filter evaluations based on an identifier prefix. +
  • +
Blocking Queries
diff --git a/website/source/docs/http/jobs.html.md b/website/source/docs/http/jobs.html.md index 8f098b1ca85e..1642ed4af44a 100644 --- a/website/source/docs/http/jobs.html.md +++ b/website/source/docs/http/jobs.html.md @@ -28,7 +28,13 @@ another region can be specified using the `?region=` query parameter.
Parameters
- None +
    +
  • + prefix + optional + Filter jobs based on an identifier prefix. +
  • +
Blocking Queries
diff --git a/website/source/docs/http/nodes.html.md b/website/source/docs/http/nodes.html.md index b8e2b91a95f4..8c58ed870e05 100644 --- a/website/source/docs/http/nodes.html.md +++ b/website/source/docs/http/nodes.html.md @@ -28,7 +28,13 @@ be specified using the `?region=` query parameter.
Parameters
- None +
    +
  • + prefix + optional + Filter nodes based on an identifier prefix. +
  • +
Blocking Queries