1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
13 "git.curoverse.com/arvados.git/sdk/go/arvados"
14 "git.curoverse.com/arvados.git/sdk/go/auth"
15 "git.curoverse.com/arvados.git/sdk/go/ctxlog"
16 "git.curoverse.com/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)
301 if route.endpoint.Method == "PATCH" {
302 // Accept PUT as a synonym for PATCH.
303 endpointPUT := route.endpoint
304 endpointPUT.Method = "PUT"
305 rtr.addRoute(endpointPUT, route.defaultOpts, route.exec)
308 rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
309 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
311 rtr.mux.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
312 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
316 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
317 rtr.mux.Methods(endpoint.Method).Path("/" + endpoint.Path).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
318 logger := ctxlog.FromContext(req.Context())
319 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
321 logger.WithFields(logrus.Fields{
323 "method": endpoint.Method,
324 "endpoint": endpoint,
325 }).WithError(err).Debug("error loading request params")
326 rtr.sendError(w, err)
329 opts := defaultOpts()
330 err = rtr.transcode(params, opts)
332 logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
333 rtr.sendError(w, err)
336 respOpts, err := rtr.responseOptions(opts)
338 logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
339 rtr.sendError(w, err)
343 creds := auth.CredentialsFromRequest(req)
344 err = creds.LoadTokensFromHTTPRequestBody(req)
346 rtr.sendError(w, fmt.Errorf("error loading tokens from request body: %s", err))
349 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
350 for _, t := range rt {
351 if t, ok := t.(string); ok {
352 creds.Tokens = append(creds.Tokens, t)
356 ctx := auth.NewContext(req.Context(), creds)
357 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
358 logger.WithFields(logrus.Fields{
359 "apiEndpoint": endpoint,
360 "apiOptsType": fmt.Sprintf("%T", opts),
363 resp, err := exec(ctx, opts)
365 logger.WithError(err).Debugf("returning error type %T", err)
366 rtr.sendError(w, err)
369 rtr.sendResponse(w, req, resp, respOpts)
373 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
374 switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
375 case "login", "logout", "auth":
377 w.Header().Set("Access-Control-Allow-Origin", "*")
378 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE")
379 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
380 w.Header().Set("Access-Control-Max-Age", "86486400")
382 if r.Method == "OPTIONS" {
386 if m := r.FormValue("_method"); m != "" {
391 rtr.mux.ServeHTTP(w, r)