github.com/djenriquez/nomad-1@v0.8.1/nomad/search_endpoint.go (about)

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