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 b3cba519c..1c5973f6a 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 30ee87259..6dd9d9c2d 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 be9b816dc..a6615c5a1 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 4a736dc7a..ed7301639 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 c07d5549d..da059f451 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 bc74e85f3..c9f68c0c6 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 18da75268..f7a17fb22 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 5bd600380..f65a9a096 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 c7e962c39..d15500346 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 0609f3048..2cd17cce4 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 c7705f9fa..2071165fb 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 1c5973f6a..6fa4bc8e2 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 567a91f55..f13ff905b 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 efb0d13fe..3d1bec199 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 da059f451..70b1161a6 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 f65a9a096..e508f04e9 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 6fa4bc8e2..1c5973f6a 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 f13ff905b..567a91f55 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 5ca63aad7..32d9b87b0 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 1478c9d5a..4c46ee8b4 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 a745a6278..ba5018803 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 3d1bec199..0478cdb63 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 1c5973f6a..e01b60d94 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 567a91f55..79a676085 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 70b1161a6..c07d5549d 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 c9f68c0c6..bc74e85f3 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 f7a17fb22..18da75268 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 e508f04e9..f004110b4 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 d15500346..d27cba9b9 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 2cd17cce4..5ff4d110d 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 bed272309..5b443c4cb 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 73f600d7e..7667b8a19 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 c8fde832f..597000472 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 9fbb83b5c..304f5ae72 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 c7772dc96..2a66534e8 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 17e75daff..055b976eb 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 3d81204c0..ba12d7fa3 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 5c8b01d47..4f3d66489 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 23bd1af9c..ed13ba78a 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 3f071b0b0..daa1fdfee 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 69a322ca5..ad3897185 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 3c960d12b..cdb1bc88f 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 074675976..d5c8104ff 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 090ebb0eb..208cca88f 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 a7c341252..f805872f3 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 a6615c5a1..dfcc5341f 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 fd0be835a..59bfb4599 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 0478cdb63..6e8321d21 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 fa437efb7..af22d5be0 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 e01b60d94..d2de41613 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 79a676085..fe0590177 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 ed7301639..d4b39473b 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 e5561e0d8..766bf305a 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 2071165fb..87f49a32d 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 2195cbf8a..9abe0548f 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 c07d5549d..ca65621b1 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 bcab0a387..2f82ad6de 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 bc74e85f3..712111697 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 55782a031..3d59f33ed 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 18da75268..04d4a6dcf 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 5bc3bb952..a9c69dfb9 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 74b154655..b7464c7e9 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 dda6ae293..5f10d7173 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 f84a217d0..8427a2910 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 7fa6a1365..2a969d763 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 ac9624e0c..3c7bdcd84 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 5d514caeb..9cd8ad602 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 7a5995e8e..c5492e1ec 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 b97942829..b2c2cdc8c 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 7cb38ab66..dea7b63f0 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 23d98cc95..b843d307a 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 8f098b1ca..1642ed4af 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 b8e2b91a9..8c58ed870 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 ed13ba78a..2e0ed5a92 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 daa1fdfee..7bb80a305 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 ad3897185..102c3d7c2 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 ba5018803..ec52d8149 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 d5c8104ff..361681768 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 dfcc5341f..c47007e4e 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 6e8321d21..4a0a696ec 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 d2de41613..fd29f2335 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 d1359cbcb..cbfdefc6d 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 87f49a32d..a309dd26a 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 8427a2910..b0dcc2ad1 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 2a969d763..0159debe8 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 3c7bdcd84..919989f42 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 9cd8ad602..066f99ab1 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 c5492e1ec..e1b10f8f5 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 b2c2cdc8c..08fcec3ee 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