From: Tom Clegg Date: Fri, 26 Jun 2020 21:15:30 +0000 (-0400) Subject: 16534: Add database access in lib/controller/localdb. X-Git-Tag: 2.1.0~170^2~2 X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/2c5417221843491727e4e5505012fc115e3bc7b0 16534: Add database access in lib/controller/localdb. Arvados-DCO-1.1-Signed-off-by: Tom Clegg --- diff --git a/lib/controller/federation.go b/lib/controller/federation.go index ac239fb9b2..aceaba8087 100644 --- a/lib/controller/federation.go +++ b/lib/controller/federation.go @@ -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 } diff --git a/lib/controller/federation/federation_test.go b/lib/controller/federation/federation_test.go index f57d827848..256afc8e6b 100644 --- a/lib/controller/federation/federation_test.go +++ b/lib/controller/federation/federation_test.go @@ -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", diff --git a/lib/controller/handler.go b/lib/controller/handler.go index 01f2161632..cc06246420 100644 --- a/lib/controller/handler.go +++ b/lib/controller/handler.go @@ -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 index 0000000000..f9c5d19e28 --- /dev/null +++ b/lib/controller/localdb/db.go @@ -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 index 0000000000..5187dfecdd --- /dev/null +++ b/lib/controller/localdb/db_test.go @@ -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 index 0000000000..90c98b7d57 --- /dev/null +++ b/lib/controller/localdb/docker_test.go @@ -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 +} diff --git a/lib/controller/localdb/login.go b/lib/controller/localdb/login.go index 905cfed15c..1cd349a10e 100644 --- a/lib/controller/localdb/login.go +++ b/lib/controller/localdb/login.go @@ -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 } diff --git a/lib/controller/localdb/login_ldap_docker_test.go b/lib/controller/localdb/login_ldap_docker_test.go index 79b5f16158..3cbf14fe0b 100644 --- a/lib/controller/localdb/login_ldap_docker_test.go +++ b/lib/controller/localdb/login_ldap_docker_test.go @@ -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) } diff --git a/lib/controller/localdb/login_ldap_docker_test.sh b/lib/controller/localdb/login_ldap_docker_test.sh index 4e0679f620..0225f20461 100755 --- a/lib/controller/localdb/login_ldap_docker_test.sh +++ b/lib/controller/localdb/login_ldap_docker_test.sh @@ -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 diff --git a/lib/controller/localdb/login_ldap_test.go b/lib/controller/localdb/login_ldap_test.go index 9a8f83f857..64ae58bce2 100644 --- a/lib/controller/localdb/login_ldap_test.go +++ b/lib/controller/localdb/login_ldap_test.go @@ -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", }) diff --git a/lib/controller/localdb/login_oidc_test.go b/lib/controller/localdb/login_oidc_test.go index 1345e86900..2ccb1fce2a 100644 --- a/lib/controller/localdb/login_oidc_test.go +++ b/lib/controller/localdb/login_oidc_test.go @@ -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 diff --git a/lib/controller/router/router.go b/lib/controller/router/router.go index c347e2f795..29c81ac5ca 100644 --- a/lib/controller/router/router.go +++ b/lib/controller/router/router.go @@ -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) diff --git a/lib/controller/router/router_test.go b/lib/controller/router/router_test.go index 4cabe70f16..c73bc64915 100644 --- a/lib/controller/router/router_test.go +++ b/lib/controller/router/router_test.go @@ -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) {