From 7ae975e1a2b81547f80d343bd09114ae1e206f71 Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Fri, 11 Dec 2015 14:18:44 +0100 Subject: [PATCH 01/12] Allow lookups based on short identifiers This change introduces the ability to specify identifiers for jobs, allocs, evals and nodes on the command line with as little as one character, provided that it uniquely identifies the resource. An error with the possible results will be provided when the short identifier has multiple results. --- command/node_status.go | 2 +- nomad/state/state_store.go | 68 +++++++++++++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/command/node_status.go b/command/node_status.go index b3cba519cbea..1c5973f6a5c6 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -132,7 +132,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/nomad/state/state_store.go b/nomad/state/state_store.go index 30ee87259821..6dd9d9c2dc32 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -241,13 +241,26 @@ func (s *StateStore) UpdateNodeDrain(index uint64, nodeID string, drain bool) er func (s *StateStore) NodeByID(nodeID string) (*structs.Node, error) { txn := s.db.Txn(false) - existing, err := txn.First("nodes", "id", nodeID) + existing, err := txn.Find("nodes", "id", nodeID) if err != nil { return nil, fmt.Errorf("node lookup failed: %v", err) } if existing != nil { - return existing.(*structs.Node), nil + // Return exact match directly + if len(existing) == 1 { + return existing[0].(*structs.Node), nil + } + + // The results were ambiguous for the given node identifier. Return + // an error with possible options so that the user can try again with + // a more specific identifier. + var nodes []string + for _, result := range existing { + node := result.(*structs.Node) + nodes = append(nodes, node.ID) + } + return nil, fmt.Errorf("Ambiguous identifier: %v", nodes) } return nil, nil } @@ -336,13 +349,26 @@ func (s *StateStore) DeleteJob(index uint64, jobID string) error { func (s *StateStore) JobByID(id string) (*structs.Job, error) { txn := s.db.Txn(false) - existing, err := txn.First("jobs", "id", id) + existing, err := txn.Find("jobs", "id", id) if err != nil { return nil, fmt.Errorf("job lookup failed: %v", err) } if existing != nil { - return existing.(*structs.Job), nil + // Return exact match directly + if len(existing) == 1 { + return existing[0].(*structs.Job), nil + } + + // The results were ambiguous for the given job identifier. Return + // an error with possible options so that the user can try again with + // a more specific identifier. + var jobs []string + for _, result := range existing { + job := result.(*structs.Job) + jobs = append(jobs, job.ID) + } + return nil, fmt.Errorf("Ambiguous identifier: %v", jobs) } return nil, nil } @@ -477,13 +503,26 @@ func (s *StateStore) DeleteEval(index uint64, evals []string, allocs []string) e func (s *StateStore) EvalByID(id string) (*structs.Evaluation, error) { txn := s.db.Txn(false) - existing, err := txn.First("evals", "id", id) + existing, err := txn.Find("evals", "id", id) if err != nil { return nil, fmt.Errorf("eval lookup failed: %v", err) } if existing != nil { - return existing.(*structs.Evaluation), nil + // Return exact match directly + if len(existing) == 1 { + return existing[0].(*structs.Evaluation), nil + } + + // The results were ambiguous for the given eval identifier. Return + // an error with possible options so that the user can try again with + // a more specific identifier. + var evals []string + for _, result := range existing { + eval := result.(*structs.Evaluation) + evals = append(evals, eval.ID) + } + return nil, fmt.Errorf("Ambiguous identifier: %v", evals) } return nil, nil } @@ -625,13 +664,26 @@ func (s *StateStore) UpsertAllocs(index uint64, allocs []*structs.Allocation) er func (s *StateStore) AllocByID(id string) (*structs.Allocation, error) { txn := s.db.Txn(false) - existing, err := txn.First("allocs", "id", id) + existing, err := txn.Find("allocs", "id", id) if err != nil { return nil, fmt.Errorf("alloc lookup failed: %v", err) } if existing != nil { - return existing.(*structs.Allocation), nil + // Return exact match directly + if len(existing) == 1 { + return existing[0].(*structs.Allocation), nil + } + + // The results were ambiguous for the given job identifier. Return + // an error with possible options so that the user can try again with + // a more specific identifier. + var allocs []string + for _, result := range existing { + alloc := result.(*structs.Allocation) + allocs = append(allocs, alloc.ID) + } + return nil, fmt.Errorf("Ambiguous identifier: %v", allocs) } return nil, nil } From 91075e130a8666493daad7a50f7cd523153d1f55 Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Sat, 19 Dec 2015 21:05:17 +0100 Subject: [PATCH 02/12] Short identifiers functionality * Use go-memdb prefix indexer for lookups * Add Job lookups * Update state store with new ByIDPrefix get methods * Call new methods when exact lookup fails or is not applicable --- command/monitor.go | 2 +- command/status.go | 4 +- nomad/alloc_endpoint.go | 38 ++++- nomad/eval_endpoint.go | 37 ++++- nomad/job_endpoint.go | 28 ++++ nomad/node_endpoint.go | 23 +++ nomad/state/state_store.go | 116 +++++++------ nomad/state/state_store_test.go | 277 ++++++++++++++++++++++++++++++++ 8 files changed, 456 insertions(+), 69 deletions(-) diff --git a/command/monitor.go b/command/monitor.go index be9b816dc246..a6615c5a18b5 100644 --- a/command/monitor.go +++ b/command/monitor.go @@ -196,7 +196,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/status.go b/command/status.go index 4a736dc7ac92..ed7301639dbd 100644 --- a/command/status.go +++ b/command/status.go @@ -106,14 +106,14 @@ func (c *StatusCommand) Run(args []string) int { var evals, allocs []string if !short { // 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/nomad/alloc_endpoint.go b/nomad/alloc_endpoint.go index c07d5549d739..da059f451f50 100644 --- a/nomad/alloc_endpoint.go +++ b/nomad/alloc_endpoint.go @@ -1,6 +1,7 @@ package nomad import ( + "fmt" "time" "github.com/armon/go-metrics" @@ -80,9 +81,40 @@ func (a *Alloc) GetAlloc(args *structs.AllocSpecificRequest, if err != nil { return err } - out, err := snap.AllocByID(args.AllocID) - if err != nil { - return err + + var out *structs.Allocation + + // Exact lookup if the identifier length is 36 (full UUID) + if len(args.AllocID) == 36 { + out, err = snap.AllocByID(args.AllocID) + if err != nil { + return err + } + } else { + iter, err := snap.AllocByIDPrefix(args.AllocID) + if err != nil { + return err + } + + // Gather all matching nodes + var allocs []*structs.Allocation + var allocIds []string + for { + raw := iter.Next() + if raw == nil { + break + } + alloc := raw.(*structs.Allocation) + allocIds = append(allocIds, alloc.ID) + allocs = append(allocs, alloc) + } + + if len(allocs) == 1 { + // Return unique allocation + out = allocs[0] + } else if len(allocs) > 1 { + return fmt.Errorf("Ambiguous identifier: %+v", allocIds) + } } // Setup the output diff --git a/nomad/eval_endpoint.go b/nomad/eval_endpoint.go index bc74e85f362c..c9f68c0c67db 100644 --- a/nomad/eval_endpoint.go +++ b/nomad/eval_endpoint.go @@ -38,9 +38,40 @@ func (e *Eval) GetEval(args *structs.EvalSpecificRequest, if err != nil { return err } - out, err := snap.EvalByID(args.EvalID) - if err != nil { - return err + + var out *structs.Evaluation + + // Exact lookup if the identifier length is 36 (full UUID) + if len(args.EvalID) == 36 { + out, err = snap.EvalByID(args.EvalID) + if err != nil { + return err + } + } else { + iter, err := snap.EvalByIDPrefix(args.EvalID) + if err != nil { + return err + } + + // Gather all matching evaluations + var evals []*structs.Evaluation + var evalIds []string + for { + raw := iter.Next() + if raw == nil { + break + } + eval := raw.(*structs.Evaluation) + evalIds = append(evalIds, eval.ID) + evals = append(evals, eval) + } + + if len(evals) == 1 { + // Return unique evaluation + out = evals[0] + } else if len(evals) > 1 { + return fmt.Errorf("Ambiguous identifier: %+v", evalIds) + } } // Setup the output diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index 18da75268ab6..f7a17fb222ab 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -218,6 +218,34 @@ func (j *Job) GetJob(args *structs.JobSpecificRequest, return err } + // Exact lookup failed so try a prefix based lookup + if out == nil { + iter, err := snap.JobByIDPrefix(args.JobID) + if err != nil { + return err + } + + // Gather all matching jobs + var jobs []*structs.Job + var jobIds []string + for { + raw := iter.Next() + if raw == nil { + break + } + job := raw.(*structs.Job) + jobIds = append(jobIds, job.ID) + jobs = append(jobs, job) + } + + if len(jobs) == 1 { + // Return unique match + out = jobs[0] + } else if len(jobs) > 1 { + return fmt.Errorf("Ambiguous identifier: %+v", jobIds) + } + } + // Setup the output reply.Job = out if out != nil { diff --git a/nomad/node_endpoint.go b/nomad/node_endpoint.go index 5bd600380f1a..f65a9a0966e7 100644 --- a/nomad/node_endpoint.go +++ b/nomad/node_endpoint.go @@ -304,6 +304,29 @@ func (n *Node) GetNode(args *structs.NodeSpecificRequest, return err } + if out == nil { + iter, err := snap.NodeByIDPrefix(args.NodeID) + if err != nil { + return err + } + + var nodes []*structs.Node + for { + raw := iter.Next() + if raw == nil { + break + } + node := raw.(*structs.Node) + nodes = append(nodes, node) + } + + if len(nodes) == 1 { + out = nodes[0] + } else { + return fmt.Errorf("Ambiguous identifier: %v", nodes) + } + } + // Setup the output reply.Node = out if out != nil { diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index c7e962c39f98..d155003466bc 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -241,30 +241,29 @@ func (s *StateStore) UpdateNodeDrain(index uint64, nodeID string, drain bool) er func (s *StateStore) NodeByID(nodeID string) (*structs.Node, error) { txn := s.db.Txn(false) - existing, err := txn.Find("nodes", "id", nodeID) + existing, err := txn.First("nodes", "id", nodeID) if err != nil { return nil, fmt.Errorf("node lookup failed: %v", err) } if existing != nil { - // Return exact match directly - if len(existing) == 1 { - return existing[0].(*structs.Node), nil - } - - // The results were ambiguous for the given node identifier. Return - // an error with possible options so that the user can try again with - // a more specific identifier. - var nodes []string - for _, result := range existing { - node := result.(*structs.Node) - nodes = append(nodes, node.ID) - } - return nil, fmt.Errorf("Ambiguous identifier: %v", nodes) + return existing.(*structs.Node), nil } return nil, nil } +// NodeByIDPrefix is used to lookup a node by partial ID +func (s *StateStore) NodeByIDPrefix(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) @@ -349,30 +348,29 @@ func (s *StateStore) DeleteJob(index uint64, jobID string) error { func (s *StateStore) JobByID(id string) (*structs.Job, error) { txn := s.db.Txn(false) - existing, err := txn.Find("jobs", "id", id) + existing, err := txn.First("jobs", "id", id) if err != nil { return nil, fmt.Errorf("job lookup failed: %v", err) } if existing != nil { - // Return exact match directly - if len(existing) == 1 { - return existing[0].(*structs.Job), nil - } - - // The results were ambiguous for the given job identifier. Return - // an error with possible options so that the user can try again with - // a more specific identifier. - var jobs []string - for _, result := range existing { - job := result.(*structs.Job) - jobs = append(jobs, job.ID) - } - return nil, fmt.Errorf("Ambiguous identifier: %v", jobs) + return existing.(*structs.Job), nil } return nil, nil } +// JobByIDPrefix is used to lookup a job by partial ID +func (s *StateStore) JobByIDPrefix(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) @@ -515,30 +513,29 @@ func (s *StateStore) DeleteEval(index uint64, evals []string, allocs []string) e func (s *StateStore) EvalByID(id string) (*structs.Evaluation, error) { txn := s.db.Txn(false) - existing, err := txn.Find("evals", "id", id) + existing, err := txn.First("evals", "id", id) if err != nil { return nil, fmt.Errorf("eval lookup failed: %v", err) } if existing != nil { - // Return exact match directly - if len(existing) == 1 { - return existing[0].(*structs.Evaluation), nil - } - - // The results were ambiguous for the given eval identifier. Return - // an error with possible options so that the user can try again with - // a more specific identifier. - var evals []string - for _, result := range existing { - eval := result.(*structs.Evaluation) - evals = append(evals, eval.ID) - } - return nil, fmt.Errorf("Ambiguous identifier: %v", evals) + return existing.(*structs.Evaluation), nil } return nil, nil } +// EvalByIDPrefix is used to lookup an eval by partial ID +func (s *StateStore) EvalByIDPrefix(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) @@ -677,30 +674,29 @@ func (s *StateStore) UpsertAllocs(index uint64, allocs []*structs.Allocation) er func (s *StateStore) AllocByID(id string) (*structs.Allocation, error) { txn := s.db.Txn(false) - existing, err := txn.Find("allocs", "id", id) + existing, err := txn.First("allocs", "id", id) if err != nil { return nil, fmt.Errorf("alloc lookup failed: %v", err) } if existing != nil { - // Return exact match directly - if len(existing) == 1 { - return existing[0].(*structs.Allocation), nil - } - - // The results were ambiguous for the given job identifier. Return - // an error with possible options so that the user can try again with - // a more specific identifier. - var allocs []string - for _, result := range existing { - alloc := result.(*structs.Allocation) - allocs = append(allocs, alloc.ID) - } - return nil, fmt.Errorf("Ambiguous identifier: %v", allocs) + return existing.(*structs.Allocation), nil } return nil, nil } +// AllocByIDPrefix is used to lookup an alloc by partial ID +func (s *StateStore) AllocByIDPrefix(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 0609f30480ac..2cd17cce42b1 100644 --- a/nomad/state/state_store_test.go +++ b/nomad/state/state_store_test.go @@ -6,6 +6,7 @@ import ( "sort" "testing" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/watch" @@ -215,6 +216,77 @@ func TestStateStore_Nodes(t *testing.T) { } } +func TestStateStore_NodeByIDPrefix(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.NodeByIDPrefix(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.NodeByIDPrefix("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.NodeByIDPrefix("11") + if err != nil { + t.Fatalf("err: %v", err) + } + + nodes = gatherNodes(iter) + if len(nodes) != 2 { + t.Fatalf("err: %v", err) + } + + iter, err = state.NodeByIDPrefix("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() @@ -404,6 +476,76 @@ func TestStateStore_Jobs(t *testing.T) { } } +func TestStateStore_JobByIDPrefix(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.JobByIDPrefix(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.JobByIDPrefix("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.JobByIDPrefix("r") + if err != nil { + t.Fatalf("err: %v", err) + } + + jobs = gatherJobs(iter) + if len(jobs) != 2 { + t.Fatalf("err: %v", err) + } + + iter, err = state.JobByIDPrefix("ri") + if err != nil { + t.Fatalf("err: %v", err) + } + + jobs = gatherJobs(iter) + if len(jobs) != 1 { + t.Fatalf("err: %v", err) + } +} + func TestStateStore_JobsByScheduler(t *testing.T) { state := testStateStore(t) var serviceJobs []*structs.Job @@ -859,6 +1001,74 @@ func TestStateStore_Evals(t *testing.T) { } } +func TestStateStore_EvalByIDPrefix(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.EvalByIDPrefix("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.EvalByIDPrefix("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() @@ -1119,6 +1329,73 @@ func TestStateStore_AllocsByJob(t *testing.T) { } } +func TestStateStore_AllocByIDPrefix(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.AllocByIDPrefix("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.AllocByIDPrefix("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 From bdf4347bc867975715885c9ab1541be2f7f75147 Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Sun, 20 Dec 2015 12:14:59 +0100 Subject: [PATCH 03/12] Allow short job identifiers for stop command --- command/stop.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/command/stop.go b/command/stop.go index c7705f9fa57d..2071165fb830 100644 --- a/command/stop.go +++ b/command/stop.go @@ -65,13 +65,14 @@ func (c *StopCommand) Run(args []string) int { } // Check if the job exists - if _, _, err := client.Jobs().Info(jobID, nil); err != nil { + job, _, err := client.Jobs().Info(jobID, 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 From 5d3bd1b6f04bb3c0367798d9bb21729f2cf82247 Mon Sep 17 00:00:00 2001 From: Armin Date: Sun, 20 Dec 2015 17:33:27 +0100 Subject: [PATCH 04/12] On cli node status list print the short Node ID when possible --- command/node_status.go | 30 ++++++++++++++++++++++++- command/node_status_test.go | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/command/node_status.go b/command/node_status.go index 1c5973f6a5c6..6fa4bc8e272f 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -4,6 +4,8 @@ import ( "fmt" "sort" "strings" + + "github.com/hashicorp/nomad/api" ) type NodeStatusCommand struct { @@ -78,12 +80,14 @@ func (c *NodeStatusCommand) Run(args []string) int { return 0 } + shortenNodeId := shouldShortenNodeIds(nodes) + // Format the nodes list 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, + shortenId(node.ID, shortenNodeId), node.Datacenter, node.Name, node.NodeClass, @@ -160,3 +164,27 @@ func (c *NodeStatusCommand) Run(args []string) int { } return 0 } + +// check if there is a collision if we shorten the Node ids +func shouldShortenNodeIds(nodes []*api.NodeListStub) bool { + ids := map[string]bool{} + + for _, node := range nodes { + if len(node.ID) != 36 { + return false //We have a custom ID, don't shorten anything + } else if ids[node.ID[:8]] == true { + return false //There is a collision + } else { + ids[node.ID[:8]] = true + } + } + return true +} + +// shorten an UUID syntax XXXXXXXX-XX... to 8 chars XXXXXXXX +func shortenId(id string, shouldShortenId bool) string { + if shouldShortenId == true { + return id[:8] + } + return id +} diff --git a/command/node_status_test.go b/command/node_status_test.go index 567a91f55639..f13ff905b5b5 100644 --- a/command/node_status_test.go +++ b/command/node_status_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" ) @@ -108,3 +109,47 @@ func TestNodeStatusCommand_Fails(t *testing.T) { t.Fatalf("expected not found error, got: %s", out) } } + +func Test_ShortenId(t *testing.T) { + id := "1234567890" + shortID := "12345678" + + dontShorten := shortenId(id, false) + if dontShorten != id { + t.Fatalf("Shorten ID should not short id on false, expected %s, got: %s", id, dontShorten) + } + + shorten := shortenId(id, true) + if shorten != shortID { + t.Fatalf("Shorten ID should short id on true, expected %s, got: %s", shortID, shorten) + } +} + +func Test_ShouldShortenNodeIds(t *testing.T) { + var list []*api.NodeListStub + nodeCustomId := &api.NodeListStub{ + ID: "my_own_id", + } + nodeOne := &api.NodeListStub{ + ID: "11111111-1111-1111-1111-111111111111", + } + nodeTwo := &api.NodeListStub{ + ID: "11111111-2222-2222-2222-222222222222", + } + + list = append(list, nodeCustomId) + if shouldShortenNodeIds(list) != false { + t.Fatalf("ShouldShortenNodeIds should return false when using custom id") + } + + list = nil + list = append(list, nodeOne) + if shouldShortenNodeIds(list) != true { + t.Fatalf("ShouldShortenNodeIds should return true when no collisions") + } + + list = append(list, nodeTwo) + if shouldShortenNodeIds(list) != false { + t.Fatalf("ShouldShortenNodeIds should return false when collision detected") + } +} From 23bfbbf66da030479af6d06d68b5c1c10a48d6bf Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Sun, 20 Dec 2015 18:02:10 +0100 Subject: [PATCH 05/12] Allow short identifiers for node-drain command --- command/node_drain.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/command/node_drain.go b/command/node_drain.go index efb0d13fe0b0..3d1bec199983 100644 --- a/command/node_drain.go +++ b/command/node_drain.go @@ -68,8 +68,15 @@ func (c *NodeDrainCommand) Run(args []string) int { return 1 } + // Check if node exists + node, _, err := client.Nodes().Info(nodeID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying node info: %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 } From 7d2f1c646da96431d3cb25f438a56e7bb51cac35 Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Sun, 20 Dec 2015 18:10:48 +0100 Subject: [PATCH 06/12] Some comment corrections and additions --- nomad/alloc_endpoint.go | 4 ++-- nomad/node_endpoint.go | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nomad/alloc_endpoint.go b/nomad/alloc_endpoint.go index da059f451f50..70b1161a674f 100644 --- a/nomad/alloc_endpoint.go +++ b/nomad/alloc_endpoint.go @@ -96,7 +96,7 @@ func (a *Alloc) GetAlloc(args *structs.AllocSpecificRequest, return err } - // Gather all matching nodes + // Gather all matching allocations var allocs []*structs.Allocation var allocIds []string for { @@ -122,7 +122,7 @@ func (a *Alloc) GetAlloc(args *structs.AllocSpecificRequest, if out != nil { reply.Index = out.ModifyIndex } else { - // Use the last index that affected the nodes table + // Use the last index that affected the allocs table index, err := snap.Index("allocs") if err != nil { return err diff --git a/nomad/node_endpoint.go b/nomad/node_endpoint.go index f65a9a0966e7..e508f04e9367 100644 --- a/nomad/node_endpoint.go +++ b/nomad/node_endpoint.go @@ -310,6 +310,7 @@ func (n *Node) GetNode(args *structs.NodeSpecificRequest, return err } + // Gather all matching nodes var nodes []*structs.Node for { raw := iter.Next() @@ -321,6 +322,7 @@ func (n *Node) GetNode(args *structs.NodeSpecificRequest, } if len(nodes) == 1 { + // return unique node out = nodes[0] } else { return fmt.Errorf("Ambiguous identifier: %v", nodes) From 1af7c579f9c5f26ce0d0425f2aada0b232ba0eb3 Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Tue, 22 Dec 2015 10:18:58 +0100 Subject: [PATCH 07/12] Revert "On cli node status list print the short Node ID when possible" This reverts commit 5d3bd1b6f04bb3c0367798d9bb21729f2cf82247. --- command/node_status.go | 30 +------------------------ command/node_status_test.go | 45 ------------------------------------- 2 files changed, 1 insertion(+), 74 deletions(-) diff --git a/command/node_status.go b/command/node_status.go index 6fa4bc8e272f..1c5973f6a5c6 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -4,8 +4,6 @@ import ( "fmt" "sort" "strings" - - "github.com/hashicorp/nomad/api" ) type NodeStatusCommand struct { @@ -80,14 +78,12 @@ func (c *NodeStatusCommand) Run(args []string) int { return 0 } - shortenNodeId := shouldShortenNodeIds(nodes) - // Format the nodes list 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", - shortenId(node.ID, shortenNodeId), + node.ID, node.Datacenter, node.Name, node.NodeClass, @@ -164,27 +160,3 @@ func (c *NodeStatusCommand) Run(args []string) int { } return 0 } - -// check if there is a collision if we shorten the Node ids -func shouldShortenNodeIds(nodes []*api.NodeListStub) bool { - ids := map[string]bool{} - - for _, node := range nodes { - if len(node.ID) != 36 { - return false //We have a custom ID, don't shorten anything - } else if ids[node.ID[:8]] == true { - return false //There is a collision - } else { - ids[node.ID[:8]] = true - } - } - return true -} - -// shorten an UUID syntax XXXXXXXX-XX... to 8 chars XXXXXXXX -func shortenId(id string, shouldShortenId bool) string { - if shouldShortenId == true { - return id[:8] - } - return id -} diff --git a/command/node_status_test.go b/command/node_status_test.go index f13ff905b5b5..567a91f55639 100644 --- a/command/node_status_test.go +++ b/command/node_status_test.go @@ -5,7 +5,6 @@ import ( "strings" "testing" - "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" ) @@ -109,47 +108,3 @@ func TestNodeStatusCommand_Fails(t *testing.T) { t.Fatalf("expected not found error, got: %s", out) } } - -func Test_ShortenId(t *testing.T) { - id := "1234567890" - shortID := "12345678" - - dontShorten := shortenId(id, false) - if dontShorten != id { - t.Fatalf("Shorten ID should not short id on false, expected %s, got: %s", id, dontShorten) - } - - shorten := shortenId(id, true) - if shorten != shortID { - t.Fatalf("Shorten ID should short id on true, expected %s, got: %s", shortID, shorten) - } -} - -func Test_ShouldShortenNodeIds(t *testing.T) { - var list []*api.NodeListStub - nodeCustomId := &api.NodeListStub{ - ID: "my_own_id", - } - nodeOne := &api.NodeListStub{ - ID: "11111111-1111-1111-1111-111111111111", - } - nodeTwo := &api.NodeListStub{ - ID: "11111111-2222-2222-2222-222222222222", - } - - list = append(list, nodeCustomId) - if shouldShortenNodeIds(list) != false { - t.Fatalf("ShouldShortenNodeIds should return false when using custom id") - } - - list = nil - list = append(list, nodeOne) - if shouldShortenNodeIds(list) != true { - t.Fatalf("ShouldShortenNodeIds should return true when no collisions") - } - - list = append(list, nodeTwo) - if shouldShortenNodeIds(list) != false { - t.Fatalf("ShouldShortenNodeIds should return false when collision detected") - } -} From e89b5af338b06f9eed5a74dc5334bdd1979a602d Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Tue, 22 Dec 2015 23:44:33 +0100 Subject: [PATCH 08/12] Refactoring * Reverted changes to get methods * Added prefix query parameter * Updated node status to use prefix based searching * Fixed tests * Removed truncation logic --- api/api.go | 6 ++++ command/agent/http.go | 9 ++++++ command/agent/node_endpoint_test.go | 49 +++++++++++++++++++++++++++++ command/node_drain.go | 2 +- command/node_status.go | 39 +++++++++++++++++++++-- command/node_status_test.go | 14 +++++++++ nomad/alloc_endpoint.go | 40 +++-------------------- nomad/eval_endpoint.go | 37 ++-------------------- nomad/job_endpoint.go | 28 ----------------- nomad/node_endpoint.go | 33 +++++-------------- nomad/state/state_store.go | 16 +++++----- nomad/state/state_store_test.go | 32 +++++++++---------- nomad/structs/structs.go | 3 ++ 13 files changed, 157 insertions(+), 151 deletions(-) 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/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/node_endpoint_test.go b/command/agent/node_endpoint_test.go index a745a62784b4..ba5018803344 100644 --- a/command/agent/node_endpoint_test.go +++ b/command/agent/node_endpoint_test.go @@ -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 job + 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/node_drain.go b/command/node_drain.go index 3d1bec199983..0478cdb632b0 100644 --- a/command/node_drain.go +++ b/command/node_drain.go @@ -71,7 +71,7 @@ func (c *NodeDrainCommand) Run(args []string) int { // Check if node exists node, _, err := client.Nodes().Info(nodeID, nil) if err != nil { - c.Ui.Error(fmt.Sprintf("Error querying node info: %s", err)) + c.Ui.Error(fmt.Sprintf("Error toggling drain mode: %s", err)) return 1 } diff --git a/command/node_status.go b/command/node_status.go index 1c5973f6a5c6..e01b60d94445 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -4,6 +4,8 @@ import ( "fmt" "sort" "strings" + + "github.com/hashicorp/nomad/api" ) type NodeStatusCommand struct { @@ -100,8 +102,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().List(&api.QueryOptions{Prefix: 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("Node not found")) + 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(formatList(out)) + return 0 + } + // Query full node information for unique prefix match + 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 diff --git a/command/node_status_test.go b/command/node_status_test.go index 567a91f55639..79a676085456 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,6 +112,7 @@ 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 { diff --git a/nomad/alloc_endpoint.go b/nomad/alloc_endpoint.go index 70b1161a674f..c07d5549d739 100644 --- a/nomad/alloc_endpoint.go +++ b/nomad/alloc_endpoint.go @@ -1,7 +1,6 @@ package nomad import ( - "fmt" "time" "github.com/armon/go-metrics" @@ -81,40 +80,9 @@ func (a *Alloc) GetAlloc(args *structs.AllocSpecificRequest, if err != nil { return err } - - var out *structs.Allocation - - // Exact lookup if the identifier length is 36 (full UUID) - if len(args.AllocID) == 36 { - out, err = snap.AllocByID(args.AllocID) - if err != nil { - return err - } - } else { - iter, err := snap.AllocByIDPrefix(args.AllocID) - if err != nil { - return err - } - - // Gather all matching allocations - var allocs []*structs.Allocation - var allocIds []string - for { - raw := iter.Next() - if raw == nil { - break - } - alloc := raw.(*structs.Allocation) - allocIds = append(allocIds, alloc.ID) - allocs = append(allocs, alloc) - } - - if len(allocs) == 1 { - // Return unique allocation - out = allocs[0] - } else if len(allocs) > 1 { - return fmt.Errorf("Ambiguous identifier: %+v", allocIds) - } + out, err := snap.AllocByID(args.AllocID) + if err != nil { + return err } // Setup the output @@ -122,7 +90,7 @@ func (a *Alloc) GetAlloc(args *structs.AllocSpecificRequest, if out != nil { reply.Index = out.ModifyIndex } else { - // Use the last index that affected the allocs table + // Use the last index that affected the nodes table index, err := snap.Index("allocs") if err != nil { return err diff --git a/nomad/eval_endpoint.go b/nomad/eval_endpoint.go index c9f68c0c67db..bc74e85f362c 100644 --- a/nomad/eval_endpoint.go +++ b/nomad/eval_endpoint.go @@ -38,40 +38,9 @@ func (e *Eval) GetEval(args *structs.EvalSpecificRequest, if err != nil { return err } - - var out *structs.Evaluation - - // Exact lookup if the identifier length is 36 (full UUID) - if len(args.EvalID) == 36 { - out, err = snap.EvalByID(args.EvalID) - if err != nil { - return err - } - } else { - iter, err := snap.EvalByIDPrefix(args.EvalID) - if err != nil { - return err - } - - // Gather all matching evaluations - var evals []*structs.Evaluation - var evalIds []string - for { - raw := iter.Next() - if raw == nil { - break - } - eval := raw.(*structs.Evaluation) - evalIds = append(evalIds, eval.ID) - evals = append(evals, eval) - } - - if len(evals) == 1 { - // Return unique evaluation - out = evals[0] - } else if len(evals) > 1 { - return fmt.Errorf("Ambiguous identifier: %+v", evalIds) - } + out, err := snap.EvalByID(args.EvalID) + if err != nil { + return err } // Setup the output diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index f7a17fb222ab..18da75268ab6 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -218,34 +218,6 @@ func (j *Job) GetJob(args *structs.JobSpecificRequest, return err } - // Exact lookup failed so try a prefix based lookup - if out == nil { - iter, err := snap.JobByIDPrefix(args.JobID) - if err != nil { - return err - } - - // Gather all matching jobs - var jobs []*structs.Job - var jobIds []string - for { - raw := iter.Next() - if raw == nil { - break - } - job := raw.(*structs.Job) - jobIds = append(jobIds, job.ID) - jobs = append(jobs, job) - } - - if len(jobs) == 1 { - // Return unique match - out = jobs[0] - } else if len(jobs) > 1 { - return fmt.Errorf("Ambiguous identifier: %+v", jobIds) - } - } - // Setup the output reply.Job = out if out != nil { diff --git a/nomad/node_endpoint.go b/nomad/node_endpoint.go index e508f04e9367..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" ) @@ -304,31 +305,6 @@ func (n *Node) GetNode(args *structs.NodeSpecificRequest, return err } - if out == nil { - iter, err := snap.NodeByIDPrefix(args.NodeID) - if err != nil { - return err - } - - // Gather all matching nodes - var nodes []*structs.Node - for { - raw := iter.Next() - if raw == nil { - break - } - node := raw.(*structs.Node) - nodes = append(nodes, node) - } - - if len(nodes) == 1 { - // return unique node - out = nodes[0] - } else { - return fmt.Errorf("Ambiguous identifier: %v", nodes) - } - } - // Setup the output reply.Node = out if out != nil { @@ -449,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/state/state_store.go b/nomad/state/state_store.go index d155003466bc..d27cba9b9cff 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -252,8 +252,8 @@ func (s *StateStore) NodeByID(nodeID string) (*structs.Node, error) { return nil, nil } -// NodeByIDPrefix is used to lookup a node by partial ID -func (s *StateStore) NodeByIDPrefix(nodeID string) (memdb.ResultIterator, error) { +// 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) @@ -359,8 +359,8 @@ func (s *StateStore) JobByID(id string) (*structs.Job, error) { return nil, nil } -// JobByIDPrefix is used to lookup a job by partial ID -func (s *StateStore) JobByIDPrefix(id string) (memdb.ResultIterator, error) { +// 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) @@ -524,8 +524,8 @@ func (s *StateStore) EvalByID(id string) (*structs.Evaluation, error) { return nil, nil } -// EvalByIDPrefix is used to lookup an eval by partial ID -func (s *StateStore) EvalByIDPrefix(id string) (memdb.ResultIterator, error) { +// 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) @@ -685,8 +685,8 @@ func (s *StateStore) AllocByID(id string) (*structs.Allocation, error) { return nil, nil } -// AllocByIDPrefix is used to lookup an alloc by partial ID -func (s *StateStore) AllocByIDPrefix(id string) (memdb.ResultIterator, error) { +// 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) diff --git a/nomad/state/state_store_test.go b/nomad/state/state_store_test.go index 2cd17cce42b1..5ff4d110d161 100644 --- a/nomad/state/state_store_test.go +++ b/nomad/state/state_store_test.go @@ -216,7 +216,7 @@ func TestStateStore_Nodes(t *testing.T) { } } -func TestStateStore_NodeByIDPrefix(t *testing.T) { +func TestStateStore_NodesByIDPrefix(t *testing.T) { state := testStateStore(t) node := mock.Node() @@ -226,7 +226,7 @@ func TestStateStore_NodeByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err := state.NodeByIDPrefix(node.ID) + iter, err := state.NodesByIDPrefix(node.ID) if err != nil { t.Fatalf("err: %v", err) } @@ -249,7 +249,7 @@ func TestStateStore_NodeByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err = state.NodeByIDPrefix("11") + iter, err = state.NodesByIDPrefix("11") if err != nil { t.Fatalf("err: %v", err) } @@ -266,7 +266,7 @@ func TestStateStore_NodeByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err = state.NodeByIDPrefix("11") + iter, err = state.NodesByIDPrefix("11") if err != nil { t.Fatalf("err: %v", err) } @@ -276,7 +276,7 @@ func TestStateStore_NodeByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err = state.NodeByIDPrefix("111") + iter, err = state.NodesByIDPrefix("111") if err != nil { t.Fatalf("err: %v", err) } @@ -476,7 +476,7 @@ func TestStateStore_Jobs(t *testing.T) { } } -func TestStateStore_JobByIDPrefix(t *testing.T) { +func TestStateStore_JobsByIDPrefix(t *testing.T) { state := testStateStore(t) job := mock.Job() @@ -486,7 +486,7 @@ func TestStateStore_JobByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err := state.JobByIDPrefix(job.ID) + iter, err := state.JobsByIDPrefix(job.ID) if err != nil { t.Fatalf("err: %v", err) } @@ -508,7 +508,7 @@ func TestStateStore_JobByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err = state.JobByIDPrefix("re") + iter, err = state.JobsByIDPrefix("re") if err != nil { t.Fatalf("err: %v", err) } @@ -525,7 +525,7 @@ func TestStateStore_JobByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err = state.JobByIDPrefix("r") + iter, err = state.JobsByIDPrefix("r") if err != nil { t.Fatalf("err: %v", err) } @@ -535,7 +535,7 @@ func TestStateStore_JobByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err = state.JobByIDPrefix("ri") + iter, err = state.JobsByIDPrefix("ri") if err != nil { t.Fatalf("err: %v", err) } @@ -1001,7 +1001,7 @@ func TestStateStore_Evals(t *testing.T) { } } -func TestStateStore_EvalByIDPrefix(t *testing.T) { +func TestStateStore_EvalsByIDPrefix(t *testing.T) { state := testStateStore(t) var evals []*structs.Evaluation @@ -1027,7 +1027,7 @@ func TestStateStore_EvalByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err := state.EvalByIDPrefix("aaaa") + iter, err := state.EvalsByIDPrefix("aaaa") if err != nil { t.Fatalf("err: %v", err) } @@ -1057,7 +1057,7 @@ func TestStateStore_EvalByIDPrefix(t *testing.T) { } } - iter, err = state.EvalByIDPrefix("b-a7bfb") + iter, err = state.EvalsByIDPrefix("b-a7bfb") if err != nil { t.Fatalf("err: %v", err) } @@ -1329,7 +1329,7 @@ func TestStateStore_AllocsByJob(t *testing.T) { } } -func TestStateStore_AllocByIDPrefix(t *testing.T) { +func TestStateStore_AllocsByIDPrefix(t *testing.T) { state := testStateStore(t) var allocs []*structs.Allocation @@ -1355,7 +1355,7 @@ func TestStateStore_AllocByIDPrefix(t *testing.T) { t.Fatalf("err: %v", err) } - iter, err := state.AllocByIDPrefix("aaaa") + iter, err := state.AllocsByIDPrefix("aaaa") if err != nil { t.Fatalf("err: %v", err) } @@ -1385,7 +1385,7 @@ func TestStateStore_AllocByIDPrefix(t *testing.T) { } } - iter, err = state.AllocByIDPrefix("b-a7bfb") + iter, err = state.AllocsByIDPrefix("b-a7bfb") if err != nil { t.Fatalf("err: %v", err) } diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index bed272309d9d..5b443c4cb6a0 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -69,6 +69,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 { From 905742249e8864722a7692a4d9f63d2eb0c0ae40 Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Thu, 24 Dec 2015 11:46:59 +0100 Subject: [PATCH 09/12] Refactoring continued * Refactor other cli commands to new design * Add PrefixList method to api package * Add more tests --- api/allocations.go | 4 ++ api/allocations_test.go | 46 +++++++++++++++++ api/evaluations.go | 4 ++ api/evaluations_test.go | 39 ++++++++++++++ api/jobs.go | 5 ++ api/jobs_test.go | 76 ++++++++++++++++++++++++++++ api/nodes.go | 4 ++ api/nodes_test.go | 46 +++++++++++++++++ command/agent/alloc_endpoint_test.go | 46 +++++++++++++++++ command/agent/eval_endpoint_test.go | 46 +++++++++++++++++ command/agent/job_endpoint_test.go | 53 +++++++++++++++++++ command/alloc_status.go | 32 +++++++++++- command/alloc_status_test.go | 3 +- command/eval_monitor_test.go | 2 +- command/monitor.go | 30 ++++++++++- command/monitor_test.go | 46 +++++++++++++++++ command/node_drain.go | 36 ++++++++++++- command/node_drain_test.go | 2 +- command/node_status.go | 6 +-- command/node_status_test.go | 2 +- command/status.go | 29 ++++++++++- command/status_test.go | 34 ++++++++++--- command/stop.go | 29 ++++++++++- command/stop_test.go | 2 +- nomad/alloc_endpoint.go | 8 ++- nomad/alloc_endpoint_test.go | 22 +++++++- nomad/eval_endpoint.go | 8 ++- nomad/eval_endpoint_test.go | 19 +++++++ nomad/job_endpoint.go | 8 ++- nomad/job_endpoint_test.go | 19 +++++++ nomad/node_endpoint_test.go | 19 +++++++ 31 files changed, 695 insertions(+), 30 deletions(-) diff --git a/api/allocations.go b/api/allocations.go index 73f600d7e8ac..7667b8a19537 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/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 17e75daff5b3..055b976ebae0 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 3d81204c0a7a..ba12d7fa3b5c 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..ed13ba78a6b3 100644 --- a/api/nodes_test.go +++ b/api/nodes_test.go @@ -38,6 +38,52 @@ 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 + testutil.WaitForResult(func() (bool, error) { + out, qm, err = nodes.PrefixList(nodeID[:4]) + if err != nil { + return false, err + } + if n := len(out); n != 1 { + return false, fmt.Errorf("expected 1 node, got: %d ", n) + } + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + + // 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..daa1fdfee92e 100644 --- a/command/agent/alloc_endpoint_test.go +++ b/command/agent/alloc_endpoint_test.go @@ -53,6 +53,52 @@ 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 job + n := obj.([]*structs.AllocListStub) + if len(n) != 1 { + t.Fatalf("bad: %#v", n) + } + }) +} + 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..ad3897185905 100644 --- a/command/agent/eval_endpoint_test.go +++ b/command/agent/eval_endpoint_test.go @@ -53,6 +53,52 @@ 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 job + e := obj.([]*structs.Evaluation) + if len(e) != 1 { + t.Fatalf("bad: %#v", e) + } + }) +} + func TestHTTP_EvalAllocations(t *testing.T) { httpTest(t, nil, func(s *TestServer) { // Directly manipulate the state 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/alloc_status.go b/command/alloc_status.go index 07467597650c..d5c8104ff627 100644 --- a/command/alloc_status.go +++ b/command/alloc_status.go @@ -68,8 +68,36 @@ 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: %s", 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(formatList(out)) + return 0 + } + 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 a6615c5a18b5..dfcc5341fa75 100644 --- a/command/monitor.go +++ b/command/monitor.go @@ -182,8 +182,34 @@ 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(formatList(out)) + return 0 + } + 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. 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 0478cdb632b0..6e8321d21c51 100644 --- a/command/node_drain.go +++ b/command/node_drain.go @@ -71,8 +71,40 @@ func (c *NodeDrainCommand) Run(args []string) int { // Check if node exists node, _, err := client.Nodes().Info(nodeID, nil) if err != nil { - c.Ui.Error(fmt.Sprintf("Error toggling drain mode: %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 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(formatList(out)) + return 0 + } + 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 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 e01b60d94445..d2de4161348e 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -4,8 +4,6 @@ import ( "fmt" "sort" "strings" - - "github.com/hashicorp/nomad/api" ) type NodeStatusCommand struct { @@ -103,14 +101,14 @@ func (c *NodeStatusCommand) Run(args []string) int { node, _, err := client.Nodes().Info(nodeID, nil) if err != nil { // Exact lookup failed, try with prefix based search - nodes, _, err := client.Nodes().List(&api.QueryOptions{Prefix: nodeID}) + 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("Node not found")) + c.Ui.Error(fmt.Sprintf("No node(s) with prefix %q found", nodeID)) return 1 } if len(nodes) > 1 { diff --git a/command/node_status_test.go b/command/node_status_test.go index 79a676085456..fe0590177f3d 100644 --- a/command/node_status_test.go +++ b/command/node_status_test.go @@ -118,7 +118,7 @@ func TestNodeStatusCommand_Fails(t *testing.T) { 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 ed7301639dbd..d4b39473bb89 100644 --- a/command/status.go +++ b/command/status.go @@ -89,8 +89,33 @@ 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(formatList(out)) + return 0 + } + job, _, err = client.Jobs().Info(jobs[0].ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying job: %s", err)) + return 1 + } } // Format the job info 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 2071165fb830..87f49a32d968 100644 --- a/command/stop.go +++ b/command/stop.go @@ -67,8 +67,33 @@ func (c *StopCommand) Run(args []string) int { // Check if the job exists job, _, err := client.Jobs().Info(jobID, nil) if err != nil { - c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err)) - return 1 + 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(formatList(out)) + return 0 + } + 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 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 18da75268ab6..04d4a6dcf796 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" ) @@ -257,7 +258,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 5bc3bb95297c..a9c69dfb99c6 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -551,6 +551,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_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) { From 8218442c43a7d0ecbb18de948ff03b7a7e3033a3 Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Thu, 24 Dec 2015 21:23:05 +0100 Subject: [PATCH 10/12] Fix test (due to merge) --- nomad/state/state_store_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nomad/state/state_store_test.go b/nomad/state/state_store_test.go index dda6ae293fda..5f10d7173ee0 100644 --- a/nomad/state/state_store_test.go +++ b/nomad/state/state_store_test.go @@ -572,6 +572,10 @@ func TestStateStore_JobsByPeriodic(t *testing.T) { } iter, err := state.JobsByPeriodic(true) + if err != nil { + t.Fatalf("err: %v", err) + } + var outPeriodic []*structs.Job for { raw := iter.Next() @@ -582,6 +586,7 @@ func TestStateStore_JobsByPeriodic(t *testing.T) { } iter, err = state.JobsByPeriodic(false) + var outNonPeriodic []*structs.Job for { raw := iter.Next() From a73aaf128ba7ca2f05e6af57f4a53ef42fbe5100 Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Tue, 29 Dec 2015 12:17:16 +0100 Subject: [PATCH 11/12] Documentation updates for short identifiers in the CLI --- website/source/docs/commands/alloc-status.html.md.erb | 4 ++-- website/source/docs/commands/eval-monitor.html.md.erb | 6 +++--- website/source/docs/commands/node-drain.html.md.erb | 6 +++--- website/source/docs/commands/node-status.html.md.erb | 10 +++++----- website/source/docs/commands/status.html.md.erb | 8 ++++---- website/source/docs/commands/stop.html.md.erb | 2 +- website/source/docs/http/allocs.html.md | 9 ++++++++- website/source/docs/http/evals.html.md | 9 ++++++++- website/source/docs/http/jobs.html.md | 8 +++++++- website/source/docs/http/nodes.html.md | 8 +++++++- 10 files changed, 48 insertions(+), 22 deletions(-) diff --git a/website/source/docs/commands/alloc-status.html.md.erb b/website/source/docs/commands/alloc-status.html.md.erb index f84a217d0e20..8427a2910f4a 100644 --- a/website/source/docs/commands/alloc-status.html.md.erb +++ b/website/source/docs/commands/alloc-status.html.md.erb @@ -19,8 +19,8 @@ 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 (prefix) must be provided. This specific allocation will be +queried and detailed information for it will be dumped. ## 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..2a969d763cb5 100644 --- a/website/source/docs/commands/eval-monitor.html.md.erb +++ b/website/source/docs/commands/eval-monitor.html.md.erb @@ -21,9 +21,9 @@ 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. +evaluation ID (prefix) 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. 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..3c7bdcd84cf1 100644 --- a/website/source/docs/commands/node-drain.html.md.erb +++ b/website/source/docs/commands/node-drain.html.md.erb @@ -21,9 +21,9 @@ 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. +This command expects exactly one argument to specify the node ID (prefix) +to enable or disable drain mode for. 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..9cd8ad602204 100644 --- a/website/source/docs/commands/node-status.html.md.erb +++ b/website/source/docs/commands/node-status.html.md.erb @@ -20,9 +20,9 @@ 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 a node ID (prefix) is +specified, then that particular node will be queried, and detailed information +will be displayed. ## General Options @@ -50,7 +50,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 +62,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..c5492e1ec44d 100644 --- a/website/source/docs/commands/status.html.md.erb +++ b/website/source/docs/commands/status.html.md.erb @@ -16,10 +16,10 @@ 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 (prefix) 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. ## General Options diff --git a/website/source/docs/commands/stop.html.md.erb b/website/source/docs/commands/stop.html.md.erb index b9794282978d..b2c2cdc8c10b 100644 --- a/website/source/docs/commands/stop.html.md.erb +++ b/website/source/docs/commands/stop.html.md.erb @@ -17,7 +17,7 @@ to cancel all of the running allocations. nomad stop [options] ``` -The stop command requires a single argument, specifying the job ID to +The stop command requires a single argument, specifying the job ID (prefix) to cancel. Upon successful deregistration, an interactive monitor session will start to 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
From 62951082e2ebb76e643f6867cb2e68ad86c801a1 Mon Sep 17 00:00:00 2001 From: Ivo Verberk Date: Wed, 6 Jan 2016 22:46:57 +0100 Subject: [PATCH 12/12] Improvements for short identifiers * Fix tests * Update documentation --- api/nodes_test.go | 17 ++++++----------- command/agent/alloc_endpoint_test.go | 9 +++++++-- command/agent/eval_endpoint_test.go | 9 +++++++-- command/agent/node_endpoint_test.go | 4 ++-- command/alloc_status.go | 5 +++-- command/monitor.go | 3 ++- command/node_drain.go | 3 ++- command/node_status.go | 4 ++-- command/status.go | 3 ++- command/stop.go | 3 ++- .../docs/commands/alloc-status.html.md.erb | 5 +++-- .../docs/commands/eval-monitor.html.md.erb | 10 ++++++---- .../source/docs/commands/node-drain.html.md.erb | 9 ++++++--- .../docs/commands/node-status.html.md.erb | 8 +++++--- website/source/docs/commands/status.html.md.erb | 11 +++++++---- website/source/docs/commands/stop.html.md.erb | 6 ++++-- 16 files changed, 66 insertions(+), 43 deletions(-) diff --git a/api/nodes_test.go b/api/nodes_test.go index ed13ba78a6b3..2e0ed5a9246a 100644 --- a/api/nodes_test.go +++ b/api/nodes_test.go @@ -67,18 +67,13 @@ func TestNodes_PrefixList(t *testing.T) { }) // Find node based on four character prefix - testutil.WaitForResult(func() (bool, error) { - out, qm, err = nodes.PrefixList(nodeID[:4]) - if err != nil { - return false, err - } - if n := len(out); n != 1 { - return false, fmt.Errorf("expected 1 node, got: %d ", n) - } - return true, nil - }, func(err error) { + 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) diff --git a/command/agent/alloc_endpoint_test.go b/command/agent/alloc_endpoint_test.go index daa1fdfee92e..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) @@ -91,11 +91,16 @@ func TestHTTP_AllocsPrefixList(t *testing.T) { t.Fatalf("missing last contact") } - // Check the job + // 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) + } }) } diff --git a/command/agent/eval_endpoint_test.go b/command/agent/eval_endpoint_test.go index ad3897185905..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) @@ -91,11 +91,16 @@ func TestHTTP_EvalPrefixList(t *testing.T) { t.Fatalf("missing last contact") } - // Check the job + // 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) + } }) } diff --git a/command/agent/node_endpoint_test.go b/command/agent/node_endpoint_test.go index ba5018803344..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) @@ -97,7 +97,7 @@ func TestHTTP_NodesPrefixList(t *testing.T) { t.Fatalf("missing last contact") } - // Check the job + // Check the nodes n := obj.([]*structs.NodeListStub) if len(n) != 3 { t.Fatalf("bad: %#v", n) diff --git a/command/alloc_status.go b/command/alloc_status.go index d5c8104ff627..3616817686af 100644 --- a/command/alloc_status.go +++ b/command/alloc_status.go @@ -70,7 +70,7 @@ func (c *AllocStatusCommand) Run(args []string) int { if err != nil { allocs, _, err := client.Allocations().PrefixList(allocID) if err != nil { - c.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err)) + c.Ui.Error(fmt.Sprintf("Error querying allocation: %v", err)) return 1 } if len(allocs) == 0 { @@ -90,9 +90,10 @@ func (c *AllocStatusCommand) Run(args []string) int { alloc.DesiredStatus, alloc.ClientStatus) } - c.Ui.Output(formatList(out)) + 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)) diff --git a/command/monitor.go b/command/monitor.go index dfcc5341fa75..c47007e4e57f 100644 --- a/command/monitor.go +++ b/command/monitor.go @@ -203,9 +203,10 @@ func (m *monitor) monitor(evalID string) int { eval.TriggeredBy, eval.Status) } - m.ui.Output(formatList(out)) + 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)) diff --git a/command/node_drain.go b/command/node_drain.go index 6e8321d21c51..4a0a696ecaf6 100644 --- a/command/node_drain.go +++ b/command/node_drain.go @@ -97,9 +97,10 @@ func (c *NodeDrainCommand) Run(args []string) int { node.Status) } // Dump the output - c.Ui.Output(formatList(out)) + 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)) diff --git a/command/node_status.go b/command/node_status.go index d2de4161348e..fd29f2335ce6 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -126,10 +126,10 @@ func (c *NodeStatusCommand) Run(args []string) int { node.Status) } // Dump the output - c.Ui.Output(formatList(out)) + c.Ui.Output(fmt.Sprintf("Please disambiguate the desired node\n\n%s", formatList(out))) return 0 } - // Query full node information for unique prefix match + // 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)) diff --git a/command/status.go b/command/status.go index d1359cbcb0c9..cbfdefc6dcff 100644 --- a/command/status.go +++ b/command/status.go @@ -114,9 +114,10 @@ func (c *StatusCommand) Run(args []string) int { job.Priority, job.Status) } - c.Ui.Output(formatList(out)) + 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)) diff --git a/command/stop.go b/command/stop.go index 87f49a32d968..a309dd26a01c 100644 --- a/command/stop.go +++ b/command/stop.go @@ -86,9 +86,10 @@ func (c *StopCommand) Run(args []string) int { job.Priority, job.Status) } - c.Ui.Output(formatList(out)) + 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)) diff --git a/website/source/docs/commands/alloc-status.html.md.erb b/website/source/docs/commands/alloc-status.html.md.erb index 8427a2910f4a..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 (prefix) 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 2a969d763cb5..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 (prefix) 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 3c7bdcd84cf1..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 (prefix) -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 9cd8ad602204..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 (prefix) 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 diff --git a/website/source/docs/commands/status.html.md.erb b/website/source/docs/commands/status.html.md.erb index c5492e1ec44d..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 (prefix) 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 b2c2cdc8c10b..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 (prefix) 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