14287: Merge branch 'master'
[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.EndpointCollectionCreate,
45                         func() interface{} { return &arvados.CreateOptions{} },
46                         func(ctx context.Context, opts interface{}) (interface{}, error) {
47                                 return rtr.fed.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
48                         },
49                 },
50                 {
51                         arvados.EndpointCollectionUpdate,
52                         func() interface{} { return &arvados.UpdateOptions{} },
53                         func(ctx context.Context, opts interface{}) (interface{}, error) {
54                                 return rtr.fed.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
55                         },
56                 },
57                 {
58                         arvados.EndpointCollectionGet,
59                         func() interface{} { return &arvados.GetOptions{} },
60                         func(ctx context.Context, opts interface{}) (interface{}, error) {
61                                 return rtr.fed.CollectionGet(ctx, *opts.(*arvados.GetOptions))
62                         },
63                 },
64                 {
65                         arvados.EndpointCollectionList,
66                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
67                         func(ctx context.Context, opts interface{}) (interface{}, error) {
68                                 return rtr.fed.CollectionList(ctx, *opts.(*arvados.ListOptions))
69                         },
70                 },
71                 {
72                         arvados.EndpointCollectionProvenance,
73                         func() interface{} { return &arvados.GetOptions{} },
74                         func(ctx context.Context, opts interface{}) (interface{}, error) {
75                                 return rtr.fed.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
76                         },
77                 },
78                 {
79                         arvados.EndpointCollectionUsedBy,
80                         func() interface{} { return &arvados.GetOptions{} },
81                         func(ctx context.Context, opts interface{}) (interface{}, error) {
82                                 return rtr.fed.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
83                         },
84                 },
85                 {
86                         arvados.EndpointCollectionDelete,
87                         func() interface{} { return &arvados.DeleteOptions{} },
88                         func(ctx context.Context, opts interface{}) (interface{}, error) {
89                                 return rtr.fed.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
90                         },
91                 },
92                 {
93                         arvados.EndpointCollectionTrash,
94                         func() interface{} { return &arvados.DeleteOptions{} },
95                         func(ctx context.Context, opts interface{}) (interface{}, error) {
96                                 return rtr.fed.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
97                         },
98                 },
99                 {
100                         arvados.EndpointCollectionUntrash,
101                         func() interface{} { return &arvados.UntrashOptions{} },
102                         func(ctx context.Context, opts interface{}) (interface{}, error) {
103                                 return rtr.fed.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
104                         },
105                 },
106                 {
107                         arvados.EndpointContainerCreate,
108                         func() interface{} { return &arvados.CreateOptions{} },
109                         func(ctx context.Context, opts interface{}) (interface{}, error) {
110                                 return rtr.fed.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
111                         },
112                 },
113                 {
114                         arvados.EndpointContainerUpdate,
115                         func() interface{} { return &arvados.UpdateOptions{} },
116                         func(ctx context.Context, opts interface{}) (interface{}, error) {
117                                 return rtr.fed.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
118                         },
119                 },
120                 {
121                         arvados.EndpointContainerGet,
122                         func() interface{} { return &arvados.GetOptions{} },
123                         func(ctx context.Context, opts interface{}) (interface{}, error) {
124                                 return rtr.fed.ContainerGet(ctx, *opts.(*arvados.GetOptions))
125                         },
126                 },
127                 {
128                         arvados.EndpointContainerList,
129                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
130                         func(ctx context.Context, opts interface{}) (interface{}, error) {
131                                 return rtr.fed.ContainerList(ctx, *opts.(*arvados.ListOptions))
132                         },
133                 },
134                 {
135                         arvados.EndpointContainerDelete,
136                         func() interface{} { return &arvados.DeleteOptions{} },
137                         func(ctx context.Context, opts interface{}) (interface{}, error) {
138                                 return rtr.fed.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
139                         },
140                 },
141                 {
142                         arvados.EndpointContainerLock,
143                         func() interface{} {
144                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
145                         },
146                         func(ctx context.Context, opts interface{}) (interface{}, error) {
147                                 return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
148                         },
149                 },
150                 {
151                         arvados.EndpointContainerUnlock,
152                         func() interface{} {
153                                 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
154                         },
155                         func(ctx context.Context, opts interface{}) (interface{}, error) {
156                                 return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
157                         },
158                 },
159                 {
160                         arvados.EndpointSpecimenCreate,
161                         func() interface{} { return &arvados.CreateOptions{} },
162                         func(ctx context.Context, opts interface{}) (interface{}, error) {
163                                 return rtr.fed.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
164                         },
165                 },
166                 {
167                         arvados.EndpointSpecimenUpdate,
168                         func() interface{} { return &arvados.UpdateOptions{} },
169                         func(ctx context.Context, opts interface{}) (interface{}, error) {
170                                 return rtr.fed.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
171                         },
172                 },
173                 {
174                         arvados.EndpointSpecimenGet,
175                         func() interface{} { return &arvados.GetOptions{} },
176                         func(ctx context.Context, opts interface{}) (interface{}, error) {
177                                 return rtr.fed.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
178                         },
179                 },
180                 {
181                         arvados.EndpointSpecimenList,
182                         func() interface{} { return &arvados.ListOptions{Limit: -1} },
183                         func(ctx context.Context, opts interface{}) (interface{}, error) {
184                                 return rtr.fed.SpecimenList(ctx, *opts.(*arvados.ListOptions))
185                         },
186                 },
187                 {
188                         arvados.EndpointSpecimenDelete,
189                         func() interface{} { return &arvados.DeleteOptions{} },
190                         func(ctx context.Context, opts interface{}) (interface{}, error) {
191                                 return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
192                         },
193                 },
194         } {
195                 rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
196                 if route.endpoint.Method == "PATCH" {
197                         // Accept PUT as a synonym for PATCH.
198                         endpointPUT := route.endpoint
199                         endpointPUT.Method = "PUT"
200                         rtr.addRoute(endpointPUT, route.defaultOpts, route.exec)
201                 }
202         }
203         rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
204                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
205         })
206         rtr.mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
207                 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
208         })
209 }
210
211 func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
212         rtr.mux.HandlerFunc(endpoint.Method, "/"+endpoint.Path, func(w http.ResponseWriter, req *http.Request) {
213                 logger := ctxlog.FromContext(req.Context())
214                 params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
215                 if err != nil {
216                         logger.WithFields(logrus.Fields{
217                                 "req":      req,
218                                 "method":   endpoint.Method,
219                                 "endpoint": endpoint,
220                         }).WithError(err).Debug("error loading request params")
221                         rtr.sendError(w, err)
222                         return
223                 }
224                 opts := defaultOpts()
225                 err = rtr.transcode(params, opts)
226                 if err != nil {
227                         logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
228                         rtr.sendError(w, err)
229                         return
230                 }
231                 respOpts, err := rtr.responseOptions(opts)
232                 if err != nil {
233                         logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
234                         rtr.sendError(w, err)
235                         return
236                 }
237
238                 creds := auth.CredentialsFromRequest(req)
239                 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
240                         for _, t := range rt {
241                                 if t, ok := t.(string); ok {
242                                         creds.Tokens = append(creds.Tokens, t)
243                                 }
244                         }
245                 }
246                 ctx := auth.NewContext(req.Context(), creds)
247                 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
248                 logger.WithFields(logrus.Fields{
249                         "apiEndpoint": endpoint,
250                         "apiOptsType": fmt.Sprintf("%T", opts),
251                         "apiOpts":     opts,
252                 }).Debug("exec")
253                 resp, err := exec(ctx, opts)
254                 if err != nil {
255                         logger.WithError(err).Debugf("returning error type %T", err)
256                         rtr.sendError(w, err)
257                         return
258                 }
259                 rtr.sendResponse(w, resp, respOpts)
260         })
261 }
262
263 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
264         switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
265         case "login", "logout", "auth":
266         default:
267                 w.Header().Set("Access-Control-Allow-Origin", "*")
268                 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE")
269                 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
270                 w.Header().Set("Access-Control-Max-Age", "86486400")
271         }
272         if r.Method == "OPTIONS" {
273                 return
274         }
275         r.ParseForm()
276         if m := r.FormValue("_method"); m != "" {
277                 r2 := *r
278                 r = &r2
279                 r.Method = m
280         }
281         rtr.mux.ServeHTTP(w, r)
282 }