gopkg.in/hashicorp/nomad.v0@v0.11.8/nomad/search_endpoint.go (about)

     1  package nomad
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"time"
     7  
     8  	metrics "github.com/armon/go-metrics"
     9  	log "github.com/hashicorp/go-hclog"
    10  	memdb "github.com/hashicorp/go-memdb"
    11  
    12  	"github.com/hashicorp/nomad/acl"
    13  	"github.com/hashicorp/nomad/nomad/state"
    14  	"github.com/hashicorp/nomad/nomad/structs"
    15  )
    16  
    17  const (
    18  	// truncateLimit is the maximum number of matches that will be returned for a
    19  	// prefix for a specific context
    20  	truncateLimit = 20
    21  )
    22  
    23  var (
    24  	// ossContexts are the oss contexts which are searched to find matches
    25  	// for a given prefix
    26  	ossContexts = []structs.Context{
    27  		structs.Allocs,
    28  		structs.Jobs,
    29  		structs.Nodes,
    30  		structs.Evals,
    31  		structs.Deployments,
    32  		structs.Plugins,
    33  		structs.Volumes,
    34  	}
    35  )
    36  
    37  // Search endpoint is used to look up matches for a given prefix and context
    38  type Search struct {
    39  	srv    *Server
    40  	logger log.Logger
    41  }
    42  
    43  // getMatches extracts matches for an iterator, and returns a list of ids for
    44  // these matches.
    45  func (s *Search) getMatches(iter memdb.ResultIterator, prefix string) ([]string, bool) {
    46  	var matches []string
    47  
    48  	for i := 0; i < truncateLimit; i++ {
    49  		raw := iter.Next()
    50  		if raw == nil {
    51  			break
    52  		}
    53  
    54  		var id string
    55  		switch t := raw.(type) {
    56  		case *structs.Job:
    57  			id = t.ID
    58  		case *structs.Evaluation:
    59  			id = t.ID
    60  		case *structs.Allocation:
    61  			id = t.ID
    62  		case *structs.Node:
    63  			id = t.ID
    64  		case *structs.Deployment:
    65  			id = t.ID
    66  		case *structs.CSIPlugin:
    67  			id = t.ID
    68  		case *structs.CSIVolume:
    69  			id = t.ID
    70  		default:
    71  			matchID, ok := getEnterpriseMatch(raw)
    72  			if !ok {
    73  				s.logger.Error("unexpected type for resources context", "type", fmt.Sprintf("%T", t))
    74  				continue
    75  			}
    76  
    77  			id = matchID
    78  		}
    79  
    80  		if !strings.HasPrefix(id, prefix) {
    81  			continue
    82  		}
    83  
    84  		matches = append(matches, id)
    85  	}
    86  
    87  	return matches, iter.Next() != nil
    88  }
    89  
    90  // getResourceIter takes a context and returns a memdb iterator specific to
    91  // that context
    92  func getResourceIter(context structs.Context, aclObj *acl.ACL, namespace, prefix string, ws memdb.WatchSet, state *state.StateStore) (memdb.ResultIterator, error) {
    93  	switch context {
    94  	case structs.Jobs:
    95  		return state.JobsByIDPrefix(ws, namespace, prefix)
    96  	case structs.Evals:
    97  		return state.EvalsByIDPrefix(ws, namespace, prefix)
    98  	case structs.Allocs:
    99  		return state.AllocsByIDPrefix(ws, namespace, prefix)
   100  	case structs.Nodes:
   101  		return state.NodesByIDPrefix(ws, prefix)
   102  	case structs.Deployments:
   103  		return state.DeploymentsByIDPrefix(ws, namespace, prefix)
   104  	case structs.Plugins:
   105  		return state.CSIPluginsByIDPrefix(ws, prefix)
   106  	case structs.Volumes:
   107  		return state.CSIVolumesByIDPrefix(ws, namespace, prefix)
   108  	default:
   109  		return getEnterpriseResourceIter(context, aclObj, namespace, prefix, ws, state)
   110  	}
   111  }
   112  
   113  // If the length of a prefix is odd, return a subset to the last even character
   114  // This only applies to UUIDs, jobs are excluded
   115  func roundUUIDDownIfOdd(prefix string, context structs.Context) string {
   116  	if context == structs.Jobs {
   117  		return prefix
   118  	}
   119  
   120  	// We ignore the count of hyphens when calculating if the prefix is even:
   121  	// E.g "e3671fa4-21"
   122  	numHyphens := strings.Count(prefix, "-")
   123  	l := len(prefix) - numHyphens
   124  	if l%2 == 0 {
   125  		return prefix
   126  	}
   127  	return prefix[:len(prefix)-1]
   128  }
   129  
   130  // PrefixSearch is used to list matches for a given prefix, and returns
   131  // matching jobs, evaluations, allocations, and/or nodes.
   132  func (s *Search) PrefixSearch(args *structs.SearchRequest, reply *structs.SearchResponse) error {
   133  	if done, err := s.srv.forward("Search.PrefixSearch", args, args, reply); done {
   134  		return err
   135  	}
   136  	defer metrics.MeasureSince([]string{"nomad", "search", "prefix_search"}, time.Now())
   137  
   138  	aclObj, err := s.srv.ResolveToken(args.AuthToken)
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	namespace := args.RequestNamespace()
   144  
   145  	// Require either node:read or namespace:read-job
   146  	if !anySearchPerms(aclObj, namespace, args.Context) {
   147  		return structs.ErrPermissionDenied
   148  	}
   149  
   150  	reply.Matches = make(map[structs.Context][]string)
   151  	reply.Truncations = make(map[structs.Context]bool)
   152  
   153  	// Setup the blocking query
   154  	opts := blockingOptions{
   155  		queryMeta: &reply.QueryMeta,
   156  		queryOpts: &structs.QueryOptions{},
   157  		run: func(ws memdb.WatchSet, state *state.StateStore) error {
   158  
   159  			iters := make(map[structs.Context]memdb.ResultIterator)
   160  
   161  			contexts := searchContexts(aclObj, namespace, args.Context)
   162  
   163  			for _, ctx := range contexts {
   164  				iter, err := getResourceIter(ctx, aclObj, namespace, roundUUIDDownIfOdd(args.Prefix, args.Context), ws, state)
   165  				if err != nil {
   166  					e := err.Error()
   167  					switch {
   168  					// Searching other contexts with job names raises an error, which in
   169  					// this case we want to ignore.
   170  					case strings.Contains(e, "Invalid UUID: encoding/hex"):
   171  					case strings.Contains(e, "UUID have 36 characters"):
   172  					case strings.Contains(e, "must be even length"):
   173  					case strings.Contains(e, "UUID should have maximum of 4"):
   174  					default:
   175  						return err
   176  					}
   177  				} else {
   178  					iters[ctx] = iter
   179  				}
   180  			}
   181  
   182  			// Return matches for the given prefix
   183  			for k, v := range iters {
   184  				res, isTrunc := s.getMatches(v, args.Prefix)
   185  				reply.Matches[k] = res
   186  				reply.Truncations[k] = isTrunc
   187  			}
   188  
   189  			// Set the index for the context. If the context has been specified, it
   190  			// will be used as the index of the response. Otherwise, the
   191  			// maximum index from all resources will be used.
   192  			for _, ctx := range contexts {
   193  				index, err := state.Index(contextToIndex(ctx))
   194  				if err != nil {
   195  					return err
   196  				}
   197  				if index > reply.Index {
   198  					reply.Index = index
   199  				}
   200  			}
   201  
   202  			s.srv.setQueryMeta(&reply.QueryMeta)
   203  			return nil
   204  		}}
   205  	return s.srv.blockingRPC(&opts)
   206  }