1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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"
23 mux *httprouter.Router
27 func New(cluster *arvados.Cluster) *router {
29 mux: httprouter.New(),
30 fed: federation.New(cluster),
36 type routableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
38 func (rtr *router) addRoutes() {
39 for _, route := range []struct {
40 endpoint arvados.APIEndpoint
41 defaultOpts func() interface{}
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))
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))
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))
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))
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))
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))
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))
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))
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))
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))
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))
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))
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))
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))
143 arvados.EndpointContainerLock,
145 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
147 func(ctx context.Context, opts interface{}) (interface{}, error) {
148 return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
152 arvados.EndpointContainerUnlock,
154 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
156 func(ctx context.Context, opts interface{}) (interface{}, error) {
157 return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
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))
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))
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))
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))
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))
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)
204 rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
205 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
207 rtr.mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
208 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
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)
217 logger.WithFields(logrus.Fields{
219 "method": endpoint.Method,
220 "endpoint": endpoint,
221 }).WithError(err).Debug("error loading request params")
222 rtr.sendError(w, err)
225 opts := defaultOpts()
226 err = rtr.transcode(params, opts)
228 logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
229 rtr.sendError(w, err)
232 respOpts, err := rtr.responseOptions(opts)
234 logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
235 rtr.sendError(w, err)
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)
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),
254 resp, err := exec(ctx, opts)
256 logger.WithError(err).Debugf("returning error type %T", err)
257 rtr.sendError(w, err)
260 rtr.sendResponse(w, resp, respOpts)
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":
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")
273 if r.Method == "OPTIONS" {
277 if m := r.FormValue("_method"); m != "" {
282 rtr.mux.ServeHTTP(w, r)