Skip to content

Commit

Permalink
Merge pull request #2964 from hashicorp/f-autocomplete-api
Browse files Browse the repository at this point in the history
autocomplete api
  • Loading branch information
chelseakomlo committed Aug 7, 2017
2 parents 2607948 + 440fea7 commit a768eca
Show file tree
Hide file tree
Showing 8 changed files with 827 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## 0.6.1 (Unreleased)

IMPROVEMENTS:
* core: Add autocomplete functionality for resources: allocations,
evaluations, jobs, and nodes [GH-2964]
* core: `distinct_property` constraint can set the number of allocations that
are allowed to share a property value [GH-2942]
* core: Lost allocations replaced even if part of failed deployment [GH-2961]
Expand Down
2 changes: 2 additions & 0 deletions command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.mux.HandleFunc("/v1/evaluations", s.wrap(s.EvalsRequest))
s.mux.HandleFunc("/v1/evaluation/", s.wrap(s.EvalSpecificRequest))

s.mux.HandleFunc("/v1/resources/", s.wrap(s.ResourceListRequest))

s.mux.HandleFunc("/v1/deployments", s.wrap(s.DeploymentsRequest))
s.mux.HandleFunc("/v1/deployment/", s.wrap(s.DeploymentSpecificRequest))

Expand Down
31 changes: 31 additions & 0 deletions command/agent/resources_endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package agent

import (
"github.com/hashicorp/nomad/nomad/structs"
"net/http"
)

// ResourceListRequest accepts a prefix and context and returns a list of matching
// IDs for that context.
func (s *HTTPServer) ResourceListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method == "POST" || req.Method == "PUT" {
return s.resourcesRequest(resp, req)
}
return nil, CodedError(405, ErrInvalidMethod)
}

func (s *HTTPServer) resourcesRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
args := structs.ResourceListRequest{}

if err := decodeBody(req, &args); err != nil {
return nil, CodedError(400, err.Error())
}

var out structs.ResourceListResponse
if err := s.agent.RPC("Resources.List", &args, &out); err != nil {
return nil, err
}

setMeta(resp, &out.QueryMeta)
return out, nil
}
304 changes: 304 additions & 0 deletions command/agent/resources_endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
package agent

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
a "github.com/stretchr/testify/assert"
)

func TestHTTP_ResourcesWithIllegalMethod(t *testing.T) {
assert := a.New(t)
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
req, err := http.NewRequest("DELETE", "/v1/resources", nil)
assert.Nil(err)
respW := httptest.NewRecorder()

_, err = s.Server.ResourceListRequest(respW, req)
assert.NotNil(err, "HTTP DELETE should not be accepted for this endpoint")
})
}

func createJobForTest(jobID string, s *TestAgent, t *testing.T) {
assert := a.New(t)

job := mock.Job()
job.ID = jobID
job.TaskGroups[0].Count = 1

state := s.Agent.server.State()
err := state.UpsertJob(1000, job)
assert.Nil(err)
}

func TestHTTP_Resources_POST(t *testing.T) {
assert := a.New(t)

testJob := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJob, s, t)

data := structs.ResourceListRequest{Prefix: testJobPrefix, Context: "jobs"}
req, err := http.NewRequest("POST", "/v1/resources", encodeReq(data))
assert.Nil(err)

respW := httptest.NewRecorder()

resp, err := s.Server.ResourceListRequest(respW, req)
assert.Nil(err)

res := resp.(structs.ResourceListResponse)

assert.Equal(1, len(res.Matches))

j := res.Matches["jobs"]

assert.Equal(1, len(j))
assert.Equal(j[0], testJob)

assert.Equal(res.Truncations["job"], false)
assert.NotEqual("0", respW.HeaderMap.Get("X-Nomad-Index"))
})
}

func TestHTTP_Resources_PUT(t *testing.T) {
assert := a.New(t)

testJob := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJob, s, t)

data := structs.ResourceListRequest{Prefix: testJobPrefix, Context: "jobs"}
req, err := http.NewRequest("PUT", "/v1/resources", encodeReq(data))
assert.Nil(err)

respW := httptest.NewRecorder()

resp, err := s.Server.ResourceListRequest(respW, req)
assert.Nil(err)

res := resp.(structs.ResourceListResponse)

assert.Equal(1, len(res.Matches))

j := res.Matches["jobs"]

assert.Equal(1, len(j))
assert.Equal(j[0], testJob)

assert.Equal(res.Truncations["job"], false)
assert.NotEqual("0", respW.HeaderMap.Get("X-Nomad-Index"))
})
}

func TestHTTP_Resources_MultipleJobs(t *testing.T) {
assert := a.New(t)

testJobA := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobB := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89707"
testJobC := "bbbbbbbb-e8f7-fd38-c855-ab94ceb89707"

testJobPrefix := "aaaaaaaa-e8f7-fd38"

t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJobA, s, t)
createJobForTest(testJobB, s, t)
createJobForTest(testJobC, s, t)

data := structs.ResourceListRequest{Prefix: testJobPrefix, Context: "jobs"}
req, err := http.NewRequest("POST", "/v1/resources", encodeReq(data))
assert.Nil(err)

respW := httptest.NewRecorder()

resp, err := s.Server.ResourceListRequest(respW, req)
assert.Nil(err)

res := resp.(structs.ResourceListResponse)

assert.Equal(1, len(res.Matches))

j := res.Matches["jobs"]

assert.Equal(2, len(j))
assert.Contains(j, testJobA)
assert.Contains(j, testJobB)
assert.NotContains(j, testJobC)

assert.Equal(res.Truncations["job"], false)
assert.NotEqual("0", respW.HeaderMap.Get("X-Nomad-Index"))
})
}

func TestHTTP_ResoucesList_Evaluation(t *testing.T) {
assert := a.New(t)

t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
eval1 := mock.Eval()
eval2 := mock.Eval()
err := state.UpsertEvals(9000,
[]*structs.Evaluation{eval1, eval2})
assert.Nil(err)

prefix := eval1.ID[:len(eval1.ID)-2]
data := structs.ResourceListRequest{Prefix: prefix, Context: "evals"}
req, err := http.NewRequest("POST", "/v1/resources", encodeReq(data))
assert.Nil(err)

respW := httptest.NewRecorder()

resp, err := s.Server.ResourceListRequest(respW, req)
assert.Nil(err)

res := resp.(structs.ResourceListResponse)

assert.Equal(1, len(res.Matches))

j := res.Matches["evals"]
assert.Equal(1, len(j))
assert.Contains(j, eval1.ID)
assert.NotContains(j, eval2.ID)

assert.Equal(res.Truncations["evals"], false)
assert.Equal("9000", respW.HeaderMap.Get("X-Nomad-Index"))
})
}

func TestHTTP_ResoucesList_Allocations(t *testing.T) {
assert := a.New(t)

t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
alloc := mock.Alloc()
err := state.UpsertAllocs(7000, []*structs.Allocation{alloc})
assert.Nil(err)

prefix := alloc.ID[:len(alloc.ID)-2]
data := structs.ResourceListRequest{Prefix: prefix, Context: "allocs"}
req, err := http.NewRequest("POST", "/v1/resources", encodeReq(data))
assert.Nil(err)

respW := httptest.NewRecorder()

resp, err := s.Server.ResourceListRequest(respW, req)
assert.Nil(err)

res := resp.(structs.ResourceListResponse)

assert.Equal(1, len(res.Matches))

a := res.Matches["allocs"]
assert.Equal(1, len(a))
assert.Contains(a, alloc.ID)

assert.Equal(res.Truncations["allocs"], false)
assert.Equal("7000", respW.HeaderMap.Get("X-Nomad-Index"))
})
}

func TestHTTP_ResoucesList_Nodes(t *testing.T) {
assert := a.New(t)

t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
node := mock.Node()
err := state.UpsertNode(6000, node)
assert.Nil(err)

prefix := node.ID[:len(node.ID)-2]
data := structs.ResourceListRequest{Prefix: prefix, Context: "nodes"}
req, err := http.NewRequest("POST", "/v1/resources", encodeReq(data))
assert.Nil(err)

respW := httptest.NewRecorder()

resp, err := s.Server.ResourceListRequest(respW, req)
assert.Nil(err)

res := resp.(structs.ResourceListResponse)

assert.Equal(1, len(res.Matches))

n := res.Matches["nodes"]
assert.Equal(1, len(n))
assert.Contains(n, node.ID)

assert.Equal(res.Truncations["nodes"], false)
assert.Equal("6000", respW.HeaderMap.Get("X-Nomad-Index"))
})
}

func TestHTTP_Resources_NoJob(t *testing.T) {
assert := a.New(t)

t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
data := structs.ResourceListRequest{Prefix: "12345", Context: "jobs"}
req, err := http.NewRequest("POST", "/v1/resources", encodeReq(data))
assert.Nil(err)

respW := httptest.NewRecorder()

resp, err := s.Server.ResourceListRequest(respW, req)
assert.Nil(err)

res := resp.(structs.ResourceListResponse)

assert.Equal(1, len(res.Matches))
assert.Equal(0, len(res.Matches["jobs"]))

assert.Equal("0", respW.HeaderMap.Get("X-Nomad-Index"))
})
}

func TestHTTP_Resources_NoContext(t *testing.T) {
assert := a.New(t)

testJobID := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJobID, s, t)

state := s.Agent.server.State()
eval1 := mock.Eval()
eval1.ID = testJobID
err := state.UpsertEvals(8000, []*structs.Evaluation{eval1})
assert.Nil(err)

data := structs.ResourceListRequest{Prefix: testJobPrefix}
req, err := http.NewRequest("POST", "/v1/resources", encodeReq(data))
assert.Nil(err)

respW := httptest.NewRecorder()

resp, err := s.Server.ResourceListRequest(respW, req)
assert.Nil(err)

res := resp.(structs.ResourceListResponse)

matchedJobs := res.Matches["jobs"]
matchedEvals := res.Matches["evals"]

assert.Equal(1, len(matchedJobs))
assert.Equal(1, len(matchedEvals))

assert.Equal(matchedJobs[0], testJobID)
assert.Equal(matchedEvals[0], eval1.ID)

assert.Equal("8000", respW.HeaderMap.Get("X-Nomad-Index"))
})
}
Loading

0 comments on commit a768eca

Please sign in to comment.