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