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/julienschmidt/httprouter"
18 "github.com/sirupsen/logrus"
22 mux *httprouter.Router
26 func New(fed arvados.API) *router {
28 mux: httprouter.New(),
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 rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
210 if route.endpoint.Method == "PATCH" {
211 // Accept PUT as a synonym for PATCH.
212 endpointPUT := route.endpoint
213 endpointPUT.Method = "PUT"
214 rtr.addRoute(endpointPUT, route.defaultOpts, route.exec)
217 rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
218 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
220 rtr.mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
221 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
225 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
226 rtr.mux.HandlerFunc(endpoint.Method, "/"+endpoint.Path, func(w http.ResponseWriter, req *http.Request) {
227 logger := ctxlog.FromContext(req.Context())
228 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
230 logger.WithFields(logrus.Fields{
232 "method": endpoint.Method,
233 "endpoint": endpoint,
234 }).WithError(err).Debug("error loading request params")
235 rtr.sendError(w, err)
238 opts := defaultOpts()
239 err = rtr.transcode(params, opts)
241 logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
242 rtr.sendError(w, err)
245 respOpts, err := rtr.responseOptions(opts)
247 logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
248 rtr.sendError(w, err)
252 creds := auth.CredentialsFromRequest(req)
253 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
254 for _, t := range rt {
255 if t, ok := t.(string); ok {
256 creds.Tokens = append(creds.Tokens, t)
260 ctx := auth.NewContext(req.Context(), creds)
261 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
262 logger.WithFields(logrus.Fields{
263 "apiEndpoint": endpoint,
264 "apiOptsType": fmt.Sprintf("%T", opts),
267 resp, err := exec(ctx, opts)
269 logger.WithError(err).Debugf("returning error type %T", err)
270 rtr.sendError(w, err)
273 rtr.sendResponse(w, req, resp, respOpts)
277 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
278 switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
279 case "login", "logout", "auth":
281 w.Header().Set("Access-Control-Allow-Origin", "*")
282 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE")
283 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
284 w.Header().Set("Access-Control-Max-Age", "86486400")
286 if r.Method == "OPTIONS" {
290 if m := r.FormValue("_method"); m != "" {
295 rtr.mux.ServeHTTP(w, r)