Merge branch 'master' into 15577-ownership-transfer
[arvados.git] / lib / controller / router / router.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package router
6
7 import (
8         "context"
9         "fmt"
10         "net/http"
11         "strings"
12
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"
19 )
20
21 type router struct {
22         mux *httprouter.Router
23         fed arvados.API
24 }
25
26 func New(fed arvados.API) *router {
27         rtr := &router{
28                 mux: httprouter.New(),
29                 fed: fed,
30         }
31         rtr.addRoutes()
32         return rtr
33 }
34
35 type routableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
36
37 func (rtr *router) addRoutes() {
38         for _, route := range []struct {
39                 endpoint    arvados.APIEndpoint
40                 defaultOpts func() interface{}
41                 exec        routableFunc
42         }{
43                 {
44                         arvados.EndpointConfigGet,
45                         func() interface{} { return &struct{}{} },
46                         func(ctx context.Context, opts interface{}) (interface{}, error) {
47                                 return rtr.fed.ConfigGet(ctx)
48                         },
49                 },
50                 {
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))
55                         },
56                 },
57                 {
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))
62                         },
63                 },
64                 {
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))
69                         },
70                 },
71                 {
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))
76                         },
77                 },
78                 {
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))
83                         },
84                 },
85                 {
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))
90                         },
91                 },
92                 {
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))
97                         },
98                 },
99                 {
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))
104                         },
105                 },
106                 {
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))
111                         },
112                 },
113                 {
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))
118                         },
119                 },
120                 {
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))
125                         },
126                 },
127                 {
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))
132                         },
133                 },
134                 {
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))
139                         },
140                 },
141                 {
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))
146                         },
147                 },
148                 {
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))
153                         },
154                 },
155                 {
156                         arvados.EndpointContainerLock,
157                         func() interface{} {
158                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
159                         },
160                         func(ctx context.Context, opts interface{}) (interface{}, error) {
161                                 return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
162                         },
163                 },
164                 {
165                         arvados.EndpointContainerUnlock,
166                         func() interface{} {
167                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
168                         },
169                         func(ctx context.Context, opts interface{}) (interface{}, error) {
170                                 return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
171                         },
172                 },
173                 {
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))
178                         },
179                 },
180                 {
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))
185                         },
186                 },
187                 {
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))
192                         },
193                 },
194                 {
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))
199                         },
200                 },
201                 {
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))
206                         },
207                 },
208         } {
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)
215                 }
216         }
217         rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
218                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
219         })
220         rtr.mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
221                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
222         })
223 }
224
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)
229                 if err != nil {
230                         logger.WithFields(logrus.Fields{
231                                 "req":      req,
232                                 "method":   endpoint.Method,
233                                 "endpoint": endpoint,
234                         }).WithError(err).Debug("error loading request params")
235                         rtr.sendError(w, err)
236                         return
237                 }
238                 opts := defaultOpts()
239                 err = rtr.transcode(params, opts)
240                 if err != nil {
241                         logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
242                         rtr.sendError(w, err)
243                         return
244                 }
245                 respOpts, err := rtr.responseOptions(opts)
246                 if err != nil {
247                         logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
248                         rtr.sendError(w, err)
249                         return
250                 }
251
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)
257                                 }
258                         }
259                 }
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),
265                         "apiOpts":     opts,
266                 }).Debug("exec")
267                 resp, err := exec(ctx, opts)
268                 if err != nil {
269                         logger.WithError(err).Debugf("returning error type %T", err)
270                         rtr.sendError(w, err)
271                         return
272                 }
273                 rtr.sendResponse(w, req, resp, respOpts)
274         })
275 }
276
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":
280         default:
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")
285         }
286         if r.Method == "OPTIONS" {
287                 return
288         }
289         r.ParseForm()
290         if m := r.FormValue("_method"); m != "" {
291                 r2 := *r
292                 r = &r2
293                 r.Method = m
294         }
295         rtr.mux.ServeHTTP(w, r)
296 }