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"
26 func New(fed arvados.API) *router {
35 type routableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
37 func (rtr *router) addRoutes() {
38 for _, route := range []struct {
39 endpoint arvados.APIEndpoint
40 defaultOpts func() interface{}
44 arvados.EndpointConfigGet,
45 func() interface{} { return &struct{}{} },
46 func(ctx context.Context, opts interface{}) (interface{}, error) {
47 return rtr.fed.ConfigGet(ctx)
51 arvados.EndpointLogin,
52 func() interface{} { return &arvados.LoginOptions{} },
53 func(ctx context.Context, opts interface{}) (interface{}, error) {
54 return rtr.fed.Login(ctx, *opts.(*arvados.LoginOptions))
58 arvados.EndpointCollectionCreate,
59 func() interface{} { return &arvados.CreateOptions{} },
60 func(ctx context.Context, opts interface{}) (interface{}, error) {
61 return rtr.fed.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
65 arvados.EndpointCollectionUpdate,
66 func() interface{} { return &arvados.UpdateOptions{} },
67 func(ctx context.Context, opts interface{}) (interface{}, error) {
68 return rtr.fed.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
72 arvados.EndpointCollectionGet,
73 func() interface{} { return &arvados.GetOptions{} },
74 func(ctx context.Context, opts interface{}) (interface{}, error) {
75 return rtr.fed.CollectionGet(ctx, *opts.(*arvados.GetOptions))
79 arvados.EndpointCollectionList,
80 func() interface{} { return &arvados.ListOptions{Limit: -1} },
81 func(ctx context.Context, opts interface{}) (interface{}, error) {
82 return rtr.fed.CollectionList(ctx, *opts.(*arvados.ListOptions))
86 arvados.EndpointCollectionProvenance,
87 func() interface{} { return &arvados.GetOptions{} },
88 func(ctx context.Context, opts interface{}) (interface{}, error) {
89 return rtr.fed.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
93 arvados.EndpointCollectionUsedBy,
94 func() interface{} { return &arvados.GetOptions{} },
95 func(ctx context.Context, opts interface{}) (interface{}, error) {
96 return rtr.fed.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
100 arvados.EndpointCollectionDelete,
101 func() interface{} { return &arvados.DeleteOptions{} },
102 func(ctx context.Context, opts interface{}) (interface{}, error) {
103 return rtr.fed.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
107 arvados.EndpointCollectionTrash,
108 func() interface{} { return &arvados.DeleteOptions{} },
109 func(ctx context.Context, opts interface{}) (interface{}, error) {
110 return rtr.fed.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
114 arvados.EndpointCollectionUntrash,
115 func() interface{} { return &arvados.UntrashOptions{} },
116 func(ctx context.Context, opts interface{}) (interface{}, error) {
117 return rtr.fed.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
121 arvados.EndpointContainerCreate,
122 func() interface{} { return &arvados.CreateOptions{} },
123 func(ctx context.Context, opts interface{}) (interface{}, error) {
124 return rtr.fed.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
128 arvados.EndpointContainerUpdate,
129 func() interface{} { return &arvados.UpdateOptions{} },
130 func(ctx context.Context, opts interface{}) (interface{}, error) {
131 return rtr.fed.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
135 arvados.EndpointContainerGet,
136 func() interface{} { return &arvados.GetOptions{} },
137 func(ctx context.Context, opts interface{}) (interface{}, error) {
138 return rtr.fed.ContainerGet(ctx, *opts.(*arvados.GetOptions))
142 arvados.EndpointContainerList,
143 func() interface{} { return &arvados.ListOptions{Limit: -1} },
144 func(ctx context.Context, opts interface{}) (interface{}, error) {
145 return rtr.fed.ContainerList(ctx, *opts.(*arvados.ListOptions))
149 arvados.EndpointContainerDelete,
150 func() interface{} { return &arvados.DeleteOptions{} },
151 func(ctx context.Context, opts interface{}) (interface{}, error) {
152 return rtr.fed.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
156 arvados.EndpointContainerLock,
158 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
160 func(ctx context.Context, opts interface{}) (interface{}, error) {
161 return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
165 arvados.EndpointContainerUnlock,
167 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
169 func(ctx context.Context, opts interface{}) (interface{}, error) {
170 return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
174 arvados.EndpointSpecimenCreate,
175 func() interface{} { return &arvados.CreateOptions{} },
176 func(ctx context.Context, opts interface{}) (interface{}, error) {
177 return rtr.fed.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
181 arvados.EndpointSpecimenUpdate,
182 func() interface{} { return &arvados.UpdateOptions{} },
183 func(ctx context.Context, opts interface{}) (interface{}, error) {
184 return rtr.fed.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
188 arvados.EndpointSpecimenGet,
189 func() interface{} { return &arvados.GetOptions{} },
190 func(ctx context.Context, opts interface{}) (interface{}, error) {
191 return rtr.fed.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
195 arvados.EndpointSpecimenList,
196 func() interface{} { return &arvados.ListOptions{Limit: -1} },
197 func(ctx context.Context, opts interface{}) (interface{}, error) {
198 return rtr.fed.SpecimenList(ctx, *opts.(*arvados.ListOptions))
202 arvados.EndpointSpecimenDelete,
203 func() interface{} { return &arvados.DeleteOptions{} },
204 func(ctx context.Context, opts interface{}) (interface{}, error) {
205 return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
209 arvados.EndpointUserCreate,
210 func() interface{} { return &arvados.CreateOptions{} },
211 func(ctx context.Context, opts interface{}) (interface{}, error) {
212 return rtr.fed.UserCreate(ctx, *opts.(*arvados.CreateOptions))
216 arvados.EndpointUserMerge,
217 func() interface{} { return &arvados.UserMergeOptions{} },
218 func(ctx context.Context, opts interface{}) (interface{}, error) {
219 return rtr.fed.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
223 arvados.EndpointUserActivate,
224 func() interface{} { return &arvados.UserActivateOptions{} },
225 func(ctx context.Context, opts interface{}) (interface{}, error) {
226 return rtr.fed.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
230 arvados.EndpointUserSetup,
231 func() interface{} { return &arvados.UserSetupOptions{} },
232 func(ctx context.Context, opts interface{}) (interface{}, error) {
233 return rtr.fed.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
237 arvados.EndpointUserUnsetup,
238 func() interface{} { return &arvados.GetOptions{} },
239 func(ctx context.Context, opts interface{}) (interface{}, error) {
240 return rtr.fed.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
244 arvados.EndpointUserGetCurrent,
245 func() interface{} { return &arvados.GetOptions{} },
246 func(ctx context.Context, opts interface{}) (interface{}, error) {
247 return rtr.fed.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
251 arvados.EndpointUserGetSystem,
252 func() interface{} { return &arvados.GetOptions{} },
253 func(ctx context.Context, opts interface{}) (interface{}, error) {
254 return rtr.fed.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
258 arvados.EndpointUserGet,
259 func() interface{} { return &arvados.GetOptions{} },
260 func(ctx context.Context, opts interface{}) (interface{}, error) {
261 return rtr.fed.UserGet(ctx, *opts.(*arvados.GetOptions))
265 arvados.EndpointUserUpdateUUID,
266 func() interface{} { return &arvados.UpdateUUIDOptions{} },
267 func(ctx context.Context, opts interface{}) (interface{}, error) {
268 return rtr.fed.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
272 arvados.EndpointUserUpdate,
273 func() interface{} { return &arvados.UpdateOptions{} },
274 func(ctx context.Context, opts interface{}) (interface{}, error) {
275 return rtr.fed.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
279 arvados.EndpointUserList,
280 func() interface{} { return &arvados.ListOptions{Limit: -1} },
281 func(ctx context.Context, opts interface{}) (interface{}, error) {
282 return rtr.fed.UserList(ctx, *opts.(*arvados.ListOptions))
286 arvados.EndpointUserBatchUpdate,
287 func() interface{} { return &arvados.UserBatchUpdateOptions{} },
288 func(ctx context.Context, opts interface{}) (interface{}, error) {
289 return rtr.fed.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
293 arvados.EndpointUserDelete,
294 func() interface{} { return &arvados.DeleteOptions{} },
295 func(ctx context.Context, opts interface{}) (interface{}, error) {
296 return rtr.fed.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
300 rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
302 rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
303 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
305 rtr.mux.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
306 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
310 var altMethod = map[string]string{
311 "PATCH": "PUT", // Accept PUT as a synonym for PATCH
312 "GET": "HEAD", // Accept HEAD at any GET route
315 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
316 methods := []string{endpoint.Method}
317 if alt, ok := altMethod[endpoint.Method]; ok {
318 methods = append(methods, alt)
320 rtr.mux.Methods(methods...).Path("/" + endpoint.Path).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
321 logger := ctxlog.FromContext(req.Context())
322 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
324 logger.WithFields(logrus.Fields{
326 "method": endpoint.Method,
327 "endpoint": endpoint,
328 }).WithError(err).Debug("error loading request params")
329 rtr.sendError(w, err)
332 opts := defaultOpts()
333 err = rtr.transcode(params, opts)
335 logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
336 rtr.sendError(w, err)
339 respOpts, err := rtr.responseOptions(opts)
341 logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
342 rtr.sendError(w, err)
346 creds := auth.CredentialsFromRequest(req)
347 err = creds.LoadTokensFromHTTPRequestBody(req)
349 rtr.sendError(w, fmt.Errorf("error loading tokens from request body: %s", err))
352 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
353 for _, t := range rt {
354 if t, ok := t.(string); ok {
355 creds.Tokens = append(creds.Tokens, t)
359 ctx := auth.NewContext(req.Context(), creds)
360 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
361 logger.WithFields(logrus.Fields{
362 "apiEndpoint": endpoint,
363 "apiOptsType": fmt.Sprintf("%T", opts),
366 resp, err := exec(ctx, opts)
368 logger.WithError(err).Debugf("returning error type %T", err)
369 rtr.sendError(w, err)
372 rtr.sendResponse(w, req, resp, respOpts)
376 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
377 switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
378 case "login", "logout", "auth":
380 w.Header().Set("Access-Control-Allow-Origin", "*")
381 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, PATCH, DELETE")
382 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
383 w.Header().Set("Access-Control-Max-Age", "86486400")
385 if r.Method == "OPTIONS" {
389 if m := r.FormValue("_method"); m != "" {
393 } else if m = r.Header.Get("X-Http-Method-Override"); m != "" {
398 rtr.mux.ServeHTTP(w, r)