e8bfef8148
ACL permissions for the search endpoints are done in three passes. The first (the `sufficientSearchPerms` method) is for performance and coarsely rejects requests based on the passed-in context parameter if the user has no permissions to any object in that context. The second (the `filteredSearchContexts` method) filters out contexts based on whether the user has permissions either to the requested namespace or again by context (to catch the "all" context). Finally, when iterating over the objects available, we do the usual filtering in the iterator. Internal testing found several bugs in this filtering: * CSI plugins can be searched by any authenticated user. * Variables can be searched if the user has `job:read` permissions to the variable's namespace instead of `variable:list`. * Variables cannot be searched by wildcard namespace. This is an information leak of the plugin names and variable paths, which we don't consider to be privileged information but intended to protect anyways. This changeset fixes these bugs by ensuring CSI plugins are filtered in the 1st and 2nd pass ACL filters, and changes variables to check `variable:list` in the 2nd pass filter unless the wildcard namespace is passed (at which point we'll fallback to filtering in the iterator). Fixes: CVE-2023-3300 Fixes: #17906
2602 lines
78 KiB
Go
2602 lines
78 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package nomad
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
|
"github.com/hashicorp/nomad/acl"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
"github.com/hashicorp/nomad/nomad/state"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/hashicorp/nomad/testutil"
|
|
"github.com/shoenig/test/must"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
const jobIndex = 1000
|
|
|
|
func registerMockJob(s *Server, t *testing.T, prefix string, counter int) *structs.Job {
|
|
job := mock.Job()
|
|
job.ID = prefix + strconv.Itoa(counter)
|
|
registerJob(s, t, job)
|
|
return job
|
|
}
|
|
|
|
func registerJob(s *Server, t *testing.T, job *structs.Job) {
|
|
fsmState := s.fsm.State()
|
|
require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, jobIndex, nil, job))
|
|
}
|
|
|
|
func mockAlloc() *structs.Allocation {
|
|
a := mock.Alloc()
|
|
a.Name = fmt.Sprintf("%s.%s[%d]", a.Job.Name, "web", 0)
|
|
return a
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_Job(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
job := registerMockJob(s, t, prefix, 0)
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_ACL(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
jobID := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"
|
|
|
|
s, root, cleanupS := TestACLServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
store := s.fsm.State()
|
|
|
|
ns := mock.Namespace()
|
|
ns.Name = "not-allowed"
|
|
must.NoError(t, store.UpsertNamespaces(10, []*structs.Namespace{ns}))
|
|
|
|
job := registerMockJob(s, t, jobID, 0)
|
|
|
|
variable := mock.VariableEncrypted()
|
|
resp := store.VarSet(1001, &structs.VarApplyStateRequest{
|
|
Op: structs.VarOpSet,
|
|
Var: variable,
|
|
})
|
|
must.NoError(t, resp.Error)
|
|
|
|
plugin := mock.CSIPlugin()
|
|
must.NoError(t, store.UpsertCSIPlugin(1002, plugin))
|
|
|
|
node := mock.Node()
|
|
must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1003, node))
|
|
|
|
disallowedVariable := mock.VariableEncrypted()
|
|
disallowedVariable.Namespace = "not-allowed"
|
|
resp = store.VarSet(2001, &structs.VarApplyStateRequest{
|
|
Op: structs.VarOpSet,
|
|
Var: disallowedVariable,
|
|
})
|
|
must.NoError(t, resp.Error)
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: "",
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
// Try without a token and expect failure
|
|
{
|
|
var resp structs.SearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
|
|
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with an invalid token and expect failure
|
|
{
|
|
invalidToken := mock.CreatePolicyAndToken(t, store, 1003, "test-invalid",
|
|
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
|
|
req.AuthToken = invalidToken.SecretID
|
|
var resp structs.SearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
|
|
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with a node:read token and expect failure due to Jobs being the context
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1005, "test-invalid2", mock.NodePolicy(acl.PolicyRead))
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.SearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
|
|
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with a node:read token and expect success due to All context
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1007, "test-valid", mock.NodePolicy(acl.PolicyRead))
|
|
req.Context = structs.All
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
|
|
|
|
// Jobs, Plugins, and Variables filtered out since token only has access
|
|
// to node:read
|
|
must.SliceEmpty(t, resp.Matches[structs.Jobs])
|
|
must.SliceEmpty(t, resp.Matches[structs.Plugins])
|
|
must.SliceEmpty(t, resp.Matches[structs.Variables])
|
|
|
|
must.Eq(t, uint64(1003), resp.Index) // index of node
|
|
}
|
|
|
|
// Try with a valid token for namespace:read-job
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1009, "test-valid2",
|
|
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}))
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
must.Eq(t, []string{job.ID}, resp.Matches[structs.Jobs])
|
|
|
|
// Nodes, Plugins, and Variables filtered out since token only has
|
|
// access to namespace:read-job
|
|
must.SliceEmpty(t, resp.Matches[structs.Nodes])
|
|
must.SliceEmpty(t, resp.Matches[structs.Plugins])
|
|
must.SliceEmpty(t, resp.Matches[structs.Variables])
|
|
|
|
// Index of job because all other contexts are filtered out
|
|
must.Eq(t, uint64(1000), resp.Index)
|
|
}
|
|
|
|
// Try with a valid token for node:read and namespace:read-job
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1011, "test-valid3", strings.Join([]string{
|
|
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
|
|
mock.NodePolicy(acl.PolicyRead),
|
|
}, "\n"))
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
must.Eq(t, []string{job.ID}, resp.Matches[structs.Jobs])
|
|
must.SliceEmpty(t, resp.Matches[structs.Plugins])
|
|
must.SliceEmpty(t, resp.Matches[structs.Variables])
|
|
must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
|
|
must.Eq(t, uint64(1003), resp.Index) // index of node
|
|
}
|
|
|
|
// Try with a valid token for node:read and namespace:variable:read
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1012, "test-valid4", strings.Join([]string{
|
|
mock.NamespacePolicyWithVariables(structs.DefaultNamespace, "", []string{},
|
|
map[string][]string{"*": []string{"list"}}),
|
|
mock.NodePolicy(acl.PolicyRead),
|
|
}, "\n"))
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
must.SliceEmpty(t, resp.Matches[structs.Jobs])
|
|
must.SliceEmpty(t, resp.Matches[structs.Plugins])
|
|
must.Eq(t, []string{variable.Path}, resp.Matches[structs.Variables])
|
|
must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
|
|
must.Eq(t, uint64(2001), resp.Index) // index of variables
|
|
}
|
|
|
|
// Try with a valid token for node:read and namespace:variable:read, wildcard ns
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1012, "test-valid4", strings.Join([]string{
|
|
mock.NamespacePolicyWithVariables(structs.DefaultNamespace, "", []string{},
|
|
map[string][]string{"*": []string{"list"}}),
|
|
mock.NodePolicy(acl.PolicyRead),
|
|
}, "\n"))
|
|
req.AuthToken = validToken.SecretID
|
|
req.Namespace = structs.AllNamespacesSentinel
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
must.SliceEmpty(t, resp.Matches[structs.Jobs])
|
|
must.SliceEmpty(t, resp.Matches[structs.Plugins])
|
|
must.Eq(t, []string{variable.Path}, resp.Matches[structs.Variables])
|
|
must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
|
|
must.Eq(t, uint64(2001), resp.Index) // index of variables
|
|
}
|
|
|
|
// Try with a management token
|
|
{
|
|
req.AuthToken = root.SecretID
|
|
req.Namespace = structs.DefaultNamespace
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
must.Eq(t, []string{job.ID}, resp.Matches[structs.Jobs])
|
|
must.Eq(t, []string{plugin.ID}, resp.Matches[structs.Plugins])
|
|
|
|
expectVars := []string{variable.Path, disallowedVariable.Path}
|
|
slices.Sort(expectVars)
|
|
slices.Sort(resp.Matches[structs.Variables])
|
|
must.Eq(t, expectVars, resp.Matches[structs.Variables])
|
|
must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
|
|
must.Eq(t, uint64(2001), resp.Index) // highest index
|
|
}
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_All_JobWithHyphen(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
prefix := "example-test-------" // Assert that a job with more than 4 hyphens works
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Register a job and an allocation
|
|
job := registerMockJob(s, t, prefix, 0)
|
|
alloc := mockAlloc()
|
|
alloc.JobID = job.ID
|
|
alloc.Namespace = job.Namespace
|
|
summary := mock.JobSummary(alloc.JobID)
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertJobSummary(999, summary))
|
|
require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}))
|
|
|
|
req := &structs.SearchRequest{
|
|
Context: structs.All,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
// req.Prefix = "example-te": 9
|
|
for i := 1; i < len(prefix); i++ {
|
|
req.Prefix = prefix[:i]
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
require.Equal(t, 1, len(resp.Matches[structs.Jobs]))
|
|
require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
|
|
require.EqualValues(t, jobIndex, resp.Index)
|
|
}
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_All_LongJob(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
prefix := strings.Repeat("a", 100)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Register a job and an allocation
|
|
job := registerMockJob(s, t, prefix, 0)
|
|
alloc := mockAlloc()
|
|
alloc.JobID = job.ID
|
|
summary := mock.JobSummary(alloc.JobID)
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertJobSummary(999, summary))
|
|
require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}))
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.All,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
|
|
require.EqualValues(t, jobIndex, resp.Index)
|
|
}
|
|
|
|
// truncate should limit results to 20
|
|
func TestSearch_PrefixSearch_Truncate(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
for counter := 0; counter < 25; counter++ {
|
|
registerMockJob(s, t, prefix, counter)
|
|
}
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: "default",
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Jobs], 20)
|
|
require.True(t, resp.Truncations[structs.Jobs])
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_AllWithJob(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
job := registerMockJob(s, t, prefix, 0)
|
|
eval1 := mock.Eval()
|
|
eval1.ID = job.ID
|
|
require.NoError(t, s.fsm.State().UpsertEvals(structs.MsgTypeTestSetup, 2000, []*structs.Evaluation{eval1}))
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.All,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
|
|
require.Len(t, resp.Matches[structs.Evals], 1)
|
|
require.Equal(t, eval1.ID, resp.Matches[structs.Evals][0])
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_Evals(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
eval1 := mock.Eval()
|
|
require.NoError(t, s.fsm.State().UpsertEvals(structs.MsgTypeTestSetup, 2000, []*structs.Evaluation{eval1}))
|
|
|
|
prefix := eval1.ID[:len(eval1.ID)-2]
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Evals,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: eval1.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Evals], 1)
|
|
require.Equal(t, eval1.ID, resp.Matches[structs.Evals][0])
|
|
require.False(t, resp.Truncations[structs.Evals])
|
|
require.Equal(t, uint64(2000), resp.Index)
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_Allocation(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
alloc := mockAlloc()
|
|
summary := mock.JobSummary(alloc.JobID)
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertJobSummary(999, summary))
|
|
require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 90, []*structs.Allocation{alloc}))
|
|
|
|
prefix := alloc.ID[:len(alloc.ID)-2]
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Allocs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: alloc.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Allocs], 1)
|
|
require.Equal(t, alloc.ID, resp.Matches[structs.Allocs][0])
|
|
require.False(t, resp.Truncations[structs.Allocs])
|
|
require.Equal(t, uint64(90), resp.Index)
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_All_UUID(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
alloc := mockAlloc()
|
|
summary := mock.JobSummary(alloc.JobID)
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertJobSummary(999, summary))
|
|
require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}))
|
|
|
|
node := mock.Node()
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, node))
|
|
|
|
eval1 := mock.Eval()
|
|
eval1.ID = node.ID
|
|
require.NoError(t, fsmState.UpsertEvals(structs.MsgTypeTestSetup, 1002, []*structs.Evaluation{eval1}))
|
|
|
|
req := &structs.SearchRequest{
|
|
Context: structs.All,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: eval1.Namespace,
|
|
},
|
|
}
|
|
|
|
for i := 1; i < len(alloc.ID); i++ {
|
|
req.Prefix = alloc.ID[:i]
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Allocs], 1)
|
|
require.Equal(t, alloc.ID, resp.Matches[structs.Allocs][0])
|
|
require.False(t, resp.Truncations[structs.Allocs])
|
|
require.EqualValues(t, 1002, resp.Index)
|
|
}
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_Node(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
fsmState := s.fsm.State()
|
|
node := mock.Node()
|
|
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 100, node))
|
|
|
|
prefix := node.ID[:len(node.ID)-2]
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Nodes,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: structs.DefaultNamespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
require.Len(t, resp.Matches[structs.Nodes], 1)
|
|
require.Equal(t, node.ID, resp.Matches[structs.Nodes][0])
|
|
require.False(t, resp.Truncations[structs.Nodes])
|
|
require.Equal(t, uint64(100), resp.Index)
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_NodePool(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Start test server.
|
|
s, cleanupS := TestServer(t, nil)
|
|
defer cleanupS()
|
|
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Populate state with test node pools.
|
|
fsmState := s.fsm.State()
|
|
dev1 := &structs.NodePool{Name: "dev-1"}
|
|
dev2 := &structs.NodePool{Name: "dev-2"}
|
|
prod := &structs.NodePool{Name: "prod"}
|
|
|
|
err := fsmState.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{dev1, dev2, prod})
|
|
must.NoError(t, err)
|
|
|
|
// Run test cases.
|
|
testCases := []struct {
|
|
name string
|
|
prefix string
|
|
context structs.Context
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "prefix match",
|
|
prefix: "dev",
|
|
context: structs.NodePools,
|
|
expected: []string{dev1.Name, dev2.Name},
|
|
},
|
|
{
|
|
name: "prefix match - all",
|
|
prefix: "dev",
|
|
context: structs.All,
|
|
expected: []string{dev1.Name, dev2.Name},
|
|
},
|
|
{
|
|
name: "empty prefix",
|
|
prefix: "",
|
|
context: structs.NodePools,
|
|
expected: []string{
|
|
structs.NodePoolAll, structs.NodePoolDefault,
|
|
dev1.Name, dev2.Name, prod.Name,
|
|
},
|
|
},
|
|
{
|
|
name: "other context",
|
|
prefix: "dev",
|
|
context: structs.Jobs,
|
|
expected: []string{},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := &structs.SearchRequest{
|
|
Prefix: tc.prefix,
|
|
Context: tc.context,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
var resp structs.SearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
|
|
must.NoError(t, err)
|
|
must.Len(t, len(tc.expected), resp.Matches[structs.NodePools])
|
|
|
|
for k, v := range resp.Matches {
|
|
switch k {
|
|
case structs.NodePools:
|
|
must.SliceContainsAll(t, v, tc.expected)
|
|
default:
|
|
must.Len(t, 0, v, must.Sprintf("found %d results in %v: %v", len(v), k, v))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_NodePool_ACL(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Start test server with ACL.
|
|
s, root, cleanupS := TestACLServer(t, nil)
|
|
defer cleanupS()
|
|
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Populate state with test node pools and ACL policies.
|
|
fsmState := s.fsm.State()
|
|
|
|
dev1 := &structs.NodePool{Name: "dev-1"}
|
|
dev2 := &structs.NodePool{Name: "dev-2"}
|
|
prod := &structs.NodePool{Name: "prod"}
|
|
err := fsmState.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{dev1, dev2, prod})
|
|
must.NoError(t, err)
|
|
|
|
devToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1001, "dev-node-pools",
|
|
mock.NodePoolPolicy("dev-*", "read", nil),
|
|
)
|
|
noPolicyToken := mock.CreateToken(t, s.fsm.State(), 1003, nil)
|
|
allPoolsToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1005, "all-node-pools",
|
|
mock.NodePoolPolicy("*", "read", nil),
|
|
)
|
|
denyDevToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1007, "deny-dev-node-pools",
|
|
mock.NodePoolPolicy("dev-*", "deny", nil),
|
|
)
|
|
|
|
// Run test cases.
|
|
testCases := []struct {
|
|
name string
|
|
token string
|
|
prefix string
|
|
expected []string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "management token has access to all",
|
|
token: root.SecretID,
|
|
prefix: "",
|
|
expected: []string{
|
|
structs.NodePoolAll, structs.NodePoolDefault,
|
|
dev1.Name, dev2.Name, prod.Name,
|
|
},
|
|
},
|
|
{
|
|
name: "all pools access",
|
|
token: allPoolsToken.SecretID,
|
|
prefix: "",
|
|
expected: []string{
|
|
structs.NodePoolAll, structs.NodePoolDefault,
|
|
dev1.Name, dev2.Name, prod.Name,
|
|
},
|
|
},
|
|
{
|
|
name: "only return what token has access",
|
|
token: devToken.SecretID,
|
|
prefix: "dev",
|
|
expected: []string{dev1.Name, dev2.Name},
|
|
},
|
|
{
|
|
name: "no results if token doesn't have access",
|
|
token: devToken.SecretID,
|
|
prefix: "prod",
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "no results if token is denied",
|
|
token: denyDevToken.SecretID,
|
|
prefix: "dev",
|
|
expectedErr: structs.ErrPermissionDenied.Error(),
|
|
},
|
|
{
|
|
name: "no policy",
|
|
token: noPolicyToken.SecretID,
|
|
prefix: "",
|
|
expectedErr: structs.ErrPermissionDenied.Error(),
|
|
},
|
|
{
|
|
name: "no token",
|
|
token: "",
|
|
prefix: "",
|
|
expectedErr: structs.ErrPermissionDenied.Error(),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := &structs.SearchRequest{
|
|
Prefix: tc.prefix,
|
|
Context: structs.NodePools,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: tc.token,
|
|
},
|
|
}
|
|
var resp structs.SearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
|
|
if tc.expectedErr != "" {
|
|
must.ErrorContains(t, err, tc.expectedErr)
|
|
return
|
|
}
|
|
|
|
must.NoError(t, err)
|
|
must.Len(t, len(tc.expected), resp.Matches[structs.NodePools])
|
|
|
|
for k, v := range resp.Matches {
|
|
switch k {
|
|
case structs.NodePools:
|
|
must.SliceContainsAll(t, v, tc.expected)
|
|
default:
|
|
must.Len(t, 0, v, must.Sprintf("found %d results in %v: %v", len(v), k, v))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_Deployment(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
deployment := mock.Deployment()
|
|
require.NoError(t, s.fsm.State().UpsertDeployment(2000, deployment))
|
|
|
|
prefix := deployment.ID[:len(deployment.ID)-2]
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Deployments,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: deployment.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Deployments], 1)
|
|
require.Equal(t, deployment.ID, resp.Matches[structs.Deployments][0])
|
|
require.False(t, resp.Truncations[structs.Deployments])
|
|
require.Equal(t, uint64(2000), resp.Index)
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_AllContext(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
fsmState := s.fsm.State()
|
|
node := mock.Node()
|
|
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 100, node))
|
|
|
|
eval1 := mock.Eval()
|
|
eval1.ID = node.ID
|
|
require.NoError(t, fsmState.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1}))
|
|
|
|
prefix := node.ID[:len(node.ID)-2]
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.All,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: eval1.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Nodes], 1)
|
|
require.Len(t, resp.Matches[structs.Evals], 1)
|
|
require.Equal(t, node.ID, resp.Matches[structs.Nodes][0])
|
|
require.Equal(t, eval1.ID, resp.Matches[structs.Evals][0])
|
|
require.Equal(t, uint64(1000), resp.Index)
|
|
}
|
|
|
|
// Tests that the top 20 matches are returned when no prefix is set
|
|
func TestSearch_PrefixSearch_NoPrefix(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
job := registerMockJob(s, t, prefix, 0)
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: "",
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
}
|
|
|
|
// Tests that the zero matches are returned when a prefix has no matching
|
|
// results
|
|
func TestSearch_PrefixSearch_NoMatches(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: structs.DefaultNamespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
require.Empty(t, resp.Matches[structs.Jobs])
|
|
require.Equal(t, uint64(0), resp.Index)
|
|
}
|
|
|
|
// Prefixes can only be looked up if their length is a power of two. For
|
|
// prefixes which are an odd length, use the length-1 characters.
|
|
func TestSearch_PrefixSearch_RoundDownToEven(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
id1 := "aaafaaaa-e8f7-fd38-c855-ab94ceb89"
|
|
id2 := "aaafeaaa-e8f7-fd38-c855-ab94ceb89"
|
|
prefix := "aaafa"
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
job := registerMockJob(s, t, id1, 0)
|
|
registerMockJob(s, t, id2, 50)
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_MultiRegion(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
jobName := "exampleexample"
|
|
|
|
s1, cleanupS1 := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
c.Region = "foo"
|
|
})
|
|
defer cleanupS1()
|
|
|
|
s2, cleanupS2 := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
c.Region = "bar"
|
|
})
|
|
defer cleanupS2()
|
|
|
|
TestJoin(t, s1, s2)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
job := registerMockJob(s1, t, jobName, 0)
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: "",
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "foo",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
codec := rpcClient(t, s2)
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_CSIPlugin(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
id := uuid.Generate()
|
|
state.CreateTestCSIPlugin(s.fsm.State(), id)
|
|
|
|
prefix := id[:len(id)-2]
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Plugins,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Plugins], 1)
|
|
require.Equal(t, id, resp.Matches[structs.Plugins][0])
|
|
require.False(t, resp.Truncations[structs.Plugins])
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_CSIVolume(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
id := uuid.Generate()
|
|
err := s.fsm.State().UpsertCSIVolume(1000, []*structs.CSIVolume{{
|
|
ID: id,
|
|
Namespace: structs.DefaultNamespace,
|
|
PluginID: "glade",
|
|
}})
|
|
require.NoError(t, err)
|
|
|
|
prefix := id[:len(id)-2]
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Volumes,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: structs.DefaultNamespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Volumes], 1)
|
|
require.Equal(t, id, resp.Matches[structs.Volumes][0])
|
|
require.False(t, resp.Truncations[structs.Volumes])
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_Namespace(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanup := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanup()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
ns := mock.Namespace()
|
|
require.NoError(t, s.fsm.State().UpsertNamespaces(2000, []*structs.Namespace{ns}))
|
|
|
|
prefix := ns.Name[:len(ns.Name)-2]
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.Namespaces,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Namespaces], 1)
|
|
require.Equal(t, ns.Name, resp.Matches[structs.Namespaces][0])
|
|
require.False(t, resp.Truncations[structs.Namespaces])
|
|
require.Equal(t, uint64(2000), resp.Index)
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_Namespace_ACL(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, root, cleanup := TestACLServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanup()
|
|
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
store := s.fsm.State()
|
|
|
|
ns := mock.Namespace()
|
|
must.NoError(t, store.UpsertNamespaces(500, []*structs.Namespace{ns}))
|
|
|
|
job1 := mock.Job()
|
|
must.NoError(t, store.UpsertJob(structs.MsgTypeTestSetup, 502, nil, job1))
|
|
|
|
job2 := mock.Job()
|
|
job2.Namespace = ns.Name
|
|
must.NoError(t, store.UpsertJob(structs.MsgTypeTestSetup, 504, nil, job2))
|
|
|
|
node := mock.Node()
|
|
must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1001, node))
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: "",
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job1.Namespace,
|
|
},
|
|
}
|
|
|
|
// Try without a token and expect failure
|
|
{
|
|
var resp structs.SearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
|
|
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with an invalid token and expect failure
|
|
{
|
|
invalidToken := mock.CreatePolicyAndToken(t, store, 1003, "test-invalid",
|
|
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
|
|
req.AuthToken = invalidToken.SecretID
|
|
var resp structs.SearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
|
|
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with a node:read token and expect failure due to Namespaces being the context
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1005, "test-invalid2", mock.NodePolicy(acl.PolicyRead))
|
|
req.Context = structs.Namespaces
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.SearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
|
|
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with a node:read token and expect success due to All context
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1007, "test-valid", mock.NodePolicy(acl.PolicyRead))
|
|
req.Context = structs.All
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
must.Eq(t, uint64(1001), resp.Index)
|
|
must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
|
|
|
|
// Jobs filtered out since token only has access to node:read
|
|
must.SliceEmpty(t, resp.Matches[structs.Jobs])
|
|
}
|
|
|
|
// Try with a valid token for non-default namespace:read-job
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1009, "test-valid2",
|
|
mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob}))
|
|
req.Context = structs.All
|
|
req.AuthToken = validToken.SecretID
|
|
req.Namespace = job2.Namespace
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
must.Eq(t, []string{job2.ID}, resp.Matches[structs.Jobs])
|
|
must.Eq(t, []string{ns.Name}, resp.Matches[structs.Namespaces])
|
|
|
|
// Index of job - not node - because node context is filtered out
|
|
must.Eq(t, uint64(504), resp.Index)
|
|
|
|
// Nodes filtered out since token only has access to namespace:read-job
|
|
must.SliceEmpty(t, resp.Matches[structs.Nodes])
|
|
}
|
|
|
|
// Try with a valid token for node:read and default namespace:read-job
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1011, "test-valid3", strings.Join([]string{
|
|
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
|
|
mock.NodePolicy(acl.PolicyRead),
|
|
}, "\n"))
|
|
req.Context = structs.All
|
|
req.AuthToken = validToken.SecretID
|
|
req.Namespace = structs.DefaultNamespace
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
must.Eq(t, []string{job1.ID}, resp.Matches[structs.Jobs])
|
|
must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
|
|
must.Eq(t, []string{"default"}, resp.Matches[structs.Namespaces])
|
|
|
|
must.Eq(t, uint64(1001), resp.Index)
|
|
|
|
}
|
|
|
|
// Try with a management token
|
|
{
|
|
req.Context = structs.All
|
|
req.AuthToken = root.SecretID
|
|
req.Namespace = structs.DefaultNamespace
|
|
var resp structs.SearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
|
|
must.Eq(t, []string{job1.ID}, resp.Matches[structs.Jobs])
|
|
must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
|
|
must.Eq(t, []string{"default", ns.Name}, resp.Matches[structs.Namespaces])
|
|
|
|
must.Eq(t, uint64(1001), resp.Index)
|
|
}
|
|
}
|
|
|
|
func TestSearch_PrefixSearch_ScalingPolicy(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
job, policy := mock.JobWithScalingPolicy()
|
|
prefix := policy.ID
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, jobIndex, nil, job))
|
|
|
|
req := &structs.SearchRequest{
|
|
Prefix: prefix,
|
|
Context: structs.ScalingPolicies,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.SearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.ScalingPolicies], 1)
|
|
require.Equal(t, policy.ID, resp.Matches[structs.ScalingPolicies][0])
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
|
|
req.Context = structs.All
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.ScalingPolicies], 1)
|
|
require.Equal(t, policy.ID, resp.Matches[structs.ScalingPolicies][0])
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_ACL(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, root, cleanupS := TestACLServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
c.SearchConfig.MinTermLength = 1
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
store := s.fsm.State()
|
|
|
|
ns := mock.Namespace()
|
|
ns.Name = "not-allowed"
|
|
must.NoError(t, store.UpsertNamespaces(10, []*structs.Namespace{ns}))
|
|
|
|
job := mock.Job()
|
|
registerJob(s, t, job)
|
|
|
|
variable := mock.VariableEncrypted()
|
|
variable.Path = "test-path/o"
|
|
resp := store.VarSet(1001, &structs.VarApplyStateRequest{
|
|
Op: structs.VarOpSet,
|
|
Var: variable,
|
|
})
|
|
must.NoError(t, resp.Error)
|
|
|
|
plugin := mock.CSIPlugin()
|
|
plugin.ID = "mock.hashicorp.com"
|
|
must.NoError(t, store.UpsertCSIPlugin(1002, plugin))
|
|
|
|
node := mock.Node()
|
|
must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1003, node))
|
|
|
|
disallowedVariable := mock.VariableEncrypted()
|
|
disallowedVariable.Namespace = "not-allowed"
|
|
resp = store.VarSet(2001, &structs.VarApplyStateRequest{
|
|
Op: structs.VarOpSet,
|
|
Var: disallowedVariable,
|
|
})
|
|
must.NoError(t, resp.Error)
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "set-this-in-test",
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{Region: "global", Namespace: job.Namespace},
|
|
}
|
|
|
|
// Try without a token and expect failure
|
|
{
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with an invalid token and expect failure
|
|
{
|
|
invalidToken := mock.CreatePolicyAndToken(t, store, 1003, "test-invalid",
|
|
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
|
|
req.AuthToken = invalidToken.SecretID
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with a node:read token and expect failure due to Jobs being the context
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1005, "test-invalid2", mock.NodePolicy(acl.PolicyRead))
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with a node:read token and expect success due to All context
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1007, "test-valid", mock.NodePolicy(acl.PolicyRead))
|
|
req.Context = structs.All
|
|
req.AuthToken = validToken.SecretID
|
|
req.Text = "oo" // mock node ID is foobar
|
|
var resp structs.FuzzySearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
must.Eq(t, []structs.FuzzyMatch{{ID: node.Name, Scope: []string{node.ID}}},
|
|
resp.Matches[structs.Nodes])
|
|
|
|
// Jobs, Plugins, Variables filtered out since token only has access to
|
|
// node:read
|
|
must.SliceEmpty(t, resp.Matches[structs.Jobs])
|
|
must.SliceEmpty(t, resp.Matches[structs.Plugins])
|
|
must.SliceEmpty(t, resp.Matches[structs.Variables])
|
|
|
|
must.Eq(t, uint64(1003), resp.Index) // index of node
|
|
}
|
|
|
|
// Try with a valid token for namespace:read-job
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1009, "test-valid2",
|
|
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}))
|
|
req.AuthToken = validToken.SecretID
|
|
req.Text = "jo" // mock job Name is my-job
|
|
var resp structs.FuzzySearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
must.Eq(t, structs.FuzzyMatch{
|
|
ID: "my-job",
|
|
Scope: []string{"default", job.ID},
|
|
}, resp.Matches[structs.Jobs][0])
|
|
|
|
// Index of job - not node - because node context is filtered out
|
|
must.Eq(t, uint64(1000), resp.Index)
|
|
|
|
// Nodes filtered out since token only has access to namespace:read-job
|
|
must.SliceEmpty(t, resp.Matches[structs.Nodes])
|
|
}
|
|
|
|
// Try with a valid token for node:read and namespace:variable:read
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, store, 1012, "test-valid4", strings.Join([]string{
|
|
mock.NamespacePolicyWithVariables(structs.DefaultNamespace, "", []string{},
|
|
map[string][]string{"*": []string{"list"}}),
|
|
mock.NodePolicy(acl.PolicyRead),
|
|
}, "\n"))
|
|
req.Text = "o" // matches Job:my-job, Node:foobar, Plugin, and Variables
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.FuzzySearchResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
must.SliceEmpty(t, resp.Matches[structs.Jobs])
|
|
must.SliceEmpty(t, resp.Matches[structs.Plugins])
|
|
|
|
must.Eq(t, []structs.FuzzyMatch{
|
|
{ID: node.Name, Scope: []string{node.ID}}},
|
|
resp.Matches[structs.Nodes])
|
|
|
|
must.Eq(t, []structs.FuzzyMatch{{
|
|
ID: variable.Path,
|
|
Scope: []string{structs.DefaultNamespace, variable.Path}}},
|
|
resp.Matches[structs.Variables])
|
|
|
|
must.Eq(t, uint64(2001), resp.Index) // index of variables
|
|
}
|
|
|
|
// Try with a management token
|
|
{
|
|
req.AuthToken = root.SecretID
|
|
var resp structs.FuzzySearchResponse
|
|
req.Text = "o" // matches Job:my-job, Node:foobar, Plugin, and Variables
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
must.Eq(t, []structs.FuzzyMatch{
|
|
{ID: job.Name, Scope: []string{"default", job.ID}}},
|
|
resp.Matches[structs.Jobs])
|
|
|
|
must.Eq(t, []structs.FuzzyMatch{
|
|
{ID: node.Name, Scope: []string{node.ID}}},
|
|
resp.Matches[structs.Nodes])
|
|
|
|
must.Eq(t, []structs.FuzzyMatch{{ID: plugin.ID}},
|
|
resp.Matches[structs.Plugins])
|
|
|
|
must.Eq(t, []structs.FuzzyMatch{{
|
|
ID: variable.Path,
|
|
Scope: []string{structs.DefaultNamespace, variable.Path}}},
|
|
resp.Matches[structs.Variables])
|
|
|
|
must.Eq(t, uint64(2001), resp.Index) // index of variables
|
|
}
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_NotEnabled(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
c.SearchConfig.FuzzyEnabled = false
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
fsmState := s.fsm.State()
|
|
|
|
job := mock.Job()
|
|
registerJob(s, t, job)
|
|
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, mock.Node()))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "foo", // min set to 5
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{Region: "global", Namespace: job.Namespace},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.EqualError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp),
|
|
"fuzzy search is not enabled")
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_ShortText(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
c.SearchConfig.MinTermLength = 5
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
fsmState := s.fsm.State()
|
|
|
|
job := mock.Job()
|
|
registerJob(s, t, job)
|
|
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, mock.Node()))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "foo", // min set to 5
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{Region: "global", Namespace: job.Namespace},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.EqualError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp),
|
|
"fuzzy search query must be at least 5 characters, got 3")
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_TruncateLimitQuery(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, mock.Node()))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "job",
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{Region: "global", Namespace: "default"},
|
|
}
|
|
|
|
for i := 0; i < 25; i++ {
|
|
job := mock.Job()
|
|
job.Name = fmt.Sprintf("my-job-%d", i)
|
|
registerJob(s, t, job)
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Jobs], 20)
|
|
require.True(t, resp.Truncations[structs.Jobs])
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_TruncateLimitResults(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
c.SearchConfig.LimitQuery = 10000
|
|
c.SearchConfig.LimitResults = 5
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, mock.Node()))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "job",
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{Region: "global", Namespace: "default"},
|
|
}
|
|
|
|
for i := 0; i < 25; i++ {
|
|
job := mock.Job()
|
|
job.Name = fmt.Sprintf("my-job-%d", i)
|
|
registerJob(s, t, job)
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Jobs], 5)
|
|
require.True(t, resp.Truncations[structs.Jobs])
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_Evals(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
eval1 := mock.Eval()
|
|
eval1.ID = "f7dee5a1-d2b0-2f6a-2e75-6c8e467a4b99"
|
|
require.NoError(t, s.fsm.State().UpsertEvals(structs.MsgTypeTestSetup, 2000, []*structs.Evaluation{eval1}))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "f7dee", // evals are prefix searched
|
|
Context: structs.Evals,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: eval1.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Evals], 1)
|
|
require.Equal(t, eval1.ID, resp.Matches[structs.Evals][0].ID)
|
|
require.False(t, resp.Truncations[structs.Evals])
|
|
require.Equal(t, uint64(2000), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_Allocation(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
alloc := mockAlloc()
|
|
summary := mock.JobSummary(alloc.JobID)
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertJobSummary(999, summary))
|
|
require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 90, []*structs.Allocation{alloc}))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "web",
|
|
Context: structs.Allocs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: alloc.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Allocs], 1)
|
|
require.Equal(t, alloc.Name, resp.Matches[structs.Allocs][0].ID)
|
|
require.False(t, resp.Truncations[structs.Allocs])
|
|
require.Equal(t, uint64(90), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_Node(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
fsmState := s.fsm.State()
|
|
node := mock.Node()
|
|
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 100, node))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "oo",
|
|
Context: structs.Nodes,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: structs.DefaultNamespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Nodes], 1)
|
|
require.Equal(t, node.Name, resp.Matches[structs.Nodes][0].ID)
|
|
require.False(t, resp.Truncations[structs.Nodes])
|
|
require.Equal(t, uint64(100), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_NodePool(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Start test server.
|
|
s, cleanupS := TestServer(t, nil)
|
|
defer cleanupS()
|
|
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Populate state with test node pools.
|
|
fsmState := s.fsm.State()
|
|
devEng := &structs.NodePool{Name: "dev-eng"}
|
|
devInfra := &structs.NodePool{Name: "dev-infra"}
|
|
prodEng := &structs.NodePool{Name: "prod-eng"}
|
|
|
|
err := fsmState.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{devEng, devInfra, prodEng})
|
|
must.NoError(t, err)
|
|
|
|
// Run test cases.
|
|
testCases := []struct {
|
|
name string
|
|
text string
|
|
context structs.Context
|
|
expected []string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "fuzzy match",
|
|
text: "eng",
|
|
context: structs.NodePools,
|
|
expected: []string{devEng.Name, prodEng.Name},
|
|
},
|
|
{
|
|
name: "fuzzy match - all",
|
|
text: "eng",
|
|
context: structs.All,
|
|
expected: []string{devEng.Name, prodEng.Name},
|
|
},
|
|
{
|
|
name: "empty prefix",
|
|
text: "",
|
|
context: structs.NodePools,
|
|
expectedErr: "search query must be at least 2 characters",
|
|
},
|
|
{
|
|
name: "other context",
|
|
text: "eng",
|
|
context: structs.Jobs,
|
|
expected: []string{},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: tc.text,
|
|
Context: tc.context,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
if tc.expectedErr != "" {
|
|
must.ErrorContains(t, err, tc.expectedErr)
|
|
return
|
|
}
|
|
must.NoError(t, err)
|
|
must.Len(t, len(tc.expected), resp.Matches[structs.NodePools])
|
|
|
|
for k, v := range resp.Matches {
|
|
switch k {
|
|
case structs.NodePools:
|
|
got := make([]string, len(v))
|
|
for i, m := range v {
|
|
got[i] = m.ID
|
|
}
|
|
must.SliceContainsAll(t, got, tc.expected)
|
|
default:
|
|
must.Len(t, 0, v, must.Sprintf("found %d results in %v: %v", len(v), k, v))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_NodePool_ACL(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Start test server with ACL.
|
|
s, root, cleanupS := TestACLServer(t, nil)
|
|
defer cleanupS()
|
|
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Populate state with test node pools and ACL policies.
|
|
fsmState := s.fsm.State()
|
|
|
|
devEng := &structs.NodePool{Name: "dev-eng"}
|
|
devInfra := &structs.NodePool{Name: "dev-infra"}
|
|
prodEng := &structs.NodePool{Name: "prod-eng"}
|
|
|
|
err := fsmState.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{devEng, devInfra, prodEng})
|
|
must.NoError(t, err)
|
|
|
|
engToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1001, "eng-node-pools",
|
|
mock.NodePoolPolicy("*eng", "read", nil),
|
|
)
|
|
noPolicyToken := mock.CreateToken(t, s.fsm.State(), 1003, nil)
|
|
allPoolsToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1005, "all-node-pools",
|
|
mock.NodePoolPolicy("*", "read", nil),
|
|
)
|
|
denyEngToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1007, "deny-eng-node-pools",
|
|
mock.NodePoolPolicy("*eng", "deny", nil),
|
|
)
|
|
|
|
// Run test cases.
|
|
testCases := []struct {
|
|
name string
|
|
token string
|
|
text string
|
|
expected []string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "management token has access to all",
|
|
token: root.SecretID,
|
|
text: "dev",
|
|
expected: []string{devEng.Name, devInfra.Name},
|
|
},
|
|
{
|
|
name: "all pools access",
|
|
token: allPoolsToken.SecretID,
|
|
text: "dev",
|
|
expected: []string{devEng.Name, devInfra.Name},
|
|
},
|
|
{
|
|
name: "only return what token has access",
|
|
token: engToken.SecretID,
|
|
text: "eng",
|
|
expected: []string{devEng.Name, prodEng.Name},
|
|
},
|
|
{
|
|
name: "no results if token doesn't have access",
|
|
token: engToken.SecretID,
|
|
text: "infra",
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "no results if token is denied",
|
|
token: denyEngToken.SecretID,
|
|
text: "eng",
|
|
expectedErr: structs.ErrPermissionDenied.Error(),
|
|
},
|
|
{
|
|
name: "no policy",
|
|
token: noPolicyToken.SecretID,
|
|
text: "dev",
|
|
expectedErr: structs.ErrPermissionDenied.Error(),
|
|
},
|
|
{
|
|
name: "no token",
|
|
token: "",
|
|
text: "dev",
|
|
expectedErr: structs.ErrPermissionDenied.Error(),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: tc.text,
|
|
Context: structs.NodePools,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: tc.token,
|
|
},
|
|
}
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
if tc.expectedErr != "" {
|
|
must.ErrorContains(t, err, tc.expectedErr)
|
|
return
|
|
}
|
|
|
|
must.NoError(t, err)
|
|
must.Len(t, len(tc.expected), resp.Matches[structs.NodePools])
|
|
|
|
for k, v := range resp.Matches {
|
|
switch k {
|
|
case structs.NodePools:
|
|
got := make([]string, len(v))
|
|
for i, m := range v {
|
|
got[i] = m.ID
|
|
}
|
|
must.SliceContainsAll(t, got, tc.expected)
|
|
default:
|
|
must.Len(t, 0, v, must.Sprintf("found %d results in %v: %v", len(v), k, v))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_Deployment(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
deployment := mock.Deployment()
|
|
require.NoError(t, s.fsm.State().UpsertDeployment(2000, deployment))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: deployment.ID[0:3], // deployments are prefix searched
|
|
Context: structs.Deployments,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: deployment.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Deployments], 1)
|
|
require.Equal(t, deployment.ID, resp.Matches[structs.Deployments][0].ID)
|
|
require.False(t, resp.Truncations[structs.Deployments])
|
|
require.Equal(t, uint64(2000), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_CSIPlugin(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
state.CreateTestCSIPlugin(s.fsm.State(), "my-plugin")
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "lug",
|
|
Context: structs.Plugins,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Plugins], 1)
|
|
require.Equal(t, "my-plugin", resp.Matches[structs.Plugins][0].ID)
|
|
require.False(t, resp.Truncations[structs.Plugins])
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_CSIVolume(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
id := uuid.Generate()
|
|
err := s.fsm.State().UpsertCSIVolume(1000, []*structs.CSIVolume{{
|
|
ID: id,
|
|
Namespace: structs.DefaultNamespace,
|
|
PluginID: "glade",
|
|
}})
|
|
require.NoError(t, err)
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: id[0:3], // volumes are prefix searched
|
|
Context: structs.Volumes,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: structs.DefaultNamespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Volumes], 1)
|
|
require.Equal(t, id, resp.Matches[structs.Volumes][0].ID)
|
|
require.False(t, resp.Truncations[structs.Volumes])
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_Namespace(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanup := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanup()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
ns := mock.Namespace()
|
|
require.NoError(t, s.fsm.State().UpsertNamespaces(2000, []*structs.Namespace{ns}))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "am", // mock is team-<uuid>
|
|
Context: structs.Namespaces,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Namespaces], 1)
|
|
require.Equal(t, ns.Name, resp.Matches[structs.Namespaces][0].ID)
|
|
require.False(t, resp.Truncations[structs.Namespaces])
|
|
require.Equal(t, uint64(2000), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_Namespace_caseInsensitive(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanup := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanup()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
ns := mock.Namespace()
|
|
ns.Name = "TheFooNamespace"
|
|
require.NoError(t, s.fsm.State().UpsertNamespaces(2000, []*structs.Namespace{ns}))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "foon",
|
|
Context: structs.Namespaces,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Namespaces], 1)
|
|
require.Equal(t, ns.Name, resp.Matches[structs.Namespaces][0].ID)
|
|
require.False(t, resp.Truncations[structs.Namespaces])
|
|
require.Equal(t, uint64(2000), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_ScalingPolicy(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
job, policy := mock.JobWithScalingPolicy()
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, jobIndex, nil, job))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: policy.ID[0:3], // scaling policies are prefix searched
|
|
Context: structs.ScalingPolicies,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.ScalingPolicies], 1)
|
|
require.Equal(t, policy.ID, resp.Matches[structs.ScalingPolicies][0].ID)
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
|
|
req.Context = structs.All
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.ScalingPolicies], 1)
|
|
require.Equal(t, policy.ID, resp.Matches[structs.ScalingPolicies][0].ID)
|
|
require.Equal(t, uint64(jobIndex), resp.Index)
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_Namespace_ACL(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, root, cleanup := TestACLServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanup()
|
|
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
fsmState := s.fsm.State()
|
|
|
|
ns := mock.Namespace()
|
|
ns.Name = "team-job-app"
|
|
require.NoError(t, fsmState.UpsertNamespaces(500, []*structs.Namespace{ns}))
|
|
|
|
job1 := mock.Job()
|
|
require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, 502, nil, job1))
|
|
|
|
job2 := mock.Job()
|
|
job2.Namespace = ns.Name
|
|
require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, 504, nil, job2))
|
|
|
|
node := mock.Node()
|
|
node.Name = "run-jobs"
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, node))
|
|
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "set-text-in-test",
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job1.Namespace,
|
|
},
|
|
}
|
|
|
|
// Try without a token and expect failure
|
|
{
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with an invalid token and expect failure
|
|
{
|
|
invalidToken := mock.CreatePolicyAndToken(t, fsmState, 1003, "test-invalid",
|
|
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
|
|
req.AuthToken = invalidToken.SecretID
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with a node:read token and expect failure due to Namespaces being the context
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, fsmState, 1005, "test-invalid2", mock.NodePolicy(acl.PolicyRead))
|
|
req.Context = structs.Namespaces
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
// Try with a node:read token and expect success due to All context
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, fsmState, 1007, "test-valid", mock.NodePolicy(acl.PolicyRead))
|
|
req.Text = "job"
|
|
req.Context = structs.All
|
|
req.AuthToken = validToken.SecretID
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Equal(t, uint64(1001), resp.Index)
|
|
require.Len(t, resp.Matches[structs.Nodes], 1)
|
|
|
|
// Jobs filtered out since token only has access to node:read
|
|
require.Len(t, resp.Matches[structs.Jobs], 0)
|
|
}
|
|
|
|
// Try with a valid token for non-default namespace:read-job
|
|
{
|
|
validToken := mock.CreatePolicyAndToken(t, fsmState, 1009, "test-valid2",
|
|
mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob}))
|
|
req.Text = "job"
|
|
req.Context = structs.All
|
|
req.AuthToken = validToken.SecretID
|
|
req.Namespace = job2.Namespace
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
require.Equal(t, job2.Name, resp.Matches[structs.Jobs][0].ID)
|
|
|
|
// Index of job - not node - because node context is filtered out
|
|
require.Equal(t, uint64(504), resp.Index)
|
|
|
|
// Nodes filtered out since token only has access to namespace:read-job
|
|
require.Len(t, resp.Matches[structs.Nodes], 0)
|
|
}
|
|
|
|
// Try with a management token
|
|
{
|
|
req.Text = "job"
|
|
req.Context = structs.All
|
|
req.AuthToken = root.SecretID
|
|
req.Namespace = job1.Namespace
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Equal(t, uint64(1001), resp.Index)
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
require.Equal(t, job1.Name, resp.Matches[structs.Jobs][0].ID)
|
|
require.Len(t, resp.Matches[structs.Nodes], 1)
|
|
require.Len(t, resp.Matches[structs.Namespaces], 1) // matches "team-job-app"
|
|
}
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_MultiNamespace_ACL(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, root, cleanupS := TestACLServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
fsmState := s.fsm.State()
|
|
|
|
require.NoError(t, fsmState.UpsertNamespaces(500, []*structs.Namespace{{
|
|
Name: "teamA",
|
|
Description: "first namespace",
|
|
CreateIndex: 100,
|
|
ModifyIndex: 200,
|
|
}, {
|
|
Name: "teamB",
|
|
Description: "second namespace",
|
|
CreateIndex: 101,
|
|
ModifyIndex: 201,
|
|
}, {
|
|
Name: "teamC",
|
|
Description: "third namespace",
|
|
CreateIndex: 102,
|
|
ModifyIndex: 202,
|
|
}}))
|
|
|
|
// Closure to simplify fsm indexing
|
|
index := uint64(1000)
|
|
inc := func() uint64 {
|
|
index++
|
|
return index
|
|
}
|
|
|
|
// Upsert 3 jobs each in separate namespace
|
|
job1 := mock.Job()
|
|
job1.Name = "teamA-job1"
|
|
job1.ID = "job1"
|
|
job1.Namespace = "teamA"
|
|
require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, inc(), nil, job1))
|
|
|
|
job2 := mock.Job()
|
|
job2.Name = "teamB-job2"
|
|
job2.ID = "job2"
|
|
job2.Namespace = "teamB"
|
|
require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, inc(), nil, job2))
|
|
|
|
job3 := mock.Job()
|
|
job3.Name = "teamC-job3"
|
|
job3.ID = "job3"
|
|
job3.Namespace = "teamC"
|
|
require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, inc(), nil, job3))
|
|
|
|
// Upsert a node
|
|
node := mock.Node()
|
|
node.Name = "node-for-teams"
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, inc(), node))
|
|
|
|
// Upsert a node that will not be matched
|
|
node2 := mock.Node()
|
|
node2.Name = "node-for-ops"
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, inc(), node2))
|
|
|
|
// Create parameterized requests
|
|
request := func(text, namespace, token string, context structs.Context) *structs.FuzzySearchRequest {
|
|
return &structs.FuzzySearchRequest{
|
|
Text: text,
|
|
Context: context,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: namespace,
|
|
AuthToken: token,
|
|
},
|
|
}
|
|
}
|
|
|
|
t.Run("without a token expect failure", func(t *testing.T) {
|
|
var resp structs.FuzzySearchResponse
|
|
req := request("anything", job1.Namespace, "", structs.Jobs)
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
})
|
|
|
|
t.Run("with an invalid token expect failure", func(t *testing.T) {
|
|
policy := mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs})
|
|
invalidToken := mock.CreatePolicyAndToken(t, fsmState, inc(), "test-invalid", policy)
|
|
req := request("anything", job1.Namespace, invalidToken.SecretID, structs.Jobs)
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
})
|
|
|
|
t.Run("with node:read token search namespaces expect failure", func(t *testing.T) {
|
|
validToken := mock.CreatePolicyAndToken(t, fsmState, inc(), "test-invalid2", mock.NodePolicy(acl.PolicyRead))
|
|
req := request("team", job1.Namespace, validToken.SecretID, structs.Namespaces)
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
})
|
|
|
|
t.Run("with node:read token search all expect success", func(t *testing.T) {
|
|
validToken := mock.CreatePolicyAndToken(t, fsmState, inc(), "test-valid", mock.NodePolicy(acl.PolicyRead))
|
|
req := request("team", job1.Namespace, validToken.SecretID, structs.All)
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
// One matching node
|
|
require.Len(t, resp.Matches[structs.Nodes], 1)
|
|
|
|
// Jobs filtered out since token only has access to node:read
|
|
require.Len(t, resp.Matches[structs.Jobs], 0)
|
|
})
|
|
|
|
t.Run("with a teamB/job:read token search all expect 1 job", func(t *testing.T) {
|
|
policy := mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob})
|
|
token := mock.CreatePolicyAndToken(t, fsmState, inc(), "test-valid2", policy)
|
|
req := request("team", job2.Namespace, token.SecretID, structs.All)
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Jobs], 1)
|
|
require.Equal(t, job2.Name, resp.Matches[structs.Jobs][0].ID)
|
|
|
|
// Nodes filtered out since token only has access to namespace:read-job
|
|
require.Len(t, resp.Matches[structs.Nodes], 0)
|
|
})
|
|
|
|
// Using a token that can read jobs in 2 namespaces, we should get job results from
|
|
// both those namespaces (using wildcard namespace in the query) but not the
|
|
// third (and from no other contexts).
|
|
t.Run("with a multi-ns job:read token search all expect 2 jobs", func(t *testing.T) {
|
|
policyB := mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob})
|
|
mock.CreatePolicy(t, fsmState, inc(), "policyB", policyB)
|
|
|
|
policyC := mock.NamespacePolicy(job3.Namespace, "", []string{acl.NamespaceCapabilityReadJob})
|
|
mock.CreatePolicy(t, fsmState, inc(), "policyC", policyC)
|
|
|
|
token := mock.CreateToken(t, fsmState, inc(), []string{"policyB", "policyC"})
|
|
req := request("team", structs.AllNamespacesSentinel, token.SecretID, structs.Jobs)
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Jobs], 2)
|
|
require.Equal(t, job2.Name, resp.Matches[structs.Jobs][0].ID)
|
|
require.Equal(t, job3.Name, resp.Matches[structs.Jobs][1].ID)
|
|
})
|
|
|
|
// Using a management token, we should get job results from all three namespaces
|
|
// (using wildcard namespace in the query).
|
|
t.Run("with a management token search all expect 3 jobs", func(t *testing.T) {
|
|
req := request("team", structs.AllNamespacesSentinel, root.SecretID, structs.Jobs)
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Jobs], 3)
|
|
require.Equal(t, job1.Name, resp.Matches[structs.Jobs][0].ID)
|
|
require.Equal(t, job2.Name, resp.Matches[structs.Jobs][1].ID)
|
|
require.Equal(t, job3.Name, resp.Matches[structs.Jobs][2].ID)
|
|
})
|
|
|
|
// Using a token that can read nodes, we should get our 1 matching node when
|
|
// searching the nodes context.
|
|
t.Run("with node:read token read nodes", func(t *testing.T) {
|
|
policy := mock.NodePolicy("read")
|
|
mock.CreatePolicy(t, fsmState, inc(), "node-read-policy", policy)
|
|
|
|
token := mock.CreateToken(t, fsmState, inc(), []string{"node-read-policy"})
|
|
req := request("team", structs.AllNamespacesSentinel, token.SecretID, structs.Nodes)
|
|
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Nodes], 1)
|
|
require.Equal(t, "node-for-teams", resp.Matches[structs.Nodes][0].ID)
|
|
})
|
|
|
|
// Using a token that cannot read nodes, we should get no matching nodes when
|
|
// searching the nodes context.
|
|
t.Run("with a job:read token read nodes", func(t *testing.T) {
|
|
policy := mock.AgentPolicy("read")
|
|
mock.CreatePolicy(t, fsmState, inc(), "agent-read-policy", policy)
|
|
|
|
token := mock.CreateToken(t, fsmState, inc(), []string{"agent-read-policy"})
|
|
req := request("team", structs.AllNamespacesSentinel, token.SecretID, structs.Nodes)
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Empty(t, resp.Matches[structs.Nodes])
|
|
})
|
|
|
|
// Using a token that can read only job:read one namespace, but with wildcard
|
|
// namespace should return only the one alloc the token can access.
|
|
t.Run("with job:read token read allocs", func(t *testing.T) {
|
|
policyD := mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob})
|
|
mock.CreatePolicy(t, fsmState, inc(), "policyD", policyD)
|
|
|
|
// Create an alloc for each of the 3 jobs
|
|
alloc1 := mockAlloc()
|
|
alloc1.JobID = job1.ID
|
|
alloc1.Name = job1.Name + ".task[0]"
|
|
alloc1.Namespace = job1.Namespace
|
|
summary1 := mock.JobSummary(alloc1.JobID)
|
|
require.NoError(t, fsmState.UpsertJobSummary(inc(), summary1))
|
|
|
|
alloc2 := mockAlloc()
|
|
alloc2.JobID = job2.ID
|
|
alloc2.Name = job2.Name + ".task[0]"
|
|
alloc2.Namespace = job2.Namespace
|
|
summary2 := mock.JobSummary(alloc2.JobID)
|
|
require.NoError(t, fsmState.UpsertJobSummary(inc(), summary2))
|
|
|
|
alloc3 := mockAlloc()
|
|
alloc3.JobID = job3.ID
|
|
alloc3.Name = job3.Name + ".task[0]"
|
|
alloc3.Namespace = job3.Namespace
|
|
summary3 := mock.JobSummary(alloc3.JobID)
|
|
require.NoError(t, fsmState.UpsertJobSummary(inc(), summary3))
|
|
|
|
// Upsert the allocs
|
|
require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, inc(), []*structs.Allocation{alloc1, alloc2, alloc3}))
|
|
|
|
token := mock.CreateToken(t, fsmState, inc(), []string{"policyD"})
|
|
req := request("team", structs.AllNamespacesSentinel, token.SecretID, structs.Allocs)
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Allocs], 1)
|
|
require.Equal(t, "teamB-job2.task[0]", resp.Matches[structs.Allocs][0].ID)
|
|
})
|
|
|
|
// Using a management token should return allocs from all the jobs.
|
|
t.Run("with job:read token read allocs", func(t *testing.T) {
|
|
// Create an alloc for each of the 3 jobs
|
|
alloc1 := mockAlloc()
|
|
alloc1.ID = uuid.Generate()
|
|
alloc1.JobID = job1.ID
|
|
alloc1.Name = "test-alloc.one[0]"
|
|
alloc1.Namespace = job1.Namespace
|
|
summary1 := mock.JobSummary(alloc1.JobID)
|
|
require.NoError(t, fsmState.UpsertJobSummary(inc(), summary1))
|
|
|
|
alloc2 := mockAlloc()
|
|
alloc2.ID = uuid.Generate()
|
|
alloc2.JobID = job2.ID
|
|
alloc2.Name = "test-alloc.two[0]"
|
|
alloc2.Namespace = job2.Namespace
|
|
summary2 := mock.JobSummary(alloc2.JobID)
|
|
require.NoError(t, fsmState.UpsertJobSummary(inc(), summary2))
|
|
|
|
alloc3 := mockAlloc()
|
|
alloc3.ID = uuid.Generate()
|
|
alloc3.JobID = job3.ID
|
|
alloc3.Name = "test-alloc.three[0]"
|
|
alloc3.Namespace = job3.Namespace
|
|
summary3 := mock.JobSummary(alloc3.JobID)
|
|
require.NoError(t, fsmState.UpsertJobSummary(inc(), summary3))
|
|
|
|
// Upsert the allocs
|
|
require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, inc(), []*structs.Allocation{alloc1, alloc2, alloc3}))
|
|
|
|
req := request("alloc", structs.AllNamespacesSentinel, root.SecretID, structs.Allocs)
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
require.Len(t, resp.Matches[structs.Allocs], 3)
|
|
require.Equal(t, alloc1.Name, resp.Matches[structs.Allocs][0].ID)
|
|
require.Equal(t, []string{"teamA", alloc1.ID}, resp.Matches[structs.Allocs][0].Scope)
|
|
require.Equal(t, alloc2.Name, resp.Matches[structs.Allocs][1].ID)
|
|
require.Equal(t, []string{"teamB", alloc2.ID}, resp.Matches[structs.Allocs][1].Scope)
|
|
require.Equal(t, alloc3.Name, resp.Matches[structs.Allocs][2].ID)
|
|
require.Equal(t, []string{"teamC", alloc3.ID}, resp.Matches[structs.Allocs][2].Scope)
|
|
})
|
|
|
|
// Allow plugin read and wildcard namespace
|
|
t.Run("with plugin:read", func(t *testing.T) {
|
|
policy := mock.PluginPolicy("read")
|
|
mock.CreatePolicy(t, fsmState, inc(), "plugin-read-policy", policy)
|
|
|
|
token := mock.CreateToken(t, fsmState, inc(), []string{"plugin-read-policy"})
|
|
|
|
// Create CSI plugins
|
|
state.CreateTestCSIPlugin(s.fsm.State(), "plugin-for-teams")
|
|
state.CreateTestCSIPlugin(s.fsm.State(), "plugin-for-ops")
|
|
|
|
req := request("teams", structs.AllNamespacesSentinel, token.SecretID, structs.Plugins)
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
|
|
require.Len(t, resp.Matches[structs.Plugins], 1)
|
|
require.Empty(t, resp.Matches[structs.Plugins][0].Scope) // no scope
|
|
})
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_Job(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s, cleanupS := TestServer(t, func(c *Config) {
|
|
c.NumSchedulers = 0
|
|
})
|
|
defer cleanupS()
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
fsmState := s.fsm.State()
|
|
|
|
job := mock.Job()
|
|
job.Name = "demo-sleep"
|
|
job.Namespace = "team-sleepy"
|
|
job.TaskGroups = []*structs.TaskGroup{{
|
|
Name: "qa-sleeper-group-one",
|
|
Services: []*structs.Service{{
|
|
Name: "qa-group-sleep-svc-one",
|
|
}},
|
|
Tasks: []*structs.Task{{
|
|
Name: "qa-sleep-task-one",
|
|
Services: []*structs.Service{{
|
|
Name: "some-sleepy-task-svc-one",
|
|
}},
|
|
Driver: "docker",
|
|
Config: map[string]interface{}{
|
|
"image": "sleeper:latest",
|
|
},
|
|
}},
|
|
}, {
|
|
Name: "prod-sleeper-group-one",
|
|
Tasks: []*structs.Task{{
|
|
Name: "prod-sleep-task-one",
|
|
Driver: "exec",
|
|
Config: map[string]interface{}{
|
|
"command": "/bin/sleep",
|
|
},
|
|
}, {
|
|
Name: "prod-task-two",
|
|
Driver: "raw_exec",
|
|
Config: map[string]interface{}{
|
|
"command": "/usr/sbin/sleep",
|
|
},
|
|
Services: []*structs.Service{{
|
|
Name: "some-sleepy-task-svc-two",
|
|
}},
|
|
}},
|
|
}, {
|
|
Name: "sleep-in-java",
|
|
Tasks: []*structs.Task{{
|
|
Name: "prod-java-sleep",
|
|
Driver: "java",
|
|
Config: map[string]interface{}{
|
|
"class": "sleep.class",
|
|
},
|
|
}},
|
|
}}
|
|
|
|
ns := mock.Namespace()
|
|
ns.Name = job.Namespace
|
|
require.NoError(t, fsmState.UpsertNamespaces(2000, []*structs.Namespace{ns}))
|
|
registerJob(s, t, job)
|
|
require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1003, mock.Node()))
|
|
|
|
t.Run("sleep", func(t *testing.T) {
|
|
req := &structs.FuzzySearchRequest{
|
|
Text: "sleep",
|
|
Context: structs.Jobs,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Namespace: job.Namespace,
|
|
},
|
|
}
|
|
var resp structs.FuzzySearchResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
|
|
m := resp.Matches
|
|
require.Equal(t, uint64(1000), resp.Index) // job is explicit search context, has id=1000
|
|
|
|
// just the one job
|
|
require.Len(t, m[structs.Jobs], 1)
|
|
|
|
// 3 services (1 group, 2 task)
|
|
require.Len(t, m[structs.Services], 3)
|
|
require.Equal(t, []structs.FuzzyMatch{{
|
|
ID: "some-sleepy-task-svc-one",
|
|
Scope: []string{"team-sleepy", job.ID, "qa-sleeper-group-one", "qa-sleep-task-one"},
|
|
}, {
|
|
ID: "some-sleepy-task-svc-two",
|
|
Scope: []string{"team-sleepy", job.ID, "prod-sleeper-group-one", "prod-task-two"},
|
|
}, {
|
|
ID: "qa-group-sleep-svc-one",
|
|
Scope: []string{"team-sleepy", job.ID, "qa-sleeper-group-one"},
|
|
}}, m[structs.Services])
|
|
|
|
// 3 groups
|
|
require.Len(t, m[structs.Groups], 3)
|
|
require.Equal(t, []structs.FuzzyMatch{{
|
|
ID: "sleep-in-java",
|
|
Scope: []string{"team-sleepy", job.ID},
|
|
}, {
|
|
ID: "qa-sleeper-group-one",
|
|
Scope: []string{"team-sleepy", job.ID},
|
|
}, {
|
|
ID: "prod-sleeper-group-one",
|
|
Scope: []string{"team-sleepy", job.ID},
|
|
}}, m[structs.Groups])
|
|
|
|
// 3 tasks (1 does not match)
|
|
require.Len(t, m[structs.Tasks], 3)
|
|
require.Equal(t, []structs.FuzzyMatch{{
|
|
ID: "qa-sleep-task-one",
|
|
Scope: []string{"team-sleepy", job.ID, "qa-sleeper-group-one"},
|
|
}, {
|
|
ID: "prod-sleep-task-one",
|
|
Scope: []string{"team-sleepy", job.ID, "prod-sleeper-group-one"},
|
|
}, {
|
|
ID: "prod-java-sleep",
|
|
Scope: []string{"team-sleepy", job.ID, "sleep-in-java"},
|
|
}}, m[structs.Tasks])
|
|
|
|
// 2 tasks with command
|
|
require.Len(t, m[structs.Commands], 2)
|
|
require.Equal(t, []structs.FuzzyMatch{{
|
|
ID: "/bin/sleep",
|
|
Scope: []string{"team-sleepy", job.ID, "prod-sleeper-group-one", "prod-sleep-task-one"},
|
|
}, {
|
|
ID: "/usr/sbin/sleep",
|
|
Scope: []string{"team-sleepy", job.ID, "prod-sleeper-group-one", "prod-task-two"},
|
|
}}, m[structs.Commands])
|
|
|
|
// 1 task with image
|
|
require.Len(t, m[structs.Images], 1)
|
|
require.Equal(t, []structs.FuzzyMatch{{
|
|
ID: "sleeper:latest",
|
|
Scope: []string{"team-sleepy", job.ID, "qa-sleeper-group-one", "qa-sleep-task-one"},
|
|
}}, m[structs.Images])
|
|
|
|
// 1 task with class
|
|
require.Len(t, m[structs.Classes], 1)
|
|
require.Equal(t, []structs.FuzzyMatch{{
|
|
ID: "sleep.class",
|
|
Scope: []string{"team-sleepy", job.ID, "sleep-in-java", "prod-java-sleep"},
|
|
}}, m[structs.Classes])
|
|
})
|
|
}
|
|
|
|
func TestSearch_FuzzySearch_fuzzyIndex(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
for _, tc := range []struct {
|
|
name, text string
|
|
exp int
|
|
}{
|
|
{name: "foo-bar-baz", text: "bar", exp: 4},
|
|
{name: "Foo-Bar-Baz", text: "bar", exp: 4},
|
|
{name: "foo-bar-baz", text: "zap", exp: -1},
|
|
} {
|
|
result := fuzzyIndex(tc.name, tc.text)
|
|
require.Equal(t, tc.exp, result, "name: %s, text: %s, exp: %d, got: %d", tc.name, tc.text, tc.exp, result)
|
|
}
|
|
}
|