Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow lookups based on short identifiers #575

Merged
merged 17 commits into from Jan 6, 2016
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/api.go
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also added in the structs. Once this PR is done I will take a look at removing the api specific structs and using those in the structs package as specified in issue #329 .

}

// WriteOptions are used to parameterize a write
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions command/agent/http.go
Expand Up @@ -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 != "" {
Expand All @@ -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)
}
49 changes: 49 additions & 0 deletions command/agent/node_endpoint_test.go
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nodes

n := obj.([]*structs.NodeListStub)
if len(n) != 3 {
t.Fatalf("bad: %#v", n)
}
})
}

func TestHTTP_NodeForceEval(t *testing.T) {
httpTest(t, nil, func(s *TestServer) {
// Create the node
Expand Down
2 changes: 1 addition & 1 deletion command/monitor.go
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion command/node_drain.go
Expand Up @@ -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 toggling drain mode: %s", err))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is strictly not an error for toggling the drain mode, but rather for looking up the node. However, the testcase checks for this string so I'd like to hear your thoughts on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine as the err will be something like "node with id "foobar" not found"

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
}
Expand Down
41 changes: 38 additions & 3 deletions command/node_status.go
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"sort"
"strings"

"github.com/hashicorp/nomad/api"
)

type NodeStatusCommand struct {
Expand Down Expand Up @@ -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"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this more verbose. fmt.Sprintf("No node(s) with prefix %q found", nodeID)

return 1
}
if len(nodes) > 1 {
// Format the nodes list that matches the prefix so that the user
// can create a more specific request
out := make([]string, len(nodes)+1)
out[0] = "ID|DC|Name|Class|Drain|Status"
for i, node := range nodes {
out[i+1] = fmt.Sprintf("%s|%s|%s|%s|%v|%s",
node.ID,
node.Datacenter,
node.Name,
node.NodeClass,
node.Drain,
node.Status)
}
// Dump the output
c.Ui.Output(formatList(out))
return 0
}
// Query full node information for unique prefix match
node, _, err = client.Nodes().Info(nodes[0].ID, nil)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra lookup is required since the list function does not return the full information that we need, kinda annoying to make three api calls but I can't see another way?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah since we return the stub you have to. Its okay tho, its the CLI, it isn't high QPS where performance matters too much

if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying node info: %s", err))
return 1
}
}

m := node.Attributes
Expand Down Expand Up @@ -132,7 +167,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
Expand Down
14 changes: 14 additions & 0 deletions command/node_status_test.go
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions command/status.go
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions command/stop.go
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion nomad/node_endpoint.go
Expand Up @@ -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"
)
Expand Down Expand Up @@ -424,7 +425,12 @@ func (n *Node) List(args *structs.NodeListRequest,
if err != nil {
return err
}
iter, err := snap.Nodes()
var iter memdb.ResultIterator
if prefix := args.QueryOptions.Prefix; prefix != "" {
iter, err = snap.NodesByIDPrefix(prefix)
} else {
iter, err = snap.Nodes()
}
if err != nil {
return err
}
Expand Down
48 changes: 48 additions & 0 deletions nomad/state/state_store.go
Expand Up @@ -252,6 +252,18 @@ func (s *StateStore) NodeByID(nodeID string) (*structs.Node, error) {
return nil, nil
}

// NodesByIDPrefix is used to lookup nodes by prefix
func (s *StateStore) NodesByIDPrefix(nodeID string) (memdb.ResultIterator, error) {
txn := s.db.Txn(false)

iter, err := txn.Get("nodes", "id_prefix", nodeID)
if err != nil {
return nil, fmt.Errorf("node lookup failed: %v", err)
}

return iter, nil
}

// Nodes returns an iterator over all the nodes
func (s *StateStore) Nodes() (memdb.ResultIterator, error) {
txn := s.db.Txn(false)
Expand Down Expand Up @@ -347,6 +359,18 @@ func (s *StateStore) JobByID(id string) (*structs.Job, error) {
return nil, nil
}

// JobsByIDPrefix is used to lookup a job by prefix
func (s *StateStore) JobsByIDPrefix(id string) (memdb.ResultIterator, error) {
txn := s.db.Txn(false)

iter, err := txn.Get("jobs", "id_prefix", id)
if err != nil {
return nil, fmt.Errorf("job lookup failed: %v", err)
}

return iter, nil
}

// Jobs returns an iterator over all the jobs
func (s *StateStore) Jobs() (memdb.ResultIterator, error) {
txn := s.db.Txn(false)
Expand Down Expand Up @@ -500,6 +524,18 @@ func (s *StateStore) EvalByID(id string) (*structs.Evaluation, error) {
return nil, nil
}

// EvalsByIDPrefix is used to lookup evaluations by prefix
func (s *StateStore) EvalsByIDPrefix(id string) (memdb.ResultIterator, error) {
txn := s.db.Txn(false)

iter, err := txn.Get("evals", "id_prefix", id)
if err != nil {
return nil, fmt.Errorf("eval lookup failed: %v", err)
}

return iter, nil
}

// EvalsByJob returns all the evaluations by job id
func (s *StateStore) EvalsByJob(jobID string) ([]*structs.Evaluation, error) {
txn := s.db.Txn(false)
Expand Down Expand Up @@ -649,6 +685,18 @@ func (s *StateStore) AllocByID(id string) (*structs.Allocation, error) {
return nil, nil
}

// AllocsByIDPrefix is used to lookup allocs by prefix
func (s *StateStore) AllocsByIDPrefix(id string) (memdb.ResultIterator, error) {
txn := s.db.Txn(false)

iter, err := txn.Get("allocs", "id_prefix", id)
if err != nil {
return nil, fmt.Errorf("alloc lookup failed: %v", err)
}

return iter, nil
}

// AllocsByNode returns all the allocations by node
func (s *StateStore) AllocsByNode(node string) ([]*structs.Allocation, error) {
txn := s.db.Txn(false)
Expand Down