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