1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
13 "git.arvados.org/arvados.git/sdk/go/arvados"
14 "git.arvados.org/arvados.git/sdk/go/auth"
15 "git.arvados.org/arvados.git/sdk/go/ctxlog"
16 "git.arvados.org/arvados.git/sdk/go/httpserver"
17 "github.com/gorilla/mux"
18 "github.com/sirupsen/logrus"
24 wrapCalls func(arvados.RoutableFunc) arvados.RoutableFunc
27 // New returns a new router (which implements the http.Handler
28 // interface) that serves requests by calling Arvados API methods on
31 // If wrapCalls is not nil, it is called once for each API method, and
32 // the returned method is used in its place. This can be used to
33 // install hooks before and after each API call and alter responses;
34 // see localdb.WrapCallsInTransaction for an example.
35 func New(backend arvados.API, wrapCalls func(arvados.RoutableFunc) arvados.RoutableFunc) *router {
45 func (rtr *router) addRoutes() {
46 for _, route := range []struct {
47 endpoint arvados.APIEndpoint
48 defaultOpts func() interface{}
49 exec arvados.RoutableFunc
52 arvados.EndpointConfigGet,
53 func() interface{} { return &struct{}{} },
54 func(ctx context.Context, opts interface{}) (interface{}, error) {
55 return rtr.backend.ConfigGet(ctx)
59 arvados.EndpointLogin,
60 func() interface{} { return &arvados.LoginOptions{} },
61 func(ctx context.Context, opts interface{}) (interface{}, error) {
62 return rtr.backend.Login(ctx, *opts.(*arvados.LoginOptions))
66 arvados.EndpointLogout,
67 func() interface{} { return &arvados.LogoutOptions{} },
68 func(ctx context.Context, opts interface{}) (interface{}, error) {
69 return rtr.backend.Logout(ctx, *opts.(*arvados.LogoutOptions))
73 arvados.EndpointCollectionCreate,
74 func() interface{} { return &arvados.CreateOptions{} },
75 func(ctx context.Context, opts interface{}) (interface{}, error) {
76 return rtr.backend.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
80 arvados.EndpointCollectionUpdate,
81 func() interface{} { return &arvados.UpdateOptions{} },
82 func(ctx context.Context, opts interface{}) (interface{}, error) {
83 return rtr.backend.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
87 arvados.EndpointCollectionGet,
88 func() interface{} { return &arvados.GetOptions{} },
89 func(ctx context.Context, opts interface{}) (interface{}, error) {
90 return rtr.backend.CollectionGet(ctx, *opts.(*arvados.GetOptions))
94 arvados.EndpointCollectionList,
95 func() interface{} { return &arvados.ListOptions{Limit: -1} },
96 func(ctx context.Context, opts interface{}) (interface{}, error) {
97 return rtr.backend.CollectionList(ctx, *opts.(*arvados.ListOptions))
101 arvados.EndpointCollectionProvenance,
102 func() interface{} { return &arvados.GetOptions{} },
103 func(ctx context.Context, opts interface{}) (interface{}, error) {
104 return rtr.backend.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
108 arvados.EndpointCollectionUsedBy,
109 func() interface{} { return &arvados.GetOptions{} },
110 func(ctx context.Context, opts interface{}) (interface{}, error) {
111 return rtr.backend.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
115 arvados.EndpointCollectionDelete,
116 func() interface{} { return &arvados.DeleteOptions{} },
117 func(ctx context.Context, opts interface{}) (interface{}, error) {
118 return rtr.backend.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
122 arvados.EndpointCollectionTrash,
123 func() interface{} { return &arvados.DeleteOptions{} },
124 func(ctx context.Context, opts interface{}) (interface{}, error) {
125 return rtr.backend.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
129 arvados.EndpointCollectionUntrash,
130 func() interface{} { return &arvados.UntrashOptions{} },
131 func(ctx context.Context, opts interface{}) (interface{}, error) {
132 return rtr.backend.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
136 arvados.EndpointContainerCreate,
137 func() interface{} { return &arvados.CreateOptions{} },
138 func(ctx context.Context, opts interface{}) (interface{}, error) {
139 return rtr.backend.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
143 arvados.EndpointContainerUpdate,
144 func() interface{} { return &arvados.UpdateOptions{} },
145 func(ctx context.Context, opts interface{}) (interface{}, error) {
146 return rtr.backend.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
150 arvados.EndpointContainerGet,
151 func() interface{} { return &arvados.GetOptions{} },
152 func(ctx context.Context, opts interface{}) (interface{}, error) {
153 return rtr.backend.ContainerGet(ctx, *opts.(*arvados.GetOptions))
157 arvados.EndpointContainerList,
158 func() interface{} { return &arvados.ListOptions{Limit: -1} },
159 func(ctx context.Context, opts interface{}) (interface{}, error) {
160 return rtr.backend.ContainerList(ctx, *opts.(*arvados.ListOptions))
164 arvados.EndpointContainerDelete,
165 func() interface{} { return &arvados.DeleteOptions{} },
166 func(ctx context.Context, opts interface{}) (interface{}, error) {
167 return rtr.backend.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
171 arvados.EndpointContainerLock,
173 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
175 func(ctx context.Context, opts interface{}) (interface{}, error) {
176 return rtr.backend.ContainerLock(ctx, *opts.(*arvados.GetOptions))
180 arvados.EndpointContainerUnlock,
182 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
184 func(ctx context.Context, opts interface{}) (interface{}, error) {
185 return rtr.backend.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
189 arvados.EndpointSpecimenCreate,
190 func() interface{} { return &arvados.CreateOptions{} },
191 func(ctx context.Context, opts interface{}) (interface{}, error) {
192 return rtr.backend.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
196 arvados.EndpointSpecimenUpdate,
197 func() interface{} { return &arvados.UpdateOptions{} },
198 func(ctx context.Context, opts interface{}) (interface{}, error) {
199 return rtr.backend.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
203 arvados.EndpointSpecimenGet,
204 func() interface{} { return &arvados.GetOptions{} },
205 func(ctx context.Context, opts interface{}) (interface{}, error) {
206 return rtr.backend.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
210 arvados.EndpointSpecimenList,
211 func() interface{} { return &arvados.ListOptions{Limit: -1} },
212 func(ctx context.Context, opts interface{}) (interface{}, error) {
213 return rtr.backend.SpecimenList(ctx, *opts.(*arvados.ListOptions))
217 arvados.EndpointSpecimenDelete,
218 func() interface{} { return &arvados.DeleteOptions{} },
219 func(ctx context.Context, opts interface{}) (interface{}, error) {
220 return rtr.backend.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
224 arvados.EndpointUserCreate,
225 func() interface{} { return &arvados.CreateOptions{} },
226 func(ctx context.Context, opts interface{}) (interface{}, error) {
227 return rtr.backend.UserCreate(ctx, *opts.(*arvados.CreateOptions))
231 arvados.EndpointUserMerge,
232 func() interface{} { return &arvados.UserMergeOptions{} },
233 func(ctx context.Context, opts interface{}) (interface{}, error) {
234 return rtr.backend.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
238 arvados.EndpointUserActivate,
239 func() interface{} { return &arvados.UserActivateOptions{} },
240 func(ctx context.Context, opts interface{}) (interface{}, error) {
241 return rtr.backend.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
245 arvados.EndpointUserSetup,
246 func() interface{} { return &arvados.UserSetupOptions{} },
247 func(ctx context.Context, opts interface{}) (interface{}, error) {
248 return rtr.backend.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
252 arvados.EndpointUserUnsetup,
253 func() interface{} { return &arvados.GetOptions{} },
254 func(ctx context.Context, opts interface{}) (interface{}, error) {
255 return rtr.backend.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
259 arvados.EndpointUserGetCurrent,
260 func() interface{} { return &arvados.GetOptions{} },
261 func(ctx context.Context, opts interface{}) (interface{}, error) {
262 return rtr.backend.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
266 arvados.EndpointUserGetSystem,
267 func() interface{} { return &arvados.GetOptions{} },
268 func(ctx context.Context, opts interface{}) (interface{}, error) {
269 return rtr.backend.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
273 arvados.EndpointUserGet,
274 func() interface{} { return &arvados.GetOptions{} },
275 func(ctx context.Context, opts interface{}) (interface{}, error) {
276 return rtr.backend.UserGet(ctx, *opts.(*arvados.GetOptions))
280 arvados.EndpointUserUpdateUUID,
281 func() interface{} { return &arvados.UpdateUUIDOptions{} },
282 func(ctx context.Context, opts interface{}) (interface{}, error) {
283 return rtr.backend.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
287 arvados.EndpointUserUpdate,
288 func() interface{} { return &arvados.UpdateOptions{} },
289 func(ctx context.Context, opts interface{}) (interface{}, error) {
290 return rtr.backend.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
294 arvados.EndpointUserList,
295 func() interface{} { return &arvados.ListOptions{Limit: -1} },
296 func(ctx context.Context, opts interface{}) (interface{}, error) {
297 return rtr.backend.UserList(ctx, *opts.(*arvados.ListOptions))
301 arvados.EndpointUserBatchUpdate,
302 func() interface{} { return &arvados.UserBatchUpdateOptions{} },
303 func(ctx context.Context, opts interface{}) (interface{}, error) {
304 return rtr.backend.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
308 arvados.EndpointUserDelete,
309 func() interface{} { return &arvados.DeleteOptions{} },
310 func(ctx context.Context, opts interface{}) (interface{}, error) {
311 return rtr.backend.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
315 arvados.EndpointUserAuthenticate,
316 func() interface{} { return &arvados.UserAuthenticateOptions{} },
317 func(ctx context.Context, opts interface{}) (interface{}, error) {
318 return rtr.backend.UserAuthenticate(ctx, *opts.(*arvados.UserAuthenticateOptions))
323 if rtr.wrapCalls != nil {
324 exec = rtr.wrapCalls(exec)
326 rtr.addRoute(route.endpoint, route.defaultOpts, exec)
328 rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
329 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
331 rtr.mux.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
332 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
336 var altMethod = map[string]string{
337 "PATCH": "PUT", // Accept PUT as a synonym for PATCH
338 "GET": "HEAD", // Accept HEAD at any GET route
341 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec arvados.RoutableFunc) {
342 methods := []string{endpoint.Method}
343 if alt, ok := altMethod[endpoint.Method]; ok {
344 methods = append(methods, alt)
346 rtr.mux.Methods(methods...).Path("/" + endpoint.Path).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
347 logger := ctxlog.FromContext(req.Context())
348 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
350 logger.WithFields(logrus.Fields{
352 "method": endpoint.Method,
353 "endpoint": endpoint,
354 }).WithError(err).Debug("error loading request params")
355 rtr.sendError(w, err)
358 opts := defaultOpts()
359 err = rtr.transcode(params, opts)
361 logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
362 rtr.sendError(w, err)
365 respOpts, err := rtr.responseOptions(opts)
367 logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
368 rtr.sendError(w, err)
372 creds := auth.CredentialsFromRequest(req)
373 err = creds.LoadTokensFromHTTPRequestBody(req)
375 rtr.sendError(w, fmt.Errorf("error loading tokens from request body: %s", err))
378 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
379 for _, t := range rt {
380 if t, ok := t.(string); ok {
381 creds.Tokens = append(creds.Tokens, t)
385 ctx := auth.NewContext(req.Context(), creds)
386 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
387 logger.WithFields(logrus.Fields{
388 "apiEndpoint": endpoint,
389 "apiOptsType": fmt.Sprintf("%T", opts),
392 resp, err := exec(ctx, opts)
394 logger.WithError(err).Debugf("returning error type %T", err)
395 rtr.sendError(w, err)
398 rtr.sendResponse(w, req, resp, respOpts)
402 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
403 switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
404 case "login", "logout", "auth":
406 w.Header().Set("Access-Control-Allow-Origin", "*")
407 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, PATCH, DELETE")
408 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Http-Method-Override")
409 w.Header().Set("Access-Control-Max-Age", "86486400")
411 if r.Method == "OPTIONS" {
414 if r.Method == "POST" {
416 if m := r.FormValue("_method"); m != "" {
420 } else if m = r.Header.Get("X-Http-Method-Override"); m != "" {
426 rtr.mux.ServeHTTP(w, r)