Merge pull request #575 from nautsio/f-short-ids

Allow lookups based on short identifiers
This commit is contained in:
Alex Dadgar
2016-01-06 14:36:19 -08:00
48 changed files with 1230 additions and 64 deletions

View File

@@ -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

View File

@@ -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},

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -19,8 +19,9 @@ current state of its tasks.
nomad alloc-status [options] <allocation>
```
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

View File

@@ -20,10 +20,12 @@ reaches a terminal state.
nomad eval-monitor [options] <eval>
```
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

View File

@@ -21,9 +21,12 @@ nicely by providing the current drain status of a given node.
nomad node-drain [options] <node>
```
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

View File

@@ -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

View File

@@ -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

View File

@@ -17,8 +17,10 @@ to cancel all of the running allocations.
nomad stop [options] <job>
```
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

View File

@@ -28,7 +28,14 @@ be specified using the `?region=` query parameter.
<dt>Parameters</dt>
<dd>
None
<ul>
<li>
<span class="param">prefix</span>
<span class="param-flags">optional</span>
<span class="param-flags">even-length</span>
Filter allocations based on an identifier prefix.
</li>
</ul>
</dd>
<dt>Blocking Queries</dt>

View File

@@ -28,7 +28,14 @@ be specified using the `?region=` query parameter.
<dt>Parameters</dt>
<dd>
None
<ul>
<li>
<span class="param">prefix</span>
<span class="param-flags">optional</span>
<span class="param-flags">even-length</span>
Filter evaluations based on an identifier prefix.
</li>
</ul>
</dd>
<dt>Blocking Queries</dt>

View File

@@ -28,7 +28,13 @@ another region can be specified using the `?region=` query parameter.
<dt>Parameters</dt>
<dd>
None
<ul>
<li>
<span class="param">prefix</span>
<span class="param-flags">optional</span>
Filter jobs based on an identifier prefix.
</li>
</ul>
</dd>
<dt>Blocking Queries</dt>

View File

@@ -28,7 +28,13 @@ be specified using the `?region=` query parameter.
<dt>Parameters</dt>
<dd>
None
<ul>
<li>
<span class="param">prefix</span>
<span class="param-flags">optional</span>
Filter nodes based on an identifier prefix.
</li>
</ul>
</dd>
<dt>Blocking Queries</dt>