15954: Merge branch 'master'
[arvados.git] / lib / controller / router / router.go
index 26de3d2bcbb932a3731487516351eae3bbafb7fd..69d707703852b7fc60acfcb6e86d8fa960e7a5c9 100644 (file)
@@ -10,35 +10,57 @@ import (
        "net/http"
        "strings"
 
-       "git.curoverse.com/arvados.git/lib/controller/federation"
-       "git.curoverse.com/arvados.git/sdk/go/arvados"
-       "git.curoverse.com/arvados.git/sdk/go/auth"
-       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
-       "git.curoverse.com/arvados.git/sdk/go/httpserver"
-       "github.com/julienschmidt/httprouter"
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/auth"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "git.arvados.org/arvados.git/sdk/go/httpserver"
+       "github.com/gorilla/mux"
        "github.com/sirupsen/logrus"
 )
 
 type router struct {
-       mux *httprouter.Router
-       fed federation.Interface
+       mux *mux.Router
+       fed arvados.API
 }
 
-func New(cluster *arvados.Cluster) *router {
+func New(fed arvados.API) *router {
        rtr := &router{
-               mux: httprouter.New(),
-               fed: federation.New(cluster),
+               mux: mux.NewRouter(),
+               fed: fed,
        }
-       rtr.addRoutes(cluster)
+       rtr.addRoutes()
        return rtr
 }
 
-func (rtr *router) addRoutes(cluster *arvados.Cluster) {
+type routableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
+
+func (rtr *router) addRoutes() {
        for _, route := range []struct {
                endpoint    arvados.APIEndpoint
                defaultOpts func() interface{}
-               exec        func(ctx context.Context, opts interface{}) (interface{}, error)
+               exec        routableFunc
        }{
+               {
+                       arvados.EndpointConfigGet,
+                       func() interface{} { return &struct{}{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.ConfigGet(ctx)
+                       },
+               },
+               {
+                       arvados.EndpointLogin,
+                       func() interface{} { return &arvados.LoginOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.Login(ctx, *opts.(*arvados.LoginOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLogout,
+                       func() interface{} { return &arvados.LogoutOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.Logout(ctx, *opts.(*arvados.LogoutOptions))
+                       },
+               },
                {
                        arvados.EndpointCollectionCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
@@ -88,6 +110,20 @@ func (rtr *router) addRoutes(cluster *arvados.Cluster) {
                                return rtr.fed.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
                        },
                },
+               {
+                       arvados.EndpointCollectionTrash,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+               {
+                       arvados.EndpointCollectionUntrash,
+                       func() interface{} { return &arvados.UntrashOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
+                       },
+               },
                {
                        arvados.EndpointContainerCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
@@ -176,63 +212,171 @@ func (rtr *router) addRoutes(cluster *arvados.Cluster) {
                                return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
                        },
                },
+               {
+                       arvados.EndpointUserCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserMerge,
+                       func() interface{} { return &arvados.UserMergeOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserActivate,
+                       func() interface{} { return &arvados.UserActivateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserSetup,
+                       func() interface{} { return &arvados.UserSetupOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserUnsetup,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserGetCurrent,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserGetSystem,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserUpdateUUID,
+                       func() interface{} { return &arvados.UpdateUUIDOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserBatchUpdate,
+                       func() interface{} { return &arvados.UserBatchUpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointUserDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
        } {
-               route := route
-               methods := []string{route.endpoint.Method}
-               if route.endpoint.Method == "PATCH" {
-                       methods = append(methods, "PUT")
+               rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
+       }
+       rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
+       })
+       rtr.mux.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
+       })
+}
+
+var altMethod = map[string]string{
+       "PATCH": "PUT",  // Accept PUT as a synonym for PATCH
+       "GET":   "HEAD", // Accept HEAD at any GET route
+}
+
+func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
+       methods := []string{endpoint.Method}
+       if alt, ok := altMethod[endpoint.Method]; ok {
+               methods = append(methods, alt)
+       }
+       rtr.mux.Methods(methods...).Path("/" + endpoint.Path).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               logger := ctxlog.FromContext(req.Context())
+               params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
+               if err != nil {
+                       logger.WithFields(logrus.Fields{
+                               "req":      req,
+                               "method":   endpoint.Method,
+                               "endpoint": endpoint,
+                       }).WithError(err).Debug("error loading request params")
+                       rtr.sendError(w, err)
+                       return
+               }
+               opts := defaultOpts()
+               err = rtr.transcode(params, opts)
+               if err != nil {
+                       logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
+                       rtr.sendError(w, err)
+                       return
+               }
+               respOpts, err := rtr.responseOptions(opts)
+               if err != nil {
+                       logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
+                       rtr.sendError(w, err)
+                       return
                }
-               for _, method := range methods {
-                       rtr.mux.HandlerFunc(method, "/"+route.endpoint.Path, func(w http.ResponseWriter, req *http.Request) {
-                               logger := ctxlog.FromContext(req.Context())
-                               params, err := rtr.loadRequestParams(req, route.endpoint.AttrsKey)
-                               if err != nil {
-                                       logger.WithField("req", req).WithField("route", route).WithError(err).Debug("error loading request params")
-                                       rtr.sendError(w, err)
-                                       return
-                               }
-                               opts := route.defaultOpts()
-                               err = rtr.transcode(params, opts)
-                               if err != nil {
-                                       logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
-                                       rtr.sendError(w, err)
-                                       return
-                               }
-                               respOpts, err := rtr.responseOptions(opts)
-                               if err != nil {
-                                       logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
-                                       rtr.sendError(w, err)
-                                       return
-                               }
 
-                               creds := auth.CredentialsFromRequest(req)
-                               if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
-                                       for _, t := range rt {
-                                               if t, ok := t.(string); ok {
-                                                       creds.Tokens = append(creds.Tokens, t)
-                                               }
-                                       }
-                               }
-                               ctx := req.Context()
-                               ctx = context.WithValue(ctx, auth.ContextKeyCredentials, creds)
-                               ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
-                               logger.WithFields(logrus.Fields{
-                                       "apiEndpoint": route.endpoint,
-                                       "apiOptsType": fmt.Sprintf("%T", opts),
-                                       "apiOpts":     opts,
-                               }).Debug("exec")
-                               resp, err := route.exec(ctx, opts)
-                               if err != nil {
-                                       logger.WithError(err).Debugf("returning error type %T", err)
-                                       rtr.sendError(w, err)
-                                       return
+               creds := auth.CredentialsFromRequest(req)
+               err = creds.LoadTokensFromHTTPRequestBody(req)
+               if err != nil {
+                       rtr.sendError(w, fmt.Errorf("error loading tokens from request body: %s", err))
+                       return
+               }
+               if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
+                       for _, t := range rt {
+                               if t, ok := t.(string); ok {
+                                       creds.Tokens = append(creds.Tokens, t)
                                }
-                               rtr.sendResponse(w, resp, respOpts)
-                       })
+                       }
                }
-       }
-       rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-               httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
+               ctx := auth.NewContext(req.Context(), creds)
+               ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
+               logger.WithFields(logrus.Fields{
+                       "apiEndpoint": endpoint,
+                       "apiOptsType": fmt.Sprintf("%T", opts),
+                       "apiOpts":     opts,
+               }).Debug("exec")
+               resp, err := exec(ctx, opts)
+               if err != nil {
+                       logger.WithError(err).Debugf("returning error type %T", err)
+                       rtr.sendError(w, err)
+                       return
+               }
+               rtr.sendResponse(w, req, resp, respOpts)
        })
 }
 
@@ -241,7 +385,7 @@ func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        case "login", "logout", "auth":
        default:
                w.Header().Set("Access-Control-Allow-Origin", "*")
-               w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE")
+               w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, PATCH, DELETE")
                w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
                w.Header().Set("Access-Control-Max-Age", "86486400")
        }
@@ -253,6 +397,10 @@ func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
                r2 := *r
                r = &r2
                r.Method = m
+       } else if m = r.Header.Get("X-Http-Method-Override"); m != "" {
+               r2 := *r
+               r = &r2
+               r.Method = m
        }
        rtr.mux.ServeHTTP(w, r)
 }