Merge pull request #10184 from hashicorp/f-fuzzy-search

api: implement fuzzy search API
This commit is contained in:
Seth Hoenig
2021-04-20 09:06:40 -06:00
committed by GitHub
25 changed files with 3258 additions and 659 deletions

View File

@@ -424,6 +424,16 @@ func convertServerConfig(agentConfig *Config) (*nomad.Config, error) {
conf.LicenseEnv = agentConfig.Server.LicenseEnv
conf.LicensePath = agentConfig.Server.LicensePath
// Add the search configuration
if search := agentConfig.Server.Search; search != nil {
conf.SearchConfig = &structs.SearchConfig{
FuzzyEnabled: search.FuzzyEnabled,
LimitQuery: search.LimitQuery,
LimitResults: search.LimitResults,
MinTermLength: search.MinTermLength,
}
}
return conf, nil
}

View File

@@ -510,6 +510,44 @@ type ServerConfig struct {
// ExtraKeysHCL is used by hcl to surface unexpected keys
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
Search *Search `hcl:"search"`
}
// Search is used in servers to configure search API options.
type Search struct {
// FuzzyEnabled toggles whether the FuzzySearch API is enabled. If not
// enabled, requests to /v1/search/fuzzy will reply with a 404 response code.
//
// Default: enabled.
FuzzyEnabled bool `hcl:"fuzzy_enabled"`
// LimitQuery limits the number of objects searched in the FuzzySearch API.
// The results are indicated as truncated if the limit is reached.
//
// Lowering this value can reduce resource consumption of Nomad server when
// the FuzzySearch API is enabled.
//
// Default value: 20.
LimitQuery int `hcl:"limit_query"`
// LimitResults limits the number of results provided by the FuzzySearch API.
// The results are indicated as truncate if the limit is reached.
//
// Lowering this value can reduce resource consumption of Nomad server per
// fuzzy search request when the FuzzySearch API is enabled.
//
// Default value: 100.
LimitResults int `hcl:"limit_results"`
// MinTermLength is the minimum length of Text required before the FuzzySearch
// API will return results.
//
// Increasing this value can avoid resource consumption on Nomad server by
// reducing searches with less meaningful results.
//
// Default value: 2.
MinTermLength int `hcl:"min_term_length"`
}
// ServerJoin is used in both clients and servers to bootstrap connections to
@@ -900,6 +938,12 @@ func DefaultConfig() *Config {
RetryInterval: 30 * time.Second,
RetryMaxAttempts: 0,
},
Search: &Search{
FuzzyEnabled: true,
LimitQuery: 20,
LimitResults: 100,
MinTermLength: 2,
},
},
ACL: &ACLConfig{
Enabled: false,
@@ -1434,6 +1478,19 @@ func (a *ServerConfig) Merge(b *ServerConfig) *ServerConfig {
result.DefaultSchedulerConfig = &c
}
if b.Search != nil {
result.Search = &Search{FuzzyEnabled: b.Search.FuzzyEnabled}
if b.Search.LimitQuery > 0 {
result.Search.LimitQuery = b.Search.LimitQuery
}
if b.Search.LimitResults > 0 {
result.Search.LimitResults = b.Search.LimitResults
}
if b.Search.MinTermLength > 0 {
result.Search.MinTermLength = b.Search.MinTermLength
}
}
// Add the schedulers
result.EnabledSchedulers = append(result.EnabledSchedulers, b.EnabledSchedulers...)

View File

@@ -317,6 +317,7 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.mux.HandleFunc("/v1/status/leader", s.wrap(s.StatusLeaderRequest))
s.mux.HandleFunc("/v1/status/peers", s.wrap(s.StatusPeersRequest))
s.mux.HandleFunc("/v1/search/fuzzy", s.wrap(s.FuzzySearchRequest))
s.mux.HandleFunc("/v1/search", s.wrap(s.SearchRequest))
s.mux.HandleFunc("/v1/operator/license", s.wrap(s.LicenseRequest))

View File

@@ -12,14 +12,14 @@ func (s *HTTPServer) SearchRequest(resp http.ResponseWriter, req *http.Request)
if req.Method == "POST" || req.Method == "PUT" {
return s.newSearchRequest(resp, req)
}
return nil, CodedError(405, ErrInvalidMethod)
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
}
func (s *HTTPServer) newSearchRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
args := structs.SearchRequest{}
if err := decodeBody(req, &args); err != nil {
return nil, CodedError(400, err.Error())
return nil, CodedError(http.StatusBadRequest, err.Error())
}
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
@@ -34,3 +34,30 @@ func (s *HTTPServer) newSearchRequest(resp http.ResponseWriter, req *http.Reques
setMeta(resp, &out.QueryMeta)
return out, nil
}
func (s *HTTPServer) FuzzySearchRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method == "POST" || req.Method == "PUT" {
return s.newFuzzySearchRequest(resp, req)
}
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
}
func (s *HTTPServer) newFuzzySearchRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var args structs.FuzzySearchRequest
if err := decodeBody(req, &args); err != nil {
return nil, CodedError(http.StatusBadRequest, err.Error())
}
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
return nil, nil
}
var out structs.FuzzySearchResponse
if err := s.agent.RPC("Search.FuzzySearch", &args, &out); err != nil {
return nil, err
}
setMeta(resp, &out.QueryMeta)
return out, nil
}

View File

@@ -1,114 +1,195 @@
package agent
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHTTP_SearchWithIllegalMethod(t *testing.T) {
assert := assert.New(t)
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
req, err := http.NewRequest("DELETE", "/v1/search", nil)
assert.Nil(err)
respW := httptest.NewRecorder()
_, err = s.Server.SearchRequest(respW, req)
assert.NotNil(err, "HTTP DELETE should not be accepted for this endpoint")
})
func header(recorder *httptest.ResponseRecorder, name string) string {
return recorder.Result().Header.Get(name)
}
func createJobForTest(jobID string, s *TestAgent, t *testing.T) {
assert := assert.New(t)
job := mock.Job()
job.ID = jobID
job.TaskGroups[0].Count = 1
state := s.Agent.server.State()
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, job)
assert.Nil(err)
require.NoError(t, err)
}
func TestHTTP_Search_POST(t *testing.T) {
assert := assert.New(t)
func TestHTTP_PrefixSearchWithIllegalMethod(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
req, err := http.NewRequest("DELETE", "/v1/search", nil)
require.NoError(t, err)
respW := httptest.NewRecorder()
_, err = s.Server.SearchRequest(respW, req)
require.EqualError(t, err, "Invalid method")
})
}
func TestHTTP_FuzzySearchWithIllegalMethod(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
req, err := http.NewRequest("DELETE", "/v1/search/fuzzy", nil)
require.NoError(t, err)
respW := httptest.NewRecorder()
_, err = s.Server.SearchRequest(respW, req)
require.EqualError(t, err, "Invalid method")
})
}
func createCmdJobForTest(name, cmd string, s *TestAgent, t *testing.T) *structs.Job {
job := mock.Job()
job.Name = name
job.TaskGroups[0].Tasks[0].Config["command"] = cmd
job.TaskGroups[0].Count = 1
state := s.Agent.server.State()
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, job)
require.NoError(t, err)
return job
}
func TestHTTP_PrefixSearch_POST(t *testing.T) {
t.Parallel()
testJob := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJob, s, t)
data := structs.SearchRequest{Prefix: testJobPrefix, Context: structs.Jobs}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
assert.Nil(err)
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
assert.Nil(err)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
assert.Equal(1, len(res.Matches))
require.Len(t, res.Matches, 1)
j := res.Matches[structs.Jobs]
require.Len(t, j, 1)
require.Equal(t, testJob, j[0])
assert.Equal(1, len(j))
assert.Equal(j[0], testJob)
assert.Equal(res.Truncations[structs.Jobs], false)
assert.NotEqual("0", respW.HeaderMap.Get("X-Nomad-Index"))
require.False(t, res.Truncations[structs.Jobs])
require.NotEqual(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_Search_PUT(t *testing.T) {
assert := assert.New(t)
func TestHTTP_FuzzySearch_POST(t *testing.T) {
t.Parallel()
testJobID := uuid.Generate()
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJobID, s, t)
data := structs.FuzzySearchRequest{Text: "fau", Context: structs.Namespaces}
req, err := http.NewRequest("POST", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.FuzzySearchResponse)
require.Len(t, res.Matches, 1) // searched one context: namespaces
ns := res.Matches[structs.Namespaces]
require.Len(t, ns, 1)
require.Equal(t, "default", ns[0].ID)
require.Nil(t, ns[0].Scope) // only job types have scope
require.False(t, res.Truncations[structs.Jobs])
require.NotEqual(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_PrefixSearch_PUT(t *testing.T) {
t.Parallel()
testJob := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJob, s, t)
data := structs.SearchRequest{Prefix: testJobPrefix, Context: structs.Jobs}
req, err := http.NewRequest("PUT", "/v1/search", encodeReq(data))
assert.Nil(err)
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
assert.Nil(err)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
assert.Equal(1, len(res.Matches))
require.Len(t, res.Matches, 1)
j := res.Matches[structs.Jobs]
require.Len(t, j, 1)
require.Equal(t, testJob, j[0])
assert.Equal(1, len(j))
assert.Equal(j[0], testJob)
assert.Equal(res.Truncations[structs.Jobs], false)
assert.NotEqual("0", respW.HeaderMap.Get("X-Nomad-Index"))
require.False(t, res.Truncations[structs.Jobs])
require.NotEqual(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_Search_MultipleJobs(t *testing.T) {
assert := assert.New(t)
func TestHTTP_FuzzySearch_PUT(t *testing.T) {
t.Parallel()
testJobID := uuid.Generate()
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJobID, s, t)
data := structs.FuzzySearchRequest{Text: "fau", Context: structs.Namespaces}
req, err := http.NewRequest("PUT", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.FuzzySearchResponse)
require.Len(t, res.Matches, 1) // searched one context: namespaces
ns := res.Matches[structs.Namespaces]
require.Len(t, ns, 1)
require.Equal(t, "default", ns[0].ID)
require.Nil(t, ns[0].Scope) // only job types have scope
require.False(t, res.Truncations[structs.Namespaces])
require.NotEqual(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_PrefixSearch_MultipleJobs(t *testing.T) {
t.Parallel()
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)
@@ -116,190 +197,367 @@ func TestHTTP_Search_MultipleJobs(t *testing.T) {
data := structs.SearchRequest{Prefix: testJobPrefix, Context: structs.Jobs}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
assert.Nil(err)
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
assert.Nil(err)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
assert.Equal(1, len(res.Matches))
require.Len(t, res.Matches, 1)
j := res.Matches[structs.Jobs]
require.Len(t, j, 2)
require.Contains(t, j, testJobA)
require.Contains(t, j, testJobB)
require.NotContains(t, j, testJobC)
assert.Equal(2, len(j))
assert.Contains(j, testJobA)
assert.Contains(j, testJobB)
assert.NotContains(j, testJobC)
assert.Equal(res.Truncations[structs.Jobs], false)
assert.NotEqual("0", respW.HeaderMap.Get("X-Nomad-Index"))
require.False(t, res.Truncations[structs.Jobs])
require.NotEqual(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_Search_Evaluation(t *testing.T) {
assert := assert.New(t)
func TestHTTP_FuzzySearch_MultipleJobs(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
job1ID := createCmdJobForTest("job1", "/bin/yes", s, t).ID
job2ID := createCmdJobForTest("job2", "/bin/no", s, t).ID
_ = createCmdJobForTest("job3", "/opt/java", s, t).ID // no match
job4ID := createCmdJobForTest("job4", "/sbin/ping", s, t).ID
data := structs.FuzzySearchRequest{Text: "bin", Context: structs.Jobs}
req, err := http.NewRequest("POST", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
// in example job, only the commands match the "bin" query
res := resp.(structs.FuzzySearchResponse)
require.Len(t, res.Matches, 1)
commands := res.Matches[structs.Commands]
require.Len(t, commands, 3)
exp := []structs.FuzzyMatch{{
ID: "/bin/no",
Scope: []string{"default", job2ID, "web", "web"},
}, {
ID: "/bin/yes",
Scope: []string{"default", job1ID, "web", "web"},
}, {
ID: "/sbin/ping",
Scope: []string{"default", job4ID, "web", "web"},
}}
require.Equal(t, exp, commands)
require.False(t, res.Truncations[structs.Jobs])
require.NotEqual(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_PrefixSearch_Evaluation(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
eval1 := mock.Eval()
eval2 := mock.Eval()
err := state.UpsertEvals(structs.MsgTypeTestSetup, 9000, []*structs.Evaluation{eval1, eval2})
assert.Nil(err)
require.NoError(t, err)
prefix := eval1.ID[:len(eval1.ID)-2]
data := structs.SearchRequest{Prefix: prefix, Context: structs.Evals}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
assert.Nil(err)
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
assert.Nil(err)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
assert.Equal(1, len(res.Matches))
require.Len(t, res.Matches, 1)
j := res.Matches[structs.Evals]
assert.Equal(1, len(j))
assert.Contains(j, eval1.ID)
assert.NotContains(j, eval2.ID)
assert.Equal(res.Truncations[structs.Evals], false)
assert.Equal("9000", respW.HeaderMap.Get("X-Nomad-Index"))
require.Len(t, j, 1)
require.Contains(t, j, eval1.ID)
require.NotContains(t, j, eval2.ID)
require.False(t, res.Truncations[structs.Evals])
require.Equal(t, "9000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_Search_Allocations(t *testing.T) {
assert := assert.New(t)
func TestHTTP_FuzzySearch_Evaluation(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
alloc := mock.Alloc()
eval1 := mock.Eval()
eval2 := mock.Eval()
err := state.UpsertEvals(structs.MsgTypeTestSetup, 9000, []*structs.Evaluation{eval1, eval2})
require.NoError(t, err)
// fuzzy search does prefix search for evaluations
prefix := eval1.ID[:len(eval1.ID)-2]
data := structs.FuzzySearchRequest{Text: prefix, Context: structs.Evals}
req, err := http.NewRequest("POST", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.FuzzySearchResponse)
require.Len(t, res.Matches, 1)
matches := res.Matches[structs.Evals]
require.Len(t, matches, 1)
require.Equal(t, structs.FuzzyMatch{
ID: eval1.ID,
}, matches[0])
require.False(t, res.Truncations[structs.Evals])
require.Equal(t, "9000", header(respW, "X-Nomad-Index"))
})
}
func mockAlloc() *structs.Allocation {
a := mock.Alloc()
a.Name = fmt.Sprintf("%s.%s[%d]", a.Job.Name, "web", 0)
return a
}
func TestHTTP_PrefixSearch_Allocations(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
alloc := mockAlloc()
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 7000, []*structs.Allocation{alloc})
assert.Nil(err)
require.NoError(t, err)
prefix := alloc.ID[:len(alloc.ID)-2]
data := structs.SearchRequest{Prefix: prefix, Context: structs.Allocs}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
assert.Nil(err)
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
assert.Nil(err)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
assert.Equal(1, len(res.Matches))
require.Len(t, res.Matches, 1)
a := res.Matches[structs.Allocs]
assert.Equal(1, len(a))
assert.Contains(a, alloc.ID)
require.Len(t, a, 1)
require.Contains(t, a, alloc.ID)
assert.Equal(res.Truncations[structs.Allocs], false)
assert.Equal("7000", respW.HeaderMap.Get("X-Nomad-Index"))
require.False(t, res.Truncations[structs.Allocs])
require.Equal(t, "7000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_Search_Nodes(t *testing.T) {
assert := assert.New(t)
func TestHTTP_FuzzySearch_Allocations(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
alloc := mockAlloc()
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 7000, []*structs.Allocation{alloc})
require.NoError(t, err)
data := structs.FuzzySearchRequest{Text: "-job", Context: structs.Allocs}
req, err := http.NewRequest("POST", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.FuzzySearchResponse)
require.Len(t, res.Matches, 1)
a := res.Matches[structs.Allocs]
require.Len(t, a, 1)
require.Equal(t, "my-job.web[0]", a[0].ID)
require.False(t, res.Truncations[structs.Allocs])
require.Equal(t, "7000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_PrefixSearch_Nodes(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
node := mock.Node()
err := state.UpsertNode(structs.MsgTypeTestSetup, 6000, node)
assert.Nil(err)
require.NoError(t, err)
prefix := node.ID[:len(node.ID)-2]
data := structs.SearchRequest{Prefix: prefix, Context: structs.Nodes}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
assert.Nil(err)
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
assert.Nil(err)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
assert.Equal(1, len(res.Matches))
require.Len(t, res.Matches, 1)
n := res.Matches[structs.Nodes]
assert.Equal(1, len(n))
assert.Contains(n, node.ID)
require.Len(t, n, 1)
require.Contains(t, n, node.ID)
assert.Equal(res.Truncations[structs.Nodes], false)
assert.Equal("6000", respW.HeaderMap.Get("X-Nomad-Index"))
require.False(t, res.Truncations[structs.Nodes])
require.Equal(t, "6000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_Search_Deployments(t *testing.T) {
assert := assert.New(t)
func TestHTTP_FuzzySearch_Nodes(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
node := mock.Node() // foobar
err := state.UpsertNode(structs.MsgTypeTestSetup, 6000, node)
require.NoError(t, err)
data := structs.FuzzySearchRequest{Text: "oo", Context: structs.Nodes}
req, err := http.NewRequest("POST", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.FuzzySearchResponse)
require.Len(t, res.Matches, 1)
n := res.Matches[structs.Nodes]
require.Len(t, n, 1)
require.Equal(t, "foobar", n[0].ID)
require.False(t, res.Truncations[structs.Nodes])
require.Equal(t, "6000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_PrefixSearch_Deployments(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
deployment := mock.Deployment()
assert.Nil(state.UpsertDeployment(999, deployment), "UpsertDeployment")
require.NoError(t, state.UpsertDeployment(999, deployment), "UpsertDeployment")
prefix := deployment.ID[:len(deployment.ID)-2]
data := structs.SearchRequest{Prefix: prefix, Context: structs.Deployments}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
assert.Nil(err)
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
assert.Nil(err)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
assert.Equal(1, len(res.Matches))
require.Len(t, res.Matches, 1)
n := res.Matches[structs.Deployments]
assert.Equal(1, len(n))
assert.Contains(n, deployment.ID)
assert.Equal("999", respW.HeaderMap.Get("X-Nomad-Index"))
require.Len(t, n, 1)
require.Contains(t, n, deployment.ID)
require.Equal(t, "999", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_Search_NoJob(t *testing.T) {
assert := assert.New(t)
func TestHTTP_FuzzySearch_Deployments(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
deployment := mock.Deployment()
require.NoError(t, state.UpsertDeployment(999, deployment), "UpsertDeployment")
// fuzzy search of deployments are prefix searches
prefix := deployment.ID[:len(deployment.ID)-2]
data := structs.FuzzySearchRequest{Text: prefix, Context: structs.Deployments}
req, err := http.NewRequest("POST", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.FuzzySearchResponse)
require.Len(t, res.Matches, 1)
n := res.Matches[structs.Deployments]
require.Len(t, n, 1)
require.Equal(t, deployment.ID, n[0].ID)
require.Equal(t, "999", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_PrefixSearch_NoJob(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
data := structs.SearchRequest{Prefix: "12345", Context: structs.Jobs}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
assert.Nil(err)
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
assert.Nil(err)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
assert.Equal(1, len(res.Matches))
assert.Equal(0, len(res.Matches[structs.Jobs]))
assert.Equal("0", respW.HeaderMap.Get("X-Nomad-Index"))
require.Len(t, res.Matches, 1)
require.Len(t, res.Matches[structs.Jobs], 0)
require.Equal(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_Search_AllContext(t *testing.T) {
assert := assert.New(t)
func TestHTTP_FuzzySearch_NoJob(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
data := structs.FuzzySearchRequest{Text: "12345", Context: structs.Jobs}
req, err := http.NewRequest("POST", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.FuzzySearchResponse)
require.Len(t, res.Matches, 0)
require.Equal(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_PrefixSearch_AllContext(t *testing.T) {
t.Parallel()
testJobID := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJobID, s, t)
@@ -307,28 +565,59 @@ func TestHTTP_Search_AllContext(t *testing.T) {
eval1 := mock.Eval()
eval1.ID = testJobID
err := state.UpsertEvals(structs.MsgTypeTestSetup, 8000, []*structs.Evaluation{eval1})
assert.Nil(err)
require.NoError(t, err)
data := structs.SearchRequest{Prefix: testJobPrefix, Context: structs.All}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
assert.Nil(err)
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
assert.Nil(err)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
matchedJobs := res.Matches[structs.Jobs]
matchedEvals := res.Matches[structs.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"))
require.Len(t, matchedJobs, 1)
require.Len(t, matchedEvals, 1)
require.Equal(t, testJobID, matchedJobs[0])
require.Equal(t, eval1.ID, matchedEvals[0])
require.Equal(t, "8000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_FuzzySearch_AllContext(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
jobID := createCmdJobForTest("job1", "/bin/aardvark", s, t).ID
state := s.Agent.server.State()
eval1 := mock.Eval()
eval1.ID = "aaaa6573-04cb-61b4-04cb-865aaaf5d400"
err := state.UpsertEvals(structs.MsgTypeTestSetup, 8000, []*structs.Evaluation{eval1})
require.NoError(t, err)
data := structs.FuzzySearchRequest{Text: "aa", Context: structs.All}
req, err := http.NewRequest("POST", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.FuzzySearchResponse)
matchedCommands := res.Matches[structs.Commands]
matchedEvals := res.Matches[structs.Evals]
require.Len(t, matchedCommands, 1)
require.Len(t, matchedEvals, 1)
require.Equal(t, eval1.ID, matchedEvals[0].ID)
require.Equal(t, "/bin/aardvark", matchedCommands[0].ID)
require.Equal(t, []string{
"default", jobID, "web", "web",
}, matchedCommands[0].Scope)
require.Equal(t, "8000", header(respW, "X-Nomad-Index"))
})
}