github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/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 }