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