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
24 fed federation.Interface
27 func New(cluster *arvados.Cluster) *router {
29 mux: httprouter.New(),
30 fed: federation.New(cluster),
32 rtr.addRoutes(cluster)
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)
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))
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))
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))
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))
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))
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))
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))
92 arvados.EndpointContainerCreate,
93 func() interface{} { return &arvados.CreateOptions{} },
94 func(ctx context.Context, opts interface{}) (interface{}, error) {
95 return rtr.fed.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
99 arvados.EndpointContainerUpdate,
100 func() interface{} { return &arvados.UpdateOptions{} },
101 func(ctx context.Context, opts interface{}) (interface{}, error) {
102 return rtr.fed.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
106 arvados.EndpointContainerGet,
107 func() interface{} { return &arvados.GetOptions{} },
108 func(ctx context.Context, opts interface{}) (interface{}, error) {
109 return rtr.fed.ContainerGet(ctx, *opts.(*arvados.GetOptions))
113 arvados.EndpointContainerList,
114 func() interface{} { return &arvados.ListOptions{Limit: -1} },
115 func(ctx context.Context, opts interface{}) (interface{}, error) {
116 return rtr.fed.ContainerList(ctx, *opts.(*arvados.ListOptions))
120 arvados.EndpointContainerDelete,
121 func() interface{} { return &arvados.DeleteOptions{} },
122 func(ctx context.Context, opts interface{}) (interface{}, error) {
123 return rtr.fed.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
127 arvados.EndpointContainerLock,
129 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
131 func(ctx context.Context, opts interface{}) (interface{}, error) {
132 return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
136 arvados.EndpointContainerUnlock,
138 return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
140 func(ctx context.Context, opts interface{}) (interface{}, error) {
141 return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
145 arvados.EndpointSpecimenCreate,
146 func() interface{} { return &arvados.CreateOptions{} },
147 func(ctx context.Context, opts interface{}) (interface{}, error) {
148 return rtr.fed.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
152 arvados.EndpointSpecimenUpdate,
153 func() interface{} { return &arvados.UpdateOptions{} },
154 func(ctx context.Context, opts interface{}) (interface{}, error) {
155 return rtr.fed.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
159 arvados.EndpointSpecimenGet,
160 func() interface{} { return &arvados.GetOptions{} },
161 func(ctx context.Context, opts interface{}) (interface{}, error) {
162 return rtr.fed.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
166 arvados.EndpointSpecimenList,
167 func() interface{} { return &arvados.ListOptions{Limit: -1} },
168 func(ctx context.Context, opts interface{}) (interface{}, error) {
169 return rtr.fed.SpecimenList(ctx, *opts.(*arvados.ListOptions))
173 arvados.EndpointSpecimenDelete,
174 func() interface{} { return &arvados.DeleteOptions{} },
175 func(ctx context.Context, opts interface{}) (interface{}, error) {
176 return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
181 methods := []string{route.endpoint.Method}
182 if route.endpoint.Method == "PATCH" {
183 methods = append(methods, "PUT")
185 for _, method := range methods {
186 rtr.mux.HandlerFunc(method, "/"+route.endpoint.Path, func(w http.ResponseWriter, req *http.Request) {
187 logger := ctxlog.FromContext(req.Context())
188 params, err := rtr.loadRequestParams(req, route.endpoint.AttrsKey)
190 logger.WithField("req", req).WithField("route", route).WithError(err).Debug("error loading request params")
191 rtr.sendError(w, err)
194 opts := route.defaultOpts()
195 err = rtr.transcode(params, opts)
197 logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
198 rtr.sendError(w, err)
201 respOpts, err := rtr.responseOptions(opts)
203 logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
204 rtr.sendError(w, err)
208 creds := auth.CredentialsFromRequest(req)
209 if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
210 for _, t := range rt {
211 if t, ok := t.(string); ok {
212 creds.Tokens = append(creds.Tokens, t)
217 ctx = context.WithValue(ctx, auth.ContextKeyCredentials, creds)
218 ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
219 logger.WithFields(logrus.Fields{
220 "apiEndpoint": route.endpoint,
221 "apiOptsType": fmt.Sprintf("%T", opts),
224 resp, err := route.exec(ctx, opts)
226 logger.WithError(err).Debugf("returning error type %T", err)
227 rtr.sendError(w, err)
230 rtr.sendResponse(w, resp, respOpts)
234 rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
235 httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
239 func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
240 switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
241 case "login", "logout", "auth":
243 w.Header().Set("Access-Control-Allow-Origin", "*")
244 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE")
245 w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
246 w.Header().Set("Access-Control-Max-Age", "86486400")
248 if r.Method == "OPTIONS" {
252 if m := r.FormValue("_method"); m != "" {
257 rtr.mux.ServeHTTP(w, r)