open-nomad/nomad/search_endpoint_oss.go
Seth Hoenig 1ee8d5ffc5 api: implement fuzzy search API
This PR introduces the /v1/search/fuzzy API endpoint, used for fuzzy
searching objects in Nomad. The fuzzy search endpoint routes requests
to the Nomad Server leader, which implements the Search.FuzzySearch RPC
method.

Requests to the fuzzy search API are based on the api.FuzzySearchRequest
object, e.g.

{
  "Text": "ed",
  "Context": "all"
}

Responses from the fuzzy search API are based on the api.FuzzySearchResponse
object, e.g.

{
  "Index": 27,
  "KnownLeader": true,
  "LastContact": 0,
  "Matches": {
    "tasks": [
      {
        "ID": "redis",
        "Scope": [
          "default",
          "example",
          "cache"
        ]
      }
    ],
    "evals": [],
    "deployment": [],
    "volumes": [],
    "scaling_policy": [],
    "images": [
      {
        "ID": "redis:3.2",
        "Scope": [
          "default",
          "example",
          "cache",
          "redis"
        ]
      }
    ]
  },
  "Truncations": {
    "volumes": false,
    "scaling_policy": false,
    "evals": false,
    "deployment": false
  }
}

The API is tunable using the new server.search stanza, e.g.

server {
  search {
    fuzzy_enabled   = true
    limit_query     = 200
    limit_results   = 1000
    min_term_length = 5
  }
}

These values can be increased or decreased, so as to provide more
search results or to reduce load on the Nomad Server. The fuzzy search
API can be disabled entirely by setting `fuzzy_enabled` to `false`.
2021-04-16 16:36:07 -06:00

137 lines
4.2 KiB
Go

// +build !ent
package nomad
import (
"fmt"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/structs"
)
var (
// allContexts are the available contexts which are searched to find matches
// for a given prefix
allContexts = ossContexts
)
// contextToIndex returns the index name to lookup in the state store.
func contextToIndex(ctx structs.Context) string {
return string(ctx)
}
// getEnterpriseMatch is a no-op in oss since there are no enterprise objects.
func getEnterpriseMatch(match interface{}) (id string, ok bool) {
return "", false
}
// getEnterpriseResourceIter is used to retrieve an iterator over an enterprise
// only table.
func getEnterpriseResourceIter(context structs.Context, _ *acl.ACL, namespace, prefix string, ws memdb.WatchSet, state *state.StateStore) (memdb.ResultIterator, error) {
// If we have made it here then it is an error since we have exhausted all
// open source contexts.
return nil, fmt.Errorf("context must be one of %v or 'all' for all contexts; got %q", allContexts, context)
}
// getEnterpriseFuzzyResourceIter is used to retrieve an iterator over an enterprise
// only table.
func getEnterpriseFuzzyResourceIter(context structs.Context, _ *acl.ACL, _ string, _ memdb.WatchSet, _ *state.StateStore) (memdb.ResultIterator, error) {
return nil, fmt.Errorf("context must be one of %v or 'all' for all contexts; got %q", allContexts, context)
}
// sufficientSearchPerms returns true if the provided ACL has access to each
// capability required for prefix searching for the given context.
//
// Returns true if aclObj is nil.
func sufficientSearchPerms(aclObj *acl.ACL, namespace string, context structs.Context) bool {
if aclObj == nil {
return true
}
nodeRead := aclObj.AllowNodeRead()
allowNS := aclObj.AllowNamespace(namespace)
jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob)
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume,
acl.NamespaceCapabilityCSIReadVolume,
acl.NamespaceCapabilityListJobs,
acl.NamespaceCapabilityReadJob)
volRead := allowVolume(aclObj, namespace)
if !nodeRead && !jobRead && !volRead && !allowNS {
return false
}
// Reject requests that explicitly specify a disallowed context. This
// should give the user better feedback then simply filtering out all
// results and returning an empty list.
if !nodeRead && context == structs.Nodes {
return false
}
if !allowNS && context == structs.Namespaces {
return false
}
if !jobRead {
switch context {
case structs.Allocs, structs.Deployments, structs.Evals, structs.Jobs:
return false
}
}
if !volRead && context == structs.Volumes {
return false
}
return true
}
// filteredSearchContexts returns the expanded set of contexts, filtered down
// to the subset of contexts the aclObj is valid for.
//
// If aclObj is nil, no contexts are filtered out.
func filteredSearchContexts(aclObj *acl.ACL, namespace string, context structs.Context) []structs.Context {
desired := expandContext(context)
// If ACLs aren't enabled return all contexts
if aclObj == nil {
return desired
}
jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob)
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume,
acl.NamespaceCapabilityCSIReadVolume,
acl.NamespaceCapabilityListJobs,
acl.NamespaceCapabilityReadJob)
volRead := allowVolume(aclObj, namespace)
policyRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityListScalingPolicies)
// Filter contexts down to those the ACL grants access to
available := make([]structs.Context, 0, len(desired))
for _, c := range desired {
switch c {
case structs.Allocs, structs.Jobs, structs.Evals, structs.Deployments:
if jobRead {
available = append(available, c)
}
case structs.ScalingPolicies:
if policyRead || jobRead {
available = append(available, c)
}
case structs.Namespaces:
if aclObj.AllowNamespace(namespace) {
available = append(available, c)
}
case structs.Nodes:
if aclObj.AllowNodeRead() {
available = append(available, c)
}
case structs.Volumes:
if volRead {
available = append(available, c)
}
}
}
return available
}