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 {