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