16534: Add database access in lib/controller/localdb.
authorTom Clegg <tom@tomclegg.ca>
Fri, 26 Jun 2020 21:15:30 +0000 (17:15 -0400)
committerTom Clegg <tom@tomclegg.ca>
Fri, 26 Jun 2020 21:15:30 +0000 (17:15 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@tomclegg.ca>

13 files changed:
lib/controller/federation.go
lib/controller/federation/federation_test.go
lib/controller/handler.go
lib/controller/localdb/db.go [new file with mode: 0644]
lib/controller/localdb/db_test.go [new file with mode: 0644]
lib/controller/localdb/docker_test.go [new file with mode: 0644]
lib/controller/localdb/login.go
lib/controller/localdb/login_ldap_docker_test.go
lib/controller/localdb/login_ldap_docker_test.sh
lib/controller/localdb/login_ldap_test.go
lib/controller/localdb/login_oidc_test.go
lib/controller/router/router.go
lib/controller/router/router_test.go

index ac239fb9b23f5c4c106034e2a910fb441b9c2218..aceaba8087ad2031413516c2671f75174c457fae 100644 (file)
@@ -152,7 +152,7 @@ type CurrentUser struct {
 // non-nil, true, nil -- if the token is valid
 func (h *Handler) validateAPItoken(req *http.Request, token string) (*CurrentUser, bool, error) {
        user := CurrentUser{Authorization: arvados.APIClientAuthorization{APIToken: token}}
-       db, err := h.db(req)
+       db, err := h.db(req.Context())
        if err != nil {
                ctxlog.FromContext(req.Context()).WithError(err).Debugf("validateAPItoken(%s): database error", token)
                return nil, false, err
@@ -189,7 +189,7 @@ func (h *Handler) validateAPItoken(req *http.Request, token string) (*CurrentUse
 }
 
 func (h *Handler) createAPItoken(req *http.Request, userUUID string, scopes []string) (*arvados.APIClientAuthorization, error) {
-       db, err := h.db(req)
+       db, err := h.db(req.Context())
        if err != nil {
                return nil, err
        }
index f57d827848cbf772e4e7b9c3c2b8ac1e860f18e2..256afc8e6b9482d53eaa520927f62761a1f71b03 100644 (file)
@@ -64,7 +64,7 @@ func (s *FederationSuite) addDirectRemote(c *check.C, id string, backend backend
 
 func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend backend) {
        srv := httpserver.Server{Addr: ":"}
-       srv.Handler = router.New(backend)
+       srv.Handler = router.New(backend, nil)
        c.Check(srv.Start(), check.IsNil)
        s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
                Scheme: "http",
index 01f2161632bf8e6562f51b4266e43602b90218c6..cc06246420559479203e24843164cee281e07633 100644 (file)
@@ -16,9 +16,11 @@ import (
        "time"
 
        "git.arvados.org/arvados.git/lib/controller/federation"
+       "git.arvados.org/arvados.git/lib/controller/localdb"
        "git.arvados.org/arvados.git/lib/controller/railsproxy"
        "git.arvados.org/arvados.git/lib/controller/router"
        "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "git.arvados.org/arvados.git/sdk/go/health"
        "git.arvados.org/arvados.git/sdk/go/httpserver"
        _ "github.com/lib/pq"
@@ -63,7 +65,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 
 func (h *Handler) CheckHealth() error {
        h.setupOnce.Do(h.setup)
-       _, _, err := railsproxy.FindRailsAPI(h.Cluster)
+       _, err := h.db(context.TODO())
+       if err != nil {
+               return err
+       }
+       _, _, err = railsproxy.FindRailsAPI(h.Cluster)
        return err
 }
 
@@ -78,10 +84,10 @@ func (h *Handler) setup() {
        mux.Handle("/_health/", &health.Handler{
                Token:  h.Cluster.ManagementToken,
                Prefix: "/_health/",
-               Routes: health.Routes{"ping": func() error { _, err := h.db(&http.Request{}); return err }},
+               Routes: health.Routes{"ping": func() error { _, err := h.db(context.TODO()); return err }},
        })
 
-       rtr := router.New(federation.New(h.Cluster))
+       rtr := router.New(federation.New(h.Cluster), localdb.WrapCallsInTransactions(h.db))
        mux.Handle("/arvados/v1/config", rtr)
        mux.Handle("/"+arvados.EndpointUserAuthenticate.Path, rtr)
 
@@ -115,7 +121,7 @@ func (h *Handler) setup() {
 
 var errDBConnection = errors.New("database connection error")
 
-func (h *Handler) db(req *http.Request) (*sql.DB, error) {
+func (h *Handler) db(ctx context.Context) (*sql.DB, error) {
        h.pgdbMtx.Lock()
        defer h.pgdbMtx.Unlock()
        if h.pgdb != nil {
@@ -124,14 +130,14 @@ func (h *Handler) db(req *http.Request) (*sql.DB, error) {
 
        db, err := sql.Open("postgres", h.Cluster.PostgreSQL.Connection.String())
        if err != nil {
-               httpserver.Logger(req).WithError(err).Error("postgresql connect failed")
+               ctxlog.FromContext(ctx).WithError(err).Error("postgresql connect failed")
                return nil, errDBConnection
        }
        if p := h.Cluster.PostgreSQL.ConnectionPool; p > 0 {
                db.SetMaxOpenConns(p)
        }
        if err := db.Ping(); err != nil {
-               httpserver.Logger(req).WithError(err).Error("postgresql connect succeeded but ping failed")
+               ctxlog.FromContext(ctx).WithError(err).Error("postgresql connect scuceeded but ping failed")
                return nil, errDBConnection
        }
        h.pgdb = db
diff --git a/lib/controller/localdb/db.go b/lib/controller/localdb/db.go
new file mode 100644 (file)
index 0000000..f9c5d19
--- /dev/null
@@ -0,0 +1,110 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+       "database/sql"
+       "errors"
+       "sync"
+
+       "git.arvados.org/arvados.git/lib/controller/router"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+)
+
+// WrapCallsInTransactions returns a call wrapper (suitable for
+// assigning to router.router.WrapCalls) that starts a new transaction
+// for each API call, and commits only if the call succeeds.
+//
+// The wrapper calls getdb() to get a database handle before each API
+// call.
+func WrapCallsInTransactions(getdb func(context.Context) (*sql.DB, error)) func(router.RoutableFunc) router.RoutableFunc {
+       return func(origFunc router.RoutableFunc) router.RoutableFunc {
+               return func(ctx context.Context, opts interface{}) (_ interface{}, err error) {
+                       ctx, finishtx := starttx(ctx, getdb)
+                       defer finishtx(&err)
+                       return origFunc(ctx, opts)
+               }
+       }
+}
+
+// WithTransaction returns a child context in which the given
+// transaction will be used by any localdb API call that needs one.
+// The caller is responsible for calling Commit or Rollback on tx.
+func Transaction(ctx context.Context, tx *sql.Tx) context.Context {
+       txn := &transaction{tx: tx}
+       txn.setup.Do(func() {})
+       return context.WithValue(ctx, contextKeyTransaction, txn)
+}
+
+type contextKeyT string
+
+var contextKeyTransaction = contextKeyT("transaction")
+
+type transaction struct {
+       tx    *sql.Tx
+       err   error
+       getdb func(context.Context) (*sql.DB, error)
+       setup sync.Once
+}
+
+type transactionFinishFunc func(*error)
+
+// starttx returns a new child context that can be used with
+// currenttx(). It does not open a database transaction until the
+// first call to currenttx().
+//
+// The caller must eventually call the returned finishtx() func to
+// commit or rollback the transaction, if any.
+//
+//     func example(ctx context.Context) (err error) {
+//             ctx, finishtx := starttx(ctx, dber)
+//             defer finishtx(&err)
+//             // ...
+//             tx, err := currenttx(ctx)
+//             if err != nil {
+//                     return fmt.Errorf("example: %s", err)
+//             }
+//             return tx.ExecContext(...)
+//     }
+//
+// If *err is nil, finishtx() commits the transaction and assigns any
+// resulting error to *err.
+//
+// If *err is non-nil, finishtx() rolls back the transaction, and
+// does not modify *err.
+func starttx(ctx context.Context, getdb func(context.Context) (*sql.DB, error)) (context.Context, transactionFinishFunc) {
+       txn := &transaction{getdb: getdb}
+       return context.WithValue(ctx, contextKeyTransaction, txn), func(err *error) {
+               // Ensure another goroutine can't open a transaction
+               // during/after finishtx().
+               txn.setup.Do(func() {})
+               if txn.tx == nil {
+                       // we never [successfully] started a transaction
+                       return
+               }
+               if *err != nil {
+                       ctxlog.FromContext(ctx).Debug("rollback")
+                       txn.tx.Rollback()
+                       return
+               }
+               *err = txn.tx.Commit()
+       }
+}
+
+func currenttx(ctx context.Context) (*sql.Tx, error) {
+       txn, ok := ctx.Value(contextKeyTransaction).(*transaction)
+       if !ok {
+               return nil, errors.New("bug: there is no transaction in this context")
+       }
+       txn.setup.Do(func() {
+               if db, err := txn.getdb(ctx); err != nil {
+                       txn.err = err
+               } else {
+                       txn.tx, txn.err = db.Begin()
+               }
+       })
+       return txn.tx, txn.err
+}
diff --git a/lib/controller/localdb/db_test.go b/lib/controller/localdb/db_test.go
new file mode 100644 (file)
index 0000000..5187dfe
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+       "database/sql"
+
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       _ "github.com/lib/pq"
+       check "gopkg.in/check.v1"
+)
+
+// testdb returns a DB connection for the given cluster config.
+func testdb(c *check.C, cluster *arvados.Cluster) *sql.DB {
+       db, err := sql.Open("postgres", cluster.PostgreSQL.Connection.String())
+       c.Assert(err, check.IsNil)
+       return db
+}
+
+// testctx returns a context suitable for running a test case in a new
+// transaction, and a rollback func which the caller should call after
+// the test.
+func testctx(c *check.C, db *sql.DB) (ctx context.Context, rollback func()) {
+       tx, err := db.Begin()
+       c.Assert(err, check.IsNil)
+       return Transaction(context.Background(), tx), func() {
+               c.Check(tx.Rollback(), check.IsNil)
+       }
+}
diff --git a/lib/controller/localdb/docker_test.go b/lib/controller/localdb/docker_test.go
new file mode 100644 (file)
index 0000000..90c98b7
--- /dev/null
@@ -0,0 +1,68 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "io"
+       "net"
+       "strings"
+
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       check "gopkg.in/check.v1"
+)
+
+type pgproxy struct {
+       net.Listener
+}
+
+// newPgProxy sets up a TCP proxy, listening on all interfaces, that
+// forwards all connections to the cluster's PostgreSQL server. This
+// allows the caller to run a docker container that can connect to a
+// postgresql instance that listens on the test host's loopback
+// interface.
+//
+// Caller is responsible for calling Close() on the returned pgproxy.
+func newPgProxy(c *check.C, cluster *arvados.Cluster) *pgproxy {
+       host := cluster.PostgreSQL.Connection["host"]
+       if host == "" {
+               host = "localhost"
+       }
+       port := cluster.PostgreSQL.Connection["port"]
+       if port == "" {
+               port = "5432"
+       }
+       target := net.JoinHostPort(host, port)
+
+       ln, err := net.Listen("tcp", ":")
+       c.Assert(err, check.IsNil)
+       go func() {
+               for {
+                       downstream, err := ln.Accept()
+                       if err != nil && strings.Contains(err.Error(), "use of closed network connection") {
+                               return
+                       }
+                       c.Assert(err, check.IsNil)
+                       go func() {
+                               c.Logf("pgproxy accepted connection from %s", downstream.RemoteAddr().String())
+                               defer downstream.Close()
+                               upstream, err := net.Dial("tcp", target)
+                               if err != nil {
+                                       c.Logf("net.Dial(%q): %s", target, err)
+                                       return
+                               }
+                               defer upstream.Close()
+                               go io.Copy(downstream, upstream)
+                               io.Copy(upstream, downstream)
+                       }()
+               }
+       }()
+       c.Logf("pgproxy listening at %s", ln.Addr().String())
+       return &pgproxy{Listener: ln}
+}
+
+func (proxy *pgproxy) Port() string {
+       _, port, _ := net.SplitHostPort(proxy.Addr().String())
+       return port
+}
index 905cfed15c500d95689857e36c1c3323165c4d3d..1cd349a10eaa94d987899ac1315f811ffbf186e1 100644 (file)
@@ -6,9 +6,13 @@ package localdb
 
 import (
        "context"
+       "database/sql"
+       "encoding/json"
        "errors"
+       "fmt"
        "net/http"
        "net/url"
+       "strings"
 
        "git.arvados.org/arvados.git/lib/controller/rpc"
        "git.arvados.org/arvados.git/sdk/go/arvados"
@@ -96,9 +100,9 @@ func noopLogout(cluster *arvados.Cluster, opts arvados.LogoutOptions) (arvados.L
        return arvados.LogoutResponse{RedirectLocation: target}, nil
 }
 
-func createAPIClientAuthorization(ctx context.Context, conn *rpc.Conn, rootToken string, authinfo rpc.UserSessionAuthInfo) (arvados.APIClientAuthorization, error) {
+func createAPIClientAuthorization(ctx context.Context, conn *rpc.Conn, rootToken string, authinfo rpc.UserSessionAuthInfo) (resp arvados.APIClientAuthorization, err error) {
        ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{rootToken}})
-       resp, err := conn.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
+       newsession, err := conn.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
                // Send a fake ReturnTo value instead of the caller's
                // opts.ReturnTo. We won't follow the resulting
                // redirect target anyway.
@@ -106,12 +110,36 @@ func createAPIClientAuthorization(ctx context.Context, conn *rpc.Conn, rootToken
                AuthInfo: authinfo,
        })
        if err != nil {
-               return arvados.APIClientAuthorization{}, err
+               return
        }
-       target, err := url.Parse(resp.RedirectLocation)
+       target, err := url.Parse(newsession.RedirectLocation)
        if err != nil {
-               return arvados.APIClientAuthorization{}, err
+               return
        }
        token := target.Query().Get("api_token")
-       return conn.APIClientAuthorizationCurrent(auth.NewContext(ctx, auth.NewCredentials(token)), arvados.GetOptions{})
+       tx, err := currenttx(ctx)
+       if err != nil {
+               return
+       }
+       tokensecret := token
+       if strings.Contains(token, "/") {
+               tokenparts := strings.Split(token, "/")
+               if len(tokenparts) >= 3 {
+                       tokensecret = tokenparts[2]
+               }
+       }
+       var exp sql.NullString
+       var scopes []byte
+       err = tx.QueryRowContext(ctx, "select uuid, api_token, expires_at, scopes from api_client_authorizations where api_token=$1", tokensecret).Scan(&resp.UUID, &resp.APIToken, &exp, &scopes)
+       if err != nil {
+               return
+       }
+       resp.ExpiresAt = exp.String
+       if len(scopes) > 0 {
+               err = json.Unmarshal(scopes, &resp.Scopes)
+               if err != nil {
+                       return resp, fmt.Errorf("unmarshal scopes: %s", err)
+               }
+       }
+       return
 }
index 79b5f16158ab0c39cba73822cb534bd9303ce24c..3cbf14fe0b0c11694bd95831179f3f0b7f6eb586 100644 (file)
@@ -24,10 +24,13 @@ func (s *LDAPSuite) TestLoginLDAPViaPAM(c *check.C) {
        if !haveDocker() {
                c.Skip("skipping docker test because docker is not available")
        }
+       pgproxy := newPgProxy(c, s.cluster)
+       defer pgproxy.Close()
+
        cmd := exec.Command("bash", "login_ldap_docker_test.sh")
        cmd.Stdout = os.Stderr
        cmd.Stderr = os.Stderr
-       cmd.Env = append(os.Environ(), "config_method=pam")
+       cmd.Env = append(os.Environ(), "config_method=pam", "pgport="+pgproxy.Port())
        err := cmd.Run()
        c.Check(err, check.IsNil)
 }
@@ -39,10 +42,13 @@ func (s *LDAPSuite) TestLoginLDAPBuiltin(c *check.C) {
        if !haveDocker() {
                c.Skip("skipping docker test because docker is not available")
        }
+       pgproxy := newPgProxy(c, s.cluster)
+       defer pgproxy.Close()
+
        cmd := exec.Command("bash", "login_ldap_docker_test.sh")
        cmd.Stdout = os.Stderr
        cmd.Stderr = os.Stderr
-       cmd.Env = append(os.Environ(), "config_method=ldap")
+       cmd.Env = append(os.Environ(), "config_method=ldap", "pgport="+pgproxy.Port())
        err := cmd.Run()
        c.Check(err, check.IsNil)
 }
index 4e0679f620bf6b4ec67d8fc118b66db5ef3332ac..0225f204611d051ce5c17a6f5eb594f5845aaa18 100755 (executable)
@@ -1,5 +1,9 @@
 #!/bin/bash
 
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
 # This script demonstrates using LDAP for Arvados user authentication.
 #
 # It configures arvados controller in a docker container, optionally
@@ -74,6 +78,7 @@ Clusters:
       Connection:
         client_encoding: utf8
         host: ${hostname}
+        port: "${pgport}"
         dbname: arvados_test
         user: arvados
         password: insecure_arvados_test
index 9a8f83f857092951f76d4f97d94f5fdef66626a1..64ae58bce2681f792020b1855c2465d5ad226ae1 100644 (file)
@@ -6,6 +6,7 @@ package localdb
 
 import (
        "context"
+       "database/sql"
        "encoding/json"
        "net"
        "net/http"
@@ -26,6 +27,11 @@ type LDAPSuite struct {
        cluster *arvados.Cluster
        ctrl    *ldapLoginController
        ldap    *godap.LDAPServer // fake ldap server that accepts auth goodusername/goodpassword
+       db      *sql.DB
+
+       // transaction context
+       ctx      context.Context
+       rollback func()
 }
 
 func (s *LDAPSuite) TearDownSuite(c *check.C) {
@@ -85,10 +91,21 @@ func (s *LDAPSuite) SetUpSuite(c *check.C) {
                Cluster:    s.cluster,
                RailsProxy: railsproxy.NewConn(s.cluster),
        }
+       s.db = testdb(c, s.cluster)
+}
+
+func (s *LDAPSuite) SetUpTest(c *check.C) {
+       s.ctx, s.rollback = testctx(c, s.db)
+}
+
+func (s *LDAPSuite) TearDownTest(c *check.C) {
+       s.rollback()
 }
 
 func (s *LDAPSuite) TestLoginSuccess(c *check.C) {
-       resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
+       conn := NewConn(s.cluster)
+       conn.loginController = s.ctrl
+       resp, err := conn.UserAuthenticate(s.ctx, arvados.UserAuthenticateOptions{
                Username: "goodusername",
                Password: "goodpassword",
        })
@@ -97,7 +114,7 @@ func (s *LDAPSuite) TestLoginSuccess(c *check.C) {
        c.Check(resp.UUID, check.Matches, `zzzzz-gj3su-.*`)
        c.Check(resp.Scopes, check.DeepEquals, []string{"all"})
 
-       ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{"v2/" + resp.UUID + "/" + resp.APIToken}})
+       ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: []string{"v2/" + resp.UUID + "/" + resp.APIToken}})
        user, err := railsproxy.NewConn(s.cluster).UserGetCurrent(ctx, arvados.GetOptions{})
        c.Check(err, check.IsNil)
        c.Check(user.Email, check.Equals, "goodusername@example.com")
@@ -107,7 +124,7 @@ func (s *LDAPSuite) TestLoginSuccess(c *check.C) {
 func (s *LDAPSuite) TestLoginFailure(c *check.C) {
        // search returns no results
        s.cluster.Login.LDAP.SearchBase = "dc=example,dc=invalid"
-       resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
+       resp, err := s.ctrl.UserAuthenticate(s.ctx, arvados.UserAuthenticateOptions{
                Username: "goodusername",
                Password: "goodpassword",
        })
@@ -120,7 +137,7 @@ func (s *LDAPSuite) TestLoginFailure(c *check.C) {
 
        // search returns result, but auth fails
        s.cluster.Login.LDAP.SearchBase = "dc=example,dc=com"
-       resp, err = s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
+       resp, err = s.ctrl.UserAuthenticate(s.ctx, arvados.UserAuthenticateOptions{
                Username: "badusername",
                Password: "badpassword",
        })
index 1345e86900dd1056da5a9259bf0d5caf179253e5..2ccb1fce2a1e9dc42592f0543b2b0fc03f7d6fea 100644 (file)
@@ -39,7 +39,6 @@ var _ = check.Suite(&OIDCLoginSuite{})
 
 type OIDCLoginSuite struct {
        cluster               *arvados.Cluster
-       ctx                   context.Context
        localdb               *Conn
        railsSpy              *arvadostest.Proxy
        fakeIssuer            *httptest.Server
index c347e2f795517f74c9f67ec0311ba41d3250dafb..29c81ac5cae9ac63431e691852230a00c2335afe 100644 (file)
@@ -19,144 +19,154 @@ import (
 )
 
 type router struct {
-       mux *mux.Router
-       fed arvados.API
+       mux       *mux.Router
+       backend   arvados.API
+       wrapCalls func(RoutableFunc) RoutableFunc
 }
 
-func New(fed arvados.API) *router {
+// 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 {
        rtr := &router{
-               mux: mux.NewRouter(),
-               fed: fed,
+               mux:       mux.NewRouter(),
+               backend:   backend,
+               wrapCalls: wrapCalls,
        }
        rtr.addRoutes()
        return rtr
 }
 
-type routableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
+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        RoutableFunc
        }{
                {
                        arvados.EndpointConfigGet,
                        func() interface{} { return &struct{}{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.ConfigGet(ctx)
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.Logout(ctx, *opts.(*arvados.LogoutOptions))
                        },
                },
                {
                        arvados.EndpointCollectionCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
+                               return rtr.backend.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
                        },
                },
                {
                        arvados.EndpointCollectionUpdate,
                        func() interface{} { return &arvados.UpdateOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                               return rtr.backend.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
                        },
                },
                {
                        arvados.EndpointCollectionGet,
                        func() interface{} { return &arvados.GetOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.CollectionGet(ctx, *opts.(*arvados.GetOptions))
+                               return rtr.backend.CollectionGet(ctx, *opts.(*arvados.GetOptions))
                        },
                },
                {
                        arvados.EndpointCollectionList,
                        func() interface{} { return &arvados.ListOptions{Limit: -1} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.CollectionList(ctx, *opts.(*arvados.ListOptions))
+                               return rtr.backend.CollectionList(ctx, *opts.(*arvados.ListOptions))
                        },
                },
                {
                        arvados.EndpointCollectionProvenance,
                        func() interface{} { return &arvados.GetOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
+                               return rtr.backend.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
                        },
                },
                {
                        arvados.EndpointCollectionUsedBy,
                        func() interface{} { return &arvados.GetOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
+                               return rtr.backend.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
                        },
                },
                {
                        arvados.EndpointCollectionDelete,
                        func() interface{} { return &arvados.DeleteOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
                        },
                },
                {
                        arvados.EndpointContainerCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
+                               return rtr.backend.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
                        },
                },
                {
                        arvados.EndpointContainerUpdate,
                        func() interface{} { return &arvados.UpdateOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                               return rtr.backend.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
                        },
                },
                {
                        arvados.EndpointContainerGet,
                        func() interface{} { return &arvados.GetOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.ContainerGet(ctx, *opts.(*arvados.GetOptions))
+                               return rtr.backend.ContainerGet(ctx, *opts.(*arvados.GetOptions))
                        },
                },
                {
                        arvados.EndpointContainerList,
                        func() interface{} { return &arvados.ListOptions{Limit: -1} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.ContainerList(ctx, *opts.(*arvados.ListOptions))
+                               return rtr.backend.ContainerList(ctx, *opts.(*arvados.ListOptions))
                        },
                },
                {
                        arvados.EndpointContainerDelete,
                        func() interface{} { return &arvados.DeleteOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
+                               return rtr.backend.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
                        },
                },
                {
@@ -165,7 +175,7 @@ func (rtr *router) addRoutes() {
                                return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
                        },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
+                               return rtr.backend.ContainerLock(ctx, *opts.(*arvados.GetOptions))
                        },
                },
                {
@@ -174,144 +184,148 @@ func (rtr *router) addRoutes() {
                                return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
                        },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
+                               return rtr.backend.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
                        },
                },
                {
                        arvados.EndpointSpecimenCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
+                               return rtr.backend.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
                        },
                },
                {
                        arvados.EndpointSpecimenUpdate,
                        func() interface{} { return &arvados.UpdateOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                               return rtr.backend.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
                        },
                },
                {
                        arvados.EndpointSpecimenGet,
                        func() interface{} { return &arvados.GetOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
+                               return rtr.backend.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
                        },
                },
                {
                        arvados.EndpointSpecimenList,
                        func() interface{} { return &arvados.ListOptions{Limit: -1} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.SpecimenList(ctx, *opts.(*arvados.ListOptions))
+                               return rtr.backend.SpecimenList(ctx, *opts.(*arvados.ListOptions))
                        },
                },
                {
                        arvados.EndpointSpecimenDelete,
                        func() interface{} { return &arvados.DeleteOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               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.fed.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.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))
+                               return rtr.backend.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
                        },
                },
                {
                        arvados.EndpointUserAuthenticate,
                        func() interface{} { return &arvados.UserAuthenticateOptions{} },
                        func(ctx context.Context, opts interface{}) (interface{}, error) {
-                               return rtr.fed.UserAuthenticate(ctx, *opts.(*arvados.UserAuthenticateOptions))
+                               return rtr.backend.UserAuthenticate(ctx, *opts.(*arvados.UserAuthenticateOptions))
                        },
                },
        } {
-               rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
+               exec := route.exec
+               if rtr.wrapCalls != nil {
+                       exec = rtr.wrapCalls(exec)
+               }
+               rtr.addRoute(route.endpoint, route.defaultOpts, exec)
        }
        rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
                httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
@@ -326,7 +340,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 RoutableFunc) {
        methods := []string{endpoint.Method}
        if alt, ok := altMethod[endpoint.Method]; ok {
                methods = append(methods, alt)
index 4cabe70f162a6f36360da58f7c820e1712e0728f..c73bc64915f12aff293f23c803e25771669fe8a9 100644 (file)
@@ -38,8 +38,8 @@ type RouterSuite struct {
 func (s *RouterSuite) SetUpTest(c *check.C) {
        s.stub = arvadostest.APIStub{}
        s.rtr = &router{
-               mux: mux.NewRouter(),
-               fed: &s.stub,
+               mux:     mux.NewRouter(),
+               backend: &s.stub,
        }
        s.rtr.addRoutes()
 }
@@ -169,7 +169,7 @@ func (s *RouterIntegrationSuite) SetUpTest(c *check.C) {
        cluster.TLS.Insecure = true
        arvadostest.SetServiceURL(&cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
        url, _ := url.Parse("https://" + os.Getenv("ARVADOS_TEST_API_HOST"))
-       s.rtr = New(rpc.NewConn("zzzzz", url, true, rpc.PassthroughTokenProvider))
+       s.rtr = New(rpc.NewConn("zzzzz", url, true, rpc.PassthroughTokenProvider), nil)
 }
 
 func (s *RouterIntegrationSuite) TearDownSuite(c *check.C) {