open-nomad/command/agent/search_endpoint_test.go
Seth Hoenig ba728f8f97
api: enable support for setting original job source (#16763)
* api: enable support for setting original source alongside job

This PR adds support for setting job source material along with
the registration of a job.

This includes a new HTTP endpoint and a new RPC endpoint for
making queries for the original source of a job. The
HTTP endpoint is /v1/job/<id>/submission?version=<version> and
the RPC method is Job.GetJobSubmission.

The job source (if submitted, and doing so is always optional), is
stored in the job_submission memdb table, separately from the
actual job. This way we do not incur overhead of reading the large
string field throughout normal job operations.

The server config now includes job_max_source_size for configuring
the maximum size the job source may be, before the server simply
drops the source material. This should help prevent Bad Things from
happening when huge jobs are submitted. If the value is set to 0,
all job source material will be dropped.

* api: avoid writing var content to disk for parsing

* api: move submission validation into RPC layer

* api: return an error if updating a job submission without namespace or job id

* api: be exact about the job index we associate a submission with (modify)

* api: reword api docs scheduling

* api: prune all but the last 6 job submissions

* api: protect against nil job submission in job validation

* api: set max job source size in test server

* api: fixups from pr
2023-04-11 08:45:08 -05:00

939 lines
26 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package agent
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/stretchr/testify/require"
)
func header(recorder *httptest.ResponseRecorder, name string) string {
return recorder.Result().Header.Get(name)
}
func createJobForTest(jobID string, s *TestAgent, t *testing.T) {
job := mock.Job()
job.ID = jobID
job.TaskGroups[0].Count = 1
state := s.Agent.server.State()
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
require.NoError(t, err)
}
func TestHTTP_PrefixSearchWithIllegalMethod(t *testing.T) {
ci.Parallel(t)
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) {
ci.Parallel(t)
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, nil, job)
require.NoError(t, err)
return job
}
func TestHTTP_PrefixSearch_POST(t *testing.T) {
ci.Parallel(t)
testJob := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
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))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
require.Len(t, res.Matches, 1)
j := res.Matches[structs.Jobs]
require.Len(t, j, 1)
require.Equal(t, testJob, j[0])
require.False(t, res.Truncations[structs.Jobs])
require.NotEqual(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_FuzzySearch_POST(t *testing.T) {
ci.Parallel(t)
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) {
ci.Parallel(t)
testJob := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
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))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
require.Len(t, res.Matches, 1)
j := res.Matches[structs.Jobs]
require.Len(t, j, 1)
require.Equal(t, testJob, j[0])
require.False(t, res.Truncations[structs.Jobs])
require.NotEqual(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_FuzzySearch_PUT(t *testing.T) {
ci.Parallel(t)
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) {
ci.Parallel(t)
testJobA := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobB := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89707"
testJobC := "bbbbbbbb-e8f7-fd38-c855-ab94ceb89707"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJobA, s, t)
createJobForTest(testJobB, s, t)
createJobForTest(testJobC, s, t)
data := structs.SearchRequest{Prefix: testJobPrefix, Context: structs.Jobs}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
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)
require.False(t, res.Truncations[structs.Jobs])
require.NotEqual(t, "0", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_FuzzySearch_MultipleJobs(t *testing.T) {
ci.Parallel(t)
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) {
ci.Parallel(t)
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})
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))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
require.Len(t, res.Matches, 1)
j := res.Matches[structs.Evals]
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_FuzzySearch_Evaluation(t *testing.T) {
ci.Parallel(t)
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})
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) {
ci.Parallel(t)
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)
prefix := alloc.ID[:len(alloc.ID)-2]
data := structs.SearchRequest{Prefix: prefix, Context: structs.Allocs}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
require.Len(t, res.Matches, 1)
a := res.Matches[structs.Allocs]
require.Len(t, a, 1)
require.Contains(t, a, alloc.ID)
require.False(t, res.Truncations[structs.Allocs])
require.Equal(t, "7000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_FuzzySearch_Allocations(t *testing.T) {
ci.Parallel(t)
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) {
ci.Parallel(t)
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
node := mock.Node()
err := state.UpsertNode(structs.MsgTypeTestSetup, 6000, node)
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))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
require.Len(t, res.Matches, 1)
n := res.Matches[structs.Nodes]
require.Len(t, n, 1)
require.Contains(t, n, node.ID)
require.False(t, res.Truncations[structs.Nodes])
require.Equal(t, "6000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_FuzzySearch_Nodes(t *testing.T) {
ci.Parallel(t)
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) {
ci.Parallel(t)
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
deployment := mock.Deployment()
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))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
require.Len(t, res.Matches, 1)
n := res.Matches[structs.Deployments]
require.Len(t, n, 1)
require.Contains(t, n, deployment.ID)
require.Equal(t, "999", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_FuzzySearch_Deployments(t *testing.T) {
ci.Parallel(t)
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) {
ci.Parallel(t)
httpTest(t, nil, func(s *TestAgent) {
data := structs.SearchRequest{Prefix: "12345", Context: structs.Jobs}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
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_FuzzySearch_NoJob(t *testing.T) {
ci.Parallel(t)
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) {
ci.Parallel(t)
testJobID := "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
testJobPrefix := "aaaaaaaa-e8f7-fd38"
httpTest(t, nil, func(s *TestAgent) {
createJobForTest(testJobID, s, t)
state := s.Agent.server.State()
eval1 := mock.Eval()
eval1.ID = testJobID
err := state.UpsertEvals(structs.MsgTypeTestSetup, 8000, []*structs.Evaluation{eval1})
require.NoError(t, err)
data := structs.SearchRequest{Prefix: testJobPrefix, Context: structs.All}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
matchedJobs := res.Matches[structs.Jobs]
matchedEvals := res.Matches[structs.Evals]
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) {
ci.Parallel(t)
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"))
})
}
func TestHTTP_PrefixSearch_Variables(t *testing.T) {
ci.Parallel(t)
testPath := "alpha/beta/charlie"
testPathPrefix := "alpha/beta"
httpTest(t, nil, func(s *TestAgent) {
sv := mock.VariableEncrypted()
state := s.Agent.server.State()
sv.Path = testPath
setResp := state.VarSet(8000, &structs.VarApplyStateRequest{
Op: structs.VarOpSet,
Var: sv,
})
require.NoError(t, setResp.Error)
data := structs.SearchRequest{Prefix: testPathPrefix, Context: structs.Variables}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.SearchResponse)
matchedVars := res.Matches[structs.Variables]
require.Len(t, matchedVars, 1)
require.Equal(t, testPath, matchedVars[0])
require.Equal(t, "8000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_FuzzySearch_Variables(t *testing.T) {
ci.Parallel(t)
testPath := "alpha/beta/charlie"
testPathText := "beta"
httpTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
sv := mock.VariableEncrypted()
sv.Path = testPath
setResp := state.VarSet(8000, &structs.VarApplyStateRequest{
Op: structs.VarOpSet,
Var: sv,
})
require.NoError(t, setResp.Error)
data := structs.FuzzySearchRequest{Text: testPathText, Context: structs.Variables}
req, err := http.NewRequest("POST", "/v1/search/", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
require.NoError(t, err)
res := resp.(structs.FuzzySearchResponse)
matchedVars := res.Matches[structs.Variables]
require.Len(t, matchedVars, 1)
require.Equal(t, testPath, matchedVars[0].ID)
require.Equal(t, []string{
"default", testPath,
}, matchedVars[0].Scope)
require.Equal(t, "8000", header(respW, "X-Nomad-Index"))
})
}
func TestHTTP_PrefixSearch_Variables_ACL(t *testing.T) {
ci.Parallel(t)
testPath := "alpha/beta/charlie"
testPathPrefix := "alpha/beta"
httpACLTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
ns := mock.Namespace()
sv1 := mock.VariableEncrypted()
sv1.Path = testPath
sv2 := sv1.Copy()
sv2.Namespace = ns.Name
require.NoError(t, state.UpsertNamespaces(7000, []*structs.Namespace{ns}))
setResp := state.VarSet(8000, &structs.VarApplyStateRequest{
Op: structs.VarOpSet,
Var: sv1,
})
require.NoError(t, setResp.Error)
setResp = state.VarSet(8001, &structs.VarApplyStateRequest{
Op: structs.VarOpSet,
Var: &sv2,
})
require.NoError(t, setResp.Error)
rootToken := s.RootToken
defNSToken := mock.CreatePolicyAndToken(t, state, 8002, "default",
mock.NamespacePolicyWithVariables(
"default", "read", []string{},
map[string][]string{"*": {"read", "list"}}))
ns1NSToken := mock.CreatePolicyAndToken(t, state, 8004, "ns-"+ns.Name,
mock.NamespacePolicyWithVariables(
ns.Name, "read", []string{},
map[string][]string{"*": {"read", "list"}}))
denyToken := mock.CreatePolicyAndToken(t, state, 8006, "none",
mock.NamespacePolicy("default", "deny", nil))
testCases := []struct {
desc string
token *structs.ACLToken
namespace string
expectedCount int
expectedNamespaces []string
expectedErr string
}{
{
desc: "management token",
token: rootToken,
namespace: "*",
expectedCount: 2,
expectedNamespaces: []string{"default", ns.Name},
},
{
desc: "default ns token",
token: defNSToken,
namespace: "default",
expectedCount: 1,
expectedNamespaces: []string{"default"},
},
{
desc: "ns specific token",
token: ns1NSToken,
namespace: ns.Name,
expectedCount: 1,
expectedNamespaces: []string{ns.Name},
},
{
desc: "denied token",
token: denyToken,
namespace: "default",
expectedCount: 0,
expectedErr: structs.ErrPermissionDenied.Error(),
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
tC := tC
data := structs.SearchRequest{
Prefix: testPathPrefix,
Context: structs.Variables,
QueryOptions: structs.QueryOptions{
AuthToken: tC.token.SecretID,
Namespace: tC.namespace,
},
}
req, err := http.NewRequest("POST", "/v1/search", encodeReq(data))
require.NoError(t, err)
respW := httptest.NewRecorder()
resp, err := s.Server.SearchRequest(respW, req)
if tC.expectedErr != "" {
require.Error(t, err)
require.Equal(t, tC.expectedErr, err.Error())
return
}
require.NoError(t, err)
res := resp.(structs.SearchResponse)
matchedVars := res.Matches[structs.Variables]
require.Len(t, matchedVars, tC.expectedCount)
for _, mv := range matchedVars {
require.Equal(t, testPath, mv)
}
require.Equal(t, "8001", header(respW, "X-Nomad-Index"))
})
}
})
}
func TestHTTP_FuzzySearch_Variables_ACL(t *testing.T) {
ci.Parallel(t)
testPath := "alpha/beta/charlie"
testPathText := "beta"
httpACLTest(t, nil, func(s *TestAgent) {
state := s.Agent.server.State()
ns := mock.Namespace()
sv1 := mock.VariableEncrypted()
sv1.Path = testPath
sv2 := sv1.Copy()
sv2.Namespace = ns.Name
require.NoError(t, state.UpsertNamespaces(7000, []*structs.Namespace{ns}))
setResp := state.VarSet(8000, &structs.VarApplyStateRequest{
Op: structs.VarOpSet,
Var: sv1,
})
require.NoError(t, setResp.Error)
setResp = state.VarSet(8001, &structs.VarApplyStateRequest{
Op: structs.VarOpSet,
Var: &sv2,
})
require.NoError(t, setResp.Error)
rootToken := s.RootToken
defNSToken := mock.CreatePolicyAndToken(t, state, 8002, "default",
mock.NamespacePolicyWithVariables(
"default", "read", []string{},
map[string][]string{"*": {"list"}}))
ns1NSToken := mock.CreatePolicyAndToken(t, state, 8004, "ns-"+ns.Name,
mock.NamespacePolicyWithVariables(
ns.Name, "read", []string{},
map[string][]string{"*": {"list"}}))
denyToken := mock.CreatePolicyAndToken(t, state, 8006, "none",
mock.NamespacePolicy("default", "deny", nil))
type testCase struct {
desc string
token *structs.ACLToken
namespace string
expectedCount int
expectedNamespaces []string
expectedErr string
}
testCases := []testCase{
{
desc: "management token",
token: rootToken,
expectedCount: 2,
expectedNamespaces: []string{"default", ns.Name},
},
{
desc: "default ns token",
token: defNSToken,
expectedCount: 1,
expectedNamespaces: []string{"default"},
},
{
desc: "ns specific token",
token: ns1NSToken,
expectedCount: 1,
expectedNamespaces: []string{ns.Name},
},
{
desc: "denied token",
token: denyToken,
expectedCount: 0,
// You would think that this should error out, but when it is
// the wildcard namespace, objects that fail the access check
// are filtered out rather than throwing a permissions error.
},
{
desc: "denied token",
token: denyToken,
namespace: "default",
expectedCount: 0,
expectedErr: structs.ErrPermissionDenied.Error(),
},
}
tcNS := func(tC testCase) string {
if tC.namespace == "" {
return "*"
}
return tC.namespace
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
data := structs.FuzzySearchRequest{
Text: testPathText,
Context: structs.Variables,
QueryOptions: structs.QueryOptions{
AuthToken: tC.token.SecretID,
Namespace: tcNS(tC),
},
}
req, err := http.NewRequest("POST", "/v1/search/fuzzy", encodeReq(data))
require.NoError(t, err)
setToken(req, tC.token)
respW := httptest.NewRecorder()
resp, err := s.Server.FuzzySearchRequest(respW, req)
if tC.expectedErr != "" {
require.Error(t, err)
require.Equal(t, tC.expectedErr, err.Error())
return
}
res := resp.(structs.FuzzySearchResponse)
matchedVars := res.Matches[structs.Variables]
require.Len(t, matchedVars, tC.expectedCount)
for _, mv := range matchedVars {
require.Equal(t, testPath, mv.ID)
require.Len(t, mv.Scope, 2)
require.Contains(t, tC.expectedNamespaces, mv.Scope[0])
require.Equal(t, "8001", header(respW, "X-Nomad-Index"))
}
})
}
})
}