mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
node pool: add search support (#17385)
This commit is contained in:
29
acl/acl.go
29
acl/acl.go
@@ -410,6 +410,35 @@ func (a *ACL) AllowNodePool(pool string) bool {
|
||||
return !capabilities.Check(PolicyDeny)
|
||||
}
|
||||
|
||||
// AllowNodePoolSearch returns true if any operation is allowed in at least one
|
||||
// node pool.
|
||||
//
|
||||
// This is a very loose check and is expected that callers perform more precise
|
||||
// verification later.
|
||||
func (a *ACL) AllowNodePoolSearch() bool {
|
||||
// Hot path if ACL is not enabled or token is management.
|
||||
if a == nil || a.management {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for any non-deny capabilities.
|
||||
iter := a.nodePools.Root().Iterator()
|
||||
for _, capability, ok := iter.Next(); ok; _, capability, ok = iter.Next() {
|
||||
if !capability.Check(NodePoolCapabilityDeny) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
iter = a.wildcardNodePools.Root().Iterator()
|
||||
for _, capability, ok := iter.Next(); ok; _, capability, ok = iter.Next() {
|
||||
if !capability.Check(NodePoolCapabilityDeny) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AllowHostVolumeOperation checks if a given operation is allowed for a host volume
|
||||
func (a *ACL) AllowHostVolumeOperation(hv string, op string) bool {
|
||||
// Hot path management tokens
|
||||
|
||||
@@ -33,6 +33,7 @@ var (
|
||||
structs.Allocs,
|
||||
structs.Jobs,
|
||||
structs.Nodes,
|
||||
structs.NodePools,
|
||||
structs.Evals,
|
||||
structs.Deployments,
|
||||
structs.Plugins,
|
||||
@@ -75,6 +76,8 @@ func (s *Search) getPrefixMatches(iter memdb.ResultIterator, prefix string) ([]s
|
||||
id = t.ID
|
||||
case *structs.Node:
|
||||
id = t.ID
|
||||
case *structs.NodePool:
|
||||
id = t.Name
|
||||
case *structs.Deployment:
|
||||
id = t.ID
|
||||
case *structs.CSIPlugin:
|
||||
@@ -216,6 +219,10 @@ func (s *Search) fuzzyMatchSingle(raw interface{}, text string) (structs.Context
|
||||
name = t.Name
|
||||
scope = []string{t.ID}
|
||||
ctx = structs.Nodes
|
||||
case *structs.NodePool:
|
||||
name = t.Name
|
||||
scope = []string{t.Name}
|
||||
ctx = structs.NodePools
|
||||
case *structs.Namespace:
|
||||
name = t.Name
|
||||
ctx = structs.Namespaces
|
||||
@@ -381,6 +388,15 @@ func getResourceIter(context structs.Context, aclObj *acl.ACL, namespace, prefix
|
||||
return store.AllocsByIDPrefix(ws, namespace, prefix, state.SortDefault)
|
||||
case structs.Nodes:
|
||||
return store.NodesByIDPrefix(ws, prefix)
|
||||
case structs.NodePools:
|
||||
iter, err := store.NodePoolsByNamePrefix(ws, prefix, state.SortDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if aclObj == nil || aclObj.IsManagement() {
|
||||
return iter, nil
|
||||
}
|
||||
return memdb.NewFilterIterator(iter, nodePoolCapFilter(aclObj)), nil
|
||||
case structs.Deployments:
|
||||
return store.DeploymentsByIDPrefix(ws, namespace, prefix, state.SortDefault)
|
||||
case structs.Plugins:
|
||||
@@ -449,6 +465,17 @@ func getFuzzyResourceIterator(context structs.Context, aclObj *acl.ACL, namespac
|
||||
}
|
||||
return store.Nodes(ws)
|
||||
|
||||
case structs.NodePools:
|
||||
iter, err := store.NodePools(ws, state.SortDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if aclObj == nil || aclObj.IsManagement() {
|
||||
return iter, nil
|
||||
}
|
||||
return memdb.NewFilterIterator(iter, nodePoolCapFilter(aclObj)), nil
|
||||
|
||||
case structs.Plugins:
|
||||
if wildcard(namespace) {
|
||||
iter, err := store.CSIPlugins(ws)
|
||||
@@ -507,6 +534,15 @@ func nsCapFilter(aclObj *acl.ACL) memdb.FilterFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// nodePoolCapFilter produces a memdb.FilterFunc for removing node pools not
|
||||
// accessible by aclObj during a table scan.
|
||||
func nodePoolCapFilter(aclObj *acl.ACL) memdb.FilterFunc {
|
||||
return func(v interface{}) bool {
|
||||
pool := v.(*structs.NodePool)
|
||||
return !aclObj.AllowNodePoolOperation(pool.Name, acl.NodePoolCapabilityRead)
|
||||
}
|
||||
}
|
||||
|
||||
// If the length of a prefix is odd, return a subset to the last even character
|
||||
// This only applies to UUIDs, jobs are excluded
|
||||
func roundUUIDDownIfOdd(prefix string, context structs.Context) string {
|
||||
@@ -633,11 +669,12 @@ func sufficientSearchPerms(aclObj *acl.ACL, namespace string, context structs.Co
|
||||
}
|
||||
|
||||
nodeRead := aclObj.AllowNodeRead()
|
||||
allowNodePool := aclObj.AllowNodePoolSearch()
|
||||
allowNS := aclObj.AllowNamespace(namespace)
|
||||
jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob)
|
||||
allowEnt := sufficientSearchPermsEnt(aclObj)
|
||||
|
||||
if !nodeRead && !allowNS && !allowEnt && !jobRead {
|
||||
if !nodeRead && !allowNodePool && !allowNS && !allowEnt && !jobRead {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -647,6 +684,12 @@ func sufficientSearchPerms(aclObj *acl.ACL, namespace string, context structs.Co
|
||||
switch context {
|
||||
case structs.Nodes:
|
||||
return nodeRead
|
||||
case structs.NodePools:
|
||||
// The search term alone is not enough to determine if the token is
|
||||
// allowed to access the given prefix since it may not match node pool
|
||||
// label in the policy. Node pools will be filtered when iterating over
|
||||
// the results.
|
||||
return true
|
||||
case structs.Namespaces:
|
||||
return allowNS
|
||||
case structs.Allocs, structs.Deployments, structs.Evals, structs.Jobs:
|
||||
@@ -673,7 +716,7 @@ func sufficientSearchPerms(aclObj *acl.ACL, namespace string, context structs.Co
|
||||
//
|
||||
// These types are available for fuzzy searching:
|
||||
//
|
||||
// Nodes, Namespaces, Jobs, Allocs, Plugins
|
||||
// Nodes, Node Pools, Namespaces, Jobs, Allocs, Plugins
|
||||
//
|
||||
// Jobs are a special case that expand into multiple types, and whose return
|
||||
// values include Scope which is a descending list of IDs of parent objects,
|
||||
@@ -891,6 +934,10 @@ func filteredSearchContexts(aclObj *acl.ACL, namespace string, context structs.C
|
||||
if aclObj.AllowNodeRead() {
|
||||
available = append(available, c)
|
||||
}
|
||||
case structs.NodePools:
|
||||
if aclObj.AllowNodePoolSearch() {
|
||||
available = append(available, c)
|
||||
}
|
||||
case structs.Volumes:
|
||||
if volRead {
|
||||
available = append(available, c)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -483,6 +484,201 @@ func TestSearch_PrefixSearch_Node(t *testing.T) {
|
||||
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
|
||||
}{
|
||||
{
|
||||
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",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "no policy",
|
||||
token: noPolicyToken.SecretID,
|
||||
prefix: "",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "no token",
|
||||
token: "",
|
||||
prefix: "",
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -1283,6 +1479,206 @@ func TestSearch_FuzzySearch_Node(t *testing.T) {
|
||||
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
|
||||
}{
|
||||
{
|
||||
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",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "no policy",
|
||||
token: noPolicyToken.SecretID,
|
||||
text: "dev",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "no token",
|
||||
token: "",
|
||||
text: "dev",
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const (
|
||||
Evals Context = "evals"
|
||||
Jobs Context = "jobs"
|
||||
Nodes Context = "nodes"
|
||||
NodePools Context = "node_pools"
|
||||
Namespaces Context = "namespaces"
|
||||
Quotas Context = "quotas"
|
||||
Recommendations Context = "recommendations"
|
||||
|
||||
@@ -9,10 +9,10 @@ description: The /search endpoint is used to search for Nomad objects
|
||||
## Prefix Searching
|
||||
|
||||
The `/search` endpoint returns matches for a given prefix and context, where a
|
||||
context can be jobs, allocations, evaluations, nodes, deployments, plugins,
|
||||
namespaces, or volumes. When using Nomad Enterprise, the allowed contexts
|
||||
include quotas. Additionally, a prefix can be searched for within every
|
||||
context.
|
||||
context can be jobs, allocations, evaluations, nodes, node pools, deployments,
|
||||
plugins, namespaces, or volumes. When using Nomad Enterprise, the allowed
|
||||
contexts include quotas. Additionally, a prefix can be searched for within
|
||||
every context.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| ------ | ------------ | ------------------ |
|
||||
@@ -22,14 +22,14 @@ The table below shows this endpoint's support for
|
||||
[blocking queries](/nomad/api-docs#blocking-queries) and
|
||||
[required ACLs](/nomad/api-docs#acls).
|
||||
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | -------------------------------- |
|
||||
| `NO` | `node:read, namespace:read-jobs` |
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| `NO` | `node:read, node_pool:read, namespace:read-jobs` |
|
||||
|
||||
When ACLs are enabled, requests must have a token valid for `node:read` or
|
||||
`namespace:read-jobs` roles. If the token is only valid for `node:read`, then
|
||||
job related results will not be returned. If the token is only valid for
|
||||
`namespace:read-jobs`, then node results will not be returned.
|
||||
When ACLs are enabled, requests must have a token valid for `node:read`,
|
||||
`node_pool:read`, or `namespace:read-jobs` roles. If the token is only valid
|
||||
for a portion of these capabilities, then results will include results
|
||||
including only data readable with the given token.
|
||||
|
||||
### Parameters
|
||||
|
||||
@@ -38,8 +38,8 @@ job related results will not be returned. If the token is only valid for
|
||||
matches might be "abcd", or "aabb".
|
||||
- `Context` `(string: <required>)` - Defines the scope in which a search for a
|
||||
prefix operates. Contexts can be: "jobs", "evals", "allocs", "nodes",
|
||||
"deployment", "plugins", "volumes" or "all", where "all" means every
|
||||
context will be searched.
|
||||
"node_pools", "deployment", "plugins", "volumes" or "all", where "all" means
|
||||
every context will be searched.
|
||||
|
||||
### Sample Payload (for all contexts)
|
||||
|
||||
@@ -123,11 +123,12 @@ $ curl \
|
||||
|
||||
## Fuzzy Searching
|
||||
|
||||
The `/search/fuzzy` endpoint returns partial substring matches for a given search
|
||||
term and context, where a context can be jobs, allocations, nodes, plugins, or namespaces.
|
||||
Additionally, fuzzy searching can be done across all contexts. For better control
|
||||
over the performance implications of fuzzy searching on Nomad servers, aspects of
|
||||
fuzzy searching can be tuned through the <code>[search]</code> block in Nomad agent config.
|
||||
The `/search/fuzzy` endpoint returns partial substring matches for a given
|
||||
search term and context, where a context can be jobs, allocations, nodes, node
|
||||
pools, plugins, or namespaces. Additionally, fuzzy searching can be done across
|
||||
all contexts. For better control over the performance implications of fuzzy
|
||||
searching on Nomad servers, aspects of fuzzy searching can be tuned through
|
||||
the <code>[search]</code> block in Nomad agent config.
|
||||
|
||||
Fuzzy search results are ordered starting with closest matching terms. Items of
|
||||
a name that exactly matches the search term are listed first.
|
||||
@@ -140,27 +141,28 @@ The table below shows this endpoint's support for
|
||||
[blocking queries](/nomad/api-docs#blocking-queries) and
|
||||
[required ACLs](/nomad/api-docs#acls).
|
||||
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | ----------------------------------------------------------- |
|
||||
| `NO` | `node:read, namespace:read-jobs, namespace:csi-list-plugin` |
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | --------------------------------------------------------------------------- |
|
||||
| `NO` | `node:read, node_pool:read, namespace:read-jobs, namespace:csi-list-plugin` |
|
||||
|
||||
When ACLs are enabled, requests must have a token valid for `node:read`, `plugin:read` or
|
||||
`namespace:read-jobs` roles. If the token is only valid for a portion of these
|
||||
capabilities, then results will include results including only data readable with
|
||||
the given token.
|
||||
When ACLs are enabled, requests must have a token valid for `node:read`,
|
||||
`node_pool:read`, `plugin:read`, or `namespace:read-jobs` roles. If the token
|
||||
is only valid for a portion of these capabilities, then results will include
|
||||
results including only data readable with the given token.
|
||||
|
||||
### Parameters
|
||||
|
||||
- `Text` `(string: <required>)` - Specifies the identifier against which
|
||||
matches will be found. For example, if the given text were "py", potential
|
||||
fuzzy matches might be "python", "spying", or "happy".
|
||||
|
||||
- `Context` `(string: <required>)` - Defines the scope in which a search for a
|
||||
prefix operates. Contexts can be: "jobs", "allocs", "nodes", "plugins", or
|
||||
"all", where "all" means every context will be searched. When "all" is selected,
|
||||
additional prefix matches will be included for the "deployments", "evals", and
|
||||
"volumes" types. When searching in the "jobs" context, results that fuzzy match
|
||||
"groups", "services", "tasks", "images", "commands", and "classes" are also
|
||||
included in the results.
|
||||
prefix operates. Contexts can be: "jobs", "allocs", "nodes", "node_pools",
|
||||
"plugins", or "all", where "all" means every context will be searched. When
|
||||
"all" is selected, additional prefix matches will be included for the
|
||||
"deployments", "evals", and "volumes" types. When searching in the "jobs"
|
||||
context, results that fuzzy match "groups", "services", "tasks", "images",
|
||||
"commands", and "classes" are also included in the results.
|
||||
|
||||
### Scope
|
||||
|
||||
@@ -339,6 +341,51 @@ $ curl \
|
||||
|
||||
- `Scope[0]` : Node ID
|
||||
|
||||
### Sample Payload (for node pools)
|
||||
|
||||
```json
|
||||
{
|
||||
"Text": "lab",
|
||||
"Context": "node_pools"
|
||||
}
|
||||
```
|
||||
|
||||
### Sample Request
|
||||
|
||||
```shell-session
|
||||
$ curl \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
https://localhost:4646/v1/search/fuzzy
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"Index": 9,
|
||||
"KnownLeader": true,
|
||||
"LastContact": 0,
|
||||
"Matches": {
|
||||
"node_pools": [
|
||||
{
|
||||
"ID": "dev-lab1",
|
||||
"Scope": [
|
||||
"dev-lab1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Truncations": {
|
||||
"nodes": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Scope (node pools)
|
||||
|
||||
- `Scope[0]` : Node Pool Name
|
||||
|
||||
### Sample Payload (for allocs)
|
||||
|
||||
```json
|
||||
|
||||
Reference in New Issue
Block a user