Merge branch 'main' into 18842-arv-mount-disk-config
[arvados.git] / lib / controller / router / router.go
index 29c81ac5cae9ac63431e691852230a00c2335afe..d4712558eae07c8ddd2a369ae40e8ce4ba55da0c 100644 (file)
@@ -7,9 +7,11 @@ package router
 import (
        "context"
        "fmt"
+       "math"
        "net/http"
        "strings"
 
+       "git.arvados.org/arvados.git/lib/controller/api"
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
@@ -19,36 +21,42 @@ import (
 )
 
 type router struct {
-       mux       *mux.Router
-       backend   arvados.API
-       wrapCalls func(RoutableFunc) RoutableFunc
+       mux     *mux.Router
+       backend arvados.API
+       config  Config
+}
+
+type Config struct {
+       // Return an error if request body exceeds this size. 0 means
+       // unlimited.
+       MaxRequestSize int
+
+       // If wrapCalls is not nil, it is called once for each API
+       // method, and the returned method is used in its place. This
+       // can be used to install hooks before and after each API call
+       // and alter responses; see localdb.WrapCallsInTransaction for
+       // an example.
+       WrapCalls func(api.RoutableFunc) api.RoutableFunc
 }
 
 // New returns a new router (which implements the http.Handler
 // interface) that serves requests by calling Arvados API methods on
 // the given backend.
-//
-// If wrapCalls is not nil, it is called once for each API method, and
-// the returned method is used in its place. This can be used to
-// install hooks before and after each API call and alter responses;
-// see localdb.WrapCallsInTransaction for an example.
-func New(backend arvados.API, wrapCalls func(RoutableFunc) RoutableFunc) *router {
+func New(backend arvados.API, config Config) *router {
        rtr := &router{
-               mux:       mux.NewRouter(),
-               backend:   backend,
-               wrapCalls: wrapCalls,
+               mux:     mux.NewRouter(),
+               backend: backend,
+               config:  config,
        }
        rtr.addRoutes()
        return rtr
 }
 
-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        RoutableFunc
+               exec        api.RoutableFunc
        }{
                {
                        arvados.EndpointConfigGet,
@@ -57,6 +65,13 @@ func (rtr *router) addRoutes() {
                                return rtr.backend.ConfigGet(ctx)
                        },
                },
+               {
+                       arvados.EndpointVocabularyGet,
+                       func() interface{} { return &struct{}{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.VocabularyGet(ctx)
+                       },
+               },
                {
                        arvados.EndpointLogin,
                        func() interface{} { return &arvados.LoginOptions{} },
@@ -169,6 +184,41 @@ func (rtr *router) addRoutes() {
                                return rtr.backend.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
                        },
                },
+               {
+                       arvados.EndpointContainerRequestCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.ContainerRequestCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerRequestUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.ContainerRequestUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerRequestGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.ContainerRequestGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerRequestList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.ContainerRequestList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerRequestDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.ContainerRequestDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
                {
                        arvados.EndpointContainerLock,
                        func() interface{} {
@@ -187,6 +237,171 @@ func (rtr *router) addRoutes() {
                                return rtr.backend.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
                        },
                },
+               {
+                       arvados.EndpointContainerSSH,
+                       func() interface{} { return &arvados.ContainerSSHOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.ContainerSSH(ctx, *opts.(*arvados.ContainerSSHOptions))
+                       },
+               },
+               {
+                       // arvados-client built before commit
+                       // bdc29d3129f6d75aa9ce0a24ffb849a272b06f08
+                       // used GET with params in headers instead of
+                       // POST form
+                       arvados.APIEndpoint{"GET", "arvados/v1/connect/{uuid}/ssh", ""},
+                       func() interface{} { return &arvados.ContainerSSHOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return nil, httpError(http.StatusGone, fmt.Errorf("API endpoint is obsolete -- please upgrade your arvados-client program"))
+                       },
+               },
+               {
+                       arvados.EndpointContainerGatewayTunnel,
+                       func() interface{} { return &arvados.ContainerGatewayTunnelOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.ContainerGatewayTunnel(ctx, *opts.(*arvados.ContainerGatewayTunnelOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupContents,
+                       func() interface{} { return &arvados.GroupContentsOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupContents(ctx, *opts.(*arvados.GroupContentsOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupContentsUUIDInPath,
+                       func() interface{} { return &arvados.GroupContentsOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupContents(ctx, *opts.(*arvados.GroupContentsOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupShared,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupShared(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupTrash,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupTrash(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+               {
+                       arvados.EndpointGroupUntrash,
+                       func() interface{} { return &arvados.UntrashOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.GroupUntrash(ctx, *opts.(*arvados.UntrashOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLinkCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLinkUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLinkList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLinkGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLinkDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LinkDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLogCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LogCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLogUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LogUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLogList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LogList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLogGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LogGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointLogDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.LogDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
                {
                        arvados.EndpointSpecimenCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
@@ -222,6 +437,48 @@ func (rtr *router) addRoutes() {
                                return rtr.backend.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
                        },
                },
+               {
+                       arvados.EndpointAPIClientAuthorizationCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationCurrent,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationCurrent(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
                {
                        arvados.EndpointUserCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
@@ -278,13 +535,6 @@ func (rtr *router) addRoutes() {
                                return rtr.backend.UserGet(ctx, *opts.(*arvados.GetOptions))
                        },
                },
-               {
-                       arvados.EndpointUserUpdateUUID,
-                       func() interface{} { return &arvados.UpdateUUIDOptions{} },
-                       func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.backend.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
-                       },
-               },
                {
                        arvados.EndpointUserUpdate,
                        func() interface{} { return &arvados.UpdateOptions{} },
@@ -322,8 +572,8 @@ func (rtr *router) addRoutes() {
                },
        } {
                exec := route.exec
-               if rtr.wrapCalls != nil {
-                       exec = rtr.wrapCalls(exec)
+               if rtr.config.WrapCalls != nil {
+                       exec = rtr.config.WrapCalls(exec)
                }
                rtr.addRoute(route.endpoint, route.defaultOpts, exec)
        }
@@ -340,7 +590,7 @@ var altMethod = map[string]string{
        "GET":   "HEAD", // Accept HEAD at any GET route
 }
 
-func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec RoutableFunc) {
+func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec api.RoutableFunc) {
        methods := []string{endpoint.Method}
        if alt, ok := altMethod[endpoint.Method]; ok {
                methods = append(methods, alt)
@@ -391,6 +641,23 @@ func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() int
                        "apiOptsType": fmt.Sprintf("%T", opts),
                        "apiOpts":     opts,
                }).Debug("exec")
+               // Extract the token UUIDs (or a placeholder for v1 tokens)
+               var tokenUUIDs []string
+               for _, t := range creds.Tokens {
+                       if strings.HasPrefix(t, "v2/") {
+                               tokenParts := strings.Split(t, "/")
+                               if len(tokenParts) >= 3 {
+                                       tokenUUIDs = append(tokenUUIDs, tokenParts[1])
+                               }
+                       } else {
+                               end := t
+                               if len(t) > 5 {
+                                       end = t[len(t)-5:]
+                               }
+                               tokenUUIDs = append(tokenUUIDs, "v1 token ending in "+end)
+                       }
+               }
+               httpserver.SetResponseLogFields(req.Context(), logrus.Fields{"tokenUUIDs": tokenUUIDs})
                resp, err := exec(ctx, opts)
                if err != nil {
                        logger.WithError(err).Debugf("returning error type %T", err)
@@ -413,8 +680,26 @@ func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        if r.Method == "OPTIONS" {
                return
        }
+       if r.Body != nil {
+               // Wrap r.Body in a http.MaxBytesReader(), otherwise
+               // r.ParseForm() uses a default max request body size
+               // of 10 megabytes. Note we rely on the Nginx
+               // configuration to enforce the real max body size.
+               max := int64(rtr.config.MaxRequestSize)
+               if max < 1 {
+                       max = math.MaxInt64 - 1
+               }
+               r.Body = http.MaxBytesReader(w, r.Body, max)
+       }
        if r.Method == "POST" {
-               r.ParseForm()
+               err := r.ParseForm()
+               if err != nil {
+                       if err.Error() == "http: request body too large" {
+                               err = httpError(http.StatusRequestEntityTooLarge, err)
+                       }
+                       rtr.sendError(w, err)
+                       return
+               }
                if m := r.FormValue("_method"); m != "" {
                        r2 := *r
                        r = &r2