github.com/adityamillind98/nomad@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 }